diff --git a/Adamant.xcodeproj/project.pbxproj b/Adamant.xcodeproj/project.pbxproj index 2db2c292e..5f87983f9 100644 --- a/Adamant.xcodeproj/project.pbxproj +++ b/Adamant.xcodeproj/project.pbxproj @@ -38,6 +38,13 @@ 645FEB35213E72C100D6BA2D /* OnboardViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 645FEB33213E72C100D6BA2D /* OnboardViewController.xib */; }; 648BCA6B213D37A900875EB5 /* AdamantAvatarService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 648BCA6A213D37A800875EB5 /* AdamantAvatarService.swift */; }; 648BCA6D213D384F00875EB5 /* AvatarService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 648BCA6C213D384F00875EB5 /* AvatarService.swift */; }; + 648DD79E2236A0B500B811FD /* DogeTransactionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 648DD79D2236A0B500B811FD /* DogeTransactionsViewController.swift */; }; + 648DD7A02236A59200B811FD /* DogeTransactionDetailsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 648DD79F2236A59200B811FD /* DogeTransactionDetailsViewController.swift */; }; + 648DD7A22237D9A000B811FD /* DogeTransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 648DD7A12237D9A000B811FD /* DogeTransaction.swift */; }; + 648DD7A42237DB9E00B811FD /* DogeWalletService+Send.swift in Sources */ = {isa = PBXBuildFile; fileRef = 648DD7A32237DB9E00B811FD /* DogeWalletService+Send.swift */; }; + 648DD7A62237DC4000B811FD /* DogeTransferViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 648DD7A52237DC4000B811FD /* DogeTransferViewController.swift */; }; + 648DD7A82239147800B811FD /* DogeWalletService+RichMessageProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 648DD7A72239147800B811FD /* DogeWalletService+RichMessageProvider.swift */; }; + 648DD7AA2239150E00B811FD /* DogeWalletService+RichMessageProviderWithStatusCheck.swift in Sources */ = {isa = PBXBuildFile; fileRef = 648DD7A92239150E00B811FD /* DogeWalletService+RichMessageProviderWithStatusCheck.swift */; }; 649D6BE821B95DB7009E727B /* LskWalletService+RichMessageProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649D6BE721B95DB7009E727B /* LskWalletService+RichMessageProvider.swift */; }; 649D6BEA21B9627B009E727B /* LskWalletService+RichMessageProviderWithStatusCheck.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649D6BE921B9627B009E727B /* LskWalletService+RichMessageProviderWithStatusCheck.swift */; }; 649D6BEC21BD5A53009E727B /* UISuffixTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649D6BEB21BD5A53009E727B /* UISuffixTextField.swift */; }; @@ -50,6 +57,10 @@ 64BD2B7520E2814B00E2CD36 /* EthTransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64BD2B7420E2814B00E2CD36 /* EthTransaction.swift */; }; 64BD2B7720E2820300E2CD36 /* TransactionDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64BD2B7620E2820300E2CD36 /* TransactionDetails.swift */; }; 64D059FF20D3116B003AD655 /* NodesListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D059FE20D3116A003AD655 /* NodesListViewController.swift */; }; + 64E1C82D222E95E2006C4DA7 /* DogeWalletRoutes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64E1C82C222E95E2006C4DA7 /* DogeWalletRoutes.swift */; }; + 64E1C82F222E95F6006C4DA7 /* DogeWallet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64E1C82E222E95F6006C4DA7 /* DogeWallet.swift */; }; + 64E1C831222E9617006C4DA7 /* DogeWalletService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64E1C830222E9617006C4DA7 /* DogeWalletService.swift */; }; + 64E1C833222EA0F0006C4DA7 /* DogeWalletViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64E1C832222EA0F0006C4DA7 /* DogeWalletViewController.swift */; }; 64E8305020F5FEEF006FA590 /* VotesAsset.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64E8304F20F5FEEF006FA590 /* VotesAsset.swift */; }; 64EE46B220FE0C8D00194DDA /* LskTransactionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64EE46B120FE0C8D00194DDA /* LskTransactionsViewController.swift */; }; 64F085D920E2D7600006DE68 /* AdmTransactionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64F085D820E2D7600006DE68 /* AdmTransactionsViewController.swift */; }; @@ -63,6 +74,7 @@ E905D39D204C13B900DDB504 /* SecuredStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = E905D39C204C13B900DDB504 /* SecuredStore.swift */; }; E905D39F204C281400DDB504 /* LoginViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E905D39E204C281400DDB504 /* LoginViewController.swift */; }; E9061B97207501E40011F104 /* AdamantUserInfoKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9061B96207501E40011F104 /* AdamantUserInfoKey.swift */; }; + E907350E2256779C00BF02CC /* DogeMainnet.swift in Sources */ = {isa = PBXBuildFile; fileRef = E907350D2256779C00BF02CC /* DogeMainnet.swift */; }; E908471B2196FE590095825D /* Adamant.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = E90847192196FE590095825D /* Adamant.xcdatamodeld */; }; E908472A2196FEA80095825D /* RichMessageTransaction+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = E908471C2196FEA80095825D /* RichMessageTransaction+CoreDataClass.swift */; }; E908472B2196FEA80095825D /* RichMessageTransaction+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = E908471D2196FEA80095825D /* RichMessageTransaction+CoreDataProperties.swift */; }; @@ -124,6 +136,7 @@ E926E034213EC454005E536B /* FullscreenAlertView.xib in Resources */ = {isa = PBXBuildFile; fileRef = E926E033213EC454005E536B /* FullscreenAlertView.xib */; }; E927171E20C04614002BB9A6 /* UIColor+hex.swift in Sources */ = {isa = PBXBuildFile; fileRef = E927171D20C04613002BB9A6 /* UIColor+hex.swift */; }; E9332B8921F1FA4400D56E72 /* OnboardRoutes.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9332B8821F1FA4400D56E72 /* OnboardRoutes.swift */; }; + E933475B225539390083839E /* DogeGetTransactionsResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = E933475A225539390083839E /* DogeGetTransactionsResponse.swift */; }; E9393FA82055C92700EE6F30 /* Decimal+adamant.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9393FA72055C92700EE6F30 /* Decimal+adamant.swift */; }; E9393FAA2055D03300EE6F30 /* AdamantMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9393FA92055D03300EE6F30 /* AdamantMessage.swift */; }; E93B0D742028B21400126346 /* ChatsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = E93B0D732028B21400126346 /* ChatsProvider.swift */; }; @@ -152,7 +165,6 @@ E94883E7203F07CD00F6E1B0 /* PassphraseValidation.swift in Sources */ = {isa = PBXBuildFile; fileRef = E94883E6203F07CD00F6E1B0 /* PassphraseValidation.swift */; }; E948E03B20235E2300975D6B /* SettingsRoutes.swift in Sources */ = {isa = PBXBuildFile; fileRef = E948E03A20235E2300975D6B /* SettingsRoutes.swift */; }; E948E0482024F02700975D6B /* VersionFooter.xib in Resources */ = {isa = PBXBuildFile; fileRef = E948E0472024F02700975D6B /* VersionFooter.xib */; }; - E948E04C2027679300975D6B /* AdamantFormattingTools.swift in Sources */ = {isa = PBXBuildFile; fileRef = E948E04B2027679300975D6B /* AdamantFormattingTools.swift */; }; E94E7B01205D3F090042B639 /* ChatListViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = E94E7B00205D3F090042B639 /* ChatListViewController.xib */; }; E94E7B08205D4CB80042B639 /* SharedRoutes.swift in Sources */ = {isa = PBXBuildFile; fileRef = E94E7B07205D4CB80042B639 /* SharedRoutes.swift */; }; E94E7B0C205D5E4A0042B639 /* TransactionsListViewControllerBase.xib in Resources */ = {isa = PBXBuildFile; fileRef = E94E7B0B205D5E4A0042B639 /* TransactionsListViewControllerBase.xib */; }; @@ -333,6 +345,13 @@ 645FEB33213E72C100D6BA2D /* OnboardViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = OnboardViewController.xib; sourceTree = ""; }; 648BCA6A213D37A800875EB5 /* AdamantAvatarService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AdamantAvatarService.swift; sourceTree = ""; }; 648BCA6C213D384F00875EB5 /* AvatarService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarService.swift; sourceTree = ""; }; + 648DD79D2236A0B500B811FD /* DogeTransactionsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DogeTransactionsViewController.swift; sourceTree = ""; }; + 648DD79F2236A59200B811FD /* DogeTransactionDetailsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DogeTransactionDetailsViewController.swift; sourceTree = ""; }; + 648DD7A12237D9A000B811FD /* DogeTransaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DogeTransaction.swift; sourceTree = ""; }; + 648DD7A32237DB9E00B811FD /* DogeWalletService+Send.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DogeWalletService+Send.swift"; sourceTree = ""; }; + 648DD7A52237DC4000B811FD /* DogeTransferViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DogeTransferViewController.swift; sourceTree = ""; }; + 648DD7A72239147800B811FD /* DogeWalletService+RichMessageProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DogeWalletService+RichMessageProvider.swift"; sourceTree = ""; }; + 648DD7A92239150E00B811FD /* DogeWalletService+RichMessageProviderWithStatusCheck.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DogeWalletService+RichMessageProviderWithStatusCheck.swift"; sourceTree = ""; }; 649D6BE721B95DB7009E727B /* LskWalletService+RichMessageProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LskWalletService+RichMessageProvider.swift"; sourceTree = ""; }; 649D6BE921B9627B009E727B /* LskWalletService+RichMessageProviderWithStatusCheck.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LskWalletService+RichMessageProviderWithStatusCheck.swift"; sourceTree = ""; }; 649D6BEB21BD5A53009E727B /* UISuffixTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UISuffixTextField.swift; sourceTree = ""; }; @@ -345,6 +364,10 @@ 64BD2B7420E2814B00E2CD36 /* EthTransaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EthTransaction.swift; sourceTree = ""; }; 64BD2B7620E2820300E2CD36 /* TransactionDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionDetails.swift; sourceTree = ""; }; 64D059FE20D3116A003AD655 /* NodesListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NodesListViewController.swift; sourceTree = ""; }; + 64E1C82C222E95E2006C4DA7 /* DogeWalletRoutes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DogeWalletRoutes.swift; sourceTree = ""; }; + 64E1C82E222E95F6006C4DA7 /* DogeWallet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DogeWallet.swift; sourceTree = ""; }; + 64E1C830222E9617006C4DA7 /* DogeWalletService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DogeWalletService.swift; sourceTree = ""; }; + 64E1C832222EA0F0006C4DA7 /* DogeWalletViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DogeWalletViewController.swift; sourceTree = ""; }; 64E8304F20F5FEEF006FA590 /* VotesAsset.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VotesAsset.swift; sourceTree = ""; }; 64EE46B120FE0C8D00194DDA /* LskTransactionsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LskTransactionsViewController.swift; sourceTree = ""; }; 64F085D820E2D7600006DE68 /* AdmTransactionsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdmTransactionsViewController.swift; sourceTree = ""; }; @@ -364,6 +387,7 @@ E905D39E204C281400DDB504 /* LoginViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginViewController.swift; sourceTree = ""; }; E9061B96207501E40011F104 /* AdamantUserInfoKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantUserInfoKey.swift; sourceTree = ""; }; E9061B982077AF8E0011F104 /* Adamant.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Adamant.entitlements; sourceTree = ""; }; + E907350D2256779C00BF02CC /* DogeMainnet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DogeMainnet.swift; sourceTree = ""; }; E908471A2196FE590095825D /* Adamant.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Adamant.xcdatamodel; sourceTree = ""; }; E908471C2196FEA80095825D /* RichMessageTransaction+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RichMessageTransaction+CoreDataClass.swift"; sourceTree = ""; }; E908471D2196FEA80095825D /* RichMessageTransaction+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RichMessageTransaction+CoreDataProperties.swift"; sourceTree = ""; }; @@ -440,6 +464,7 @@ E926E033213EC454005E536B /* FullscreenAlertView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = FullscreenAlertView.xib; sourceTree = ""; }; E927171D20C04613002BB9A6 /* UIColor+hex.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIColor+hex.swift"; sourceTree = ""; }; E9332B8821F1FA4400D56E72 /* OnboardRoutes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardRoutes.swift; sourceTree = ""; }; + E933475A225539390083839E /* DogeGetTransactionsResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DogeGetTransactionsResponse.swift; sourceTree = ""; }; E9393FA72055C92700EE6F30 /* Decimal+adamant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Decimal+adamant.swift"; sourceTree = ""; }; E9393FA92055D03300EE6F30 /* AdamantMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantMessage.swift; sourceTree = ""; }; E93B0D732028B21400126346 /* ChatsProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatsProvider.swift; sourceTree = ""; }; @@ -468,7 +493,6 @@ E94883E6203F07CD00F6E1B0 /* PassphraseValidation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PassphraseValidation.swift; sourceTree = ""; }; E948E03A20235E2300975D6B /* SettingsRoutes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsRoutes.swift; sourceTree = ""; }; E948E0472024F02700975D6B /* VersionFooter.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = VersionFooter.xib; sourceTree = ""; }; - E948E04B2027679300975D6B /* AdamantFormattingTools.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantFormattingTools.swift; sourceTree = ""; }; E94E7B00205D3F090042B639 /* ChatListViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ChatListViewController.xib; sourceTree = ""; }; E94E7B07205D4CB80042B639 /* SharedRoutes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedRoutes.swift; sourceTree = ""; }; E94E7B0B205D5E4A0042B639 /* TransactionsListViewControllerBase.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TransactionsListViewControllerBase.xib; sourceTree = ""; }; @@ -654,6 +678,24 @@ path = TokensApiService; sourceTree = ""; }; + 64E1C82B222E958C006C4DA7 /* Doge */ = { + isa = PBXGroup; + children = ( + E907350D2256779C00BF02CC /* DogeMainnet.swift */, + 64E1C82C222E95E2006C4DA7 /* DogeWalletRoutes.swift */, + 64E1C82E222E95F6006C4DA7 /* DogeWallet.swift */, + 64E1C830222E9617006C4DA7 /* DogeWalletService.swift */, + 648DD7A32237DB9E00B811FD /* DogeWalletService+Send.swift */, + 648DD7A72239147800B811FD /* DogeWalletService+RichMessageProvider.swift */, + 648DD7A92239150E00B811FD /* DogeWalletService+RichMessageProviderWithStatusCheck.swift */, + 64E1C832222EA0F0006C4DA7 /* DogeWalletViewController.swift */, + 648DD7A52237DC4000B811FD /* DogeTransferViewController.swift */, + 648DD79D2236A0B500B811FD /* DogeTransactionsViewController.swift */, + 648DD79F2236A59200B811FD /* DogeTransactionDetailsViewController.swift */, + ); + path = Doge; + sourceTree = ""; + }; E59396A8E0053F21F768E69B /* Pods */ = { isa = PBXGroup; children = ( @@ -778,6 +820,7 @@ E993302321369B8A00CD5200 /* RichMessage.swift */, E971591921681D6900A5F904 /* TransactionStatus.swift */, E9FCA1E5218334C00005E83D /* SimpleTransactionDetails.swift */, + 648DD7A12237D9A000B811FD /* DogeTransaction.swift */, ); path = Models; sourceTree = ""; @@ -858,6 +901,7 @@ E95F85792007F0260070534A /* ServerResponse.swift */, E9C51EEE20139DC600385EB7 /* TransactionIdResponse.swift */, E95F856E2007B61D0070534A /* GetPublicKeyResponse.swift */, + E933475A225539390083839E /* DogeGetTransactionsResponse.swift */, ); path = ServerResponses; sourceTree = ""; @@ -913,6 +957,7 @@ E94008902119D22400CD2D67 /* Adamant */, E94008792114ECF100CD2D67 /* Ethereum */, E94008812114EE3900CD2D67 /* Lisk */, + 64E1C82B222E958C006C4DA7 /* Doge */, E94008712114EACF00CD2D67 /* WalletAccount.swift */, E940086D2114AA2E00CD2D67 /* WalletService.swift */, E9B1AA582122D59600080A2A /* WalletsRoutes.swift */, @@ -987,7 +1032,6 @@ E950651F20404997008352E5 /* Utilities */ = { isa = PBXGroup; children = ( - E948E04B2027679300975D6B /* AdamantFormattingTools.swift */, E9942B83203CBFCE00C163AF /* AdamantQRTools.swift */, E9E7CDB62003994E00DFC4DB /* AdamantUtilities.swift */, E950652220404C84008352E5 /* AdamantUriTools.swift */, @@ -1380,6 +1424,7 @@ "${PODS_ROOT}/Target Support Files/Pods-Adamant/Pods-Adamant-frameworks.sh", "${BUILT_PRODUCTS_DIR}/Alamofire/Alamofire.framework", "${BUILT_PRODUCTS_DIR}/BigInt/BigInt.framework", + "${BUILT_PRODUCTS_DIR}/BitcoinKit/BitcoinKit.framework", "${BUILT_PRODUCTS_DIR}/ByteBackpacker/ByteBackpacker.framework", "${BUILT_PRODUCTS_DIR}/CryptoSwift/CryptoSwift.framework", "${BUILT_PRODUCTS_DIR}/DateToolsSwift/DateToolsSwift.framework", @@ -1387,6 +1432,9 @@ "${BUILT_PRODUCTS_DIR}/Eureka/Eureka.framework", "${BUILT_PRODUCTS_DIR}/FTIndicator/FTIndicator.framework", "${BUILT_PRODUCTS_DIR}/FreakingSimpleRoundImageView/FreakingSimpleRoundImageView.framework", + "${BUILT_PRODUCTS_DIR}/GRDB.swift/GRDB.framework", + "${BUILT_PRODUCTS_DIR}/GRDBCipher/GRDBCipher.framework", + "${PODS_ROOT}/GRKOpenSSLFramework/OpenSSL-iOS/bin/openssl.framework", "${BUILT_PRODUCTS_DIR}/KeychainAccess/KeychainAccess.framework", "${BUILT_PRODUCTS_DIR}/Lisk/Lisk.framework", "${BUILT_PRODUCTS_DIR}/MarkdownKit/MarkdownKit.framework", @@ -1401,6 +1449,7 @@ "${BUILT_PRODUCTS_DIR}/RNCryptor/RNCryptor.framework", "${BUILT_PRODUCTS_DIR}/ReachabilitySwift/Reachability.framework", "${BUILT_PRODUCTS_DIR}/Result/Result.framework", + "${BUILT_PRODUCTS_DIR}/SQLCipher/SQLCipher.framework", "${BUILT_PRODUCTS_DIR}/SipHash/SipHash.framework", "${BUILT_PRODUCTS_DIR}/SwiftRLP/SwiftRLP.framework", "${BUILT_PRODUCTS_DIR}/SwiftyOnboard/SwiftyOnboard.framework", @@ -1415,6 +1464,7 @@ outputPaths = ( "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Alamofire.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/BigInt.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/BitcoinKit.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ByteBackpacker.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/CryptoSwift.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DateToolsSwift.framework", @@ -1422,6 +1472,9 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Eureka.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FTIndicator.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FreakingSimpleRoundImageView.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GRDB.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GRDBCipher.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/openssl.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/KeychainAccess.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Lisk.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MarkdownKit.framework", @@ -1436,6 +1489,7 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RNCryptor.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Reachability.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Result.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SQLCipher.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SipHash.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwiftRLP.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwiftyOnboard.framework", @@ -1506,11 +1560,13 @@ files = ( E908472A2196FEA80095825D /* RichMessageTransaction+CoreDataClass.swift in Sources */, 64A223D620F760BB005157CB /* Localization.swift in Sources */, + 64E1C82F222E95F6006C4DA7 /* DogeWallet.swift in Sources */, E94E7B08205D4CB80042B639 /* SharedRoutes.swift in Sources */, E9256F5F2034C21100DE86E9 /* String+localized.swift in Sources */, E9960B3621F5154300C840A8 /* DummyAccount+CoreDataProperties.swift in Sources */, E9CAE8D22018AA7700345E76 /* AdamantApi+Accounts.swift in Sources */, E987024920C2B1F700E393F4 /* AdamantChatsProvider+fakeMessages.swift in Sources */, + 648DD7A42237DB9E00B811FD /* DogeWalletService+Send.swift in Sources */, E9942B84203CBFCE00C163AF /* AdamantQRTools.swift in Sources */, E908472F2196FEA80095825D /* BaseTransaction+CoreDataProperties.swift in Sources */, E94008872114F05B00CD2D67 /* AddressValidationResult.swift in Sources */, @@ -1535,6 +1591,8 @@ E9960B3321F5154300C840A8 /* BaseAccount+CoreDataClass.swift in Sources */, E9FCA1E6218334C00005E83D /* SimpleTransactionDetails.swift in Sources */, 64A223DA20F7A14B005157CB /* AdamantLskApiService.swift in Sources */, + E933475B225539390083839E /* DogeGetTransactionsResponse.swift in Sources */, + 648DD7A02236A59200B811FD /* DogeTransactionDetailsViewController.swift in Sources */, 6455E9F321075D8000B2E94C /* AdamantAddressBookService.swift in Sources */, E9204B5020C94C4A00F3B9AB /* Date+humanizedString.swift in Sources */, E9E7CD9120026FA100DFC4DB /* SwinjectDependencies.swift in Sources */, @@ -1542,6 +1600,7 @@ E908472B2196FEA80095825D /* RichMessageTransaction+CoreDataProperties.swift in Sources */, E9E7CD8B20026B0600DFC4DB /* AccountService.swift in Sources */, 64F085D920E2D7600006DE68 /* AdmTransactionsViewController.swift in Sources */, + 648DD7AA2239150E00B811FD /* DogeWalletService+RichMessageProviderWithStatusCheck.swift in Sources */, E9D1BE1C211DABE100E86B72 /* WalletPagingItem.swift in Sources */, E940086E2114AA2E00CD2D67 /* WalletService.swift in Sources */, 645FEB34213E72C100D6BA2D /* OnboardViewController.swift in Sources */, @@ -1554,6 +1613,7 @@ 6416B19F21AD7CBE006089AC /* LskWalletViewController.swift in Sources */, 64FA53CD20E1300B006783C9 /* EthTransactionsViewController.swift in Sources */, E9147B612050599000145913 /* LoginViewController+QR.swift in Sources */, + E907350E2256779C00BF02CC /* DogeMainnet.swift in Sources */, E9147B6F205088DE00145913 /* LoginViewController+Pinpad.swift in Sources */, E9FAE5E2203ED1AE008D3A6B /* ShareQrViewController.swift in Sources */, E983AE2120E655C500497E1A /* AccountHeaderView.swift in Sources */, @@ -1564,6 +1624,7 @@ E926E032213EC43B005E536B /* FullscreenAlertView.swift in Sources */, 644EC35B20EFB8E900F40C73 /* AdamantDelegateCell.swift in Sources */, 6416B1A521AEE157006089AC /* LskWalletService+Transfers.swift in Sources */, + 648DD7A62237DC4000B811FD /* DogeTransferViewController.swift in Sources */, E9960B3421F5154300C840A8 /* BaseAccount+CoreDataProperties.swift in Sources */, E90A494B204D9EB8009F6A65 /* AdamantAuthentication.swift in Sources */, E9215973206119FB0000CA5C /* ReachabilityMonitor.swift in Sources */, @@ -1594,6 +1655,7 @@ E941CCDE20E7B70200C96220 /* WalletCollectionViewCell.swift in Sources */, E9AA8BFA212C166600F9249F /* EthWalletService+Send.swift in Sources */, 644793C32166314A00FC4CF5 /* OnboardPage.swift in Sources */, + 64E1C831222E9617006C4DA7 /* DogeWalletService.swift in Sources */, E91947B22000246A001362F8 /* AdamantError.swift in Sources */, E95F85802008C8D70070534A /* ChatsRoutes.swift in Sources */, 6416B1A721B024B6006089AC /* LskWalletService+Send.swift in Sources */, @@ -1606,6 +1668,7 @@ E99818942120892F0018C84C /* WalletViewControllerBase.swift in Sources */, E9B3D39E201F99F40019EB36 /* DataProvider.swift in Sources */, 643ED0B12109F4BD005A9FDA /* NativeAdamantCore.swift in Sources */, + 648DD7A82239147800B811FD /* DogeWalletService+RichMessageProvider.swift in Sources */, E93EB0A320DA4CCA001F9601 /* Node.swift in Sources */, 645E7B062111DF3A006CC9FD /* Crypto.swift in Sources */, E9E7CDC02003AF6D00DFC4DB /* AdamantCellFactory.swift in Sources */, @@ -1617,9 +1680,11 @@ E9C51EEF20139DC600385EB7 /* TransactionIdResponse.swift in Sources */, E9722066201F42BB004F2AAD /* CoreDataStack.swift in Sources */, E913C8F21FFFA51D001A83F7 /* AppDelegate.swift in Sources */, + 648DD79E2236A0B500B811FD /* DogeTransactionsViewController.swift in Sources */, E905D39B2048A9BD00DDB504 /* KeychainStore.swift in Sources */, E9E7CDCA20040CC200DFC4DB /* Transaction.swift in Sources */, E9AA8BFC212C169200F9249F /* EthWalletService+Transfers.swift in Sources */, + 64E1C82D222E95E2006C4DA7 /* DogeWalletRoutes.swift in Sources */, 649E9A152111B3C200686B01 /* Mnemonic.swift in Sources */, E95F85692006AB9D0070534A /* NormalizedTransaction.swift in Sources */, E913C90D1FFFA99B001A83F7 /* Keypair.swift in Sources */, @@ -1710,9 +1775,9 @@ E94008892114F0F700CD2D67 /* AdmWalletService.swift in Sources */, 64EE46B220FE0C8D00194DDA /* LskTransactionsViewController.swift in Sources */, E94008832114EE4700CD2D67 /* LskWallet.swift in Sources */, + 64E1C833222EA0F0006C4DA7 /* DogeWalletViewController.swift in Sources */, E93EB09F20DA3FA4001F9601 /* NodesEditorRoutes.swift in Sources */, E98FC34220F9209900032D65 /* UIColor+adamant.swift in Sources */, - E948E04C2027679300975D6B /* AdamantFormattingTools.swift in Sources */, E9E7CDB12002B97B00DFC4DB /* AccountRoutes.swift in Sources */, E9240BF9215D813A00187B09 /* CustomCellDeleage.swift in Sources */, E9AA8BF82129F13000F9249F /* ComplexTransferViewController.swift in Sources */, @@ -1720,6 +1785,7 @@ E9147B5F20500E9300145913 /* MyLittlePinpad+adamant.swift in Sources */, E9981896212095CA0018C84C /* EthWalletViewController.swift in Sources */, E98FC34620F9210100032D65 /* Date+adamant.swift in Sources */, + 648DD7A22237D9A000B811FD /* DogeTransaction.swift in Sources */, E90847372196FEA80095825D /* Chatroom+CoreDataProperties.swift in Sources */, E905D39D204C13B900DDB504 /* SecuredStore.swift in Sources */, E98FC34820F921EA00032D65 /* DelegateVote.swift in Sources */, @@ -1901,7 +1967,7 @@ MTL_ENABLE_DEBUG_INFO = NO; PRODUCT_NAME = ADAMANT; SDKROOT = iphoneos; - SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_COMPILATION_MODE = wholemodule; SWIFT_VERSION = 4.2; VALIDATE_PRODUCT = YES; }; diff --git a/Adamant.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/Adamant.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 000000000..0c67376eb --- /dev/null +++ b/Adamant.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,5 @@ + + + + + diff --git a/Adamant/AppDelegate.swift b/Adamant/AppDelegate.swift index 57b575bd2..157b454b3 100644 --- a/Adamant/AppDelegate.swift +++ b/Adamant/AppDelegate.swift @@ -56,6 +56,10 @@ struct AdamantResources { static let lskServers = [ "https://lisknode1.adamant.im" ] + + static let dogeServers: [URL] = [ + URL(string: "https://dogenode1.adamant.im/api")! + ] // MARK: ADAMANT Addresses static let supportEmail = "ios@adamant.im" @@ -85,6 +89,8 @@ struct AdamantResources { static let liskExplorerAddress = "https://explorer.lisk.io/tx/" // static let liskExplorerAddress = "https://testnet-explorer.lisk.io/tx/" // LISK Testnet + static let dogeExplorerAddress = "https://dogechain.info/tx/" + private init() {} } diff --git a/Adamant/Assets/Assets.xcassets/Icons/wallet_lsk.imageset/wallet_lsk.png b/Adamant/Assets/Assets.xcassets/Icons/wallet_lsk.imageset/wallet_lsk.png deleted file mode 100644 index 908fda752..000000000 Binary files a/Adamant/Assets/Assets.xcassets/Icons/wallet_lsk.imageset/wallet_lsk.png and /dev/null differ diff --git a/Adamant/Assets/Assets.xcassets/Icons/wallet_lsk.imageset/wallet_lsk@2x.png b/Adamant/Assets/Assets.xcassets/Icons/wallet_lsk.imageset/wallet_lsk@2x.png deleted file mode 100644 index c6a0035e6..000000000 Binary files a/Adamant/Assets/Assets.xcassets/Icons/wallet_lsk.imageset/wallet_lsk@2x.png and /dev/null differ diff --git a/Adamant/Assets/Assets.xcassets/Icons/wallet_lsk.imageset/wallet_lsk@3x.png b/Adamant/Assets/Assets.xcassets/Icons/wallet_lsk.imageset/wallet_lsk@3x.png deleted file mode 100644 index 8044e1a49..000000000 Binary files a/Adamant/Assets/Assets.xcassets/Icons/wallet_lsk.imageset/wallet_lsk@3x.png and /dev/null differ diff --git a/Adamant/Assets/Assets.xcassets/Wallets/wallet_doge.imageset/Contents.json b/Adamant/Assets/Assets.xcassets/Wallets/wallet_doge.imageset/Contents.json new file mode 100644 index 000000000..03b0dc290 --- /dev/null +++ b/Adamant/Assets/Assets.xcassets/Wallets/wallet_doge.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "wallet_doge.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "wallet_doge@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "wallet_doge@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Adamant/Assets/Assets.xcassets/Wallets/wallet_doge.imageset/wallet_doge.png b/Adamant/Assets/Assets.xcassets/Wallets/wallet_doge.imageset/wallet_doge.png new file mode 100644 index 000000000..42fdb3255 Binary files /dev/null and b/Adamant/Assets/Assets.xcassets/Wallets/wallet_doge.imageset/wallet_doge.png differ diff --git a/Adamant/Assets/Assets.xcassets/Wallets/wallet_doge.imageset/wallet_doge@2x.png b/Adamant/Assets/Assets.xcassets/Wallets/wallet_doge.imageset/wallet_doge@2x.png new file mode 100644 index 000000000..1a3e1ce22 Binary files /dev/null and b/Adamant/Assets/Assets.xcassets/Wallets/wallet_doge.imageset/wallet_doge@2x.png differ diff --git a/Adamant/Assets/Assets.xcassets/Wallets/wallet_doge.imageset/wallet_doge@3x.png b/Adamant/Assets/Assets.xcassets/Wallets/wallet_doge.imageset/wallet_doge@3x.png new file mode 100644 index 000000000..606d8bdff Binary files /dev/null and b/Adamant/Assets/Assets.xcassets/Wallets/wallet_doge.imageset/wallet_doge@3x.png differ diff --git a/Adamant/Assets/Assets.xcassets/Wallets/wallet_eth.imageset/wallet_eth.png b/Adamant/Assets/Assets.xcassets/Wallets/wallet_eth.imageset/wallet_eth.png index abb0511b3..7c44276ab 100644 Binary files a/Adamant/Assets/Assets.xcassets/Wallets/wallet_eth.imageset/wallet_eth.png and b/Adamant/Assets/Assets.xcassets/Wallets/wallet_eth.imageset/wallet_eth.png differ diff --git a/Adamant/Assets/Assets.xcassets/Wallets/wallet_eth.imageset/wallet_eth@2x.png b/Adamant/Assets/Assets.xcassets/Wallets/wallet_eth.imageset/wallet_eth@2x.png index d7224b879..192000517 100644 Binary files a/Adamant/Assets/Assets.xcassets/Wallets/wallet_eth.imageset/wallet_eth@2x.png and b/Adamant/Assets/Assets.xcassets/Wallets/wallet_eth.imageset/wallet_eth@2x.png differ diff --git a/Adamant/Assets/Assets.xcassets/Wallets/wallet_eth.imageset/wallet_eth@3x.png b/Adamant/Assets/Assets.xcassets/Wallets/wallet_eth.imageset/wallet_eth@3x.png index 296c262e9..972971241 100644 Binary files a/Adamant/Assets/Assets.xcassets/Wallets/wallet_eth.imageset/wallet_eth@3x.png and b/Adamant/Assets/Assets.xcassets/Wallets/wallet_eth.imageset/wallet_eth@3x.png differ diff --git a/Adamant/Assets/Assets.xcassets/Icons/wallet_lsk.imageset/Contents.json b/Adamant/Assets/Assets.xcassets/Wallets/wallet_lsk.imageset/Contents.json similarity index 100% rename from Adamant/Assets/Assets.xcassets/Icons/wallet_lsk.imageset/Contents.json rename to Adamant/Assets/Assets.xcassets/Wallets/wallet_lsk.imageset/Contents.json diff --git a/Adamant/Assets/Assets.xcassets/Wallets/wallet_lsk.imageset/wallet_lsk.png b/Adamant/Assets/Assets.xcassets/Wallets/wallet_lsk.imageset/wallet_lsk.png new file mode 100644 index 000000000..157ca9011 Binary files /dev/null and b/Adamant/Assets/Assets.xcassets/Wallets/wallet_lsk.imageset/wallet_lsk.png differ diff --git a/Adamant/Assets/Assets.xcassets/Wallets/wallet_lsk.imageset/wallet_lsk@2x.png b/Adamant/Assets/Assets.xcassets/Wallets/wallet_lsk.imageset/wallet_lsk@2x.png new file mode 100644 index 000000000..cf2528f8e Binary files /dev/null and b/Adamant/Assets/Assets.xcassets/Wallets/wallet_lsk.imageset/wallet_lsk@2x.png differ diff --git a/Adamant/Assets/Assets.xcassets/Wallets/wallet_lsk.imageset/wallet_lsk@3x.png b/Adamant/Assets/Assets.xcassets/Wallets/wallet_lsk.imageset/wallet_lsk@3x.png new file mode 100644 index 000000000..f079d9577 Binary files /dev/null and b/Adamant/Assets/Assets.xcassets/Wallets/wallet_lsk.imageset/wallet_lsk@3x.png differ diff --git a/Adamant/Assets/l18n/de.lproj/Localizable.strings b/Adamant/Assets/l18n/de.lproj/Localizable.strings index 4d20ab34f..007332211 100755 --- a/Adamant/Assets/l18n/de.lproj/Localizable.strings +++ b/Adamant/Assets/l18n/de.lproj/Localizable.strings @@ -130,9 +130,12 @@ /* Account tab: 'Send ETH tokens' button */ "AccountTab.Row.SendEth" = "ETH senden"; -/* Account tab: 'Send ETH tokens' button */ +/* Account tab: 'Send LSK tokens' button */ "AccountTab.Row.SendLsk" = "LSK senden"; +/* Account tab: 'Send DOGE tokens' button */ +"AccountTab.Row.SendDoge" = "DOGE senden"; + /* Account tab: Anonymously buy ADM tokens */ "AccountTab.Row.AnonymouslyBuyADM" = "Kaufen Sie ADM anonym"; @@ -155,7 +158,7 @@ "AccountTab.Row.About" = "Über ADAMANT"; /* Account tab: 'Vote for delegates' button */ -"AccountTab.Row.VoteForDelegates" = "Delegate wählen"; +"AccountTab.Row.VoteForDelegates" = "Delegierte wählen"; /* Account tab: Actions section title */ "AccountTab.Section.Actions" = "Aktionen"; @@ -172,11 +175,14 @@ /* Account tab: Lisk wallet */ "AccountTab.Wallets.lisk_wallet" = "Lisk Wallet"; +/* Account tab: Doge wallet */ +"AccountTab.Wallets.doge_wallet" = "Doge Wallet"; + /* Account page: scene title */ "AccountTab.Title" = "Konto"; /* Account tab: Delegates section title */ -"AccountTab.Section.Delegates" = "Delegate"; +"AccountTab.Section.Delegates" = "Delegierte"; /* Product name */ "ADAMANT" = "ADAMANT"; @@ -284,7 +290,7 @@ "ChatScene.Warning.UnsupportedUrl" = "Nicht unterstütztes URL-Protokoll"; /* Delegates page: scene title */ -"Delegates.Title" = "Delegate"; +"Delegates.Title" = "Delegierte"; /* Delegates tab: Message about 50 ADM fee for vote */ "Delegates.NotEnoughtTokensForVote" = "Nicht genug Tokens für die Abstimmung. Sie brauchen mindestens 50 ADM auf Ihrem Konto"; diff --git a/Adamant/Assets/l18n/de.lproj/Localizable.stringsdict b/Adamant/Assets/l18n/de.lproj/Localizable.stringsdict index ea2e9c282..782d5918b 100644 --- a/Adamant/Assets/l18n/de.lproj/Localizable.stringsdict +++ b/Adamant/Assets/l18n/de.lproj/Localizable.stringsdict @@ -2,6 +2,38 @@ + Doge.TransactionDetails.SendersFormat + + NSStringLocalizedFormatKey + %#@senders@ + Variable + + NSStringFormatValueTypeKey + d + NSStringFormatSpecTypeKey + NSStringPluralRuleType + other + %d Absender + one + %d Absender + + + Doge.TransactionDetails.RecipientsFormat + + NSStringLocalizedFormatKey + %#@recipients@ + recipients + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + other + %d Empfänger + one + %d Empfänger + + NotificationsService.NewMessage.BodyFormat NSStringLocalizedFormatKey diff --git a/Adamant/Assets/l18n/en.lproj/Localizable.strings b/Adamant/Assets/l18n/en.lproj/Localizable.strings index e5c2b4657..8ba32a2c1 100755 --- a/Adamant/Assets/l18n/en.lproj/Localizable.strings +++ b/Adamant/Assets/l18n/en.lproj/Localizable.strings @@ -127,9 +127,12 @@ /* Account tab: 'Send ETH tokens' button */ "AccountTab.Row.SendEth" = "Send ETH"; -/* Account tab: 'Send ETH tokens' button */ +/* Account tab: 'Send LSK tokens' button */ "AccountTab.Row.SendLsk" = "Send LSK"; +/* Account tab: 'Send DOGE tokens' button */ +"AccountTab.Row.SendDoge" = "Send DOGE"; + /* Account tab: Anonymously buy ADM tokens */ "AccountTab.Row.AnonymouslyBuyADM" = "Buy ADM anonymously"; @@ -169,6 +172,9 @@ /* Account tab: Lisk wallet */ "AccountTab.Wallets.lisk_wallet" = "Lisk Wallet"; +/* Account tab: Doge wallet */ +"AccountTab.Wallets.doge_wallet" = "Doge Wallet"; + /* Account page: scene title */ "AccountTab.Title" = "Account"; diff --git a/Adamant/Assets/l18n/en.lproj/Localizable.stringsdict b/Adamant/Assets/l18n/en.lproj/Localizable.stringsdict index df7859949..3a198341c 100644 --- a/Adamant/Assets/l18n/en.lproj/Localizable.stringsdict +++ b/Adamant/Assets/l18n/en.lproj/Localizable.stringsdict @@ -2,6 +2,38 @@ + Doge.TransactionDetails.SendersFormat + + NSStringLocalizedFormatKey + %#@senders@ + senders + + NSStringFormatValueTypeKey + d + NSStringFormatSpecTypeKey + NSStringPluralRuleType + other + %d senders + one + %d sender + + + Doge.TransactionDetails.RecipientsFormat + + NSStringLocalizedFormatKey + %#@recipients@ + recipients + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + other + %d recipients + one + %d recipient + + NotificationsService.NewMessage.BodyFormat NSStringLocalizedFormatKey diff --git a/Adamant/Assets/l18n/ru.lproj/Localizable.strings b/Adamant/Assets/l18n/ru.lproj/Localizable.strings index 841489b57..2b36e789e 100644 --- a/Adamant/Assets/l18n/ru.lproj/Localizable.strings +++ b/Adamant/Assets/l18n/ru.lproj/Localizable.strings @@ -127,9 +127,12 @@ /* Account tab: 'Send ETH tokens' button */ "AccountTab.Row.SendEth" = "Отправить ETH"; -/* Account tab: 'Send ETH tokens' button */ +/* Account tab: 'Send LSK tokens' button */ "AccountTab.Row.SendLsk" = "Отправить LSK"; +/* Account tab: 'Send DOGE tokens' button */ +"AccountTab.Row.SendDoge" = "Отправить DOGE"; + /* Account tab: Anonymously buy ADM tokens */ "AccountTab.Row.AnonymouslyBuyADM" = "Купить ADM анонимно"; @@ -169,6 +172,9 @@ /* Account tab: Lisk wallet */ "AccountTab.Wallets.lisk_wallet" = "Кошелек Lisk"; +/* Account tab: Doge wallet */ +"AccountTab.Wallets.doge_wallet" = "Кошелек Doge"; + /* Account tab: Delegates section title */ "AccountTab.Section.Delegates" = "Делегаты"; diff --git a/Adamant/Assets/l18n/ru.lproj/Localizable.stringsdict b/Adamant/Assets/l18n/ru.lproj/Localizable.stringsdict index f9bf89c2a..908b1294f 100644 --- a/Adamant/Assets/l18n/ru.lproj/Localizable.stringsdict +++ b/Adamant/Assets/l18n/ru.lproj/Localizable.stringsdict @@ -2,6 +2,46 @@ + Doge.TransactionDetails.SendersFormat + + NSStringLocalizedFormatKey + %#@senders@ + senders + + NSStringFormatValueTypeKey + d + NSStringFormatSpecTypeKey + NSStringPluralRuleType + other + %d отправителя + one + %d отправитель + few + %d отправителя + many + %d отправителей + + + Doge.TransactionDetails.RecipientsFormat + + NSStringLocalizedFormatKey + %#@recipients@ + recipients + + NSStringFormatValueTypeKey + d + NSStringFormatSpecTypeKey + NSStringPluralRuleType + other + %d получателя + one + %d получатель + many + %d получателей + few + %d получателя + + NotificationsService.NewMessage.BodyFormat NSStringLocalizedFormatKey diff --git a/Adamant/CoreData/BaseTransaction+TransactionDetails.swift b/Adamant/CoreData/BaseTransaction+TransactionDetails.swift index e98659c88..a38564880 100644 --- a/Adamant/CoreData/BaseTransaction+TransactionDetails.swift +++ b/Adamant/CoreData/BaseTransaction+TransactionDetails.swift @@ -9,6 +9,8 @@ import Foundation extension BaseTransaction: TransactionDetails { + static var defaultCurrencySymbol: String? { return AdmWalletService.currencySymbol } + var txId: String { return transactionId ?? "" } var senderAddress: String { return senderId ?? "" } var recipientAddress: String { return recipientId ?? "" } diff --git a/Adamant/Helpers/Decimal+adamant.swift b/Adamant/Helpers/Decimal+adamant.swift index 061d38556..dd674373f 100644 --- a/Adamant/Helpers/Decimal+adamant.swift +++ b/Adamant/Helpers/Decimal+adamant.swift @@ -10,11 +10,11 @@ import Foundation extension Decimal { func shiftedFromAdamant() -> Decimal { - return Decimal(sign: self.isSignMinus ? .minus : .plus, exponent: AdamantUtilities.currencyExponent, significand: self) + return Decimal(sign: self.isSignMinus ? .minus : .plus, exponent: AdmWalletService.currencyExponent, significand: self) } func shiftedToAdamant() -> Decimal { - return Decimal(sign: self.isSignMinus ? .minus : .plus, exponent: -AdamantUtilities.currencyExponent, significand: self) + return Decimal(sign: self.isSignMinus ? .minus : .plus, exponent: -AdmWalletService.currencyExponent, significand: self) } var doubleValue: Double { diff --git a/Adamant/Info.plist b/Adamant/Info.plist index 3cfa6f44f..6137ab786 100644 --- a/Adamant/Info.plist +++ b/Adamant/Info.plist @@ -17,9 +17,9 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.4 + 1.5 CFBundleVersion - 69 + 75 LSRequiresIPhoneOS NSAppTransportSecurity diff --git a/Adamant/Models/DogeTransaction.swift b/Adamant/Models/DogeTransaction.swift new file mode 100644 index 000000000..cabe2a89f --- /dev/null +++ b/Adamant/Models/DogeTransaction.swift @@ -0,0 +1,361 @@ +// +// DogeTransaction.swift +// Adamant +// +// Created by Anton Boyarkin on 12/03/2019. +// Copyright © 2019 Adamant. All rights reserved. +// + +import Foundation + +extension String.adamantLocalized { + struct dogeTransaction { + static func recipients(_ recipients: Int) -> String { + return String.localizedStringWithFormat(NSLocalizedString("Doge.TransactionDetails.RecipientsFormat", comment: "DogeTransaction: amount of recipients, if more than one."), recipients) + } + + static func senders(_ senders: Int) -> String { + return String.localizedStringWithFormat(NSLocalizedString("Doge.TransactionDetails.SendersFormat", comment: "DogeTransaction: amount of senders, if more than one."), senders) + } + + private init() {} + } +} + +struct DogeTransaction: TransactionDetails { + static var defaultCurrencySymbol: String? { return DogeWalletService.currencySymbol } + + let txId: String + let dateValue: Date? + let blockValue: String? + + let senderAddress: String + let recipientAddress: String + + let amountValue: Decimal + let feeValue: Decimal? + let confirmationsValue: String? + + let isOutgoing: Bool + let transactionStatus: TransactionStatus? +} + + +// MARK: - Raw Doge Transaction, for easy parsing +struct DogeRawTransaction { + let txId: String + let date: Date? + + let valueIn: Decimal + let valueOut: Decimal + let fee: Decimal + + let confirmations: Int? + let blockHash: String? + + let inputs: [DogeInput] + let outputs: [DogeOutput] + + func asDogeTransaction(for address: String, blockId: String? = nil) -> DogeTransaction { + // MARK: Known values + let confirmationsValue: String? + let transactionStatus: TransactionStatus + + if let confirmations = confirmations { + confirmationsValue = String(confirmations) + transactionStatus = confirmations > 0 ? .success : .pending + } else { + confirmationsValue = nil + transactionStatus = .pending + } + + // Transfers + var myInputs = inputs.filter { $0.sender == address } + var myOutputs = outputs.filter { $0.addresses.contains(address) } + + var totalInputsValue = myInputs.map { $0.value }.reduce(0, +) - fee + var totalOutputsValue = myOutputs.map { $0.value }.reduce(0, +) + + if totalInputsValue == totalOutputsValue { + totalInputsValue = 0 + totalOutputsValue = 0 + } + + if totalInputsValue > totalOutputsValue { + while let out = myOutputs.first { + totalInputsValue -= out.value + totalOutputsValue -= out.value + + myOutputs.removeFirst() + } + } + + if totalInputsValue < totalOutputsValue { + while let i = myInputs.first { + totalInputsValue -= i.value + totalOutputsValue -= i.value + + myInputs.removeFirst() + } + } + + let senders = Set(inputs.map { $0.sender } ) + let recipients = Set(outputs.compactMap { $0.addresses.first } ) + + let sender: String + let recipient: String + + if senders.count == 1 { + sender = senders.first! + } else { + let filtered = senders.filter { $0 != address } + + if filtered.count == 1 { + sender = filtered.first! + } else { + sender = String.adamantLocalized.dogeTransaction.senders(senders.count) + } + } + + if recipients.count == 1 { + recipient = recipients.first! + } else { + let filtered = recipients.filter { $0 != address } + + if filtered.count == 1 { + recipient = filtered.first! + } else { + recipient = String.adamantLocalized.dogeTransaction.recipients(recipients.count) + } + } + + + // MARK: Inputs + if myInputs.count > 0 { + let inputTransaction = DogeTransaction(txId: txId, + dateValue: date, + blockValue: blockId, + senderAddress: address, + recipientAddress: recipient, + amountValue: totalInputsValue, + feeValue: fee, + confirmationsValue: confirmationsValue, + isOutgoing: true, + transactionStatus: transactionStatus) + + return inputTransaction + } + + // MARK: Outputs + let outputTransaction = DogeTransaction(txId: txId, + dateValue: date, + blockValue: blockId, + senderAddress: sender, + recipientAddress: address, + amountValue: totalOutputsValue, + feeValue: fee, + confirmationsValue: confirmationsValue, + isOutgoing: false, + transactionStatus: transactionStatus) + + return outputTransaction + } +} + +extension DogeRawTransaction: Decodable { + enum CodingKeys: String, CodingKey { + case txId = "txid" + case date = "time" + case valueIn + case valueOut + case fee = "fees" + case confirmations + case blockHash = "blockhash" + case inputs = "vin" + case outputs = "vout" + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + // MARK: Required + self.txId = try container.decode(String.self, forKey: .txId) + + // MARK: Optionals for new transactions + if let timeInterval = try? container.decode(TimeInterval.self, forKey: .date) { + self.date = Date(timeIntervalSince1970: timeInterval) + } else { + self.date = nil + } + + self.confirmations = try? container.decode(Int.self, forKey: .confirmations) + self.blockHash = try? container.decode(String.self, forKey: .blockHash) + + // MARK: Inputs & Outputs + let inputs = try container.decode([DogeInput].self, forKey: .inputs) + self.inputs = inputs.filter { !$0.sender.isEmpty } // Filter incomplete transactions without sender + self.outputs = try container.decode([DogeOutput].self, forKey: .outputs) + + // Total In & Out. Can be null sometimes... + if let raw = try? container.decode(Decimal.self, forKey: .valueIn) { + self.valueIn = raw + } else { + self.valueIn = self.inputs.map { $0.value }.reduce(0, +) + } + + if let raw = try? container.decode(Decimal.self, forKey: .valueOut) { + self.valueOut = raw + } else { + self.valueOut = self.outputs.map { $0.value }.reduce(0, +) + } + + if let raw = try? container.decode(Decimal.self, forKey: .fee) { + self.fee = raw + } else { + self.fee = self.valueIn - self.valueOut + } + } +} + + +// MARK: Doge internal +struct DogeInput: Decodable { + enum CodingKeys: String, CodingKey { + case sender = "addr" + case value = "valueSat" + case txId = "txid" + case vOut = "vout" + } + + let sender: String + let value: Decimal + let txId: String + let vOut: Int + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + // Incomplete inputs doesn't contains address. We will filter them out + if let raw = try? container.decode(String.self, forKey: .sender) { + self.sender = raw + } else { + self.sender = "" + } + + self.txId = try container.decode(String.self, forKey: .txId) + self.vOut = try container.decode(Int.self, forKey: .vOut) + + if let raw = try? container.decode(Decimal.self, forKey: .value) { + self.value = Decimal(sign: .plus, exponent: DogeWalletService.currencyExponent, significand: raw) + } else { + self.value = 0 + } + } +} + +struct DogeOutput: Decodable { + enum CodingKeys: String, CodingKey { + case signature = "scriptPubKey" + case value + case spentTxId + case spentIndex + } + + enum SignatureCodingKeys: String, CodingKey { + case addresses + } + + let addresses: [String] + let value: Decimal + let spentTxId: String? + let spentIndex: Int? + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + let signatureContainer = try container.nestedContainer(keyedBy: SignatureCodingKeys.self, forKey: .signature) + self.addresses = try signatureContainer.decode([String].self, forKey: .addresses) + + if let raw = try? container.decode(String.self, forKey: .value), let value = Decimal(string: raw) { + self.value = value + } else { + self.value = 0 + } + + self.spentTxId = try? container.decode(String.self, forKey: .spentTxId) + self.spentIndex = try? container.decode(Int.self, forKey: .spentIndex) + } +} + +// MARK: - Sample Json + +/* Doge Transaction + +{ + "txid": "5879c2257fdd0b44e2e66e3ffca4bb6ba77c8e5f6773f3c7d7162da9b3237b5a", + "version": 1, + "locktime": 0, + "vin": [], + "vout": [], + "blockhash": "49c0f690455804aa0c96cb8e08ede058ee853ef216958656315ed76e115b0fe4", + "confirmations": 99, + "time": 1554298220, + "blocktime": 1554298220, + "valueOut": 1855.6647, + "size": 226, + "valueIn": 1856.6647, + "fees": 1, + "firstSeenTs": 1554298214 +} + +new transaction: +{ + "txid": "60cd612335c9797ea67689b9cde4a41e20c20c1b96eb0731c59c5b0eab8bad31", + "version": 1, + "locktime": 0, + "vin": [], + "vout": [], + "valueOut": 283, + "size": 225, + "valueIn": 284, + "fees": 1 +} + +*/ + +/* Inputs + +{ + "txid": "3f4fa05bef67b1aacc0392fd5c3be3f94c991394166bc12ca73df28b63fe0aab", + "vout": 0, + "scriptSig": { + "asm": "0 3045022100d5b2470b6eb2f1933506f80bf5158526fc8262d2f29fd2c217f7deb8699fdd3d02205ae2d07e04849af40d252526418da9d0b1995f796463c9a2e73e2a3621a6d64901 3044022026f93ee27fe6fbd6ca4edd01a842881f96998af5012831e0003c5c8907ee31a902206d61ebeed160c4dae8853438d916c494867c57eaa45c6ba9351e4a212e26a4d801 522103ce2fb71cceec5c4e18ab8907ebd5c2a5dbbbed116088ae9f67f2067d3f698bb02103693c5397bade9b433e80bce0785457f9899a960ad70f159f09006e31e79f690c52ae", + "hex": "00483045022100d5b2470b6eb2f1933506f80bf5158526fc8262d2f29fd2c217f7deb8699fdd3d02205ae2d07e04849af40d252526418da9d0b1995f796463c9a2e73e2a3621a6d64901473044022026f93ee27fe6fbd6ca4edd01a842881f96998af5012831e0003c5c8907ee31a902206d61ebeed160c4dae8853438d916c494867c57eaa45c6ba9351e4a212e26a4d80147522103ce2fb71cceec5c4e18ab8907ebd5c2a5dbbbed116088ae9f67f2067d3f698bb02103693c5397bade9b433e80bce0785457f9899a960ad70f159f09006e31e79f690c52ae" + }, + "sequence": 4294967295, + "n": 0, + "addr": "A6qMXXr5WdroSeLRZVwRwbiPBVP8gBGS6W", + "valueSat": 99800000000, + "value": 998, + "doubleSpentTxID": null +} +*/ + +/* Outputs +{ + "value": "172436.00000000", + "n": 1, + "scriptPubKey": { + "asm": "OP_HASH160 9def6388804f6e46700059747c0218d4108a76f3 OP_EQUAL", + "hex": "a9149def6388804f6e46700059747c0218d4108a76f387", + "reqSigs": 1, + "type": "scripthash", + "addresses": [ + "A6qMXXr5WdroSeLRZVwRwbiPBVP8gBGS6W" + ] + }, + "spentTxId": "966342801119bdd5601823df2a98e9a0482e6b6cd3a69c84c0d8d7cb120caa4d", + "spentIndex": 2, + "spentTs": 1554229560 +} +*/ diff --git a/Adamant/Models/EthTransaction.swift b/Adamant/Models/EthTransaction.swift index 5b5e95c62..5dbf289e6 100644 --- a/Adamant/Models/EthTransaction.swift +++ b/Adamant/Models/EthTransaction.swift @@ -134,6 +134,8 @@ extension EthTransaction: Decodable { // MARK: - TransactionDetails extension EthTransaction: TransactionDetails { + static var defaultCurrencySymbol: String? { return EthWalletService.currencySymbol } + var txId: String { return hash } var senderAddress: String { return from } var recipientAddress: String { return to } diff --git a/Adamant/Models/SimpleTransactionDetails.swift b/Adamant/Models/SimpleTransactionDetails.swift index 9c46ddb07..e6c1546fe 100644 --- a/Adamant/Models/SimpleTransactionDetails.swift +++ b/Adamant/Models/SimpleTransactionDetails.swift @@ -9,6 +9,8 @@ import Foundation struct SimpleTransactionDetails: TransactionDetails { + static var defaultCurrencySymbol: String? { return nil } + var txId: String var senderAddress: String @@ -28,6 +30,4 @@ struct SimpleTransactionDetails: TransactionDetails { var isOutgoing: Bool var transactionStatus: TransactionStatus? - - } diff --git a/Adamant/ServerResponses/DogeGetTransactionsResponse.swift b/Adamant/ServerResponses/DogeGetTransactionsResponse.swift new file mode 100644 index 000000000..e3eae9c94 --- /dev/null +++ b/Adamant/ServerResponses/DogeGetTransactionsResponse.swift @@ -0,0 +1,28 @@ +// +// DogeGetTransactionsResponse.swift +// Adamant +// +// Created by Anokhov Pavel on 03/04/2019. +// Copyright © 2019 Adamant. All rights reserved. +// + +import Foundation + +class DogeGetTransactionsResponse: Decodable { + let totalItems: Int + let from: Int + let to: Int + + let items: [DogeRawTransaction] +} + +/* Json + +{ + "totalItems": 1, + "from": 0, + "to": 1, + "items": [] +} + +*/ diff --git a/Adamant/ServiceProtocols/LskApiService.swift b/Adamant/ServiceProtocols/LskApiService.swift index 3a94f7d33..e9d00f07f 100644 --- a/Adamant/ServiceProtocols/LskApiService.swift +++ b/Adamant/ServiceProtocols/LskApiService.swift @@ -26,9 +26,6 @@ protocol LskApiService: class { var account: LskAccount? { get } - // MARK: - Accounts - func newAccount(byPassphrase passphrase: String, completion: @escaping (ApiServiceResult) -> Void) - // MARK: - Transactions func createTransaction(toAddress address: String, amount: Double, completion: @escaping (ApiServiceResult) -> Void) func sendTransaction(transaction: LocalTransaction, completion: @escaping (ApiServiceResult) -> Void) diff --git a/Adamant/ServiceProtocols/RichMessageProvider.swift b/Adamant/ServiceProtocols/RichMessageProvider.swift index cde72b977..ab1f8fab4 100644 --- a/Adamant/ServiceProtocols/RichMessageProvider.swift +++ b/Adamant/ServiceProtocols/RichMessageProvider.swift @@ -15,6 +15,7 @@ enum CellSource { } protocol RichMessageProvider: class { + /// Lowercased!! static var richMessageType: String { get } var cellIdentifierSent: String { get } @@ -40,6 +41,6 @@ protocol RichMessageProviderWithStatusCheck: RichMessageProvider { extension RichMessageProviderWithStatusCheck { var delayBetweenChecks: TimeInterval { - return 10.0 + return 30.0 } } diff --git a/Adamant/Services/AdamantAccountService.swift b/Adamant/Services/AdamantAccountService.swift index eeed4dc82..4965d0ba5 100644 --- a/Adamant/Services/AdamantAccountService.swift +++ b/Adamant/Services/AdamantAccountService.swift @@ -88,7 +88,8 @@ class AdamantAccountService: AccountService { var wallets: [WalletService] = [ AdmWalletService(), EthWalletService(), - LskWalletService(mainnet: true, origins: AdamantResources.lskServers) + LskWalletService(mainnet: true, origins: AdamantResources.lskServers), + DogeWalletService() // Testnet // LskWalletService(mainnet: false) @@ -99,7 +100,11 @@ class AdamantAccountService: AccountService { fatalError("Failed to get EthWalletService") } - ethWallet.initiateNetwork(apiUrl: AdamantResources.ethServers.first!) { result in + guard let url = AdamantResources.ethServers.randomElement() else { + fatalError("Failed to get ETH endpoint") + } + + ethWallet.initiateNetwork(apiUrl: url) { result in switch result { case .success: break @@ -117,7 +122,7 @@ class AdamantAccountService: AccountService { break case .wifi, .cellular: - ethWallet.initiateNetwork(apiUrl: AdamantResources.ethServers.first!) { result in + ethWallet.initiateNetwork(apiUrl: url) { result in switch result { case .success: NotificationCenter.default.removeObserver(self, name: Notification.Name.AdamantReachabilityMonitor.reachabilityChanged, object: nil) diff --git a/Adamant/Services/DataProviders/AdamantChatsProvider.swift b/Adamant/Services/DataProviders/AdamantChatsProvider.swift index b07f3614a..53dfef62f 100644 --- a/Adamant/Services/DataProviders/AdamantChatsProvider.swift +++ b/Adamant/Services/DataProviders/AdamantChatsProvider.swift @@ -1002,9 +1002,13 @@ extension AdamantChatsProvider { if let data = decodedMessage.data(using: String.Encoding.utf8), let jsonRaw = try? JSONSerialization.jsonObject(with: data, options: []) { switch jsonRaw { // MARK: Valid json - case let json as [String:String]: + case var json as [String:String]: // Supported rich message type - if let type = json[RichContentKeys.type] { + if var type = json[RichContentKeys.type] { + // Fix lowercase + type = type.lowercased() + json[RichContentKeys.type] = type + let trs = RichMessageTransaction(entity: RichMessageTransaction.entity(), insertInto: context) trs.richContent = json trs.richType = type @@ -1022,7 +1026,10 @@ extension AdamantChatsProvider { // MARK: Bad json, try to fix it case let json as [String:Any]: // Supported type but in wrong format - if let type = json[RichContentKeys.type] as? String { + if var type = json[RichContentKeys.type] as? String { + // Fix lowercase + type = type.lowercased() + var fixedJson = [String:String]() for (key, raw) in json { @@ -1035,6 +1042,8 @@ extension AdamantChatsProvider { } } + fixedJson[RichContentKeys.type] = type // Lowercased + let trs = RichMessageTransaction(entity: RichMessageTransaction.entity(), insertInto: context) trs.richContent = fixedJson trs.richType = type diff --git a/Adamant/Services/DataProviders/AdamantTransfersProvider.swift b/Adamant/Services/DataProviders/AdamantTransfersProvider.swift index 73b2422e1..33bee1ba2 100644 --- a/Adamant/Services/DataProviders/AdamantTransfersProvider.swift +++ b/Adamant/Services/DataProviders/AdamantTransfersProvider.swift @@ -366,7 +366,9 @@ extension AdamantTransfersProvider { let context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) context.parent = stack.container.viewContext - guard let id = recipientAccount.chatroom?.objectID, let chatroom = context.object(with: id) as? Chatroom else { + guard let id = recipientAccount.chatroom?.objectID, + let chatroom = context.object(with: id) as? Chatroom, + let partner = context.object(with: recipientAccount.objectID) as? BaseAccount else { completion(.failure(.accountNotFound(address: recipient))) return } @@ -384,6 +386,7 @@ extension AdamantTransfersProvider { transaction.statusEnum = MessageStatus.pending transaction.comment = comment transaction.fee = transferFee as NSDecimalNumber + transaction.partner = partner chatroom.addToTransactions(transaction) diff --git a/Adamant/Services/TokensApiService/AdamantLskApiService.swift b/Adamant/Services/TokensApiService/AdamantLskApiService.swift index c608306b7..3e3a8f7c4 100644 --- a/Adamant/Services/TokensApiService/AdamantLskApiService.swift +++ b/Adamant/Services/TokensApiService/AdamantLskApiService.swift @@ -36,71 +36,6 @@ class AdamantLskApiService: LskApiService { } } - func newAccount(byPassphrase passphrase: String, completion: @escaping (ApiServiceResult) -> Void) { - /* - do { - let keys: KeyPair! = try Crypto.keyPair(fromPassphrase: passphrase) - let address: String! = Crypto.address(fromPublicKey: keys.publicKeyString) - let account = LskAccount(keys: keys, address: address, balance: BigUInt(0), balanceString: "0") - self.account = account -// print(address) - completion(.success(account)) - } catch { - print("\(error)") - completion(.failure(.accountNotFound)) - return - } - - NotificationCenter.default.post(name: Notification.Name.LskApiService.userLoggedIn, object: self) - - self.getBalance({ _ in }) - - if let account = self.account, let address = self.accountService.account?.address, let keypair = self.accountService.keypair { - self.getLskAddress(byAdamandAddress: address) { (result) in - switch result { - case .success(let value): - if value == nil { - guard let loggedAccount = self.accountService.account else { - DispatchQueue.main.async { - completion(.failure(.notLogged)) - } - return - } - - guard loggedAccount.balance >= AdamantApiService.KvsFee else { - DispatchQueue.main.async { - completion(.failure(.internalError(message: "LSK Wallet: Not enought ADM to save address to KVS", error: nil))) - } - return - } - - self.apiService.store(key: AdamantLskApiService.kvsAddress, value: account.address, type: StateType.keyValue, sender: address, keypair: keypair, completion: { (result) in - switch result { - case .success(let transactionId): - print("SAVED LSK in KVS: \(transactionId)") - break - case .failure(let error): - DispatchQueue.main.async { - completion(.failure(.internalError(message: "LSK Wallet: fail to save address to KVS", error: error))) - } - break - } - }) - } else { - print("FOUND LSK in KVS: \(value!)") - } - break - case .failure(let error): - DispatchQueue.main.async { - completion(.failure(.internalError(message: "LSK Wallet: fail to get address from KVS", error: error))) - } - break - } - } - } - */ - } - func createTransaction(toAddress address: String, amount: Double, completion: @escaping (ApiServiceResult) -> Void) { if let keys = self.account?.keys { do { @@ -206,32 +141,31 @@ class AdamantLskApiService: LskApiService { // MARK: - Tools func getBalance(_ completion: @escaping (ApiServiceResult) -> Void) { - if let address = self.account?.address { - accountApi.accounts(address: address) { (response) in - switch response { - case .success(response: let response): - if let account = response.data.first { - let balance = BigUInt(account.balance ?? "0") ?? BigUInt(0) - - self.account?.balance = balance - self.account?.balanceString = self.fromRawLsk(value: balance) - - if let balanceString = self.account?.balanceString, let balance = Double(balanceString) { - self.account?.balanceString = "\(balance)" - } - } + guard let address = self.account?.address else { + completion(.failure(.notLogged)) + return + } + + accountApi.accounts(address: address) { (response) in + switch response { + case .success(response: let response): + if let account = response.data.first { + let balance = BigUInt(account.balance ?? "0") ?? BigUInt(0) - completion(.success("\(self.account?.balanceString ?? "--") LSK")) + self.account?.balance = balance + self.account?.balanceString = self.fromRawLsk(value: balance) - break - case .error(response: let error): - print(error) - completion(.failure(.serverError(error: error.message))) - break + if let balanceString = self.account?.balanceString, let balance = Double(balanceString) { + self.account?.balanceString = "\(balance)" + } } + + completion(.success("\(self.account?.balanceString ?? "--") LSK")) + + case .error(response: let error): + print(error) + completion(.failure(.serverError(error: error.message))) } - } else { - completion(.failure(.internalError(message: "LSK Wallet: not found", error: nil))) } } diff --git a/Adamant/Stories/Chats/ChatViewController.swift b/Adamant/Stories/Chats/ChatViewController.swift index 0b0821641..4698240c8 100644 --- a/Adamant/Stories/Chats/ChatViewController.swift +++ b/Adamant/Stories/Chats/ChatViewController.swift @@ -545,7 +545,7 @@ extension ChatViewController { return } - let text = "~\(AdamantUtilities.format(balance: fee))" + let text = "~\(AdamantBalanceFormat.full.format(fee, withCurrencySymbol: AdmWalletService.currencySymbol))" prevFee = fee feeLabel.title = text @@ -743,6 +743,8 @@ private class StatusUpdateProcedure: Procedure { self.provider = provider self.controller = controller super.init() + + log.severity = .warning } override func execute() { diff --git a/Adamant/Utilities/AdamantFormattingTools.swift b/Adamant/Utilities/AdamantFormattingTools.swift deleted file mode 100644 index dd5da0d1c..000000000 --- a/Adamant/Utilities/AdamantFormattingTools.swift +++ /dev/null @@ -1,73 +0,0 @@ -// -// AdamantFormattingTools.swift -// Adamant -// -// Created by Anokhov Pavel on 04.02.2018. -// Copyright © 2018 Adamant. All rights reserved. -// - -import UIKit - -extension String.adamantLocalized.chat { - static let sent = NSLocalizedString("ChatScene.Sent", comment: "Chat: 'Sent funds' bubble title") - static let tapForDetails = NSLocalizedString("ChatScene.tapForDetails", comment: "Chat: 'Sent funds' buble 'Tap for details' tip") -} - -class AdamantFormattingTools { - static func summaryFor(transaction: Transaction, url: URL?) -> String { - return summaryFor(id: String(transaction.id), - sender: transaction.senderId, - recipient: transaction.recipientId, - date: transaction.date, - amount: transaction.amount, - fee: transaction.fee, - confirmations: String(transaction.confirmations), - blockId: transaction.blockId, - url: url) - } - - static func summaryFor(transaction: TransactionDetails, url: URL?) -> String { - return summaryFor(id: transaction.txId, - sender: transaction.senderAddress, - recipient: transaction.recipientAddress, - date: transaction.dateValue, - amount: transaction.amountValue, - fee: transaction.feeValue ?? 0, - confirmations: transaction.confirmationsValue, - blockId: transaction.blockValue, - url: url) - } - - private static func summaryFor(id: String, sender: String, recipient: String, date: Date?, amount: Decimal, fee: Decimal, confirmations: String?, blockId: String?, url: URL?) -> String { - - var summary = """ -Transaction #\(id) - -Summary -Sender: \(sender) -Recipient: \(recipient) -Amount: \(AdamantUtilities.format(balance: amount)) -Fee: \(AdamantUtilities.format(balance: fee)) -""" - - if let date = date { - summary = summary + "Date: \(DateFormatter.localizedString(from: date, dateStyle: .short, timeStyle: .medium))" - } - - if let confirmations = confirmations { - summary = summary + "Confirmations: \(confirmations)" - } - - if let blockId = blockId { - summary = summary + "\nBlock: \(blockId)" - } - - if let url = url { - summary = summary + "\nURL: \(url)" - } - - return summary - } - - private init() {} -} diff --git a/Adamant/Utilities/AdamantUtilities.swift b/Adamant/Utilities/AdamantUtilities.swift index 3cfa6094f..c1343c906 100644 --- a/Adamant/Utilities/AdamantUtilities.swift +++ b/Adamant/Utilities/AdamantUtilities.swift @@ -37,45 +37,6 @@ class AdamantUtilities { } -// MARK: - Currency -extension AdamantUtilities { - static let currencyExponent: Int = -8 - static let currencyCode = "ADM" - - static var currencyFormatter: NumberFormatter = { - let formatter = NumberFormatter() - formatter.numberStyle = .decimal - formatter.roundingMode = .floor - formatter.positiveFormat = "#.######## \(currencyCode)" - return formatter - }() - - static func currencyFormatter(currencyCode: String) -> NumberFormatter { - let formatter = NumberFormatter() - formatter.numberStyle = .decimal - formatter.roundingMode = .floor - formatter.positiveFormat = "#.######## \(currencyCode)" - return formatter - } - - static func format(balance: Decimal) -> String { - return currencyFormatter.string(from: balance as NSNumber)! - } - - static func format(balance: NSDecimalNumber) -> String { - return currencyFormatter.string(from: balance as NSNumber)! - } - - static func validateAmount(amount: Decimal) -> Bool { - if amount < Decimal(sign: .plus, exponent: AdamantUtilities.currencyExponent, significand: 1) { - return false - } - - return true - } -} - - // MARK: - Validating Addresses and Passphrases extension AdamantUtilities { static let addressRegexString = "^U([0-9]{6,20})$" diff --git a/Adamant/Wallets/Adamant/AdmTransferViewController.swift b/Adamant/Wallets/Adamant/AdmTransferViewController.swift index 624259cd2..c0dd0f167 100644 --- a/Adamant/Wallets/Adamant/AdmTransferViewController.swift +++ b/Adamant/Wallets/Adamant/AdmTransferViewController.swift @@ -27,7 +27,11 @@ class AdmTransferViewController: TransferViewControllerBase { // MARK: Properties override var balanceFormatter: NumberFormatter { - return AdamantUtilities.currencyFormatter + if let service = service { + return AdamantBalanceFormat.currencyFormatter(for: .full, currencySymbol: type(of: service).currencySymbol) + } else { + return AdamantBalanceFormat.currencyFormatterFull + } } private var skipValueChange: Bool = false diff --git a/Adamant/Wallets/Adamant/AdmWalletService.swift b/Adamant/Wallets/Adamant/AdmWalletService.swift index d3abe5fcc..1810ee21e 100644 --- a/Adamant/Wallets/Adamant/AdmWalletService.swift +++ b/Adamant/Wallets/Adamant/AdmWalletService.swift @@ -17,8 +17,9 @@ class AdmWalletService: NSObject, WalletService { let addressRegex = try! NSRegularExpression(pattern: "^U([0-9]{6,20})$") let transactionFee: Decimal = 0.5 - static var currencySymbol = "ADM" - static var currencyLogo = #imageLiteral(resourceName: "wallet_adm") + static let currencySymbol = "ADM" + static let currencyLogo = #imageLiteral(resourceName: "wallet_adm") + static let currencyExponent: Int = -8 // MARK: - Dependencies weak var accountService: AccountService! diff --git a/Adamant/Wallets/Doge/DogeMainnet.swift b/Adamant/Wallets/Doge/DogeMainnet.swift new file mode 100644 index 000000000..fdae743aa --- /dev/null +++ b/Adamant/Wallets/Doge/DogeMainnet.swift @@ -0,0 +1,58 @@ +// +// DogeMainnet.swift +// Adamant +// +// Created by Anokhov Pavel on 04/04/2019. +// Copyright © 2019 Adamant. All rights reserved. +// + +import Foundation +import BitcoinKit + +class DogeMainnet: Network { + override var name: String { + return "livenet" + } + + override var alias: String { + return "mainnet" + } + + override var scheme: String { + return "dogecoin" + } + + override var magic: UInt32 { + return 0xc0c0c0c0 + } + + override var pubkeyhash: UInt8 { + return 0x1e + } + + override var privatekey: UInt8 { + return 0x9e + } + + override var scripthash: UInt8 { + return 0x16 + } + + override var xpubkey: UInt32 { + return 0x02facafd + } + + override var xprivkey: UInt32 { + return 0x02fac398 + } + + override var port: UInt32 { + return 22556 + } + + override var dnsSeeds: [String] { + return [ + "dogenode1.adamant.im" + ] + } +} diff --git a/Adamant/Wallets/Doge/DogeTransactionDetailsViewController.swift b/Adamant/Wallets/Doge/DogeTransactionDetailsViewController.swift new file mode 100644 index 000000000..94bf774af --- /dev/null +++ b/Adamant/Wallets/Doge/DogeTransactionDetailsViewController.swift @@ -0,0 +1,137 @@ +// +// DogeTransactionDetailsViewController.swift +// Adamant +// +// Created by Anton Boyarkin on 11/03/2019. +// Copyright © 2019 Adamant. All rights reserved. +// + +import UIKit +import Eureka + +class DogeTransactionDetailsViewController: TransactionDetailsViewControllerBase { + // MARK: - Dependencies + + weak var service: DogeWalletService? + + // MARK: - Properties + + private var cachedBlockInfo: (hash: String, height: String)? = nil + + private let autoupdateInterval: TimeInterval = 5.0 + weak var timer: Timer? + + private lazy var refreshControl: UIRefreshControl = { + let control = UIRefreshControl() + control.addTarget(self, action: #selector(refresh), for: UIControl.Event.valueChanged) + return control + }() + + // MARK: - Lifecycle + + override func viewDidLoad() { + currencySymbol = DogeWalletService.currencySymbol + + super.viewDidLoad() + if service != nil { tableView.refreshControl = refreshControl } + + updateTransaction() + + // MARK: Start update + if transaction != nil { + startUpdate() + } + } + + deinit { + stopUpdate() + } + + // MARK: - Overrides + + override func explorerUrl(for transaction: TransactionDetails) -> URL? { + let id = transaction.txId + + return URL(string: "\(AdamantResources.dogeExplorerAddress)\(id)") + } + + @objc func refresh() { + updateTransaction { [weak self] error in + DispatchQueue.main.async { + self?.refreshControl.endRefreshing() + + if let error = error { + self?.dialogService.showRichError(error: error) + } + } + } + } + + // MARK: Autoupdate + + func startUpdate() { + timer?.invalidate() + timer = Timer.scheduledTimer(withTimeInterval: autoupdateInterval, repeats: true) { [weak self] _ in + self?.updateTransaction() // Silent, without errors + } + } + + func stopUpdate() { + timer?.invalidate() + } + + // MARK: Updating methods + + func updateTransaction(completion: ((RichError?) -> Void)? = nil) { + guard let service = service, let address = service.wallet?.address, let id = transaction?.txId else { + completion?(nil) + return + } + + service.getTransaction(by: id) { [weak self] result in + switch result { + case .success(let trs): + if let blockInfo = self?.cachedBlockInfo, blockInfo.hash == trs.blockHash { + self?.transaction = trs.asDogeTransaction(for: address, blockId: blockInfo.height) + + DispatchQueue.main.async { [weak self] in + self?.tableView.reloadData() + } + + completion?(nil) + } else if let blockHash = trs.blockHash { + service.getBlockId(by: blockHash) { result in + let blockInfo: (hash: String, height: String)? + switch result { + case .success(let height): + blockInfo = (hash: blockHash, height: height) + + case .failure: + blockInfo = nil + } + + self?.transaction = trs.asDogeTransaction(for: address, blockId: blockInfo?.height) + self?.cachedBlockInfo = blockInfo + + DispatchQueue.main.async { [weak self] in + self?.tableView.reloadData() + } + + completion?(nil) + } + } else { + self?.transaction = trs.asDogeTransaction(for: address) + + DispatchQueue.main.async { [weak self] in + self?.tableView.reloadData() + } + + completion?(nil) + } + + case .failure(let error): + completion?(error) + } + } + } +} diff --git a/Adamant/Wallets/Doge/DogeTransactionsViewController.swift b/Adamant/Wallets/Doge/DogeTransactionsViewController.swift new file mode 100644 index 000000000..110785b0e --- /dev/null +++ b/Adamant/Wallets/Doge/DogeTransactionsViewController.swift @@ -0,0 +1,244 @@ +// +// DogeTransactionsViewController.swift +// Adamant +// +// Created by Anton Boyarkin on 11/03/2019. +// Copyright © 2019 Adamant. All rights reserved. +// + +import UIKit +import ProcedureKit + +class DogeTransactionsViewController: TransactionsListViewControllerBase { + + // MARK: - Dependencies + var walletService: DogeWalletService! + var dialogService: DialogService! + var router: Router! + + // MARK: - Properties + var transactions: [DogeTransaction] = [] + + private let limit = 200 // Limit autoload, as some wallets can have thousands of transactions. + private(set) var loadedTo: Int = 0 + private let procedureQueue = ProcedureQueue() + + override func viewDidLoad() { + super.viewDidLoad() + currencySymbol = DogeWalletService.currencySymbol + + refreshControl.beginRefreshing() + handleRefresh(refreshControl) + } + + deinit { + procedureQueue.cancelAllOperations() + } + + override func handleRefresh(_ refreshControl: UIRefreshControl) { + procedureQueue.cancelAllOperations() + + loadedTo = 0 + walletService.getTransactions(from: loadedTo) { [weak self] result in + guard let vc = self else { + refreshControl.endRefreshing() + return + } + + switch result { + case .success(let tuple): + vc.transactions = tuple.transactions + vc.loadedTo = tuple.transactions.count + + DispatchQueue.main.async { + vc.tableView.reloadData() + refreshControl.endRefreshing() + + // Update tableView, then call loadMore() + if tuple.hasMore { + vc.loadMoreTransactions(from: tuple.transactions.count) + } + } + + case .failure(let error): + vc.transactions.removeAll() + vc.dialogService.showRichError(error: error) + + DispatchQueue.main.async { + vc.tableView.reloadData() + refreshControl.endRefreshing() + } + } + } + } + + + // MARK: - UITableView + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return transactions.count + } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + guard let controller = router.get(scene: AdamantScene.Wallets.Doge.transactionDetails) as? DogeTransactionDetailsViewController else { + fatalError("Failed to get DogeTransactionDetailsViewController") + } + + // Hold reference + guard let sender = walletService.wallet?.address else { + return + } + + controller.service = self.walletService + dialogService.showProgress(withMessage: nil, userInteractionEnable: false) + let txId = transactions[indexPath.row].txId + + walletService.getTransaction(by: txId) { [weak self] result in + guard let vc = self else { + return + } + + switch result { + case .success(let dogeTransaction): + let transaction = dogeTransaction.asDogeTransaction(for: sender) + + // Sender name + if transaction.senderAddress == sender { + controller.senderName = String.adamantLocalized.transactionDetails.yourAddress + } + + if transaction.recipientAddress == sender { + controller.recipientName = String.adamantLocalized.transactionDetails.yourAddress + } + + // Block Id + guard let blockHash = dogeTransaction.blockHash else { + controller.transaction = transaction + DispatchQueue.main.async { + vc.navigationController?.pushViewController(controller, animated: true) + vc.tableView.deselectRow(at: indexPath, animated: true) + vc.dialogService.dismissProgress() + } + break + } + + vc.walletService.getBlockId(by: blockHash) { result in + switch result { + case .success(let id): + controller.transaction = dogeTransaction.asDogeTransaction(for: sender, blockId: id) + + case .failure: + controller.transaction = transaction + } + + DispatchQueue.main.async { + vc.tableView.deselectRow(at: indexPath, animated: true) + vc.dialogService.dismissProgress() + vc.navigationController?.pushViewController(controller, animated: true) + } + } + + case .failure(let error): + DispatchQueue.main.async { + vc.tableView.deselectRow(at: indexPath, animated: true) + vc.dialogService.dismissProgress() + vc.dialogService.showRichError(error: error) + } + } + } + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + guard let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifierCompact, for: indexPath) as? TransactionTableViewCell else { + // TODO: Display & Log error + return UITableViewCell(style: .default, reuseIdentifier: "cell") + } + + let transaction = transactions[indexPath.row] + + cell.accessoryType = .disclosureIndicator + + configureCell(cell, for: transaction) + return cell + } + + func configureCell(_ cell: TransactionTableViewCell, for transaction: DogeTransaction) { + let outgoing = transaction.isOutgoing + let partnerId = outgoing ? transaction.recipientAddress : transaction.senderAddress + + let partnerName: String? + if let address = walletService.wallet?.address, partnerId == address { + partnerName = String.adamantLocalized.transactionDetails.yourAddress + } else { + partnerName = nil + } + + configureCell(cell, + isOutgoing: outgoing, + partnerId: partnerId, + partnerName: partnerName, + amount: transaction.amountValue, + date: transaction.dateValue) + } + + // MARK: - Load more + private func loadMoreTransactions(from: Int) { + let procedure = LoadMoreDogeTransactionsProcedure(service: walletService, from: from) + + procedure.addDidFinishBlockObserver { [weak self] (procedure, error) in + guard let vc = self, let result = procedure.result else { + return + } + + let total = vc.loadedTo + result.transactions.count + + var indexPaths = [IndexPath]() + for index in from.. 0 { + detailsVc.comment = comments + } + + vc.delegate?.transferViewController(vc, didFinishWithTransfer: transaction, detailsViewController: detailsVc) + + case .failure(let error): + guard case let .internalError(message, _) = error, message == "No transaction" else { + vc.dialogService.showRichError(error: error) + vc.delegate?.transferViewController(vc, didFinishWithTransfer: nil, detailsViewController: nil) + break + } + + vc.dialogService.showSuccess(withMessage: String.adamantLocalized.transfer.transferSuccess) + + guard let detailsVc = vc.router.get(scene: AdamantScene.Wallets.Doge.transactionDetails) as? DogeTransactionDetailsViewController else { + vc.delegate?.transferViewController(vc, didFinishWithTransfer: transaction, detailsViewController: nil) + break + } + + detailsVc.transaction = transaction + detailsVc.service = service + detailsVc.senderName = String.adamantLocalized.transactionDetails.yourAddress + detailsVc.recipientName = self?.recipientName + + if comments.count > 0 { + detailsVc.comment = comments + } + + vc.delegate?.transferViewController(vc, didFinishWithTransfer: transaction, detailsViewController: detailsVc) + } + } + + case .failure(let error): + vc.dialogService.showRichError(error: error) + } + } + + case .failure(let error): + dialogService.showRichError(error: error) + } + } + } + + + // MARK: Overrides + + private var _recipient: String? + + override var recipientAddress: String? { + set { + _recipient = newValue + + if let row: RowOf = form.rowBy(tag: BaseRows.address.tag) { + row.value = _recipient + row.updateCell() + } + } + get { + return _recipient + } + } + + override func validateRecipient(_ address: String) -> Bool { + guard let service = service else { + return false + } + + switch service.validate(address: address) { + case .valid: + return true + + case .invalid, .system: + return false + } + } + + override func recipientRow() -> BaseRow { + let row = TextRow() { + $0.tag = BaseRows.address.tag + $0.cell.textField.placeholder = String.adamantLocalized.newChat.addressPlaceholder + + if let recipient = recipientAddress { + $0.value = recipient + } + + if recipientIsReadonly { + $0.disabled = true + $0.cell.textField.isEnabled = false + } + }.onChange { [weak self] row in + if let text = row.value { + self?._recipient = text + } + + if let skip = self?.skipValueChange, skip { + self?.skipValueChange = false + return + } + + self?.validateForm() + } + + return row + } + + override func handleRawAddress(_ address: String) -> Bool { + guard let service = service else { + return false + } + + let parsedAddress: String + if address.hasPrefix("doge:"), let firstIndex = address.firstIndex(of: ":") { + let index = address.index(firstIndex, offsetBy: 1) + parsedAddress = String(address[index...]) + } else { + parsedAddress = address + } + + switch service.validate(address: parsedAddress) { + case .valid: + if let row: RowOf = form.rowBy(tag: BaseRows.address.tag) { + row.value = parsedAddress + row.updateCell() + } + + return true + + default: + return false + } + } + + func reportTransferTo(admAddress: String, amount: Decimal, comments: String, hash: String) { + let payload = RichMessageTransfer(type: DogeWalletService.richMessageType, amount: amount, hash: hash, comments: comments) + + let message = AdamantMessage.richMessage(payload: payload) + + chatsProvider.sendMessage(message, recipientId: admAddress) { [weak self] result in + if case .failure(let error) = result { + self?.dialogService.showRichError(error: error) + } + } + } + + override func defaultSceneTitle() -> String? { + return String.adamantLocalized.sendDoge + } +} diff --git a/Adamant/Wallets/Doge/DogeWallet.swift b/Adamant/Wallets/Doge/DogeWallet.swift new file mode 100644 index 000000000..8ca351d87 --- /dev/null +++ b/Adamant/Wallets/Doge/DogeWallet.swift @@ -0,0 +1,32 @@ +// +// DogeWallet.swift +// Adamant +// +// Created by Anton Boyarkin on 05/03/2019. +// Copyright © 2019 Adamant. All rights reserved. +// + +import Foundation +import BitcoinKit + +class DogeWallet: WalletAccount { + let address: String + let privateKey: PrivateKey + let publicKey: PublicKey + var balance: Decimal = 0.0 + var notifications: Int = 0 + + init(privateKey: PrivateKey) { + self.privateKey = privateKey + self.publicKey = privateKey.publicKey() + self.address = publicKey.toCashaddr().base58 + } + + init(address: String, privateKey: PrivateKey, balance: Decimal, notifications: Int) { + self.address = address + self.privateKey = privateKey + self.publicKey = privateKey.publicKey() + self.balance = balance + self.notifications = notifications + } +} diff --git a/Adamant/Wallets/Doge/DogeWalletRoutes.swift b/Adamant/Wallets/Doge/DogeWalletRoutes.swift new file mode 100644 index 000000000..75b621e39 --- /dev/null +++ b/Adamant/Wallets/Doge/DogeWalletRoutes.swift @@ -0,0 +1,46 @@ +// +// DogeWalletRoutes.swift +// Adamant +// +// Created by Anton Boyarkin on 05/03/2019. +// Copyright © 2019 Adamant. All rights reserved. +// + +import Foundation + +extension AdamantScene.Wallets { + struct Doge { + /// Wallet preview + static let wallet = AdamantScene(identifier: "DogeWalletViewController") { r in + let c = DogeWalletViewController(nibName: "WalletViewControllerBase", bundle: nil) + c.dialogService = r.resolve(DialogService.self) + return c + } + + /// Send tokens + static let transfer = AdamantScene(identifier: "DogeTransferViewController") { r in + let c = DogeTransferViewController() + c.dialogService = r.resolve(DialogService.self) + c.chatsProvider = r.resolve(ChatsProvider.self) + c.accountService = r.resolve(AccountService.self) + c.accountsProvider = r.resolve(AccountsProvider.self) + c.router = r.resolve(Router.self) + return c + } + + /// List of transactions + static let transactionsList = AdamantScene(identifier: "DogeTransactionsViewController") { r in + let c = DogeTransactionsViewController(nibName: "TransactionsListViewControllerBase", bundle: nil) + c.dialogService = r.resolve(DialogService.self) + c.router = r.resolve(Router.self) + return c + } + + /// Transaction details + static let transactionDetails = AdamantScene(identifier: "TransactionDetailsViewControllerBase") { r in + let c = DogeTransactionDetailsViewController() + c.dialogService = r.resolve(DialogService.self) + return c + } + } +} diff --git a/Adamant/Wallets/Doge/DogeWalletService+RichMessageProvider.swift b/Adamant/Wallets/Doge/DogeWalletService+RichMessageProvider.swift new file mode 100644 index 000000000..388ce7675 --- /dev/null +++ b/Adamant/Wallets/Doge/DogeWalletService+RichMessageProvider.swift @@ -0,0 +1,205 @@ +// +// DogeWalletService+RichMessageProvider.swift +// Adamant +// +// Created by Anton Boyarkin on 13/03/2019. +// Copyright © 2019 Adamant. All rights reserved. +// + +import Foundation +import MessageKit + +extension DogeWalletService: RichMessageProvider { + + // MARK: Events + + func richMessageTapped(for transaction: RichMessageTransaction, at indexPath: IndexPath, in chat: ChatViewController) { + // MARK: 0. Prepare + guard let richContent = transaction.richContent, + let hash = richContent[RichContentKeys.transfer.hash], + let dialogService = dialogService, + let address = wallet?.address else { + return + } + + dialogService.showProgress(withMessage: nil, userInteractionEnable: false) + + let comment: String? + if let raw = transaction.richContent?[RichContentKeys.transfer.comments], raw.count > 0 { + comment = raw + } else { + comment = nil + } + + // MARK: Get transaction + getTransaction(by: hash) { [weak self] result in + guard let vc = self?.router.get(scene: AdamantScene.Wallets.Doge.transactionDetails) as? DogeTransactionDetailsViewController, let service = self else { + return + } + + // MARK: 1. Prepare details view controller + vc.service = service + vc.comment = comment + + switch result { + case .success(let dogeRawTransaction): + let dogeTransaction = dogeRawTransaction.asDogeTransaction(for: address) + + // MARK: 2. Self name + if dogeTransaction.senderAddress == address { + vc.senderName = String.adamantLocalized.transactionDetails.yourAddress + } + if dogeTransaction.recipientAddress == address { + vc.recipientName = String.adamantLocalized.transactionDetails.yourAddress + } + + vc.transaction = dogeTransaction + + let group = DispatchGroup() + + // MARK: 3. Get partner name async + if let partner = transaction.partner, let partnerAddress = partner.address, let partnerName = partner.name { + group.enter() // Enter 1 + service.getDogeAddress(byAdamandAddress: partnerAddress) { result in + switch result { + case .success(let address): + if dogeTransaction.senderAddress == address { + vc.senderName = partnerName + } + if dogeTransaction.recipientAddress == address { + vc.recipientName = partnerName + } + + case .failure: + break + } + + group.leave() // Leave 1 + } + } + + // MARK: 4. Get block id async + if let blockHash = dogeRawTransaction.blockHash { + group.enter() // Enter 2 + service.getBlockId(by: blockHash) { result in + switch result { + case .success(let id): + vc.transaction = dogeRawTransaction.asDogeTransaction(for: address, blockId: id) + + case .failure: + break + } + + group.leave() // Leave 2 + } + } + + // MARK: 5. Wait async operations + group.wait() + + // MARK: 6. Display details view controller + DispatchQueue.main.async { + dialogService.dismissProgress() + chat.navigationController?.pushViewController(vc, animated: true) + } + + case .failure(let error): + switch error { + case .internalError(let message, _) where message == "No transaction": + let amount: Decimal + if let amountRaw = transaction.richContent?[RichContentKeys.transfer.amount], let decimal = Decimal(string: amountRaw) { + amount = decimal + } else { + amount = 0 + } + + let failedTransaction = SimpleTransactionDetails(txId: hash, + senderAddress: transaction.senderAddress, + recipientAddress: transaction.recipientAddress, + dateValue: nil, + amountValue: amount, + feeValue: nil, + confirmationsValue: nil, + blockValue: nil, + isOutgoing: transaction.isOutgoing, + transactionStatus: TransactionStatus.failed) + + vc.transaction = failedTransaction + + DispatchQueue.main.async { + dialogService.dismissProgress() + chat.navigationController?.pushViewController(vc, animated: true) + } + + default: + dialogService.dismissProgress() + dialogService.showRichError(error: error) + } + } + + } + } + + // MARK: Cells + + func cellSizeCalculator(for messagesCollectionViewFlowLayout: MessagesCollectionViewFlowLayout) -> CellSizeCalculator { + let calculator = TransferMessageSizeCalculator(layout: messagesCollectionViewFlowLayout) + calculator.font = UIFont.systemFont(ofSize: 24) + return calculator + } + + func cell(for message: MessageType, isFromCurrentSender: Bool, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UICollectionViewCell { + guard case .custom(let raw) = message.kind, let transfer = raw as? RichMessageTransfer else { + fatalError("DOGE service tried to render wrong message kind: \(message.kind)") + } + + let cellIdentifier = isFromCurrentSender ? cellIdentifierSent : cellIdentifierReceived + guard let cell = messagesCollectionView.dequeueReusableCell(withReuseIdentifier: cellIdentifier, for: indexPath) as? TransferCollectionViewCell else { + fatalError("Can't dequeue \(cellIdentifier) cell") + } + + cell.currencyLogoImageView.image = DogeWalletService.currencyLogo + cell.currencySymbolLabel.text = DogeWalletService.currencySymbol + + cell.amountLabel.text = AdamantBalanceFormat.full.format(transfer.amount) + cell.dateLabel.text = message.sentDate.humanizedDateTime(withWeekday: false) + cell.transactionStatus = (message as? RichMessageTransaction)?.transactionStatus + + cell.commentsLabel.text = transfer.comments + + if cell.isAlignedRight != isFromCurrentSender { + cell.isAlignedRight = isFromCurrentSender + } + + return cell + } + + // MARK: Short description + + private static var formatter: NumberFormatter = { + return AdamantBalanceFormat.currencyFormatter(for: .full, currencySymbol: currencySymbol) + }() + + func shortDescription(for transaction: RichMessageTransaction) -> NSAttributedString { + let amount: String + + guard let raw = transaction.richContent?[RichContentKeys.transfer.amount] else { + return NSAttributedString(string: "⬅️ \(DogeWalletService.currencySymbol)") + } + + if let decimal = Decimal(string: raw) { + amount = AdamantBalanceFormat.full.format(decimal) + } else { + amount = raw + } + + let string: String + if transaction.isOutgoing { + string = "⬅️ \(amount) \(DogeWalletService.currencySymbol)" + } else { + string = "➡️ \(amount) \(DogeWalletService.currencySymbol)" + } + + return NSAttributedString(string: string) + } +} diff --git a/Adamant/Wallets/Doge/DogeWalletService+RichMessageProviderWithStatusCheck.swift b/Adamant/Wallets/Doge/DogeWalletService+RichMessageProviderWithStatusCheck.swift new file mode 100644 index 000000000..a212d1202 --- /dev/null +++ b/Adamant/Wallets/Doge/DogeWalletService+RichMessageProviderWithStatusCheck.swift @@ -0,0 +1,115 @@ +// +// DogeWalletService+RichMessageProviderWithStatusCheck.swift +// Adamant +// +// Created by Anton Boyarkin on 13/03/2019. +// Copyright © 2019 Adamant. All rights reserved. +// + +import Foundation + +extension DogeWalletService: RichMessageProviderWithStatusCheck { + func statusFor(transaction: RichMessageTransaction, completion: @escaping (WalletServiceResult) -> Void) { + guard let hash = transaction.richContent?[RichContentKeys.transfer.hash], let date = transaction.date as Date? else { + completion(.failure(error: WalletServiceError.internalError(message: "Failed to get transaction hash", error: nil))) + return + } + + guard let walletAddress = dogeWallet?.address else { + completion(.failure(error: .notLogged)) + return + } + + getTransaction(by: hash) { result in + switch result { + case .success(let dogeTransaction): + // MARK: Check confirmations + guard let confirmations = dogeTransaction.confirmations, let dogeDate = dogeTransaction.date, (confirmations > 0 || dogeDate.timeIntervalSinceNow > -60 * 15) else { + completion(.success(result: .pending)) + return + } + + // MARK: Check date + guard let sentDate = dogeTransaction.date else { + let timeAgo = -1 * date.timeIntervalSinceNow + + let result: TransactionStatus + if timeAgo > 60 * 10 { + // 10m waiting for pending status + result = .failed + } else { + // Note: No info about processing transactions + result = .pending + } + completion(.success(result: result)) + return + } + + // 1 day + let dayInterval = TimeInterval(60 * 60 * 24) + let start = date.addingTimeInterval(-dayInterval) + let end = date.addingTimeInterval(dayInterval) + let range = start...end + + guard range.contains(sentDate) else { + completion(.success(result: .warning)) + return + } + + // MARK: Check amount & address + guard let raw = transaction.richContent?[RichContentKeys.transfer.amount], let reportedValue = AdamantBalanceFormat.deserializeBalance(from: raw) else { + completion(.success(result: .warning)) + return + } + + var result: TransactionStatus = .warning + if transaction.isOutgoing { + var totalIncome: Decimal = 0 + for input in dogeTransaction.inputs { + guard input.sender == walletAddress else { + continue + } + + totalIncome += input.value + } + + if totalIncome >= reportedValue { + result = .success + } + } else { + var totalOutcome: Decimal = 0 + for output in dogeTransaction.outputs { + guard output.addresses.contains(walletAddress) else { + continue + } + + totalOutcome += output.value + } + + if totalOutcome >= reportedValue { + result = .success + } + } + + completion(.success(result: result)) + + case .failure(let error): + if case let .internalError(message, _) = error, message == "No transaction" { + let timeAgo = -1 * date.timeIntervalSinceNow + + let result: TransactionStatus + if timeAgo > 60 * 10 { + // 10m waiting for pending status + result = .failed + } else { + // Note: No info about processing transactions + result = .pending + } + completion(.success(result: result)) + } else { + completion(.failure(error: error.asWalletServiceError())) + } + } + } + } +} diff --git a/Adamant/Wallets/Doge/DogeWalletService+Send.swift b/Adamant/Wallets/Doge/DogeWalletService+Send.swift new file mode 100644 index 000000000..fd5c2f781 --- /dev/null +++ b/Adamant/Wallets/Doge/DogeWalletService+Send.swift @@ -0,0 +1,158 @@ +// +// DogeWalletService+Send.swift +// Adamant +// +// Created by Anton Boyarkin on 12/03/2019. +// Copyright © 2019 Adamant. All rights reserved. +// + +import BitcoinKit +import BitcoinKit.Private +import Alamofire + +extension BitcoinKit.Transaction: RawTransaction { + var txHash: String? { + return txID + } +} + +extension DogeWalletService: WalletServiceTwoStepSend { + typealias T = BitcoinKit.Transaction + + func transferViewController() -> UIViewController { + guard let vc = router.get(scene: AdamantScene.Wallets.Doge.transfer) as? DogeTransferViewController else { + fatalError("Can't get DogeTransferViewController") + } + + vc.service = self + return vc + } + + + // MARK: Create & Send + func createTransaction(recipient: String, amount: Decimal, completion: @escaping (WalletServiceResult) -> Void) { + // MARK: 1. Prepare + guard let wallet = self.dogeWallet else { + completion(.failure(error: .notLogged)) + return + } + + let changeAddress = wallet.publicKey.toCashaddr() + let key = wallet.privateKey + + guard let toAddress = try? LegacyAddress(recipient, for: self.network) else { + completion(.failure(error: .accountNotFound)) + return + } + + let rawAmount = NSDecimalNumber(decimal: amount * DogeWalletService.multiplier).uint64Value + let fee = NSDecimalNumber(decimal: self.transactionFee * DogeWalletService.multiplier).uint64Value + + // MARK: 2. Search for unspent transactions + getUnspentTransactions { result in + switch result { + case .success(let utxos): + // MARK: 3. Check if we have enought money + let totalAmount: UInt64 = UInt64(utxos.reduce(0) { $0 + $1.output.value }) + guard totalAmount >= rawAmount + fee else { // This shit can crash BitcoinKit + completion(.failure(error: .notEnoughMoney)) + break + } + + // MARK: 4. Create local transaction + let transaction = BitcoinKit.Transaction.createNewTransaction(toAddress: toAddress, amount: rawAmount, fee: fee, changeAddress: changeAddress, utxos: utxos, keys: [key]) + completion(.success(result: transaction)) + + case .failure: + completion(.failure(error: .notEnoughMoney)) + } + } + } + + func sendTransaction(_ transaction: BitcoinKit.Transaction, completion: @escaping (WalletServiceResult) -> Void) { + guard let url = AdamantResources.dogeServers.randomElement() else { + fatalError("Failed to get DOGE endpoint URL") + } + + // Request url + let endpoint = url.appendingPathComponent(DogeApiCommands.sendTransaction()) + + // Headers + let headers = [ + "Content-Type": "application/json" + ] + + // MARK: Prepare params + let txHex = transaction.serialized().hex + + let parameters: [String : Any] = [ + "rawtx": txHex + ] + + // MARK: Sending request + Alamofire.request(endpoint, method: .post, parameters: parameters, encoding: JSONEncoding.default, headers: headers).responseJSON(queue: defaultDispatchQueue) { response in + switch response.result { + case .success(let data): + if let result = data as? [String: Any], let txid = result["txid"] as? String { + completion(.success(result: txid)) + } else { + completion(.failure(error: .internalError(message: "DOGE Wallet: not valid response", error: nil))) + } + + case .failure(let error): + completion(.failure(error: .remoteServiceError(message: error.localizedDescription))) + } + } + } +} + +extension BitcoinKit.Transaction: TransactionDetails { + static var defaultCurrencySymbol: String? { return DogeWalletService.currencySymbol } + + var txId: String { + return txID + } + + var dateValue: Date? { + switch lockTime { + case 1..<500000000: + return nil + case 500000000...: + return Date(timeIntervalSince1970: TimeInterval(lockTime)) + default: + return nil + } + } + + var amountValue: Decimal { + return Decimal(outputs[0].value) / Decimal(100000000) + } + + var feeValue: Decimal? { + return nil + } + + var confirmationsValue: String? { + return "0" + } + + var blockValue: String? { + return nil + } + + var isOutgoing: Bool { + return true + } + + var transactionStatus: TransactionStatus? { + return .pending + } + + var senderAddress: String { + return "" + } + + var recipientAddress: String { + return "" + } +} diff --git a/Adamant/Wallets/Doge/DogeWalletService.swift b/Adamant/Wallets/Doge/DogeWalletService.swift new file mode 100644 index 000000000..9f3193d40 --- /dev/null +++ b/Adamant/Wallets/Doge/DogeWalletService.swift @@ -0,0 +1,598 @@ +// +// DogeWalletService.swift +// Adamant +// +// Created by Anton Boyarkin on 05/03/2019. +// Copyright © 2019 Adamant. All rights reserved. +// + +import Foundation +import Swinject +import Alamofire +import BitcoinKit +import BitcoinKit.Private + +struct DogeApiCommands { + static func balance(for address: String) -> String { + return "/addr/\(address)/balance" + } + + static func getTransactions(for address: String) -> String { + return "/addrs/\(address)/txs" + } + + static func getTransaction(by hash: String) -> String { + return "/tx/\(hash)" + } + + static func getBlock(by hash: String) -> String { + return "/block/\(hash)" + } + + static func getUnspentTransactions(for address: String) -> String { + return "/addr/\(address)/utxo" + } + + static func sendTransaction() -> String { + return "/tx/send" + } +} + +class DogeWalletService: WalletService { + var wallet: WalletAccount? { return dogeWallet } + + var walletViewController: WalletViewController { + guard let vc = router.get(scene: AdamantScene.Wallets.Doge.wallet) as? DogeWalletViewController else { + fatalError("Can't get DogeWalletViewController") + } + + vc.service = self + return vc + } + + // MARK: RichMessageProvider properties + static let richMessageType = "doge_transaction" + let cellIdentifierSent = "dogeTransferSent" + let cellIdentifierReceived = "dogeTransferReceived" + let cellSource: CellSource? = CellSource.nib(nib: UINib(nibName: "TransferCollectionViewCell", bundle: nil)) + + // MARK: - Dependencies + var apiService: ApiService! + var accountService: AccountService! + var dialogService: DialogService! + var router: Router! + + // MARK: - Constants + static var currencySymbol = "DOGE" + static var currencyLogo = #imageLiteral(resourceName: "wallet_doge") + static let currencyExponent = -8 + static let multiplier = Decimal(sign: .plus, exponent: 8, significand: 1) + static let chunkSize = 20 + + private (set) var transactionFee: Decimal = 1.0 // 1 DOGE per transaction + + static let kvsAddress = "doge:address" + + // MARK: - Notifications + let walletUpdatedNotification = Notification.Name("adamant.dogeWallet.walletUpdated") + let serviceEnabledChanged = Notification.Name("adamant.dogeWallet.enabledChanged") + let serviceStateChanged = Notification.Name("adamant.dogeWallet.stateChanged") + let transactionFeeUpdated = Notification.Name("adamant.dogeWallet.feeUpdated") + + // MARK: - Delayed KVS save + private var balanceObserver: NSObjectProtocol? = nil + + // MARK: - Properties + private (set) var dogeWallet: DogeWallet? = nil + + private (set) var enabled = true + + public var network: Network + + private var initialBalanceCheck = false + + let defaultDispatchQueue = DispatchQueue(label: "im.adamant.dogeWalletService", qos: .utility, attributes: [.concurrent]) + let stateSemaphore = DispatchSemaphore(value: 1) + + private static let jsonDecoder = JSONDecoder() + + // MARK: - State + private (set) var state: WalletServiceState = .notInitiated + + private func setState(_ newState: WalletServiceState, silent: Bool = false) { + guard newState != state else { + return + } + + state = newState + + if !silent { + NotificationCenter.default.post(name: serviceStateChanged, + object: self, + userInfo: [AdamantUserInfoKey.WalletService.walletState: state]) + } + } + + init() { + self.network = DogeMainnet() + + self.setState(.notInitiated) + } + + func update() { + guard let wallet = dogeWallet else { + return + } + + defer { stateSemaphore.signal() } + stateSemaphore.wait() + + switch state { + case .notInitiated, .updating, .initiationFailed(_): + return + + case .upToDate: + break + } + + setState(.updating) + + getBalance { [weak self] result in + if let stateSemaphore = self?.stateSemaphore { + defer { + stateSemaphore.signal() + } + stateSemaphore.wait() + } + + switch result { + case .success(let balance): + let notification: Notification.Name? + + if wallet.balance != balance { + wallet.balance = balance + notification = self?.walletUpdatedNotification + self?.initialBalanceCheck = false + } else if let initialBalanceCheck = self?.initialBalanceCheck, initialBalanceCheck { + self?.initialBalanceCheck = false + notification = self?.walletUpdatedNotification + } else { + notification = nil + } + + if let notification = notification { + NotificationCenter.default.post(name: notification, object: self, userInfo: [AdamantUserInfoKey.WalletService.wallet: wallet]) + } + + case .failure(let error): + switch error { + case .networkError: + break + + case .remoteServiceError(let message) where message.contains("Server not yet ready"): + break + + default: + self?.dialogService.showRichError(error: error) + } + } + + self?.setState(.upToDate) + } + } + + func validate(address: String) -> AddressValidationResult { + return AddressFactory.isValid(bitcoinAddress: address) ? .valid : .invalid + } +} + +// MARK: - WalletInitiatedWithPassphrase +extension DogeWalletService: InitiatedWithPassphraseService { + func setInitiationFailed(reason: String) { + stateSemaphore.wait() + setState(.initiationFailed(reason: reason)) + dogeWallet = nil + stateSemaphore.signal() + } + + func initWallet(withPassphrase passphrase: String, completion: @escaping (WalletServiceResult) -> Void) { + guard let adamant = accountService.account else { + completion(.failure(error: .notLogged)) + return + } + + stateSemaphore.wait() + + setState(.notInitiated) + + if enabled { + enabled = false + NotificationCenter.default.post(name: serviceEnabledChanged, object: self) + } + + defaultDispatchQueue.async { [unowned self] in + let privateKeyData = passphrase.data(using: .utf8)!.sha256() + let privateKey = PrivateKey(data: privateKeyData, network: self.network, isPublicKeyCompressed: true) + + let eWallet = DogeWallet(privateKey: privateKey) + self.dogeWallet = eWallet + + if !self.enabled { + self.enabled = true + NotificationCenter.default.post(name: self.serviceEnabledChanged, object: self) + } + + self.stateSemaphore.signal() + + // MARK: 4. Save address into KVS + self.getWalletAddress(byAdamantAddress: adamant.address) { [weak self] result in + guard let service = self else { + return + } + + switch result { + case .success(let address): + // DOGE already saved + if address != eWallet.address { + service.save(dogeAddress: eWallet.address) { result in + service.kvsSaveCompletionRecursion(dogeAddress: eWallet.address, result: result) + } + } + + service.initialBalanceCheck = true + service.setState(.upToDate, silent: true) + service.update() + + completion(.success(result: eWallet)) + + case .failure(let error): + switch error { + case .walletNotInitiated: + // Show '0' without waiting for balance update + if let wallet = service.dogeWallet { + NotificationCenter.default.post(name: service.walletUpdatedNotification, object: service, userInfo: [AdamantUserInfoKey.WalletService.wallet: wallet]) + } + + service.save(dogeAddress: eWallet.address) { result in + service.kvsSaveCompletionRecursion(dogeAddress: eWallet.address, result: result) + } + service.setState(.upToDate) + completion(.success(result: eWallet)) + + default: + service.setState(.upToDate) + completion(.failure(error: error)) + } + } + } + } + } +} + +// MARK: - Dependencies +extension DogeWalletService: SwinjectDependentService { + func injectDependencies(from container: Container) { + accountService = container.resolve(AccountService.self) + apiService = container.resolve(ApiService.self) + dialogService = container.resolve(DialogService.self) + router = container.resolve(Router.self) + } +} + +// MARK: - Balances & addresses +extension DogeWalletService { + func getBalance(_ completion: @escaping (WalletServiceResult) -> Void) { + guard let url = AdamantResources.dogeServers.randomElement() else { + fatalError("Failed to get DOGE endpoint URL") + } + + guard let address = self.dogeWallet?.address else { + completion(.failure(error: .walletNotInitiated)) + return + } + + // Headers + let headers = [ + "Content-Type": "application/json" + ] + + // Request url + let endpoint = url.appendingPathComponent(DogeApiCommands.balance(for: address)) + + // MARK: Sending request + Alamofire.request(endpoint, method: .get, headers: headers).responseString(queue: defaultDispatchQueue) { response in + + switch response.result { + case .success(let data): + if let raw = Decimal(string: data) { + let balance = raw / DogeWalletService.multiplier + completion(.success(result: balance)) + } else { + completion(.failure(error: .remoteServiceError(message: "DOGE Wallet: \(data)"))) + } + + case .failure: + completion(.failure(error: .networkError)) + } + } + } + + func getDogeAddress(byAdamandAddress address: String, completion: @escaping (ApiServiceResult) -> Void) { + apiService.get(key: DogeWalletService.kvsAddress, sender: address, completion: completion) + } + + func getWalletAddress(byAdamantAddress address: String, completion: @escaping (WalletServiceResult) -> Void) { + apiService.get(key: DogeWalletService.kvsAddress, sender: address) { (result) in + switch result { + case .success(let value): + if let address = value { + completion(.success(result: address)) + } else { + completion(.failure(error: .walletNotInitiated)) + } + + case .failure(let error): + completion(.failure(error: .internalError(message: "DOGE Wallet: fail to get address from KVS", error: error))) + } + } + } +} + +// MARK: - KVS +extension DogeWalletService { + /// - Parameters: + /// - dogeAddress: DOGE address to save into KVS + /// - adamantAddress: Owner of Doge address + /// - completion: success + private func save(dogeAddress: String, completion: @escaping (WalletServiceSimpleResult) -> Void) { + guard let adamant = accountService.account, let keypair = accountService.keypair else { + completion(.failure(error: .notLogged)) + return + } + + guard adamant.balance >= AdamantApiService.KvsFee else { + completion(.failure(error: .notEnoughMoney)) + return + } + + apiService.store(key: DogeWalletService.kvsAddress, value: dogeAddress, type: .keyValue, sender: adamant.address, keypair: keypair) { result in + switch result { + case .success: + completion(.success) + + case .failure(let error): + completion(.failure(error: .apiError(error))) + } + } + } + + /// New accounts doesn't have enought money to save KVS. We need to wait for balance update, and then - retry save + private func kvsSaveCompletionRecursion(dogeAddress: String, result: WalletServiceSimpleResult) { + if let observer = balanceObserver { + NotificationCenter.default.removeObserver(observer) + balanceObserver = nil + } + + switch result { + case .success: + break + + case .failure(let error): + switch error { + case .notEnoughMoney: // Possibly new account, we need to wait for dropship + // Register observer + let observer = NotificationCenter.default.addObserver(forName: NSNotification.Name.AdamantAccountService.accountDataUpdated, object: nil, queue: nil) { [weak self] _ in + guard let balance = self?.accountService.account?.balance, balance > AdamantApiService.KvsFee else { + return + } + + self?.save(dogeAddress: dogeAddress) { result in + self?.kvsSaveCompletionRecursion(dogeAddress: dogeAddress, result: result) + } + } + + // Save referense to unregister it later + balanceObserver = observer + + default: + dialogService.showRichError(error: error) + } + } + } +} + +// MARK: - Transactions +extension DogeWalletService { + func getTransactions(from: Int, completion: @escaping (ApiServiceResult<(transactions: [DogeTransaction], hasMore: Bool)>) -> Void) { + guard let address = self.wallet?.address else { + completion(.failure(.notLogged)) + return + } + + getTransactions(for: address, from: from, to: from + DogeWalletService.chunkSize) { response in + switch response { + case .success(let doge): + let hasMore = doge.to < doge.totalItems + + let transactions = doge.items.map { $0.asDogeTransaction(for: address) } + + completion(.success((transactions: transactions, hasMore: hasMore))) + + case .failure(let error): + completion(.failure(error)) + } + } + } + + private func getTransactions(for address: String, from: Int, to: Int, completion: @escaping (ApiServiceResult) -> Void) { + guard let url = AdamantResources.dogeServers.randomElement() else { + fatalError("Failed to get DOGE endpoint URL") + } + + // Headers + let headers = [ + "Content-Type": "application/json" + ] + + let parameters = [ + "from": from, + "to": to + ] + + // Request url + let endpoint = url.appendingPathComponent(DogeApiCommands.getTransactions(for: address)) + + // MARK: Sending request + Alamofire.request(endpoint, method: .get, parameters: parameters, headers: headers).responseData(queue: defaultDispatchQueue) { response in + switch response.result { + case .success(let data): + do { + let dogeResponse = try DogeWalletService.jsonDecoder.decode(DogeGetTransactionsResponse.self, from: data) + completion(.success(dogeResponse)) + } catch { + completion(.failure(.internalError(message: "DOGE Wallet: not a valid response", error: error))) + } + + case .failure(let error as URLError): + completion(.failure(.networkError(error: error))) + + case .failure(let error): + completion(.failure(.serverError(error: error.localizedDescription))) + } + } + } + + func getUnspentTransactions(_ completion: @escaping (ApiServiceResult<[UnspentTransaction]>) -> Void) { + guard let url = AdamantResources.dogeServers.randomElement() else { + fatalError("Failed to get DOGE endpoint URL") + } + + guard let wallet = self.dogeWallet else { + completion(.failure(.notLogged)) + return + } + + let address = wallet.address + + // Headers + let headers = [ + "Content-Type": "application/json" + ] + + // Request url + let endpoint = url.appendingPathComponent(DogeApiCommands.getUnspentTransactions(for: address)) + + let parameters = [ + "noCache": "1" + ] + + // MARK: Sending request + Alamofire.request(endpoint, method: .get, parameters: parameters, headers: headers).responseJSON(queue: defaultDispatchQueue) { response in + switch response.result { + case .success(let data): + guard let items = data as? [[String: Any]] else { + completion(.failure(.internalError(message: "DOGE Wallet: not valid response", error: nil))) + break + } + + var utxos = [UnspentTransaction]() + for item in items { + guard let txid = item["txid"] as? String, + let vout = item["vout"] as? NSNumber, + let amount = item["amount"] as? NSNumber else { + continue + } + + let value = NSDecimalNumber(decimal: (amount.decimalValue * DogeWalletService.multiplier)).uint64Value + + let lockScript = Script.buildPublicKeyHashOut(pubKeyHash: wallet.publicKey.toCashaddr().data) + let txHash = Data(hex: txid).map { Data($0.reversed()) } ?? Data() + let txIndex = vout.uint32Value + + let unspentOutput = TransactionOutput(value: value, lockingScript: lockScript) + let unspentOutpoint = TransactionOutPoint(hash: txHash, index: txIndex) + let utxo = UnspentTransaction(output: unspentOutput, outpoint: unspentOutpoint) + + utxos.append(utxo) + } + + completion(.success(utxos)) + + case .failure: + completion(.failure(.internalError(message: "DOGE Wallet: server not response", error: nil))) + } + } + } + + func getTransaction(by hash: String, completion: @escaping (ApiServiceResult) -> Void) { + guard let url = AdamantResources.dogeServers.randomElement() else { + fatalError("Failed to get DOGE endpoint URL") + } + + // Headers + let headers = [ + "Content-Type": "application/json" + ] + + // Request url + let endpoint = url.appendingPathComponent(DogeApiCommands.getTransaction(by: hash)) + + // MARK: Sending request + Alamofire.request(endpoint, method: .get, headers: headers).responseData(queue: defaultDispatchQueue) { response in + switch response.result { + case .success(let data): + do { + let transfers = try DogeWalletService.jsonDecoder.decode(DogeRawTransaction.self, from: data) + completion(.success(transfers)) + } catch { + completion(.failure(.internalError(message: "DOGE: Parsing transaction error", error: error))) + } + + case .failure(let error): + completion(.failure(.internalError(message: "No transaction", error: error))) + } + } + } + + func getBlockId(by hash: String, completion: @escaping (ApiServiceResult) -> Void) { + guard let url = AdamantResources.dogeServers.randomElement() else { + fatalError("Failed to get DOGE endpoint URL") + } + + // Headers + let headers = [ + "Content-Type": "application/json" + ] + + // Request url + let endpoint = url.appendingPathComponent(DogeApiCommands.getBlock(by: hash)) + Alamofire.request(endpoint, method: .get, headers: headers).responseJSON(queue: defaultDispatchQueue) { response in + switch response.result { + case .success(let json as [String: Any]): + if let height = json["height"] as? NSNumber { + completion(.success(height.stringValue)) + } else { + completion(.failure(.internalError(message: "Failed to parse block", error: nil))) + } + + case .failure(let error): + completion(.failure(.internalError(message: "No block", error: error))) + + default: + completion(.failure(.internalError(message: "No block", error: nil))) + } + } + } +} + +// MARK: - WalletServiceWithTransfers +extension DogeWalletService: WalletServiceWithTransfers { + func transferListViewController() -> UIViewController { + guard let vc = router.get(scene: AdamantScene.Wallets.Doge.transactionsList) as? DogeTransactionsViewController else { + fatalError("Can't get DogeTransactionsViewController") + } + + vc.walletService = self + return vc + } +} diff --git a/Adamant/Wallets/Doge/DogeWalletViewController.swift b/Adamant/Wallets/Doge/DogeWalletViewController.swift new file mode 100644 index 000000000..5c6029fa6 --- /dev/null +++ b/Adamant/Wallets/Doge/DogeWalletViewController.swift @@ -0,0 +1,33 @@ +// +// DogeWalletViewController.swift +// Adamant +// +// Created by Anton Boyarkin on 05/03/2019. +// Copyright © 2019 Adamant. All rights reserved. +// + +import UIKit + +extension String.adamantLocalized { + static let doge = NSLocalizedString("AccountTab.Wallets.doge_wallet", comment: "Account tab: Doge wallet") + + static let sendDoge = NSLocalizedString("AccountTab.Row.SendDoge", comment: "Account tab: 'Send DOGE tokens' button") +} + +class DogeWalletViewController: WalletViewControllerBase { + // MARK: Lifecycle + + override func viewDidLoad() { + super.viewDidLoad() + + walletTitleLabel.text = String.adamantLocalized.doge + } + + override func sendRowLocalizedLabel() -> String { + return String.adamantLocalized.sendDoge + } + + override func encodeForQr(address: String) -> String? { + return "doge:\(address)" + } +} diff --git a/Adamant/Wallets/Ethereum/EthWalletService.swift b/Adamant/Wallets/Ethereum/EthWalletService.swift index aeb7bbf5e..fe27c439e 100644 --- a/Adamant/Wallets/Ethereum/EthWalletService.swift +++ b/Adamant/Wallets/Ethereum/EthWalletService.swift @@ -22,6 +22,9 @@ extension Web3Error { case .nodeError(let message): return .remoteServiceError(message: message) + case .generalError(_ as URLError): + return .networkError + case .generalError(let error), .keystoreError(let error as Error): return .internalError(message: error.localizedDescription, error: error) @@ -199,7 +202,13 @@ class EthWalletService: WalletService { } case .failure(let error): - self?.dialogService.showRichError(error: error) + switch error { + case .networkError: + break + + default: + self?.dialogService.showRichError(error: error) + } } self?.setState(.upToDate) diff --git a/Adamant/Wallets/Lisk/LskTransactionsViewController.swift b/Adamant/Wallets/Lisk/LskTransactionsViewController.swift index eb96db786..ad0e78c7e 100644 --- a/Adamant/Wallets/Lisk/LskTransactionsViewController.swift +++ b/Adamant/Wallets/Lisk/LskTransactionsViewController.swift @@ -117,6 +117,8 @@ class LskTransactionsViewController: TransactionsListViewControllerBase { } extension Transactions.TransactionModel: TransactionDetails { + static var defaultCurrencySymbol: String? { return LskWalletService.currencySymbol } + var txId: String { return id } @@ -170,6 +172,8 @@ extension Transactions.TransactionModel: TransactionDetails { } extension LocalTransaction: TransactionDetails { + static var defaultCurrencySymbol: String? { return LskWalletService.currencySymbol } + var txId: String { return id ?? "" } diff --git a/Adamant/Wallets/Lisk/LskWalletService.swift b/Adamant/Wallets/Lisk/LskWalletService.swift index 167a1103b..1b361ccbc 100644 --- a/Adamant/Wallets/Lisk/LskWalletService.swift +++ b/Adamant/Wallets/Lisk/LskWalletService.swift @@ -445,30 +445,31 @@ extension LskWalletService: SwinjectDependentService { // MARK: - Balances & addresses extension LskWalletService { func getBalance(_ completion: @escaping (WalletServiceResult) -> Void) { - if let address = self.lskWallet?.address, let accountApi = accountApi { - defaultDispatchQueue.async { - accountApi.accounts(address: address) { (response) in - switch response { - case .success(response: let response): - if let account = response.data.first { - let balance = BigUInt(account.balance ?? "0") ?? BigUInt(0) - - completion(.success(result: balance.asDecimal(exponent: LskWalletService.currencyExponent))) - } else { - completion(.success(result: 0)) - } - - break - case .error(response: let error): - print(error) + guard let address = self.lskWallet?.address, let accountApi = accountApi else { + completion(.failure(error: .notLogged)) + return + } + + defaultDispatchQueue.async { + accountApi.accounts(address: address) { (response) in + switch response { + case .success(response: let response): + if let account = response.data.first { + let balance = BigUInt(account.balance ?? "0") ?? BigUInt(0) + completion(.success(result: balance.asDecimal(exponent: LskWalletService.currencyExponent))) + } else { + completion(.success(result: 0)) + } + + case .error(response: let error): + if error.message == "Unexpected Error" { + completion(.failure(error: .networkError)) + } else { completion(.failure(error: .internalError(message: error.message, error: nil))) - break } } } - } else { - completion(.failure(error: .internalError(message: "LSK Wallet: not found", error: nil))) } } diff --git a/Adamant/Wallets/TransactionDetails.swift b/Adamant/Wallets/TransactionDetails.swift index 436c02ca7..548c9b2ce 100644 --- a/Adamant/Wallets/TransactionDetails.swift +++ b/Adamant/Wallets/TransactionDetails.swift @@ -7,8 +7,6 @@ // import Foundation -import web3swift -import BigInt /// A standard protocol representing a Transaction details. protocol TransactionDetails { @@ -39,22 +37,48 @@ protocol TransactionDetails { var isOutgoing: Bool { get } var transactionStatus: TransactionStatus? { get } + + static var defaultCurrencySymbol: String? { get } + func summary(with url: String?) -> String } extension TransactionDetails { -// func getSummary() -> String { -// return """ -// Transaction #\(id) -// -// Summary -// Sender: \(senderAddress) -// Recipient: \(recipientAddress) -// Date: \(DateFormatter.localizedString(from: sentDate, dateStyle: .short, timeStyle: .medium)) -// Amount: \(formattedAmount()) -// Fee: \(formattedFee()) -// Confirmations: \(String(confirmationsValue)) -// Block: \(block) -// URL: \(explorerUrl?.absoluteString ?? "") -// """ -// } + func summary(with url: String? = nil) -> String { + let symbol = type(of: self).defaultCurrencySymbol + + var summary = """ + Transaction \(txId) + + Summary + Sender: \(senderAddress) + Recipient: \(recipientAddress) + Amount: \(AdamantBalanceFormat.full.format(amountValue, withCurrencySymbol: symbol)) + """ + + if let fee = feeValue { + summary += "\nFee: \(AdamantBalanceFormat.full.format(fee, withCurrencySymbol: symbol))" + } + + if let date = dateValue { + summary += "\nDate: \(DateFormatter.localizedString(from: date, dateStyle: .short, timeStyle: .medium))" + } + + if let confirmations = confirmationsValue { + summary += "\nConfirmations: \(confirmations)" + } + + if let block = blockValue { + summary += "\nBlock: \(block)" + } + + if let status = transactionStatus { + summary += "\nStatus: \(status.localized)" + } + + if let url = url { + summary += "\nURL: \(url)" + } + + return summary + } } diff --git a/Adamant/Wallets/TransactionDetailsViewControllerBase.swift b/Adamant/Wallets/TransactionDetailsViewControllerBase.swift index 8144eb913..e3a264e1c 100644 --- a/Adamant/Wallets/TransactionDetailsViewControllerBase.swift +++ b/Adamant/Wallets/TransactionDetailsViewControllerBase.swift @@ -112,8 +112,14 @@ class TransactionDetailsViewControllerBase: FormViewController { // MARK: - Properties var transaction: TransactionDetails? = nil + private lazy var dateFormatter: DateFormatter = { + let dateFormatter = DateFormatter() + dateFormatter.dateStyle = .medium + dateFormatter.timeStyle = .short + return dateFormatter + }() - private static let awaitingValueString = "⏱" + static let awaitingValueString = "⏱" private lazy var currencyFormatter: NumberFormatter = { return AdamantBalanceFormat.currencyFormatter(for: .full, currencySymbol: currencySymbol) @@ -267,26 +273,31 @@ class TransactionDetailsViewControllerBase: FormViewController { detailsSection.append(recipientRow) // MARK: Date - let dateRow = DateTimeRow() { + let dateRow = LabelRow() { [weak self] in $0.disabled = true $0.tag = Rows.date.tag $0.title = Rows.date.localized - $0.value = transaction?.dateValue - let dateFormatter = DateFormatter() - dateFormatter.dateStyle = .medium - dateFormatter.timeStyle = .short - $0.dateFormatter = dateFormatter + if let raw = transaction?.dateValue, let value = self?.dateFormatter.string(from: raw) { + $0.value = value + } else { + $0.value = TransactionDetailsViewControllerBase.awaitingValueString + } }.cellSetup { (cell, _) in cell.selectionStyle = .gray }.onCellSelection { [weak self] (cell, row) in - if let value = row.value { + if let value = self?.transaction?.dateValue { let text = value.humanizedDateTimeFull() self?.shareValue(text, from: cell) } }.cellUpdate { [weak self] (cell, row) in cell.textLabel?.textColor = .black - row.value = self?.transaction?.dateValue + + if let raw = self?.transaction?.dateValue, let value = self?.dateFormatter.string(from: raw) { + row.value = value + } else { + row.value = TransactionDetailsViewControllerBase.awaitingValueString + } } detailsSection.append(dateRow) @@ -548,6 +559,6 @@ class TransactionDetailsViewControllerBase: FormViewController { } func summary(for transaction: TransactionDetails) -> String? { - return AdamantFormattingTools.summaryFor(transaction: transaction, url: explorerUrl(for: transaction)) + return transaction.summary(with: explorerUrl(for: transaction)?.absoluteString) } } diff --git a/Podfile b/Podfile index a10a6c8b7..8472863c7 100644 --- a/Podfile +++ b/Podfile @@ -32,7 +32,8 @@ target 'Adamant' do pod 'libsodium' # Sodium crypto library pod 'web3swift' # ETH Web3 Swift Port pod 'Lisk', :git => 'https://github.com/adamant-im/lisk-swift.git' # LSK - + pod 'BitcoinKit', :git => 'https://github.com/boyarkin-anton/BitcoinKit.git', :branch => 'dev' # BTC + # Utility pod 'ByteBackpacker' # Utility to pack value types into a Byte array diff --git a/Podfile.lock b/Podfile.lock index a2412764c..44c591bed 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -2,6 +2,11 @@ PODS: - Alamofire (4.8.0) - BigInt (3.1.0): - SipHash (~> 1.2) + - BitcoinKit (1.0.2): + - GRDB.swift (~> 3.6.2) + - GRDBCipher (~> 3.6.2) + - GRKOpenSSLFramework (~> 1.0.2.15) + - secp256k1_swift (~> 1.0.3) - ByteBackpacker (1.2.1) - CryptoSwift (0.12.0) - DateToolsSwift (4.0.0) @@ -16,6 +21,10 @@ PODS: - FTIndicator/FTNotificationIndicator (1.2.9) - FTIndicator/FTProgressIndicator (1.2.9) - FTIndicator/FTToastIndicator (1.2.9) + - GRDB.swift (3.6.2) + - GRDBCipher (3.6.2): + - SQLCipher (~> 3.4.1) + - GRKOpenSSLFramework (1.0.2.15) - KeychainAccess (3.1.2) - libsodium (1.0.12) - Lisk (1.1.0): @@ -48,6 +57,11 @@ PODS: - CryptoSwift (~> 0.11) - secp256k1_swift (1.0.3) - SipHash (1.2.2) + - SQLCipher (3.4.2): + - SQLCipher/standard (= 3.4.2) + - SQLCipher/common (3.4.2) + - SQLCipher/standard (3.4.2): + - SQLCipher/common - swift_qrcodejs (1.0.1) - SwiftRLP (1.2): - BigInt (~> 3.1) @@ -64,6 +78,7 @@ PODS: DEPENDENCIES: - Alamofire + - BitcoinKit (from `https://github.com/boyarkin-anton/BitcoinKit.git`, branch `dev`) - ByteBackpacker - CryptoSwift - DateToolsSwift @@ -98,6 +113,9 @@ SPEC REPOS: - Eureka - FreakingSimpleRoundImageView - FTIndicator + - GRDB.swift + - GRDBCipher + - GRKOpenSSLFramework - KeychainAccess - libsodium - MessageInputBar @@ -114,12 +132,16 @@ SPEC REPOS: - scrypt - secp256k1_swift - SipHash + - SQLCipher - swift_qrcodejs - SwiftRLP - Swinject - web3swift EXTERNAL SOURCES: + BitcoinKit: + :branch: dev + :git: https://github.com/boyarkin-anton/BitcoinKit.git Lisk: :git: https://github.com/adamant-im/lisk-swift.git MarkdownKit: @@ -129,6 +151,9 @@ EXTERNAL SOURCES: :git: https://github.com/RealBonus/SwiftyOnboard CHECKOUT OPTIONS: + BitcoinKit: + :commit: b78dd8df08bee99bdd75eef5cb5e8de0e27638e8 + :git: https://github.com/boyarkin-anton/BitcoinKit.git Lisk: :commit: 154a3af05772136b776ff14ae05f92c8d844ea20 :git: https://github.com/adamant-im/lisk-swift.git @@ -142,6 +167,7 @@ CHECKOUT OPTIONS: SPEC CHECKSUMS: Alamofire: 3ec537f71edc9804815215393ae2b1a8ea33a844 BigInt: 76b5dfdfa3e2e478d4ffdf161aeede5502e2742f + BitcoinKit: af68da8b2c9a23bc355c711fdc6bbd0abc8cc447 ByteBackpacker: df001da117faacdbf09b69a116ec480f6651c9ec CryptoSwift: 1c07ca50843dd48bc54e6ea53d7a4dba3b645716 DateToolsSwift: 875d97ff9e3a5d54abdd67a269b3f51c757b71ab @@ -149,6 +175,9 @@ SPEC CHECKSUMS: Eureka: 28ea296f06710f6745266b71f17862048941a32d FreakingSimpleRoundImageView: 0d687cb05da8684e85c4c2ae9945bafcbe89d2a2 FTIndicator: f7f071fd159e5befa1d040a9ef2e3ab53fa9322c + GRDB.swift: e10787d857b6b8135354b9e08fb23b4bb8e2fab7 + GRDBCipher: caccb02f13acd7f745e595cf84a70be3e91fb5f3 + GRKOpenSSLFramework: 8180d66833be66fc0f2d4942757d095edb0778d0 KeychainAccess: b3816fddcf28aa29d94b10ec305cd52be14c472b libsodium: 9a8faa5ef2fa0d2d57bd7f7d79bf8fb7c1a9f0ea Lisk: 2b21f9a60f4b1583349a1b1677d83781ec32ca70 @@ -167,12 +196,13 @@ SPEC CHECKSUMS: scrypt: 3fe5b1a3b0976f97cd87488673a8f7c65708cc84 secp256k1_swift: 4fc5c4b2d2c6d21ee8ccb868cdc92da12f38bed9 SipHash: fad90a4683e420c52ef28063063dbbce248ea6d4 + SQLCipher: f9fcf29b2e59ced7defc2a2bdd0ebe79b40d4990 swift_qrcodejs: c181fe5c849d30c699546a23762d7e3dd143ab37 SwiftRLP: 98a02b2210128353ca02e4c2f4d83e2a9796db4f SwiftyOnboard: de60e90c2af23dd61b01ec0c6a8d0be3370f857b Swinject: 82cdb851f63f91bba974e3eca1d69780f2f7677e web3swift: d80d1b9a3feca16e614acec9eea47ec26310d37d -PODFILE CHECKSUM: f183aea9085cd37a477cff56eb908cee1843ba18 +PODFILE CHECKSUM: e2142b4b30c5188f99e941771fd278f74c36dcda COCOAPODS: 1.6.0