Support HD wallets, only using account 0, as well as raw private keys

* Store seed and raw private keys using symmetric encryption based on Diffie-Hellman with ECDSA keys stored in Secure Enclave, instead of storing raw private keys using storing AES-encrypted keystore files on disk and the AES key into keychain
* Set User-presence required flag when writing seed and private keys to keychain so Touch ID/Face ID is required when reading them from the keychain
* Back up alerts to prompt user to back up wallet to go to "yellow bar" status
* Allow user to elevate security making wallets go to "green bar"
* Visually guide user to where wallet address can be found after wallet has been created
* Change wording of delete wallet action to "Lose this wallet" to make the effect obvious to the user
* Inform user that it's a bad idea to take a screenshot of their seed phrase during back up/verify after they have done it. Can't prevent it
pull/1272/head
Hwee-Boon Yar 5 years ago
parent 7ab60defd0
commit 1e6a51b7fe
  1. 216
      AlphaWallet.xcodeproj/project.pbxproj
  2. 61
      AlphaWallet/Accounts/Coordinators/AccountsCoordinator.swift
  3. 122
      AlphaWallet/Accounts/ViewModels/AccountsViewController.swift
  4. 11
      AlphaWallet/Accounts/ViewModels/AccountsViewModel.swift
  5. 79
      AlphaWallet/Accounts/Views/AccountViewTableSectionHeader.swift
  6. 23
      AlphaWallet/AppCoordinator.swift
  7. 21
      AlphaWallet/Assets.xcassets/biometric-lock.imageset/Contents.json
  8. BIN
      AlphaWallet/Assets.xcassets/biometric-lock.imageset/biometric-lock@3x.png
  9. 21
      AlphaWallet/Assets.xcassets/keystore-introduction.imageset/Contents.json
  10. BIN
      AlphaWallet/Assets.xcassets/keystore-introduction.imageset/keystore-introduction@3x.png
  11. 12
      AlphaWallet/Assets.xcassets/successOverlay.imageset/Contents.json
  12. BIN
      AlphaWallet/Assets.xcassets/successOverlay.imageset/successOverlay.pdf
  13. 12
      AlphaWallet/Assets.xcassets/toggle-password.imageset/Contents.json
  14. BIN
      AlphaWallet/Assets.xcassets/toggle-password.imageset/toggle-password.pdf
  15. 5
      AlphaWallet/Browser/Coordinators/DappBrowserCoordinator.swift
  16. 16
      AlphaWallet/Core/Types/AlphaWalletAddress.swift
  17. 447
      AlphaWallet/EtherClient/EtherKeystore.swift
  18. 2
      AlphaWallet/EtherClient/ImportType.swift
  19. 12
      AlphaWallet/EtherClient/KeyStoreError.swift
  20. 33
      AlphaWallet/EtherClient/Keystore.swift
  21. 5
      AlphaWallet/EtherClient/Wallet.swift
  22. 153
      AlphaWallet/Export/Coordinators/BackupCoordinator.swift
  23. 383
      AlphaWallet/Export/Coordinators/PromptBackupCoordinator.swift
  24. 79
      AlphaWallet/Export/Models/BackupState.swift
  25. 86
      AlphaWallet/Export/ViewControllers/BackUpViewController.swift
  26. 26
      AlphaWallet/Export/ViewModels/PromptBackupWalletAfterExceedingThresholdViewViewModel.swift
  27. 24
      AlphaWallet/Export/ViewModels/PromptBackupWalletAfterIntervalViewViewModel.swift
  28. 28
      AlphaWallet/Export/ViewModels/PromptBackupWalletAfterReceivingEtherViewViewModel.swift
  29. 24
      AlphaWallet/Export/ViewModels/PromptBackupWalletAfterWalletCreationViewViewModel.swift
  30. 75
      AlphaWallet/Export/ViewModels/PromptBackupWalletViewViewModel.swift
  31. 106
      AlphaWallet/Export/Views/PromptBackupWalletView.swift
  32. 1
      AlphaWallet/Extensions/Data.swift
  33. 8
      AlphaWallet/Foundation/CoordinatorThatEnds.swift
  34. 49
      AlphaWallet/InCoordinator.swift
  35. 813
      AlphaWallet/KeyManagement/EtherKeystore.swift
  36. 26
      AlphaWallet/KeyManagement/EthereumAccount.swift
  37. 14
      AlphaWallet/KeyManagement/EthereumSigner.swift
  38. 47
      AlphaWallet/KeyManagement/Keystore.swift
  39. 120
      AlphaWallet/KeyManagement/LegacyFileBasedKeystore.swift
  40. 180
      AlphaWallet/KeyManagement/SecureEnclave.swift
  41. 72
      AlphaWallet/Localization/en.lproj/Localizable.strings
  42. 72
      AlphaWallet/Localization/es.lproj/Localizable.strings
  43. 72
      AlphaWallet/Localization/ja.lproj/Localizable.strings
  44. 72
      AlphaWallet/Localization/ko.lproj/Localizable.strings
  45. 72
      AlphaWallet/Localization/zh-Hans.lproj/Localizable.strings
  46. 23
      AlphaWallet/Market/Coordinators/UniversalLinkCoordinator.swift
  47. 4
      AlphaWallet/Market/OrderHandler.swift
  48. 3
      AlphaWallet/Models/UnsignedTransaction.swift
  49. 5
      AlphaWallet/Redeem/Helpers/SignatureHelper.swift
  50. 19
      AlphaWallet/Settings/Coordinators/SettingsCoordinator.swift
  51. 14
      AlphaWallet/Settings/Types/Config.swift
  52. 1
      AlphaWallet/Settings/ViewControllers/EnabledServersViewController.swift
  53. 40
      AlphaWallet/Settings/ViewControllers/SettingsViewController.swift
  54. 1
      AlphaWallet/Tokens/Coordinators/SingleChainTokenCoordinator.swift
  55. 16
      AlphaWallet/Tokens/Coordinators/TokensCoordinator.swift
  56. 2
      AlphaWallet/Tokens/ViewControllers/TokenInstanceActionViewController.swift
  57. 1
      AlphaWallet/Tokens/ViewControllers/TokensCardViewController.swift
  58. 29
      AlphaWallet/Tokens/ViewControllers/TokensViewController.swift
  59. 31
      AlphaWallet/Tokens/ViewModels/TokensViewModel.swift
  60. 25
      AlphaWallet/Tokens/Views/TextField.swift
  61. 5
      AlphaWallet/Tokens/Views/TokenInstanceWebView.swift
  62. 15
      AlphaWallet/Transactions/Coordinators/SingleChainTransactionDataCoordinator.swift
  63. 1
      AlphaWallet/Transactions/Coordinators/TokensCardCoordinator.swift
  64. 8
      AlphaWallet/Transactions/Coordinators/TransactionCoordinator.swift
  65. 7
      AlphaWallet/Transactions/Coordinators/TransactionDataCoordinator.swift
  66. 1
      AlphaWallet/Transactions/Storage/TransactionsStorage.swift
  67. 6
      AlphaWallet/Transfer/Controllers/TransactionConfigurator.swift
  68. 5
      AlphaWallet/Transfer/Coordinators/ConfirmCoordinator.swift
  69. 1
      AlphaWallet/Transfer/Coordinators/PaymentCoordinator.swift
  70. 5
      AlphaWallet/Transfer/Coordinators/SendCoordinator.swift
  71. 6
      AlphaWallet/Transfer/Coordinators/SignMessageCoordinator.swift
  72. 5
      AlphaWallet/Transfer/Coordinators/TransferNFTCoordinator.swift
  73. 5
      AlphaWallet/Transfer/ViewControllers/SendViewController.swift
  74. 8
      AlphaWallet/UI/AddressTextField.swift
  75. 42
      AlphaWallet/UI/ButtonsBar.swift
  76. 39
      AlphaWallet/UI/Form/HDWallet+Extension.swift
  77. 23
      AlphaWallet/UI/Form/MnemonicRule.swift
  78. 6
      AlphaWallet/UI/Form/PrivateKeyRule.swift
  79. 60
      AlphaWallet/UI/SuccessOverlayView.swift
  80. 14
      AlphaWallet/UI/Views/TextView.swift
  81. 2
      AlphaWallet/UI/Views/TokenCardRowView.swift
  82. 149
      AlphaWallet/UI/WhereIsWalletAddressFoundOverlayView.swift
  83. 54
      AlphaWallet/Wallet/Coordinators/BackupSeedPhraseCoordinator.swift
  84. 65
      AlphaWallet/Wallet/Coordinators/ElevateWalletSecurityCoordinator.swift
  85. 63
      AlphaWallet/Wallet/Coordinators/EnterPasswordCoordinator.swift
  86. 18
      AlphaWallet/Wallet/Coordinators/InitialWalletCreationCoordinator.swift
  87. 47
      AlphaWallet/Wallet/Coordinators/VerifySeedPhraseCoordinator.swift
  88. 103
      AlphaWallet/Wallet/Coordinators/WalletCoordinator.swift
  89. 6
      AlphaWallet/Wallet/Types/ImportSelectionType.swift
  90. 1
      AlphaWallet/Wallet/Types/ImportWalletTab.swift
  91. 3
      AlphaWallet/Wallet/Types/WalletEntryPoint.swift
  92. 139
      AlphaWallet/Wallet/ViewControllers/CreateInitialWalletViewController.swift
  93. 130
      AlphaWallet/Wallet/ViewControllers/ElevateWalletSecurityViewController.swift
  94. 45
      AlphaWallet/Wallet/ViewControllers/EnterPasswordCoordinator.swift
  95. 70
      AlphaWallet/Wallet/ViewControllers/EnterPasswordViewController.swift
  96. 201
      AlphaWallet/Wallet/ViewControllers/ImportWalletViewController.swift
  97. 106
      AlphaWallet/Wallet/ViewControllers/KeystoreBackupIntroductionViewController.swift
  98. 226
      AlphaWallet/Wallet/ViewControllers/ShowSeedPhraseViewController.swift
  99. 314
      AlphaWallet/Wallet/ViewControllers/VerifySeedPhraseViewController.swift
  100. 58
      AlphaWallet/Wallet/ViewModels/CreateInitialWalletViewModel.swift
  101. Some files were not shown because too many files have changed in this diff Show More

@ -32,7 +32,6 @@
291F52A71F6B766100B369AB /* BalanceRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 291F52A61F6B766100B369AB /* BalanceRequest.swift */; };
291F52A91F6B7BE100B369AB /* BlockNumber.swift in Sources */ = {isa = PBXBuildFile; fileRef = 291F52A81F6B7BE100B369AB /* BlockNumber.swift */; };
291F52B71F6B870400B369AB /* CastError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 291F52B61F6B870400B369AB /* CastError.swift */; };
291F52B91F6B880F00B369AB /* EtherKeystore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 291F52B81F6B880F00B369AB /* EtherKeystore.swift */; };
291F52BF1F6C874E00B369AB /* AccountsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 291F52BE1F6C874E00B369AB /* AccountsViewController.swift */; };
291F52C11F6C8A1F00B369AB /* AccountsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 291F52C01F6C8A1F00B369AB /* AccountsViewModel.swift */; };
2923D9B11FDA49D8000CF3F8 /* Initializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2923D9B01FDA49D8000CF3F8 /* Initializer.swift */; };
@ -152,7 +151,6 @@
29CAEB8E1F70A2FB00F7357D /* TransactionCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29CAEB8D1F70A2FB00F7357D /* TransactionCellViewModel.swift */; };
29D03F1D1F712183006E548C /* Button.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29D03F1C1F712183006E548C /* Button.swift */; };
29D72A2A1F6A8D1500CE9209 /* AppCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29D72A291F6A8D1500CE9209 /* AppCoordinator.swift */; };
29DBF2A11F9DA6EF00327C60 /* BackupViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29DBF2A01F9DA6EF00327C60 /* BackupViewController.swift */; };
29DBF2A31F9DBFF400327C60 /* BackupCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29DBF2A21F9DBFF400327C60 /* BackupCoordinator.swift */; };
29DBF2A51F9EB75E00327C60 /* BackupCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29DBF2A41F9EB75E00327C60 /* BackupCoordinatorTests.swift */; };
29DBF2A71F9F145900327C60 /* StateViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29DBF2A61F9F145900327C60 /* StateViewModel.swift */; };
@ -200,7 +198,6 @@
29FF12F61F74799D00AFD326 /* NSAttributedString.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29FF12F51F74799D00AFD326 /* NSAttributedString.swift */; };
29FF12F81F747D6C00AFD326 /* Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29FF12F71F747D6C00AFD326 /* Error.swift */; };
29FF12FB1F74CC8200AFD326 /* EthereumAddressRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29FF12FA1F74CC8200AFD326 /* EthereumAddressRule.swift */; };
29FF12FE1F75EA3F00AFD326 /* Keystore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29FF12FD1F75EA3F00AFD326 /* Keystore.swift */; };
29FF13011F75EAD900AFD326 /* FakeKeystore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29FF13001F75EAD900AFD326 /* FakeKeystore.swift */; };
29FF13031F75EB7500AFD326 /* Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29FF13021F75EB7500AFD326 /* Account.swift */; };
29FF13081F75F0AE00AFD326 /* AppCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29FF13071F75F0AE00AFD326 /* AppCoordinatorTests.swift */; };
@ -233,6 +230,7 @@
5E7C702C4B29AF2B8D61CCA4 /* DappsAutoCompletionViewControllerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7D19E3CF96929FB8CEA3 /* DappsAutoCompletionViewControllerViewModel.swift */; };
5E7C70397E7E3A9C88E995B1 /* WKWebViewExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7BEDC786FB048A1DD9A8 /* WKWebViewExtension.swift */; };
5E7C705166218753CAA19A6D /* TokenIdOrigin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7587F30FF1896039B8C8 /* TokenIdOrigin.swift */; };
5E7C7067E8FEA5055BF83553 /* HDWallet+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7C6759CA1C223DABA462 /* HDWallet+Extension.swift */; };
5E7C706F4BDF994D539F8040 /* AssetDefinitionsOverridesViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7E54BE622E49554FE4A9 /* AssetDefinitionsOverridesViewCell.swift */; };
5E7C70724527639BE4069AB1 /* AssetAttributeToJavaScriptConvertor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7ACC1A14248A68E95F33 /* AssetAttributeToJavaScriptConvertor.swift */; };
5E7C70794E07FF6B26AE297B /* DirectoryContentsWatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7ED522A9E75F10594EFB /* DirectoryContentsWatcher.swift */; };
@ -240,18 +238,23 @@
5E7C708D05DBC08C8304F274 /* GetIsERC875Encode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7DCB0BDDD30D10130AE7 /* GetIsERC875Encode.swift */; };
5E7C70AE62DBB193399C7F5E /* ServerViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7CDB0BAD5D27D2F24F57 /* ServerViewCell.swift */; };
5E7C70BE9AE35408038E1971 /* HelpContentsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7B089FD4C96810DD10FD /* HelpContentsViewController.swift */; };
5E7C70C8C9FB794488211122 /* CoordinatorThatEnds.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C762E8065342D6B80BE56 /* CoordinatorThatEnds.swift */; };
5E7C70CF1C732CE07D074A8B /* BookmarksStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7AB4464F82391AAD68C1 /* BookmarksStore.swift */; };
5E7C70DDE7C3BD32FA525753 /* PromptBackupWalletAfterIntervalViewViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7F1B07B1403E6382B21F /* PromptBackupWalletAfterIntervalViewViewModel.swift */; };
5E7C70E4C7053E9794A8FE30 /* CallForAssetAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7251A04A7D77CE07C94D /* CallForAssetAttribute.swift */; };
5E7C70E4DF263B30CDF4BDB2 /* FetchAssetDefinitionsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C72445F818B38DAEA783A /* FetchAssetDefinitionsCoordinator.swift */; };
5E7C70E4E194FEA5DA2F610C /* OpenSeaNonFungible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7382EAC8B9CE5EE0668D /* OpenSeaNonFungible.swift */; };
5E7C70F407FBA1BD69901861 /* SecureEnclave.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7778166E01A0D483C58D /* SecureEnclave.swift */; };
5E7C70F550D982833859D8B4 /* MigrationInitializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C720596AD351B21CE7583 /* MigrationInitializer.swift */; };
5E7C710331196CD591B51785 /* LockCreatePasscodeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C741196D9D9C9C3EE5E30 /* LockCreatePasscodeViewController.swift */; };
5E7C7110A4DF17DA65B912AC /* EnterSellTokensCardPriceQuantityViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7ABB1538A0E83EEAEB0C /* EnterSellTokensCardPriceQuantityViewControllerTests.swift */; };
5E7C71145CE952518B3EECE3 /* AccountViewTableSectionHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7E7720DE64069CCF37D5 /* AccountViewTableSectionHeader.swift */; };
5E7C7131E338A806132D989B /* DateEntryField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7CD1FB7D353704EF3389 /* DateEntryField.swift */; };
5E7C713ACE8C72642B1C9F93 /* SendHeaderViewViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7B7A45EDFA8ED1E25863 /* SendHeaderViewViewModel.swift */; };
5E7C71570B651B3B56CAA1CC /* StringExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C76D132F4BEA5CE4FFD0A /* StringExtensionTests.swift */; };
5E7C716AB8079EA1283B2317 /* Bookmark.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7ED293E477AD7C13056C /* Bookmark.swift */; };
5E7C716F9A06085DF5F903E8 /* UserEntryOrigin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C72655E6DC767D497D580 /* UserEntryOrigin.swift */; };
5E7C71769464FDBDE6803A91 /* PromptBackupCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C70B3651BDFE549C28466 /* PromptBackupCoordinator.swift */; };
5E7C718043636901114BF76C /* LocalesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7FB99843529061368DA1 /* LocalesViewModel.swift */; };
5E7C7186B20660F2C1462AA9 /* DiscoverDappsViewControllerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7F840AFFD4459FD3DBD6 /* DiscoverDappsViewControllerViewModel.swift */; };
5E7C71A2EAA5124E07AA54B6 /* Favicon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7BAC4E511FE8446D212F /* Favicon.swift */; };
@ -267,6 +270,7 @@
5E7C71DC13B2040F5408BF3C /* ImportMagicTokenCardRowViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C781F82F9E4903C460E33 /* ImportMagicTokenCardRowViewModel.swift */; };
5E7C71F8050CCF990539B293 /* LockView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C79D674D45A07E694CE31 /* LockView.swift */; };
5E7C720C2A5CA7D41B12D666 /* RateLimiter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7A92352937F37294F12C /* RateLimiter.swift */; };
5E7C72311A412C022AA20F51 /* PromptBackupWalletView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C70165BA8A9F342DC7874 /* PromptBackupWalletView.swift */; };
5E7C72402E57B627B6E56934 /* TokenInstanceViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C75506A766DF9B746E62F /* TokenInstanceViewModel.swift */; };
5E7C724638271FD2FA0EB93C /* BaseTokenListFormatTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C734D61C0347C1638A1F7 /* BaseTokenListFormatTableViewCell.swift */; };
5E7C725AC96979DEF4DE8B85 /* ConvertSVGToPNG.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7BCCCFE7B99162518FB7 /* ConvertSVGToPNG.swift */; };
@ -297,9 +301,11 @@
5E7C7350C5F9ADE212A0F1CA /* CallForAssetAttributeCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C74F4900AB6D34CDD3674 /* CallForAssetAttributeCoordinator.swift */; };
5E7C7364BF4F6EC3F15804C2 /* GetIsERC721Encode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7CA7D65743AEE3411F3A /* GetIsERC721Encode.swift */; };
5E7C7376B566E5A59CC8F463 /* ImportMagicTokenViewControllerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C72D0E7CA03ADE5CFAE7A /* ImportMagicTokenViewControllerViewModel.swift */; };
5E7C738BCA59B1DE116ECC96 /* WhereIsWalletAddressFoundOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C76052512831B707659CA /* WhereIsWalletAddressFoundOverlayView.swift */; };
5E7C739447F6BADDBBBF7278 /* AssetDefinitionsOverridesViewCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7695F7C45A31C7EAF97F /* AssetDefinitionsOverridesViewCellViewModel.swift */; };
5E7C73CE76679D1E1D6714F7 /* SolidityType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7C43F1B371836552CC18 /* SolidityType.swift */; };
5E7C73E7A68C56162FA2E845 /* SingleChainTransactionDataCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C74259A3F3B0277B0E8C5 /* SingleChainTransactionDataCoordinator.swift */; };
5E7C73F6762376F5B2B4214D /* EtherKeystore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7BA8A301CEEDE36D76A3 /* EtherKeystore.swift */; };
5E7C73FC3990D110C474C3D6 /* WalletFilterViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C75CC640BAFFE0E789F44 /* WalletFilterViewModel.swift */; };
5E7C73FD5BD75D90C8D0EF3C /* WalletFilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7C58586099F082973073 /* WalletFilterView.swift */; };
5E7C7402B29A987B0AF7061D /* VerifiableStatusViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7CC48CA7A1EA7D539C87 /* VerifiableStatusViewController.swift */; };
@ -308,6 +314,7 @@
5E7C741CA88EFAC66756DE7F /* EditMyDappViewControllerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C776B129861728FFB8CC8 /* EditMyDappViewControllerViewModel.swift */; };
5E7C742B144FBC72E082B8F3 /* TokenScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C74DCDDEC6E0C17A157D4 /* TokenScript.swift */; };
5E7C742DDE5C63804A2C420D /* Collection+UIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7914F01B5FEA5B20398B /* Collection+UIView.swift */; };
5E7C743467F5D428AF4E4F0F /* ElevateWalletSecurityViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C77E2559C7C9117C0F75F /* ElevateWalletSecurityViewModel.swift */; };
5E7C74438E1FBF28ADFAFAD1 /* BaseTokenCardTableViewCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7AD33AC8BE19F5C66489 /* BaseTokenCardTableViewCellViewModel.swift */; };
5E7C744B0282FD7B3A29AA7B /* ScriptMessageProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C708DE897B6677EAD769B /* ScriptMessageProxy.swift */; };
5E7C745A423BD10CFDED9A81 /* ServersViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7CBEBF984CFCA29D6866 /* ServersViewModel.swift */; };
@ -320,6 +327,7 @@
5E7C7499A8D6814F7950DA70 /* LockCreatePasscodeCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7AB3440C01136DF4F3E9 /* LockCreatePasscodeCoordinator.swift */; };
5E7C749B7C5CBC729B7E256F /* OpenSeaNonFungibleTokenTraitCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C765A9FA64E4CC1B6C726 /* OpenSeaNonFungibleTokenTraitCell.swift */; };
5E7C74A655E5D34953FFEBF2 /* AlphaWalletAddressTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7570889AD33EA942D9A6 /* AlphaWalletAddressTests.swift */; };
5E7C74B07DDDBE3344273CB7 /* PromptBackupWalletAfterWalletCreationViewViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C723C21F6376387AD1DCE /* PromptBackupWalletAfterWalletCreationViewViewModel.swift */; };
5E7C74B5796FB59C8427C7A0 /* GenerateTransferMagicLinkViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7D46C7CABC31A7477F37 /* GenerateTransferMagicLinkViewController.swift */; };
5E7C74B99922D0CAB635970E /* PasscodeCharacterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7B9220E616F82EDA956F /* PasscodeCharacterView.swift */; };
5E7C74BD08801CABF9695853 /* LocaleViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C79778E4BFE1322711EA6 /* LocaleViewModel.swift */; };
@ -344,6 +352,7 @@
5E7C75D46140FACBD12333BF /* EthTokenViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7EE374A74F2B00013C18 /* EthTokenViewCell.swift */; };
5E7C75E5C64619ABFD246183 /* TransferTokensCardViaWalletAddressViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C78B63FDE2FAF25389260 /* TransferTokensCardViaWalletAddressViewController.swift */; };
5E7C75E81F85353844CACECC /* EnterSellTokensCardPriceQuantityViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7F610139D24D947B1625 /* EnterSellTokensCardPriceQuantityViewController.swift */; };
5E7C75F1B2340BB6AA86A2EE /* ShowSeedPhraseViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7346D83DE2DB22E28F16 /* ShowSeedPhraseViewModel.swift */; };
5E7C75F80A7E178B49830BCD /* TokenCardsViewControllerHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C796039C0F47CDCA236C0 /* TokenCardsViewControllerHeader.swift */; };
5E7C760D5AF93B79BB9BDB5A /* OpenSeaNonFungibleTokenAttributeCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7C34A7BDCFE17CEF8F79 /* OpenSeaNonFungibleTokenAttributeCellViewModel.swift */; };
5E7C760E41FF2E035BC968C5 /* AssetFunctionCall.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C70021319DD5C92314A8A /* AssetFunctionCall.swift */; };
@ -354,13 +363,18 @@
5E7C7648BFF9AE93CD97A1BE /* ConsoleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7CF1465A1DCB44371BA9 /* ConsoleViewController.swift */; };
5E7C76696EF7F27EC0788CDD /* GenerateTransferMagicLinkViewControllerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7EEAAE9C23B68419E9F5 /* GenerateTransferMagicLinkViewControllerViewModel.swift */; };
5E7C7669BBE6255A2377E070 /* SetSellTokensCardExpiryDateViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7962AE417E12F13FF58E /* SetSellTokensCardExpiryDateViewController.swift */; };
5E7C76738DDAAD623C6FB4DC /* PromptBackupWalletAfterExceedingThresholdViewViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C77685B78D5372F6C7CB0 /* PromptBackupWalletAfterExceedingThresholdViewViewModel.swift */; };
5E7C767595F664FF33157ADF /* FunctionOrigin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C72FABB78B9E1655A06A2 /* FunctionOrigin.swift */; };
5E7C767848E46E079557A039 /* SeedPhraseCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C717B39E95B653F384AA0 /* SeedPhraseCellViewModel.swift */; };
5E7C767C9166726E96ED4C07 /* DelegateContract.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C72DDBF109139E4C661D5 /* DelegateContract.swift */; };
5E7C768AA9654954A0DD87F0 /* OpenSeaNonFungibleTokenCardRowViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7D913DAA3322F1C7DD46 /* OpenSeaNonFungibleTokenCardRowViewModel.swift */; };
5E7C768CFA892DEBB633961B /* SeedPhraseCollectionViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C788BDFBDF222B3F4BEAF /* SeedPhraseCollectionViewModel.swift */; };
5E7C7692C981580CD32228EB /* ChooseTokenCardTransferModeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C70FB40612BB02594EC00 /* ChooseTokenCardTransferModeViewController.swift */; };
5E7C76A0365D128B7F19A0C2 /* ProtectionCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C74BEC095303B66FB4B1E /* ProtectionCoordinator.swift */; };
5E7C76A50F321F8A0A0005EB /* ABIValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7318B6059C18BE87ECAE /* ABIValue.swift */; };
5E7C76A585ED45EDAD8825CF /* BackupState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C70A03A120A74A832DD62 /* BackupState.swift */; };
5E7C76AE768F82036ED2B2D8 /* TokenInstanceAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7B43816C35C3FE2EFFBE /* TokenInstanceAction.swift */; };
5E7C76AFB9D51A7890FD4C60 /* KeystoreBackupIntroductionViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7809B67CC2D8D6AA31C4 /* KeystoreBackupIntroductionViewModel.swift */; };
5E7C76B7DDF7BB124B12A738 /* DappButtonViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C754BF8B4CC2DA82B1025 /* DappButtonViewModel.swift */; };
5E7C76B917517C93D1E26B0A /* LockEnterPasscodeCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7981AB6584B25C72D46B /* LockEnterPasscodeCoordinator.swift */; };
5E7C76C8A983A91A82F646E5 /* TokenInstanceWebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C704499C81ACA3B08A752 /* TokenInstanceWebView.swift */; };
@ -377,10 +391,10 @@
5E7C774B5332AC0DC19C5B1B /* EthTokenViewCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C74B82783A94091A43470 /* EthTokenViewCellViewModel.swift */; };
5E7C77649E432A905B836E95 /* DappViewCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7D8C3613A9BD9F147B3C /* DappViewCellViewModel.swift */; };
5E7C776BE1B19F824954962D /* BaseTokenCardTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7F5C10E3895E805EA7E0 /* BaseTokenCardTableViewCell.swift */; };
5E7C7777BDB574E021D2D6F1 /* LegacyFileBasedKeystore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7AD8C4F68E49E1A5479A /* LegacyFileBasedKeystore.swift */; };
5E7C7788984F7ADCFE5B4DE0 /* AddressTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C75B5AF76279A71395FC7 /* AddressTextField.swift */; };
5E7C7788FA549A0402BB33CB /* HiddenContract.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7C01F8C42D7A43792C26 /* HiddenContract.swift */; };
5E7C7793AB6B577906F2BCA3 /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7AFE9AF9FE6B58C925D4 /* SettingsViewController.swift */; };
5E7C77A8425E0AFAB11F1FCD /* PromptBackupCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7ADD0FBE8708A6E98AF8 /* PromptBackupCoordinator.swift */; };
5E7C77AD9FAAC18211B6F355 /* TransferTokensCardQuantitySelectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7419F47CC8B2996AA8F9 /* TransferTokensCardQuantitySelectionViewController.swift */; };
5E7C77AF8CA540D8F1404B6F /* AssetAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7B878AC9FDC342C435F7 /* AssetAttribute.swift */; };
5E7C77B397BCB0E254F359A8 /* DappsAutoCompletionCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C74BE9900543A755CB76A /* DappsAutoCompletionCellViewModel.swift */; };
@ -406,6 +420,7 @@
5E7C78FF93B0DD68700FAFB6 /* NativeCryptoCurrencyBalanceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7D0476B2C4EEB526189D /* NativeCryptoCurrencyBalanceView.swift */; };
5E7C79284D45EF4C5440E546 /* EnabledServersCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7C7CB95B7EE4B2547585 /* EnabledServersCoordinator.swift */; };
5E7C792AA15C1D3560A18CF8 /* ConsoleCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7C51CEC4AAFDFBD75482 /* ConsoleCoordinator.swift */; };
5E7C793BF142AEC0792A68C3 /* BackupSeedPhraseCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C79107D8AAA345518435F /* BackupSeedPhraseCoordinator.swift */; };
5E7C793F7E346402CDAF771F /* AssetDefinitionStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7FE30D58E4022AF04E48 /* AssetDefinitionStoreTests.swift */; };
5E7C797BE2C8DB7EF6F217B3 /* OnboardingPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7103135DCCCAB96EE5FC /* OnboardingPage.swift */; };
5E7C798A5D213DD92F24CBFB /* AssetImplicitAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C78FAB9070B10A476DB29 /* AssetImplicitAttributes.swift */; };
@ -414,10 +429,13 @@
5E7C79DE8864702C51C0A7CC /* ResultResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C79FE0C70AC4198F2AEB7 /* ResultResult.swift */; };
5E7C79E89E5D9BAB41433ABE /* SendHeaderViewViewModelWithIntroduction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C77A8710ADB45488DE4E9 /* SendHeaderViewViewModelWithIntroduction.swift */; };
5E7C79E9A6A3DFB7FA680752 /* DappViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C793CDFA907BFDFECB6CB /* DappViewCell.swift */; };
5E7C79EB8A6EC89F6F033268 /* CreateInitialWalletViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C74A5B5B9D8AD0BD913C1 /* CreateInitialWalletViewController.swift */; };
5E7C79F32F7054020B2F835A /* ServerDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C77A1F1399FD7EE2812E8 /* ServerDictionary.swift */; };
5E7C7A0B5FDADC60DC01F060 /* CallSmartContractFunction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C77C2844B3579A59C3F2F /* CallSmartContractFunction.swift */; };
5E7C7A13AB42B38C061D46C6 /* MnemonicRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C700AC0A7FFE95EC960FA /* MnemonicRule.swift */; };
5E7C7A205F8F66D8486FAD49 /* OpenSeaNonFungibleTokenCardTableViewCellWithCheckbox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7F66EE7899E4573C64AE /* OpenSeaNonFungibleTokenCardTableViewCellWithCheckbox.swift */; };
5E7C7A4384A8E3F22D3F8249 /* SetSellTokensCardExpiryDateViewControllerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C700CD3E43689E88FBE9B /* SetSellTokensCardExpiryDateViewControllerViewModel.swift */; };
5E7C7A4700C79350AA486E09 /* SeedPhraseCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C791C60E8AAF3F4239375 /* SeedPhraseCell.swift */; };
5E7C7A496E69849D06DF2731 /* AssetDefinitionStoreCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7D58E7A5DE05425623D2 /* AssetDefinitionStoreCoordinator.swift */; };
5E7C7A656817D7E5AADF310E /* TokenInstanceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C742D015C0DEAE7759C0D /* TokenInstanceViewController.swift */; };
5E7C7A67B6143DFB9B1CF02B /* ConfirmSignMessageTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7DD9C564F2C7DE435894 /* ConfirmSignMessageTableViewCell.swift */; };
@ -427,6 +445,7 @@
5E7C7A6E4C1AEFED0B0630A6 /* TokenViewControllerTransactionCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7B5838E12930000D5029 /* TokenViewControllerTransactionCellViewModel.swift */; };
5E7C7A91D0F6CBDA3C89DEAC /* LocaleViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C79E3BC4CACB123840A42 /* LocaleViewCell.swift */; };
5E7C7A928412AF3E16CDA038 /* AmountTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C73D0DCE61EA2DE2DA21D /* AmountTextField.swift */; };
5E7C7A9363A9DF11B233F9DC /* Keystore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7DD409C330DA4033F504 /* Keystore.swift */; };
5E7C7A957F0CF58041D4929E /* AssetAttributeSyntaxValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C77EEE333F036B2C3DBD4 /* AssetAttributeSyntaxValue.swift */; };
5E7C7A9628548EB8AB8B7A26 /* TokenCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7C83B57FC8FAE9AF8F26 /* TokenCollection.swift */; };
5E7C7AB2ECFB589632F2A26C /* WalletFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7E2DCCE0D775ECF83088 /* WalletFilter.swift */; };
@ -435,16 +454,20 @@
5E7C7ACB2F44B820940EACEB /* TokenInstanceActionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C741DF75781BBB24CE6D0 /* TokenInstanceActionViewController.swift */; };
5E7C7ACBFEEE07105CC513C3 /* BaseOpenSeaNonFungibleTokenCardTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C761E148B46943FC38979 /* BaseOpenSeaNonFungibleTokenCardTableViewCell.swift */; };
5E7C7AD1BA92A8FFF930F8DC /* BrowserURLParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7C8CA3706DC14167786C /* BrowserURLParserTests.swift */; };
5E7C7AD59EA28935E32B3E91 /* KeystoreBackupIntroductionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7F829626688EA50E1B68 /* KeystoreBackupIntroductionViewController.swift */; };
5E7C7AD6B20E857DAF560E4E /* AssetAttributeValues.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7E05C83CCB7E910ADC43 /* AssetAttributeValues.swift */; };
5E7C7AE1389D3179239249F0 /* ImportWalletTabBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C743172FCBDCD362C03A6 /* ImportWalletTabBar.swift */; };
5E7C7AE2EF04A23EC7C5ADFD /* ImportMagicTokenViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7535095323B035CA47C0 /* ImportMagicTokenViewController.swift */; };
5E7C7B0367CFB413C6885474 /* GenerateSellMagicLinkViewControllerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7624D6F7EA55F6F167B3 /* GenerateSellMagicLinkViewControllerViewModel.swift */; };
5E7C7B129C55A8458AEF3F61 /* URLViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7D69938F484C2A186FAE /* URLViewModel.swift */; };
5E7C7B14B0D172F91D52F0BF /* EthereumSigner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C771FE9E98CDEBB012C28 /* EthereumSigner.swift */; };
5E7C7B3E08EEA63C5B68B9C4 /* TokenCardRedemptionInfoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C778F20D32B70D7FF2135 /* TokenCardRedemptionInfoViewController.swift */; };
5E7C7B414FA8A428798D73EF /* OpenSeaNonFungibleTokenHandling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C758EEBD945A3451C96C8 /* OpenSeaNonFungibleTokenHandling.swift */; };
5E7C7B4778FF36371701242E /* BrowserHistoryViewControllerHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C79FA3E6A05845ECFCDCF /* BrowserHistoryViewControllerHeaderView.swift */; };
5E7C7B4C7DECBF4834B1E6A4 /* CreateInitialWalletViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C73BA4FF25754ACB41255 /* CreateInitialWalletViewModel.swift */; };
5E7C7B4E3DEA90147A5A9E0A /* TokensDataStoreTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C71E355BD14E975AF7491 /* TokensDataStoreTest.swift */; };
5E7C7B8242E31B4DA0263BE5 /* EthTypedData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7FC30FF22C3EA71451BC /* EthTypedData.swift */; };
5E7C7BAF922C3EB4D1B22C46 /* VerifySeedPhraseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C788ADDEA0609433B1FDF /* VerifySeedPhraseViewController.swift */; };
5E7C7BC8F2E31F4E2BA534D9 /* Ether.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C727CF8549291E71C1640 /* Ether.swift */; };
5E7C7BCAB0CD58ACA37ED6A4 /* Dapps.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C77A400E5145C04083FEB /* Dapps.swift */; };
5E7C7BDCB6A8279E1B8ADB59 /* TokenCardTableViewCellWithCheckbox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7ECCB155D49910973F05 /* TokenCardTableViewCellWithCheckbox.swift */; };
@ -457,8 +480,11 @@
5E7C7C21E5CAF122AA4F6617 /* HowDoIGetMyMoneyInfoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C78B001F9F95F404D5FEF /* HowDoIGetMyMoneyInfoViewController.swift */; };
5E7C7C3C16408F4782523D8D /* AssetAttributeSyntax.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7037994332AE52798488 /* AssetAttributeSyntax.swift */; };
5E7C7C7142C4519873B2BB4E /* ImportWalletTabBarViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7C2872E213BBB05D55BA /* ImportWalletTabBarViewModel.swift */; };
5E7C7C8DC2A8B5CD2E06CFEE /* VerifySeedPhraseCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7709FCE9770D999F8A5C /* VerifySeedPhraseCoordinator.swift */; };
5E7C7C98EAF40E8110241DBD /* NonFungibleTokenViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C783E3ADA4CF9554A0E7D /* NonFungibleTokenViewCell.swift */; };
5E7C7C9E89056069C8FEFA76 /* AlphaWalletSettingsSwitchRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7534FB6BF4D199643246 /* AlphaWalletSettingsSwitchRow.swift */; };
5E7C7CA7562FD8352967EFBD /* PromptBackupWalletAfterReceivingEtherViewViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7F6C71DDC98D7DF754B2 /* PromptBackupWalletAfterReceivingEtherViewViewModel.swift */; };
5E7C7CCA2D436A940E874D47 /* VerifySeedPhraseViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C739774984CDE1D3D7555 /* VerifySeedPhraseViewModel.swift */; };
5E7C7CCA357CB7BF12E1F2B4 /* UIStackView+Array.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C73ED9226646D562B5A3C /* UIStackView+Array.swift */; };
5E7C7CCC8D376C6E5C245715 /* EthCurrencyHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C73DF5FBFE756097D32B1 /* EthCurrencyHelper.swift */; };
5E7C7CDE2814481CD7BC47AC /* PushNotificationsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7FC75FF544B1DF0B0D8B /* PushNotificationsCoordinator.swift */; };
@ -467,13 +493,16 @@
5E7C7CF3BB38045FA40F38AE /* PrivacyPolicyViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C72142D5817EF8FA8CADA /* PrivacyPolicyViewController.swift */; };
5E7C7CF43176653FFCE86644 /* SettingsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7B1FB2702A2A8A4EBD76 /* SettingsCoordinator.swift */; };
5E7C7CF668657D6EE78381CD /* SearchEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7F1B66DB15E6167416F8 /* SearchEngine.swift */; };
5E7C7CF6B5C96376B7BF382B /* ElevateWalletSecurityCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7CE6E3560E773D2287E2 /* ElevateWalletSecurityCoordinator.swift */; };
5E7C7D03D745BF5C202A2CD1 /* TokensCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7F932B48011A24C26733 /* TokensCoordinator.swift */; };
5E7C7D1EE18AC83D633A6749 /* EnterPasswordCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C78454B11E39CF8B5E695 /* EnterPasswordCoordinator.swift */; };
5E7C7D28171AB1C7FF5379A7 /* OpenSeaNonFungibleTokenViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C703366F010BFEF6B06C6 /* OpenSeaNonFungibleTokenViewCell.swift */; };
5E7C7D4CC66FE44C01C0F393 /* Scrollable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C75F65E8C1E20EBA6A5F4 /* Scrollable.swift */; };
5E7C7D5843B705ACD26E9090 /* AssetDefinitionDiskBackingStoreWithOverridesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C79EF9D2C12F396364B92 /* AssetDefinitionDiskBackingStoreWithOverridesTests.swift */; };
5E7C7D5D3FBA199DB0449776 /* TokenListFormatRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7D27B0DA47F340CEA70C /* TokenListFormatRowView.swift */; };
5E7C7D6AC52076681FE8C43E /* AssetDefinitionStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7716E500124CA910FB2C /* AssetDefinitionStore.swift */; };
5E7C7D71D3184F44C397FFE7 /* HelpViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C715F395B973FB61056CF /* HelpViewController.swift */; };
5E7C7D7A68C7780C293301F7 /* HDWalletTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C75DE215F0AAEF284948F /* HDWalletTest.swift */; };
5E7C7D8173CB1089D622DA38 /* HelpViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7646352F10C96B5FC6F6 /* HelpViewCell.swift */; };
5E7C7DC485AD4401A3F6D071 /* TokenViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7EC53B2B5DFAAC7965EC /* TokenViewController.swift */; };
5E7C7DCE5242D2AC0A8DA65C /* TokenCardRowViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7CAA3D0C19444005EA83 /* TokenCardRowViewModel.swift */; };
@ -481,8 +510,10 @@
5E7C7DD4D2EAA036961F18F0 /* DAppRequster.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C778A54D7D3E196BC5542 /* DAppRequster.swift */; };
5E7C7E02785866606FF298F3 /* OpenSeaNonFungibleTokenViewCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C72FBC0D2787AAA804098 /* OpenSeaNonFungibleTokenViewCellViewModel.swift */; };
5E7C7E04D4DDD7D8881A2AB1 /* UniversalLinkCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C76AF81B8DFF605558499 /* UniversalLinkCoordinator.swift */; };
5E7C7E16F37F92123610D4FB /* EthereumAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7A6AF163B603B1C8E601 /* EthereumAccount.swift */; };
5E7C7E2BCAB70E73795B5B48 /* EtherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7228C9BEB801D4CD34DE /* EtherTests.swift */; };
5E7C7E2E47ED7EDD5C127D1D /* HistoryStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7AB627E023FFD95F2276 /* HistoryStore.swift */; };
5E7C7E34C8D389FB7056EB7D /* CollectionViewLeftAlignedFlowLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7D8A8A4E7A77F7344E2F /* CollectionViewLeftAlignedFlowLayout.swift */; };
5E7C7E47C3C412A52DED7380 /* TextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7AC5A210D034DBC75FB0 /* TextView.swift */; };
5E7C7E4B4054AAD41C5BE3EC /* SettingsAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7564AF453BAB0BDAAA57 /* SettingsAction.swift */; };
5E7C7E5C30EFDC70DF1E00C1 /* TokensCardViewControllerHeaderViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C77316522DF2B256F1F92 /* TokensCardViewControllerHeaderViewModel.swift */; };
@ -490,6 +521,7 @@
5E7C7E747797C72C67BBDFF4 /* ABIEncoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7185AA9F93D4F0B67AF7 /* ABIEncoder.swift */; };
5E7C7E7AEF01B9D170228342 /* TimeEntryField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C73EFA9494B31C683A287 /* TimeEntryField.swift */; };
5E7C7E83AD74AFB0C717EAC0 /* DappsHomeViewControllerHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C799D2B7D91072FC0050B /* DappsHomeViewControllerHeaderView.swift */; };
5E7C7E8E89279EB6DB805620 /* SuccessOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7FF2EF77D5004A600DDB /* SuccessOverlayView.swift */; };
5E7C7EAAF2BD4D12987968E4 /* MyDappCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7324C9AC776E3A7B43D1 /* MyDappCellViewModel.swift */; };
5E7C7EAEBB435F3909DA36FB /* TransferTokensCardViaWalletAddressViewControllerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C76D3CFA12C2236E73E10 /* TransferTokensCardViaWalletAddressViewControllerViewModel.swift */; };
5E7C7EAED92E4AE8B99217AB /* TransferTokensCardQuantitySelectionViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7021EE19C4B81CAAF3C0 /* TransferTokensCardQuantitySelectionViewControllerTests.swift */; };
@ -505,7 +537,10 @@
5E7C7EE445B044CA15171BD5 /* DappsHomeHeaderViewViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C799836611BEE66000EE1 /* DappsHomeHeaderViewViewModel.swift */; };
5E7C7EEE563D81793CB96FA0 /* TransferNFTCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C755132D9B6F95080A1BE /* TransferNFTCoordinator.swift */; };
5E7C7EF1F2CDFA52BBF1C620 /* BrowserHistoryCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C712F42374C0B8DF8C64F /* BrowserHistoryCellViewModel.swift */; };
5E7C7F01A771565A1BCF7FFA /* SeedPhraseCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C77BCBD2C2BE682D384DB /* SeedPhraseCollectionView.swift */; };
5E7C7F1623D246AD32378D29 /* PromptBackupWalletViewViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C796C7DEA3C2A70861828 /* PromptBackupWalletViewViewModel.swift */; };
5E7C7F1B297CE042114EF095 /* LockEnterPasscodeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C75CBBFF0273EF476F95B /* LockEnterPasscodeViewController.swift */; };
5E7C7F53A21D1D0FE576DD8F /* ElevateWalletSecurityViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C753A2216F043CBDEC07C /* ElevateWalletSecurityViewController.swift */; };
5E7C7F60056FDD6ACC390400 /* UniversalLinkInPasteboardCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7F3DD81D44A996789FC4 /* UniversalLinkInPasteboardCoordinator.swift */; };
5E7C7F67945615E242B61CC3 /* BoxView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7C5454600A70DCFD7C0E /* BoxView.swift */; };
5E7C7F72926D84CF741C0D18 /* ABIType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7CBCC0A74A084AC2F053 /* ABIType.swift */; };
@ -520,6 +555,7 @@
5E7C7FDD73F658772181896B /* TermsOfServiceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7607B0EF9B8F1BC41073 /* TermsOfServiceViewController.swift */; };
5E7C7FDEE19E17F35B27D5DE /* AssetAttributesCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C717F1D00B38575B18834 /* AssetAttributesCache.swift */; };
5E7C7FE10C2FEA7316401F04 /* WelcomeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C71CE10548877F1124BF2 /* WelcomeViewModel.swift */; };
5E7C7FE1E7806D33001F9867 /* ShowSeedPhraseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C73CC82B9877AF4A42333 /* ShowSeedPhraseViewController.swift */; };
5E7C7FE5F70D5777FD7258B2 /* DappsHomeEmptyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C790B6371A5BCD733A4BE /* DappsHomeEmptyView.swift */; };
5E7C7FE8247F0E50BEF35D77 /* HowDoITransferETHIntoMyWalletInfoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7D4F7C566EDD30EF1C19 /* HowDoITransferETHIntoMyWalletInfoViewController.swift */; };
613D04891FDE15F8008DE72E /* COMODO ECC Domain Validation Secure Server CA 2.cer in Resources */ = {isa = PBXBuildFile; fileRef = 613D04881FDE15F8008DE72E /* COMODO ECC Domain Validation Secure Server CA 2.cer */; };
@ -584,7 +620,6 @@
77872D232023F43B0032D687 /* TransactionsTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77872D222023F43B0032D687 /* TransactionsTracker.swift */; };
77872D25202505B70032D687 /* EnterPasswordViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77872D24202505B70032D687 /* EnterPasswordViewController.swift */; };
77872D27202505C00032D687 /* EnterPasswordViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77872D26202505C00032D687 /* EnterPasswordViewModel.swift */; };
77872D292025116E0032D687 /* EnterPasswordCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77872D282025116E0032D687 /* EnterPasswordCoordinator.swift */; };
77872D2D202514AD0032D687 /* EnterPasswordCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77872D2C202514AD0032D687 /* EnterPasswordCoordinatorTests.swift */; };
77872D302026DC570032D687 /* SplashViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77872D2F2026DC570032D687 /* SplashViewController.swift */; };
77872D322027AA4A0032D687 /* SliderTextFieldRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77872D312027AA4A0032D687 /* SliderTextFieldRow.swift */; };
@ -679,7 +714,6 @@
291F52A61F6B766100B369AB /* BalanceRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BalanceRequest.swift; sourceTree = "<group>"; };
291F52A81F6B7BE100B369AB /* BlockNumber.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockNumber.swift; sourceTree = "<group>"; };
291F52B61F6B870400B369AB /* CastError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CastError.swift; sourceTree = "<group>"; };
291F52B81F6B880F00B369AB /* EtherKeystore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EtherKeystore.swift; sourceTree = "<group>"; };
291F52BE1F6C874E00B369AB /* AccountsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountsViewController.swift; sourceTree = "<group>"; };
291F52C01F6C8A1F00B369AB /* AccountsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountsViewModel.swift; sourceTree = "<group>"; };
2923D9B01FDA49D8000CF3F8 /* Initializer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Initializer.swift; sourceTree = "<group>"; };
@ -800,7 +834,6 @@
29CAEB8D1F70A2FB00F7357D /* TransactionCellViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionCellViewModel.swift; sourceTree = "<group>"; };
29D03F1C1F712183006E548C /* Button.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Button.swift; sourceTree = "<group>"; };
29D72A291F6A8D1500CE9209 /* AppCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinator.swift; sourceTree = "<group>"; };
29DBF2A01F9DA6EF00327C60 /* BackupViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupViewController.swift; sourceTree = "<group>"; };
29DBF2A21F9DBFF400327C60 /* BackupCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupCoordinator.swift; sourceTree = "<group>"; };
29DBF2A41F9EB75E00327C60 /* BackupCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupCoordinatorTests.swift; sourceTree = "<group>"; };
29DBF2A61F9F145900327C60 /* StateViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StateViewModel.swift; sourceTree = "<group>"; };
@ -848,7 +881,6 @@
29FF12F51F74799D00AFD326 /* NSAttributedString.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSAttributedString.swift; sourceTree = "<group>"; };
29FF12F71F747D6C00AFD326 /* Error.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Error.swift; sourceTree = "<group>"; };
29FF12FA1F74CC8200AFD326 /* EthereumAddressRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EthereumAddressRule.swift; sourceTree = "<group>"; };
29FF12FD1F75EA3F00AFD326 /* Keystore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Keystore.swift; sourceTree = "<group>"; };
29FF13001F75EAD900AFD326 /* FakeKeystore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FakeKeystore.swift; sourceTree = "<group>"; };
29FF13021F75EB7500AFD326 /* Account.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Account.swift; sourceTree = "<group>"; };
29FF13071F75F0AE00AFD326 /* AppCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinatorTests.swift; sourceTree = "<group>"; };
@ -878,7 +910,9 @@
4E16B6D97BD699F844AB4E2B /* Pods-AlphaWalletTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AlphaWalletTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-AlphaWalletTests/Pods-AlphaWalletTests.release.xcconfig"; sourceTree = "<group>"; };
5E7C70021319DD5C92314A8A /* AssetFunctionCall.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssetFunctionCall.swift; sourceTree = "<group>"; };
5E7C70088832B2D161EB4AAB /* SendViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendViewController.swift; sourceTree = "<group>"; };
5E7C700AC0A7FFE95EC960FA /* MnemonicRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MnemonicRule.swift; sourceTree = "<group>"; };
5E7C700CD3E43689E88FBE9B /* SetSellTokensCardExpiryDateViewControllerViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SetSellTokensCardExpiryDateViewControllerViewModel.swift; sourceTree = "<group>"; };
5E7C70165BA8A9F342DC7874 /* PromptBackupWalletView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PromptBackupWalletView.swift; sourceTree = "<group>"; };
5E7C7021EE19C4B81CAAF3C0 /* TransferTokensCardQuantitySelectionViewControllerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransferTokensCardQuantitySelectionViewControllerTests.swift; sourceTree = "<group>"; };
5E7C702300BB7DB0FD7788EF /* XMLHandlerTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = XMLHandlerTest.swift; sourceTree = "<group>"; };
5E7C702A684DF27DC8ED4E42 /* TokenObjectTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenObjectTest.swift; sourceTree = "<group>"; };
@ -888,7 +922,9 @@
5E7C704499C81ACA3B08A752 /* TokenInstanceWebView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenInstanceWebView.swift; sourceTree = "<group>"; };
5E7C706658D72CC1C8BB698C /* BookmarkViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BookmarkViewCell.swift; sourceTree = "<group>"; };
5E7C708DE897B6677EAD769B /* ScriptMessageProxy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScriptMessageProxy.swift; sourceTree = "<group>"; };
5E7C70A03A120A74A832DD62 /* BackupState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackupState.swift; sourceTree = "<group>"; };
5E7C70A6D4A3737631D092D9 /* EnabledServersViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnabledServersViewModel.swift; sourceTree = "<group>"; };
5E7C70B3651BDFE549C28466 /* PromptBackupCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PromptBackupCoordinator.swift; sourceTree = "<group>"; };
5E7C70FB40612BB02594EC00 /* ChooseTokenCardTransferModeViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChooseTokenCardTransferModeViewController.swift; sourceTree = "<group>"; };
5E7C7103135DCCCAB96EE5FC /* OnboardingPage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OnboardingPage.swift; sourceTree = "<group>"; };
5E7C7117B1DF438E213B406A /* History.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = History.swift; sourceTree = "<group>"; };
@ -897,6 +933,7 @@
5E7C715F395B973FB61056CF /* HelpViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HelpViewController.swift; sourceTree = "<group>"; };
5E7C71684B93F60206992E10 /* AlphaWalletSettingsTextRow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AlphaWalletSettingsTextRow.swift; path = Views/AlphaWalletSettingsTextRow.swift; sourceTree = "<group>"; };
5E7C7171B802C0C2718EEED0 /* MyDappsViewControllerHeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MyDappsViewControllerHeaderView.swift; sourceTree = "<group>"; };
5E7C717B39E95B653F384AA0 /* SeedPhraseCellViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SeedPhraseCellViewModel.swift; sourceTree = "<group>"; };
5E7C717F1D00B38575B18834 /* AssetAttributesCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssetAttributesCache.swift; sourceTree = "<group>"; };
5E7C7185AA9F93D4F0B67AF7 /* ABIEncoder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ABIEncoder.swift; sourceTree = "<group>"; };
5E7C71C2C110B621EFDE336F /* TokensCardViewControllerTitleHeader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokensCardViewControllerTitleHeader.swift; sourceTree = "<group>"; };
@ -906,6 +943,7 @@
5E7C720596AD351B21CE7583 /* MigrationInitializer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MigrationInitializer.swift; sourceTree = "<group>"; };
5E7C72142D5817EF8FA8CADA /* PrivacyPolicyViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PrivacyPolicyViewController.swift; sourceTree = "<group>"; };
5E7C7228C9BEB801D4CD34DE /* EtherTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EtherTests.swift; sourceTree = "<group>"; };
5E7C723C21F6376387AD1DCE /* PromptBackupWalletAfterWalletCreationViewViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PromptBackupWalletAfterWalletCreationViewViewModel.swift; sourceTree = "<group>"; };
5E7C72445F818B38DAEA783A /* FetchAssetDefinitionsCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FetchAssetDefinitionsCoordinator.swift; sourceTree = "<group>"; };
5E7C7251A04A7D77CE07C94D /* CallForAssetAttribute.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallForAssetAttribute.swift; sourceTree = "<group>"; };
5E7C7251DB61EB9468910C81 /* GetERC875Balance.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GetERC875Balance.swift; sourceTree = "<group>"; };
@ -927,9 +965,13 @@
5E7C7318B6059C18BE87ECAE /* ABIValue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ABIValue.swift; sourceTree = "<group>"; };
5E7C731B6F01534683227123 /* NonFungibleTokenViewCellViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NonFungibleTokenViewCellViewModel.swift; sourceTree = "<group>"; };
5E7C7324C9AC776E3A7B43D1 /* MyDappCellViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MyDappCellViewModel.swift; sourceTree = "<group>"; };
5E7C7346D83DE2DB22E28F16 /* ShowSeedPhraseViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShowSeedPhraseViewModel.swift; sourceTree = "<group>"; };
5E7C734D61C0347C1638A1F7 /* BaseTokenListFormatTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseTokenListFormatTableViewCell.swift; sourceTree = "<group>"; };
5E7C7375430F36C549EA8748 /* DappsHomeViewControllerHeaderViewViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DappsHomeViewControllerHeaderViewViewModel.swift; sourceTree = "<group>"; };
5E7C7382EAC8B9CE5EE0668D /* OpenSeaNonFungible.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OpenSeaNonFungible.swift; sourceTree = "<group>"; };
5E7C739774984CDE1D3D7555 /* VerifySeedPhraseViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VerifySeedPhraseViewModel.swift; sourceTree = "<group>"; };
5E7C73BA4FF25754ACB41255 /* CreateInitialWalletViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreateInitialWalletViewModel.swift; sourceTree = "<group>"; };
5E7C73CC82B9877AF4A42333 /* ShowSeedPhraseViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShowSeedPhraseViewController.swift; sourceTree = "<group>"; };
5E7C73D0DCE61EA2DE2DA21D /* AmountTextField.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AmountTextField.swift; path = Views/AmountTextField.swift; sourceTree = "<group>"; };
5E7C73D26F24C4AAE981E2F2 /* ImportWalletTab.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImportWalletTab.swift; sourceTree = "<group>"; };
5E7C73D55C366BCC53208686 /* BrowserHistoryCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BrowserHistoryCell.swift; sourceTree = "<group>"; };
@ -950,6 +992,7 @@
5E7C7487BDF72352446E1266 /* ImportTokenViewControllerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImportTokenViewControllerTests.swift; sourceTree = "<group>"; };
5E7C74A1A13A1A6CB9E61BAC /* TokenListFormatRowViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenListFormatRowViewModel.swift; sourceTree = "<group>"; };
5E7C74A1C56EB33FDAD6E5B0 /* TokenInstanceActionViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenInstanceActionViewModel.swift; sourceTree = "<group>"; };
5E7C74A5B5B9D8AD0BD913C1 /* CreateInitialWalletViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreateInitialWalletViewController.swift; sourceTree = "<group>"; };
5E7C74B424FB5DE3A4D6A2F4 /* DappsHomeEmptyViewViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DappsHomeEmptyViewViewModel.swift; sourceTree = "<group>"; };
5E7C74B82783A94091A43470 /* EthTokenViewCellViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EthTokenViewCellViewModel.swift; sourceTree = "<group>"; };
5E7C74B9EB81C51E956566E7 /* TokensDataStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokensDataStore.swift; sourceTree = "<group>"; };
@ -964,6 +1007,7 @@
5E7C74F4900AB6D34CDD3674 /* CallForAssetAttributeCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallForAssetAttributeCoordinator.swift; sourceTree = "<group>"; };
5E7C7534FB6BF4D199643246 /* AlphaWalletSettingsSwitchRow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AlphaWalletSettingsSwitchRow.swift; path = Views/AlphaWalletSettingsSwitchRow.swift; sourceTree = "<group>"; };
5E7C7535095323B035CA47C0 /* ImportMagicTokenViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImportMagicTokenViewController.swift; sourceTree = "<group>"; };
5E7C753A2216F043CBDEC07C /* ElevateWalletSecurityViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ElevateWalletSecurityViewController.swift; sourceTree = "<group>"; };
5E7C754BF8B4CC2DA82B1025 /* DappButtonViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DappButtonViewModel.swift; sourceTree = "<group>"; };
5E7C754C0E2E57F32A61F9A3 /* SetTransferTokensCardExpiryDateViewControllerViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SetTransferTokensCardExpiryDateViewControllerViewModel.swift; sourceTree = "<group>"; };
5E7C75506A766DF9B746E62F /* TokenInstanceViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenInstanceViewModel.swift; sourceTree = "<group>"; };
@ -979,11 +1023,14 @@
5E7C75CC640BAFFE0E789F44 /* WalletFilterViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletFilterViewModel.swift; sourceTree = "<group>"; };
5E7C75CE3F1D6B7993E7A840 /* OnboardingCollectionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OnboardingCollectionViewController.swift; sourceTree = "<group>"; };
5E7C75D384C0D727BB43305E /* SettingsHeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsHeaderView.swift; sourceTree = "<group>"; };
5E7C75DE215F0AAEF284948F /* HDWalletTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HDWalletTest.swift; sourceTree = "<group>"; };
5E7C75E7D995ABE0E6B7AD55 /* DappButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DappButton.swift; sourceTree = "<group>"; };
5E7C75F65E8C1E20EBA6A5F4 /* Scrollable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Scrollable.swift; sourceTree = "<group>"; };
5E7C76052512831B707659CA /* WhereIsWalletAddressFoundOverlayView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WhereIsWalletAddressFoundOverlayView.swift; sourceTree = "<group>"; };
5E7C7607B0EF9B8F1BC41073 /* TermsOfServiceViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TermsOfServiceViewController.swift; sourceTree = "<group>"; };
5E7C761E148B46943FC38979 /* BaseOpenSeaNonFungibleTokenCardTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseOpenSeaNonFungibleTokenCardTableViewCell.swift; sourceTree = "<group>"; };
5E7C7624D6F7EA55F6F167B3 /* GenerateSellMagicLinkViewControllerViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GenerateSellMagicLinkViewControllerViewModel.swift; sourceTree = "<group>"; };
5E7C762E8065342D6B80BE56 /* CoordinatorThatEnds.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoordinatorThatEnds.swift; sourceTree = "<group>"; };
5E7C7646352F10C96B5FC6F6 /* HelpViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HelpViewCell.swift; sourceTree = "<group>"; };
5E7C764B98F526271E4C2A6A /* StaticHTMLViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StaticHTMLViewController.swift; sourceTree = "<group>"; };
5E7C765A9FA64E4CC1B6C726 /* OpenSeaNonFungibleTokenTraitCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OpenSeaNonFungibleTokenTraitCell.swift; sourceTree = "<group>"; };
@ -996,30 +1043,40 @@
5E7C76D132F4BEA5CE4FFD0A /* StringExtensionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StringExtensionTests.swift; sourceTree = "<group>"; };
5E7C76D3CFA12C2236E73E10 /* TransferTokensCardViaWalletAddressViewControllerViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransferTokensCardViaWalletAddressViewControllerViewModel.swift; sourceTree = "<group>"; };
5E7C76EE22984D66A3C18E70 /* DappsAutoCompletionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DappsAutoCompletionViewController.swift; sourceTree = "<group>"; };
5E7C7709FCE9770D999F8A5C /* VerifySeedPhraseCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VerifySeedPhraseCoordinator.swift; sourceTree = "<group>"; };
5E7C7716E500124CA910FB2C /* AssetDefinitionStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssetDefinitionStore.swift; sourceTree = "<group>"; };
5E7C771FE9E98CDEBB012C28 /* EthereumSigner.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EthereumSigner.swift; sourceTree = "<group>"; };
5E7C7721E0E4D4EFDD35E196 /* ScanQRCodeCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScanQRCodeCoordinator.swift; sourceTree = "<group>"; };
5E7C772DC28C5110021894E3 /* ImageCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageCache.swift; sourceTree = "<group>"; };
5E7C77316522DF2B256F1F92 /* TokensCardViewControllerHeaderViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokensCardViewControllerHeaderViewModel.swift; sourceTree = "<group>"; };
5E7C774238AE6861A7935EEE /* ButtonsBar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ButtonsBar.swift; path = AlphaWallet/UI/ButtonsBar.swift; sourceTree = SOURCE_ROOT; };
5E7C774BCA281E4B077DBBFA /* WhatIsEthereumInfoViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WhatIsEthereumInfoViewController.swift; sourceTree = "<group>"; };
5E7C775FD95FE80B0F1CEA33 /* TokenAdaptorTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenAdaptorTest.swift; sourceTree = "<group>"; };
5E7C77685B78D5372F6C7CB0 /* PromptBackupWalletAfterExceedingThresholdViewViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PromptBackupWalletAfterExceedingThresholdViewViewModel.swift; sourceTree = "<group>"; };
5E7C776B129861728FFB8CC8 /* EditMyDappViewControllerViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EditMyDappViewControllerViewModel.swift; sourceTree = "<group>"; };
5E7C7778166E01A0D483C58D /* SecureEnclave.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecureEnclave.swift; sourceTree = "<group>"; };
5E7C778A54D7D3E196BC5542 /* DAppRequster.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DAppRequster.swift; sourceTree = "<group>"; };
5E7C778F20D32B70D7FF2135 /* TokenCardRedemptionInfoViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenCardRedemptionInfoViewController.swift; sourceTree = "<group>"; };
5E7C77A1F1399FD7EE2812E8 /* ServerDictionary.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServerDictionary.swift; sourceTree = "<group>"; };
5E7C77A400E5145C04083FEB /* Dapps.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Dapps.swift; sourceTree = "<group>"; };
5E7C77A8710ADB45488DE4E9 /* SendHeaderViewViewModelWithIntroduction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendHeaderViewViewModelWithIntroduction.swift; sourceTree = "<group>"; };
5E7C77B790551456E111ED4F /* PeekOpenSeaNonFungibleTokenViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeekOpenSeaNonFungibleTokenViewController.swift; sourceTree = "<group>"; };
5E7C77BCBD2C2BE682D384DB /* SeedPhraseCollectionView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SeedPhraseCollectionView.swift; sourceTree = "<group>"; };
5E7C77C2844B3579A59C3F2F /* CallSmartContractFunction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallSmartContractFunction.swift; sourceTree = "<group>"; };
5E7C77E1E6194F5A1DC8D645 /* ScreenChecker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScreenChecker.swift; sourceTree = "<group>"; };
5E7C77E2559C7C9117C0F75F /* ElevateWalletSecurityViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ElevateWalletSecurityViewModel.swift; sourceTree = "<group>"; };
5E7C77EEE333F036B2C3DBD4 /* AssetAttributeSyntaxValue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssetAttributeSyntaxValue.swift; sourceTree = "<group>"; };
5E7C7809B67CC2D8D6AA31C4 /* KeystoreBackupIntroductionViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeystoreBackupIntroductionViewModel.swift; sourceTree = "<group>"; };
5E7C781F82F9E4903C460E33 /* ImportMagicTokenCardRowViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImportMagicTokenCardRowViewModel.swift; sourceTree = "<group>"; };
5E7C7828BD821B6F04B71C00 /* SendHeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendHeaderView.swift; sourceTree = "<group>"; };
5E7C783E3ADA4CF9554A0E7D /* NonFungibleTokenViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NonFungibleTokenViewCell.swift; sourceTree = "<group>"; };
5E7C78421F01D14741DDF5BF /* ConfirmSignMessageViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConfirmSignMessageViewController.swift; sourceTree = "<group>"; };
5E7C78454B11E39CF8B5E695 /* EnterPasswordCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnterPasswordCoordinator.swift; sourceTree = "<group>"; };
5E7C785114A3266813AC92A6 /* AssetDefinitionInMemoryBackingStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssetDefinitionInMemoryBackingStore.swift; sourceTree = "<group>"; };
5E7C786486937661D0DCD4E2 /* Origin.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Origin.swift; sourceTree = "<group>"; };
5E7C787E1D2A6529C07709DB /* AssetDefinitionDiskBackingStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssetDefinitionDiskBackingStore.swift; sourceTree = "<group>"; };
5E7C788ADDEA0609433B1FDF /* VerifySeedPhraseViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VerifySeedPhraseViewController.swift; sourceTree = "<group>"; };
5E7C788BDFBDF222B3F4BEAF /* SeedPhraseCollectionViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SeedPhraseCollectionViewModel.swift; sourceTree = "<group>"; };
5E7C788F7461AC159215BFA7 /* AssetAttributeMapping.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssetAttributeMapping.swift; sourceTree = "<group>"; };
5E7C7892A9FC3F53B13498D9 /* GenerateSellMagicLinkViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GenerateSellMagicLinkViewController.swift; sourceTree = "<group>"; };
5E7C78B001F9F95F404D5FEF /* HowDoIGetMyMoneyInfoViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HowDoIGetMyMoneyInfoViewController.swift; sourceTree = "<group>"; };
@ -1029,15 +1086,18 @@
5E7C78EFAF641C41F06C46BF /* ServersCoordinatorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServersCoordinatorTests.swift; sourceTree = "<group>"; };
5E7C78FAB9070B10A476DB29 /* AssetImplicitAttributes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssetImplicitAttributes.swift; sourceTree = "<group>"; };
5E7C790B6371A5BCD733A4BE /* DappsHomeEmptyView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DappsHomeEmptyView.swift; sourceTree = "<group>"; };
5E7C79107D8AAA345518435F /* BackupSeedPhraseCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackupSeedPhraseCoordinator.swift; sourceTree = "<group>"; };
5E7C7914F01B5FEA5B20398B /* Collection+UIView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Collection+UIView.swift"; sourceTree = "<group>"; };
5E7C79199D828A206A7F23B1 /* AssetInternalValue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssetInternalValue.swift; sourceTree = "<group>"; };
5E7C791BD7AFEA4A419BAE24 /* OpenSeaNonFungibleTokenCardTableViewCellWithoutCheckbox.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OpenSeaNonFungibleTokenCardTableViewCellWithoutCheckbox.swift; sourceTree = "<group>"; };
5E7C791C60E8AAF3F4239375 /* SeedPhraseCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SeedPhraseCell.swift; sourceTree = "<group>"; };
5E7C793CDFA907BFDFECB6CB /* DappViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DappViewCell.swift; sourceTree = "<group>"; };
5E7C793E23E2364B73C4D813 /* WelcomeViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WelcomeViewController.swift; sourceTree = "<group>"; };
5E7C794F8EBAEE5E8F2821C2 /* MarketplaceViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarketplaceViewController.swift; sourceTree = "<group>"; };
5E7C7957AA1BC0B5BD6E98FF /* TransactionCollection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionCollection.swift; sourceTree = "<group>"; };
5E7C796039C0F47CDCA236C0 /* TokenCardsViewControllerHeader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenCardsViewControllerHeader.swift; sourceTree = "<group>"; };
5E7C7962AE417E12F13FF58E /* SetSellTokensCardExpiryDateViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SetSellTokensCardExpiryDateViewController.swift; sourceTree = "<group>"; };
5E7C796C7DEA3C2A70861828 /* PromptBackupWalletViewViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PromptBackupWalletViewViewModel.swift; sourceTree = "<group>"; };
5E7C79778E4BFE1322711EA6 /* LocaleViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocaleViewModel.swift; sourceTree = "<group>"; };
5E7C7981AB6584B25C72D46B /* LockEnterPasscodeCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LockEnterPasscodeCoordinator.swift; sourceTree = "<group>"; };
5E7C799836611BEE66000EE1 /* DappsHomeHeaderViewViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DappsHomeHeaderViewViewModel.swift; sourceTree = "<group>"; };
@ -1053,6 +1113,7 @@
5E7C7A16ABC8BD5D508AA641 /* ImportWalletHelpBubbleViewViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImportWalletHelpBubbleViewViewModel.swift; sourceTree = "<group>"; };
5E7C7A3D7408DC690C0F601C /* SingleChainTokenCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SingleChainTokenCoordinator.swift; sourceTree = "<group>"; };
5E7C7A92352937F37294F12C /* RateLimiter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RateLimiter.swift; sourceTree = "<group>"; };
5E7C7A6AF163B603B1C8E601 /* EthereumAccount.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EthereumAccount.swift; sourceTree = "<group>"; };
5E7C7A9876B43B1D9D17A9A9 /* OpenSeaNonFungibleTokenDisplayHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OpenSeaNonFungibleTokenDisplayHelper.swift; sourceTree = "<group>"; };
5E7C7AB3440C01136DF4F3E9 /* LockCreatePasscodeCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LockCreatePasscodeCoordinator.swift; sourceTree = "<group>"; };
5E7C7AB4464F82391AAD68C1 /* BookmarksStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BookmarksStore.swift; sourceTree = "<group>"; };
@ -1061,7 +1122,7 @@
5E7C7AC5A210D034DBC75FB0 /* TextView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = TextView.swift; path = Views/TextView.swift; sourceTree = "<group>"; };
5E7C7ACC1A14248A68E95F33 /* AssetAttributeToJavaScriptConvertor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssetAttributeToJavaScriptConvertor.swift; sourceTree = "<group>"; };
5E7C7AD33AC8BE19F5C66489 /* BaseTokenCardTableViewCellViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseTokenCardTableViewCellViewModel.swift; sourceTree = "<group>"; };
5E7C7ADD0FBE8708A6E98AF8 /* PromptBackupCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PromptBackupCoordinator.swift; sourceTree = "<group>"; };
5E7C7AD8C4F68E49E1A5479A /* LegacyFileBasedKeystore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LegacyFileBasedKeystore.swift; sourceTree = "<group>"; };
5E7C7AE6FAE0DF969B4F52E9 /* ContactUsBannerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactUsBannerView.swift; sourceTree = "<group>"; };
5E7C7AF9A592D7224ED58016 /* OnboardingPageStyle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OnboardingPageStyle.swift; sourceTree = "<group>"; };
5E7C7AFE9AF9FE6B58C925D4 /* SettingsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = "<group>"; };
@ -1083,6 +1144,7 @@
5E7C7B8FD1E2BCC325DF4EE4 /* ServersCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServersCoordinator.swift; sourceTree = "<group>"; };
5E7C7B9220E616F82EDA956F /* PasscodeCharacterView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PasscodeCharacterView.swift; sourceTree = "<group>"; };
5E7C7BA578BE5FB0E613A6D6 /* ChooseTokenCardTransferModeViewControllerViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChooseTokenCardTransferModeViewControllerViewModel.swift; sourceTree = "<group>"; };
5E7C7BA8A301CEEDE36D76A3 /* EtherKeystore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EtherKeystore.swift; sourceTree = "<group>"; };
5E7C7BAC4E511FE8446D212F /* Favicon.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Favicon.swift; sourceTree = "<group>"; };
5E7C7BCCCFE7B99162518FB7 /* ConvertSVGToPNG.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConvertSVGToPNG.swift; sourceTree = "<group>"; };
5E7C7BD9B4BDAFC2D9EBD741 /* StatusViewControllerViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatusViewControllerViewModel.swift; sourceTree = "<group>"; };
@ -1100,6 +1162,7 @@
5E7C7C5454600A70DCFD7C0E /* BoxView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BoxView.swift; sourceTree = "<group>"; };
5E7C7C58586099F082973073 /* WalletFilterView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletFilterView.swift; sourceTree = "<group>"; };
5E7C7C58A99977A9D4BE0512 /* SendHeaderViewWithIntroduction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendHeaderViewWithIntroduction.swift; sourceTree = "<group>"; };
5E7C7C6759CA1C223DABA462 /* HDWallet+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "HDWallet+Extension.swift"; sourceTree = "<group>"; };
5E7C7C781CCE43B6451671B9 /* TokenListFormatTableViewCellWithoutCheckbox.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenListFormatTableViewCellWithoutCheckbox.swift; sourceTree = "<group>"; };
5E7C7C7CB95B7EE4B2547585 /* EnabledServersCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnabledServersCoordinator.swift; sourceTree = "<group>"; };
5E7C7C83B57FC8FAE9AF8F26 /* TokenCollection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenCollection.swift; sourceTree = "<group>"; };
@ -1112,6 +1175,7 @@
5E7C7CD1FB7D353704EF3389 /* DateEntryField.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = DateEntryField.swift; path = Views/DateEntryField.swift; sourceTree = "<group>"; };
5E7C7CD7ABB18C1121D5776F /* LiveLocaleSwitcherBundle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LiveLocaleSwitcherBundle.swift; sourceTree = "<group>"; };
5E7C7CDB0BAD5D27D2F24F57 /* ServerViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServerViewCell.swift; sourceTree = "<group>"; };
5E7C7CE6E3560E773D2287E2 /* ElevateWalletSecurityCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ElevateWalletSecurityCoordinator.swift; sourceTree = "<group>"; };
5E7C7CF1465A1DCB44371BA9 /* ConsoleViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConsoleViewController.swift; sourceTree = "<group>"; };
5E7C7CFDE7DEA8C06C4100AF /* TextField.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextField.swift; sourceTree = "<group>"; };
5E7C7D0476B2C4EEB526189D /* NativeCryptoCurrencyBalanceView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NativeCryptoCurrencyBalanceView.swift; sourceTree = "<group>"; };
@ -1126,18 +1190,21 @@
5E7C7D5F3CAE69CF932AB236 /* LockPasscodeViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LockPasscodeViewController.swift; sourceTree = "<group>"; };
5E7C7D674F6B2415FB5552B0 /* CanOpenContractWebPage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CanOpenContractWebPage.swift; sourceTree = "<group>"; };
5E7C7D69938F484C2A186FAE /* URLViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = URLViewModel.swift; path = Protocols/URLViewModel.swift; sourceTree = "<group>"; };
5E7C7D8A8A4E7A77F7344E2F /* CollectionViewLeftAlignedFlowLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CollectionViewLeftAlignedFlowLayout.swift; sourceTree = "<group>"; };
5E7C7D8B2A619304329E09BA /* AssetAttributeValueUsableAsFunctionArguments.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssetAttributeValueUsableAsFunctionArguments.swift; sourceTree = "<group>"; };
5E7C7D8C3613A9BD9F147B3C /* DappViewCellViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DappViewCellViewModel.swift; sourceTree = "<group>"; };
5E7C7D8D618A8A8D55479CDF /* Dapp.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Dapp.swift; sourceTree = "<group>"; };
5E7C7D913DAA3322F1C7DD46 /* OpenSeaNonFungibleTokenCardRowViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OpenSeaNonFungibleTokenCardRowViewModel.swift; sourceTree = "<group>"; };
5E7C7D931F68BFB5E1DCE001 /* TokenCardRowView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = TokenCardRowView.swift; path = Views/TokenCardRowView.swift; sourceTree = "<group>"; };
5E7C7DCB0BDDD30D10130AE7 /* GetIsERC875Encode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GetIsERC875Encode.swift; sourceTree = "<group>"; };
5E7C7DD409C330DA4033F504 /* Keystore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Keystore.swift; sourceTree = "<group>"; };
5E7C7DD9C564F2C7DE435894 /* ConfirmSignMessageTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConfirmSignMessageTableViewCell.swift; sourceTree = "<group>"; };
5E7C7E014F0C6BD090E955A2 /* TokenInterfaceType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenInterfaceType.swift; sourceTree = "<group>"; };
5E7C7E05C83CCB7E910ADC43 /* AssetAttributeValues.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssetAttributeValues.swift; sourceTree = "<group>"; };
5E7C7E24936CC2190D2A16C2 /* OnboardingPageViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OnboardingPageViewModel.swift; sourceTree = "<group>"; };
5E7C7E2DCCE0D775ECF83088 /* WalletFilter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = WalletFilter.swift; path = Models/WalletFilter.swift; sourceTree = "<group>"; };
5E7C7E54BE622E49554FE4A9 /* AssetDefinitionsOverridesViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssetDefinitionsOverridesViewCell.swift; sourceTree = "<group>"; };
5E7C7E7720DE64069CCF37D5 /* AccountViewTableSectionHeader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountViewTableSectionHeader.swift; sourceTree = "<group>"; };
5E7C7E7EBDAAD262715E5EC4 /* TokenScriptSignatureVerifier.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenScriptSignatureVerifier.swift; sourceTree = "<group>"; };
5E7C7E9A5E7D36AA3BC108A4 /* GetENSOwnerCoordinatorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GetENSOwnerCoordinatorTests.swift; sourceTree = "<group>"; };
5E7C7EA385280B0BAB6F0745 /* TransactionViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionViewModelTests.swift; sourceTree = "<group>"; };
@ -1149,6 +1216,7 @@
5E7C7EE467A7F5F2E5B1F660 /* TokensViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokensViewModel.swift; sourceTree = "<group>"; };
5E7C7EE6BFC8BB79CD1C5565 /* AlphaWalletAddressExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlphaWalletAddressExtension.swift; sourceTree = "<group>"; };
5E7C7EEAAE9C23B68419E9F5 /* GenerateTransferMagicLinkViewControllerViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GenerateTransferMagicLinkViewControllerViewModel.swift; sourceTree = "<group>"; };
5E7C7F1B07B1403E6382B21F /* PromptBackupWalletAfterIntervalViewViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PromptBackupWalletAfterIntervalViewViewModel.swift; sourceTree = "<group>"; };
5E7C7F1B66DB15E6167416F8 /* SearchEngine.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchEngine.swift; sourceTree = "<group>"; };
5E7C7F21FA7A02F6341FB58D /* AssetDefinitionsOverridesViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssetDefinitionsOverridesViewController.swift; sourceTree = "<group>"; };
5E7C7F3DD81D44A996789FC4 /* UniversalLinkInPasteboardCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UniversalLinkInPasteboardCoordinator.swift; sourceTree = "<group>"; };
@ -1156,8 +1224,10 @@
5E7C7F5C10E3895E805EA7E0 /* BaseTokenCardTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseTokenCardTableViewCell.swift; sourceTree = "<group>"; };
5E7C7F610139D24D947B1625 /* EnterSellTokensCardPriceQuantityViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnterSellTokensCardPriceQuantityViewController.swift; sourceTree = "<group>"; };
5E7C7F66EE7899E4573C64AE /* OpenSeaNonFungibleTokenCardTableViewCellWithCheckbox.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OpenSeaNonFungibleTokenCardTableViewCellWithCheckbox.swift; sourceTree = "<group>"; };
5E7C7F6C71DDC98D7DF754B2 /* PromptBackupWalletAfterReceivingEtherViewViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PromptBackupWalletAfterReceivingEtherViewViewModel.swift; sourceTree = "<group>"; };
5E7C7F718714A0EA529664E7 /* BrowserErrorView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BrowserErrorView.swift; sourceTree = "<group>"; };
5E7C7F7EB3E0127AFDE01EEF /* AssetDefinitionDiskBackingStoreWithOverrides.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssetDefinitionDiskBackingStoreWithOverrides.swift; sourceTree = "<group>"; };
5E7C7F829626688EA50E1B68 /* KeystoreBackupIntroductionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeystoreBackupIntroductionViewController.swift; sourceTree = "<group>"; };
5E7C7F840AFFD4459FD3DBD6 /* DiscoverDappsViewControllerViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiscoverDappsViewControllerViewModel.swift; sourceTree = "<group>"; };
5E7C7F89E3480D3680750EA9 /* TokenRowView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenRowView.swift; sourceTree = "<group>"; };
5E7C7F8F3CB3847D0E4E977B /* AlphaWalletAddress.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlphaWalletAddress.swift; sourceTree = "<group>"; };
@ -1169,6 +1239,7 @@
5E7C7FCE2427A30ACD860DF8 /* ServerViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServerViewModel.swift; sourceTree = "<group>"; };
5E7C7FE30D58E4022AF04E48 /* AssetDefinitionStoreTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssetDefinitionStoreTests.swift; sourceTree = "<group>"; };
5E7C7FE5EEC96A7CDF62213F /* DappsHomeHeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DappsHomeHeaderView.swift; sourceTree = "<group>"; };
5E7C7FF2EF77D5004A600DDB /* SuccessOverlayView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SuccessOverlayView.swift; sourceTree = "<group>"; };
613D04881FDE15F8008DE72E /* COMODO ECC Domain Validation Secure Server CA 2.cer */ = {isa = PBXFileReference; lastKnownFileType = file.cer; path = "COMODO ECC Domain Validation Secure Server CA 2.cer"; sourceTree = "<group>"; };
613D048A1FDE162B008DE72E /* AlphaWalletProviderFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlphaWalletProviderFactory.swift; sourceTree = "<group>"; };
61C359DF2002AA590097B04D /* TransactionSigning.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionSigning.swift; sourceTree = "<group>"; };
@ -1230,7 +1301,6 @@
77872D222023F43B0032D687 /* TransactionsTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionsTracker.swift; sourceTree = "<group>"; };
77872D24202505B70032D687 /* EnterPasswordViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnterPasswordViewController.swift; sourceTree = "<group>"; };
77872D26202505C00032D687 /* EnterPasswordViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnterPasswordViewModel.swift; sourceTree = "<group>"; };
77872D282025116E0032D687 /* EnterPasswordCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnterPasswordCoordinator.swift; sourceTree = "<group>"; };
77872D2C202514AD0032D687 /* EnterPasswordCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnterPasswordCoordinatorTests.swift; sourceTree = "<group>"; };
77872D2F2026DC570032D687 /* SplashViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashViewController.swift; sourceTree = "<group>"; };
77872D312027AA4A0032D687 /* SliderTextFieldRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SliderTextFieldRow.swift; sourceTree = "<group>"; };
@ -1410,6 +1480,7 @@
5E7C7CE9D3BEED5AA1FC1599 /* RPC */,
5E7C7FC75FF544B1DF0B0D8B /* PushNotificationsCoordinator.swift */,
5E7C75F7FC107AE3CF396331 /* TokenScriptClient */,
5E7C7429614BD6865E3C63C5 /* KeyManagement */,
);
path = AlphaWallet;
sourceTree = "<group>";
@ -1525,6 +1596,8 @@
5E7C73EFA9494B31C683A287 /* TimeEntryField.swift */,
5E7C7C5454600A70DCFD7C0E /* BoxView.swift */,
5E7C728B3CA6A429AB5EE5DF /* ContainerViewWithShadow.swift */,
5E7C7FF2EF77D5004A600DDB /* SuccessOverlayView.swift */,
5E7C76052512831B707659CA /* WhereIsWalletAddressFoundOverlayView.swift */,
);
path = UI;
sourceTree = "<group>";
@ -1550,10 +1623,8 @@
2959961D1FAE756400DB66A8 /* TrustClient */,
291F52B61F6B870400B369AB /* CastError.swift */,
2961BD061FB146EB00C4B840 /* ChainState.swift */,
291F52B81F6B880F00B369AB /* EtherKeystore.swift */,
291F52A41F6B762300B369AB /* EtherServiceRequest.swift */,
29F114ED1FA65DEF00114A29 /* ImportType.swift */,
29FF12FD1F75EA3F00AFD326 /* Keystore.swift */,
291ED08C1F6F5F0A00E7E93A /* KeyStoreError.swift */,
2923D9B41FDA4E07000CF3F8 /* PasswordGenerator.swift */,
61DCE17A2001A6BE0053939F /* RLP.swift */,
@ -1635,6 +1706,7 @@
isa = PBXGroup;
children = (
295247DE1F8326EF007FDC31 /* AccountViewCell.swift */,
5E7C7E7720DE64069CCF37D5 /* AccountViewTableSectionHeader.swift */,
);
path = Views;
sourceTree = "<group>";
@ -1690,7 +1762,7 @@
isa = PBXGroup;
children = (
29DBF2A21F9DBFF400327C60 /* BackupCoordinator.swift */,
5E7C7ADD0FBE8708A6E98AF8 /* PromptBackupCoordinator.swift */,
5E7C70B3651BDFE549C28466 /* PromptBackupCoordinator.swift */,
);
path = Coordinators;
sourceTree = "<group>";
@ -1745,6 +1817,10 @@
children = (
296AF9A21F733AB30058AF78 /* WalletCoordinator.swift */,
293E62701FA2F63500CB0A66 /* InitialWalletCreationCoordinator.swift */,
5E7C78454B11E39CF8B5E695 /* EnterPasswordCoordinator.swift */,
5E7C79107D8AAA345518435F /* BackupSeedPhraseCoordinator.swift */,
5E7C7709FCE9770D999F8A5C /* VerifySeedPhraseCoordinator.swift */,
5E7C7CE6E3560E773D2287E2 /* ElevateWalletSecurityCoordinator.swift */,
);
path = Coordinators;
sourceTree = "<group>";
@ -1807,7 +1883,11 @@
2912CD2E1F6A83A100C6CBE3 /* ImportWalletViewController.swift */,
29F1C852200363B2003780D8 /* PassphraseViewController.swift */,
77872D24202505B70032D687 /* EnterPasswordViewController.swift */,
77872D282025116E0032D687 /* EnterPasswordCoordinator.swift */,
5E7C73CC82B9877AF4A42333 /* ShowSeedPhraseViewController.swift */,
5E7C788ADDEA0609433B1FDF /* VerifySeedPhraseViewController.swift */,
5E7C7F829626688EA50E1B68 /* KeystoreBackupIntroductionViewController.swift */,
5E7C74A5B5B9D8AD0BD913C1 /* CreateInitialWalletViewController.swift */,
5E7C753A2216F043CBDEC07C /* ElevateWalletSecurityViewController.swift */,
);
path = ViewControllers;
sourceTree = "<group>";
@ -1819,6 +1899,13 @@
771AA961200D5EC700D25403 /* PassphraseViewModel.swift */,
77872D26202505C00032D687 /* EnterPasswordViewModel.swift */,
5E7C7C2872E213BBB05D55BA /* ImportWalletTabBarViewModel.swift */,
5E7C7346D83DE2DB22E28F16 /* ShowSeedPhraseViewModel.swift */,
5E7C717B39E95B653F384AA0 /* SeedPhraseCellViewModel.swift */,
5E7C739774984CDE1D3D7555 /* VerifySeedPhraseViewModel.swift */,
5E7C7809B67CC2D8D6AA31C4 /* KeystoreBackupIntroductionViewModel.swift */,
5E7C73BA4FF25754ACB41255 /* CreateInitialWalletViewModel.swift */,
5E7C788BDFBDF222B3F4BEAF /* SeedPhraseCollectionViewModel.swift */,
5E7C77E2559C7C9117C0F75F /* ElevateWalletSecurityViewModel.swift */,
);
path = ViewModels;
sourceTree = "<group>";
@ -1997,8 +2084,9 @@
isa = PBXGroup;
children = (
299B5E391FD141A40051361C /* ViewModels */,
29DBF29F1F9DA6E200327C60 /* ViewControllers */,
2977CAE11F7E0B17009682A0 /* Coordinators */,
5E7C7E72DDB6CFF11FF96AA6 /* Views */,
5E7C7D446E83A4AB2A36AEE9 /* Models */,
);
path = Export;
sourceTree = "<group>";
@ -2034,6 +2122,11 @@
isa = PBXGroup;
children = (
299B5E3A1FD141B70051361C /* BackupViewModel.swift */,
5E7C796C7DEA3C2A70861828 /* PromptBackupWalletViewViewModel.swift */,
5E7C723C21F6376387AD1DCE /* PromptBackupWalletAfterWalletCreationViewViewModel.swift */,
5E7C7F6C71DDC98D7DF754B2 /* PromptBackupWalletAfterReceivingEtherViewViewModel.swift */,
5E7C7F1B07B1403E6382B21F /* PromptBackupWalletAfterIntervalViewViewModel.swift */,
5E7C77685B78D5372F6C7CB0 /* PromptBackupWalletAfterExceedingThresholdViewViewModel.swift */,
);
path = ViewModels;
sourceTree = "<group>";
@ -2064,6 +2157,7 @@
298542F81FBE9A0100CB5081 /* CryptoAddressValidator.swift */,
61FC5ECE1FCFBAE500CCB12A /* EtherNumberFormatter.swift */,
73ED85A420349BE400593BF3 /* StringFormatter.swift */,
5E7C762E8065342D6B80BE56 /* CoordinatorThatEnds.swift */,
);
path = Foundation;
sourceTree = "<group>";
@ -2175,14 +2269,6 @@
path = Factory;
sourceTree = "<group>";
};
29DBF29F1F9DA6E200327C60 /* ViewControllers */ = {
isa = PBXGroup;
children = (
29DBF2A01F9DA6EF00327C60 /* BackupViewController.swift */,
);
path = ViewControllers;
sourceTree = "<group>";
};
29E14FCF1F7F456A00185568 /* Storage */ = {
isa = PBXGroup;
children = (
@ -2298,6 +2384,7 @@
children = (
29F1C85520036887003780D8 /* AppTrackerTests.swift */,
5E7C7570889AD33EA942D9A6 /* AlphaWalletAddressTests.swift */,
5E7C75DE215F0AAEF284948F /* HDWalletTest.swift */,
);
path = Types;
sourceTree = "<group>";
@ -2379,6 +2466,8 @@
2963B6AC1F981A96003063C1 /* TransactionAppearance.swift */,
29F114F11FA7966300114A29 /* PrivateKeyRule.swift */,
77872D312027AA4A0032D687 /* SliderTextFieldRow.swift */,
5E7C700AC0A7FFE95EC960FA /* MnemonicRule.swift */,
5E7C7C6759CA1C223DABA462 /* HDWallet+Extension.swift */,
);
path = Form;
sourceTree = "<group>";
@ -2593,6 +2682,19 @@
path = Views;
sourceTree = "<group>";
};
5E7C7429614BD6865E3C63C5 /* KeyManagement */ = {
isa = PBXGroup;
children = (
5E7C7AD8C4F68E49E1A5479A /* LegacyFileBasedKeystore.swift */,
5E7C7BA8A301CEEDE36D76A3 /* EtherKeystore.swift */,
5E7C7DD409C330DA4033F504 /* Keystore.swift */,
5E7C7A6AF163B603B1C8E601 /* EthereumAccount.swift */,
5E7C771FE9E98CDEBB012C28 /* EthereumSigner.swift */,
5E7C7778166E01A0D483C58D /* SecureEnclave.swift */,
);
path = KeyManagement;
sourceTree = "<group>";
};
5E7C755521B5CAF98176AB84 /* ViewModels */ = {
isa = PBXGroup;
children = (
@ -2823,6 +2925,14 @@
path = Views;
sourceTree = "<group>";
};
5E7C7D446E83A4AB2A36AEE9 /* Models */ = {
isa = PBXGroup;
children = (
5E7C70A03A120A74A832DD62 /* BackupState.swift */,
);
path = Models;
sourceTree = "<group>";
};
5E7C7D5E39590AF6C371C1AF /* Storage */ = {
isa = PBXGroup;
children = (
@ -2849,6 +2959,14 @@
path = ViewControllers;
sourceTree = "<group>";
};
5E7C7E72DDB6CFF11FF96AA6 /* Views */ = {
isa = PBXGroup;
children = (
5E7C70165BA8A9F342DC7874 /* PromptBackupWalletView.swift */,
);
path = Views;
sourceTree = "<group>";
};
5E7C7F4DD4AC9B413701BBAD /* web3swift-pod */ = {
isa = PBXGroup;
children = (
@ -3061,6 +3179,9 @@
771AA963200D5EDB00D25403 /* WordCollectionViewCell.swift */,
771AA965200D5F1900D25403 /* WordCollectionViewCell.xib */,
5E7C743172FCBDCD362C03A6 /* ImportWalletTabBar.swift */,
5E7C791C60E8AAF3F4239375 /* SeedPhraseCell.swift */,
5E7C7D8A8A4E7A77F7344E2F /* CollectionViewLeftAlignedFlowLayout.swift */,
5E7C77BCBD2C2BE682D384DB /* SeedPhraseCollectionView.swift */,
);
path = Views;
sourceTree = "<group>";
@ -3516,10 +3637,12 @@
"${BUILT_PRODUCTS_DIR}/SeedStackViewController/StackViewController.framework",
"${BUILT_PRODUCTS_DIR}/SipHash/SipHash.framework",
"${BUILT_PRODUCTS_DIR}/StatefulViewController/StatefulViewController.framework",
"${BUILT_PRODUCTS_DIR}/SwiftProtobuf/SwiftProtobuf.framework",
"${BUILT_PRODUCTS_DIR}/SwiftyJSON/SwiftyJSON.framework",
"${BUILT_PRODUCTS_DIR}/SwiftyXMLParser/SwiftyXMLParser.framework",
"${BUILT_PRODUCTS_DIR}/TrezorCrypto/TrezorCrypto.framework",
"${BUILT_PRODUCTS_DIR}/TrustKeystore/TrustKeystore.framework",
"${BUILT_PRODUCTS_DIR}/TrustWalletCore/TrustWalletCore.framework",
"${BUILT_PRODUCTS_DIR}/libsodium/libsodium.framework",
"${BUILT_PRODUCTS_DIR}/secp256k1_ios/secp256k1_ios.framework",
"${BUILT_PRODUCTS_DIR}/web3swift/web3swift.framework",
@ -3553,10 +3676,12 @@
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/StackViewController.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SipHash.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/StatefulViewController.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwiftProtobuf.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwiftyJSON.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwiftyXMLParser.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/TrezorCrypto.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/TrustKeystore.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/TrustWalletCore.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/libsodium.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/secp256k1_ios.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/web3swift.framework",
@ -3662,7 +3787,6 @@
29C70C7F20199AEB0072E454 /* WKWebViewConfiguration.swift in Sources */,
73ACEF0120163ED4003DD71D /* LockViewModel.swift in Sources */,
29AD8A041F93D6CD008E10E7 /* Constants.swift in Sources */,
29FF12FE1F75EA3F00AFD326 /* Keystore.swift in Sources */,
2963B6B11F9891F5003063C1 /* UIButton.swift in Sources */,
29F114F21FA7966300114A29 /* PrivateKeyRule.swift in Sources */,
29F1C853200363B2003780D8 /* PassphraseViewController.swift in Sources */,
@ -3683,10 +3807,8 @@
29FF12F61F74799D00AFD326 /* NSAttributedString.swift in Sources */,
AA26C62320412A4100318B9B /* UIViewInspectableEnhancements.swift in Sources */,
293B8B431F70815900356286 /* BalanceTitleView.swift in Sources */,
29DBF2A11F9DA6EF00327C60 /* BackupViewController.swift in Sources */,
29FC9BC61F830899000209CD /* MigrationInitializerForOneChainPerDatabase.swift in Sources */,
29AD8A091F93F8B2008E10E7 /* Session.swift in Sources */,
291F52B91F6B880F00B369AB /* EtherKeystore.swift in Sources */,
29BB94971F6FCD60009B09CC /* SendViewModel.swift in Sources */,
296106D01F778A8D0006164B /* TransferType.swift in Sources */,
29E14FDB1F7F4F3D00185568 /* Transaction.swift in Sources */,
@ -3795,7 +3917,6 @@
CCA4FE361FD4282400749AE4 /* DeviceChecker.swift in Sources */,
732086B9201508690047F605 /* SplashCoordinator.swift in Sources */,
AA26C62420412A4100318B9B /* Double.swift in Sources */,
77872D292025116E0032D687 /* EnterPasswordCoordinator.swift in Sources */,
298542FB1FBEA03300CB5081 /* SendInputErrors.swift in Sources */,
29EB102A1F6CBD23000907A4 /* UIAlertController.swift in Sources */,
296AF9A51F736BA20058AF78 /* Config.swift in Sources */,
@ -3956,7 +4077,6 @@
5E7C75E5C64619ABFD246183 /* TransferTokensCardViaWalletAddressViewController.swift in Sources */,
5E7C7EAEBB435F3909DA36FB /* TransferTokensCardViaWalletAddressViewControllerViewModel.swift in Sources */,
5E7C7CCC8D376C6E5C245715 /* EthCurrencyHelper.swift in Sources */,
5E7C77A8425E0AFAB11F1FCD /* PromptBackupCoordinator.swift in Sources */,
5E7C7317533D24B6A292F88D /* UIStackView+Array.swift in Sources */,
5E7C78F1D29280E3FF4EAF5E /* RoundedBackground.swift in Sources */,
5E7C7E68425E20834B898D06 /* AppLocale.swift in Sources */,
@ -4171,6 +4291,45 @@
5E7C767595F664FF33157ADF /* FunctionOrigin.swift in Sources */,
5E7C7FC4F00168189F1623C7 /* TokenInterfaceType.swift in Sources */,
5E7C720C2A5CA7D41B12D666 /* RateLimiter.swift in Sources */,
5E7C7777BDB574E021D2D6F1 /* LegacyFileBasedKeystore.swift in Sources */,
5E7C73F6762376F5B2B4214D /* EtherKeystore.swift in Sources */,
5E7C7A9363A9DF11B233F9DC /* Keystore.swift in Sources */,
5E7C7E16F37F92123610D4FB /* EthereumAccount.swift in Sources */,
5E7C7B14B0D172F91D52F0BF /* EthereumSigner.swift in Sources */,
5E7C7A13AB42B38C061D46C6 /* MnemonicRule.swift in Sources */,
5E7C7067E8FEA5055BF83553 /* HDWallet+Extension.swift in Sources */,
5E7C71145CE952518B3EECE3 /* AccountViewTableSectionHeader.swift in Sources */,
5E7C7D1EE18AC83D633A6749 /* EnterPasswordCoordinator.swift in Sources */,
5E7C793BF142AEC0792A68C3 /* BackupSeedPhraseCoordinator.swift in Sources */,
5E7C7FE1E7806D33001F9867 /* ShowSeedPhraseViewController.swift in Sources */,
5E7C75F1B2340BB6AA86A2EE /* ShowSeedPhraseViewModel.swift in Sources */,
5E7C7A4700C79350AA486E09 /* SeedPhraseCell.swift in Sources */,
5E7C767848E46E079557A039 /* SeedPhraseCellViewModel.swift in Sources */,
5E7C7C8DC2A8B5CD2E06CFEE /* VerifySeedPhraseCoordinator.swift in Sources */,
5E7C7BAF922C3EB4D1B22C46 /* VerifySeedPhraseViewController.swift in Sources */,
5E7C7CCA2D436A940E874D47 /* VerifySeedPhraseViewModel.swift in Sources */,
5E7C7E34C8D389FB7056EB7D /* CollectionViewLeftAlignedFlowLayout.swift in Sources */,
5E7C7AD59EA28935E32B3E91 /* KeystoreBackupIntroductionViewController.swift in Sources */,
5E7C76AFB9D51A7890FD4C60 /* KeystoreBackupIntroductionViewModel.swift in Sources */,
5E7C70C8C9FB794488211122 /* CoordinatorThatEnds.swift in Sources */,
5E7C79EB8A6EC89F6F033268 /* CreateInitialWalletViewController.swift in Sources */,
5E7C7B4C7DECBF4834B1E6A4 /* CreateInitialWalletViewModel.swift in Sources */,
5E7C71769464FDBDE6803A91 /* PromptBackupCoordinator.swift in Sources */,
5E7C72311A412C022AA20F51 /* PromptBackupWalletView.swift in Sources */,
5E7C7F1623D246AD32378D29 /* PromptBackupWalletViewViewModel.swift in Sources */,
5E7C74B07DDDBE3344273CB7 /* PromptBackupWalletAfterWalletCreationViewViewModel.swift in Sources */,
5E7C7CA7562FD8352967EFBD /* PromptBackupWalletAfterReceivingEtherViewViewModel.swift in Sources */,
5E7C70DDE7C3BD32FA525753 /* PromptBackupWalletAfterIntervalViewViewModel.swift in Sources */,
5E7C76A585ED45EDAD8825CF /* BackupState.swift in Sources */,
5E7C76738DDAAD623C6FB4DC /* PromptBackupWalletAfterExceedingThresholdViewViewModel.swift in Sources */,
5E7C7F01A771565A1BCF7FFA /* SeedPhraseCollectionView.swift in Sources */,
5E7C768CFA892DEBB633961B /* SeedPhraseCollectionViewModel.swift in Sources */,
5E7C7E8E89279EB6DB805620 /* SuccessOverlayView.swift in Sources */,
5E7C738BCA59B1DE116ECC96 /* WhereIsWalletAddressFoundOverlayView.swift in Sources */,
5E7C70F407FBA1BD69901861 /* SecureEnclave.swift in Sources */,
5E7C7CF6B5C96376B7BF382B /* ElevateWalletSecurityCoordinator.swift in Sources */,
5E7C7F53A21D1D0FE576DD8F /* ElevateWalletSecurityViewController.swift in Sources */,
5E7C743467F5D428AF4E4F0F /* ElevateWalletSecurityViewModel.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -4255,6 +4414,7 @@
5E7C7FC3D8846843465B0F90 /* ServersCoordinatorTests.swift in Sources */,
5E7C74A655E5D34953FFEBF2 /* AlphaWalletAddressTests.swift in Sources */,
76F1D09ED5C81F9B420FACD4 /* TokenScriptSignatureVerifierTest.swift in Sources */,
5E7C7D7A68C7780C293301F7 /* HDWalletTest.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};

@ -1,7 +1,6 @@
// Copyright SIX DAY LLC. All rights reserved.
import Foundation
import TrustKeystore
import UIKit
protocol AccountsCoordinatorDelegate: class {
@ -17,6 +16,7 @@ class AccountsCoordinator: Coordinator {
//Only show Ether balances from mainnet for now
private let balanceCoordinator = GetBalanceCoordinator(forServer: .main)
private let keystore: Keystore
private let promptBackupCoordinator: PromptBackupCoordinator
let navigationController: UINavigationController
var coordinators: [Coordinator] = []
@ -24,7 +24,7 @@ class AccountsCoordinator: Coordinator {
lazy var accountsViewController: AccountsViewController = {
let controller = AccountsViewController(keystore: keystore, balanceCoordinator: balanceCoordinator)
controller.navigationItem.leftBarButtonItem = UIBarButtonItem(title: R.string.localizable.done(), style: .done, target: self, action: #selector(dismiss))
controller.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(add))
controller.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(addWallet))
controller.allowsAccountDeletion = true
controller.delegate = self
return controller
@ -35,31 +35,39 @@ class AccountsCoordinator: Coordinator {
init(
config: Config,
navigationController: UINavigationController,
keystore: Keystore
keystore: Keystore,
promptBackupCoordinator: PromptBackupCoordinator
) {
self.config = config
self.navigationController = navigationController
self.navigationController.modalPresentationStyle = .formSheet
self.keystore = keystore
self.promptBackupCoordinator = promptBackupCoordinator
}
func start() {
navigationController.pushViewController(accountsViewController, animated: false)
}
@objc func dismiss() {
@objc private func dismiss() {
delegate?.didCancel(in: self)
}
@objc func add() {
chooseImportOrCreateWallet()
}
func chooseImportOrCreateWallet() {
@objc private func addWallet() {
UIAlertController.alert(title: nil,
message: nil,
alertButtonTitles: [R.string.localizable.walletCreateButtonTitle(), R.string.localizable.walletImportButtonTitle(), R.string.localizable.cancel()],
alertButtonStyles: [.default, .default, .cancel],
alertButtonTitles: [
R.string.localizable.walletCreateButtonTitle(),
R.string.localizable.walletImportButtonTitle(),
R.string.localizable.walletWatchButtonTitle(),
R.string.localizable.cancel()
],
alertButtonStyles: [
.default,
.default,
.default,
.cancel
],
viewController: navigationController,
preferredStyle: .actionSheet) { [weak self] index in
guard let strongSelf = self else { return }
@ -67,11 +75,13 @@ class AccountsCoordinator: Coordinator {
strongSelf.showCreateWallet()
} else if index == 1 {
strongSelf.showImportWallet()
} else if index == 2 {
strongSelf.showWatchWallet()
}
}
}
func importOrCreateWallet(entryPoint: WalletEntryPoint) {
private func importOrCreateWallet(entryPoint: WalletEntryPoint) {
let coordinator = WalletCoordinator(config: config, keystore: keystore)
if case .createInstantWallet = entryPoint {
coordinator.navigationController = navigationController
@ -84,28 +94,37 @@ class AccountsCoordinator: Coordinator {
}
}
func showCreateWallet() {
private func showCreateWallet() {
importOrCreateWallet(entryPoint: .createInstantWallet)
}
func showImportWallet() {
private func showImportWallet() {
importOrCreateWallet(entryPoint: .importWallet)
}
func showInfoSheet(for account: Wallet, sender: UIView) {
private func showWatchWallet() {
importOrCreateWallet(entryPoint: .watchWallet)
}
private func showInfoSheet(for account: Wallet, sender: UIView) {
let controller = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
controller.popoverPresentationController?.sourceView = sender
controller.popoverPresentationController?.sourceRect = sender.centerRect
switch account.type {
case .real(let account):
let actionTitle = R.string.localizable.walletsBackupAlertSheetTitle()
let actionTitle: String
if keystore.isHdWallet(account: account) {
actionTitle = R.string.localizable.walletsBackupHdWalletAlertSheetTitle()
} else {
actionTitle = R.string.localizable.walletsBackupKeystoreWalletAlertSheetTitle()
}
let backupKeystoreAction = UIAlertAction(title: actionTitle, style: .default) { [weak self] _ in
guard let strongSelf = self else { return }
let coordinator = BackupCoordinator(
navigationController: strongSelf.navigationController,
keystore: strongSelf.keystore,
account: account
navigationController: strongSelf.navigationController,
keystore: strongSelf.keystore,
account: account
)
coordinator.delegate = strongSelf
coordinator.start()
@ -173,7 +192,9 @@ extension AccountsCoordinator: BackupCoordinatorDelegate {
removeCoordinator(coordinator)
}
func didFinish(account: Account, in coordinator: BackupCoordinator) {
func didFinish(account: EthereumAccount, in coordinator: BackupCoordinator) {
promptBackupCoordinator.markBackupDone()
promptBackupCoordinator.showHideCurrentPrompt()
removeCoordinator(coordinator)
}
}

@ -14,16 +14,11 @@ class AccountsViewController: UIViewController {
private let header = TokensCardViewControllerTitleHeader()
private let tableView = UITableView(frame: .zero, style: .plain)
private var viewModel: AccountsViewModel {
return AccountsViewModel(
wallets: wallets
)
}
private var wallets: [Wallet] = [] {
didSet {
tableView.reloadData()
configure(viewModel: viewModel)
}
return AccountsViewModel(hdWallets: hdWallets, keystoreWallets: keystoreWallets, watchedWallets: watchedWallets)
}
private var hdWallets: [Wallet] = []
private var keystoreWallets: [Wallet] = []
private var watchedWallets: [Wallet] = []
private var balances: [AlphaWallet.Address: Balance?] = [:]
private let keystore: Keystore
private let balanceCoordinator: GetBalanceCoordinator
@ -71,19 +66,37 @@ class AccountsViewController: UIViewController {
fetch()
refreshWalletBalances()
}
func fetch() {
wallets = keystore.wallets.sorted { $0.address.eip55String < $1.address.eip55String }
hdWallets = keystore.wallets.filter { keystore.isHdWallet(wallet: $0) }.sorted { $0.address.eip55String < $1.address.eip55String }
keystoreWallets = keystore.wallets.filter { keystore.isKeystore(wallet: $0) }.sorted { $0.address.eip55String < $1.address.eip55String }
watchedWallets = keystore.wallets.filter { keystore.isWatched(wallet: $0) }.sorted { $0.address.eip55String < $1.address.eip55String }
tableView.reloadData()
configure(viewModel: viewModel)
}
func configure(viewModel: AccountsViewModel) {
tableView.dataSource = self
header.configure(title: viewModel.title)
header.frame.size.height = headerHeight
tableView.tableHeaderView = header
}
func account(for indexPath: IndexPath) -> Wallet {
return viewModel.wallets[indexPath.row]
private func account(for indexPath: IndexPath) -> Wallet {
switch AccountViewTableSectionHeader.HeaderType(rawValue: indexPath.section) {
case .some(.hdWallet):
return viewModel.hdWallets[indexPath.row]
case .some(.keystoreWallet):
return viewModel.keystoreWallets[indexPath.row]
case .some(.watchedWallet):
return viewModel.watchedWallets[indexPath.row]
case .none:
//TODO really shouldn't be here
return viewModel.hdWallets.first ?? (viewModel.keystoreWallets.first ?? viewModel.watchedWallets[0])
}
}
func confirmDelete(account: Wallet) {
private func confirmDelete(account: Wallet) {
confirm(
title: R.string.localizable.accountsConfirmDeleteTitle(),
message: R.string.localizable.accountsConfirmDeleteMessage(),
@ -98,7 +111,8 @@ class AccountsViewController: UIViewController {
}
}
}
func delete(account: Wallet) {
private func delete(account: Wallet) {
navigationController?.displayLoading(text: R.string.localizable.deleting())
keystore.delete(wallet: account) { [weak self] result in
guard let strongSelf = self else { return }
@ -112,10 +126,11 @@ class AccountsViewController: UIViewController {
}
}
}
private func refreshWalletBalances() {
let addresses = wallets.compactMap { $0.address }
var counter = 0
for address in addresses {
let addresses = (hdWallets + keystoreWallets + watchedWallets).compactMap { $0.address }
var counter = 0
for address in addresses {
balanceCoordinator.getEthBalance(for: address, completion: { [weak self] (result) in
self?.balances[address] = result.value
counter += 1
@ -125,6 +140,7 @@ class AccountsViewController: UIViewController {
})
}
}
private func getAccountViewModels(for path: IndexPath) -> AccountViewModel {
let account = self.account(for: path)
let balance = self.balances[account.address].flatMap { $0 }
@ -137,9 +153,22 @@ class AccountsViewController: UIViewController {
}
}
extension AccountsViewController: UITableViewDelegate, UITableViewDataSource {
extension AccountsViewController: UITableViewDataSource {
public func numberOfSections(in tableView: UITableView) -> Int {
return 3
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return viewModel.wallets.count
switch AccountViewTableSectionHeader.HeaderType(rawValue: section) {
case .some(.hdWallet):
return viewModel.hdWallets.count
case .some(.keystoreWallet):
return viewModel.keystoreWallets.count
case .some(.watchedWallet):
return viewModel.watchedWallets.count
case .none:
return 0
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
@ -152,24 +181,67 @@ extension AccountsViewController: UITableViewDelegate, UITableViewDataSource {
}
func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
return allowsAccountDeletion && (etherKeystore?.recentlyUsedWallet != viewModel.wallets[indexPath.row])
guard allowsAccountDeletion else { return false }
switch AccountViewTableSectionHeader.HeaderType(rawValue: indexPath.section) {
case .some(.hdWallet):
return etherKeystore?.recentlyUsedWallet != viewModel.hdWallets[indexPath.row]
case .some(.keystoreWallet):
return etherKeystore?.recentlyUsedWallet != viewModel.keystoreWallets[indexPath.row]
case .some(.watchedWallet):
return etherKeystore?.recentlyUsedWallet != viewModel.watchedWallets[indexPath.row]
case .none:
return false
}
}
}
extension AccountsViewController: UITableViewDelegate {
//We don't show the section headers unless there are 2 "types" of wallets
private func shouldHideAllSectionHeaders() -> Bool {
if viewModel.keystoreWallets.isEmpty && viewModel.watchedWallets.isEmpty {
return true
}
if viewModel.hdWallets.isEmpty && viewModel.keystoreWallets.isEmpty {
return true
}
if viewModel.hdWallets.isEmpty && viewModel.watchedWallets.isEmpty {
return true
}
return false
}
public func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let v = AccountViewTableSectionHeader()
let shouldHideSectionHeaders = shouldHideAllSectionHeaders()
switch AccountViewTableSectionHeader.HeaderType(rawValue: section) {
case .some(.hdWallet):
v.configure(type: .hdWallet, shouldHide: true)
case .some(.keystoreWallet):
v.configure(type: .keystoreWallet, shouldHide: shouldHideSectionHeaders || viewModel.keystoreWallets.isEmpty)
case .some(.watchedWallet):
v.configure(type: .watchedWallet, shouldHide: shouldHideSectionHeaders || viewModel.watchedWallets.isEmpty)
case .none:
return nil
}
return v
}
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
let action = UITableViewRowAction(style: .destructive, title: R.string.localizable.accountsConfirmDeleteAction()) { rowAction, indexPath in
let account = self.account(for: indexPath)
confirmDelete(account: account)
self.confirmDelete(account: account)
}
return [action]
}
}
extension AccountsViewController: AccountViewCellDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
let account = self.account(for: indexPath)
delegate?.didSelectAccount(account: account, in: self)
}
}
extension AccountsViewController: AccountViewCellDelegate {
func accountViewCell(_ cell: AccountViewCell, didTapInfoViewForAccount account: Wallet) {
delegate?.didSelectInfoForAccount(account: account, sender: cell.infoButton, in: self)
}

@ -3,11 +3,14 @@
import Foundation
struct AccountsViewModel {
let hdWallets: [Wallet]
let keystoreWallets: [Wallet]
let watchedWallets: [Wallet]
let wallets: [Wallet]
init(wallets: [Wallet]) {
self.wallets = wallets
init(hdWallets: [Wallet], keystoreWallets: [Wallet], watchedWallets: [Wallet]) {
self.hdWallets = hdWallets
self.keystoreWallets = keystoreWallets
self.watchedWallets = watchedWallets
}
var title: String {

@ -0,0 +1,79 @@
// Copyright © 2019 Stormbird PTE. LTD.
import Foundation
import UIKit
class AccountViewTableSectionHeader: UIView {
enum HeaderType: Int {
case hdWallet = 0
case keystoreWallet = 1
case watchedWallet = 2
var title: String {
switch self {
case .hdWallet:
return R.string.localizable.walletTypesHdWallets()
case .keystoreWallet:
return R.string.localizable.walletTypesKeystoreWallets()
case .watchedWallet:
return R.string.localizable.walletTypesWatchedWallets()
}
}
}
private let label = UILabel()
private var topConstraint: NSLayoutConstraint?
private var heightConstraint: NSLayoutConstraint?
private var constraintsWhenVisible: [NSLayoutConstraint]?
override init(frame: CGRect) {
super.init(frame: CGRect())
label.translatesAutoresizingMaskIntoConstraints = false
addSubview(label)
let topConstraint = label.topAnchor.constraint(equalTo: topAnchor, constant: 10)
let constraintsWhenVisible = [
label.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 20),
label.trailingAnchor.constraint(equalTo: trailingAnchor),
topConstraint,
label.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -7)
]
NSLayoutConstraint.activate(constraintsWhenVisible)
self.topConstraint = topConstraint
//UIKit doesn't like headers with a height of 0
self.heightConstraint = heightAnchor.constraint(equalToConstant: 1)
self.constraintsWhenVisible = constraintsWhenVisible
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configure(type: HeaderType, shouldHide: Bool) {
backgroundColor = Colors.appWhite
label.backgroundColor = Colors.appWhite
label.textColor = UIColor(red: 141, green: 141, blue: 141)
label.font = Fonts.semibold(size: 15)!
label.text = type.title
label.isHidden = shouldHide
heightConstraint?.isActive = shouldHide
if shouldHide {
NSLayoutConstraint.deactivate(constraintsWhenVisible!)
} else {
NSLayoutConstraint.activate(constraintsWhenVisible!)
}
switch type {
case .hdWallet:
topConstraint?.constant = 0
case .keystoreWallet:
topConstraint?.constant = 17
case .watchedWallet:
topConstraint?.constant = 10
}
}
}

@ -51,6 +51,7 @@ class AppCoordinator: NSObject, Coordinator {
applyStyle()
resetToWelcomeScreen()
setupAssetDefinitionStoreCoordinator()
migrateToStoringRawPrivateKeysInKeychain()
if keystore.hasWallets {
showTransactions(for: keystore.recentlyUsedWallet ?? keystore.wallets.first!)
@ -61,6 +62,10 @@ class AppCoordinator: NSObject, Coordinator {
assetDefinitionStore.delegate = self
}
private func migrateToStoringRawPrivateKeysInKeychain() {
(try? LegacyFileBasedKeystore())?.migrateKeystoreFilesToRawPrivateKeysInKeychain()
}
/// Return true if handled
func handleOpen(url: URL) -> Bool {
if let assetDefinitionStoreCoordinator = assetDefinitionStoreCoordinator {
@ -108,7 +113,7 @@ class AppCoordinator: NSObject, Coordinator {
private func initializers() {
var paths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .allDomainsMask, true).compactMap { URL(fileURLWithPath: $0) }
paths.append(keystore.keystoreDirectory)
paths.append((try! LegacyFileBasedKeystore()).keystoreDirectory)
let initializers: [Initializer] = [
SkipBackupFilesInitializer(paths: paths),
@ -151,12 +156,12 @@ class AppCoordinator: NSObject, Coordinator {
addCoordinator(coordinator)
}
private func createInitialWallet() {
WalletCoordinator(config: config, keystore: keystore).createInitialWallet()
private func createInitialWalletIfMissing() {
WalletCoordinator(config: config, keystore: keystore).createInitialWalletIfMissing()
}
@discardableResult func handleUniversalLink(url: URL) -> Bool {
createInitialWallet()
createInitialWalletIfMissing()
closeWelcomeWindow()
//TODO refactor. Some of these should be moved into InCoordinator instead of reaching into its internals
guard let inCoordinator = self.inCoordinator else { return false }
@ -210,8 +215,8 @@ class AppCoordinator: NSObject, Coordinator {
//}
extension AppCoordinator: WelcomeViewControllerDelegate {
func didPressCreateWallet(in viewController: WelcomeViewController) {
showInitialWalletCoordinator(entryPoint: .createInstantWallet)
func didPressGettingStartedButton(in viewController: WelcomeViewController) {
showInitialWalletCoordinator(entryPoint: .addInitialWallet)
}
}
@ -222,9 +227,9 @@ extension AppCoordinator: InitialWalletCreationCoordinatorDelegate {
}
func didAddAccount(_ account: Wallet, in coordinator: InitialWalletCreationCoordinator) {
coordinator.navigationController.dismiss(animated: true, completion: nil)
removeCoordinator(coordinator)
showTransactions(for: account)
navigationController.dismiss(animated: true, completion: nil)
self.removeCoordinator(coordinator)
self.showTransactions(for: account)
}
}

@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "biometric-lock@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "keystore-introduction@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

@ -0,0 +1,12 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "successOverlay.pdf"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

@ -0,0 +1,12 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "toggle-password.pdf"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

@ -4,7 +4,6 @@ import Foundation
import UIKit
import BigInt
import QRCodeReaderViewController
import TrustKeystore
import RealmSwift
import WebKit
@ -135,7 +134,7 @@ final class DappBrowserCoordinator: NSObject, Coordinator {
navigationController.dismiss(animated: true, completion: nil)
}
private func executeTransaction(account: Account, action: DappAction, callbackID: Int, transaction: UnconfirmedTransaction, type: ConfirmType, server: RPCServer) {
private func executeTransaction(account: EthereumAccount, action: DappAction, callbackID: Int, transaction: UnconfirmedTransaction, type: ConfirmType, server: RPCServer) {
let configurator = TransactionConfigurator(
session: session,
account: account,
@ -201,7 +200,7 @@ final class DappBrowserCoordinator: NSObject, Coordinator {
browserViewController.goTo(url: url)
}
func signMessage(with type: SignMessageType, account: Account, callbackID: Int) {
func signMessage(with type: SignMessageType, account: EthereumAccount, callbackID: Int) {
let coordinator = SignMessageCoordinator(
navigationController: navigationController,
keystore: keystore,

@ -3,6 +3,7 @@
import Foundation
//TODO remove all dependencies
import TrustKeystore
import TrustWalletCore
///Use an enum as a namespace until Swift has proper namespaces
enum AlphaWallet {}
@ -31,6 +32,11 @@ extension AlphaWallet {
self = .ethereumAddress(eip55String: address.eip55String)
}
init(fromPrivateKey privateKey: Data) {
let publicKey = Secp256k1.shared.pubicKey(from: privateKey)
self = Address.deriveEthereumAddress(fromPublicKey: publicKey)
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: Key.self)
let address = try container.decode(String.self, forKey: .ethereumAddress)
@ -82,6 +88,16 @@ extension AlphaWallet.Address: CustomDebugStringConvertible {
}
}
extension AlphaWallet.Address {
private static func deriveEthereumAddress(fromPublicKey publicKey: Data) -> AlphaWallet.Address {
precondition(publicKey.count == 65, "Expect 64-byte public key")
precondition(publicKey[0] == 4, "Invalid public key")
let sha3 = publicKey[1...].sha3(.keccak256)
let eip55String = sha3[12..<32].hex()
return AlphaWallet.Address(string: eip55String)!
}
}
extension AlphaWallet.Address {
var isLegacy875Contract: Bool {
let contractString = eip55String

@ -1,447 +0,0 @@
// Copyright SIX DAY LLC. All rights reserved.
import BigInt
import Foundation
import Result
import KeychainSwift
import CryptoSwift
import TrustKeystore
enum EtherKeystoreError: LocalizedError {
case protectionDisabled
}
open class EtherKeystore: Keystore {
private struct Keys {
static let recentlyUsedAddress: String = "recentlyUsedAddress"
static let watchAddresses = "watchAddresses"
}
private let keychain: KeychainSwift
private let datadir = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]
private let keyStore: KeyStore
private let defaultKeychainAccess: KeychainSwiftAccessOptions = .accessibleWhenUnlockedThisDeviceOnly
private let userDefaults: UserDefaults
let keystoreDirectory: URL
public init(
keychain: KeychainSwift = KeychainSwift(keyPrefix: Constants.keychainKeyPrefix),
keyStoreSubfolder: String = "/keystore",
userDefaults: UserDefaults = UserDefaults.standard
) throws {
if !UIApplication.shared.isProtectedDataAvailable {
throw EtherKeystoreError.protectionDisabled
}
self.keystoreDirectory = URL(fileURLWithPath: datadir + keyStoreSubfolder)
self.keychain = keychain
self.keychain.synchronizable = false
self.keyStore = try KeyStore(keydir: keystoreDirectory)
self.userDefaults = userDefaults
}
var hasWallets: Bool {
return !wallets.isEmpty
}
private var watchAddresses: [String] {
set {
let data = NSKeyedArchiver.archivedData(withRootObject: newValue)
return userDefaults.set(data, forKey: Keys.watchAddresses)
}
get {
guard let data = userDefaults.data(forKey: Keys.watchAddresses) else { return [] }
return NSKeyedUnarchiver.unarchiveObject(with: data) as? [String] ?? []
}
}
var recentlyUsedWallet: Wallet? {
set {
keychain.set(newValue?.address.eip55String ?? "", forKey: Keys.recentlyUsedAddress, withAccess: defaultKeychainAccess)
}
get {
guard let address = keychain.get(Keys.recentlyUsedAddress) else { return nil }
return wallets.filter { $0.address.sameContract(as: address) }.first
}
}
static var current: Wallet? {
do {
return try EtherKeystore().recentlyUsedWallet
} catch {
return .none
}
}
// Async
@available(iOS 10.0, *)
func createAccount(with password: String, completion: @escaping (Result<Account, KeystoreError>) -> Void) {
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
guard let strongSelf = self else { return }
let account = strongSelf.createAccount(password: password)
DispatchQueue.main.async {
completion(.success(account))
}
}
}
func importWallet(type: ImportType, completion: @escaping (Result<Wallet, KeystoreError>) -> Void) {
let newPassword = PasswordGenerator.generateRandom()
switch type {
case .keystore(let string, let password):
importKeystore(
value: string,
password: password,
newPassword: newPassword
) { result in
switch result {
case .success(let account):
completion(.success(Wallet(type: .real(account))))
case .failure(let error):
completion(.failure(error))
}
}
case .privateKey(let privateKey):
keystore(for: privateKey, password: newPassword) { [weak self] result in
guard let strongSelf = self else { return }
switch result {
case .success(let value):
strongSelf.importKeystore(
value: value,
password: newPassword,
newPassword: newPassword
) { result in
switch result {
case .success(let account):
completion(.success(Wallet(type: .real(account))))
case .failure(let error):
completion(.failure(error))
}
}
case .failure(let error):
completion(.failure(error))
}
}
case .mnemonic:
let key = ""
// TODO: Implement it
keystore(for: key, password: newPassword) { [weak self] result in
guard let strongSelf = self else { return }
switch result {
case .success(let value):
strongSelf.importKeystore(
value: value,
password: newPassword,
newPassword: newPassword
) { result in
switch result {
case .success(let account):
completion(.success(Wallet(type: .real(account))))
case .failure(let error):
completion(.failure(error))
}
}
case .failure(let error):
completion(.failure(error))
}
}
case .watch(let address):
guard !watchAddresses.contains(where: { address.sameContract(as: $0) }) else {
completion(.failure(.duplicateAccount))
return
}
watchAddresses = [watchAddresses, [address.eip55String]].flatMap { $0 }
completion(.success(Wallet(type: .watch(address))))
}
}
func keystore(for privateKey: String, password: String, completion: @escaping (Result<String, KeystoreError>) -> Void) {
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
guard let strongSelf = self else { return }
let keystore = strongSelf.convertPrivateKeyToKeystoreFile(
privateKey: privateKey,
passphrase: password
)
DispatchQueue.main.async {
switch keystore {
case .success(let result):
completion(.success(result.jsonString ?? ""))
case .failure(let error):
completion(.failure(error))
}
}
}
}
func importKeystore(value: String, password: String, newPassword: String, completion: @escaping (Result<Account, KeystoreError>) -> Void) {
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
guard let strongSelf = self else { return }
let result = strongSelf.importKeystore(value: value, password: password, newPassword: newPassword)
DispatchQueue.main.async {
switch result {
case .success(let account):
completion(.success(account))
case .failure(let error):
completion(.failure(error))
}
}
}
}
func createAccount(password: String) -> Account {
let account = try! keyStore.createAccount(password: password)
let _ = setPassword(password, for: account)
return account
}
func importKeystore(value: String, password: String, newPassword: String) -> Result<Account, KeystoreError> {
guard let data = value.data(using: .utf8) else {
return (.failure(.failedToParseJSON))
}
do {
let account = try keyStore.import(json: data, password: password, newPassword: newPassword)
let _ = setPassword(newPassword, for: account)
return .success(account)
} catch {
if case KeyStore.Error.accountAlreadyExists = error {
return .failure(.duplicateAccount)
} else {
return .failure(.failedToImport(error))
}
}
}
var wallets: [Wallet] {
let addresses = watchAddresses.compactMap { AlphaWallet.Address(string: $0) }
return [
keyStore.accounts.map { Wallet(type: .real($0)) },
addresses.map { Wallet(type: .watch($0)) },
].flatMap { $0 }
}
func export(account: Account, password: String, newPassword: String) -> Result<String, KeystoreError> {
let result = exportData(account: account, password: password, newPassword: newPassword)
switch result {
case .success(let data):
let string = String(data: data, encoding: .utf8) ?? ""
return .success(string)
case .failure(let error):
return .failure(error)
}
}
func export(account: Account, password: String, newPassword: String, completion: @escaping (Result<String, KeystoreError>) -> Void) {
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
guard let strongSelf = self else { return }
let result = strongSelf.export(account: account, password: password, newPassword: newPassword)
DispatchQueue.main.async {
completion(result)
}
}
}
func exportData(account: Account, password: String, newPassword: String) -> Result<Data, KeystoreError> {
guard let account = getAccount(for: account.address) else {
return .failure(.accountNotFound)
}
do {
let data = try keyStore.export(account: account, password: password, newPassword: newPassword)
return (.success(data))
} catch {
return (.failure(.failedToDecryptKey))
}
}
func exportPrivateKey(account: Account) -> Result<Data, KeystoreError> {
guard let password = getPassword(for: account) else {
return .failure(KeystoreError.accountNotFound)
}
do {
let privateKey = try keyStore.exportPrivateKey(account: account, password: password)
return .success(privateKey)
} catch {
return .failure(KeystoreError.failedToExportPrivateKey)
}
}
@discardableResult func delete(wallet: Wallet) -> Result<Void, KeystoreError> {
switch wallet.type {
case .real(let account):
guard let account = getAccount(for: account.address) else {
return .failure(.accountNotFound)
}
guard let password = getPassword(for: account) else {
return .failure(.failedToDeleteAccount)
}
do {
try keyStore.delete(account: account, password: password)
return .success(())
} catch {
return .failure(.failedToDeleteAccount)
}
case .watch(let address):
watchAddresses = watchAddresses.filter { $0 != address.eip55String }
return .success(())
}
}
func delete(wallet: Wallet, completion: @escaping (Result<Void, KeystoreError>) -> Void) {
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
guard let strongSelf = self else { return }
let result = strongSelf.delete(wallet: wallet)
DispatchQueue.main.async {
completion(result)
}
}
}
func updateAccount(account: Account, password: String, newPassword: String) -> Result<Void, KeystoreError> {
guard let account = getAccount(for: account.address) else {
return .failure(.accountNotFound)
}
do {
try keyStore.update(account: account, password: password, newPassword: newPassword)
return .success(())
} catch {
return .failure(.failedToUpdatePassword)
}
}
func signPersonalMessage(_ message: Data, for account: Account) -> Result<Data, KeystoreError> {
let prefix = "\u{19}Ethereum Signed Message:\n\(message.count)".data(using: .utf8)!
return signMessage(prefix + message, for: account)
}
func signHash(_ hash: Data, for account: Account) -> Result<Data, KeystoreError> {
guard
let password = getPassword(for: account) else {
return .failure(KeystoreError.failedToSignMessage)
}
do {
var data = try keyStore.signHash(hash, account: account, password: password)
// TODO: Make it configurable, instead of overriding last byte.
data[64] += 27
return .success(data)
} catch {
return .failure(KeystoreError.failedToSignMessage)
}
}
func signTypedMessage(_ datas: [EthTypedData], for account: Account) -> Result<Data, KeystoreError> {
let schemas = datas.map { $0.schemaData }.reduce(Data(), { $0 + $1 }).sha3(.keccak256)
let values = datas.map { $0.typedData }.reduce(Data(), { $0 + $1 }).sha3(.keccak256)
let combined = (schemas + values).sha3(.keccak256)
return signHash(combined, for: account)
}
func signMessage(_ message: Data, for account: Account) -> Result<Data, KeystoreError> {
return signHash(message.sha3(.keccak256), for: account)
}
func signMessageBulk(_ data: [Data], for account: Account) -> Result<[Data], KeystoreError> {
guard
let password = getPassword(for: account) else {
return .failure(KeystoreError.failedToSignMessage)
}
do {
var messageHashes = [Data]()
for i in 0...data.count - 1 {
let hash = data[i].sha3(.keccak256)
messageHashes.append(hash)
}
var data = try keyStore.signHashes(messageHashes, account: account, password: password)
// TODO: Make it configurable, instead of overriding last byte.
for i in 0...data.count - 1 {
data[i][64] += 27
}
return .success(data)
} catch {
return .failure(KeystoreError.failedToSignMessage)
}
}
public func signMessageData(_ message: Data?, for account: Account) -> Result<Data, KeystoreError> {
guard
let hash = message?.sha3(.keccak256),
let password = getPassword(for: account) else {
return .failure(KeystoreError.failedToSignMessage)
}
do {
var data = try keyStore.signHash(hash, account: account, password: password)
data[64] += 27
return .success(data)
} catch {
return .failure(KeystoreError.failedToSignMessage)
}
}
func signTransaction(_ transaction: UnsignedTransaction) -> Result<Data, KeystoreError> {
guard let account = keyStore.account(for: transaction.account.address) else {
return .failure(.failedToSignTransaction)
}
guard let password = getPassword(for: account) else {
return .failure(.failedToSignTransaction)
}
let signer: Signer
if transaction.server.chainID == 0 {
signer = HomesteadSigner()
} else {
signer = EIP155Signer(server: transaction.server)
}
do {
let hash = signer.hash(transaction: transaction)
let signature = try keyStore.signHash(hash, account: account, password: password)
let (r, s, v) = signer.values(transaction: transaction, signature: signature)
let data = RLP.encode([
transaction.nonce,
transaction.gasPrice,
transaction.gasLimit,
transaction.to?.data ?? Data(),
transaction.value,
transaction.data,
v, r, s,
])!
return .success(data)
} catch {
return .failure(.failedToSignTransaction)
}
}
func getPassword(for account: Account) -> String? {
return keychain.get(account.address.eip55String.lowercased())
}
@discardableResult
func setPassword(_ password: String, for account: Account) -> Bool {
return keychain.set(password, forKey: account.address.eip55String.lowercased(), withAccess: defaultKeychainAccess)
}
func getAccount(for address: AlphaWallet.Address) -> Account? {
return getAccount(for: .init(address: address))
}
func getAccount(for address: Address) -> Account? {
return keyStore.account(for: address)
}
func convertPrivateKeyToKeystoreFile(privateKey: String, passphrase: String) -> Result<[String: Any], KeystoreError> {
guard let data = Data(hexString: privateKey) else {
return .failure(KeystoreError.failedToImportPrivateKey)
}
do {
let key = try KeystoreKey(password: passphrase, key: data)
let data = try JSONEncoder().encode(key)
let dict = try JSONSerialization.jsonObject(with: data, options: []) as! [String: Any]
return .success(dict)
} catch {
return .failure(KeystoreError.failedToImportPrivateKey)
}
}
}

@ -4,7 +4,7 @@ import Foundation
enum ImportType {
case keystore(string: String, password: String)
case privateKey(privateKey: String)
case privateKey(privateKey: Data)
case mnemonic(words: [String], password: String)
case watch(address: AlphaWallet.Address)
}

@ -8,13 +8,15 @@ public enum KeystoreError: LocalizedError {
case failedToImport(Error)
case duplicateAccount
case failedToSignTransaction
case failedToUpdatePassword
case failedToCreateWallet
case failedToImportPrivateKey
case failedToParseJSON
case accountNotFound
case failedToSignMessage
case failedToExportPrivateKey
case failedToExportSeed
case accountMayNeedImportingAgainOrEnablePasscode
case userCancelled
public var errorDescription: String? {
switch self {
@ -28,8 +30,6 @@ public enum KeystoreError: LocalizedError {
return R.string.localizable.accountsDeleteErrorDuplicateAccount()
case .failedToSignTransaction:
return R.string.localizable.accountsDeleteErrorFailedToSignTransaction()
case .failedToUpdatePassword:
return R.string.localizable.accountsDeleteErrorFailedToUpdatePassword()
case .failedToCreateWallet:
return R.string.localizable.accountsDeleteErrorFailedToCreateWallet()
case .failedToImportPrivateKey:
@ -42,6 +42,12 @@ public enum KeystoreError: LocalizedError {
return R.string.localizable.accountsDeleteErrorFailedToSignMessage()
case .failedToExportPrivateKey:
return R.string.localizable.accountsDeleteErrorFailedToExportPrivateKey()
case .failedToExportSeed:
return R.string.localizable.accountsDeleteErrorFailedToExportSeed()
case .accountMayNeedImportingAgainOrEnablePasscode:
return R.string.localizable.keystoreAccessKeyNeedImportOrPasscode()
case .userCancelled:
return R.string.localizable.keystoreAccessKeyCancelled()
}
}
}

@ -1,33 +0,0 @@
// Copyright SIX DAY LLC. All rights reserved.
import Foundation
import Result
import TrustKeystore
protocol Keystore {
var hasWallets: Bool { get }
var wallets: [Wallet] { get }
var keystoreDirectory: URL { get }
var recentlyUsedWallet: Wallet? { get set }
static var current: Wallet? { get }
@available(iOS 10.0, *)
func createAccount(with password: String, completion: @escaping (Result<Account, KeystoreError>) -> Void)
func importWallet(type: ImportType, completion: @escaping (Result<Wallet, KeystoreError>) -> Void)
func keystore(for privateKey: String, password: String, completion: @escaping (Result<String, KeystoreError>) -> Void)
func importKeystore(value: String, password: String, newPassword: String, completion: @escaping (Result<Account, KeystoreError>) -> Void)
func createAccount(password: String) -> Account
func importKeystore(value: String, password: String, newPassword: String) -> Result<Account, KeystoreError>
func export(account: Account, password: String, newPassword: String) -> Result<String, KeystoreError>
func export(account: Account, password: String, newPassword: String, completion: @escaping (Result<String, KeystoreError>) -> Void)
func exportData(account: Account, password: String, newPassword: String) -> Result<Data, KeystoreError>
@discardableResult func delete(wallet: Wallet) -> Result<Void, KeystoreError>
func delete(wallet: Wallet, completion: @escaping (Result<Void, KeystoreError>) -> Void)
func updateAccount(account: Account, password: String, newPassword: String) -> Result<Void, KeystoreError>
func signPersonalMessage(_ data: Data, for account: Account) -> Result<Data, KeystoreError>
func signTypedMessage(_ datas: [EthTypedData], for account: Account) -> Result<Data, KeystoreError>
func signMessage(_ data: Data, for account: Account) -> Result<Data, KeystoreError>
func signHash(_ data: Data, for account: Account) -> Result<Data, KeystoreError>
func signTransaction(_ signTransaction: UnsignedTransaction) -> Result<Data, KeystoreError>
func getPassword(for account: Account) -> String?
func convertPrivateKeyToKeystoreFile(privateKey: String, passphrase: String) -> Result<[String: Any], KeystoreError>
}

@ -1,10 +1,9 @@
// Copyright SIX DAY LLC. All rights reserved.
import Foundation
import TrustKeystore
enum WalletType: Equatable {
case real(Account)
case real(EthereumAccount)
case watch(AlphaWallet.Address)
}
@ -14,7 +13,7 @@ struct Wallet: Equatable {
var address: AlphaWallet.Address {
switch type {
case .real(let account):
return AlphaWallet.Address(address: account.address)
return account.address
case .watch(let address):
return address
}

@ -3,27 +3,21 @@
import Foundation
import UIKit
import Result
import TrustKeystore
import Result
protocol BackupCoordinatorDelegate: class {
func didCancel(coordinator: BackupCoordinator)
func didFinish(account: Account, in coordinator: BackupCoordinator)
func didFinish(account: EthereumAccount, in coordinator: BackupCoordinator)
}
class BackupCoordinator: Coordinator {
private let keystore: Keystore
private let account: Account
private let account: EthereumAccount
let navigationController: UINavigationController
weak var delegate: BackupCoordinatorDelegate?
var coordinators: [Coordinator] = []
init(
navigationController: UINavigationController,
keystore: Keystore,
account: Account
) {
init(navigationController: UINavigationController, keystore: Keystore, account: EthereumAccount) {
self.navigationController = navigationController
self.keystore = keystore
self.account = account
@ -33,7 +27,7 @@ class BackupCoordinator: Coordinator {
export(for: account)
}
func finish(result: Result<Bool, AnyError>) {
private func finish(result: Result<Bool, AnyError>) {
switch result {
case .success:
delegate?.didFinish(account: account, in: self)
@ -42,11 +36,11 @@ class BackupCoordinator: Coordinator {
}
}
func presentActivityViewController(for account: Account, password: String, newPassword: String, completion: @escaping (Result<Bool, AnyError>) -> Void) {
private func presentActivityViewController(for account: EthereumAccount, newPassword: String, completion: @escaping (Result<Bool, AnyError>) -> Void) {
navigationController.displayLoading(
text: R.string.localizable.exportPresentBackupOptionsLabelTitle()
)
keystore.export(account: account, password: password, newPassword: newPassword) { [weak self] result in
keystore.exportRawPrivateKeyForNonHdWalletForBackup(forAccount: account, newPassword: newPassword) { [weak self] result in
guard let strongSelf = self else { return }
strongSelf.handleExport(result: result, completion: completion)
}
@ -67,14 +61,17 @@ class BackupCoordinator: Coordinator {
applicationActivities: nil
)
activityViewController.completionWithItemsHandler = { _, result, _, error in
do { try FileManager.default.removeItem(at: url)
} catch { }
do {
try FileManager.default.removeItem(at: url)
} catch {
//no-op
}
completion(.success(result))
}
activityViewController.popoverPresentationController?.sourceView = navigationController.view
activityViewController.popoverPresentationController?.sourceRect = navigationController.view.centerRect
navigationController.present(activityViewController, animated: true) { [unowned self] in
self.navigationController.hideLoading()
navigationController.present(activityViewController, animated: true) { [weak self] in
self?.navigationController.hideLoading()
}
case .failure(let error):
navigationController.hideLoading()
@ -82,20 +79,94 @@ class BackupCoordinator: Coordinator {
}
}
func presentShareActivity(for account: Account, password: String, newPassword: String) {
presentActivityViewController(for: account, password: password, newPassword: newPassword) { [weak self] result in
private func presentShareActivity(for account: EthereumAccount, newPassword: String ) {
presentActivityViewController(for: account, newPassword: newPassword) { [weak self] result in
guard let strongSelf = self else { return }
strongSelf.finish(result: result)
switch result {
case .success(let isBackedUp):
if isBackedUp {
self?.promptElevateSecurityOrEnd()
}
case .failure:
break
}
}
}
func export(for account: Account) {
let coordinator = EnterPasswordCoordinator(account: account)
private func promptElevateSecurityOrEnd() {
guard keystore.isUserPresenceCheckPossible else { return cleanUpAfterBackupAndNotPromptedToElevateSecurity() }
guard !keystore.isProtectedByUserPresence(account: account) else { return cleanUpAfterBackupAndNotPromptedToElevateSecurity() }
let coordinator = ElevateWalletSecurityCoordinator(navigationController: navigationController, keystore: keystore, account: account)
coordinator.delegate = self
coordinator.start()
navigationController.present(coordinator.navigationController, animated: true, completion: nil)
addCoordinator(coordinator)
}
private func export(for account: EthereumAccount) {
if keystore.isHdWallet(account: account) {
let coordinator = BackupSeedPhraseCoordinator(navigationController: navigationController, keystore: keystore, account: account)
coordinator.delegate = self
coordinator.start()
addCoordinator(coordinator)
} else {
let coordinator = EnterPasswordCoordinator(navigationController: navigationController, account: account)
coordinator.delegate = self
coordinator.start()
addCoordinator(coordinator)
}
}
private func cleanUpAfterBackupAndPromptedToElevateSecurity() {
let backupSeedPhraseCoordinator = coordinators.first { $0 is BackupSeedPhraseCoordinator } as? BackupSeedPhraseCoordinator
defer { backupSeedPhraseCoordinator.flatMap { removeCoordinator($0) } }
let elevateWalletSecurityCoordinator = coordinators.first { $0 is ElevateWalletSecurityCoordinator } as? ElevateWalletSecurityCoordinator
defer { elevateWalletSecurityCoordinator.flatMap { removeCoordinator($0) } }
let verifySeedPhraseCoordinator = coordinators.first { $0 is VerifySeedPhraseCoordinator } as? VerifySeedPhraseCoordinator
defer { verifySeedPhraseCoordinator.flatMap { removeCoordinator($0) } }
let enterPasswordCoordinator = coordinators.first { $0 is EnterPasswordCoordinator } as? EnterPasswordCoordinator
defer { enterPasswordCoordinator.flatMap { removeCoordinator($0) } }
enterPasswordCoordinator?.end()
backupSeedPhraseCoordinator?.end()
verifySeedPhraseCoordinator?.end()
elevateWalletSecurityCoordinator?.end()
//Must only call endUserInterface() on the coordinators managing the bottom-most view controller
//Only one of these 2 coordinators will be nil
backupSeedPhraseCoordinator?.endUserInterface(animated: true)
enterPasswordCoordinator?.endUserInterface(animated: true)
finish(result: .success(true))
//Bit of delay to wait for the UI animation to almost finish
DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
SuccessOverlayView.show()
}
}
private func cleanUpAfterBackupAndNotPromptedToElevateSecurity() {
let backupSeedPhraseCoordinator = coordinators.first { $0 is BackupSeedPhraseCoordinator } as? BackupSeedPhraseCoordinator
defer { backupSeedPhraseCoordinator.flatMap { removeCoordinator($0) } }
let verifySeedPhraseCoordinator = coordinators.first { $0 is VerifySeedPhraseCoordinator } as? VerifySeedPhraseCoordinator
defer { verifySeedPhraseCoordinator.flatMap { removeCoordinator($0) } }
let enterPasswordCoordinator = coordinators.first { $0 is EnterPasswordCoordinator } as? EnterPasswordCoordinator
defer { enterPasswordCoordinator.flatMap { removeCoordinator($0) } }
enterPasswordCoordinator?.end()
backupSeedPhraseCoordinator?.end()
verifySeedPhraseCoordinator?.end()
//Must only call endUserInterface() on the coordinators managing the bottom-most view controller
//Only one of these 2 coordinators will be nil
backupSeedPhraseCoordinator?.endUserInterface(animated: true)
enterPasswordCoordinator?.endUserInterface(animated: true)
finish(result: .success(true))
//Bit of delay to wait for ttoree UI animation to almost finish
DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
SuccessOverlayView.show()
}
}
}
extension BackupCoordinator: EnterPasswordCoordinatorDelegate {
@ -104,14 +175,36 @@ extension BackupCoordinator: EnterPasswordCoordinatorDelegate {
removeCoordinator(coordinator)
}
func didEnterPassword(password: String, account: Account, in coordinator: EnterPasswordCoordinator) {
coordinator.navigationController.dismiss(animated: true) { [unowned self] in
if let currentPassword = self.keystore.getPassword(for: account) {
self.presentShareActivity(for: account, password: currentPassword, newPassword: password)
} else {
self.presentShareActivity(for: account, password: password, newPassword: password)
}
}
func didEnterPassword(password: String, account: EthereumAccount, in coordinator: EnterPasswordCoordinator) {
presentShareActivity(for: account, newPassword: password)
}
}
extension BackupCoordinator: BackupSeedPhraseCoordinatorDelegate {
func didTapTestSeedPhrase(forAccount account: EthereumAccount, inCoordinator coordinator: BackupSeedPhraseCoordinator) {
let coordinator = VerifySeedPhraseCoordinator(navigationController: navigationController, keystore: keystore, account: account)
coordinator.delegate = self
coordinator.start()
addCoordinator(coordinator)
}
func didClose(forAccount account: EthereumAccount, inCoordinator coordinator: BackupSeedPhraseCoordinator) {
removeCoordinator(coordinator)
}
}
extension BackupCoordinator: VerifySeedPhraseCoordinatorDelegate {
func didVerifySeedPhraseSuccessfully(forAccount account: EthereumAccount, inCoordinator coordinator: VerifySeedPhraseCoordinator) {
promptElevateSecurityOrEnd()
}
}
extension BackupCoordinator: ElevateWalletSecurityCoordinatorDelegate {
func didLockWalletSuccessfully(forAccount account: EthereumAccount, inCoordinator coordinator: ElevateWalletSecurityCoordinator) {
cleanUpAfterBackupAndPromptedToElevateSecurity()
}
func didCancelLock(forAccount account: EthereumAccount, inCoordinator coordinator: ElevateWalletSecurityCoordinator) {
cleanUpAfterBackupAndPromptedToElevateSecurity()
}
}

@ -1,64 +1,381 @@
// Copyright © 2018 Stormbird PTE. LTD.
// Copyright © 2019 Stormbird PTE. LTD.
import Foundation
import UIKit
import BigInt
protocol PromptBackupCoordinatorDelegate: class {
func viewControllerForPresenting(in coordinator: PromptBackupCoordinator) -> UIViewController?
func didFinish(in coordinator: PromptBackupCoordinator)
protocol PromptBackupCoordinatorProminentPromptDelegate: class {
var viewControllerToShowBackupLaterAlert: UIViewController { get }
func updatePrompt(inCoordinator coordinator: PromptBackupCoordinator)
}
protocol PromptBackupCoordinatorSubtlePromptDelegate: class {
var viewControllerToShowBackupLaterAlert: UIViewController { get }
func updatePrompt(inCoordinator coordinator: PromptBackupCoordinator)
}
///We allow user to switch wallets, so it's important to know which wallet we are prompting for. It might not be the current wallet
class PromptBackupCoordinator: Coordinator {
private static let secondsInAMonth = TimeInterval(30*24*60*60)
private static let thresholdNativeCryptoCurrencyAmountInUsdToPromptBackup = Double(200)
private let documentsDirectory = URL(fileURLWithPath: NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0])
private let filename = "backupState.json"
lazy private var fileUrl = documentsDirectory.appendingPathComponent(filename)
private let keystore: Keystore
private var walletAddress: AlphaWallet.Address
private let wallet: Wallet
private let config: Config
//TODO this should be the total of mainnets instead of just Ethereum mainnet
private var nativeCryptoCurrencyDollarValueInUsd: Double = 0
var prominentPromptView: UIView?
var subtlePromptView: UIView?
var coordinators: [Coordinator] = []
weak var delegate: PromptBackupCoordinatorDelegate?
weak var prominentPromptDelegate: PromptBackupCoordinatorProminentPromptDelegate?
weak var subtlePromptDelegate: PromptBackupCoordinatorSubtlePromptDelegate?
init(keystore: Keystore, walletAddress: AlphaWallet.Address, config: Config) {
init(keystore: Keystore, wallet: Wallet, config: Config) {
self.keystore = keystore
self.walletAddress = walletAddress
self.wallet = wallet
self.config = config
}
func start() {
guard let vc = delegate?.viewControllerForPresenting(in: self) else {
finish()
return
migrateOldData()
guard canBackupWallet else { return }
setUpAndPromptIfWalletHasNotBeenPromptedBefore()
showCreateBackupAfterIntervalPrompt()
showHideCurrentPrompt()
}
private func setUpAndPromptIfWalletHasNotBeenPromptedBefore() {
guard !hasState else { return }
updateState { state in
state.backupState[wallet.address] = .init(shownNativeCryptoCurrencyReceivedPrompt: false, timeToShowIntervalPassedPrompt: nil, shownNativeCryptoCurrencyDollarValueExceedThresholdPrompt: false, lastBackedUpTime: nil, isImported: false)
}
let coordinator = WalletCoordinator(config: config, keystore: keystore)
coordinator.delegate = self
let proceed = coordinator.start(.backupWallet(address: walletAddress))
guard proceed else {
finish()
return
showCreateBackupAfterWalletCreationPrompt()
}
func showHideCurrentPrompt() {
if let prompt = readState()?.prompt[wallet.address] {
switch prompt {
case .newWallet:
createBackupAfterWalletCreationView()
case .intervalPassed:
createBackupAfterIntervalView()
case .nativeCryptoCurrencyDollarValueExceededThreshold:
createBackupAfterExceedingThresholdView()
case .receivedNativeCryptoCurrency(let nativeCryptoCurrency):
createBackupAfterReceiveNativeCryptoCurrencyView(nativeCryptoCurrency: nativeCryptoCurrency)
}
} else {
removeBackupView()
}
vc.present(coordinator.navigationController, animated: true, completion: nil)
addCoordinator(coordinator)
}
func finish() {
delegate?.didFinish(in: self)
private func informDelegatesPromptHasChanged() {
subtlePromptDelegate?.updatePrompt(inCoordinator: self)
prominentPromptDelegate?.updatePrompt(inCoordinator: self)
}
private func migrateOldData() {
guard !FileManager.default.fileExists(atPath: fileUrl.path) else { return }
let addressesAlreadyPromptedForBackup = config.oldWalletAddressesAlreadyPromptedForBackUp
var walletsBackupState: WalletsBackupState = .init()
for eachAlreadyBackedUp in addressesAlreadyPromptedForBackup {
guard let walletAddress = AlphaWallet.Address(string: eachAlreadyBackedUp) else { continue }
walletsBackupState.prompt[walletAddress] = nil
//We'll just take the last backed up time as when this migration runs
walletsBackupState.backupState[walletAddress] = .init(shownNativeCryptoCurrencyReceivedPrompt: true, timeToShowIntervalPassedPrompt: nil, shownNativeCryptoCurrencyDollarValueExceedThresholdPrompt: true, lastBackedUpTime: Date(), isImported: false)
}
writeState(walletsBackupState)
}
private func createBackupViewImpl(viewModel: PromptBackupWalletViewViewModel) -> UIView {
let view = PromptBackupWalletView(viewModel: viewModel)
view.delegate = self
view.configure()
return view
}
//TODO not the best way to watch Ether balance
func listenToNativeCryptoCurrencyBalance(withTokenCollection tokenCollection: TokenCollection) {
tokenCollection.subscribe { [weak self] result in
guard let strongSelf = self else { return }
switch result {
case .success(let viewModel):
if let nativeCryptoCurrencyToken = viewModel.nativeCryptoCurrencyToken(forServer: .main) {
let dollarValue = viewModel.amount(for: nativeCryptoCurrencyToken)
if !dollarValue.isZero {
self?.showCreateBackupAfterExceedThresholdPrompt(valueInUsd: dollarValue)
}
}
case .failure(let error):
break
}
}
}
//MARK: Update UI
private func createBackupAfterWalletCreationView() {
let view = createBackupViewImpl(viewModel: PromptBackupWalletAfterWalletCreationViewViewModel(walletAddress: wallet.address))
prominentPromptView = nil
subtlePromptView = view
informDelegatesPromptHasChanged()
}
private func createBackupAfterReceiveNativeCryptoCurrencyView(nativeCryptoCurrency: BigInt) {
let view = createBackupViewImpl(viewModel: PromptBackupWalletAfterReceivingNativeCryptoCurrencyViewViewModel(walletAddress: wallet.address, nativeCryptoCurrency: nativeCryptoCurrency))
prominentPromptView = view
subtlePromptView = nil
informDelegatesPromptHasChanged()
}
private func createBackupAfterIntervalView() {
let view = createBackupViewImpl(viewModel: PromptBackupWalletAfterIntervalViewViewModel(walletAddress: wallet.address))
prominentPromptView = view
subtlePromptView = nil
informDelegatesPromptHasChanged()
}
private func createBackupAfterExceedingThresholdView() {
let view = createBackupViewImpl(viewModel: PromptBackupWalletAfterExceedingThresholdViewViewModel(walletAddress: wallet.address, dollarValueInUsd: nativeCryptoCurrencyDollarValueInUsd))
prominentPromptView = view
subtlePromptView = nil
informDelegatesPromptHasChanged()
}
private func removeBackupView() {
prominentPromptView = nil
subtlePromptView = nil
informDelegatesPromptHasChanged()
}
//MARK: Set current prompt and state
private func showCreateBackupAfterWalletCreationPrompt() {
guard canBackupWallet else { return }
guard !isBackedUp else { return }
guard !isImported else { return }
updateState { state in
state.prompt[wallet.address] = .newWallet
writeState(state)
}
showHideCurrentPrompt()
}
func showCreateBackupAfterReceiveNativeCryptoCurrencyPrompt(nativeCryptoCurrency: BigInt) {
guard canBackupWallet else { return }
guard !isBackedUp else { return }
guard !isImported else { return }
guard !hasShownNativeCryptoCurrencyReceivedPrompt else { return }
updateState { state in
state.prompt[wallet.address] = .receivedNativeCryptoCurrency(nativeCryptoCurrency)
state.backupState[wallet.address]?.shownNativeCryptoCurrencyReceivedPrompt = true
writeState(state)
}
showHideCurrentPrompt()
}
private func showCreateBackupAfterIntervalPrompt() {
guard canBackupWallet else { return }
guard !isBackedUp else { return }
guard !isImported else { return }
guard let time = timeToShowIntervalPassedPrompt else { return }
guard time.isEarlierThan(date: .init()) else { return }
updateState { state in
state.prompt[wallet.address] = .intervalPassed
state.backupState[wallet.address]?.timeToShowIntervalPassedPrompt = nil
writeState(state)
}
showHideCurrentPrompt()
}
private func showCreateBackupAfterExceedThresholdPrompt(valueInUsd: Double) {
nativeCryptoCurrencyDollarValueInUsd = valueInUsd
guard canBackupWallet else { return }
guard !isBackedUp else { return }
guard !isImported else { return }
let hasExceededThreshold = valueInUsd >= PromptBackupCoordinator.thresholdNativeCryptoCurrencyAmountInUsdToPromptBackup
let toShow: Bool
if isShowingExceededThresholdPrompt {
if hasExceededThreshold {
toShow = true
} else {
toShow = false
}
} else {
guard !hasShownExceededThresholdPrompt else { return }
guard hasExceededThreshold else { return }
toShow = true
}
if toShow {
updateState { state in
state.prompt[wallet.address] = .nativeCryptoCurrencyDollarValueExceededThreshold
state.backupState[wallet.address]?.shownNativeCryptoCurrencyDollarValueExceedThresholdPrompt = true
writeState(state)
}
showHideCurrentPrompt()
} else {
updateState { state in
state.prompt[wallet.address] = nil
writeState(state)
}
showHideCurrentPrompt()
}
}
func markBackupDone() {
guard canBackupWallet else { return }
updateState { state in
state.prompt[wallet.address] = nil
state.backupState[wallet.address]?.lastBackedUpTime = Date()
writeState(state)
}
}
private func remindLater() {
guard canBackupWallet else { return }
guard !isBackedUp else { return }
guard !isImported else { return }
updateState { state in
state.prompt[wallet.address] = nil
state.backupState[wallet.address]?.timeToShowIntervalPassedPrompt = Date(timeIntervalSinceNow: PromptBackupCoordinator.secondsInAMonth)
writeState(state)
}
}
func markWalletAsImported() {
updateState { state in
state.prompt[wallet.address] = nil
if let backupState = state.backupState[wallet.address] {
state.backupState[wallet.address]?.isImported = true
} else {
state.backupState[wallet.address] = .init(shownNativeCryptoCurrencyReceivedPrompt: false, timeToShowIntervalPassedPrompt: nil, shownNativeCryptoCurrencyDollarValueExceedThresholdPrompt: false, lastBackedUpTime: nil, isImported: true)
}
writeState(state)
}
}
func deleteWallet() {
updateState { state in
state.prompt[wallet.address] = nil
state.backupState[wallet.address] = nil
writeState(state)
}
}
//MARK: State
private var hasState: Bool {
guard let state = WalletsBackupState.load(fromUrl: fileUrl) else { return false }
return state.backupState[wallet.address] != nil
}
private var hasShownNativeCryptoCurrencyReceivedPrompt: Bool {
if let shown = readState()?.backupState[wallet.address]?.shownNativeCryptoCurrencyReceivedPrompt {
return shown
} else {
return false
}
}
private var hasShownExceededThresholdPrompt: Bool {
if let shown = readState()?.backupState[wallet.address]?.shownNativeCryptoCurrencyDollarValueExceedThresholdPrompt {
return shown
} else {
return false
}
}
private var isBackedUp: Bool {
if let backedUpTime = readState()?.backupState[wallet.address]?.lastBackedUpTime {
return true
} else {
return false
}
}
private var isImported: Bool {
return readState()?.backupState[wallet.address]?.isImported ?? false
}
private var canBackupWallet: Bool {
switch wallet.type {
case .real:
return true
case .watch:
return false
}
}
private var isShowingExceededThresholdPrompt: Bool {
guard let prompt = readState()?.prompt[wallet.address] else { return false }
switch prompt {
case .nativeCryptoCurrencyDollarValueExceededThreshold:
return true
case .newWallet, .intervalPassed, .receivedNativeCryptoCurrency:
return false
}
}
private var timeToShowIntervalPassedPrompt: Date? {
return readState()?.backupState[wallet.address]?.timeToShowIntervalPassedPrompt
}
private func readState() -> WalletsBackupState? {
return WalletsBackupState.load(fromUrl: fileUrl)
}
private func writeState(_ state: WalletsBackupState) {
state.writeTo(url: fileUrl)
}
private func updateState(block: (inout WalletsBackupState) -> ()) {
if var state = readState() {
block(&state)
writeState(state)
}
}
}
extension PromptBackupCoordinator: WalletCoordinatorDelegate {
func didFinish(with account: Wallet, in coordinator: WalletCoordinator) {
coordinator.navigationController.dismiss(animated: true, completion: nil)
removeCoordinator(coordinator)
finish()
extension PromptBackupCoordinator: PromptBackupWalletViewDelegate {
func viewControllerToShowBackupLaterAlert(forView view: PromptBackupWalletView) -> UIViewController? {
switch view {
case prominentPromptView:
return prominentPromptDelegate?.viewControllerToShowBackupLaterAlert
case subtlePromptView:
return subtlePromptDelegate?.viewControllerToShowBackupLaterAlert
default:
return nil
}
}
func didChooseBackupLater(inView view: PromptBackupWalletView) {
remindLater()
showHideCurrentPrompt()
}
func didFail(with error: Error, in coordinator: WalletCoordinator) {
coordinator.navigationController.dismiss(animated: true, completion: nil)
func didChooseBackup(inView view: PromptBackupWalletView) {
guard let nc = viewControllerToShowBackupLaterAlert(forView: view)?.navigationController else { return }
let coordinator = BackupCoordinator(navigationController: nc, keystore: keystore, account: .init(address: wallet.address))
coordinator.delegate = self
coordinator.start()
addCoordinator(coordinator)
}
}
extension PromptBackupCoordinator: BackupCoordinatorDelegate {
func didCancel(coordinator: BackupCoordinator) {
removeCoordinator(coordinator)
finish()
}
func didCancel(in coordinator: WalletCoordinator) {
coordinator.navigationController.dismiss(animated: true, completion: nil)
func didFinish(account: EthereumAccount, in coordinator: BackupCoordinator) {
removeCoordinator(coordinator)
finish()
markBackupDone()
showHideCurrentPrompt()
}
}

@ -0,0 +1,79 @@
// Copyright © 2019 Stormbird PTE. LTD.
import Foundation
import BigInt
struct WalletsBackupState: Codable {
enum Prompt {
case newWallet
case receivedNativeCryptoCurrency(BigInt)
case intervalPassed
case nativeCryptoCurrencyDollarValueExceededThreshold
}
struct BackupState: Codable {
var shownNativeCryptoCurrencyReceivedPrompt = false
var timeToShowIntervalPassedPrompt: Date?
var shownNativeCryptoCurrencyDollarValueExceedThresholdPrompt = false
var lastBackedUpTime: Date?
var isImported: Bool
}
var prompt = [AlphaWallet.Address: Prompt]()
var backupState = [AlphaWallet.Address: BackupState]()
func writeTo(url: URL) {
let encoder = JSONEncoder()
let data = try! encoder.encode(self)
try? data.write(to: url)
}
static func load(fromUrl url: URL) -> WalletsBackupState? {
guard let data = try? Data(contentsOf: url) else { return nil }
return try? JSONDecoder().decode(WalletsBackupState.self, from: data)
}
}
extension WalletsBackupState.Prompt: Codable {
enum Key: CodingKey {
case rawValue
case associatedValue
}
enum CodingError: Error {
case unknownValue
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: Key.self)
let rawValue = try container.decode(Int.self, forKey: .rawValue)
switch rawValue {
case 0:
self = .newWallet
case 1:
let nativeCryptoCurrency = try container.decode(BigInt.self, forKey: .associatedValue)
self = .receivedNativeCryptoCurrency(nativeCryptoCurrency)
case 2:
self = .intervalPassed
case 3:
self = .nativeCryptoCurrencyDollarValueExceededThreshold
default:
throw CodingError.unknownValue
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: Key.self)
switch self {
case .newWallet:
try container.encode(0, forKey: .rawValue)
case .receivedNativeCryptoCurrency(let nativeCryptoCurrency):
try container.encode(1, forKey: .rawValue)
try container.encode(nativeCryptoCurrency, forKey: .associatedValue)
case .intervalPassed:
try container.encode(2, forKey: .rawValue)
case .nativeCryptoCurrencyDollarValueExceededThreshold:
try container.encode(3, forKey: .rawValue)
}
}
}

@ -1,86 +0,0 @@
// Copyright SIX DAY LLC. All rights reserved.
import Foundation
import TrustKeystore
import UIKit
protocol BackupViewControllerDelegate: class {
func didPressBackup(account: Account, in viewController: BackupViewController)
}
class BackupViewController: UIViewController {
private let account: Account
lazy private var viewModel = BackupViewModel()
weak var delegate: BackupViewControllerDelegate?
init(account: Account) {
self.account = account
super.init(nibName: nil, bundle: nil)
let warningImageView = UIImageView()
warningImageView.translatesAutoresizingMaskIntoConstraints = false
warningImageView.image = R.image.backup_warning()
let noBackupLabel = UILabel()
noBackupLabel.translatesAutoresizingMaskIntoConstraints = false
noBackupLabel.text = viewModel.headlineText
noBackupLabel.font = Fonts.semibold(size: 24)
noBackupLabel.adjustsFontSizeToFitWidth = true
noBackupLabel.textColor = Colors.lightBlack
let controlMoneyLabel = UILabel()
controlMoneyLabel.translatesAutoresizingMaskIntoConstraints = false
controlMoneyLabel.text = R.string.localizable.exportControlYourMoneyLabelTitle()
controlMoneyLabel.numberOfLines = 0
controlMoneyLabel.textAlignment = .center
controlMoneyLabel.textColor = Colors.darkGray
let neverStoredLabel = UILabel()
neverStoredLabel.translatesAutoresizingMaskIntoConstraints = false
neverStoredLabel.text = R.string.localizable.exportNeverStoredLabelTitle()
neverStoredLabel.numberOfLines = 0
neverStoredLabel.textAlignment = .center
neverStoredLabel.textColor = Colors.darkGray
let backupButton = Button(size: .large, style: .solid)
backupButton.translatesAutoresizingMaskIntoConstraints = false
backupButton.setTitle(R.string.localizable.exportBackupButtonTitle(), for: .normal)
backupButton.addTarget(self, action: #selector(backup), for: .touchUpInside)
let stackView = [
warningImageView,
.spacer(),
noBackupLabel,
.spacer(height: 15),
controlMoneyLabel,
neverStoredLabel,
.spacer(height: 15),
backupButton,
].asStackView(axis: .vertical, spacing: 20, alignment: .center)
stackView.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .white
view.addSubview(stackView)
NSLayoutConstraint.activate([
stackView.topAnchor.constraint(greaterThanOrEqualTo: view.layoutGuide.topAnchor, constant: StyleLayout.sideMargin),
stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
stackView.leadingAnchor.constraint(equalTo: view.layoutGuide.leadingAnchor, constant: StyleLayout.sideMargin),
stackView.trailingAnchor.constraint(equalTo: view.layoutGuide.trailingAnchor, constant: -StyleLayout.sideMargin),
stackView.bottomAnchor.constraint(lessThanOrEqualTo: view.layoutGuide.bottomAnchor, constant: -StyleLayout.sideMargin),
backupButton.leadingAnchor.constraint(equalTo: stackView.leadingAnchor),
backupButton.trailingAnchor.constraint(equalTo: stackView.trailingAnchor),
])
}
@objc func backup() {
delegate?.didPressBackup(account: account, in: self)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

@ -0,0 +1,26 @@
// Copyright © 2019 Stormbird PTE. LTD.
import Foundation
import UIKit
struct PromptBackupWalletAfterExceedingThresholdViewViewModel: PromptBackupWalletViewViewModel {
let walletAddress: AlphaWallet.Address
let dollarValueInUsd: Double
var backgroundColor: UIColor {
return .init(red: 183, green: 80, blue: 70)
}
var title: String {
return R.string.localizable.backupPromptAfterHittingThresholdTitle()
}
var description: String {
let prettyAmount = CurrencyFormatter.formatter.string(from: NSNumber(value: dollarValueInUsd)) ?? "-"
return R.string.localizable.backupPromptAfterHittingThresholdDescription(prettyAmount)
}
var backupButtonBackgroundColor: UIColor {
return UIColor(red: 119, green: 56, blue: 50)
}
}

@ -0,0 +1,24 @@
// Copyright © 2019 Stormbird PTE. LTD.
import Foundation
import UIKit
struct PromptBackupWalletAfterIntervalViewViewModel: PromptBackupWalletViewViewModel {
let walletAddress: AlphaWallet.Address
var backgroundColor: UIColor {
return .init(red: 97, green: 103, blue: 123)
}
var title: String {
return R.string.localizable.backupPromptAfterIntervalTitle()
}
var description: String {
return R.string.localizable.backupPromptAfterIntervalDescription()
}
var backupButtonBackgroundColor: UIColor {
return UIColor(red: 65, green: 71, blue: 89)
}
}

@ -0,0 +1,28 @@
// Copyright © 2019 Stormbird PTE. LTD.
import Foundation
import UIKit
import BigInt
struct PromptBackupWalletAfterReceivingNativeCryptoCurrencyViewViewModel: PromptBackupWalletViewViewModel {
let walletAddress: AlphaWallet.Address
let nativeCryptoCurrency: BigInt
var backgroundColor: UIColor {
return .init(red: 97, green: 103, blue: 123)
}
var title: String {
let formatter = EtherNumberFormatter.short
let amount = formatter.string(from: nativeCryptoCurrency, decimals: 18)
return R.string.localizable.backupPromptAfterReceivingEtherTitle(amount)
}
var description: String {
return R.string.localizable.backupPromptDescriptionWithoutAmount()
}
var backupButtonBackgroundColor: UIColor {
return UIColor(red: 65, green: 71, blue: 89)
}
}

@ -0,0 +1,24 @@
// Copyright © 2019 Stormbird PTE. LTD.
import Foundation
import UIKit
struct PromptBackupWalletAfterWalletCreationViewViewModel: PromptBackupWalletViewViewModel {
let walletAddress: AlphaWallet.Address
var backgroundColor: UIColor {
return .init(red: 183, green: 80, blue: 70)
}
var title: String {
return R.string.localizable.backupPromptTitle()
}
var description: String {
return R.string.localizable.backupPromptDescriptionWithoutAmount()
}
var backupButtonBackgroundColor: UIColor {
return UIColor(red: 119, green: 56, blue: 50)
}
}

@ -0,0 +1,75 @@
// Copyright © 2019 Stormbird PTE. LTD.
import Foundation
import UIKit
protocol PromptBackupWalletViewViewModel {
var backgroundColor: UIColor { get }
var cornerRadius: CGFloat { get }
var titleFont: UIFont { get }
var titleColor: UIColor { get }
var title: String { get }
var descriptionFont: UIFont { get }
var descriptionColor: UIColor { get }
var description: String { get }
var backupButtonBackgroundColor: UIColor { get }
var backupButtonTitleColor: UIColor { get }
var backupButtonTitle: String { get }
var backupButtonTitleFont: UIFont { get }
var backupButtonImage: UIImage { get }
var backupButtonContentEdgeInsets: UIEdgeInsets { get }
var moreButtonImage: UIImage { get }
var moreButtonColor: UIColor { get }
var walletAddress: AlphaWallet.Address { get }
}
extension PromptBackupWalletViewViewModel {
var cornerRadius: CGFloat {
return 20
}
var titleFont: UIFont {
return Fonts.regular(size: 22)!
}
var titleColor: UIColor {
return Colors.appWhite
}
var descriptionFont: UIFont {
return Fonts.regular(size: 13)!
}
var descriptionColor: UIColor {
return Colors.appWhite
}
var backupButtonTitleColor: UIColor {
return Colors.appWhite
}
var backupButtonTitleFont: UIFont {
return Fonts.semibold(size: 16)!
}
var backupButtonImage: UIImage {
return R.image.toolbarForward()!
}
var backupButtonContentEdgeInsets: UIEdgeInsets {
return .init(top: 7, left: 21, bottom: 7, right: 21)
}
var moreButtonImage: UIImage {
return R.image.toolbarMenu()!
}
var moreButtonColor: UIColor {
return Colors.appWhite
}
var backupButtonTitle: String {
let firstFewCharactersOfWalletAddress = walletAddress.eip55String.substring(with: Range(uncheckedBounds: (0, 4)))
return "\(R.string.localizable.backupPromptBackupButtonTitle().uppercased()) \(firstFewCharactersOfWalletAddress) "
}
}

@ -0,0 +1,106 @@
// Copyright © 2019 Stormbird PTE. LTD.
import Foundation
import UIKit
protocol PromptBackupWalletViewDelegate: class {
func viewControllerToShowBackupLaterAlert(forView view: PromptBackupWalletView) -> UIViewController?
func didChooseBackupLater(inView view: PromptBackupWalletView)
func didChooseBackup(inView view: PromptBackupWalletView)
}
class PromptBackupWalletView: UIView {
private let titleLabel = UILabel()
private let descriptionLabel = UILabel()
private let backupButton = UIButton(type: .system)
private let remindMeLaterButton = UIButton(type: .system)
private let viewModel: PromptBackupWalletViewViewModel
weak var delegate: PromptBackupWalletViewDelegate?
init(viewModel: PromptBackupWalletViewViewModel) {
self.viewModel = viewModel
super.init(frame: .zero)
backupButton.addTarget(self, action: #selector(backup), for: .touchUpInside)
backupButton.setContentHuggingPriority(.required, for: .horizontal)
remindMeLaterButton.addTarget(self, action: #selector(remindMeLater), for: .touchUpInside)
remindMeLaterButton.setContentHuggingPriority(.required, for: .horizontal)
let row0 = [titleLabel, remindMeLaterButton].asStackView(axis: .horizontal)
let stackView = [
row0,
UIView.spacer(height: 10),
descriptionLabel,
UIView.spacer(height: 10),
backupButton,
].asStackView(axis: .vertical, alignment: .leading)
stackView.translatesAutoresizingMaskIntoConstraints = false
addSubview(stackView)
NSLayoutConstraint.activate([
row0.widthAnchor.constraint(equalTo: stackView.widthAnchor),
descriptionLabel.widthAnchor.constraint(equalTo: backupButton.widthAnchor, constant: 30),
stackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 20),
stackView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -20),
stackView.topAnchor.constraint(equalTo: topAnchor, constant: 20),
stackView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -20),
])
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configure() {
backgroundColor = viewModel.backgroundColor
cornerRadius = viewModel.cornerRadius
titleLabel.font = viewModel.titleFont
titleLabel.textColor = viewModel.titleColor
titleLabel.text = viewModel.title
//For small screens
titleLabel.adjustsFontSizeToFitWidth = true
remindMeLaterButton.setImage(viewModel.moreButtonImage, for: .normal)
remindMeLaterButton.tintColor = viewModel.moreButtonColor
descriptionLabel.font = viewModel.descriptionFont
descriptionLabel.textColor = viewModel.descriptionColor
descriptionLabel.text = viewModel.description
descriptionLabel.numberOfLines = 0
backupButton.tintColor = viewModel.backupButtonTitleColor
backupButton.titleLabel?.font = viewModel.backupButtonTitleFont
backupButton.setBackgroundColor(viewModel.backupButtonBackgroundColor, forState: .normal)
backupButton.setTitleColor(viewModel.backupButtonTitleColor, for: .normal)
backupButton.setTitle(viewModel.backupButtonTitle, for: .normal)
backupButton.setImage(viewModel.backupButtonImage, for: .normal)
backupButton.contentEdgeInsets = viewModel.backupButtonContentEdgeInsets
swapButtonTextAndImage(backupButton)
}
@objc private func backup() {
delegate?.didChooseBackup(inView: self)
}
@objc private func remindMeLater() {
delegate?.viewControllerToShowBackupLaterAlert(forView: self)?.confirm(message: R.string.localizable.backupPromptBackupRemindLater()) { result in
switch result {
case .success:
self.delegate?.didChooseBackupLater(inView: self)
case .failure:
break
}
}
}
private func swapButtonTextAndImage(_ button: UIButton) {
button.titleLabel?.transform = CGAffineTransform(scaleX: -1.0, y: 1.0)
button.imageView?.transform = CGAffineTransform(scaleX: -1.0, y: 1.0)
button.transform = CGAffineTransform(scaleX: -1.0, y: 1.0)
}
}

@ -33,6 +33,7 @@ extension Data {
self = data
}
//TODO remove if unused. Also confusing
init?(fromHexEncodedString string: String) {
// Convert 0 ... 9, a ... f, A ...F to their decimal value,
// return nil for all other input characters

@ -0,0 +1,8 @@
// Copyright © 2019 Stormbird PTE. LTD.
import Foundation
protocol CoordinatorThatEnds: Coordinator {
func endUserInterface(animated: Bool)
func end()
}

@ -93,6 +93,8 @@ class InCoordinator: NSObject, Coordinator {
self.appTracker = appTracker
self.assetDefinitionStore = assetDefinitionStore
self.assetDefinitionStore.enableFetchXMLForContractInPasteboard()
super.init()
}
func start() {
@ -235,7 +237,6 @@ class InCoordinator: NSObject, Coordinator {
guard !(balance.isZero) else { return }
//TODO we don'backup wallets if we are running tests. Maybe better to move this into app delegate's application(_:didFinishLaunchingWithOptions:)
guard !isRunningTests() else { return }
strongSelf.promptBackupWallet(withAddress: strongSelf.wallet.address)
}
}
nativeCryptoCurrencyBalances[each] = price
@ -286,15 +287,17 @@ class InCoordinator: NSObject, Coordinator {
showTab(inCoordinatorViewModel.initialTab)
}
private func createTokensCoordinator() -> TokensCoordinator {
private func createTokensCoordinator(promptBackupCoordinator: PromptBackupCoordinator) -> TokensCoordinator {
let tokensStoragesForEnabledServers = config.enabledServers.map { tokensStorages[$0] }
let tokenCollection = TokenCollection(tokenDataStores: tokensStoragesForEnabledServers)
promptBackupCoordinator.listenToNativeCryptoCurrencyBalance(withTokenCollection: tokenCollection)
let coordinator = TokensCoordinator(
sessions: walletSessions,
keystore: keystore,
tokenCollection: tokenCollection,
nativeCryptoCurrencyPrices: nativeCryptoCurrencyPrices,
assetDefinitionStore: assetDefinitionStore
assetDefinitionStore: assetDefinitionStore,
promptBackupCoordinator: promptBackupCoordinator
)
coordinator.rootViewController.tabBarItem = UITabBarItem(title: R.string.localizable.walletTokensTabbarItemTitle(), image: R.image.tab_wallet()?.withRenderingMode(.alwaysOriginal), selectedImage: R.image.tab_wallet())
coordinator.delegate = self
@ -303,14 +306,15 @@ class InCoordinator: NSObject, Coordinator {
return coordinator
}
private func createTransactionCoordinator() -> TransactionCoordinator {
private func createTransactionCoordinator(promptBackupCoordinator: PromptBackupCoordinator) -> TransactionCoordinator {
let transactionsStoragesForEnabledServers = config.enabledServers.map { transactionsStorages[$0] }
let transactionsCollection = TransactionCollection(transactionsStorages: transactionsStoragesForEnabledServers)
let coordinator = TransactionCoordinator(
sessions: walletSessions,
transactionsCollection: transactionsCollection,
keystore: keystore,
tokensStorages: tokensStorages
tokensStorages: tokensStorages,
promptBackupCoordinator: promptBackupCoordinator
)
coordinator.rootViewController.tabBarItem = UITabBarItem(title: R.string.localizable.transactionsTabbarItemTitle(), image: R.image.feed()?.withRenderingMode(.alwaysOriginal), selectedImage: R.image.feed())
coordinator.delegate = self
@ -328,11 +332,12 @@ class InCoordinator: NSObject, Coordinator {
return coordinator
}
private func createSettingsCoordinator(keystore: Keystore) -> SettingsCoordinator {
private func createSettingsCoordinator(keystore: Keystore, promptBackupCoordinator: PromptBackupCoordinator) -> SettingsCoordinator {
let coordinator = SettingsCoordinator(
keystore: keystore,
config: config,
sessions: walletSessions
sessions: walletSessions,
promptBackupCoordinator: promptBackupCoordinator
)
coordinator.rootViewController.tabBarItem = UITabBarItem(
title: R.string.localizable.aSettingsNavigationTitle(),
@ -349,31 +354,29 @@ class InCoordinator: NSObject, Coordinator {
private func createTabBarController(realm: Realm) -> UITabBarController {
var viewControllers = [UIViewController]()
let tokensCoordinator = createTokensCoordinator()
let promptBackupCoordinator = PromptBackupCoordinator(keystore: keystore, wallet: wallet, config: config)
addCoordinator(promptBackupCoordinator)
let tokensCoordinator = createTokensCoordinator(promptBackupCoordinator: promptBackupCoordinator)
viewControllers.append(tokensCoordinator.navigationController)
let transactionCoordinator = createTransactionCoordinator()
let transactionCoordinator = createTransactionCoordinator(promptBackupCoordinator: promptBackupCoordinator)
viewControllers.append(transactionCoordinator.navigationController)
let browserCoordinator = createBrowserCoordinator(sessions: walletSessions, realm: realm, browserOnly: false)
viewControllers.append(browserCoordinator.navigationController)
let settingsCoordinator = createSettingsCoordinator(keystore: keystore)
let settingsCoordinator = createSettingsCoordinator(keystore: keystore, promptBackupCoordinator: promptBackupCoordinator)
viewControllers.append(settingsCoordinator.navigationController)
let tabBarController = TabBarController()
tabBarController.tabBar.isTranslucent = false
tabBarController.viewControllers = viewControllers
tabBarController.delegate = self
return tabBarController
}
private func promptBackupWallet(withAddress address: AlphaWallet.Address) {
//TODo wallet or Address instead?
let coordinator = PromptBackupCoordinator(keystore: keystore, walletAddress: address, config: config)
addCoordinator(coordinator)
coordinator.delegate = self
coordinator.start()
promptBackupCoordinator.start()
return tabBarController
}
@objc private func dismissTransactions() {
@ -706,16 +709,6 @@ extension InCoordinator: PaymentCoordinatorDelegate {
}
}
extension InCoordinator: PromptBackupCoordinatorDelegate {
func viewControllerForPresenting(in coordinator: PromptBackupCoordinator) -> UIViewController? {
return navigationController
}
func didFinish(in coordinator: PromptBackupCoordinator) {
removeCoordinator(coordinator)
}
}
extension InCoordinator: DappBrowserCoordinatorDelegate {
func didSentTransaction(transaction: SentTransaction, inCoordinator coordinator: DappBrowserCoordinator) {
handlePendingTransaction(transaction: transaction)

@ -0,0 +1,813 @@
// Copyright SIX DAY LLC. All rights reserved.
import Foundation
import LocalAuthentication
import BigInt
import KeychainSwift
import Result
import TrustWalletCore
enum EtherKeystoreError: LocalizedError {
case protectionDisabled
}
///We use ECDSA keys (created and stored in the Secure Enclave), achieving symmetric encryption based on Diffie-Hellman to encrypt the HD wallet seed and raw private keys and store the ciphertext in the keychain.
///
///There are 2 sets of (ECDSA key and ciphertext) for each Ethereum raw private key or HD wallet seed. 1 set is stored requiring user presence for access and the other doesn't. The second set is needed to ensure the user has does not lose access to the Ethereum raw private key (or HD wallet seed) when they delete their iOS passcode. Once the user has verified that they have backed up their wallet, they can choose to elevate the security of their wallet which deletes the set of (ECDSA key and ciphertext) that do not require user-presence.
///
///Technically, having 2 sets of (ECDSA key and ciphertext) for each Ethereum raw private key or HD wallet seed may not be required for iOS. But it is done:
///(A) to be confident that we don't cause the user to lose access to their wallets and
///(B) to be consistent with Android's UI and implementation which seems like users will lose access to the data (i.e wallet) which requires user presence if the equivalent of their iOS passcode/biometrics is disabled/deleted
// swiftlint:disable type_body_length
open class EtherKeystore: Keystore {
private struct Keys {
static let recentlyUsedAddress: String = "recentlyUsedAddress"
static let watchAddresses = "watchAddresses"
static let ethereumAddressesWithPrivateKeys = "ethereumAddressesWithPrivateKeys"
static let ethereumAddressesWithSeed = "ethereumAddressesWithSeed"
static let ethereumAddressesProtectedByUserPresence = "ethereumAddressesProtectedByUserPresence"
static let ethereumRawPrivateKeyUserPresenceNotRequiredPrefix = "ethereumRawPrivateKeyUserPresenceNotRequired-"
static let ethereumSeedUserPresenceNotRequiredPrefix = "ethereumSeedUserPresenceNotRequired-"
static let ethereumRawPrivateKeyUserPresenceRequiredPrefix = "ethereumRawPrivateKeyUserPresenceRequired-"
static let ethereumSeedUserPresenceRequiredPrefix = "ethereumSeedUserPresenceRequired-"
//These aren't actually the label for the encryption key, but rather, the label for the ECDSA keys that will be used to generate the AES encryption keys since iOS Secure Enclave only supports ECDSA and not AES
static let encryptionKeyForSeedUserPresenceRequiredPrefix = "encryptionKeyForSeedUserPresenceRequired-"
static let encryptionKeyForPrivateKeyUserPresenceRequiredPrefix = "encryptionKeyForPrivateKeyUserPresenceRequired-"
static let encryptionKeyForSeedUserPresenceNotRequiredPrefix = "encryptionKeyForSeedUserPresenceNotRequired-"
static let encryptionKeyForPrivateKeyUserPresenceNotRequiredPrefix = "encryptionKeyForPrivateKeyUserPresenceNotRequired-"
}
enum WalletSeedOrKey {
case key(Data)
case seed(String)
case seedPhrase(String)
case userCancelled
case notFound
case otherFailure
}
private let emptyPassphrase = ""
private let keychain: KeychainSwift
private let defaultKeychainAccessUserPresenceRequired: KeychainSwiftAccessOptions = .accessibleWhenUnlockedThisDeviceOnly(userPresenceRequired: true)
private let defaultKeychainAccessUserPresenceNotRequired: KeychainSwiftAccessOptions = .accessibleWhenUnlockedThisDeviceOnly(userPresenceRequired: false)
private let userDefaults: UserDefaults
private var watchAddresses: [String] {
set {
let data = NSKeyedArchiver.archivedData(withRootObject: newValue)
return userDefaults.set(data, forKey: Keys.watchAddresses)
}
get {
guard let data = userDefaults.data(forKey: Keys.watchAddresses) else {
return []
}
return NSKeyedUnarchiver.unarchiveObject(with: data) as? [String] ?? []
}
}
private var ethereumAddressesWithPrivateKeys: [String] {
set {
let data = NSKeyedArchiver.archivedData(withRootObject: newValue)
return userDefaults.set(data, forKey: Keys.ethereumAddressesWithPrivateKeys)
}
get {
guard let data = userDefaults.data(forKey: Keys.ethereumAddressesWithPrivateKeys) else {
return []
}
return NSKeyedUnarchiver.unarchiveObject(with: data) as? [String] ?? []
}
}
private var ethereumAddressesWithSeed: [String] {
set {
let data = NSKeyedArchiver.archivedData(withRootObject: newValue)
return userDefaults.set(data, forKey: Keys.ethereumAddressesWithSeed)
}
get {
guard let data = userDefaults.data(forKey: Keys.ethereumAddressesWithSeed) else {
return []
}
return NSKeyedUnarchiver.unarchiveObject(with: data) as? [String] ?? []
}
}
private var ethereumAddressesProtectedByUserPresence: [String] {
set {
let data = NSKeyedArchiver.archivedData(withRootObject: newValue)
return userDefaults.set(data, forKey: Keys.ethereumAddressesProtectedByUserPresence)
}
get {
guard let data = userDefaults.data(forKey: Keys.ethereumAddressesProtectedByUserPresence) else {
return []
}
return NSKeyedUnarchiver.unarchiveObject(with: data) as? [String] ?? []
}
}
//i.e if passcode is enabled. Face ID/Touch ID wouldn't work without passcode being enabled and we can't write to the keychain or generate a key in secure enclave when passcode is disabled
var isUserPresenceCheckPossible: Bool {
let authContext = LAContext()
return authContext.canEvaluatePolicy(.deviceOwnerAuthentication, error: nil)
}
var hasWallets: Bool {
return !wallets.isEmpty
}
var wallets: [Wallet] {
let watchAddresses = self.watchAddresses.compactMap { AlphaWallet.Address(string: $0) }.map { Wallet(type: .watch($0)) }
let addressesWithPrivateKeys = ethereumAddressesWithPrivateKeys.compactMap { AlphaWallet.Address(string: $0) }.map { Wallet(type: .real(.init(address: $0))) }
let addressesWithSeed = ethereumAddressesWithSeed.compactMap { AlphaWallet.Address(string: $0) }.map { Wallet(type: .real(.init(address: $0))) }
return addressesWithSeed + addressesWithPrivateKeys + watchAddresses
}
var hasMigratedFromKeystoreFiles: Bool {
return userDefaults.data(forKey: Keys.ethereumAddressesWithPrivateKeys) != nil
}
var recentlyUsedWallet: Wallet? {
set {
keychain.set(newValue?.address.eip55String ?? "", forKey: Keys.recentlyUsedAddress, withAccess: defaultKeychainAccessUserPresenceNotRequired)
}
get {
guard let address = keychain.get(Keys.recentlyUsedAddress) else {
return nil
}
return wallets.filter {
$0.address.sameContract(as: address)
}.first
}
}
//TODO improve
static var current: Wallet? {
do {
return try EtherKeystore().recentlyUsedWallet
} catch {
return .none
}
}
public init(
keychain: KeychainSwift = KeychainSwift(keyPrefix: Constants.keychainKeyPrefix),
userDefaults: UserDefaults = UserDefaults.standard
) throws {
if !UIApplication.shared.isProtectedDataAvailable {
throw EtherKeystoreError.protectionDisabled
}
self.keychain = keychain
self.keychain.synchronizable = false
self.userDefaults = userDefaults
}
// Async
func createAccount(completion: @escaping (Result<EthereumAccount, KeystoreError>) -> Void) {
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
guard let strongSelf = self else {
return
}
let result = strongSelf.createAccount()
DispatchQueue.main.async {
completion(result)
}
}
}
func importWallet(type: ImportType, completion: @escaping (Result<Wallet, KeystoreError>) -> Void) {
let results = importWallet(type: type)
switch results {
case .success(let wallet):
//TODO not the best way to do this but let's see if there's a better way to inform the coordinator that a wallet has been imported to avoid it being prompted for back
PromptBackupCoordinator(keystore: self, wallet: wallet, config: .init()).markWalletAsImported()
case .failure:
break
}
completion(results)
}
func importWallet(type: ImportType) -> Result<Wallet, KeystoreError> {
switch type {
case .keystore(let json, let password):
guard let keystore = try? LegacyFileBasedKeystore() else {
return .failure(.failedToExportPrivateKey)
}
let result = keystore.getPrivateKeyFromKeystoreFile(json: json, password: password)
switch result {
case .success(let privateKey):
return importWallet(type: .privateKey(privateKey: privateKey))
case .failure(let error):
return .failure(error)
}
case .privateKey(let privateKey):
let address = AlphaWallet.Address(fromPrivateKey: privateKey)
let hasEthereumAddressAlready = wallets.map({ $0.address }).contains {
$0.sameContract(as: address)
}
guard !hasEthereumAddressAlready else {
return .failure(.duplicateAccount)
}
if isUserPresenceCheckPossible {
let isSuccessful = savePrivateKeyForNonHdWallet(privateKey, forAccount: .init(address: address), withUserPresence: false)
guard isSuccessful else { return .failure(.failedToCreateWallet) }
let _ = savePrivateKeyForNonHdWallet(privateKey, forAccount: .init(address: address), withUserPresence: true)
} else {
let isSuccessful = savePrivateKeyForNonHdWallet(privateKey, forAccount: .init(address: address), withUserPresence: false)
guard isSuccessful else { return .failure(.failedToCreateWallet) }
}
addToListOfEthereumAddressesWithPrivateKeys(address)
return .success(Wallet(type: .real(.init(address: address))))
case .mnemonic(let mnemonic, _):
let mnemonicString = mnemonic.joined(separator: " ")
let wallet = HDWallet(mnemonic: mnemonicString, passphrase: emptyPassphrase)
let privateKey = derivePrivateKeyOfAccount0(fromHdWallet: wallet)
let address = AlphaWallet.Address(fromPrivateKey: privateKey)
let hasEthereumAddressAlready = wallets.map({ $0.address }).contains {
$0.sameContract(as: address)
}
guard !hasEthereumAddressAlready else {
return .failure(.duplicateAccount)
}
let seed = HDWallet.computeSeedWithChecksum(fromSeedPhrase: mnemonicString)
if isUserPresenceCheckPossible {
let isSuccessful = saveSeedForHdWallet(seed, forAccount: .init(address: address), withUserPresence: false)
guard isSuccessful else { return .failure(.failedToCreateWallet) }
let _ = saveSeedForHdWallet(seed, forAccount: .init(address: address), withUserPresence: true)
} else {
let isSuccessful = saveSeedForHdWallet(seed, forAccount: .init(address: address), withUserPresence: false)
guard isSuccessful else { return .failure(.failedToCreateWallet) }
}
addToListOfEthereumAddressesWithSeed(address)
return .success(Wallet(type: .real(.init(address: address))))
case .watch(let address):
guard !watchAddresses.contains(where: { address.sameContract(as: $0) }) else {
return .failure(.duplicateAccount)
}
watchAddresses = [watchAddresses, [address.eip55String]].flatMap {
$0
}
return .success(Wallet(type: .watch(address)))
}
}
private func addToListOfEthereumAddressesWithPrivateKeys(_ address: AlphaWallet.Address) {
let updatedOwnedAddresses = Array(Set(ethereumAddressesWithPrivateKeys + [address.eip55String]))
ethereumAddressesWithPrivateKeys = updatedOwnedAddresses
}
private func addToListOfEthereumAddressesWithSeed(_ address: AlphaWallet.Address) {
let updated = Array(Set(ethereumAddressesWithSeed + [address.eip55String]))
ethereumAddressesWithSeed = updated
}
private func addToListOfEthereumAddressesProtectedByUserPresence(_ address: AlphaWallet.Address) {
let updated = Array(Set(ethereumAddressesProtectedByUserPresence + [address.eip55String]))
ethereumAddressesProtectedByUserPresence = updated
}
func createAccount() -> Result<EthereumAccount, KeystoreError> {
let strength = Int32(128)
let newHdWallet = HDWallet(strength: strength, passphrase: emptyPassphrase)
let mnemonic = newHdWallet.mnemonic.split(separator: " ").map {
String($0)
}
let result = importWallet(type: .mnemonic(words: mnemonic, password: emptyPassphrase))
switch result {
case .success(let wallet):
return .success(.init(address: wallet.address))
case .failure(let error):
return .failure(.failedToCreateWallet)
}
}
private func derivePrivateKeyOfAccount0(fromHdWallet wallet: HDWallet) -> Data {
let firstAccountIndex = UInt32(0)
let externalChangeConstant = UInt32(0)
let addressIndex = UInt32(0)
let privateKey = wallet.getKey(purpose: .bip44, coin: .ethereum, account: firstAccountIndex, change: externalChangeConstant, address: addressIndex)
return privateKey.data
}
func exportRawPrivateKeyForNonHdWalletForBackup(forAccount account: EthereumAccount, newPassword: String, completion: @escaping (Result<String, KeystoreError>) -> Void) {
let key: Data
switch getPrivateKeyFromNonHdWallet(forAccount: account, prompt: R.string.localizable.keystoreAccessKeyNonHdBackup(), withUserPresence: isUserPresenceCheckPossible) {
case .seed, .seedPhrase:
//Not possible
return completion(.failure(.failedToExportPrivateKey))
case .key(let k):
key = k
case .userCancelled:
return completion(.failure(.userCancelled))
case .notFound, .otherFailure:
return completion(.failure(.accountMayNeedImportingAgainOrEnablePasscode))
}
//Careful to not replace the if-let with a flatMap(). Because the value is a Result and it has flatMap() defined to "resolve" only when it's .success
if let result = (try? LegacyFileBasedKeystore())?.export(privateKey: key, newPassword: newPassword) {
completion(result)
} else {
completion(.failure(.failedToExportPrivateKey))
}
}
func exportSeedPhraseOfHdWallet(forAccount account: EthereumAccount, reason: KeystoreExportReason, completion: @escaping (Result<String, KeystoreError>) -> Void) {
let seedPhrase = getSeedPhraseForHdWallet(forAccount: account, prompt: reason.prompt, withUserPresence: isUserPresenceCheckPossible)
switch seedPhrase {
case .seedPhrase(let seedPhrase):
completion(.success(seedPhrase))
case .seed, .key:
completion(.failure(.failedToExportSeed))
case .userCancelled:
completion(.failure(.userCancelled))
case .notFound, .otherFailure:
completion(.failure(.failedToExportSeed))
}
}
func verifySeedPhraseOfHdWallet(_ inputSeedPhrase: String, forAccount account: EthereumAccount, completion: @escaping (Result<Bool, KeystoreError>) -> Void) {
switch getSeedPhraseForHdWallet(forAccount: account, prompt: R.string.localizable.keystoreAccessKeyHdVerify(), withUserPresence: isUserPresenceCheckPossible) {
case .seedPhrase(let actualSeedPhrase):
let matched = inputSeedPhrase.lowercased() == actualSeedPhrase.lowercased()
completion(.success(matched))
case .seed, .key:
completion(.failure(.failedToExportSeed))
case .userCancelled:
completion(.failure(.userCancelled))
case .notFound, .otherFailure:
completion(.failure(.failedToExportSeed))
}
}
@discardableResult func delete(wallet: Wallet) -> Result<Void, KeystoreError> {
switch wallet.type {
case .real(let account):
//TODO not the best way to do this but let's see if there's a better way to inform the coordinator that a wallet has been deleted
PromptBackupCoordinator(keystore: self, wallet: wallet, config: .init()).deleteWallet()
removeAccountFromBookkeeping(account)
deleteKeysAndSeedCipherTextFromKeychain(forAccount: account)
deletePrivateKeysFromSecureEnclave(forAccount: account)
case .watch(let address):
removeAccountFromBookkeeping(.init(address: address))
}
(try? LegacyFileBasedKeystore())?.delete(wallet: wallet)
return .success(())
}
private func deletePrivateKeysFromSecureEnclave(forAccount account: EthereumAccount) {
let secureEnclave = SecureEnclave()
secureEnclave.deletePrivateKeys(withName: encryptionKeyForPrivateKeyLabel(fromAccount: account, withUserPresence: true))
secureEnclave.deletePrivateKeys(withName: encryptionKeyForPrivateKeyLabel(fromAccount: account, withUserPresence: false))
secureEnclave.deletePrivateKeys(withName: encryptionKeyForSeedLabel(fromAccount: account, withUserPresence: true))
secureEnclave.deletePrivateKeys(withName: encryptionKeyForSeedLabel(fromAccount: account, withUserPresence: false))
}
private func deleteKeysAndSeedCipherTextFromKeychain(forAccount account: EthereumAccount) {
keychain.delete("\(Keys.ethereumRawPrivateKeyUserPresenceNotRequiredPrefix)\(account.address.eip55String)")
keychain.delete("\(Keys.ethereumRawPrivateKeyUserPresenceRequiredPrefix)\(account.address.eip55String)")
keychain.delete("\(Keys.ethereumSeedUserPresenceNotRequiredPrefix)\(account.address.eip55String)")
keychain.delete("\(Keys.ethereumSeedUserPresenceRequiredPrefix)\(account.address.eip55String)")
}
private func removeAccountFromBookkeeping(_ account: EthereumAccount) {
ethereumAddressesWithPrivateKeys = ethereumAddressesWithPrivateKeys.filter { $0 != account.address.eip55String }
ethereumAddressesWithSeed = ethereumAddressesWithSeed.filter { $0 != account.address.eip55String }
ethereumAddressesProtectedByUserPresence = ethereumAddressesProtectedByUserPresence.filter { $0 != account.address.eip55String }
watchAddresses = watchAddresses.filter { $0 != account.address.eip55String }
}
func delete(wallet: Wallet, completion: @escaping (Result<Void, KeystoreError>) -> Void) {
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
guard let strongSelf = self else {
return
}
let result = strongSelf.delete(wallet: wallet)
DispatchQueue.main.async {
completion(result)
}
}
}
func isHdWallet(account: EthereumAccount) -> Bool {
return ethereumAddressesWithSeed.contains(account.address.eip55String)
}
func isHdWallet(wallet: Wallet) -> Bool {
switch wallet.type {
case .real(let account):
return ethereumAddressesWithSeed.contains(account.address.eip55String)
case .watch:
return false
}
}
func isKeystore(wallet: Wallet) -> Bool {
switch wallet.type {
case .real(let account):
return ethereumAddressesWithPrivateKeys.contains(account.address.eip55String)
case .watch:
return false
}
}
func isWatched(wallet: Wallet) -> Bool {
switch wallet.type {
case .real:
return false
case .watch(let address):
return watchAddresses.contains(address.eip55String)
}
}
func isProtectedByUserPresence(account: EthereumAccount) -> Bool {
return ethereumAddressesProtectedByUserPresence.contains(account.address.eip55String)
}
func signPersonalMessage(_ message: Data, for account: EthereumAccount) -> Result<Data, KeystoreError> {
let prefix = "\u{19}Ethereum Signed Message:\n\(message.count)".data(using: .utf8)!
return signMessage(prefix + message, for: account)
}
func signHash(_ hash: Data, for account: EthereumAccount) -> Result<Data, KeystoreError> {
let key = getPrivateKeyForSigning(forAccount: account)
switch key {
case .seed, .seedPhrase:
return .failure(.failedToExportPrivateKey)
case .key(let key):
do {
var data = try EthereumSigner().sign(hash: hash, withPrivateKey: key)
// TODO: Make it configurable, instead of overriding last byte.
data[64] += 27
return .success(data)
} catch {
return .failure(KeystoreError.failedToSignMessage)
}
case .userCancelled:
return .failure(.userCancelled)
case .notFound, .otherFailure:
return .failure(.accountMayNeedImportingAgainOrEnablePasscode)
}
}
func signTypedMessage(_ datas: [EthTypedData], for account: EthereumAccount) -> Result<Data, KeystoreError> {
let schemas = datas.map { $0.schemaData }.reduce(Data(), { $0 + $1 }).sha3(.keccak256)
let values = datas.map { $0.typedData }.reduce(Data(), { $0 + $1 }).sha3(.keccak256)
let combined = (schemas + values).sha3(.keccak256)
return signHash(combined, for: account)
}
func signMessage(_ message: Data, for account: EthereumAccount) -> Result<Data, KeystoreError> {
return signHash(message.sha3(.keccak256), for: account)
}
func signMessageBulk(_ data: [Data], for account: EthereumAccount) -> Result<[Data], KeystoreError> {
switch getPrivateKeyForSigning(forAccount: account) {
case .seed, .seedPhrase:
return .failure(.failedToExportPrivateKey)
case .key(let key):
do {
var messageHashes = [Data]()
for i in 0...data.count - 1 {
let hash = data[i].sha3(.keccak256)
messageHashes.append(hash)
}
var data = try EthereumSigner().signHashes(messageHashes, withPrivateKey: key)
// TODO: Make it configurable, instead of overriding last byte.
for i in 0...data.count - 1 {
data[i][64] += 27
}
return .success(data)
} catch {
return .failure(KeystoreError.failedToSignMessage)
}
case .userCancelled:
return .failure(.userCancelled)
case .notFound, .otherFailure:
return .failure(.accountMayNeedImportingAgainOrEnablePasscode)
}
}
func signMessageData(_ message: Data?, for account: EthereumAccount) -> Result<Data, KeystoreError> {
guard let hash = message?.sha3(.keccak256) else { return .failure(KeystoreError.failedToSignMessage) }
switch getPrivateKeyForSigning(forAccount: account) {
case .seed, .seedPhrase:
return .failure(.failedToExportPrivateKey)
case .key(let key):
do {
var data = try EthereumSigner().sign(hash: hash, withPrivateKey: key)
data[64] += 27
return .success(data)
} catch {
return .failure(KeystoreError.failedToSignMessage)
}
case .userCancelled:
return .failure(.userCancelled)
case .notFound, .otherFailure:
return .failure(.accountMayNeedImportingAgainOrEnablePasscode)
}
}
func signTransaction(_ transaction: UnsignedTransaction) -> Result<Data, KeystoreError> {
let signer: Signer
if transaction.server.chainID == 0 {
signer = HomesteadSigner()
} else {
signer = EIP155Signer(server: transaction.server)
}
do {
let hash = signer.hash(transaction: transaction)
switch getPrivateKeyForSigning(forAccount: transaction.account) {
case .seed, .seedPhrase:
return .failure(.failedToExportPrivateKey)
case .key(let key):
let signature = try EthereumSigner().sign(hash: hash, withPrivateKey: key)
let (r, s, v) = signer.values(transaction: transaction, signature: signature)
let data = RLP.encode([
transaction.nonce,
transaction.gasPrice,
transaction.gasLimit,
transaction.to?.data ?? Data(),
transaction.value,
transaction.data,
v, r, s,
])!
return .success(data)
case .userCancelled:
return .failure(.userCancelled)
case .notFound, .otherFailure:
return .failure(.accountMayNeedImportingAgainOrEnablePasscode)
}
} catch {
return .failure(.failedToSignTransaction)
}
}
func getAccount(for address: AlphaWallet.Address) -> EthereumAccount? {
return .init(address: address)
}
private func getPrivateKeyForSigning(forAccount account: EthereumAccount) -> WalletSeedOrKey {
let prompt = R.string.localizable.keystoreAccessKeySign()
if isHdWallet(account: account) {
let seed = getSeedForHdWallet(forAccount: account, prompt: prompt, withUserPresence: isUserPresenceCheckPossible)
switch seed {
case .seed(let seed):
let wallet = HDWallet(seed: seed, passphrase: emptyPassphrase)
let privateKey = derivePrivateKeyOfAccount0(fromHdWallet: wallet)
return .key(privateKey)
case .key, .seedPhrase:
//Not possible
return seed
case .userCancelled, .notFound, .otherFailure:
return seed
}
} else {
return getPrivateKeyFromNonHdWallet(forAccount: account, prompt: prompt, withUserPresence: isUserPresenceCheckPossible)
}
}
private func getPrivateKeyFromNonHdWallet(forAccount account: EthereumAccount, prompt: String, withUserPresence: Bool, shouldWriteWithUserPresenceIfNotFound: Bool = true) -> WalletSeedOrKey {
let prefix: String
if withUserPresence {
prefix = Keys.ethereumRawPrivateKeyUserPresenceRequiredPrefix
} else {
prefix = Keys.ethereumRawPrivateKeyUserPresenceNotRequiredPrefix
}
let context = createContext()
let data = keychain.getData("\(prefix)\(account.address.eip55String)", prompt: prompt, withContext: context)
.flatMap { decryptPrivateKey(fromCipherTextData: $0, forAccount: account, withUserPresence: withUserPresence, withContext: context) }
//We copy the record that doesn't require user-presence make a new one which requires user-presence and read from that. We don't want to read the one without user-presence unless absolutely necessary (e.g user has disabled passcode)
if data == nil && withUserPresence && shouldWriteWithUserPresenceIfNotFound && keychain.isDataNotFoundForLastAccess {
switch getPrivateKeyFromNonHdWallet(forAccount: account, prompt: prompt, withUserPresence: false, shouldWriteWithUserPresenceIfNotFound: false) {
case .seed, .seedPhrase:
//Not possible
break
case .key(let keyWithoutUserPresence):
savePrivateKeyForNonHdWallet(keyWithoutUserPresence, forAccount: account, withUserPresence: true)
case .userCancelled, .notFound, .otherFailure:
break
}
return getPrivateKeyFromNonHdWallet(forAccount: account, prompt: prompt, withUserPresence: true, shouldWriteWithUserPresenceIfNotFound: false)
} else {
if let data = data {
return .key(data)
} else {
if keychain.hasUserCancelledLastAccess {
return .userCancelled
} else if keychain.isDataNotFoundForLastAccess {
return .notFound
} else {
return .otherFailure
}
}
}
}
private func getSeedPhraseForHdWallet(forAccount account: EthereumAccount, prompt: String, withUserPresence: Bool) -> WalletSeedOrKey {
let seedOrKey = getSeedForHdWallet(forAccount: account, prompt: prompt, withUserPresence: withUserPresence)
switch seedOrKey {
case .seed(let seed):
return .seedPhrase(HDWallet(seed: seed, passphrase: emptyPassphrase).mnemonic)
case .seedPhrase, .key:
//Not possible
return seedOrKey
case .userCancelled, .notFound, .otherFailure:
return seedOrKey
}
}
private func getSeedForHdWallet(forAccount account: EthereumAccount, prompt: String, withUserPresence: Bool, shouldWriteWithUserPresenceIfNotFound: Bool = true) -> WalletSeedOrKey {
let prefix: String
if withUserPresence {
prefix = Keys.ethereumSeedUserPresenceRequiredPrefix
} else {
prefix = Keys.ethereumSeedUserPresenceNotRequiredPrefix
}
let context = createContext()
let data = keychain.getData("\(prefix)\(account.address.eip55String)", prompt: prompt, withContext: context)
.flatMap { decryptHdWalletSeed(fromCipherTextData: $0, forAccount: account, withUserPresence: withUserPresence, withContext: context) }
.flatMap { String(data: $0, encoding: .utf8) }
//We copy the record that doesn't require user-presence make a new one which requires user-presence and read from that. We don't want to read the one without user-presence unless absolutely necessary (e.g user has disabled passcode)
if data == nil && withUserPresence && shouldWriteWithUserPresenceIfNotFound && keychain.isDataNotFoundForLastAccess {
switch getSeedForHdWallet(forAccount: account, prompt: prompt, withUserPresence: false, shouldWriteWithUserPresenceIfNotFound: false) {
case .seed(let seedWithoutUserPresence):
saveSeedForHdWallet(seedWithoutUserPresence, forAccount: account, withUserPresence: true)
case .key, .seedPhrase:
//Not possible
break
case .userCancelled, .notFound, .otherFailure:
break
}
return getSeedForHdWallet(forAccount: account, prompt: prompt, withUserPresence: true, shouldWriteWithUserPresenceIfNotFound: false)
} else {
if let data = data {
return .seed(data)
} else {
if keychain.hasUserCancelledLastAccess {
return .userCancelled
} else if keychain.isDataNotFoundForLastAccess {
return .notFound
} else {
return .otherFailure
}
}
}
}
private func savePrivateKeyForNonHdWallet(_ privateKey: Data, forAccount account: EthereumAccount, withUserPresence: Bool) -> Bool {
let context = createContext()
guard let cipherTextData = encryptPrivateKey(privateKey, forAccount: account, withUserPresence: withUserPresence, withContext: context) else { return false }
let access: KeychainSwiftAccessOptions
let prefix: String
if withUserPresence {
access = defaultKeychainAccessUserPresenceRequired
prefix = Keys.ethereumRawPrivateKeyUserPresenceRequiredPrefix
} else {
access = defaultKeychainAccessUserPresenceNotRequired
prefix = Keys.ethereumRawPrivateKeyUserPresenceNotRequiredPrefix
}
return keychain.set(cipherTextData, forKey: "\(prefix)\(account.address.eip55String)", withAccess: access)
}
private func saveSeedForHdWallet(_ seed: String, forAccount account: EthereumAccount, withUserPresence: Bool) -> Bool {
let context = createContext()
guard let cipherTextData = seed.data(using: .utf8).flatMap({ self.encryptHdWalletSeed($0, forAccount: account, withUserPresence: withUserPresence, withContext: context) }) else { return false }
let access: KeychainSwiftAccessOptions
let prefix: String
if withUserPresence {
access = defaultKeychainAccessUserPresenceRequired
prefix = Keys.ethereumSeedUserPresenceRequiredPrefix
} else {
access = defaultKeychainAccessUserPresenceNotRequired
prefix = Keys.ethereumSeedUserPresenceNotRequiredPrefix
}
return keychain.set(cipherTextData, forKey: "\(prefix)\(account.address.eip55String)", withAccess: access)
}
private func decryptHdWalletSeed(fromCipherTextData cipherTextData: Data, forAccount account: EthereumAccount, withUserPresence: Bool, withContext context: LAContext) -> Data? {
let secureEnclave = SecureEnclave(userPresenceRequired: withUserPresence)
return try? secureEnclave.decrypt(cipherText: cipherTextData, withPrivateKeyFromLabel: encryptionKeyForSeedLabel(fromAccount: account, withUserPresence: withUserPresence), withContext: context)
}
private func decryptPrivateKey(fromCipherTextData cipherTextData: Data, forAccount account: EthereumAccount, withUserPresence: Bool, withContext context: LAContext) -> Data? {
let secureEnclave = SecureEnclave(userPresenceRequired: withUserPresence)
return try? secureEnclave.decrypt(cipherText: cipherTextData, withPrivateKeyFromLabel: encryptionKeyForPrivateKeyLabel(fromAccount: account, withUserPresence: withUserPresence), withContext: context)
}
private func encryptHdWalletSeed(_ seed: Data, forAccount account: EthereumAccount, withUserPresence: Bool, withContext context: LAContext) -> Data? {
let secureEnclave = SecureEnclave(userPresenceRequired: withUserPresence)
return try? secureEnclave.encrypt(plainTextData: seed, withPublicKeyFromLabel: encryptionKeyForSeedLabel(fromAccount: account, withUserPresence: withUserPresence), withContext: context)
}
private func encryptPrivateKey(_ key: Data, forAccount account: EthereumAccount, withUserPresence: Bool, withContext context: LAContext) -> Data? {
let secureEnclave = SecureEnclave(userPresenceRequired: withUserPresence)
return try? secureEnclave.encrypt(plainTextData: key, withPublicKeyFromLabel: encryptionKeyForPrivateKeyLabel(fromAccount: account, withUserPresence: withUserPresence), withContext: context)
}
private func encryptionKeyForSeedLabel(fromAccount account: EthereumAccount, withUserPresence: Bool) -> String {
let prefix: String
if withUserPresence {
prefix = Keys.encryptionKeyForSeedUserPresenceRequiredPrefix
} else {
prefix = Keys.encryptionKeyForSeedUserPresenceNotRequiredPrefix
}
return "\(prefix)\(account.address.eip55String)"
}
private func encryptionKeyForPrivateKeyLabel(fromAccount account: EthereumAccount, withUserPresence: Bool) -> String {
let prefix: String
if withUserPresence {
prefix = Keys.encryptionKeyForPrivateKeyUserPresenceRequiredPrefix
} else {
prefix = Keys.encryptionKeyForPrivateKeyUserPresenceNotRequiredPrefix
}
return "\(prefix)\(account.address.eip55String)"
}
func elevateSecurity(forAccount account: EthereumAccount) -> Bool {
guard !isProtectedByUserPresence(account: account) else { return true }
guard isUserPresenceCheckPossible else { return false }
let prompt: String
var isSuccessful: Bool
if isHdWallet(account: account) {
prompt = R.string.localizable.keystoreAccessKeyHdLock()
let seed = getSeedForHdWallet(forAccount: account, prompt: prompt, withUserPresence: false)
switch seed {
case .seed(let seed):
isSuccessful = saveSeedForHdWallet(seed, forAccount: account, withUserPresence: true)
if isSuccessful {
//Read it back, forcing iOS to check for user-presence
switch getSeedForHdWallet(forAccount: account, prompt: prompt, withUserPresence: true) {
case .seed:
isSuccessful = true
case .key, .seedPhrase:
//Not possible
isSuccessful = false
case .userCancelled, .notFound, .otherFailure:
isSuccessful = false
}
}
case .key, .seedPhrase:
//Not possible
return false
case .userCancelled:
return false
case .notFound, .otherFailure:
return false
}
} else {
prompt = R.string.localizable.keystoreAccessKeyNonHdLock()
let keyStoredAsRawPrivateKey = getPrivateKeyFromNonHdWallet(forAccount: account, prompt: prompt, withUserPresence: false)
switch keyStoredAsRawPrivateKey {
case .seed, .seedPhrase:
//Not possible
return false
case .key(let keyStoredAsRawPrivateKey):
isSuccessful = savePrivateKeyForNonHdWallet(keyStoredAsRawPrivateKey, forAccount: account, withUserPresence: true)
if isSuccessful {
//Read it back, forcing iOS to check for user-presence
switch getPrivateKeyFromNonHdWallet(forAccount: account, prompt: prompt, withUserPresence: true) {
case .seed, .seedPhrase:
//Not possible
isSuccessful = false
case .key:
isSuccessful = true
case .userCancelled, .notFound, .otherFailure:
isSuccessful = false
}
}
case .userCancelled:
return false
case .notFound, .otherFailure:
return false
}
}
if isSuccessful {
addToListOfEthereumAddressesProtectedByUserPresence(account.address)
let secureEnclave = SecureEnclave()
if isHdWallet(account: account) {
secureEnclave.deletePrivateKeys(withName: encryptionKeyForSeedLabel(fromAccount: account, withUserPresence: false))
keychain.delete("\(Keys.ethereumSeedUserPresenceNotRequiredPrefix)\(account.address.eip55String)")
} else {
secureEnclave.deletePrivateKeys(withName: encryptionKeyForPrivateKeyLabel(fromAccount: account, withUserPresence: false))
keychain.delete("\(Keys.ethereumRawPrivateKeyUserPresenceNotRequiredPrefix)\(account.address.eip55String)")
}
}
return isSuccessful
}
private func createContext() -> LAContext {
return .init()
}
}
// swiftlint:enable type_body_length
extension KeychainSwift {
var hasUserCancelledLastAccess: Bool {
return lastResultCode == errSecUserCanceled
}
var isDataNotFoundForLastAccess: Bool {
return lastResultCode == errSecItemNotFound
}
}

@ -0,0 +1,26 @@
// Copyright © 2019 Stormbird PTE. LTD.
import Foundation
import TrustKeystore
struct EthereumAccount: Hashable {
var address: AlphaWallet.Address
init(address: AlphaWallet.Address) {
self.address = address
}
public var hashValue: Int {
return address.hashValue
}
public static func == (lhs: EthereumAccount, rhs: EthereumAccount) -> Bool {
return lhs.address == rhs.address
}
}
extension EthereumAccount {
init(account: Account) {
self.init(address: .init(address: account.address))
}
}

@ -0,0 +1,14 @@
// Copyright © 2019 Stormbird PTE. LTD.
import Foundation
import TrustKeystore
struct EthereumSigner {
public func sign(hash: Data, withPrivateKey key: Data) throws -> Data {
return try Secp256k1.shared.sign(hash: hash, privateKey: key)
}
public func signHashes(_ hashes: [Data], withPrivateKey key: Data) throws -> [Data] {
return try hashes.map { try sign(hash: $0, withPrivateKey: key) }
}
}

@ -0,0 +1,47 @@
// Copyright SIX DAY LLC. All rights reserved.
import Foundation
import Result
enum KeystoreExportReason {
case backup
case prepareForVerification
var prompt: String {
switch self {
case .backup:
return R.string.localizable.keystoreAccessKeyHdBackup()
case .prepareForVerification:
return R.string.localizable.keystoreAccessKeyHdPrepareToVerify()
}
}
}
protocol Keystore {
static var current: Wallet? { get }
var hasWallets: Bool { get }
var isUserPresenceCheckPossible: Bool { get }
var wallets: [Wallet] { get }
var recentlyUsedWallet: Wallet? { get set }
@available(iOS 10.0, *)
func createAccount(completion: @escaping (Result<EthereumAccount, KeystoreError>) -> Void)
func importWallet(type: ImportType, completion: @escaping (Result<Wallet, KeystoreError>) -> Void)
func createAccount() -> Result<EthereumAccount, KeystoreError>
func elevateSecurity(forAccount account: EthereumAccount) -> Bool
func exportRawPrivateKeyForNonHdWalletForBackup(forAccount: EthereumAccount, newPassword: String, completion: @escaping (Result<String, KeystoreError>) -> Void)
func exportSeedPhraseOfHdWallet(forAccount account: EthereumAccount, reason: KeystoreExportReason, completion: @escaping (Result<String, KeystoreError>) -> Void)
func verifySeedPhraseOfHdWallet(_ seedPhrase: String, forAccount account: EthereumAccount, completion: @escaping (Result<Bool, KeystoreError>) -> Void)
func delete(wallet: Wallet, completion: @escaping (Result<Void, KeystoreError>) -> Void)
func isHdWallet(account: EthereumAccount) -> Bool
func isHdWallet(wallet: Wallet) -> Bool
func isKeystore(wallet: Wallet) -> Bool
func isWatched(wallet: Wallet) -> Bool
func isProtectedByUserPresence(account: EthereumAccount) -> Bool
func signPersonalMessage(_ data: Data, for account: EthereumAccount) -> Result<Data, KeystoreError>
func signTypedMessage(_ datas: [EthTypedData], for account: EthereumAccount) -> Result<Data, KeystoreError>
func signMessage(_ data: Data, for account: EthereumAccount) -> Result<Data, KeystoreError>
func signHash(_ data: Data, for account: EthereumAccount) -> Result<Data, KeystoreError>
func signTransaction(_ signTransaction: UnsignedTransaction) -> Result<Data, KeystoreError>
}

@ -0,0 +1,120 @@
// Copyright SIX DAY LLC. All rights reserved.
import BigInt
import Foundation
import Result
import KeychainSwift
import CryptoSwift
import TrustKeystore
enum FileBasedKeystoreError: LocalizedError {
case protectionDisabled
}
class LegacyFileBasedKeystore {
private let keychain: KeychainSwift
private let datadir = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]
private let keyStore: KeyStore
private let defaultKeychainAccess: KeychainSwiftAccessOptions = .accessibleWhenUnlockedThisDeviceOnly(userPresenceRequired: false)
private let userDefaults: UserDefaults
let keystoreDirectory: URL
public init(
keychain: KeychainSwift = KeychainSwift(keyPrefix: Constants.keychainKeyPrefix),
keyStoreSubfolder: String = "/keystore",
userDefaults: UserDefaults = UserDefaults.standard
) throws {
if !UIApplication.shared.isProtectedDataAvailable {
throw FileBasedKeystoreError.protectionDisabled
}
self.keystoreDirectory = URL(fileURLWithPath: datadir + keyStoreSubfolder)
self.keychain = keychain
self.keychain.synchronizable = false
self.keyStore = try KeyStore(keydir: keystoreDirectory)
self.userDefaults = userDefaults
}
func getPrivateKeyFromKeystoreFile(json: String, password: String) -> Result<Data, KeystoreError> {
let newPassword = PasswordGenerator.generateRandom()
guard let data = json.data(using: .utf8) else { return .failure(.failedToDecryptKey) }
guard let key = try? JSONDecoder().decode(KeystoreKey.self, from: data) else { return .failure(.failedToImportPrivateKey) }
guard let privateKey = try? key.decrypt(password: password) else { return .failure(.failedToDecryptKey) }
return .success(privateKey)
}
func export(privateKey: Data, newPassword: String) -> Result<String, KeystoreError> {
switch convertPrivateKeyToKeystoreFile(privateKey: privateKey, passphrase: newPassword) {
case .success(let dict):
if let jsonString = dict.jsonString {
return .success(jsonString)
} else {
return .failure(.failedToExportPrivateKey)
}
case .failure(let error):
return .failure(.failedToExportPrivateKey)
}
}
private func exportPrivateKey(account: EthereumAccount) -> Result<Data, KeystoreError> {
guard let password = getPassword(for: account) else { return .failure(KeystoreError.accountNotFound) }
guard let account = getAccount(forAddress: account.address) else { return .failure(.accountNotFound) }
do {
let privateKey = try keyStore.exportPrivateKey(account: account, password: password)
return .success(privateKey)
} catch {
return .failure(KeystoreError.failedToExportPrivateKey)
}
}
@discardableResult func delete(wallet: Wallet) -> Result<Void, KeystoreError> {
switch wallet.type {
case .real(let ethereumAccount):
guard let account = getAccount(forAddress: ethereumAccount.address) else { return .failure(.accountNotFound) }
guard let password = getPassword(for: ethereumAccount) else { return .failure(.failedToDeleteAccount) }
do {
try keyStore.delete(account: account, password: password)
return .success(())
} catch {
return .failure(.failedToDeleteAccount)
}
case .watch(let address):
return .success(())
}
}
func getPassword(for account: EthereumAccount) -> String? {
//This has to be lowercased due to legacy reasons it had been written to as lowercased() earlier
return keychain.get(account.address.eip55String.lowercased())
}
func getAccount(forAddress address: AlphaWallet.Address) -> Account? {
return keyStore.account(for: .init(address: address))
}
func convertPrivateKeyToKeystoreFile(privateKey: Data, passphrase: String) -> Result<[String: Any], KeystoreError> {
do {
let key = try KeystoreKey(password: passphrase, key: privateKey)
let data = try JSONEncoder().encode(key)
let dict = try JSONSerialization.jsonObject(with: data, options: []) as! [String: Any]
return .success(dict)
} catch {
return .failure(KeystoreError.failedToImportPrivateKey)
}
}
func migrateKeystoreFilesToRawPrivateKeysInKeychain() {
guard let etherKeystore = try? EtherKeystore() else { return }
guard !etherKeystore.hasMigratedFromKeystoreFiles else { return }
for each in keyStore.accounts {
switch exportPrivateKey(account: .init(account: each)) {
case .success(let privateKey):
etherKeystore.importWallet(type: .privateKey(privateKey: privateKey), completion: { _ in })
case .failure:
break
}
}
}
}

@ -0,0 +1,180 @@
// Copyright © 2019 Stormbird PTE. LTD.
import Foundation
import LocalAuthentication
import Security
class SecureEnclave {
enum Error: LocalizedError {
case keyAlreadyExists(name: String)
case cannotAccessPrivateKey(osStatus: OSStatus)
case cannotAccessPublicKey
case encryptionNotSupported(algorithm: SecKeyAlgorithm)
case decryptionNotSupported(algorithm: SecKeyAlgorithm)
case cannotEncrypt
case cannotDecrypt
case unexpected(description: String)
var errorDescription: String {
switch self {
case .keyAlreadyExists(let name):
return "Encryption key already exist for: \(name)"
case .cannotAccessPrivateKey(let osStatus):
return "Cannot access private key because: \(osStatus)"
case .cannotAccessPublicKey:
return "Cannot access public key"
case .encryptionNotSupported(let algorithm):
return "Encryption not supported: \(algorithm)"
case .decryptionNotSupported(let algorithm):
return "Decryption not supported: \(algorithm)"
case .cannotEncrypt:
return "Cannot encrypt"
case .cannotDecrypt:
return "Cannot decrypt"
case .unexpected(let description):
return description
}
}
}
private let requiresBiometry = true
private let numberOfBitsInKey = 256
private let algorithm = SecKeyAlgorithm.eciesEncryptionCofactorVariableIVX963SHA256AESGCM
private let userPresenceRequired: Bool
private var isSimulator: Bool {
return TARGET_OS_SIMULATOR != 0
}
init(userPresenceRequired: Bool = false) {
self.userPresenceRequired = userPresenceRequired
}
private func getPrivateKey(withName name: String, withContext context: LAContext) throws -> SecKey {
let params: [String: Any] = [
kSecClass as String: kSecClassKey,
kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom,
kSecAttrApplicationTag as String: tagData(fromName: name),
kSecReturnRef as String: true,
kSecUseAuthenticationContext as String: context,
]
var raw: CFTypeRef?
let status = SecItemCopyMatching(params as CFDictionary, &raw)
guard status == errSecSuccess, let result = raw else {
throw Error.cannotAccessPrivateKey(osStatus: status)
}
return result as! SecKey
}
private func getPrivateKeyCount(withName name: String) -> Int {
let params: [String: Any] = [
kSecClass as String: kSecClassKey,
kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom,
kSecAttrApplicationTag as String: tagData(fromName: name),
kSecReturnRef as String: true,
kSecMatchLimit as String: kSecMatchLimitAll
]
var raw: CFTypeRef?
let status = SecItemCopyMatching(params as CFDictionary, &raw)
if status == errSecSuccess, let all = raw as? [SecKey] {
return all.count
} else {
return 0
}
}
private func encrypt(plainTextData: Data, withPublicKey publicKey: SecKey) throws -> Data {
guard SecKeyIsAlgorithmSupported(publicKey, .encrypt, algorithm) else {
throw Error.encryptionNotSupported(algorithm: algorithm)
}
var error: Unmanaged<CFError>?
guard let cipherTextData = SecKeyCreateEncryptedData(publicKey, algorithm, plainTextData as CFData, &error) as? Data else {
throw Error.cannotEncrypt
}
return cipherTextData
}
private func decrypt(cipherText: Data, privateKey: SecKey) throws -> Data {
guard SecKeyIsAlgorithmSupported(privateKey, .decrypt, algorithm) else {
throw Error.decryptionNotSupported(algorithm: algorithm)
}
var error: Unmanaged<CFError>?
guard let plainTextData = SecKeyCreateDecryptedData(privateKey, algorithm, cipherText as CFData, &error) as Data? else {
throw Error.cannotDecrypt
}
return plainTextData
}
private func tagData(fromName name: String) -> Data {
return name.data(using: .utf8)!
}
private func createPrivateKey(withName name: String) throws -> SecKey {
let count = getPrivateKeyCount(withName: name)
// swiftlint:disable empty_count
guard count == 0 else { throw Error.keyAlreadyExists(name: name) }
// swiftlint:enable empty_count
let flags: SecAccessControlCreateFlags
if requiresBiometry && userPresenceRequired {
flags = [.privateKeyUsage, .userPresence]
} else {
flags = .privateKeyUsage
}
guard let access = SecAccessControlCreateWithFlags(kCFAllocatorDefault, kSecAttrAccessibleWhenUnlockedThisDeviceOnly, flags, nil) else { throw Error.unexpected(description: "Unable to create flags to create private key") }
var attributes: [String: Any] = [
kSecAttrKeyType as String : kSecAttrKeyTypeECSECPrimeRandom,
kSecAttrKeySizeInBits as String: numberOfBitsInKey,
kSecPrivateKeyAttrs as String : [
kSecAttrIsPermanent as String: true,
kSecAttrApplicationTag as String: tagData(fromName: name),
kSecAttrAccessControl as String: access
]
]
if isSimulator {
//do nothing
} else {
attributes[kSecAttrTokenID as String] = kSecAttrTokenIDSecureEnclave
}
var error: Unmanaged<CFError>?
guard let privateKey = SecKeyCreateRandomKey(attributes as CFDictionary, &error) else {
throw error!.takeRetainedValue() as Swift.Error
}
return privateKey
}
//MARK: Public interface
func encrypt(plainTextData: Data, withPublicKeyFromLabel name: String, withContext context: LAContext) throws -> Data {
let privateKey: SecKey
do {
privateKey = try getPrivateKey(withName: name, withContext: context)
} catch {
privateKey = try createPrivateKey(withName: name)
}
guard let publicKey = SecKeyCopyPublicKey(privateKey) else { throw Error.cannotAccessPublicKey }
return try encrypt(plainTextData: plainTextData, withPublicKey: publicKey)
}
func decrypt(cipherText: Data, withPrivateKeyFromLabel name: String, withContext context: LAContext) throws -> Data {
let privateKey = try getPrivateKey(withName: name, withContext: context)
return try decrypt(cipherText: cipherText, privateKey: privateKey)
}
func deletePrivateKeys(withName name: String) {
let params: [String: Any] = [
kSecClass as String: kSecClassKey,
kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom,
kSecAttrApplicationTag as String: tagData(fromName: name)
]
let status = SecItemDelete(params as CFDictionary)
}
}

@ -3,6 +3,7 @@
"app.device.jailbreak.title" = "DEVICE SECURITY COMPROMISED";
"Done" = "Done";
"Cancel" = "Cancel";
"Skip" = "Skip";
"Save" = "Save";
"Delete" = "Delete";
"Reload" = "Reload";
@ -71,10 +72,26 @@
"settings.contactUs.title" = "Contact Us";
"transactions.noTransactions.label.title" = "No Transactions Yet!";
"wallet.navigation.title" = "Wallets";
"wallet.types.hdWallets" = "HD Wallets";
"wallet.types.keystoreWallets" = "Keystore Wallets";
"wallet.types.watchedWallets" = "Watched Wallets";
"wallet.create.button.title" = "Create Wallet";
"wallet.watch.button.title" = "Watch Wallet";
"wallet.create.inProgress" = "Creating wallet...";
"wallet.import.button.title" = "Import Wallet";
"wallets.backup.alertSheet.title" = "Backup Encrypted Private Key";
"wallets.backupKeystoreWallet.alertSheet.title" = "Export Keystore JSON";
"wallets.backupKeystoreWallet.alertSheet.title.shorter" = "Export Keystore";
"wallets.backupKeystoreWallet.introduction.title" = "What is Keystore JSON?";
"wallets.backupKeystoreWallet.introduction.description" = "A Keystore is a text file. You can copy its contents when you want to import your wallet. This is a safe way to back up a wallet.";
"wallets.backupHdWallet.alertSheet.title" = "Show Seed Phrase";
"wallets.showSeedPhrase.title" = "Seed Phrase";
"wallets.showSeedPhrase.subtitle" = "Making a backup is very simple and safe:";
"wallets.showSeedPhrase.description" = "just write down 12 words and keep them in a safe place, offline";
"wallets.showSeedPhrase.testSeedPhrase" = "Test My Seed Phrase";
"wallets.showSeedPhrase.doNotTakeScreenshotDescription" = "It's not a good idea to take a screenshot of your seed phrase";
"wallets.verifySeedPhrase.title" = "Verify Seed Phrase";
"wallets.verifySeedPhrase.wrong" = "Invalid seed phrase. Please check and try again.";
"wallets.verifySeedPhrase.doNotTakeScreenshotDescription" = "It's not a good idea to take a screenshot of your seed phrase";
"transactions.tabbar.item.title" = "Transactions";
"transactions.received.ether" = "You have received %@ %@";
"transactions.received.ether.notification.prompt" = "Allow Notifications When You Receive %@?";
@ -95,9 +112,13 @@
"importWallet.import.button.title" = "Import";
"importWallet.import.invalidAddress" = "Invalid Ethereum Address";
"importWallet.import.invalidPrivateKey" = "Private Key has to be 64 characters long";
"Keystore JSON" = "Encrypted Private Key";
"importWallet.import.invalidMnemonic" = "Seed phrase has to be 12 words";
"importWallet.import.fromCloud.title" = "Import from iCloud/Dropbox/Google Drive";
"importWallet.import.seedPhrase.description" = "If your seed phrase is not in English, please convert it to English first";
"Keystore JSON" = "Keystore JSON";
"More Details" = "More Details";
"Keystore" = "Encrypted Private Key";
"Keystore" = "Keystore JSON";
"Keystore.shorter" = "Keystore";
"Private Key" = "Private Key";
"settings.currency.button.title" = "Currency";
"settings.network.test.label.title" = "Test";
@ -124,7 +145,8 @@
"Deleting" = "Deleting";
"Ethereum Address" = "Ethereum Address";
"InCoordinatorError.onlyWatchAccount" = "This wallet could be only used for watching. Import Private Key/Keystore to sign transactions";
"Mnemonic" = "Mnemonic";
"mnemonic.shorter" = "Seed";
"mnemonic" = "Seed Phrase";
"Watch" = "Watch";
"passphrase.remember.label.title" = "Write this down, and keep it private and secure. You won't be able to restore your wallet if you lose this!";
"recoveryPhrase.navigation.title" = "Recovery Phrase";
@ -144,10 +166,11 @@
"browser.noBookmarks.label.title" = "No bookmarks yet!";
"browser.url.textfield.placeholder" = "Search or enter website URL";
"configureTransaction.gasPriceGwei.label.title" = "Gas Price (Gwei)";
"enterPassword.confirmPassword.textField.placeholder" = "Confirm Password";
"enterPassword.navigation.title" = "Backup Password";
"enterPassword.password.header.placeholder" = "Password used to encrypt your backup file to keep it secure!";
"enterPassword.password.textField.placeholder" = "Password";
"enterPassword.navigation.title" = "Set Keystore Password";
"enterPassword.navigation.title.shorter" = "Keystore Password";
"enterPassword.password.header.placeholder" = "You must remember your password. We do not store your password elsewhere. KeyStore JSON file is always encrypted, otherwise whoever has it has access to your money.";
"enterPassword.password.textField.placeholder" = "Password to encrypt Keystore JSON";
"enterPassword.password.textField.placeholder.shorter" = "Password to encrypt Keystore";
"lock.create.passcode.view.model.confirm" = "Please re-enter your passcode";
"lock.create.passcode.view.model.initial" = "Enter a new password";
"lock.create.passcode.view.model.title" = "Set Passcode";
@ -287,6 +310,7 @@
"a.send.receive.button.title" = "Send/Receive";
"a.send.recipient.address.title" = "ADDRESS";
"a.send.balance.insufficient" = "Insufficient balance";
"accounts.confirm.delete.action" = "Lose this Wallet";
"accounts.confirm.delete.title" = "Are you sure you would like to delete this wallet?";
"accounts.confirm.delete.message" = "Make sure you have backup of your wallet.";
"accounts.confirm.delete.okTitle" = "Delete";
@ -301,6 +325,7 @@
"accounts.delete.error.accountNotFound" = "Account not found";
"accounts.delete.error.failedToSignMessage" = "Failed to sign message";
"accounts.delete.error.failedToExportPrivateKey" = "Failed to export private key";
"accounts.delete.error.failedToExportSeed" = "Failed to export wallet seed";
"Chain ID" = "Chain ID";
"Endpoint" = "Endpoint";
"settings.preferences.button.title" = "Show Tokens on Launch";
@ -346,3 +371,34 @@
"TokenScript.type2.conflictingFiles" = "Conflicting TokenScript Files";
"TokenScript.type2.oldSchemaVersion" = "Old TokenScript Version";
"TokenScript.type2.invalidSignature" = "Invalid Signature";
"gettingStarted.subtitle" = "Welcome to AlphaWallet";
"gettingStarted.newWallet" = "Create a New Wallet";
"gettingStarted.alreadyHaveWallet" = "I already have a wallet:";
"backup.prompt.title" = "Your Wallet is not backed up!";
"backup.prompt.description.withAmount" = "You have not backed up your wallet yet. You have %@ USD net. Act now.";
"backup.prompt.backupButton.title" = "Back up Wallet";
"backup.prompt.backup.remindLater" = "Remind me to back up later?";
"backup.prompt.afterReceivingEther.title" = "Just received %@ ETH?";
"backup.prompt.description.withoutAmount" = "Remember to back up your wallet by a secure seed phrase.";
"backup.prompt.afterInterval.title" = "Time to back up your Wallet";
"backup.prompt.afterInterval.description" = "We highly recommend to back up your wallet";
"backup.prompt.afterHittingThreshold.title" = "Your Wallet is at risk!";
"backup.prompt.afterHittingThreshold.description" = "You have not backed up your wallet yet. You have %@ USD net. Act now. ";
"onboarding.newWallet.findAddress.title" = "Find your Wallet Address";
"onboarding.newWallet.findAddress.description" = "Tap here to find your Ethereum Wallet address";
"keystore.accessKey.nonHd.backup" = "Accessing your wallet key to back it up";
"keystore.accessKey.hd.backup" = "Accessing your wallet seed phrase to back it up";
"keystore.accessKey.hd.verify" = "Accessing your wallet seed phrase to verify against it";
"keystore.accessKey.hd.prepareToVerify" = "Accessing your wallet seed phrase to let you verify against it";
"keystore.accessKey.sign" = "Accessing your wallet key to sign";
"keystore.accessKey.needImportOrPasscode" = "Can't access your wallet. Import your wallet again or enable passcode";
"keystore.accessKey.cancelled" = "You have denied access to your wallet";
"keystore.accessKey.lock.fail" = "Can't lock your wallet key. Maybe your iOS Passcode is not enabled?";
"keystore.accessKey.hd.lock" = "To lock your seed phrase to increase security";
"keystore.accessKey.nonHd.lock" = "To lock your private key to increase security";
"keystore.lock.wallet.seed.title" = "Lock your Seed Phrase to Increase Security";
"keystore.lock.wallet.privateKey.title" = "Lock your Private Key to Increase Security";
"keystore.lock.wallet.seed.description" = "AlphaWallet security is being upgraded to the best achievable. Lock your seed phrase with biometrics";
"keystore.lock.wallet.privateKey.description" = "AlphaWallet security is being upgraded to the best achievable. Lock your private key with biometrics";
"keystore.lock.wallet.seed.button" = "Lock Seed Phrase";
"keystore.lock.wallet.privateKey.button" = "Lock Private Key";

@ -3,6 +3,7 @@
"app.device.jailbreak.title" = "SEGURIDAD DEL DISPOSITIVO COMPROMETIDA";
"Done" = "Hecho";
"Cancel" = "Cancelar";
"Skip" = "Skip";
"Save" = "Guardar";
"Delete" = "Eliminar";
"Reload" = "Volver a cargar";
@ -71,10 +72,26 @@
"settings.contactUs.title" = "Contact Us";
"transactions.noTransactions.label.title" = "¡Todavía no hay transacciones!";
"wallet.navigation.title" = "Monederos";
"wallet.types.hdWallets" = "HD Wallets";
"wallet.types.keystoreWallets" = "Keystore Wallets";
"wallet.types.watchedWallets" = "Watched Wallets";
"wallet.create.button.title" = "Crear monedero";
"wallet.watch.button.title" = "Watch Wallet";
"wallet.create.inProgress" = "Creando monedero...";
"wallet.import.button.title" = "Importar monedero";
"wallets.backup.alertSheet.title" = "Copia de seguridad de clave privada cifrada";
"wallets.backupKeystoreWallet.alertSheet.title" = "Export Keystore JSON";
"wallets.backupKeystoreWallet.alertSheet.title.shorter" = "Export Keystore";
"wallets.backupKeystoreWallet.introduction.title" = "What is Keystore JSON?";
"wallets.backupKeystoreWallet.introduction.description" = "A Keystore is a text file. You can copy its contents when you want to import your wallet. This is a safe way to back up a wallet.";
"wallets.backupHdWallet.alertSheet.title" = "Show Seed Phrase";
"wallets.showSeedPhrase.title" = "Seed Phrase";
"wallets.showSeedPhrase.subtitle" = "Making a backup is very simple and safe:";
"wallets.showSeedPhrase.description" = "just write down 12 words and keep them in a safe place, offline";
"wallets.showSeedPhrase.testSeedPhrase" = "Test My Seed Phrase";
"wallets.showSeedPhrase.doNotTakeScreenshotDescription" = "It's not a good idea to take a screenshot of your seed phrase";
"wallets.verifySeedPhrase.title" = "Verify Seed Phrase";
"wallets.verifySeedPhrase.wrong" = "Invalid seed phrase. Please check and try again.";
"wallets.verifySeedPhrase.doNotTakeScreenshotDescription" = "It's not a good idea to take a screenshot of your seed phrase";
"transactions.tabbar.item.title" = "transacciones";
"transactions.received.ether" = "You have received %@ %@";
"transactions.received.ether.notification.prompt" = "Allow Notifications When You Receive %@?";
@ -95,9 +112,13 @@
"importWallet.import.button.title" = "Importar";
"importWallet.import.invalidAddress" = "Dirección de Ethereum no válida";
"importWallet.import.invalidPrivateKey" = "La clave privada debe tener 64 caracteres de longitud";
"Keystore JSON" = "Clave privada cifrada";
"importWallet.import.invalidMnemonic" = "Seed phrase has to be 12 words";
"importWallet.import.fromCloud.title" = "Import from iCloud/Dropbox/Google Drive";
"importWallet.import.seedPhrase.description" = "If your seed phrase is not in English, please convert it to English first";
"Keystore JSON" = "Keystore JSON";
"More Details" = "Más detalles";
"Keystore" = "Clave privada cifrada";
"Keystore" = "Keystore JSON";
"Keystore.shorter" = "Keystore";
"Private Key" = "Clave privada";
"settings.currency.button.title" = "Moneda";
"settings.network.test.label.title" = "Prueba";
@ -124,7 +145,8 @@
"Deleting" = "Eliminando";
"Ethereum Address" = "Dirección de Ethereum";
"InCoordinatorError.onlyWatchAccount" = "Este monedero solo se podría utilizar para observar. Importa la clave privada/almacén de claves para firmar transacciones";
"Mnemonic" = "Mnemotécnico";
"mnemonic.shorter" = "Seed";
"mnemonic" = "Seed Phrase";
"Watch" = "Observar";
"passphrase.remember.label.title" = "Escríbela y guárdala en un lugar privado y seguro. ¡No podrás restaurar tu monedero si la pierdes!";
"recoveryPhrase.navigation.title" = "Frase de recuperación";
@ -144,10 +166,11 @@
"browser.noBookmarks.label.title" = "No bookmarks yet!";
"browser.url.textfield.placeholder" = "Search or enter website URL";
"configureTransaction.gasPriceGwei.label.title" = "Precio del gas (Gwei)";
"enterPassword.confirmPassword.textField.placeholder" = "Confirmar contraseña";
"enterPassword.navigation.title" = "Contraseña de copia de seguridad";
"enterPassword.password.header.placeholder" = "¡Contraseña usada para cifrar el archivo de tu copia de seguridad y así mantenerlo seguro!";
"enterPassword.password.textField.placeholder" = "Contraseña";
"enterPassword.navigation.title" = "Set Keystore Password";
"enterPassword.navigation.title.shorter" = "Keystore Password";
"enterPassword.password.header.placeholder" = "You must remember your password. We do not store your password elsewhere. KeyStore JSON file is always encrypted, otherwise whoever has it has access to your money.";
"enterPassword.password.textField.placeholder" = "Password to encrypt Keystore JSON";
"enterPassword.password.textField.placeholder.shorter" = "Password to encrypt Keystore";
"lock.create.passcode.view.model.confirm" = "Vuelve a introducir tu código de acceso";
"lock.create.passcode.view.model.initial" = "Introduce una contraseña nueva";
"lock.create.passcode.view.model.title" = "Establecer código de acceso";
@ -285,6 +308,7 @@
"a.send.receive.button.title" = "Enviar/Recibir";
"a.send.recipient.address.title" = "DIRECCIÓN";
"a.send.balance.insufficient" = "Saldo insuficiente";
"accounts.confirm.delete.action" = "Lose this Wallet";
"accounts.confirm.delete.title" = "¿Seguro que deseas eliminar este monedero?";
"accounts.confirm.delete.message" = "Asegúrate de tener una copia de seguridad de tu monedero.";
"accounts.confirm.delete.okTitle" = "Eliminar";
@ -299,6 +323,7 @@
"accounts.delete.error.accountNotFound" = "No se ha encontrado la cuenta";
"accounts.delete.error.failedToSignMessage" = "Error al firmar el mensaje";
"accounts.delete.error.failedToExportPrivateKey" = "Error al exportar la clave privada";
"accounts.delete.error.failedToExportSeed" = "Failed to export wallet seed";
"Chain ID" = "ID de cadena";
"Endpoint" = "Punto final";
"settings.preferences.button.title" = "Mostrar tokens al iniciar";
@ -346,3 +371,34 @@
"TokenScript.type2.conflictingFiles" = "Conflicting TokenScript Files";
"TokenScript.type2.oldSchemaVersion" = "Old TokenScript Version";
"TokenScript.type2.invalidSignature" = "Invalid Signature";
"gettingStarted.subtitle" = "Welcome to AlphaWallet";
"gettingStarted.newWallet" = "Create a New Wallet";
"gettingStarted.alreadyHaveWallet" = "I already have a wallet:";
"backup.prompt.title" = "Your Wallet is not backed up!";
"backup.prompt.description.withAmount" = "You have not backed up your wallet yet. You have %@ USD net. Act now.";
"backup.prompt.backupButton.title" = "Back up Wallet";
"backup.prompt.backup.remindLater" = "Remind me to back up later?";
"backup.prompt.afterReceivingEther.title" = "Just received %@ ETH?";
"backup.prompt.description.withoutAmount" = "Remember to back up your wallet by a secure seed phrase.";
"backup.prompt.afterInterval.title" = "Time to back up your Wallet";
"backup.prompt.afterInterval.description" = "We highly recommend to back up your wallet";
"backup.prompt.afterHittingThreshold.title" = "Your Wallet is at risk!";
"backup.prompt.afterHittingThreshold.description" = "You have not backed up your wallet yet. You have %@ USD net. Act now. ";
"onboarding.newWallet.findAddress.title" = "Find your Wallet Address";
"onboarding.newWallet.findAddress.description" = "Tap here to find your Ethereum Wallet address";
"keystore.accessKey.nonHd.backup" = "Accessing your wallet key to back it up";
"keystore.accessKey.hd.backup" = "Accessing your wallet seed phrase to back it up";
"keystore.accessKey.hd.verify" = "Accessing your wallet seed phrase to verify against it";
"keystore.accessKey.hd.prepareToVerify" = "Accessing your wallet seed phrase to let you verify against it";
"keystore.accessKey.sign" = "Accessing your wallet key to sign";
"keystore.accessKey.needImportOrPasscode" = "Can't access your wallet. Import your wallet again or enable passcode";
"keystore.accessKey.cancelled" = "You have denied access to your wallet";
"keystore.accessKey.lock.fail" = "Can't lock your wallet key. Maybe your iOS Passcode is not enabled?";
"keystore.accessKey.hd.lock" = "To lock your seed phrase to increase security";
"keystore.accessKey.nonHd.lock" = "To lock your private key to increase security";
"keystore.lock.wallet.seed.title" = "Lock your Seed Phrase to Increase Security";
"keystore.lock.wallet.privateKey.title" = "Lock your Private Key to Increase Security";
"keystore.lock.wallet.seed.description" = "AlphaWallet security is being upgraded to the best achievable. Lock your seed phrase with biometrics";
"keystore.lock.wallet.privateKey.description" = "AlphaWallet security is being upgraded to the best achievable. Lock your private key with biometrics";
"keystore.lock.wallet.seed.button" = "Lock Seed Phrase";
"keystore.lock.wallet.privateKey.button" = "Lock Private Key";

@ -3,6 +3,7 @@
"app.device.jailbreak.title" = "デバイスのセキュリティが危険にさらされています";
"Done" = "完了";
"Cancel" = "キャンセル";
"Skip" = "Skip";
"Save" = "保存";
"Delete" = "削除";
"Reload" = "再読み込み";
@ -71,10 +72,26 @@
"settings.contactUs.title" = "Contact Us";
"transactions.noTransactions.label.title" = "トランザクションはまだありません!";
"wallet.navigation.title" = "ウォレット";
"wallet.types.hdWallets" = "HD Wallets";
"wallet.types.keystoreWallets" = "Keystore Wallets";
"wallet.types.watchedWallets" = "Watched Wallets";
"wallet.create.button.title" = "ウォレットを作成";
"wallet.watch.button.title" = "Watch Wallet";
"wallet.create.inProgress" = "ウォレットを作成しています...";
"wallet.import.button.title" = "ウォレットをインポート";
"wallets.backup.alertSheet.title" = "暗号化された秘密鍵のバックアップ";
"wallets.backupKeystoreWallet.alertSheet.title" = "Export Keystore JSON";
"wallets.backupKeystoreWallet.alertSheet.title.shorter" = "Export Keystore";
"wallets.backupKeystoreWallet.introduction.title" = "What is Keystore JSON?";
"wallets.backupKeystoreWallet.introduction.description" = "A Keystore is a text file. You can copy its contents when you want to import your wallet. This is a safe way to back up a wallet.";
"wallets.backupHdWallet.alertSheet.title" = "Show Seed Phrase";
"wallets.showSeedPhrase.title" = "Seed Phrase";
"wallets.showSeedPhrase.subtitle" = "Making a backup is very simple and safe:";
"wallets.showSeedPhrase.description" = "just write down 12 words and keep them in a safe place, offline";
"wallets.showSeedPhrase.testSeedPhrase" = "Test My Seed Phrase";
"wallets.showSeedPhrase.doNotTakeScreenshotDescription" = "It's not a good idea to take a screenshot of your seed phrase";
"wallets.verifySeedPhrase.title" = "Verify Seed Phrase";
"wallets.verifySeedPhrase.wrong" = "Invalid seed phrase. Please check and try again.";
"wallets.verifySeedPhrase.doNotTakeScreenshotDescription" = "It's not a good idea to take a screenshot of your seed phrase";
"transactions.tabbar.item.title" = "私のトランザクション";
"transactions.received.ether" = "You have received %@ %@";
"transactions.received.ether.notification.prompt" = "Allow Notifications When You Receive %@?";
@ -95,9 +112,13 @@
"importWallet.import.button.title" = "インポート";
"importWallet.import.invalidAddress" = "無効な Ethereum アドレス";
"importWallet.import.invalidPrivateKey" = "秘密鍵は64文字でなければなりません";
"Keystore JSON" = "暗号化された秘密鍵";
"importWallet.import.invalidMnemonic" = "Seed phrase has to be 12 words";
"importWallet.import.fromCloud.title" = "Import from iCloud/Dropbox/Google Drive";
"importWallet.import.seedPhrase.description" = "If your seed phrase is not in English, please convert it to English first";
"Keystore JSON" = "Keystore JSON";
"More Details" = "詳細";
"Keystore" = "暗号化された秘密鍵";
"Keystore" = "Keystore JSON";
"Keystore.shorter" = "Keystore";
"Private Key" = "秘密鍵";
"settings.currency.button.title" = "通貨";
"settings.network.test.label.title" = "テスト";
@ -124,7 +145,8 @@
"Deleting" = "削除";
"Ethereum Address" = "Ethereum アドレス";
"InCoordinatorError.onlyWatchAccount" = "このウォレットは、見るためだけに使用できます。秘密鍵/キーストアをインポートしてトランザクションを署名します";
"Mnemonic" = "ニーモニック";
"mnemonic.shorter" = "Seed";
"mnemonic" = "Seed Phrase";
"Watch" = "見る";
"passphrase.remember.label.title" = "これを書き留めて、秘密にして安全に保管してください。これを紛失した場合、ウォレットを復元することはできません!";
"recoveryPhrase.navigation.title" = "回復フレーズ";
@ -144,10 +166,11 @@
"browser.noBookmarks.label.title" = "No bookmarks yet!";
"browser.url.textfield.placeholder" = "Search or enter website URL";
"configureTransaction.gasPriceGwei.label.title" = "ガス価格 (Gwei)";
"enterPassword.confirmPassword.textField.placeholder" = "パスワードを確認";
"enterPassword.navigation.title" = "バックアップのパスワード";
"enterPassword.password.header.placeholder" = "安全に保つためにバックアップ ファイルの暗号化に使用されるパスワードです!";
"enterPassword.password.textField.placeholder" = "パスワード";
"enterPassword.navigation.title" = "Set Keystore Password";
"enterPassword.navigation.title.shorter" = "Keystore Password";
"enterPassword.password.header.placeholder" = "You must remember your password. We do not store your password elsewhere. KeyStore JSON file is always encrypted, otherwise whoever has it has access to your money.";
"enterPassword.password.textField.placeholder" = "Password to encrypt Keystore JSON";
"enterPassword.password.textField.placeholder.shorter" = "Password to encrypt Keystore";
"lock.create.passcode.view.model.confirm" = "パスコードを再入力してください";
"lock.create.passcode.view.model.initial" = "新しいパスワードを入力してください";
"lock.create.passcode.view.model.title" = "パスコードの設定";
@ -285,6 +308,7 @@
"a.send.receive.button.title" = "送信/受信";
"a.send.recipient.address.title" = "アドレス";
"a.send.balance.insufficient" = "残高不足";
"accounts.confirm.delete.action" = "Lose this Wallet";
"accounts.confirm.delete.title" = "このウォレットを削除してもよろしいですか?";
"accounts.confirm.delete.message" = "あなたのウォレットのバックアップがあることを確認してください。";
"accounts.confirm.delete.okTitle" = "削除";
@ -299,6 +323,7 @@
"accounts.delete.error.accountNotFound" = "アカウントが見つかりません";
"accounts.delete.error.failedToSignMessage" = "メッセージの署名に失敗しました";
"accounts.delete.error.failedToExportPrivateKey" = "秘密鍵をエクスポートできませんでした";
"accounts.delete.error.failedToExportSeed" = "Failed to export wallet seed";
"Chain ID" = "チェーン ID";
"Endpoint" = "エンドポイント";
"settings.preferences.button.title" = "起動時にトークンを表示";
@ -346,3 +371,34 @@
"TokenScript.type2.conflictingFiles" = "Conflicting TokenScript Files";
"TokenScript.type2.oldSchemaVersion" = "Old TokenScript Version";
"TokenScript.type2.invalidSignature" = "Invalid Signature";
"gettingStarted.subtitle" = "Welcome to AlphaWallet";
"gettingStarted.newWallet" = "Create a New Wallet";
"gettingStarted.alreadyHaveWallet" = "I already have a wallet:";
"backup.prompt.title" = "Your Wallet is not backed up!";
"backup.prompt.description.withAmount" = "You have not backed up your wallet yet. You have %@ USD net. Act now.";
"backup.prompt.backupButton.title" = "Back up Wallet";
"backup.prompt.backup.remindLater" = "Remind me to back up later?";
"backup.prompt.afterReceivingEther.title" = "Just received %@ ETH?";
"backup.prompt.description.withoutAmount" = "Remember to back up your wallet by a secure seed phrase.";
"backup.prompt.afterInterval.title" = "Time to back up your Wallet";
"backup.prompt.afterInterval.description" = "We highly recommend to back up your wallet";
"backup.prompt.afterHittingThreshold.title" = "Your Wallet is at risk!";
"backup.prompt.afterHittingThreshold.description" = "You have not backed up your wallet yet. You have %@ USD net. Act now. ";
"onboarding.newWallet.findAddress.title" = "Find your Wallet Address";
"onboarding.newWallet.findAddress.description" = "Tap here to find your Ethereum Wallet address";
"keystore.accessKey.nonHd.backup" = "Accessing your wallet key to back it up";
"keystore.accessKey.hd.backup" = "Accessing your wallet seed phrase to back it up";
"keystore.accessKey.hd.verify" = "Accessing your wallet seed phrase to verify against it";
"keystore.accessKey.hd.prepareToVerify" = "Accessing your wallet seed phrase to let you verify against it";
"keystore.accessKey.sign" = "Accessing your wallet key to sign";
"keystore.accessKey.needImportOrPasscode" = "Can't access your wallet. Import your wallet again or enable passcode";
"keystore.accessKey.cancelled" = "You have denied access to your wallet";
"keystore.accessKey.lock.fail" = "Can't lock your wallet key. Maybe your iOS Passcode is not enabled?";
"keystore.accessKey.hd.lock" = "To lock your seed phrase to increase security";
"keystore.accessKey.nonHd.lock" = "To lock your private key to increase security";
"keystore.lock.wallet.seed.title" = "Lock your Seed Phrase to Increase Security";
"keystore.lock.wallet.privateKey.title" = "Lock your Private Key to Increase Security";
"keystore.lock.wallet.seed.description" = "AlphaWallet security is being upgraded to the best achievable. Lock your seed phrase with biometrics";
"keystore.lock.wallet.privateKey.description" = "AlphaWallet security is being upgraded to the best achievable. Lock your private key with biometrics";
"keystore.lock.wallet.seed.button" = "Lock Seed Phrase";
"keystore.lock.wallet.privateKey.button" = "Lock Private Key";

@ -3,6 +3,7 @@
"app.device.jailbreak.title" = "장치 보안이 침해되었습니다";
"Done" = "완료";
"Cancel" = "취소";
"Skip" = "Skip";
"Save" = "저장";
"Delete" = "삭제";
"Reload" = "다시 불러오기";
@ -71,10 +72,26 @@
"settings.contactUs.title" = "Contact Us";
"transactions.noTransactions.label.title" = "아직 거래가 없습니다!";
"wallet.navigation.title" = "지갑";
"wallet.types.hdWallets" = "HD Wallets";
"wallet.types.keystoreWallets" = "Keystore Wallets";
"wallet.types.watchedWallets" = "Watched Wallets";
"wallet.create.button.title" = "지갑 생성";
"wallet.watch.button.title" = "Watch Wallet";
"wallet.create.inProgress" = "지갑 생성 중...";
"wallet.import.button.title" = "지갑 가져오기";
"wallets.backup.alertSheet.title" = "암호화된 개인 키 백업";
"wallets.backupKeystoreWallet.alertSheet.title" = "Export Keystore JSON";
"wallets.backupKeystoreWallet.alertSheet.title.shorter" = "Export Keystore";
"wallets.backupKeystoreWallet.introduction.title" = "What is Keystore JSON?";
"wallets.backupKeystoreWallet.introduction.description" = "A Keystore is a text file. You can copy its contents when you want to import your wallet. This is a safe way to back up a wallet.";
"wallets.backupHdWallet.alertSheet.title" = "Show Seed Phrase";
"wallets.showSeedPhrase.title" = "Seed Phrase";
"wallets.showSeedPhrase.subtitle" = "Making a backup is very simple and safe:";
"wallets.showSeedPhrase.description" = "just write down 12 words and keep them in a safe place, offline";
"wallets.showSeedPhrase.testSeedPhrase" = "Test My Seed Phrase";
"wallets.showSeedPhrase.doNotTakeScreenshotDescription" = "It's not a good idea to take a screenshot of your seed phrase";
"wallets.verifySeedPhrase.title" = "Verify Seed Phrase";
"wallets.verifySeedPhrase.wrong" = "Invalid seed phrase. Please check and try again.";
"wallets.verifySeedPhrase.doNotTakeScreenshotDescription" = "It's not a good idea to take a screenshot of your seed phrase";
"transactions.tabbar.item.title" = "내 거래";
"transactions.received.ether" = "You have received %@ %@";
"transactions.received.ether.notification.prompt" = "Allow Notifications When You Receive %@?";
@ -95,9 +112,13 @@
"importWallet.import.button.title" = "가져오기";
"importWallet.import.invalidAddress" = "잘못된 이더리움 주소";
"importWallet.import.invalidPrivateKey" = "개인 키의 길이는 64자이어야 합니다.";
"Keystore JSON" = "암호화된 개인 키";
"importWallet.import.invalidMnemonic" = "Seed phrase has to be 12 words";
"importWallet.import.fromCloud.title" = "Import from iCloud/Dropbox/Google Drive";
"importWallet.import.seedPhrase.description" = "If your seed phrase is not in English, please convert it to English first";
"Keystore JSON" = "Keystore JSON";
"More Details" = "자세한 정보";
"Keystore" = "암호화된 개인 키";
"Keystore" = "Keystore JSON";
"Keystore.shorter" = "Keystore";
"Private Key" = "개인 키";
"settings.currency.button.title" = "통화";
"settings.network.test.label.title" = "테스트";
@ -124,7 +145,8 @@
"Deleting" = "삭제 중";
"Ethereum Address" = "이더리움 주소";
"InCoordinatorError.onlyWatchAccount" = "이 지갑은 열람용으로만 사용할 수 있습니다. 거래에 서명하려면 개인 키/키스토어를 가져오십시오";
"Mnemonic" = "니모닉";
"mnemonic.shorter" = "Seed";
"mnemonic" = "Seed Phrase";
"Watch" = "열람";
"passphrase.remember.label.title" = "이 정보를 적어두고, 안전하게 보관하십시오. 그렇지 않으면, 지갑을 잃어버릴 경우 복구할 수 없게 됩니다!";
"recoveryPhrase.navigation.title" = "복구 단계";
@ -144,10 +166,11 @@
"browser.noBookmarks.label.title" = "No bookmarks yet!";
"browser.url.textfield.placeholder" = "Search or enter website URL";
"configureTransaction.gasPriceGwei.label.title" = "가스 요금 (Gwei)";
"enterPassword.confirmPassword.textField.placeholder" = "비밀번호 확인";
"enterPassword.navigation.title" = "백업 비밀번호";
"enterPassword.password.header.placeholder" = "안전한 보관을 위하여 백업 파일을 암호화하는 데 사용되는 비밀번호!";
"enterPassword.password.textField.placeholder" = "비밀번호";
"enterPassword.navigation.title" = "Set Keystore Password";
"enterPassword.navigation.title.shorter" = "Keystore Password";
"enterPassword.password.header.placeholder" = "You must remember your password. We do not store your password elsewhere. KeyStore JSON file is always encrypted, otherwise whoever has it has access to your money.";
"enterPassword.password.textField.placeholder" = "Password to encrypt Keystore JSON";
"enterPassword.password.textField.placeholder.shorter" = "Password to encrypt Keystore";
"lock.create.passcode.view.model.confirm" = "암호를 다시 입력하십시오";
"lock.create.passcode.view.model.initial" = "새 비밀번호를 입력하십시오";
"lock.create.passcode.view.model.title" = "암호 설정";
@ -285,6 +308,7 @@
"a.send.receive.button.title" = "보내기/받기";
"a.send.recipient.address.title" = "주소";
"a.send.balance.insufficient" = "잔고 부족";
"accounts.confirm.delete.action" = "Lose this Wallet";
"accounts.confirm.delete.title" = "이 지갑을 삭제하시겠습니까?";
"accounts.confirm.delete.message" = "지갑이 백업되어 있는지 확인하십시오.";
"accounts.confirm.delete.okTitle" = "삭제";
@ -299,6 +323,7 @@
"accounts.delete.error.accountNotFound" = "계정을 찾을 수 없습니다";
"accounts.delete.error.failedToSignMessage" = "메시지에 서명하지 못했습니다";
"accounts.delete.error.failedToExportPrivateKey" = "개인 키를 내보내지 못했습니다";
"accounts.delete.error.failedToExportSeed" = "Failed to export wallet seed";
"Chain ID" = "체인 ID";
"Endpoint" = "엔드포인트";
"settings.preferences.button.title" = "시작 시 토큰 표시";
@ -346,3 +371,34 @@
"TokenScript.type2.conflictingFiles" = "Conflicting TokenScript Files";
"TokenScript.type2.oldSchemaVersion" = "Old TokenScript Version";
"TokenScript.type2.invalidSignature" = "Invalid Signature";
"gettingStarted.subtitle" = "Welcome to AlphaWallet";
"gettingStarted.newWallet" = "Create a New Wallet";
"gettingStarted.alreadyHaveWallet" = "I already have a wallet:";
"backup.prompt.title" = "Your Wallet is not backed up!";
"backup.prompt.description.withAmount" = "You have not backed up your wallet yet. You have %@ USD net. Act now.";
"backup.prompt.backupButton.title" = "Back up Wallet";
"backup.prompt.backup.remindLater" = "Remind me to back up later?";
"backup.prompt.afterReceivingEther.title" = "Just received %@ ETH?";
"backup.prompt.description.withoutAmount" = "Remember to back up your wallet by a secure seed phrase.";
"backup.prompt.afterInterval.title" = "Time to back up your Wallet";
"backup.prompt.afterInterval.description" = "We highly recommend to back up your wallet";
"backup.prompt.afterHittingThreshold.title" = "Your Wallet is at risk!";
"backup.prompt.afterHittingThreshold.description" = "You have not backed up your wallet yet. You have %@ USD net. Act now. ";
"onboarding.newWallet.findAddress.title" = "Find your Wallet Address";
"onboarding.newWallet.findAddress.description" = "Tap here to find your Ethereum Wallet address";
"keystore.accessKey.nonHd.backup" = "Accessing your wallet key to back it up";
"keystore.accessKey.hd.backup" = "Accessing your wallet seed phrase to back it up";
"keystore.accessKey.hd.verify" = "Accessing your wallet seed phrase to verify against it";
"keystore.accessKey.hd.prepareToVerify" = "Accessing your wallet seed phrase to let you verify against it";
"keystore.accessKey.sign" = "Accessing your wallet key to sign";
"keystore.accessKey.needImportOrPasscode" = "Can't access your wallet. Import your wallet again or enable passcode";
"keystore.accessKey.cancelled" = "You have denied access to your wallet";
"keystore.accessKey.lock.fail" = "Can't lock your wallet key. Maybe your iOS Passcode is not enabled?";
"keystore.accessKey.hd.lock" = "To lock your seed phrase to increase security";
"keystore.accessKey.nonHd.lock" = "To lock your private key to increase security";
"keystore.lock.wallet.seed.title" = "Lock your Seed Phrase to Increase Security";
"keystore.lock.wallet.privateKey.title" = "Lock your Private Key to Increase Security";
"keystore.lock.wallet.seed.description" = "AlphaWallet security is being upgraded to the best achievable. Lock your seed phrase with biometrics";
"keystore.lock.wallet.privateKey.description" = "AlphaWallet security is being upgraded to the best achievable. Lock your private key with biometrics";
"keystore.lock.wallet.seed.button" = "Lock Seed Phrase";
"keystore.lock.wallet.privateKey.button" = "Lock Private Key";

@ -3,6 +3,7 @@
"app.device.jailbreak.title" = "设备安全受损";
"Done" = "完成";
"Cancel" = "取消";
"Skip" = "Skip";
"Save" = "保存";
"Delete" = "删除";
"Reload" = "刷新";
@ -63,7 +64,22 @@
"settings.contactUs.title" = "Contact Us";
"transactions.noTransactions.label.title" = "暂无记录";
"wallet.navigation.title" = "钱包";
"wallets.backup.alertSheet.title" = "备份加密后的私钥文档";
"wallet.types.hdWallets" = "HD Wallets";
"wallet.types.keystoreWallets" = "Keystore Wallets";
"wallet.types.watchedWallets" = "Watched Wallets";
"wallets.backupKeystoreWallet.alertSheet.title" = "Export Keystore JSON";
"wallets.backupKeystoreWallet.alertSheet.title.shorter" = "Export Keystore";
"wallets.backupKeystoreWallet.introduction.title" = "What is Keystore JSON?";
"wallets.backupKeystoreWallet.introduction.description" = "A Keystore is a text file. You can copy its contents when you want to import your wallet. This is a safe way to back up a wallet.";
"wallets.backupHdWallet.alertSheet.title" = "Show Seed Phrase";
"wallets.showSeedPhrase.title" = "Seed Phrase";
"wallets.showSeedPhrase.subtitle" = "Making a backup is very simple and safe:";
"wallets.showSeedPhrase.description" = "just write down 12 words and keep them in a safe place, offline";
"wallets.showSeedPhrase.testSeedPhrase" = "Test My Seed Phrase";
"wallets.showSeedPhrase.doNotTakeScreenshotDescription" = "It's not a good idea to take a screenshot of your seed phrase";
"wallets.verifySeedPhrase.title" = "Verify Seed Phrase";
"wallets.verifySeedPhrase.wrong" = "Invalid seed phrase. Please check and try again.";
"wallets.verifySeedPhrase.doNotTakeScreenshotDescription" = "It's not a good idea to take a screenshot of your seed phrase";
"transactions.tabbar.item.title" = "我的账单";
"transactions.received.ether" = "你已收到 %@ %@";
"transactions.received.ether.notification.prompt" = "当你收到 %@ 的时候是否允许系统通知?";
@ -84,9 +100,13 @@
"importWallet.import.button.title" = "导入";
"importWallet.import.invalidAddress" = "无效的以太坊地址";
"importWallet.import.invalidPrivateKey" = "私钥长度必须为64个字符";
"Keystore JSON" = "加密后的私钥文档";
"importWallet.import.invalidMnemonic" = "Seed phrase has to be 12 words";
"importWallet.import.fromCloud.title" = "Import from iCloud/Dropbox/Google Drive";
"importWallet.import.seedPhrase.description" = "If your seed phrase is not in English, please convert it to English first";
"Keystore JSON" = "Keystore JSON";
"More Details" = "更多详情";
"Keystore" = "加密后的私钥文档";
"Keystore" = "Keystore JSON";
"Keystore.shorter" = "Keystore";
"Private Key" = "私有密钥";
"settings.currency.button.title" = "货币";
"settings.network.test.label.title" = "测试网络";
@ -113,7 +133,8 @@
"Deleting" = "删除";
"Ethereum Address" = "以太坊地址";
"InCoordinatorError.onlyWatchAccount" = "这个钱包只能观察,导入私钥 / Keystore 来签署交易";
"Mnemonic" = "助记词";
"mnemonic.shorter" = "Seed";
"mnemonic" = "Seed Phrase";
"Watch" = "观察";
"passphrase.remember.label.title" = "把这个写下来,并保持隐私和安全。如果你失了这个,你将不可能恢复你的钱包!";
"recoveryPhrase.navigation.title" = "恢复短语";
@ -131,6 +152,7 @@
"transaction.blockNumber.label.title" = "区块#";
"send.error.invalidAmount" = "错误数额";
"wallet.create.button.title" = "创建钱包";
"wallet.watch.button.title" = "Watch Wallet";
"wallet.create.inProgress" = "创建钱包...";
"wallet.import.button.title" = "导入钱包";
"browser.home.button.title" = "主页";
@ -144,10 +166,11 @@
"browser.noBookmarks.label.title" = "目前没有书签";
"browser.url.textfield.placeholder" = "搜索或直接输入网址 URL ";
"configureTransaction.gasPriceGwei.label.title" = "交易手续费 (Gwei)";
"enterPassword.confirmPassword.textField.placeholder" = "确认密码";
"enterPassword.navigation.title" = "备份密码";
"enterPassword.password.header.placeholder" = "请输入一个密码(大于6个字符)来加密你的钱包备份,之后请将备份文件妥善保存!";
"enterPassword.password.textField.placeholder" = "密码";
"enterPassword.navigation.title" = "Set Keystore Password";
"enterPassword.navigation.title.shorter" = "Keystore Password";
"enterPassword.password.header.placeholder" = "You must remember your password. We do not store your password elsewhere. KeyStore JSON file is always encrypted, otherwise whoever has it has access to your money.";
"enterPassword.password.textField.placeholder" = "Password to encrypt Keystore JSON";
"enterPassword.password.textField.placeholder.shorter" = "Password to encrypt Keystore";
"lock.create.passcode.view.model.confirm" = "确认密码";
"lock.create.passcode.view.model.initial" = "输入密码";
"lock.create.passcode.view.model.title" = "Set 密码";
@ -285,6 +308,7 @@
"a.send.receive.button.title" = "发送/接收";
"a.send.recipient.address.title" = "地址";
"a.send.balance.insufficient" = "余额不足";
"accounts.confirm.delete.action" = "Lose this Wallet";
"accounts.confirm.delete.title" = "你确认要删除这个钱包么?";
"accounts.confirm.delete.message" = "请确认你已经做好钱包备份.";
"accounts.confirm.delete.okTitle" = "删除";
@ -299,6 +323,7 @@
"accounts.delete.error.accountNotFound" = "找不到账户";
"accounts.delete.error.failedToSignMessage" = "签署消息失败";
"accounts.delete.error.failedToExportPrivateKey" = "导出私钥失败";
"accounts.delete.error.failedToExportSeed" = "Failed to export wallet seed";
"Chain ID" = "Chain ID";
"Endpoint" = "端点";
"settings.preferences.button.title" = "在开始页面显示 Token";
@ -346,3 +371,34 @@
"TokenScript.type2.conflictingFiles" = "Conflicting TokenScript Files";
"TokenScript.type2.oldSchemaVersion" = "Old TokenScript Version";
"TokenScript.type2.invalidSignature" = "Invalid Signature";
"gettingStarted.subtitle" = "Welcome to AlphaWallet";
"gettingStarted.newWallet" = "Create a New Wallet";
"gettingStarted.alreadyHaveWallet" = "I already have a wallet:";
"backup.prompt.title" = "Your Wallet is not backed up!";
"backup.prompt.description.withAmount" = "You have not backed up your wallet yet. You have %@ USD net. Act now.";
"backup.prompt.backupButton.title" = "Back up Wallet";
"backup.prompt.backup.remindLater" = "Remind me to back up later?";
"backup.prompt.afterReceivingEther.title" = "Just received %@ ETH?";
"backup.prompt.description.withoutAmount" = "Remember to back up your wallet by a secure seed phrase.";
"backup.prompt.afterInterval.title" = "Time to back up your Wallet";
"backup.prompt.afterInterval.description" = "We highly recommend to back up your wallet";
"backup.prompt.afterHittingThreshold.title" = "Your Wallet is at risk!";
"backup.prompt.afterHittingThreshold.description" = "You have not backed up your wallet yet. You have %@ USD net. Act now. ";
"onboarding.newWallet.findAddress.title" = "Find your Wallet Address";
"onboarding.newWallet.findAddress.description" = "Tap here to find your Ethereum Wallet address";
"keystore.accessKey.nonHd.backup" = "Accessing your wallet key to back it up";
"keystore.accessKey.hd.backup" = "Accessing your wallet seed phrase to back it up";
"keystore.accessKey.hd.verify" = "Accessing your wallet seed phrase to verify against it";
"keystore.accessKey.hd.prepareToVerify" = "Accessing your wallet seed phrase to let you verify against it";
"keystore.accessKey.sign" = "Accessing your wallet key to sign";
"keystore.accessKey.needImportOrPasscode" = "Can't access your wallet. Import your wallet again or enable passcode";
"keystore.accessKey.cancelled" = "You have denied access to your wallet";
"keystore.accessKey.lock.fail" = "Can't lock your wallet key. Maybe your iOS Passcode is not enabled?";
"keystore.accessKey.hd.lock" = "To lock your seed phrase to increase security";
"keystore.accessKey.nonHd.lock" = "To lock your private key to increase security";
"keystore.lock.wallet.seed.title" = "Lock your Seed Phrase to Increase Security";
"keystore.lock.wallet.privateKey.title" = "Lock your Private Key to Increase Security";
"keystore.lock.wallet.seed.description" = "AlphaWallet security is being upgraded to the best achievable. Lock your seed phrase with biometrics";
"keystore.lock.wallet.privateKey.description" = "AlphaWallet security is being upgraded to the best achievable. Lock your private key with biometrics";
"keystore.lock.wallet.seed.button" = "Lock Seed Phrase";
"keystore.lock.wallet.privateKey.button" = "Lock Private Key";

@ -53,7 +53,7 @@ class UniversalLinkCoordinator: Coordinator {
}
private var walletAddress: AlphaWallet.Address {
//TODO pass in the wallet instead
return (try! EtherKeystore()).recentlyUsedWallet!.address
return EtherKeystore.current!.address
}
var coordinators: [Coordinator] = []
@ -481,7 +481,7 @@ class UniversalLinkCoordinator: Coordinator {
private func makeTokenHolderImpl(name: String, symbol: String, bytes32Tokens: [String], contractAddress: AlphaWallet.Address) {
//TODO pass in the wallet instead
let account = (try! EtherKeystore()).recentlyUsedWallet!
let account = EtherKeystore.current!
var tokens = [Token]()
let xmlHandler = XMLHandler(contract: contractAddress, assetDefinitionStore: assetDefinitionStore)
for i in 0..<bytes32Tokens.count {
@ -537,15 +537,6 @@ class UniversalLinkCoordinator: Coordinator {
private func showImportSuccessful() {
updateImportTokenController(with: .succeeded)
promptBackupWallet()
}
private func promptBackupWallet() {
guard let keystore = try? EtherKeystore() else { return }
let coordinator = PromptBackupCoordinator(keystore: keystore, walletAddress: walletAddress, config: config)
addCoordinator(coordinator)
coordinator.delegate = self
coordinator.start()
}
private func showImportError(errorMessage: String, cost: ImportMagicTokenViewControllerViewModel.Cost? = nil) {
@ -643,13 +634,3 @@ extension UniversalLinkCoordinator: CanOpenURL {
delegate?.didPressOpenWebPage(url, in: viewController)
}
}
extension UniversalLinkCoordinator: PromptBackupCoordinatorDelegate {
func viewControllerForPresenting(in coordinator: PromptBackupCoordinator) -> UIViewController? {
return delegate?.viewControllerForPresenting(in: self)
}
func didFinish(in coordinator: PromptBackupCoordinator) {
removeCoordinator(coordinator)
}
}

@ -1,7 +1,7 @@
// Copyright © 2018 Stormbird PTE. LTD.
import BigInt
import TrustKeystore
import TrustWalletCore
public struct Order {
var price: BigUInt
@ -52,7 +52,7 @@ public class OrderHandler {
private let keyStore = try! EtherKeystore()
func signOrders(orders: [Order], account: Account) throws -> [SignedOrder] {
func signOrders(orders: [Order], account: EthereumAccount) throws -> [SignedOrder] {
var signedOrders = [SignedOrder]()
var messages = [Data]()

@ -2,11 +2,10 @@
import Foundation
import BigInt
import TrustKeystore
public struct UnsignedTransaction {
let value: BigInt
let account: Account
let account: EthereumAccount
let to: AlphaWallet.Address?
let nonce: Int
let data: Data

@ -7,11 +7,10 @@
//
import Foundation
import TrustKeystore
import BigInt
class SignatureHelper {
class func signatureAsHex(for message: String, account: Account) -> String? {
class func signatureAsHex(for message: String, account: EthereumAccount) -> String? {
let keystore = try! EtherKeystore()
let signature = keystore.signMessageData(message.data(using: String.Encoding.utf8), for: account)
let signatureHex = try? signature.dematerialize().hex(options: .upperCase)
@ -21,7 +20,7 @@ class SignatureHelper {
return data
}
class func signatureAsDecimal(for message: String, account: Account) -> String? {
class func signatureAsDecimal(for message: String, account: EthereumAccount) -> String? {
let signatureHex = signatureAsHex(for: message, account: account)!
return BigInt(signatureHex, radix: 16)!.description
}

@ -18,6 +18,7 @@ class SettingsCoordinator: Coordinator {
private let keystore: Keystore
var config: Config
private let sessions: ServerDictionary<WalletSession>
private let promptBackupCoordinator: PromptBackupCoordinator
private var account: Wallet {
return sessions.anyValue.account
@ -38,13 +39,16 @@ class SettingsCoordinator: Coordinator {
navigationController: UINavigationController = NavigationController(),
keystore: Keystore,
config: Config,
sessions: ServerDictionary<WalletSession>
sessions: ServerDictionary<WalletSession>,
promptBackupCoordinator: PromptBackupCoordinator
) {
self.navigationController = navigationController
self.navigationController.modalPresentationStyle = .formSheet
self.keystore = keystore
self.config = config
self.sessions = sessions
self.promptBackupCoordinator = promptBackupCoordinator
promptBackupCoordinator.subtlePromptDelegate = self
}
func start() {
@ -59,7 +63,8 @@ class SettingsCoordinator: Coordinator {
let coordinator = AccountsCoordinator(
config: config,
navigationController: NavigationController(),
keystore: keystore
keystore: keystore,
promptBackupCoordinator: promptBackupCoordinator
)
coordinator.delegate = self
coordinator.start()
@ -189,3 +194,13 @@ extension SettingsCoordinator: EnabledServersCoordinatorDelegate {
removeCoordinator(coordinator)
}
}
extension SettingsCoordinator: PromptBackupCoordinatorSubtlePromptDelegate {
var viewControllerToShowBackupLaterAlert: UIViewController {
return rootViewController
}
func updatePrompt(inCoordinator coordinator: PromptBackupCoordinator) {
rootViewController.promptBackupWalletView = coordinator.subtlePromptView
}
}

@ -98,8 +98,9 @@ struct Config {
let priceInfoEndpoints = URL(string: "https://api.coinmarketcap.com")!
var walletAddressesAlreadyPromptedForBackUp: [String] {
if let addresses = defaults.array(forKey: Keys.walletAddressesAlreadyPromptedForBackUp) {
var oldWalletAddressesAlreadyPromptedForBackUp: [String] {
//We hard code the key here because it's used for migrating off the old value, there should be no reason why this key will change in the next line
if let addresses = defaults.array(forKey: "walletAddressesAlreadyPromptedForBackUp ") {
return addresses as! [String]
} else {
return []
@ -119,13 +120,4 @@ struct Config {
addresses.append(address.eip55String)
defaults.setValue(addresses, forKey: Keys.walletAddressesAlreadyPromptedForBackUp)
}
func isWalletAddressAlreadyPromptedForBackUp(address: AlphaWallet.Address) -> Bool {
if let value = defaults.array(forKey: Keys.walletAddressesAlreadyPromptedForBackUp) {
let addresses = value as! [String]
return addresses.contains(address.eip55String)
} else {
return false
}
}
}

@ -1,6 +1,5 @@
// Copyright © 2019 Stormbird PTE. LTD.
import TrustKeystore
import UIKit
protocol EnabledServersViewControllerDelegate: class {

@ -21,8 +21,29 @@ class SettingsViewController: FormViewController {
return SettingsViewModel(isDebug: isDebug)
}()
private let account: Wallet
private let promptBackupWalletViewHolder = UIView()
weak var delegate: SettingsViewControllerDelegate?
var promptBackupWalletView: UIView? {
didSet {
oldValue?.removeFromSuperview()
if let promptBackupWalletView = promptBackupWalletView {
promptBackupWalletViewHolder.isHidden = false
promptBackupWalletView.translatesAutoresizingMaskIntoConstraints = false
promptBackupWalletViewHolder.addSubview(promptBackupWalletView)
NSLayoutConstraint.activate([
promptBackupWalletView.leadingAnchor.constraint(equalTo: promptBackupWalletViewHolder.leadingAnchor, constant: 7),
promptBackupWalletView.trailingAnchor.constraint(equalTo: promptBackupWalletViewHolder.trailingAnchor, constant: -7),
promptBackupWalletView.topAnchor.constraint(equalTo: promptBackupWalletViewHolder.topAnchor, constant: 7),
promptBackupWalletView.bottomAnchor.constraint(equalTo: promptBackupWalletViewHolder.bottomAnchor, constant: 0),
])
tabBarItem.badgeValue = "1"
} else {
promptBackupWalletViewHolder.isHidden = true
tabBarItem.badgeValue = nil
}
}
}
init(account: Wallet) {
self.account = account
@ -167,6 +188,25 @@ class SettingsViewController: FormViewController {
cell.mainLabel.text = R.string.localizable.settingsVersionLabelTitle()
cell.subLabel.text = "\(Bundle.main.fullVersion). \(TokenScript.supportedTokenScriptNamespaceVersion)"
}
//Check for nil is important because the prompt might have been shown before viewDidLoad
if promptBackupWalletView == nil {
promptBackupWalletViewHolder.isHidden = true
}
let bodyStackView = [
promptBackupWalletViewHolder,
tableView,
].asStackView(axis: .vertical)
bodyStackView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(bodyStackView)
NSLayoutConstraint.activate([
bodyStackView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
bodyStackView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
bodyStackView.topAnchor.constraint(equalTo: view.topAnchor),
bodyStackView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
}
func setPasscode(completion: ((Bool) -> Void)? = .none) {

@ -5,7 +5,6 @@ import UIKit
import Alamofire
import BigInt
import RealmSwift
import TrustKeystore
import PromiseKit
enum ContractData {

@ -19,6 +19,7 @@ class TokensCoordinator: Coordinator {
private let tokenCollection: TokenCollection
private let nativeCryptoCurrencyPrices: ServerDictionary<Subscribable<Double>>
private let assetDefinitionStore: AssetDefinitionStore
private let promptBackupCoordinator: PromptBackupCoordinator
private var serverToAddCustomTokenOn: RPCServerOrAuto = .auto {
didSet {
switch serverToAddCustomTokenOn {
@ -74,7 +75,8 @@ class TokensCoordinator: Coordinator {
keystore: Keystore,
tokenCollection: TokenCollection,
nativeCryptoCurrencyPrices: ServerDictionary<Subscribable<Double>>,
assetDefinitionStore: AssetDefinitionStore
assetDefinitionStore: AssetDefinitionStore,
promptBackupCoordinator: PromptBackupCoordinator
) {
self.navigationController = navigationController
self.navigationController.modalPresentationStyle = .formSheet
@ -83,6 +85,8 @@ class TokensCoordinator: Coordinator {
self.tokenCollection = tokenCollection
self.nativeCryptoCurrencyPrices = nativeCryptoCurrencyPrices
self.assetDefinitionStore = assetDefinitionStore
self.promptBackupCoordinator = promptBackupCoordinator
promptBackupCoordinator.prominentPromptDelegate = self
setupSingleChainTokenCoordinators()
}
@ -324,3 +328,13 @@ extension TokensCoordinator: ServersCoordinatorDelegate {
removeCoordinator(coordinator)
}
}
extension TokensCoordinator: PromptBackupCoordinatorProminentPromptDelegate {
var viewControllerToShowBackupLaterAlert: UIViewController {
return tokensViewController
}
func updatePrompt(inCoordinator coordinator: PromptBackupCoordinator) {
tokensViewController.promptBackupWalletView = coordinator.prominentPromptView
}
}

@ -21,7 +21,7 @@ class TokenInstanceActionViewController: UIViewController, TokenVerifiableStatus
private let roundedBackground = RoundedBackground()
lazy private var tokenScriptRendererView: TokenInstanceWebView = {
//TODO pass in keystore or wallet address instead
let walletAddress = try! EtherKeystore().recentlyUsedWallet!.address
let walletAddress = EtherKeystore.current!.address
let webView = TokenInstanceWebView(server: server, walletAddress: walletAddress, assetDefinitionStore: assetDefinitionStore)
webView.isWebViewInteractionEnabled = true
webView.delegate = self

@ -9,7 +9,6 @@
import Foundation
import UIKit
import Result
import TrustKeystore
protocol TokensCardViewControllerDelegate: class, CanOpenURL {
func didPressRedeem(token: TokenObject, tokenHolder: TokenHolder, in viewController: TokensCardViewController)

@ -42,6 +42,8 @@ class TokensViewController: UIViewController {
private var currentCollectiblesContractsDisplayed = [AlphaWallet.Address]()
private let searchController: UISearchController
private let consoleButton = UIButton(type: .system)
private let promptBackupWalletViewHolder = UIView()
private var shouldHidePromptBackupWalletViewHolderBecauseSearchIsActive = false
weak var delegate: TokensViewControllerDelegate?
//TODO The name "bad" isn't correct. Because it includes "conflicts" too
@ -57,6 +59,24 @@ class TokensViewController: UIViewController {
}
}
}
var promptBackupWalletView: UIView? {
didSet {
oldValue?.removeFromSuperview()
if let promptBackupWalletView = promptBackupWalletView {
promptBackupWalletViewHolder.isHidden = shouldHidePromptBackupWalletViewHolderBecauseSearchIsActive
promptBackupWalletView.translatesAutoresizingMaskIntoConstraints = false
promptBackupWalletViewHolder.addSubview(promptBackupWalletView)
NSLayoutConstraint.activate([
promptBackupWalletView.leadingAnchor.constraint(equalTo: promptBackupWalletViewHolder.leadingAnchor, constant: 7),
promptBackupWalletView.trailingAnchor.constraint(equalTo: promptBackupWalletViewHolder.trailingAnchor, constant: -7),
promptBackupWalletView.topAnchor.constraint(equalTo: promptBackupWalletViewHolder.topAnchor, constant: 7),
promptBackupWalletView.bottomAnchor.constraint(equalTo: promptBackupWalletViewHolder.bottomAnchor, constant: -4),
])
} else {
promptBackupWalletViewHolder.isHidden = true
}
}
}
init(sessions: ServerDictionary<WalletSession>,
account: Wallet,
@ -94,8 +114,11 @@ class TokensViewController: UIViewController {
tableViewRefreshControl.addTarget(self, action: #selector(pullToRefresh), for: .valueChanged)
tableView.addSubview(tableViewRefreshControl)
promptBackupWalletViewHolder.isHidden = true
let bodyStackView = [
consoleButton,
promptBackupWalletViewHolder,
tableView,
].asStackView(axis: .vertical)
bodyStackView.translatesAutoresizingMaskIntoConstraints = false
@ -173,6 +196,7 @@ class TokensViewController: UIViewController {
private func reload() {
tableView.isHidden = !viewModel.shouldShowTable
promptBackupWalletViewHolder.isHidden = !(viewModel.shouldShowBackupPromptViewHolder && !promptBackupWalletViewHolder.subviews.isEmpty) || shouldHidePromptBackupWalletViewHolderBecauseSearchIsActive
collectiblesCollectionView.isHidden = !viewModel.shouldShowCollectiblesCollectionView
if viewModel.hasContent {
if viewModel.shouldShowTable {
@ -419,6 +443,11 @@ extension TokensViewController: UICollectionViewDelegate {
extension TokensViewController: UISearchResultsUpdating {
public func updateSearchResults(for searchController: UISearchController) {
if searchController.isActive {
shouldHidePromptBackupWalletViewHolderBecauseSearchIsActive = true
} else {
shouldHidePromptBackupWalletViewHolderBecauseSearchIsActive = false
}
let keyword = searchController.searchBar.text ?? ""
filterView.searchFor(keyword: keyword)
}

@ -17,14 +17,6 @@ class TokensViewModel {
return CurrencyFormatter.formatter.string(from: NSNumber(value: totalAmount))
}
private func amount(for token: TokenObject) -> Double {
guard let tickers = tickers[token.server] else { return 0 }
guard !token.valueBigInt.isZero, let tickersSymbol = tickers[token.contractAddress] else { return 0 }
let tokenValue = CurrencyFormatter.plainFormatter.string(from: token.valueBigInt, decimals: token.decimals).doubleValue
let price = Double(tickersSymbol.price_usd) ?? 0
return tokenValue * price
}
var filter: WalletFilter = .all {
didSet {
filteredTokens = getFilteredtokens()
@ -56,6 +48,17 @@ class TokensViewModel {
}
}
var shouldShowBackupPromptViewHolder: Bool {
//TODO show the prompt in both ASSETS and COLLECTIBLES tab too
switch filter {
case .all, .currencyOnly, .keyword:
return true
case .assetsOnly, .collectiblesOnly:
return false
}
}
var shouldShowCollectiblesCollectionView: Bool {
switch filter {
case .all, .currencyOnly, .assetsOnly, .keyword:
@ -123,4 +126,16 @@ class TokensViewModel {
}
}
}
func nativeCryptoCurrencyToken(forServer server: RPCServer) -> TokenObject? {
return tokens.first(where: { $0.primaryKey == TokensDataStore.etherToken(forServer: .main).primaryKey })
}
func amount(for token: TokenObject) -> Double {
guard let tickers = tickers[token.server] else { return 0 }
guard !token.valueBigInt.isZero, let tickersSymbol = tickers[token.contractAddress] else { return 0 }
let tokenValue = CurrencyFormatter.plainFormatter.string(from: token.valueBigInt, decimals: token.decimals).doubleValue
let price = Double(tickersSymbol.price_usd) ?? 0
return tokenValue * price
}
}

@ -6,6 +6,13 @@ protocol TextFieldDelegate: class {
func shouldReturn(in textField: TextField) -> Bool
func doneButtonTapped(for textField: TextField)
func nextButtonTapped(for textField: TextField)
func shouldChangeCharacters(inRange range: NSRange, replacementString string: String, for textField: TextField) -> Bool
}
extension TextFieldDelegate {
func shouldChangeCharacters(inRange range: NSRange, replacementString string: String, for textField: TextField) -> Bool {
return true
}
}
class TextField: UIControl {
@ -16,7 +23,6 @@ class TextField: UIControl {
}
private var isConfigured = false
private let textField = UITextField()
var returnKeyType: UIReturnKeyType {
get {
@ -46,6 +52,7 @@ class TextField: UIControl {
}
let label = UILabel()
let textField = UITextField()
weak var delegate: TextFieldDelegate?
var value: String {
@ -82,8 +89,8 @@ class TextField: UIControl {
addSubview(textField)
NSLayoutConstraint.activate([
textField.leadingAnchor.constraint(equalTo: leadingAnchor),
textField.trailingAnchor.constraint(equalTo: trailingAnchor),
textField.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 22),
textField.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -22),
textField.topAnchor.constraint(equalTo: topAnchor),
textField.bottomAnchor.constraint(equalTo: bottomAnchor),
heightAnchor.constraint(equalToConstant: ScreenChecker().isNarrowScreen ? 30 : 50),
@ -97,12 +104,10 @@ class TextField: UIControl {
label.font = Fonts.regular(size: 10)!
label.textColor = Colors.appGrayLabelColor
textField.leftView = .spacerWidth(22)
textField.rightView = .spacerWidth(22)
textField.textColor = Colors.appBackground
textField.font = Fonts.bold(size: 21)
textField.layer.borderColor = Colors.appBackground.cgColor
textField.layer.borderWidth = 1
layer.borderColor = Colors.appBackground.cgColor
layer.borderWidth = 1
}
required init?(coder aDecoder: NSCoder) {
@ -115,7 +120,7 @@ class TextField: UIControl {
}
private func roundCornersBasedOnHeight() {
textField.layer.cornerRadius = textField.frame.size.height / 2
layer.cornerRadius = frame.size.height / 2
}
private func makeToolbarWithDoneButton() -> UIToolbar {
@ -164,4 +169,8 @@ extension TextField: UITextFieldDelegate {
guard let delegate = delegate else { return true }
return delegate.shouldReturn(in: self)
}
public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
return delegate?.shouldChangeCharacters(inRange: range, replacementString: string, for: self) ?? true
}
}

@ -4,7 +4,6 @@ import Foundation
import UIKit
import BigInt
import PromiseKit
import TrustKeystore
import WebKit
protocol TokenInstanceWebViewDelegate: class {
@ -192,7 +191,7 @@ extension TokenInstanceWebView: WKScriptMessageHandler {
let action = DappAction.fromCommand(command, transfer: transfer)
//TODO pass this in instead
let wallet = (try! EtherKeystore()).recentlyUsedWallet!
let wallet = EtherKeystore.current!
guard case .real(let account) = wallet.type else { return }
@ -235,7 +234,7 @@ extension TokenInstanceWebView {
}
}
func signMessage(with type: SignMessageType, account: Account, callbackID: Int) {
func signMessage(with type: SignMessageType, account: EthereumAccount, callbackID: Int) {
guard let navigationController = delegate?.navigationControllerFor(tokenInstanceWebView: self) else { return }
//TODO pass in keystore

@ -18,6 +18,7 @@ class SingleChainTransactionDataCoordinator: Coordinator {
private let session: WalletSession
private let keystore: Keystore
private let tokensStorage: TokensDataStore
private let promptBackupCoordinator: PromptBackupCoordinator
private let fetchLatestTransactionsQueue: OperationQueue
private var timer: Timer?
private var updateTransactionsTimer: Timer?
@ -35,12 +36,14 @@ class SingleChainTransactionDataCoordinator: Coordinator {
storage: TransactionsStorage,
keystore: Keystore,
tokensStorage: TokensDataStore,
promptBackupCoordinator: PromptBackupCoordinator,
onFetchLatestTransactionsQueue fetchLatestTransactionsQueue: OperationQueue
) {
self.session = session
self.storage = storage
self.keystore = keystore
self.tokensStorage = tokensStorage
self.promptBackupCoordinator = promptBackupCoordinator
self.fetchLatestTransactionsQueue = fetchLatestTransactionsQueue
}
@ -200,6 +203,18 @@ class SingleChainTransactionDataCoordinator: Coordinator {
notifyUserEtherReceived(for: each.id, amount: amount)
}
}
let etherReceivedUsedForBackupPrompt = newIncomingEthTransactions
.last { wallet.address.sameContract(as: $0.to) }
.flatMap { BigInt($0.value) }
switch session.server {
//TODO make this work for other mainnets
case .main:
etherReceivedUsedForBackupPrompt.flatMap { promptBackupCoordinator.showCreateBackupAfterReceiveNativeCryptoCurrencyPrompt(nativeCryptoCurrency: $0) }
case .classic, .xDai:
break
case .kovan, .ropsten, .rinkeby, .poa, .sokol, .callisto, .goerli, .custom:
break
}
}
private func notifyUserEtherReceived(for transactionId: String, amount: String) {

@ -9,7 +9,6 @@ import Foundation
import UIKit
import Result
import SafariServices
import TrustKeystore
import MessageUI
import BigInt

@ -10,6 +10,7 @@ protocol TransactionCoordinatorDelegate: class, CanOpenURL {
class TransactionCoordinator: Coordinator {
private let keystore: Keystore
private let transactionsCollection: TransactionCollection
private let promptBackupCoordinator: PromptBackupCoordinator
lazy var rootViewController: TransactionsViewController = {
return makeTransactionsController()
@ -20,7 +21,8 @@ class TransactionCoordinator: Coordinator {
sessions: sessions,
transactionCollection: transactionsCollection,
keystore: keystore,
tokensStorages: tokensStorages
tokensStorages: tokensStorages,
promptBackupCoordinator: promptBackupCoordinator
)
return coordinator
}()
@ -37,13 +39,15 @@ class TransactionCoordinator: Coordinator {
navigationController: UINavigationController = NavigationController(),
transactionsCollection: TransactionCollection,
keystore: Keystore,
tokensStorages: ServerDictionary<TokensDataStore>
tokensStorages: ServerDictionary<TokensDataStore>,
promptBackupCoordinator: PromptBackupCoordinator
) {
self.sessions = sessions
self.keystore = keystore
self.navigationController = navigationController
self.transactionsCollection = transactionsCollection
self.tokensStorages = tokensStorages
self.promptBackupCoordinator = promptBackupCoordinator
NotificationCenter.default.addObserver(self, selector: #selector(didEnterForeground), name: UIApplication.willEnterForegroundNotification, object: nil)
}

@ -19,6 +19,7 @@ class TransactionDataCoordinator: Coordinator {
private let sessions: ServerDictionary<WalletSession>
private let keystore: Keystore
private let tokensStorages: ServerDictionary<TokensDataStore>
private let promptBackupCoordinator: PromptBackupCoordinator
private var singleChainTransactionDataCoordinators: [SingleChainTransactionDataCoordinator] {
return coordinators.compactMap { $0 as? SingleChainTransactionDataCoordinator }
}
@ -39,12 +40,14 @@ class TransactionDataCoordinator: Coordinator {
sessions: ServerDictionary<WalletSession>,
transactionCollection: TransactionCollection,
keystore: Keystore,
tokensStorages: ServerDictionary<TokensDataStore>
tokensStorages: ServerDictionary<TokensDataStore>,
promptBackupCoordinator: PromptBackupCoordinator
) {
self.sessions = sessions
self.transactionCollection = transactionCollection
self.keystore = keystore
self.tokensStorages = tokensStorages
self.promptBackupCoordinator = promptBackupCoordinator
setupSingleChainTransactionDataCoordinators()
NotificationCenter.default.addObserver(self, selector: #selector(stopTimers), name: UIApplication.willResignActiveNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(restartTimers), name: UIApplication.didBecomeActiveNotification, object: nil)
@ -55,7 +58,7 @@ class TransactionDataCoordinator: Coordinator {
let server = each.server
let session = sessions[server]
let tokensDataStore = tokensStorages[server]
let coordinator = SingleChainTransactionDataCoordinator(session: session, storage: each, keystore: keystore, tokensStorage: tokensDataStore, onFetchLatestTransactionsQueue: fetchLatestTransactionsQueue)
let coordinator = SingleChainTransactionDataCoordinator(session: session, storage: each, keystore: keystore, tokensStorage: tokensDataStore, promptBackupCoordinator: promptBackupCoordinator, onFetchLatestTransactionsQueue: fetchLatestTransactionsQueue)
coordinator.delegate = self
addCoordinator(coordinator)
}

@ -1,4 +1,3 @@
// Copyright SIX DAY LLC. All rights reserved.
import Foundation
import RealmSwift

@ -9,7 +9,7 @@ import APIKit
public struct PreviewTransaction {
let value: BigInt
let account: Account
let account: EthereumAccount
let address: AlphaWallet.Address?
let contract: AlphaWallet.Address?
let nonce: Int
@ -21,7 +21,7 @@ public struct PreviewTransaction {
class TransactionConfigurator {
private let session: WalletSession
private let account: Account
private let account: EthereumAccount
private lazy var calculatedGasPrice: BigInt = {
switch session.server {
case .xDai:
@ -56,7 +56,7 @@ class TransactionConfigurator {
init(
session: WalletSession,
account: Account,
account: EthereumAccount,
transaction: UnconfirmedTransaction
) {
self.session = session

@ -2,7 +2,6 @@
import Foundation
import UIKit
import TrustKeystore
import Result
protocol ConfirmCoordinatorDelegate: class {
@ -11,7 +10,7 @@ protocol ConfirmCoordinatorDelegate: class {
class ConfirmCoordinator: Coordinator {
private let session: WalletSession
private let account: Account
private let account: EthereumAccount
private let keystore: Keystore
private let configurator: TransactionConfigurator
private let type: ConfirmType
@ -26,7 +25,7 @@ class ConfirmCoordinator: Coordinator {
session: WalletSession,
configurator: TransactionConfigurator,
keystore: Keystore,
account: Account,
account: EthereumAccount,
type: ConfirmType
) {
self.navigationController = navigationController

@ -2,7 +2,6 @@
import Foundation
import UIKit
import TrustKeystore
protocol PaymentCoordinatorDelegate: class, CanOpenURL {
func didFinish(_ result: ConfirmResult, in coordinator: PaymentCoordinator)

@ -3,7 +3,6 @@
import Foundation
import UIKit
import BigInt
import TrustKeystore
protocol SendCoordinatorDelegate: class, CanOpenURL {
func didFinish(_ result: ConfirmResult, in coordinator: SendCoordinator)
@ -13,7 +12,7 @@ protocol SendCoordinatorDelegate: class, CanOpenURL {
class SendCoordinator: Coordinator {
private let transferType: TransferType
private let session: WalletSession
private let account: Account
private let account: EthereumAccount
private let keystore: Keystore
private let storage: TokensDataStore
private let ethPrice: Subscribable<Double>
@ -34,7 +33,7 @@ class SendCoordinator: Coordinator {
session: WalletSession,
keystore: Keystore,
storage: TokensDataStore,
account: Account,
account: EthereumAccount,
ethPrice: Subscribable<Double>,
tokenHolders: [TokenHolder] = [],
assetDefinitionStore: AssetDefinitionStore

@ -2,7 +2,6 @@
import Foundation
import UIKit
import TrustKeystore
import CryptoSwift
import Result
@ -19,7 +18,7 @@ protocol SignMessageCoordinatorDelegate: class {
class SignMessageCoordinator: Coordinator {
private let navigationController: UINavigationController
private let keystore: Keystore
private let account: Account
private let account: EthereumAccount
private var message: SignMessageType?
var coordinators: [Coordinator] = []
@ -29,7 +28,7 @@ class SignMessageCoordinator: Coordinator {
init(
navigationController: UINavigationController,
keystore: Keystore,
account: Account
account: EthereumAccount
) {
self.navigationController = navigationController
self.keystore = keystore
@ -69,6 +68,7 @@ class SignMessageCoordinator: Coordinator {
case .success(let data):
didComplete?(.success(data))
case .failure(let error):
navigationController.displaySuccess(message: error.errorDescription)
didComplete?(.failure(AnyError(error)))
}
}

@ -1,7 +1,6 @@
// Copyright © 2018 Stormbird PTE. LTD.
import Foundation
import TrustKeystore
import BigInt
protocol TransferNFTCoordinatorDelegate: class {
@ -16,7 +15,7 @@ class TransferNFTCoordinator: Coordinator {
private let paymentFlow: PaymentFlow
private let keystore: Keystore
private let session: WalletSession
private let account: Account
private let account: EthereumAccount
private let viewController: UIViewController
private var statusViewController: StatusViewController?
private var status = StatusViewControllerViewModel.State.processing {
@ -35,7 +34,7 @@ class TransferNFTCoordinator: Coordinator {
var coordinators: [Coordinator] = []
weak var delegate: TransferNFTCoordinatorDelegate?
init(tokenHolder: TokenHolder, walletAddress: AlphaWallet.Address, paymentFlow: PaymentFlow, keystore: Keystore, session: WalletSession, account: Account, assetDefinitionStore: AssetDefinitionStore, on viewController: UIViewController) {
init(tokenHolder: TokenHolder, walletAddress: AlphaWallet.Address, paymentFlow: PaymentFlow, keystore: Keystore, session: WalletSession, account: EthereumAccount, assetDefinitionStore: AssetDefinitionStore, on viewController: UIViewController) {
self.tokenHolder = tokenHolder
self.walletAddress = walletAddress
self.paymentFlow = paymentFlow

@ -7,7 +7,6 @@ import JSONRPCKit
import APIKit
import QRCodeReaderViewController
import BigInt
import TrustKeystore
import MBProgressHUD
protocol SendViewControllerDelegate: class, CanOpenURL {
@ -29,7 +28,7 @@ class SendViewController: UIViewController, CanScanQRCode {
lazy private var headerViewModel = SendHeaderViewViewModelWithIntroduction(server: session.server, assetDefinitionStore: assetDefinitionStore)
private var balanceViewModel: BalanceBaseViewModel?
private let session: WalletSession
private let account: Account
private let account: EthereumAccount
private let ethPrice: Subscribable<Double>
private let assetDefinitionStore: AssetDefinitionStore
private var gasPrice: BigInt?
@ -46,7 +45,7 @@ class SendViewController: UIViewController, CanScanQRCode {
init(
session: WalletSession,
storage: TokensDataStore,
account: Account,
account: EthereumAccount,
transferType: TransferType,
cryptoPrice: Subscribable<Double>,
assetDefinitionStore: AssetDefinitionStore

@ -213,11 +213,9 @@ extension AddressTextField: UITextFieldDelegate {
private func informDelegateDidChange(to string: String) {
//DispatchQueue because the textfield hasn't been updated yet
DispatchQueue.global().async { [weak self] in
DispatchQueue.main.sync { [weak self] in
guard let strongSelf = self else { return }
strongSelf.delegate?.didChange(to: string, in: strongSelf)
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
guard let strongSelf = self else { return }
strongSelf.delegate?.didChange(to: string, in: strongSelf)
}
}
}

@ -73,6 +73,28 @@ class ButtonsBar: UIView {
}
}
func configureSecondary() {
let viewModel = ButtonsBarViewModel()
for each in buttonContainerViews {
each.configureShadow(color: viewModel.buttonShadowColor, offset: viewModel.buttonShadowOffset, opacity: viewModel.buttonShadowOpacity, radius: viewModel.buttonShadowRadius, cornerRadius: viewModel.buttonCornerRadius)
let button = each.childView
button.setBackgroundColor(viewModel.buttonSecondaryBackgroundColor, forState: .normal)
button.setBackgroundColor(viewModel.disabledButtonBackgroundColor, forState: .disabled)
button.setTitleColor(viewModel.buttonSecondaryTitleColor, for: .normal)
button.setTitleColor(viewModel.disabledButtonTitleColor, for: .disabled)
button.titleLabel?.font = viewModel.buttonFont
button.titleLabel?.adjustsFontSizeToFitWidth = true
//So long titles (that cause font to be adjusted) have some margins on the left and right
button.contentEdgeInsets = .init(top: 0, left: 3, bottom: 0, right: 3)
button.cornerRadius = viewModel.buttonSecondaryCornerRadius
button.borderColor = viewModel.buttonSecondaryBorderColor
button.borderWidth = viewModel.buttonSecondaryBorderWidth
}
}
private static func bar(numberOfButtons: Int) -> [ContainerViewWithShadow<UIButton>] {
return (0..<numberOfButtons).map { _ in ContainerViewWithShadow(aroundView: UIButton(type: .system)) }
}
@ -118,4 +140,24 @@ private struct ButtonsBarViewModel {
var buttonFont: UIFont {
return Fonts.regular(size: 20)!
}
var buttonSecondaryBackgroundColor: UIColor {
return Colors.appWhite
}
var buttonSecondaryTitleColor: UIColor? {
return nil
}
var buttonSecondaryCornerRadius: CGFloat {
return 16
}
var buttonSecondaryBorderColor: UIColor {
return .init(red: 202, green: 202, blue: 202)
}
var buttonSecondaryBorderWidth: CGFloat {
return 1
}
}

File diff suppressed because one or more lines are too long

@ -0,0 +1,23 @@
// Copyright © 2019 Stormbird PTE. LTD.
import Foundation
import Eureka
import TrustWalletCore
struct MnemonicRule<T: Equatable>: RuleType {
public init(msg: String = "") {
let msg = msg.isEmpty ? R.string.localizable.importWalletImportInvalidMnemonic() : msg
self.validationError = ValidationError(msg: msg)
}
public var id: String?
public var validationError: ValidationError
public func isValid(value: T?) -> ValidationError? {
if let str = value as? String {
let words = str.trimmed.split(separator: " ")
return (words.count != HDWallet.mnemonicWordCount) ? validationError : nil
}
return value != nil ? nil : validationError
}
}

@ -17,7 +17,11 @@ public struct PrivateKeyRule<T: Equatable>: RuleType {
if let str = value as? String {
//allows for private key import to have 0x or not
let drop0xKey = str.drop0x
return (drop0xKey.count != 64) ? validationError : nil
let regex = try! NSRegularExpression(pattern: "^[0-9a-fA-F]{64}$")
let range = NSRange(location: 0, length: drop0xKey.utf16.count)
let result = regex.matches(in: drop0xKey, range: range)
let matched = !result.isEmpty
return matched ? nil : validationError
}
return value != nil ? nil : validationError
}

@ -0,0 +1,60 @@
// Copyright © 2019 Stormbird PTE. LTD.
import Foundation
import UIKit
class SuccessOverlayView: UIView {
static func show() {
let view = SuccessOverlayView(frame: UIScreen.main.bounds)
view.show()
}
private let imageView = UIImageView(image: R.image.successOverlay()!)
override init(frame: CGRect) {
super.init(frame: frame)
let blurEffect = UIBlurEffect(style: .extraLight)
let blurView = UIVisualEffectView(effect: blurEffect)
blurView.translatesAutoresizingMaskIntoConstraints = false
addSubview(blurView)
blurView.alpha = 0.3
imageView.translatesAutoresizingMaskIntoConstraints = false
addSubview(imageView)
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(hide))
addGestureRecognizer(tapGestureRecognizer)
NSLayoutConstraint.activate([
blurView.leadingAnchor.constraint(equalTo: leadingAnchor),
blurView.trailingAnchor.constraint(equalTo: trailingAnchor),
blurView.topAnchor.constraint(equalTo: topAnchor),
blurView.bottomAnchor.constraint(equalTo: bottomAnchor),
imageView.centerXAnchor.constraint(equalTo: centerXAnchor),
imageView.centerYAnchor.constraint(equalTo: centerYAnchor),
])
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func hide() {
removeFromSuperview()
}
func show() {
imageView.transform = CGAffineTransform(scaleX: 0.6, y: 0.6)
UIApplication.shared.keyWindow?.addSubview(self)
UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.5, initialSpringVelocity: 7, options: .curveEaseInOut, animations: {
self.imageView.transform = .identity
})
//TODO sound too
let feedbackGenerator = UINotificationFeedbackGenerator()
feedbackGenerator.prepare()
feedbackGenerator.notificationOccurred(.success)
}
}

@ -6,6 +6,13 @@ protocol TextViewDelegate: class {
func shouldReturn(in textView: TextView) -> Bool
func doneButtonTapped(for textView: TextView)
func nextButtonTapped(for textView: TextView)
func didChange(inTextView textView: TextView)
}
extension TextViewDelegate {
func didChange(inTextView textView: TextView) {
//do nothing
}
}
class TextView: UIControl {
@ -15,8 +22,7 @@ class TextView: UIControl {
case none
}
private let textView = UITextView()
let textView = UITextView()
let label = UILabel()
var value: String {
get {
@ -137,4 +143,8 @@ extension TextView: UITextViewDelegate {
return true
}
}
public func textViewDidChange(_ textView: UITextView) {
delegate?.didChange(inTextView: self)
}
}

@ -41,7 +41,7 @@ class TokenCardRowView: UIView {
}
lazy private var tokenScriptRendererView: TokenInstanceWebView = {
//TODO pass in keystore or wallet address instead
let walletAddress = try! EtherKeystore().recentlyUsedWallet!.address
let walletAddress = EtherKeystore.current!.address
//TODO this can't sign personal message because we didn't set a delegate, but we don't need it also
return TokenInstanceWebView(server: server, walletAddress: walletAddress, assetDefinitionStore: assetDefinitionStore)
}()

@ -0,0 +1,149 @@
// Copyright © 2019 Stormbird PTE. LTD.
import Foundation
import UIKit
class WhereIsWalletAddressFoundOverlayView: UIView {
static func show() {
let view = WhereIsWalletAddressFoundOverlayView(frame: UIScreen.main.bounds)
view.show()
}
private let dialog = Dialog()
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .init(red: 0, green: 0, blue: 0, alpha: 0.3)
let blurEffect = UIBlurEffect(style: .regular)
let blurView = UIVisualEffectView(effect: blurEffect)
blurView.translatesAutoresizingMaskIntoConstraints = false
addSubview(blurView)
blurView.alpha = 0.3
clipBottomRight()
dialog.delegate = self
dialog.translatesAutoresizingMaskIntoConstraints = false
addSubview(dialog)
NSLayoutConstraint.activate([
blurView.leadingAnchor.constraint(equalTo: leadingAnchor),
blurView.trailingAnchor.constraint(equalTo: trailingAnchor),
blurView.topAnchor.constraint(equalTo: topAnchor),
blurView.bottomAnchor.constraint(equalTo: bottomAnchor),
dialog.rightAnchor.constraint(equalTo: rightAnchor, constant: -20),
dialog.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -120),
])
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func clipBottomRight() {
//TODO support clipping for iPad too
if UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiom.phone {
let clipDimension = CGFloat(180)
let clipPath = UIBezierPath(ovalIn: CGRect(x: UIScreen.main.bounds.size.width - clipDimension / 2 - 20, y: UIScreen.main.bounds.size.height - clipDimension / 2 - 20, width: clipDimension, height: clipDimension))
let maskPath = UIBezierPath(rect: UIScreen.main.bounds)
maskPath.append(clipPath.reversing())
let mask = CAShapeLayer()
mask.backgroundColor = UIColor.red.cgColor
mask.path = maskPath.cgPath
layer.mask = mask
}
}
@objc private func hide() {
removeFromSuperview()
}
func show() {
dialog.configure()
dialog.transform = CGAffineTransform(scaleX: 0.6, y: 0.6)
UIApplication.shared.keyWindow?.addSubview(self)
UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.5, initialSpringVelocity: 7, options: .curveEaseInOut, animations: {
self.dialog.transform = .identity
})
//TODO sound too
let feedbackGenerator = UINotificationFeedbackGenerator()
feedbackGenerator.prepare()
feedbackGenerator.notificationOccurred(.success)
}
}
extension WhereIsWalletAddressFoundOverlayView: DialogDelegate {
fileprivate func tappedContinue(inDialog dialog: Dialog) {
hide()
}
}
fileprivate protocol DialogDelegate: class {
func tappedContinue(inDialog dialog: Dialog)
}
fileprivate class Dialog: UIView {
private let titleLabel = UILabel()
private let descriptionLabel = UILabel()
private let buttonsBar = ButtonsBar(numberOfButtons: 1)
weak var delegate: DialogDelegate?
override init(frame: CGRect) {
super.init(frame: frame)
let stackView = [
titleLabel,
UIView.spacer(height: 12),
descriptionLabel,
UIView.spacer(height: 30),
buttonsBar
].asStackView(axis: .vertical)
stackView.translatesAutoresizingMaskIntoConstraints = false
addSubview(stackView)
NSLayoutConstraint.activate([
buttonsBar.heightAnchor.constraint(equalToConstant: ButtonsBar.buttonsHeight),
widthAnchor.constraint(equalToConstant: 300),
heightAnchor.constraint(equalToConstant: 250),
stackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 20),
stackView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -20),
stackView.topAnchor.constraint(equalTo: topAnchor, constant: 30),
stackView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -30),
])
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configure() {
backgroundColor = Colors.appWhite
titleLabel.font = Fonts.regular(size: 24)
titleLabel.textColor = .init(red: 33, green: 33, blue: 33)
titleLabel.textAlignment = .center
titleLabel.text = R.string.localizable.onboardingNewWalletFindAddressTitle()
descriptionLabel.numberOfLines = 0
descriptionLabel.font = Fonts.regular(size: 18)
descriptionLabel.textColor = .init(red: 102, green: 102, blue: 102)
descriptionLabel.textAlignment = .center
descriptionLabel.text = R.string.localizable.onboardingNewWalletFindAddressDescription()
buttonsBar.configure()
let continueButton = buttonsBar.buttons[0]
continueButton.setTitle("Continue".localizedUppercase, for: .normal)
continueButton.addTarget(self, action: #selector(hide), for: .touchUpInside)
}
@objc private func hide() {
delegate?.tappedContinue(inDialog: self)
}
}

@ -0,0 +1,54 @@
// Copyright © 2019 Stormbird PTE. LTD.
import Foundation
import UIKit
protocol BackupSeedPhraseCoordinatorDelegate: class {
func didTapTestSeedPhrase(forAccount account: EthereumAccount, inCoordinator coordinator: BackupSeedPhraseCoordinator)
func didClose(forAccount account: EthereumAccount, inCoordinator coordinator: BackupSeedPhraseCoordinator)
}
class BackupSeedPhraseCoordinator: Coordinator {
private lazy var rootViewController: ShowSeedPhraseViewController = {
let controller = ShowSeedPhraseViewController(keystore: keystore, account: account)
controller.configure()
controller.delegate = self
return controller
}()
private let account: EthereumAccount
private let keystore: Keystore
let navigationController: UINavigationController
var coordinators: [Coordinator] = []
weak var delegate: BackupSeedPhraseCoordinatorDelegate?
init(navigationController: UINavigationController = UINavigationController(), keystore: Keystore, account: EthereumAccount) {
self.navigationController = navigationController
self.keystore = keystore
self.account = account
}
func start() {
navigationController.pushViewController(rootViewController, animated: true)
}
func end() {
rootViewController.markDone()
}
func endUserInterface(animated: Bool) {
navigationController.viewControllers.firstIndex(of: rootViewController)
.flatMap { navigationController.viewControllers[$0 - 1] }
.flatMap { navigationController.popToViewController($0, animated: animated) }
}
}
extension BackupSeedPhraseCoordinator: ShowSeedPhraseViewControllerDelegate {
func didTapTestSeedPhrase(for account: EthereumAccount, inViewController viewController: ShowSeedPhraseViewController) {
delegate?.didTapTestSeedPhrase(forAccount: account, inCoordinator: self)
}
func didClose(for account: EthereumAccount, inViewController viewController: ShowSeedPhraseViewController) {
delegate?.didClose(forAccount: account, inCoordinator: self)
}
}

@ -0,0 +1,65 @@
// Copyright © 2019 Stormbird PTE. LTD.
import Foundation
import UIKit
protocol ElevateWalletSecurityCoordinatorDelegate: class {
func didLockWalletSuccessfully(forAccount account: EthereumAccount, inCoordinator coordinator: ElevateWalletSecurityCoordinator)
func didCancelLock(forAccount account: EthereumAccount, inCoordinator coordinator: ElevateWalletSecurityCoordinator)
}
class ElevateWalletSecurityCoordinator: Coordinator {
fileprivate struct Error: LocalizedError {
var errorDescription: String?
}
private lazy var rootViewController: ElevateWalletSecurityViewController = {
let controller = ElevateWalletSecurityViewController(keystore: keystore, account: account)
controller.configure()
controller.delegate = self
return controller
}()
private let account: EthereumAccount
private let keystore: Keystore
let navigationController: UINavigationController
var coordinators: [Coordinator] = []
weak var delegate: ElevateWalletSecurityCoordinatorDelegate?
init(navigationController: UINavigationController = UINavigationController(), keystore: Keystore, account: EthereumAccount) {
self.navigationController = navigationController
self.keystore = keystore
self.account = account
}
func start() {
navigationController.pushViewController(rootViewController, animated: true)
}
func end() {
//do nothing
}
func endUserInterface(animated: Bool) {
navigationController.popViewController(animated: animated)
}
}
extension ElevateWalletSecurityCoordinator: ElevateWalletSecurityViewControllerDelegate {
func didTapLock(inViewController viewController: ElevateWalletSecurityViewController) {
let isSuccessful = keystore.elevateSecurity(forAccount: account)
if isSuccessful {
delegate?.didLockWalletSuccessfully(forAccount: account, inCoordinator: self)
} else {
if keystore.isUserPresenceCheckPossible {
//do nothing. User cancelled
} else {
viewController.displayError(error: Error(errorDescription: R.string.localizable.keystoreAccessKeyLockFail()))
}
}
}
func didCancelLock(inViewController viewController: ElevateWalletSecurityViewController) {
delegate?.didCancelLock(forAccount: account, inCoordinator: self)
}
}

@ -0,0 +1,63 @@
// Copyright SIX DAY LLC. All rights reserved.
import Foundation
import UIKit
protocol EnterPasswordCoordinatorDelegate: class {
func didEnterPassword(password: String, account: EthereumAccount, in coordinator: EnterPasswordCoordinator)
func didCancel(in coordinator: EnterPasswordCoordinator)
}
class EnterPasswordCoordinator: CoordinatorThatEnds {
private lazy var rootViewController: KeystoreBackupIntroductionViewController = {
let controller = KeystoreBackupIntroductionViewController()
controller.delegate = self
controller.configure()
return controller
}()
private let account: EthereumAccount
let navigationController: UINavigationController
var coordinators: [Coordinator] = []
weak var delegate: EnterPasswordCoordinatorDelegate?
init(
navigationController: UINavigationController = UINavigationController(),
account: EthereumAccount
) {
self.navigationController = navigationController
self.account = account
}
func start() {
navigationController.pushViewController(rootViewController, animated: true)
}
func end() {
//do nothing
}
func endUserInterface(animated: Bool) {
navigationController.viewControllers.firstIndex(of: rootViewController)
.flatMap { navigationController.viewControllers[$0 - 1] }
.flatMap { navigationController.popToViewController($0, animated: animated) }
}
func createEnterPasswordController() -> EnterPasswordViewController {
let controller = EnterPasswordViewController(account: account)
controller.delegate = self
return controller
}
}
extension EnterPasswordCoordinator: KeystoreBackupIntroductionViewControllerDelegate {
func didTapExport(inViewController viewController: KeystoreBackupIntroductionViewController) {
navigationController.pushViewController(createEnterPasswordController(), animated: true)
}
}
extension EnterPasswordCoordinator: EnterPasswordViewControllerDelegate {
func didEnterPassword(password: String, for account: EthereumAccount, inViewController viewController: EnterPasswordViewController) {
delegate?.didEnterPassword(password: password, account: account, in: self)
}
}

@ -33,10 +33,12 @@ class InitialWalletCreationCoordinator: Coordinator {
switch entryPoint {
case .createInstantWallet, .welcome:
showCreateWallet()
case .addInitialWallet:
presentAddInitialWallet()
case .importWallet:
presentImportWallet()
case .backupWallet:
break
presentImportOrWatchWallet()
case .watchWallet:
presentImportOrWatchWallet()
}
}
@ -47,7 +49,15 @@ class InitialWalletCreationCoordinator: Coordinator {
addCoordinator(coordinator)
}
func presentImportWallet() {
func presentImportOrWatchWallet() {
let coordinator = WalletCoordinator(config: config, keystore: keystore)
coordinator.delegate = self
let _ = coordinator.start(entryPoint)
navigationController.present(coordinator.navigationController, animated: true, completion: nil)
addCoordinator(coordinator)
}
func presentAddInitialWallet() {
let coordinator = WalletCoordinator(config: config, keystore: keystore)
coordinator.delegate = self
let _ = coordinator.start(entryPoint)

@ -0,0 +1,47 @@
// Copyright © 2019 Stormbird PTE. LTD.
import Foundation
import UIKit
protocol VerifySeedPhraseCoordinatorDelegate: class {
func didVerifySeedPhraseSuccessfully(forAccount account: EthereumAccount, inCoordinator coordinator: VerifySeedPhraseCoordinator)
}
class VerifySeedPhraseCoordinator: Coordinator {
private lazy var rootViewController: VerifySeedPhraseViewController = {
let controller = VerifySeedPhraseViewController(keystore: keystore, account: account)
controller.configure()
controller.delegate = self
return controller
}()
private let account: EthereumAccount
private let keystore: Keystore
let navigationController: UINavigationController
var coordinators: [Coordinator] = []
weak var delegate: VerifySeedPhraseCoordinatorDelegate?
init(navigationController: UINavigationController = UINavigationController(), keystore: Keystore, account: EthereumAccount) {
self.navigationController = navigationController
self.keystore = keystore
self.account = account
}
func start() {
navigationController.pushViewController(rootViewController, animated: true)
}
func end() {
//do nothing
}
func endUserInterface(animated: Bool) {
navigationController.popViewController(animated: animated)
}
}
extension VerifySeedPhraseCoordinator: VerifySeedPhraseViewControllerDelegate {
func didVerifySeedPhraseSuccessfully(for account: EthereumAccount, in viewController: VerifySeedPhraseViewController) {
delegate?.didVerifySeedPhraseSuccessfully(forAccount: account, inCoordinator: self)
}
}

@ -1,7 +1,6 @@
// Copyright SIX DAY LLC. All rights reserved.
import Foundation
import TrustKeystore
import UIKit
protocol WalletCoordinatorDelegate: class {
@ -43,18 +42,20 @@ class WalletCoordinator: Coordinator {
controller.delegate = self
controller.navigationItem.leftBarButtonItem = UIBarButtonItem(title: R.string.localizable.cancel(), style: .plain, target: self, action: #selector(dismiss))
navigationController.viewControllers = [controller]
case .watchWallet:
let controller = ImportWalletViewController(keystore: keystore)
controller.delegate = self
controller.navigationItem.leftBarButtonItem = UIBarButtonItem(title: R.string.localizable.cancel(), style: .plain, target: self, action: #selector(dismiss))
controller.showWatchTab()
navigationController.viewControllers = [controller]
case .createInstantWallet:
createInstantWallet()
return false
case .backupWallet(let address):
if let type = keystore.recentlyUsedWallet?.type, case let .real(account) = type {
guard address.sameContract(as: account.address.eip55String) else { return false }
guard !config.isWalletAddressAlreadyPromptedForBackUp(address: AlphaWallet.Address(address: account.address)) else { return false }
config.addToWalletAddressesAlreadyPromptedForBackup(address: AlphaWallet.Address(address: account.address))
pushBackup(for: account)
} else {
return false
}
case .addInitialWallet:
let controller = CreateInitialWalletViewController(keystore: keystore)
controller.delegate = self
controller.configure()
navigationController.viewControllers = [controller]
}
return true
}
@ -64,22 +65,32 @@ class WalletCoordinator: Coordinator {
controller.delegate = self
navigationController.pushViewController(controller, animated: true)
}
func createInitialWallet() {
func createInitialWalletIfMissing() {
if !keystore.hasWallets {
let account = keystore.createAccount(password: PasswordGenerator.generateRandom())
keystore.recentlyUsedWallet = Wallet(type: WalletType.real(account))
let result = keystore.createAccount()
switch result {
case .success(let account):
keystore.recentlyUsedWallet = Wallet(type: WalletType.real(account))
case .failure:
//TODO handle initial wallet creation error. App can't be used!
break
}
}
}
//TODO Rename this is create in both settings and new install
func createInstantWallet() {
navigationController.displayLoading(text: R.string.localizable.walletCreateInProgress(), animated: false)
let password = PasswordGenerator.generateRandom()
keystore.createAccount(with: password) { [weak self] result in
keystore.createAccount() { [weak self] result in
guard let strongSelf = self else { return }
switch result {
case .success(let account):
let wallet = Wallet(type: WalletType.real(account))
//Bit of delay to wait for the UI animation to almost finish
DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
WhereIsWalletAddressFoundOverlayView.show()
}
strongSelf.delegate?.didFinish(with: wallet, in: strongSelf)
case .failure(let error):
//TODO this wouldn't work since navigationController isn't shown anymore
@ -89,32 +100,26 @@ class WalletCoordinator: Coordinator {
}
}
func pushBackup(for account: Account) {
let controller = BackupViewController(account: account)
controller.delegate = self
controller.navigationItem.backBarButtonItem = nil
controller.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "", style: .plain, target: self, action: nil)
navigationController.setNavigationBarHidden(true, animated: false)
navigationController.pushViewController(controller, animated: true)
private func addWalletWith(entryPoint: WalletEntryPoint) {
//Intentionally creating an instance of myself
let coordinator = WalletCoordinator(config: config, keystore: keystore)
coordinator.delegate = self
addCoordinator(coordinator)
let showUI = coordinator.start(entryPoint)
navigationController.present(coordinator.navigationController, animated: true, completion: nil)
}
@objc func dismiss() {
delegate?.didCancel(in: self)
}
//TODO Rename this is import in both settings and new install
func didCreateAccount(account: Wallet) {
delegate?.didFinish(with: account, in: self)
}
func backup(account: Account) {
let coordinator = BackupCoordinator(
navigationController: navigationController,
keystore: keystore,
account: account
)
coordinator.delegate = self
addCoordinator(coordinator)
coordinator.start()
//Bit of delay to wait for the UI animation to almost finish
DispatchQueue.main.asyncAfter(deadline: .now() + 0.6) {
SuccessOverlayView.show()
}
}
}
@ -130,7 +135,7 @@ class WalletCoordinator: Coordinator {
//}
extension WalletCoordinator: WelcomeViewControllerDelegate {
func didPressCreateWallet(in viewController: WelcomeViewController) {
func didPressGettingStartedButton(in viewController: WelcomeViewController) {
// showInitialWalletCoordinator(entryPoint: .createInstantWallet)
}
}
@ -142,19 +147,29 @@ extension WalletCoordinator: ImportWalletViewControllerDelegate {
}
}
extension WalletCoordinator: BackupViewControllerDelegate {
func didPressBackup(account: Account, in viewController: BackupViewController) {
backup(account: account)
extension WalletCoordinator: CreateInitialWalletViewControllerDelegate {
func didTapCreateWallet(inViewController viewController: CreateInitialWalletViewController) {
createInstantWallet()
}
func didTapWatchWallet(inViewController viewController: CreateInitialWalletViewController) {
addWalletWith(entryPoint: .watchWallet)
}
func didTapImportWallet(inViewController viewController: CreateInitialWalletViewController) {
addWalletWith(entryPoint: .importWallet)
}
}
extension WalletCoordinator: BackupCoordinatorDelegate {
func didCancel(coordinator: BackupCoordinator) {
removeCoordinator(coordinator)
extension WalletCoordinator: WalletCoordinatorDelegate {
func didFinish(with account: Wallet, in coordinator: WalletCoordinator) {
coordinator.navigationController.dismiss(animated: false, completion: nil)
self.removeCoordinator(coordinator)
self.delegate?.didFinish(with: account, in: self)
}
func didFinish(account: Account, in coordinator: BackupCoordinator) {
removeCoordinator(coordinator)
didCreateAccount(account: Wallet(type: .real(account)))
func didCancel(in coordinator: WalletCoordinator) {
coordinator.navigationController.dismiss(animated: true, completion: nil)
self.removeCoordinator(coordinator)
}
}

@ -11,7 +11,11 @@ enum ImportSelectionType {
var title: String {
switch self {
case .keystore:
return R.string.localizable.keystore()
if ScreenChecker().isNarrowScreen {
return R.string.localizable.keystoreShorter()
} else {
return R.string.localizable.keystore()
}
case .privateKey:
return R.string.localizable.privateKey()
case .mnemonic:

@ -3,6 +3,7 @@
import Foundation
enum ImportWalletTab {
case mnemonic
case keystore
case privateKey
case watch

@ -4,7 +4,8 @@ import Foundation
enum WalletEntryPoint {
case welcome
case addInitialWallet
case createInstantWallet
case importWallet
case backupWallet(address: AlphaWallet.Address)
case watchWallet
}

@ -0,0 +1,139 @@
// Copyright © 2019 Stormbird PTE. LTD.
import UIKit
protocol CreateInitialWalletViewControllerDelegate: class {
func didTapCreateWallet(inViewController viewController: CreateInitialWalletViewController)
func didTapWatchWallet(inViewController viewController: CreateInitialWalletViewController)
func didTapImportWallet(inViewController viewController: CreateInitialWalletViewController)
}
class CreateInitialWalletViewController: UIViewController {
private let keystore: Keystore
private var viewModel = CreateInitialViewModel()
private let roundedBackground = RoundedBackground()
private let subtitleLabel = UILabel()
private let imageView = UIImageView()
private let createWalletButtonBar = ButtonsBar(numberOfButtons: 1)
private let separator = UIView.spacer(height: 1)
private let haveWalletLabel = UILabel()
private let buttonsBar = ButtonsBar(numberOfButtons: 2)
private var imageViewDimension: CGFloat {
if ScreenChecker().isNarrowScreen {
return 60
} else {
return 90
}
}
private var topMarginOfImageView: CGFloat {
if ScreenChecker().isNarrowScreen {
return 100
} else {
return 170
}
}
weak var delegate: CreateInitialWalletViewControllerDelegate?
init(keystore: Keystore) {
self.keystore = keystore
super.init(nibName: nil, bundle: nil)
roundedBackground.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(roundedBackground)
imageView.contentMode = .scaleAspectFit
let stackView = [
UIView.spacer(height: topMarginOfImageView),
imageView,
UIView.spacer(height: 10),
subtitleLabel,
UIView.spacerWidth(flexible: true),
createWalletButtonBar,
UIView.spacer(height: 25),
separator,
UIView.spacer(height: 25),
haveWalletLabel,
UIView.spacer(height: 25),
].asStackView(axis: .vertical)
stackView.translatesAutoresizingMaskIntoConstraints = false
roundedBackground.addSubview(stackView)
let footerBar = UIView()
footerBar.translatesAutoresizingMaskIntoConstraints = false
footerBar.backgroundColor = .clear
roundedBackground.addSubview(footerBar)
footerBar.addSubview(buttonsBar)
NSLayoutConstraint.activate([
imageView.heightAnchor.constraint(equalToConstant: imageViewDimension),
stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
stackView.topAnchor.constraint(equalTo: view.topAnchor),
stackView.bottomAnchor.constraint(equalTo: footerBar.topAnchor),
createWalletButtonBar.heightAnchor.constraint(equalToConstant: ButtonsBar.buttonsHeight),
buttonsBar.leadingAnchor.constraint(equalTo: footerBar.leadingAnchor),
buttonsBar.trailingAnchor.constraint(equalTo: footerBar.trailingAnchor),
buttonsBar.topAnchor.constraint(equalTo: footerBar.topAnchor),
buttonsBar.heightAnchor.constraint(equalToConstant: ButtonsBar.buttonsHeight),
footerBar.leadingAnchor.constraint(equalTo: view.leadingAnchor),
footerBar.trailingAnchor.constraint(equalTo: view.trailingAnchor),
footerBar.topAnchor.constraint(equalTo: view.layoutGuide.bottomAnchor, constant: -ButtonsBar.buttonsHeight - ButtonsBar.marginAtBottomScreen),
footerBar.bottomAnchor.constraint(equalTo: view.bottomAnchor),
] + roundedBackground.createConstraintsWithContainer(view: view))
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configure() {
view.backgroundColor = Colors.appBackground
subtitleLabel.textAlignment = .center
subtitleLabel.textColor = viewModel.subtitleColor
subtitleLabel.font = viewModel.subtitleFont
subtitleLabel.text = viewModel.subtitle
imageView.image = viewModel.imageViewImage
separator.backgroundColor = viewModel.separatorColor
haveWalletLabel.textAlignment = .center
haveWalletLabel.textColor = viewModel.alreadyHaveWalletTextColor
haveWalletLabel.font = viewModel.alreadyHaveWalletTextFont
haveWalletLabel.text = viewModel.alreadyHaveWalletText
createWalletButtonBar.configure()
let createWalletButton = createWalletButtonBar .buttons[0]
createWalletButton.setTitle(viewModel.createButtonTitle, for: .normal)
createWalletButton.addTarget(self, action: #selector(createWallet), for: .touchUpInside)
buttonsBar.configureSecondary()
let watchButton = buttonsBar.buttons[0]
watchButton.setTitle(viewModel.watchButtonTitle, for: .normal)
watchButton.addTarget(self, action: #selector(watchWallet), for: .touchUpInside)
let importButton = buttonsBar.buttons[1]
importButton.setTitle(viewModel.importButtonTitle, for: .normal)
importButton.addTarget(self, action: #selector(importWallet), for: .touchUpInside)
}
@objc private func createWallet() {
delegate?.didTapCreateWallet(inViewController: self)
}
@objc private func watchWallet() {
delegate?.didTapWatchWallet(inViewController: self)
}
@objc private func importWallet() {
delegate?.didTapImportWallet(inViewController: self)
}
}

@ -0,0 +1,130 @@
// Copyright © 2019 Stormbird PTE. LTD.
import UIKit
protocol ElevateWalletSecurityViewControllerDelegate: class {
func didTapLock(inViewController viewController: ElevateWalletSecurityViewController)
func didCancelLock(inViewController viewController: ElevateWalletSecurityViewController)
}
class ElevateWalletSecurityViewController: UIViewController {
private let keystore: Keystore
private let account: EthereumAccount
lazy private var viewModel = ElevateWalletSecurityViewModel(isHdWallet: keystore.isHdWallet(account: account))
private let roundedBackground = RoundedBackground()
private let subtitleLabel = UILabel()
private let imageView = UIImageView()
private let descriptionLabel = UILabel()
private let cancelButton = UIButton(type: .system)
private let buttonsBar = ButtonsBar(numberOfButtons: 1)
private var imageViewDimension: CGFloat {
if ScreenChecker().isNarrowScreen {
return 180
} else {
return 250
}
}
weak var delegate: ElevateWalletSecurityViewControllerDelegate?
init(keystore: Keystore, account: EthereumAccount) {
self.keystore = keystore
self.account = account
super.init(nibName: nil, bundle: nil)
hidesBottomBarWhenPushed = true
roundedBackground.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(roundedBackground)
imageView.contentMode = .scaleAspectFit
let stackView = [
UIView.spacer(height: 30),
subtitleLabel,
UIView.spacer(height: 40),
imageView,
UIView.spacer(height: 40),
descriptionLabel,
].asStackView(axis: .vertical)
stackView.translatesAutoresizingMaskIntoConstraints = false
roundedBackground.addSubview(stackView)
cancelButton.translatesAutoresizingMaskIntoConstraints = false
roundedBackground.addSubview(cancelButton)
let footerBar = UIView()
footerBar.translatesAutoresizingMaskIntoConstraints = false
footerBar.backgroundColor = .clear
roundedBackground.addSubview(footerBar)
footerBar.addSubview(buttonsBar)
NSLayoutConstraint.activate([
imageView.heightAnchor.constraint(equalToConstant: imageViewDimension),
stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
stackView.topAnchor.constraint(equalTo: view.topAnchor),
stackView.bottomAnchor.constraint(lessThanOrEqualTo: view.bottomAnchor),
cancelButton.leadingAnchor.constraint(equalTo: footerBar.leadingAnchor, constant: 10),
cancelButton.trailingAnchor.constraint(equalTo: footerBar.trailingAnchor, constant: -10),
cancelButton.bottomAnchor.constraint(equalTo: footerBar.topAnchor, constant: -20),
buttonsBar.leadingAnchor.constraint(equalTo: footerBar.leadingAnchor),
buttonsBar.trailingAnchor.constraint(equalTo: footerBar.trailingAnchor),
buttonsBar.topAnchor.constraint(equalTo: footerBar.topAnchor),
buttonsBar.heightAnchor.constraint(equalToConstant: ButtonsBar.buttonsHeight),
footerBar.leadingAnchor.constraint(equalTo: view.leadingAnchor),
footerBar.trailingAnchor.constraint(equalTo: view.trailingAnchor),
footerBar.topAnchor.constraint(equalTo: view.layoutGuide.bottomAnchor, constant: -ButtonsBar.buttonsHeight - ButtonsBar.marginAtBottomScreen),
footerBar.bottomAnchor.constraint(equalTo: view.bottomAnchor),
] + roundedBackground.createConstraintsWithContainer(view: view))
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configure() {
view.backgroundColor = Colors.appBackground
title = viewModel.title
subtitleLabel.textAlignment = .center
subtitleLabel.numberOfLines = 0
subtitleLabel.textColor = viewModel.subtitleColor
subtitleLabel.font = viewModel.subtitleFont
subtitleLabel.text = viewModel.subtitle
imageView.image = viewModel.imageViewImage
descriptionLabel.textAlignment = .center
descriptionLabel.textColor = viewModel.descriptionColor
descriptionLabel.font = viewModel.descriptionFont
descriptionLabel.numberOfLines = 0
descriptionLabel.text = viewModel.description
cancelButton.addTarget(self, action: #selector(cancel), for: .touchUpInside)
cancelButton.setTitle(R.string.localizable.skip(), for: .normal)
cancelButton.titleLabel?.font = viewModel.cancelLockingButtonFont
cancelButton.titleLabel?.adjustsFontSizeToFitWidth = true
cancelButton.setTitleColor(viewModel.cancelLockingButtonTitleColor, for: .normal)
buttonsBar.configure()
let exportButton = buttonsBar.buttons[0]
exportButton.setTitle(viewModel.title, for: .normal)
exportButton.addTarget(self, action: #selector(tappedLockButton), for: .touchUpInside)
}
@objc private func tappedLockButton() {
delegate?.didTapLock(inViewController: self)
}
@objc func cancel() {
delegate?.didCancelLock(inViewController: self)
}
}

@ -1,45 +0,0 @@
// Copyright SIX DAY LLC. All rights reserved.
import Foundation
import TrustKeystore
protocol EnterPasswordCoordinatorDelegate: class {
func didEnterPassword(password: String, account: Account, in coordinator: EnterPasswordCoordinator)
func didCancel(in coordinator: EnterPasswordCoordinator)
}
class EnterPasswordCoordinator: Coordinator {
private lazy var enterPasswordController: EnterPasswordViewController = {
let controller = EnterPasswordViewController(account: account)
controller.navigationItem.leftBarButtonItem = UIBarButtonItem(title: R.string.localizable.cancel(), style: .plain, target: self, action: #selector(dismiss))
controller.delegate = self
return controller
}()
private let account: Account
let navigationController: UINavigationController
var coordinators: [Coordinator] = []
weak var delegate: EnterPasswordCoordinatorDelegate?
init(
navigationController: UINavigationController = UINavigationController(),
account: Account
) {
self.navigationController = navigationController
self.account = account
}
func start() {
navigationController.viewControllers = [enterPasswordController]
}
@objc func dismiss() {
delegate?.didCancel(in: self)
}
}
extension EnterPasswordCoordinator: EnterPasswordViewControllerDelegate {
func didEnterPassword(password: String, for account: Account, in viewController: EnterPasswordViewController) {
delegate?.didEnterPassword(password: password, account: account, in: self)
}
}

@ -2,10 +2,9 @@
import UIKit
import Eureka
import TrustKeystore
protocol EnterPasswordViewControllerDelegate: class {
func didEnterPassword(password: String, for account: Account, in viewController: EnterPasswordViewController)
func didEnterPassword(password: String, for account: EthereumAccount, inViewController viewController: EnterPasswordViewController)
}
class EnterPasswordViewController: FormViewController {
@ -18,31 +17,27 @@ class EnterPasswordViewController: FormViewController {
struct Values {
static var password = "password"
static var confirmPassword = "confirmPassword"
}
private let viewModel = EnterPasswordViewModel()
private let account: EthereumAccount
private var passwordTextField: UITextField?
private var passwordRow: TextFloatLabelRow? {
return form.rowBy(tag: Values.password) as? TextFloatLabelRow
}
private var confirmPasswordRow: TextFloatLabelRow? {
return form.rowBy(tag: Values.confirmPassword) as? TextFloatLabelRow
}
private let account: Account
weak var delegate: EnterPasswordViewControllerDelegate?
init(
account: Account
) {
init(account: EthereumAccount) {
self.account = account
super.init(nibName: nil, bundle: nil)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
@ -59,18 +54,20 @@ class EnterPasswordViewController: FormViewController {
$0.add(rule: RuleRequired())
$0.add(rule: ruleMin)
$0.validationOptions = .validatesOnDemand
}.cellUpdate { [unowned self] cell, _ in
cell.textField.isSecureTextEntry = true
}.cellUpdate { cell, _ in
cell.textField.isSecureTextEntry = false
cell.textField.placeholder = self.viewModel.passwordFieldPlaceholder
}
<<< AppFormAppearance.textFieldFloat(tag: Values.confirmPassword) {
$0.add(rule: RuleRequired())
$0.add(rule: ruleMin)
$0.validationOptions = .validatesOnDemand
}.cellUpdate { [unowned self] cell, _ in
cell.textField.isSecureTextEntry = true
cell.textField.placeholder = self.viewModel.confirmPasswordFieldPlaceholder
cell.textField.rightView = {
let button = UIButton(type: .system)
button.frame = .init(x: 0, y: 0, width: 30, height: 30)
button.setImage(R.image.togglePassword(), for: .normal)
button.tintColor = .init(red: 111, green: 111, blue: 111)
button.addTarget(self, action: #selector(self.toggleMaskPassword), for: .touchUpInside)
return button
}()
cell.textField.rightViewMode = .unlessEditing
cell.textField.autocorrectionType = .no
self.passwordTextField = cell.textField
}
+++ Section()
@ -78,25 +75,22 @@ class EnterPasswordViewController: FormViewController {
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
passwordRow?.cell.textField.becomeFirstResponder()
}
@objc func done() {
guard
form.validate().isEmpty,
let password = passwordRow?.value,
let confirmPassword = confirmPasswordRow?.value
else { return }
guard password == confirmPassword else {
displayError(error: ValidationError(msg: R.string.localizable.backupPasswordConfirmationMustMatch()))
return
}
delegate?.didEnterPassword(password: password, for: account, in: self)
guard form.validate().isEmpty, let password = passwordRow?.value else { return }
delegate?.didEnterPassword(password: password, for: account, inViewController: self)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
@objc private func toggleMaskPassword() {
guard let passwordTextField = passwordTextField else { return }
passwordTextField.isSecureTextEntry = !passwordTextField.isSecureTextEntry
guard let button = passwordTextField.rightView as? UIButton else { return }
if passwordTextField.isSecureTextEntry {
button.tintColor = Colors.appBackground
} else {
button.tintColor = .init(red: 111, green: 111, blue: 111)
}
}
}

@ -1,6 +1,8 @@
// Copyright SIX DAY LLC. All rights reserved.
import UIKit
import QRCodeReaderViewController
import TrustWalletCore
protocol ImportWalletViewControllerDelegate: class {
func didImportAccount(account: Wallet, in viewController: ImportWalletViewController)
@ -20,13 +22,17 @@ class ImportWalletViewController: UIViewController, CanScanQRCode {
private let roundedBackground = RoundedBackground()
private let scrollView = UIScrollView()
private let tabBar = ImportWalletTabBar()
private let mnemonicTextView = TextView()
private let keystoreJSONTextView = TextView()
private let passwordTextField = TextField()
private let privateKeyTextView = TextView()
private let watchAddressTextField = AddressTextField()
private var mnemonicControlsStackView: UIStackView!
private var keystoreJSONControlsStackView: UIStackView!
private var privateKeyControlsStackView: UIStackView!
private var watchControlsStackView: UIStackView!
private let importKeystoreJsonFromCloudButton = UIButton(type: .system)
private let importSeedDescriptionLabel = UILabel()
private let buttonsBar = ButtonsBar(numberOfButtons: 1)
weak var delegate: ImportWalletViewControllerDelegate?
@ -48,26 +54,56 @@ class ImportWalletViewController: UIViewController, CanScanQRCode {
tabBar.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(tabBar)
mnemonicTextView.label.translatesAutoresizingMaskIntoConstraints = false
mnemonicTextView.delegate = self
mnemonicTextView.translatesAutoresizingMaskIntoConstraints = false
mnemonicTextView.returnKeyType = .done
mnemonicTextView.textView.autocorrectionType = .no
mnemonicTextView.textView.autocapitalizationType = .none
keystoreJSONTextView.label.translatesAutoresizingMaskIntoConstraints = false
keystoreJSONTextView.delegate = self
keystoreJSONTextView.translatesAutoresizingMaskIntoConstraints = false
keystoreJSONTextView.returnKeyType = .next
keystoreJSONTextView.textView.autocorrectionType = .no
keystoreJSONTextView.textView.autocapitalizationType = .none
passwordTextField.label.translatesAutoresizingMaskIntoConstraints = false
passwordTextField.delegate = self
passwordTextField.translatesAutoresizingMaskIntoConstraints = false
passwordTextField.textField.autocorrectionType = .no
passwordTextField.textField.autocapitalizationType = .none
passwordTextField.returnKeyType = .done
passwordTextField.isSecureTextEntry = true
passwordTextField.isSecureTextEntry = false
passwordTextField.textField.clearButtonMode = .whileEditing
passwordTextField.textField.rightView = {
let button = UIButton(type: .system)
button.frame = .init(x: 0, y: 0, width: 30, height: 30)
button.setImage(R.image.togglePassword(), for: .normal)
button.tintColor = .init(red: 111, green: 111, blue: 111)
button.addTarget(self, action: #selector(self.toggleMaskPassword), for: .touchUpInside)
return button
}()
passwordTextField.textField.rightViewMode = .unlessEditing
privateKeyTextView.label.translatesAutoresizingMaskIntoConstraints = false
privateKeyTextView.delegate = self
privateKeyTextView.translatesAutoresizingMaskIntoConstraints = false
privateKeyTextView.returnKeyType = .done
privateKeyTextView.textView.autocorrectionType = .no
privateKeyTextView.textView.autocapitalizationType = .none
watchAddressTextField.translatesAutoresizingMaskIntoConstraints = false
watchAddressTextField.delegate = self
watchAddressTextField.returnKeyType = .done
mnemonicControlsStackView = [
mnemonicTextView.label,
.spacer(height: 4),
mnemonicTextView,
].asStackView(axis: .vertical)
mnemonicControlsStackView.translatesAutoresizingMaskIntoConstraints = false
keystoreJSONControlsStackView = [
keystoreJSONTextView.label,
.spacer(height: 4),
@ -97,6 +133,7 @@ class ImportWalletViewController: UIViewController, CanScanQRCode {
let stackView = [
tabBar,
.spacer(height: 10),
mnemonicControlsStackView,
keystoreJSONControlsStackView,
privateKeyControlsStackView,
watchControlsStackView,
@ -104,6 +141,15 @@ class ImportWalletViewController: UIViewController, CanScanQRCode {
stackView.translatesAutoresizingMaskIntoConstraints = false
scrollView.addSubview(stackView)
importKeystoreJsonFromCloudButton.isHidden = true
importKeystoreJsonFromCloudButton.translatesAutoresizingMaskIntoConstraints = false
roundedBackground.addSubview(importKeystoreJsonFromCloudButton)
importSeedDescriptionLabel.isHidden = false
importSeedDescriptionLabel.translatesAutoresizingMaskIntoConstraints = false
importSeedDescriptionLabel.textAlignment = .center
roundedBackground.addSubview(importSeedDescriptionLabel)
let footerBar = UIView()
footerBar.translatesAutoresizingMaskIntoConstraints = false
footerBar.backgroundColor = .clear
@ -114,12 +160,15 @@ class ImportWalletViewController: UIViewController, CanScanQRCode {
let xMargin = CGFloat(7)
let heightThatFitsPrivateKeyNicely = CGFloat(100)
NSLayoutConstraint.activate([
mnemonicTextView.heightAnchor.constraint(equalToConstant: heightThatFitsPrivateKeyNicely),
keystoreJSONTextView.heightAnchor.constraint(equalToConstant: heightThatFitsPrivateKeyNicely),
privateKeyTextView.heightAnchor.constraint(equalToConstant: heightThatFitsPrivateKeyNicely),
tabBar.leadingAnchor.constraint(equalTo: stackView.leadingAnchor),
tabBar.trailingAnchor.constraint(equalTo: stackView.trailingAnchor),
mnemonicControlsStackView.leadingAnchor.constraint(equalTo: stackView.leadingAnchor, constant: xMargin),
mnemonicControlsStackView.trailingAnchor.constraint(equalTo: stackView.trailingAnchor, constant: -xMargin),
keystoreJSONControlsStackView.leadingAnchor.constraint(equalTo: stackView.leadingAnchor, constant: xMargin),
keystoreJSONControlsStackView.trailingAnchor.constraint(equalTo: stackView.trailingAnchor, constant: -xMargin),
privateKeyControlsStackView.leadingAnchor.constraint(equalTo: stackView.leadingAnchor, constant: xMargin),
@ -132,6 +181,14 @@ class ImportWalletViewController: UIViewController, CanScanQRCode {
stackView.topAnchor.constraint(equalTo: scrollView.topAnchor),
stackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
importKeystoreJsonFromCloudButton.leadingAnchor.constraint(equalTo: footerBar.leadingAnchor, constant: 10),
importKeystoreJsonFromCloudButton.trailingAnchor.constraint(equalTo: footerBar.trailingAnchor, constant: -10),
importKeystoreJsonFromCloudButton.bottomAnchor.constraint(equalTo: footerBar.topAnchor, constant: -20),
importSeedDescriptionLabel.leadingAnchor.constraint(equalTo: footerBar.leadingAnchor, constant: 30),
importSeedDescriptionLabel.trailingAnchor.constraint(equalTo: footerBar.trailingAnchor, constant: -30),
importSeedDescriptionLabel.bottomAnchor.constraint(equalTo: footerBar.topAnchor, constant: -20),
buttonsBar.leadingAnchor.constraint(equalTo: footerBar.leadingAnchor),
buttonsBar.trailingAnchor.constraint(equalTo: footerBar.trailingAnchor),
buttonsBar.topAnchor.constraint(equalTo: footerBar.topAnchor),
@ -149,12 +206,9 @@ class ImportWalletViewController: UIViewController, CanScanQRCode {
] + roundedBackground.createConstraintsWithContainer(view: view))
configure()
showKeystoreControlsOnly()
showMnemonicControlsOnly()
navigationItem.rightBarButtonItems = [
UIBarButtonItem(image: R.image.import_options(), style: .done, target: self, action: #selector(importOptions)),
UIBarButtonItem(image: R.image.qr_code_icon(), style: .done, target: self, action: #selector(openReader)),
]
navigationItem.rightBarButtonItem = UIBarButtonItem(image: R.image.qr_code_icon(), style: .done, target: self, action: #selector(openReader))
if UserDefaults.standard.bool(forKey: "FASTLANE_SNAPSHOT") {
DispatchQueue.main.asyncAfter(deadline: .now() + 3) { [weak self] in
@ -164,9 +218,30 @@ class ImportWalletViewController: UIViewController, CanScanQRCode {
}
}
private func showCorrectTab() {
switch tabBar.tab {
case .mnemonic:
showMnemonicControlsOnly()
case .keystore:
showKeystoreControlsOnly()
case .privateKey:
showPrivateKeyControlsOnly()
case .watch:
showWatchControlsOnly()
}
}
func showWatchTab() {
tabBar.showWatchTab()
}
func configure() {
view.backgroundColor = viewModel.backgroundColor
mnemonicTextView.configureOnce()
mnemonicTextView.label.textAlignment = .center
mnemonicTextView.label.text = viewModel.mnemonicLabel
keystoreJSONTextView.configureOnce()
keystoreJSONTextView.label.textAlignment = .center
keystoreJSONTextView.label.text = viewModel.keystoreJSONLabel
@ -183,10 +258,25 @@ class ImportWalletViewController: UIViewController, CanScanQRCode {
watchAddressTextField.configureOnce()
importKeystoreJsonFromCloudButton.addTarget(self, action: #selector(importOptions), for: .touchUpInside)
importKeystoreJsonFromCloudButton.setTitle(R.string.localizable.importWalletImportFromCloudTitle(), for: .normal)
importKeystoreJsonFromCloudButton.titleLabel?.font = viewModel.importKeystoreJsonButtonFont
importKeystoreJsonFromCloudButton.titleLabel?.adjustsFontSizeToFitWidth = true
importSeedDescriptionLabel.font = viewModel.importSeedDescriptionFont
importSeedDescriptionLabel.textColor = viewModel.importSeedDescriptionColor
importSeedDescriptionLabel.text = R.string.localizable.importWalletImportSeedPhraseDescription()
importSeedDescriptionLabel.numberOfLines = 0
buttonsBar.configure()
let importButton = buttonsBar.buttons[0]
importButton.setTitle(R.string.localizable.importWalletImportButtonTitle(), for: .normal)
importButton.addTarget(self, action: #selector(importWallet), for: .touchUpInside)
configureImportButtonTitle(R.string.localizable.importWalletImportButtonTitle())
}
private func configureImportButtonTitle(_ title: String) {
let importButton = buttonsBar.buttons[0]
importButton.setTitle(title, for: .normal)
}
func didImport(account: Wallet) {
@ -196,6 +286,8 @@ class ImportWalletViewController: UIViewController, CanScanQRCode {
///Returns true only if valid
private func validate() -> Bool {
switch tabBar.tab {
case .mnemonic:
return validateMnemonic()
case .keystore:
return validateKeystore()
case .privateKey:
@ -205,6 +297,15 @@ class ImportWalletViewController: UIViewController, CanScanQRCode {
}
}
///Returns true only if valid
private func validateMnemonic() -> Bool {
if let validationError = MnemonicRule().isValid(value: mnemonicTextView.value) {
displayError(error: ValidationError(msg: validationError.msg))
return false
}
return true
}
///Returns true only if valid
private func validateKeystore() -> Bool {
if keystoreJSONTextView.value.isEmpty {
@ -220,7 +321,7 @@ class ImportWalletViewController: UIViewController, CanScanQRCode {
///Returns true only if valid
private func validatePrivateKey() -> Bool {
if let validationError = PrivateKeyRule().isValid(value: privateKeyTextView.value) {
if let validationError = PrivateKeyRule().isValid(value: privateKeyTextView.value.trimmed) {
displayError(error: ValidationError(msg: validationError.msg))
return false
}
@ -239,6 +340,7 @@ class ImportWalletViewController: UIViewController, CanScanQRCode {
@objc func importWallet() {
guard validate() else { return }
let mnemonicInput = mnemonicTextView.value.trimmed
let keystoreInput = keystoreJSONTextView.value.trimmed
let privateKeyInput = privateKeyTextView.value.trimmed.drop0x
let password = passwordTextField.value.trimmed
@ -246,17 +348,25 @@ class ImportWalletViewController: UIViewController, CanScanQRCode {
displayLoading(text: R.string.localizable.importWalletImportingIndicatorLabelTitle(), animated: false)
let importType: ImportType = {
let importTypeOptional: ImportType? = {
switch tabBar.tab {
case .mnemonic:
return .mnemonic(words: mnemonicInput.split(separator: " ").map { String($0) }, password: "")
case .keystore:
return .keystore(string: keystoreInput, password: password)
case .privateKey:
return .privateKey(privateKey: privateKeyInput)
guard let data = Data(hexString: privateKeyInput) else {
hideLoading(animated: false)
displayError(error: ValidationError(msg: R.string.localizable.importWalletImportInvalidPrivateKey()))
return nil
}
return .privateKey(privateKey: data)
case .watch:
let address = AlphaWallet.Address(string: watchInput)! // Address validated by form view.
return .watch(address: address)
}
}()
guard let importType = importTypeOptional else { return }
keystore.importWallet(type: importType) { [weak self] result in
guard let strongSelf = self else { return }
@ -313,6 +423,8 @@ class ImportWalletViewController: UIViewController, CanScanQRCode {
func setValueForCurrentField(string: String) {
switch tabBar.tab {
case .mnemonic:
mnemonicTextView.value = string
case .keystore:
keystoreJSONTextView.value = string
case .privateKey:
@ -320,30 +432,65 @@ class ImportWalletViewController: UIViewController, CanScanQRCode {
case .watch:
watchAddressTextField.value = string
}
showCorrectTab()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func showMnemonicControlsOnly() {
mnemonicControlsStackView.isHidden = false
keystoreJSONControlsStackView.isHidden = true
privateKeyControlsStackView.isHidden = true
watchControlsStackView.isHidden = true
configureImportButtonTitle(R.string.localizable.importWalletImportButtonTitle())
importKeystoreJsonFromCloudButton.isHidden = true
importSeedDescriptionLabel.isHidden = false
let importButton = buttonsBar.buttons[0]
importButton.isEnabled = !mnemonicTextView.value.isEmpty
}
private func showKeystoreControlsOnly() {
mnemonicControlsStackView.isHidden = true
keystoreJSONControlsStackView.isHidden = false
privateKeyControlsStackView.isHidden = true
watchControlsStackView.isHidden = true
configureImportButtonTitle(R.string.localizable.importWalletImportButtonTitle())
importKeystoreJsonFromCloudButton.isHidden = false
importSeedDescriptionLabel.isHidden = true
let importButton = buttonsBar.buttons[0]
importButton.isEnabled = !keystoreJSONTextView.value.isEmpty && !passwordTextField.value.isEmpty
}
private func showPrivateKeyControlsOnly() {
mnemonicControlsStackView.isHidden = true
keystoreJSONControlsStackView.isHidden = true
privateKeyControlsStackView.isHidden = false
watchControlsStackView.isHidden = true
configureImportButtonTitle(R.string.localizable.importWalletImportButtonTitle())
importKeystoreJsonFromCloudButton.isHidden = true
importSeedDescriptionLabel.isHidden = true
let importButton = buttonsBar.buttons[0]
importButton.isEnabled = !privateKeyTextView.value.isEmpty
}
private func showWatchControlsOnly() {
mnemonicControlsStackView.isHidden = true
keystoreJSONControlsStackView.isHidden = true
privateKeyControlsStackView.isHidden = true
watchControlsStackView.isHidden = false
configureImportButtonTitle(R.string.localizable.walletWatchButtonTitle())
importKeystoreJsonFromCloudButton.isHidden = true
importSeedDescriptionLabel.isHidden = true
let importButton = buttonsBar.buttons[0]
importButton.isEnabled = !watchAddressTextField.value.isEmpty
}
private func moveFocusToTextEntryField(after textInput: UIView) {
switch textInput {
case mnemonicTextView:
view.endEditing(true)
case keystoreJSONTextView:
_ = passwordTextField.becomeFirstResponder()
case passwordTextField:
@ -356,6 +503,16 @@ class ImportWalletViewController: UIViewController, CanScanQRCode {
break
}
}
@objc private func toggleMaskPassword() {
passwordTextField.isSecureTextEntry = !passwordTextField.isSecureTextEntry
guard let button = passwordTextField.textField.rightView as? UIButton else { return }
if passwordTextField.isSecureTextEntry {
button.tintColor = Colors.appBackground
} else {
button.tintColor = .init(red: 111, green: 111, blue: 111)
}
}
}
extension ImportWalletViewController: UIDocumentPickerDelegate {
@ -373,6 +530,7 @@ extension ImportWalletViewController: QRCodeReaderDelegate {
reader.stopScanning()
reader.dismiss(animated: true, completion: nil)
}
func reader(_ reader: QRCodeReaderViewController!, didScanResult result: String!) {
reader.stopScanning()
setValueForCurrentField(string: result)
@ -393,6 +551,14 @@ extension ImportWalletViewController: TextFieldDelegate {
func nextButtonTapped(for textField: TextField) {
moveFocusToTextEntryField(after: textField)
}
func shouldChangeCharacters(inRange range: NSRange, replacementString string: String, for textField: TextField) -> Bool {
//Just easier to dispatch
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.showCorrectTab()
}
return true
}
}
extension ImportWalletViewController: TextViewDelegate {
@ -408,6 +574,10 @@ extension ImportWalletViewController: TextViewDelegate {
func nextButtonTapped(for textView: TextView) {
moveFocusToTextEntryField(after: textView)
}
func didChange(inTextView textView: TextView) {
showCorrectTab()
}
}
extension ImportWalletViewController: AddressTextFieldDelegate {
@ -421,6 +591,7 @@ extension ImportWalletViewController: AddressTextFieldDelegate {
func didPaste(in textField: AddressTextField) {
view.endEditing(true)
showCorrectTab()
}
func shouldReturn(in textField: AddressTextField) -> Bool {
@ -429,18 +600,12 @@ extension ImportWalletViewController: AddressTextFieldDelegate {
}
func didChange(to string: String, in textField: AddressTextField) {
showCorrectTab()
}
}
extension ImportWalletViewController: ImportWalletTabBarDelegate {
func didPressImportWalletTab(tab: ImportWalletTab, in tabBar: ImportWalletTabBar) {
switch tab {
case .keystore:
showKeystoreControlsOnly()
case .privateKey:
showPrivateKeyControlsOnly()
case .watch:
showWatchControlsOnly()
}
showCorrectTab()
}
}

@ -0,0 +1,106 @@
// Copyright © 2019 Stormbird PTE. LTD.
import UIKit
protocol KeystoreBackupIntroductionViewControllerDelegate: class {
func didTapExport(inViewController viewController: KeystoreBackupIntroductionViewController)
}
class KeystoreBackupIntroductionViewController: UIViewController {
private var viewModel = KeystoreBackupIntroductionViewModel()
private let roundedBackground = RoundedBackground()
private let subtitleLabel = UILabel()
private let imageView = UIImageView()
private let descriptionLabel = UILabel()
private let buttonsBar = ButtonsBar(numberOfButtons: 1)
private var imageViewDimension: CGFloat {
if ScreenChecker().isNarrowScreen {
return 180
} else {
return 250
}
}
weak var delegate: KeystoreBackupIntroductionViewControllerDelegate?
init() {
super.init(nibName: nil, bundle: nil)
hidesBottomBarWhenPushed = true
roundedBackground.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(roundedBackground)
imageView.contentMode = .scaleAspectFit
let stackView = [
UIView.spacer(height: 30),
subtitleLabel,
UIView.spacer(height: 40),
imageView,
UIView.spacer(height: 40),
descriptionLabel,
].asStackView(axis: .vertical)
stackView.translatesAutoresizingMaskIntoConstraints = false
roundedBackground.addSubview(stackView)
let footerBar = UIView()
footerBar.translatesAutoresizingMaskIntoConstraints = false
footerBar.backgroundColor = .clear
roundedBackground.addSubview(footerBar)
footerBar.addSubview(buttonsBar)
NSLayoutConstraint.activate([
imageView.heightAnchor.constraint(equalToConstant: imageViewDimension),
stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
stackView.topAnchor.constraint(equalTo: view.topAnchor),
stackView.bottomAnchor.constraint(lessThanOrEqualTo: view.bottomAnchor),
buttonsBar.leadingAnchor.constraint(equalTo: footerBar.leadingAnchor),
buttonsBar.trailingAnchor.constraint(equalTo: footerBar.trailingAnchor),
buttonsBar.topAnchor.constraint(equalTo: footerBar.topAnchor),
buttonsBar.heightAnchor.constraint(equalToConstant: ButtonsBar.buttonsHeight),
footerBar.leadingAnchor.constraint(equalTo: view.leadingAnchor),
footerBar.trailingAnchor.constraint(equalTo: view.trailingAnchor),
footerBar.topAnchor.constraint(equalTo: view.layoutGuide.bottomAnchor, constant: -ButtonsBar.buttonsHeight - ButtonsBar.marginAtBottomScreen),
footerBar.bottomAnchor.constraint(equalTo: view.bottomAnchor),
] + roundedBackground.createConstraintsWithContainer(view: view))
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configure() {
view.backgroundColor = Colors.appBackground
title = viewModel.title
subtitleLabel.textAlignment = .center
subtitleLabel.textColor = viewModel.subtitleColor
subtitleLabel.font = viewModel.subtitleFont
subtitleLabel.text = viewModel.subtitle
imageView.image = viewModel.imageViewImage
descriptionLabel.textAlignment = .center
descriptionLabel.textColor = viewModel.descriptionColor
descriptionLabel.font = viewModel.descriptionFont
descriptionLabel.numberOfLines = 0
descriptionLabel.text = viewModel.description
buttonsBar.configure()
let exportButton = buttonsBar.buttons[0]
exportButton.setTitle(viewModel.title, for: .normal)
exportButton.addTarget(self, action: #selector(tappedExportButton), for: .touchUpInside)
}
@objc private func tappedExportButton() {
delegate?.didTapExport(inViewController: self)
}
}

@ -0,0 +1,226 @@
// Copyright © 2019 Stormbird PTE. LTD.
import UIKit
protocol ShowSeedPhraseViewControllerDelegate: class {
func didTapTestSeedPhrase(for account: EthereumAccount, inViewController viewController: ShowSeedPhraseViewController)
func didClose(for account: EthereumAccount, inViewController viewController: ShowSeedPhraseViewController)
}
//We must be careful to no longer show the seedphrase and remove it from memory when this screen is hidden because another VC is displayed over it or because the device is locked
class ShowSeedPhraseViewController: UIViewController {
private enum State {
case notDisplayedSeedPhrase
case displayingSeedPhrase(words: [String])
case errorDisplaySeedPhrase(KeystoreError)
case done
}
private var viewModel: ShowSeedPhraseViewModel {
didSet {
seedPhraseCollectionView.viewModel = .init(words: viewModel.words, shouldShowSequenceNumber: true)
}
}
private let keystore: Keystore
private let account: EthereumAccount
private let roundedBackground = RoundedBackground()
private let subtitleLabel = UILabel()
private let descriptionLabel = UILabel()
private let errorLabel = UILabel()
private var state: State = .notDisplayedSeedPhrase {
didSet {
switch state {
case .notDisplayedSeedPhrase:
viewModel = .init(words: [])
case .displayingSeedPhrase(let words):
viewModel = .init(words: words)
case .errorDisplaySeedPhrase(let error):
viewModel = .init(error: error)
case .done:
viewModel = .init(words: [])
}
configure()
}
}
private let seedPhraseCollectionView = SeedPhraseCollectionView()
private let buttonsBar = ButtonsBar(numberOfButtons: 1)
private var notDisplayingSeedPhrase: Bool {
switch state {
case .notDisplayedSeedPhrase:
return true
case .displayingSeedPhrase:
return false
case .errorDisplaySeedPhrase:
return false
case .done:
return false
}
}
private var isDone: Bool {
switch state {
case .notDisplayedSeedPhrase:
return false
case .displayingSeedPhrase:
return false
case .errorDisplaySeedPhrase:
return false
case .done:
return true
}
}
//We have this flag because when prompted for Touch ID/Face ID, the app becomes inactive, and the order is:
//1. we read the seed, thus the prompt shows up, making the app inactive
//2. user authenticates and we get the seed
//3. app is now notified as inactive! (note that this is after authentication succeeds)
//4. app becomes active
//Without this flag, we will be removing the seed in (3) and trying to read it in (4) again and triggering (1), thus going into an infinite loop of reading
private var isInactiveBecauseWeAccessingBiometrics = false
weak var delegate: ShowSeedPhraseViewControllerDelegate?
init(keystore: Keystore, account: EthereumAccount) {
self.keystore = keystore
self.account = account
self.viewModel = .init(words: [])
super.init(nibName: nil, bundle: nil)
hidesBottomBarWhenPushed = true
roundedBackground.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(roundedBackground)
let stackView = [
UIView.spacer(height: 30),
subtitleLabel,
descriptionLabel,
UIView.spacer(height: 10),
errorLabel,
UIView.spacer(height: 50),
seedPhraseCollectionView,
].asStackView(axis: .vertical)
stackView.translatesAutoresizingMaskIntoConstraints = false
roundedBackground.addSubview(stackView)
let footerBar = UIView()
footerBar.translatesAutoresizingMaskIntoConstraints = false
footerBar.backgroundColor = .clear
roundedBackground.addSubview(footerBar)
footerBar.addSubview(buttonsBar)
NSLayoutConstraint.activate([
stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
stackView.topAnchor.constraint(equalTo: view.topAnchor),
stackView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
buttonsBar.leadingAnchor.constraint(equalTo: footerBar.leadingAnchor),
buttonsBar.trailingAnchor.constraint(equalTo: footerBar.trailingAnchor),
buttonsBar.topAnchor.constraint(equalTo: footerBar.topAnchor),
buttonsBar.heightAnchor.constraint(equalToConstant: ButtonsBar.buttonsHeight),
footerBar.leadingAnchor.constraint(equalTo: view.leadingAnchor),
footerBar.trailingAnchor.constraint(equalTo: view.trailingAnchor),
footerBar.topAnchor.constraint(equalTo: view.layoutGuide.bottomAnchor, constant: -ButtonsBar.buttonsHeight - ButtonsBar.marginAtBottomScreen),
footerBar.bottomAnchor.constraint(equalTo: view.bottomAnchor),
] + roundedBackground.createConstraintsWithContainer(view: view))
NotificationCenter.default.addObserver(self, selector: #selector(appWillResignsActive), name: UIApplication.willResignActiveNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(appDidBecomeActive), name: UIApplication.didBecomeActiveNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(didTakeScreenShot), name: UIApplication.userDidTakeScreenshotNotification, object: nil)
hidesBottomBarWhenPushed = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
if isMovingFromParent || isBeingDismissed {
delegate?.didClose(for: account, inViewController: self)
return
}
removeSeedPhraseFromDisplay()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
showSeedPhrases()
}
@objc private func appWillResignsActive() {
if isInactiveBecauseWeAccessingBiometrics {
isInactiveBecauseWeAccessingBiometrics = false
return
}
removeSeedPhraseFromDisplay()
}
@objc private func appDidBecomeActive() {
showSeedPhrases()
}
@objc private func didTakeScreenShot() {
displaySuccess(message: R.string.localizable.walletsShowSeedPhraseDoNotTakeScreenshotDescription())
}
private func showSeedPhrases() {
guard !isDone else { return }
guard isTopViewController else { return }
guard notDisplayingSeedPhrase else { return }
isInactiveBecauseWeAccessingBiometrics = true
keystore.exportSeedPhraseOfHdWallet(forAccount: account, reason: .backup) { result in
switch result {
case .success(let words):
self.state = .displayingSeedPhrase(words: words.split(separator: " ").map { String($0) })
case .failure(let error):
self.state = .errorDisplaySeedPhrase(error)
}
}
}
func configure() {
view.backgroundColor = Colors.appBackground
title = viewModel.title
subtitleLabel.textAlignment = .center
subtitleLabel.textColor = viewModel.subtitleColor
subtitleLabel.font = viewModel.subtitleFont
//Important for smaller screens
subtitleLabel.numberOfLines = 0
subtitleLabel.text = viewModel.subtitle
descriptionLabel.textAlignment = .center
descriptionLabel.textColor = viewModel.descriptionColor
descriptionLabel.font = viewModel.descriptionFont
descriptionLabel.numberOfLines = 0
descriptionLabel.text = viewModel.description
errorLabel.textColor = viewModel.errorColor
errorLabel.font = viewModel.errorFont
errorLabel.text = viewModel.errorMessage
seedPhraseCollectionView.configure()
buttonsBar.configure()
let testSeedPhraseButton = buttonsBar.buttons[0]
testSeedPhraseButton.setTitle(R.string.localizable.walletsShowSeedPhraseTestSeedPhrase(), for: .normal)
testSeedPhraseButton.addTarget(self, action: #selector(testSeedPhrase), for: .touchUpInside)
}
@objc private func testSeedPhrase() {
delegate?.didTapTestSeedPhrase(for: account, inViewController: self)
}
private func removeSeedPhraseFromDisplay() {
guard !isDone else { return }
state = .notDisplayedSeedPhrase
}
func markDone() {
state = .done
}
}

@ -0,0 +1,314 @@
// Copyright © 2019 Stormbird PTE. LTD.
import UIKit
protocol VerifySeedPhraseViewControllerDelegate: class {
func didVerifySeedPhraseSuccessfully(for account: EthereumAccount, in viewController: VerifySeedPhraseViewController)
}
class VerifySeedPhraseViewController: UIViewController {
private enum State {
case editingSeedPhrase(words: [String])
case seedPhraseNotMatched(words: [String])
case seedPhraseMatched(words: [String])
case keystoreError(KeystoreError)
case notDisplayedSeedPhrase
case errorDisplaySeedPhrase(KeystoreError)
var words: [String] {
switch self {
case .editingSeedPhrase(let words), .seedPhraseMatched(let words), .seedPhraseNotMatched(let words):
return words
case .keystoreError, .notDisplayedSeedPhrase, .errorDisplaySeedPhrase:
return .init()
}
}
}
private var viewModel: VerifySeedPhraseViewModel
private let keystore: Keystore
private let account: EthereumAccount
private let roundedBackground = RoundedBackground()
private let seedPhraseTextView = UITextView()
private let seedPhraseCollectionView = SeedPhraseCollectionView()
private let errorLabel = UILabel()
private let clearChooseSeedPhraseButton = UIButton(type: .system)
private let buttonsBar = ButtonsBar(numberOfButtons: 1)
private var state: State {
didSet {
switch state {
case .editingSeedPhrase(let words):
seedPhraseCollectionView.viewModel = .init(words: words, isSelectable: true)
clearError()
case .seedPhraseMatched(let words):
seedPhraseCollectionView.viewModel = .init(words: words, isSelectable: true)
errorLabel.text = viewModel.noErrorText
errorLabel.textColor = viewModel.noErrorColor
seedPhraseTextView.borderColor = viewModel.seedPhraseTextViewBorderNormalColor
delegate?.didVerifySeedPhraseSuccessfully(for: account, in: self)
case .seedPhraseNotMatched(let words):
errorLabel.text = R.string.localizable.walletsVerifySeedPhraseWrong()
errorLabel.textColor = viewModel.errorColor
seedPhraseTextView.borderColor = viewModel.seedPhraseTextViewBorderErrorColor
case .keystoreError(let error):
seedPhraseCollectionView.viewModel = .init(words: [], isSelectable: true)
errorLabel.text = error.errorDescription
errorLabel.textColor = viewModel.errorColor
seedPhraseTextView.borderColor = viewModel.seedPhraseTextViewBorderErrorColor
case .notDisplayedSeedPhrase:
seedPhraseCollectionView.viewModel = .init(words: [], isSelectable: true)
case .errorDisplaySeedPhrase(let error):
seedPhraseCollectionView.viewModel = .init(words: [], isSelectable: true)
errorLabel.text = error.errorDescription
errorLabel.textColor = viewModel.errorColor
seedPhraseTextView.borderColor = viewModel.seedPhraseTextViewBorderErrorColor
}
}
}
private var notDisplayingSeedPhrase: Bool {
switch state {
case .editingSeedPhrase:
return false
case .seedPhraseMatched:
return false
case .seedPhraseNotMatched:
return false
case .keystoreError(let error):
return false
case .notDisplayedSeedPhrase:
return true
case .errorDisplaySeedPhrase:
return false
}
}
//We have this flag because when prompted for Touch ID/Face ID, the app becomes inactive, and the order is:
//1. we read the seed, thus the prompt shows up, making the app inactive
//2. user authenticates and we get the seed
//3. app is now notified as inactive! (note that this is after authentication succeeds)
//4. app becomes active
//Without this flag, we will be removing the seed in (3) and trying to read it in (4) again and triggering (1), thus going into an infinite loop of reading
private var isInactiveBecauseWeAccessingBiometrics = false
weak var delegate: VerifySeedPhraseViewControllerDelegate?
init(keystore: Keystore, account: EthereumAccount) {
self.keystore = keystore
self.account = account
self.viewModel = .init()
self.state = .notDisplayedSeedPhrase
super.init(nibName: nil, bundle: nil)
seedPhraseCollectionView.seedPhraseDelegate = self
roundedBackground.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(roundedBackground)
seedPhraseTextView.isEditable = false
//Disable copying
seedPhraseTextView.isUserInteractionEnabled = false
seedPhraseTextView.delegate = self
let stackView = [
UIView.spacer(height: 30),
seedPhraseTextView,
UIView.spacer(height: 7),
errorLabel,
UIView.spacer(height: 30),
seedPhraseCollectionView,
].asStackView(axis: .vertical)
stackView.translatesAutoresizingMaskIntoConstraints = false
roundedBackground.addSubview(stackView)
clearChooseSeedPhraseButton.isHidden = true
clearChooseSeedPhraseButton.translatesAutoresizingMaskIntoConstraints = false
roundedBackground.addSubview(clearChooseSeedPhraseButton)
let footerBar = UIView()
footerBar.translatesAutoresizingMaskIntoConstraints = false
footerBar.backgroundColor = .clear
roundedBackground.addSubview(footerBar)
footerBar.addSubview(buttonsBar)
seedPhraseTextView.becomeFirstResponder()
NSLayoutConstraint.activate([
seedPhraseTextView.heightAnchor.constraint(equalToConstant: 140),
stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
stackView.topAnchor.constraint(equalTo: view.topAnchor),
stackView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
clearChooseSeedPhraseButton.leadingAnchor.constraint(equalTo: footerBar.leadingAnchor, constant: 10),
clearChooseSeedPhraseButton.trailingAnchor.constraint(equalTo: footerBar.trailingAnchor, constant: -10),
clearChooseSeedPhraseButton.bottomAnchor.constraint(equalTo: footerBar.topAnchor, constant: -20),
buttonsBar.leadingAnchor.constraint(equalTo: footerBar.leadingAnchor),
buttonsBar.trailingAnchor.constraint(equalTo: footerBar.trailingAnchor),
buttonsBar.topAnchor.constraint(equalTo: footerBar.topAnchor),
buttonsBar.heightAnchor.constraint(equalToConstant: ButtonsBar.buttonsHeight),
footerBar.leadingAnchor.constraint(equalTo: view.leadingAnchor),
footerBar.trailingAnchor.constraint(equalTo: view.trailingAnchor),
footerBar.topAnchor.constraint(equalTo: view.layoutGuide.bottomAnchor, constant: -ButtonsBar.buttonsHeight - ButtonsBar.marginAtBottomScreen),
footerBar.bottomAnchor.constraint(equalTo: view.bottomAnchor),
] + roundedBackground.createConstraintsWithContainer(view: view))
NotificationCenter.default.addObserver(self, selector: #selector(appWillResignsActive), name: UIApplication.willResignActiveNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(appDidBecomeActive), name: UIApplication.didBecomeActiveNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(didTakeScreenShot), name: UIApplication.userDidTakeScreenshotNotification, object: nil)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
if isMovingFromParent || isBeingDismissed {
return
}
removeSeedPhraseFromDisplay()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
showSeedPhrases()
}
@objc private func appDidBecomeActive() {
showSeedPhrases()
}
@objc private func didTakeScreenShot() {
displaySuccess(message: R.string.localizable.walletsVerifySeedPhraseDoNotTakeScreenshotDescription())
}
@objc private func appWillResignsActive() {
if isInactiveBecauseWeAccessingBiometrics {
isInactiveBecauseWeAccessingBiometrics = false
return
}
removeSeedPhraseFromDisplay()
}
private func showSeedPhrases() {
guard isTopViewController else { return }
guard notDisplayingSeedPhrase else { return }
isInactiveBecauseWeAccessingBiometrics = true
keystore.exportSeedPhraseOfHdWallet(forAccount: account, reason: .prepareForVerification) { result in
switch result {
case .success(let words):
self.state = .editingSeedPhrase(words: words.split(separator: " ").map { String($0) }.shuffled())
case .failure(let error):
self.state = .errorDisplaySeedPhrase(error)
}
}
}
func configure() {
view.backgroundColor = Colors.appBackground
title = viewModel.title
seedPhraseTextView.keyboardType = .alphabet
seedPhraseTextView.returnKeyType = .done
seedPhraseTextView.autocapitalizationType = .none
seedPhraseTextView.autocorrectionType = .no
seedPhraseTextView.enablesReturnKeyAutomatically = true
seedPhraseTextView.borderColor = viewModel.seedPhraseTextViewBorderNormalColor
seedPhraseTextView.borderWidth = viewModel.seedPhraseTextViewBorderWidth
seedPhraseTextView.cornerRadius = viewModel.seedPhraseTextViewBorderCornerRadius
seedPhraseTextView.font = viewModel.seedPhraseTextViewFont
seedPhraseTextView.contentInset = viewModel.seedPhraseTextViewContentInset
errorLabel.textColor = viewModel.noErrorColor
errorLabel.text = viewModel.noErrorText
errorLabel.font = viewModel.errorFont
errorLabel.numberOfLines = 0
seedPhraseCollectionView.configure()
clearChooseSeedPhraseButton.addTarget(self, action: #selector(clearChosenSeedPhrases), for: .touchUpInside)
clearChooseSeedPhraseButton.setTitle(R.string.localizable.clearButtonTitle(), for: .normal)
clearChooseSeedPhraseButton.titleLabel?.font = viewModel.importKeystoreJsonButtonFont
clearChooseSeedPhraseButton.titleLabel?.adjustsFontSizeToFitWidth = true
buttonsBar.configure()
let continueButton = buttonsBar.buttons[0]
continueButton.setTitle(R.string.localizable.walletsVerifySeedPhraseTitle(), for: .normal)
continueButton.addTarget(self, action: #selector(verify), for: .touchUpInside)
}
@objc func clearChosenSeedPhrases() {
seedPhraseTextView.text = ""
seedPhraseCollectionView.viewModel.clearSelectedWords()
clearChooseSeedPhraseButton.isHidden = true
state = .editingSeedPhrase(words: state.words)
}
@objc func verify() {
isInactiveBecauseWeAccessingBiometrics = true
keystore.verifySeedPhraseOfHdWallet(seedPhraseTextView.text.lowercased().trimmed, forAccount: account) { result in
switch result {
case .success(let isMatched):
//Safety precaution, we clear the seed phrase. The next screen may be the prompt to elevate security of wallet screen which the user can go back from
self.clearChosenSeedPhrases()
self.updateStateWithVerificationResult(isMatched)
case .failure(let error):
self.reflectError(error)
}
}
}
private func updateStateWithVerificationResult(_ isMatched: Bool) {
if isMatched {
state = .seedPhraseMatched(words: state.words)
} else {
state = .seedPhraseNotMatched(words: state.words)
}
}
private func reflectError(_ error: KeystoreError) {
state = .keystoreError(error)
}
private func removeSeedPhraseFromDisplay() {
state = .notDisplayedSeedPhrase
}
private func clearError() {
errorLabel.text = viewModel.noErrorText
errorLabel.textColor = viewModel.noErrorColor
seedPhraseTextView.borderColor = viewModel.seedPhraseTextViewBorderNormalColor
}
}
extension VerifySeedPhraseViewController: UITextViewDelegate {
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
if text == "\n" {
verify()
seedPhraseTextView.resignFirstResponder()
return false
} else {
state = .editingSeedPhrase(words: state.words)
return true
}
}
}
extension VerifySeedPhraseViewController: SeedPhraseCollectionViewDelegate {
func didTap(word: String, atIndex index: Int, inCollectionView: SeedPhraseCollectionView) {
if seedPhraseTextView.text.isEmpty {
seedPhraseTextView.text += word
} else {
seedPhraseTextView.text += " \(word)"
}
clearChooseSeedPhraseButton.isHidden = false
clearError()
}
}

@ -0,0 +1,58 @@
// Copyright © 2019 Stormbird PTE. LTD.
import Foundation
import UIKit
struct CreateInitialViewModel {
var backgroundColor: UIColor {
return Colors.appWhite
}
var subtitle: String {
return R.string.localizable.gettingStartedSubtitle()
}
var subtitleColor: UIColor {
return Colors.appText
}
var subtitleFont: UIFont {
if ScreenChecker().isNarrowScreen {
return Fonts.regular(size: 20)!
} else {
return Fonts.regular(size: 30)!
}
}
var imageViewImage: UIImage {
return R.image.launch_icon()!
}
var createButtonTitle: String {
return R.string.localizable.gettingStartedNewWallet()
}
var watchButtonTitle: String {
return R.string.localizable.watch()
}
var importButtonTitle: String {
return R.string.localizable.importWalletImportButtonTitle()
}
var alreadyHaveWalletText: String {
return R.string.localizable.gettingStartedAlreadyHaveWallet()
}
var alreadyHaveWalletTextColor: UIColor {
return Colors.appText
}
var alreadyHaveWalletTextFont: UIFont {
return Fonts.regular(size: 18)!
}
var separatorColor: UIColor {
return .init(red: 235, green: 235, blue: 235)
}
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save