From 77b7672a78180e0fe79f25edf2e615e48bf67b22 Mon Sep 17 00:00:00 2001 From: Anton B Date: Mon, 11 Mar 2019 16:40:49 +0300 Subject: [PATCH 01/46] DogeWallet init --- .../wallet_doge.imageset/Contents.json | 23 ++ .../wallet_doge.imageset/wallet_doge.png | Bin 0 -> 1002 bytes .../wallet_doge.imageset/wallet_doge@2x.png | Bin 0 -> 2231 bytes .../wallet_doge.imageset/wallet_doge@3x.png | Bin 0 -> 3607 bytes Adamant/Services/AdamantAccountService.swift | 3 +- Adamant/Wallets/Doge/DogeWallet.swift | 24 ++ Adamant/Wallets/Doge/DogeWalletRoutes.swift | 20 + Adamant/Wallets/Doge/DogeWalletService.swift | 357 ++++++++++++++++++ .../Doge/DogeWalletViewController.swift | 33 ++ Podfile | 3 +- Podfile.lock | 34 +- 11 files changed, 493 insertions(+), 4 deletions(-) create mode 100644 Adamant/Assets/Assets.xcassets/Wallets/wallet_doge.imageset/Contents.json create mode 100644 Adamant/Assets/Assets.xcassets/Wallets/wallet_doge.imageset/wallet_doge.png create mode 100644 Adamant/Assets/Assets.xcassets/Wallets/wallet_doge.imageset/wallet_doge@2x.png create mode 100644 Adamant/Assets/Assets.xcassets/Wallets/wallet_doge.imageset/wallet_doge@3x.png create mode 100644 Adamant/Wallets/Doge/DogeWallet.swift create mode 100644 Adamant/Wallets/Doge/DogeWalletRoutes.swift create mode 100644 Adamant/Wallets/Doge/DogeWalletService.swift create mode 100644 Adamant/Wallets/Doge/DogeWalletViewController.swift 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 0000000000000000000000000000000000000000..68e9f0609c18d98346c7e44a2dcd727020a3ba08 GIT binary patch literal 1002 zcmVE3Z5w1s0d;aEEO9I z8%41b6f9K`1e=IhDToImC<+pc;sp;Cl6c_}#UuWIvLoZ>?Plh^eJ?xt;Fme}+sXHT zGrK!Gvo#~qZMF^{6=N*MDqkx`i&0T(=S>~yckwl%Pw6M|TD-F8Qod9C6jill`fvSJ z0O7aAaj{j@i;7#d#KnI=di#RdB*s-rEF|e3mMnyJGQRafK)jO-QuORIet8y0jQ7PhLGQU55B4cI=Up5zUKU5lI2w3Aay&@QKUlc3XpsR7M1|yDGPx8mn=E|==lwpac$D%;S!I&5<2}vB9?l}lEF7ugN z{xGE?X-$#3q^dPKkk*ztkLT7@Q}9d?x8CubM#MoD#t-4rmWxkPJP(r&6sd0rZCHLN zA|k13QfT5>v39lKdd^2&a6R|gwcF$uoVW%LaY4;c(ADH2t~azBijTOS;ViD32kNYp zu4ZNGe9CeA&{Y+7+Z73UmI6vOR?A%NDdL`|c(Y{YYP^=DwdLAQn&0xYgRx^zLK>Dc z6+F0FS4iU8Tvf^My?7RqIG5|$=w}bFKNNB7$$_igz%dkY2V7M}e(YNWA#T6oToncF zl61<6Z(DLNZi8YxOrRci*41~Y_6qo7>6rK}eD(@@c{d+CSQru)U%z9y8#5XHX5Ac!9cniJ^;**PJF-NpUsgjcHjm|pZbEeO*^cg;t z@ZA#^7RsX_9(TNaO%-e#|7XKN9Jl?AV!)bd=;FF1YPDc4Ilw{|$CmP1@nV3gs3KHx zyf?OpJ4wQ*4^bRnEU-fG00HG#o*o6v8CRAgCVp-n#9dKP>>zoMvMgsP@gJtta<5Kg ztLl>I5Cu_^4YvO_QLQczOh$pSowv)To|LIoov4jc@;rxFL;~(~q|5_f3Wl74+j}PV Y2Ss8CzU&9v(*OVf07*qoM6N<$f_m!Ji2wiq literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..6d7df23b1335e6e18f486bbb32c46a93c0dd7991 GIT binary patch literal 2231 zcmV;o2uSydP)$M=PY znVp&Unc4R_vuDrjobA%)XT#w>66g`3rj#B1#0G-~td7i>NM9L3E>CIQT(w|_=0Q;b9 zScLlOT_~W4D=ciSZ`2CM({K{}gtjs^;QVF)6M*hOOW4sMoc*EffNJxANkFF-@n{py z7Lk|GSAcVX!tI#Cdu*K!`{7g`1Nfb|1Q-t#W-45mP+tz3#lTA7hA@=rEGk@jd|Ycs z!C^kI6gbb-*`>V-=Mt^#Z7)UBuYos!zS-Tfw^ZS>$I7)OSAZS{)&mn=otw2+;hIIr zzrH^xJ_23>g8hV0;rxZk!oc;_Cx8{esaYJFwNc@kMJR0j#i08fIMrSQ zs1CWv%w=~qI zDnrteHWg=eZI&}D>zga8Z}3$)AG!85VjWUuJs~gX7H# zAJisVS%pg+a8$u3(VTsRtM!Kih2)d#OMub9FrX|~AAZng0^A-UW>Q-h?hzYtJn``; z<`eAaf%SmBMd3*0$6_2})+)l)2Z)!d7l7}9BPHAy^@FmH%W`?J$5e(q_mMmU30t_HpWiZt#LkG&63;j+uH2u=A|<_>^6o{KQf_*>&T zQsLsFl3w->s(Clc{~Q!y55#q%!o@|UdD&Vt;lGp?HgD?Pf|qHos??YYR}&7WgATOj zzm?`WdF5-|FzZyfe1SAx{Hbs!u+$U?rMH8(X2nN^^N%#wP;Ul)^QTH=;4a zsS0Nz5hnc!G;_jG#jc#c)&&Ojnr_70yH=T*}8nQfrzcz+KTyg$pN` z%{1FS2$<(ney(4Z=BaPMb1MZE?=Jb{T*A&Lsm?fw(6+Vx4%&4x)NLx9P+$$M)H0X~ zXCf?=N(oo_Z@FG7g;WFWDj{m_dhW(0l-(X3@UOA4kJddn>B>#82Ru1!cRuv2d{XT~ z;XEhBPM~I0KTw~88FUF^-|_JSL4>lNp@6oBb(b8C1B~u!Jj2-T&{=%|N|WdPFaSD_<=vVSQCNiLh2VrG#r&;i^HF z$nB6I1!oTxPAIYwEq{+nn9ewfs9K!-C#{63a1v3qICaKZ!RSm`uAWGke#c2f)#Btv zRtY<#HzyKdt#Y1R%?8bFSK)%jvw=)XxNRz2L#%@qsBl5USsd9Qp{{hs2?f^Bo@p|H zQxz_7K-MEWp1+NmymepI#~@Lfh6>^M-_gUGR=_HThJyfKj!m zuCwwXvtNR&@>E@g3mMk`xp1m*t>Q9r%@V;H6!r9UA%X49zKfI;v zJ)2mCv!jtee-~UQ=XY=FxdtsND<-C;3TGmbKOGL&*Ymq)@~oL{^ibh4#VxTBf3NZJ z({YI{!c?emVd7X};mZJ)00RPY#!>OL8x4O+))^NUs`N6xsp3(9%TGhnn+B=nO;3aI z3AkGJ2nLOr51in~?-)Be4o<+xdk~%M|aN{uGgTSr`JFPrV zW*@-kQZ{gS*t#NT&>i^`fxdO%_tj9~>?mcMve-8m$V|z>OanQu!DRsd{YkOQFc#l2 zjKO)NT3KgY>Yx)UTjgBl;QW>+oa5nQy{kMNic)0$z$hNiG7=RoOO%Rf!^`{>fUgB^ zYARgQ5Ea(oIrQ{N*4|XOtdWv!$3M7Ccj-cfbBUHr+iyX88^Al!%%;L+4wXp5bzq$U z{O!uF!nsB)TzfvM84u)o!=4HkP6!vK4UuTP{m*48T&}QrIn9UnXkd#EiwfrxJvU!& zXnhKp1a!E$Hnwh+WunG2D_6;O42M6iKMpl4opGTenand6ReVGgOf+_-GmcS^vDKh` z1Xw0PuQM)uRQcrkAz%a`A{vLRGmcRRW87`bjj^+V#3$Ebx>DhMs|+*Q^o>_6%|ep()n(5nCd002ovPDHLk FV1m}XKV<*_ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..5ddedc0c50e13fc0e14aea2b0786444872a62ce3 GIT binary patch literal 3607 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 dfN~od{tw^_w$d~ AddressValidationResult { + // Todo + return .valid + } + + func getWalletAddress(byAdamantAddress address: String, completion: @escaping (WalletServiceResult) -> Void) { + // Todo + } +} + +// 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: - KVS +extension DogeWalletService { + /// - Parameters: + /// - dogeAddress: DOGE address to save into KVS + /// - adamantAddress: Owner of Lisk 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) + } + } + } +} + +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 [ + "seed.dogecoin.com", + "seed.multidoge.org", + "seed2.multidoge.org", + "seed.doger.dogecoin.com" + ] + } + + // todo hashGenesisBlock = "1a91e3dace36e2be3bf030a65679fe821aa1d6ef92e7c9902eb318182c355691" +} + +class DogeTestnet: 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 0x71 + } + + override var privatekey: UInt8 { + return 0xf1 + } + + override var scripthash: UInt8 { + return 0xc4 + } + + override var xpubkey: UInt32 { + return 0x02facafd + } + + override var xprivkey: UInt32 { + return 0x02fac398 + } + + override var port: UInt32 { + return 22556 + } + + override var dnsSeeds: [String] { + return [ + // "seed.dogecoin.com", + // "seed.multidoge.org", + // "seed2.multidoge.org", + // "seed.doger.dogecoin.com" + ] + } + + // todo hashGenesisBlock = "" +} 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/Podfile b/Podfile index d71be7937..fabc79033 100644 --- a/Podfile +++ b/Podfile @@ -33,7 +33,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 4a13330d2..aa37676b8 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) - Haring (2.1) - KeychainAccess (3.1.2) - KZFileWatchers (1.0.5) @@ -49,6 +58,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 - Stylist (0.2.1): - KZFileWatchers (~> 1.0.5) - Yams (~> 1.0.0) @@ -69,6 +83,7 @@ PODS: DEPENDENCIES: - Alamofire + - BitcoinKit (from `https://github.com/boyarkin-anton/BitcoinKit.git`, branch `dev`) - ByteBackpacker - CryptoSwift - DateToolsSwift @@ -104,6 +119,9 @@ SPEC REPOS: - Eureka - FreakingSimpleRoundImageView - FTIndicator + - GRDB.swift + - GRDBCipher + - GRKOpenSSLFramework - Haring - KeychainAccess - KZFileWatchers @@ -122,6 +140,7 @@ SPEC REPOS: - scrypt - secp256k1_swift - SipHash + - SQLCipher - Stylist - swift_qrcodejs - SwiftRLP @@ -130,6 +149,9 @@ SPEC REPOS: - Yams EXTERNAL SOURCES: + BitcoinKit: + :branch: dev + :git: https://github.com/boyarkin-anton/BitcoinKit.git Lisk: :git: https://github.com/adamant-im/lisk-swift.git SwiftyOnboard: @@ -137,6 +159,9 @@ EXTERNAL SOURCES: :git: https://github.com/RealBonus/SwiftyOnboard CHECKOUT OPTIONS: + BitcoinKit: + :commit: 4c0ba2fce414436e2288a354411675a12c2866dc + :git: https://github.com/boyarkin-anton/BitcoinKit.git Lisk: :commit: 154a3af05772136b776ff14ae05f92c8d844ea20 :git: https://github.com/adamant-im/lisk-swift.git @@ -147,6 +172,7 @@ CHECKOUT OPTIONS: SPEC CHECKSUMS: Alamofire: 3ec537f71edc9804815215393ae2b1a8ea33a844 BigInt: 76b5dfdfa3e2e478d4ffdf161aeede5502e2742f + BitcoinKit: af68da8b2c9a23bc355c711fdc6bbd0abc8cc447 ByteBackpacker: df001da117faacdbf09b69a116ec480f6651c9ec CryptoSwift: 1c07ca50843dd48bc54e6ea53d7a4dba3b645716 DateToolsSwift: 875d97ff9e3a5d54abdd67a269b3f51c757b71ab @@ -154,6 +180,9 @@ SPEC CHECKSUMS: Eureka: 28ea296f06710f6745266b71f17862048941a32d FreakingSimpleRoundImageView: 0d687cb05da8684e85c4c2ae9945bafcbe89d2a2 FTIndicator: f7f071fd159e5befa1d040a9ef2e3ab53fa9322c + GRDB.swift: e10787d857b6b8135354b9e08fb23b4bb8e2fab7 + GRDBCipher: caccb02f13acd7f745e595cf84a70be3e91fb5f3 + GRKOpenSSLFramework: 8180d66833be66fc0f2d4942757d095edb0778d0 Haring: b3a8495c073a016f03296d67debc609a7ecf7710 KeychainAccess: b3816fddcf28aa29d94b10ec305cd52be14c472b KZFileWatchers: cdb033e4120fbe61c8e0f4df3613d5894741653d @@ -173,6 +202,7 @@ SPEC CHECKSUMS: scrypt: 3fe5b1a3b0976f97cd87488673a8f7c65708cc84 secp256k1_swift: 4fc5c4b2d2c6d21ee8ccb868cdc92da12f38bed9 SipHash: fad90a4683e420c52ef28063063dbbce248ea6d4 + SQLCipher: f9fcf29b2e59ced7defc2a2bdd0ebe79b40d4990 Stylist: 62917294a7b21ef61735a7067c3e9232d14ceb6f swift_qrcodejs: c181fe5c849d30c699546a23762d7e3dd143ab37 SwiftRLP: 98a02b2210128353ca02e4c2f4d83e2a9796db4f @@ -181,6 +211,6 @@ SPEC CHECKSUMS: web3swift: d80d1b9a3feca16e614acec9eea47ec26310d37d Yams: 572f625a8b719b73e0b57fd313c680f3e2161fe9 -PODFILE CHECKSUM: bca4bad85b3c921d691ed32e78fb34ad530f997f +PODFILE CHECKSUM: 378abf25974f82bedf445bb836328c373ef39582 -COCOAPODS: 1.6.0.beta.2 +COCOAPODS: 1.6.1 From 25f930f30bff03afe57b74a7768a8ca93a460c0f Mon Sep 17 00:00:00 2001 From: Anton B Date: Mon, 11 Mar 2019 16:53:39 +0300 Subject: [PATCH 02/46] Doge wallet balance --- Adamant.xcodeproj/project.pbxproj | 34 +++++ Adamant/AppDelegate.swift | 4 + Adamant/Wallets/Doge/DogeWalletService.swift | 139 ++++++++++++++++++- 3 files changed, 172 insertions(+), 5 deletions(-) diff --git a/Adamant.xcodeproj/project.pbxproj b/Adamant.xcodeproj/project.pbxproj index f9668ae6f..144bdae1a 100644 --- a/Adamant.xcodeproj/project.pbxproj +++ b/Adamant.xcodeproj/project.pbxproj @@ -52,6 +52,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 */; }; @@ -358,6 +362,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 = ""; }; @@ -676,6 +684,17 @@ path = TokensApiService; sourceTree = ""; }; + 64E1C82B222E958C006C4DA7 /* Doge */ = { + isa = PBXGroup; + children = ( + 64E1C82C222E95E2006C4DA7 /* DogeWalletRoutes.swift */, + 64E1C82E222E95F6006C4DA7 /* DogeWallet.swift */, + 64E1C830222E9617006C4DA7 /* DogeWalletService.swift */, + 64E1C832222EA0F0006C4DA7 /* DogeWalletViewController.swift */, + ); + path = Doge; + sourceTree = ""; + }; E59396A8E0053F21F768E69B /* Pods */ = { isa = PBXGroup; children = ( @@ -959,6 +978,7 @@ E94008902119D22400CD2D67 /* Adamant */, E94008792114ECF100CD2D67 /* Ethereum */, E94008812114EE3900CD2D67 /* Lisk */, + 64E1C82B222E958C006C4DA7 /* Doge */, E94008712114EACF00CD2D67 /* WalletAccount.swift */, E940086D2114AA2E00CD2D67 /* WalletService.swift */, E9B1AA582122D59600080A2A /* WalletsRoutes.swift */, @@ -1432,6 +1452,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", @@ -1439,6 +1460,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}/Haring/Haring.framework", "${BUILT_PRODUCTS_DIR}/KZFileWatchers/KZFileWatchers.framework", "${BUILT_PRODUCTS_DIR}/KeychainAccess/KeychainAccess.framework", @@ -1454,6 +1478,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}/Stylist/Stylist.framework", "${BUILT_PRODUCTS_DIR}/SwiftRLP/SwiftRLP.framework", @@ -1470,6 +1495,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", @@ -1477,6 +1503,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}/Haring.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/KZFileWatchers.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/KeychainAccess.framework", @@ -1492,6 +1521,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}/Stylist.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwiftRLP.framework", @@ -1565,6 +1595,7 @@ E908472A2196FEA80095825D /* RichMessageTransaction+CoreDataClass.swift in Sources */, 64A223D620F760BB005157CB /* Localization.swift in Sources */, E93E289121FE18B50044F7C7 /* ThemesManager.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 */, @@ -1657,6 +1688,7 @@ E93303DA22032CB00082F128 /* ThemeTableViewCell.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 */, @@ -1684,6 +1716,7 @@ 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 */, @@ -1775,6 +1808,7 @@ 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 */, E93303D72201C89F0082F128 /* ThemesViewController.swift in Sources */, E98FC34220F9209900032D65 /* UIColor+adamant.swift in Sources */, diff --git a/Adamant/AppDelegate.swift b/Adamant/AppDelegate.swift index fff66a2b5..7a37b1789 100644 --- a/Adamant/AppDelegate.swift +++ b/Adamant/AppDelegate.swift @@ -58,6 +58,10 @@ struct AdamantResources { static let lskServers = [ "https://lisknode1.adamant.im" ] + + static let dogeServers = [ + "https://dogenode1.adamant.im/api" + ] // MARK: ADAMANT Addresses static let supportEmail = "ios@adamant.im" diff --git a/Adamant/Wallets/Doge/DogeWalletService.swift b/Adamant/Wallets/Doge/DogeWalletService.swift index bb426188c..b2b03d103 100644 --- a/Adamant/Wallets/Doge/DogeWalletService.swift +++ b/Adamant/Wallets/Doge/DogeWalletService.swift @@ -8,9 +8,24 @@ import Foundation import Swinject +import Alamofire import BitcoinKit import BitcoinKit.Private +struct DogeApiCommands { + static func balance(address: String) -> String { + return "/addr/\(address)/balance" + } + + static func getTransactions(address: String) -> String { + return "/addrs/\(address)/txs" + } + + static func getTransaction(txId: String) -> String { + return "/tx/\(txId)" + } +} + class DogeWalletService: WalletService { var wallet: WalletAccount? { return dogeWallet } @@ -91,17 +106,62 @@ class DogeWalletService: WalletService { } func update() { - // Tooo + 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): + self?.dialogService.showRichError(error: error) + } + + self?.setState(.upToDate) + } } func validate(address: String) -> AddressValidationResult { // Todo return .valid } - - func getWalletAddress(byAdamantAddress address: String, completion: @escaping (WalletServiceResult) -> Void) { - // Todo - } } // MARK: - WalletInitiatedWithPassphrase @@ -187,6 +247,75 @@ extension DogeWalletService: InitiatedWithPassphraseService { } } +// 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 raw = AdamantResources.dogeServers.randomElement(), let url = URL(string: raw) else { + fatalError("Failed to build DOGE endpoint URL") + } + + guard let address = self.dogeWallet?.address else { + completion(.failure(error: .internalError(message: "DOGE Wallet: not found", error: nil))) + return + } + + // Headers + let headers = [ + "Content-Type": "application/json" + ] + + // Request url + let endpoint = url.appendingPathComponent(DogeApiCommands.balance(address: 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 / Decimal(DogeWalletService.multiplier) + completion(.success(result: balance)) + } else { + completion(.failure(error: .internalError(message: "DOGE Wallet: balance not found", error: nil))) + } + + 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: From fbe292d32616baddce8e6d629d43a87f816b8e67 Mon Sep 17 00:00:00 2001 From: Anton B Date: Tue, 12 Mar 2019 12:13:57 +0300 Subject: [PATCH 03/46] Doge transactions list and details --- Adamant.xcodeproj/project.pbxproj | 8 + Adamant/AppDelegate.swift | 2 + Adamant/Services/AdamantAccountService.swift | 2 +- ...DogeTransactionDetailsViewController.swift | 109 ++++++++++++ .../Doge/DogeTransactionsViewController.swift | 163 ++++++++++++++++++ Adamant/Wallets/Doge/DogeWalletRoutes.swift | 15 ++ Adamant/Wallets/Doge/DogeWalletService.swift | 146 +++++++++------- 7 files changed, 385 insertions(+), 60 deletions(-) create mode 100644 Adamant/Wallets/Doge/DogeTransactionDetailsViewController.swift create mode 100644 Adamant/Wallets/Doge/DogeTransactionsViewController.swift diff --git a/Adamant.xcodeproj/project.pbxproj b/Adamant.xcodeproj/project.pbxproj index 144bdae1a..ffa0e0c15 100644 --- a/Adamant.xcodeproj/project.pbxproj +++ b/Adamant.xcodeproj/project.pbxproj @@ -40,6 +40,8 @@ 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 */; }; 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 */; }; @@ -350,6 +352,8 @@ 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 = ""; }; 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 = ""; }; @@ -691,6 +695,8 @@ 64E1C82E222E95F6006C4DA7 /* DogeWallet.swift */, 64E1C830222E9617006C4DA7 /* DogeWalletService.swift */, 64E1C832222EA0F0006C4DA7 /* DogeWalletViewController.swift */, + 648DD79D2236A0B500B811FD /* DogeTransactionsViewController.swift */, + 648DD79F2236A59200B811FD /* DogeTransactionDetailsViewController.swift */, ); path = Doge; sourceTree = ""; @@ -1627,6 +1633,7 @@ E9960B3321F5154300C840A8 /* BaseAccount+CoreDataClass.swift in Sources */, E9FCA1E6218334C00005E83D /* SimpleTransactionDetails.swift in Sources */, 64A223DA20F7A14B005157CB /* AdamantLskApiService.swift in Sources */, + 648DD7A02236A59200B811FD /* DogeTransactionDetailsViewController.swift in Sources */, 6455E9F321075D8000B2E94C /* AdamantAddressBookService.swift in Sources */, E9204B5020C94C4A00F3B9AB /* Date+humanizedString.swift in Sources */, E9E7CD9120026FA100DFC4DB /* SwinjectDependencies.swift in Sources */, @@ -1713,6 +1720,7 @@ 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 */, diff --git a/Adamant/AppDelegate.swift b/Adamant/AppDelegate.swift index 7a37b1789..c3265af25 100644 --- a/Adamant/AppDelegate.swift +++ b/Adamant/AppDelegate.swift @@ -91,6 +91,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/Services/AdamantAccountService.swift b/Adamant/Services/AdamantAccountService.swift index 139ebc55c..0656edb56 100644 --- a/Adamant/Services/AdamantAccountService.swift +++ b/Adamant/Services/AdamantAccountService.swift @@ -89,7 +89,7 @@ class AdamantAccountService: AccountService { AdmWalletService(), EthWalletService(), LskWalletService(mainnet: true, origins: AdamantResources.lskServers), - DogeWalletService(mainnet: true) + DogeWalletService() // Testnet // LskWalletService(mainnet: false) diff --git a/Adamant/Wallets/Doge/DogeTransactionDetailsViewController.swift b/Adamant/Wallets/Doge/DogeTransactionDetailsViewController.swift new file mode 100644 index 000000000..c5e3bc75e --- /dev/null +++ b/Adamant/Wallets/Doge/DogeTransactionDetailsViewController.swift @@ -0,0 +1,109 @@ +// +// DogeTransactionDetailsViewController.swift +// Adamant +// +// Created by Anton Boyarkin on 11/03/2019. +// Copyright © 2019 Adamant. All rights reserved. +// + +import UIKit + +class DogeTransactionDetailsViewController: TransactionDetailsViewControllerBase { + // MARK: - Dependencies + + weak var service: DogeWalletService? + + // MARK: - Properties + + 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 + } + + 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() { + guard let id = transaction?.txId, let service = service else { + refreshControl.endRefreshing() + return + } + + service.getTransaction(by: id) { [weak self] result in + switch result { + case .success(let trs): + self?.transaction = trs + + DispatchQueue.main.async { + self?.tableView.reloadData() + self?.refreshControl.endRefreshing() + } + + case .failure(let error): + self?.dialogService.showRichError(error: error) + + DispatchQueue.main.async { + self?.refreshControl.endRefreshing() + } + } + } + } + + // MARK: - Autoupdate + + func startUpdate() { + timer?.invalidate() + timer = Timer.scheduledTimer(withTimeInterval: autoupdateInterval, repeats: true) { [weak self] _ in + guard let id = self?.transaction?.txId, let service = self?.service else { + return + } + + service.getTransaction(by: id) { result in + switch result { + case .success(let trs): + self?.transaction = trs + + DispatchQueue.main.async { + self?.tableView.reloadData() + } + + case .failure: + break + } + } + } + } + + func stopUpdate() { + timer?.invalidate() + } +} diff --git a/Adamant/Wallets/Doge/DogeTransactionsViewController.swift b/Adamant/Wallets/Doge/DogeTransactionsViewController.swift new file mode 100644 index 000000000..7fa689e43 --- /dev/null +++ b/Adamant/Wallets/Doge/DogeTransactionsViewController.swift @@ -0,0 +1,163 @@ +// +// DogeTransactionsViewController.swift +// Adamant +// +// Created by Anton Boyarkin on 11/03/2019. +// Copyright © 2019 Adamant. All rights reserved. +// + +import UIKit + +class DogeTransactionsViewController: TransactionsListViewControllerBase { + + // MARK: - Dependencies + var walletService: DogeWalletService! + var dialogService: DialogService! + var router: Router! + + // MARK: - Properties + var transactions: [DogeTransaction] = [] + + override func viewDidLoad() { + super.viewDidLoad() + + self.refreshControl.beginRefreshing() + + currencySymbol = DogeWalletService.currencySymbol + + handleRefresh(self.refreshControl) + } + + override func handleRefresh(_ refreshControl: UIRefreshControl) { + self.walletService.getTransactions({ (result) in + switch result { + case .success(let transactions): + self.transactions = transactions + DispatchQueue.main.async { + self.tableView.reloadData() + } + break + case .failure(let error): + if case .internalError(let message, _ ) = error { + let localizedErrorMessage = NSLocalizedString(message, comment: "TransactionList: 'Transactions not found' message.") + self.dialogService.showWarning(withMessage: localizedErrorMessage) + } else { + self.dialogService.showError(withMessage: String.adamantLocalized.transactionList.notFound, error: error) + } + break + } + DispatchQueue.main.async { + self.refreshControl.endRefreshing() + } + }) + } + + + // MARK: - UITableView + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return transactions.count + } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) + + let transaction = transactions[indexPath.row] + + guard let controller = router.get(scene: AdamantScene.Wallets.Doge.transactionDetails) as? DogeTransactionDetailsViewController else { + return + } + + controller.transaction = transaction + controller.service = walletService + + if let address = walletService.wallet?.address { + if transaction.senderAddress.caseInsensitiveCompare(address) == .orderedSame { + controller.senderName = String.adamantLocalized.transactionDetails.yourAddress + } else if transaction.recipientAddress.caseInsensitiveCompare(address) == .orderedSame { + controller.recipientName = String.adamantLocalized.transactionDetails.yourAddress + } + } + + navigationController?.pushViewController(controller, animated: true) + } + + 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 = isOutgoing(transaction) + let partnerId = outgoing ? transaction.recipientAddress : transaction.senderAddress + + configureCell(cell, + isOutgoing: outgoing, + partnerId: partnerId ?? "", + partnerName: nil, + amount: transaction.amountValue, + date: transaction.dateValue) + } + + private func isOutgoing(_ transaction: DogeTransaction) -> Bool { + return transaction.senderAddress.lowercased() == walletService.wallet?.address.lowercased() + } +} + +class DogeTransaction: TransactionDetails { + var txId: String = "" + var senderAddress: String = "" + var recipientAddress: String = "" + var dateValue: Date? + var amountValue: Decimal = 0 + var feeValue: Decimal? + var confirmationsValue: String? + var blockValue: String? = nil + var isOutgoing: Bool = false + var transactionStatus: TransactionStatus? + + static func from(_ dictionry: [String: Any], with walletAddress: String) -> DogeTransaction { + let transaction = DogeTransaction() + + if let txid = dictionry["txid"] as? String { transaction.txId = txid } + if let vin = dictionry["vin"] as? [[String: Any]], let input = vin.first, let address = input["addr"] as? String { + transaction.senderAddress = address + if address == walletAddress { + transaction.isOutgoing = true + } + } + if let vout = dictionry["vout"] as? [[String: Any]] { + let outputs = vout.filter { item -> Bool in + if let publickKey = item["scriptPubKey"] as? [String: Any], let addresses = publickKey["addresses"] as? [String], let address = addresses.first { + if transaction.isOutgoing, address != walletAddress { + return true + } else if !transaction.isOutgoing, address == walletAddress { + return true + } + } + return false + } + if let output = outputs.first, let publickKey = output["scriptPubKey"] as? [String: Any], let addresses = publickKey["addresses"] as? [String], let address = addresses.first, let valueRaw = output["value"] as? String, let value = Decimal(string: valueRaw) { + transaction.recipientAddress = address + transaction.amountValue = value + } + } + if let time = dictionry["time"] as? NSNumber { transaction.dateValue = Date(timeIntervalSince1970: time.doubleValue) } + if let fees = dictionry["fees"] as? NSNumber { transaction.feeValue = fees.decimalValue } + if let confirmations = dictionry["confirmations"] as? NSNumber { transaction.confirmationsValue = confirmations.stringValue } + if let blockhash = dictionry["blockhash"] as? String { transaction.blockValue = blockhash } + + transaction.transactionStatus = TransactionStatus.success + + return transaction + } +} diff --git a/Adamant/Wallets/Doge/DogeWalletRoutes.swift b/Adamant/Wallets/Doge/DogeWalletRoutes.swift index bd42c0935..307c17b72 100644 --- a/Adamant/Wallets/Doge/DogeWalletRoutes.swift +++ b/Adamant/Wallets/Doge/DogeWalletRoutes.swift @@ -16,5 +16,20 @@ extension AdamantScene.Wallets { c.dialogService = r.resolve(DialogService.self) return c } + + /// List of Lisk 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 + } + + /// Lisk 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.swift b/Adamant/Wallets/Doge/DogeWalletService.swift index b2b03d103..2b90120be 100644 --- a/Adamant/Wallets/Doge/DogeWalletService.swift +++ b/Adamant/Wallets/Doge/DogeWalletService.swift @@ -99,8 +99,8 @@ class DogeWalletService: WalletService { } } - init(mainnet: Bool) { - self.network = mainnet ? DogeMainnet() : DogeTestnet() + init() { + self.network = DogeMainnet() self.setState(.notInitiated) } @@ -379,60 +379,93 @@ extension DogeWalletService { } } -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 +// MARK: - Transactions +extension DogeWalletService { + func getTransactions(_ completion: @escaping (ApiServiceResult<[DogeTransaction]>) -> Void) { + guard let raw = AdamantResources.dogeServers.randomElement(), let url = URL(string: raw) else { + fatalError("Failed to build DOGE endpoint URL") + } + + if let address = self.wallet?.address { + // Headers + let headers = [ + "Content-Type": "application/json" + ] + + // Request url + let endpoint = url.appendingPathComponent(DogeApiCommands.getTransactions(address: address)) + + // MARK: Sending request + Alamofire.request(endpoint, method: .get, headers: headers).responseJSON(queue: defaultDispatchQueue) { response in + + switch response.result { + case .success(let data): + + if let result = data as? [String: Any], let items = result["items"] as? [[String: Any]] { + var transactions = [DogeTransaction]() + for item in items { + transactions.append(DogeTransaction.from(item, with: address)) + } + completion(.success(transactions)) + } else { + completion(.failure(.internalError(message: "DOGE Wallet: not valid response", error: nil))) + } + + case .failure: + completion(.failure(.internalError(message: "DOGE Wallet: server not response", error: nil))) + } + } + } else { + completion(.failure(.internalError(message: "DOGE Wallet: not found", error: nil))) + } } - override var port: UInt32 { - return 22556 + func getTransaction(by hash: String, completion: @escaping (ApiServiceResult) -> Void) { + guard let raw = AdamantResources.dogeServers.randomElement(), let url = URL(string: raw) else { + fatalError("Failed to build DOGE endpoint URL") + } + + if let address = self.wallet?.address { + // Headers + let headers = [ + "Content-Type": "application/json" + ] + + // Request url + let endpoint = url.appendingPathComponent(DogeApiCommands.getTransaction(txId: hash)) + + // MARK: Sending request + Alamofire.request(endpoint, method: .get, headers: headers).responseJSON(queue: defaultDispatchQueue) { response in + + switch response.result { + case .success(let data): + if let item = data as? [String: Any] { + completion(.success(DogeTransaction.from(item, with: address))) + } else { + completion(.failure(.internalError(message: "No transaction", error: nil))) + } + case .failure: + completion(.failure(.internalError(message: "No transaction", error: nil))) + } + } + } else { + completion(.failure(.internalError(message: "DOGE Wallet: not found", error: nil))) + } } - - override var dnsSeeds: [String] { - return [ - "seed.dogecoin.com", - "seed.multidoge.org", - "seed2.multidoge.org", - "seed.doger.dogecoin.com" - ] +} + +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 } - - // todo hashGenesisBlock = "1a91e3dace36e2be3bf030a65679fe821aa1d6ef92e7c9902eb318182c355691" } -class DogeTestnet: Network { +class DogeMainnet: Network { override var name: String { return "livenet" } @@ -450,15 +483,15 @@ class DogeTestnet: Network { } override var pubkeyhash: UInt8 { - return 0x71 + return 0x1e } override var privatekey: UInt8 { - return 0xf1 + return 0x9e } override var scripthash: UInt8 { - return 0xc4 + return 0x16 } override var xpubkey: UInt32 { @@ -475,12 +508,7 @@ class DogeTestnet: Network { override var dnsSeeds: [String] { return [ - // "seed.dogecoin.com", - // "seed.multidoge.org", - // "seed2.multidoge.org", - // "seed.doger.dogecoin.com" + "dogenode1.adamant.im" ] } - - // todo hashGenesisBlock = "" } From 769fab064589d128a05519af93f10c35e2736fc4 Mon Sep 17 00:00:00 2001 From: Anton B Date: Thu, 14 Mar 2019 16:38:07 +0300 Subject: [PATCH 04/46] Tansfer DOGE via chat --- Adamant.xcodeproj/project.pbxproj | 20 ++ Adamant/Models/DogeTransaction.swift | 58 ++++ .../Doge/DogeTransactionsViewController.swift | 49 ---- .../Doge/DogeTransferViewController.swift | 223 ++++++++++++++ Adamant/Wallets/Doge/DogeWalletRoutes.swift | 15 +- ...ogeWalletService+RichMessageProvider.swift | 178 ++++++++++++ ...e+RichMessageProviderWithStatusCheck.swift | 101 +++++++ .../Wallets/Doge/DogeWalletService+Send.swift | 271 ++++++++++++++++++ Adamant/Wallets/Doge/DogeWalletService.swift | 119 +++++++- 9 files changed, 979 insertions(+), 55 deletions(-) create mode 100644 Adamant/Models/DogeTransaction.swift create mode 100644 Adamant/Wallets/Doge/DogeTransferViewController.swift create mode 100644 Adamant/Wallets/Doge/DogeWalletService+RichMessageProvider.swift create mode 100644 Adamant/Wallets/Doge/DogeWalletService+RichMessageProviderWithStatusCheck.swift create mode 100644 Adamant/Wallets/Doge/DogeWalletService+Send.swift diff --git a/Adamant.xcodeproj/project.pbxproj b/Adamant.xcodeproj/project.pbxproj index ffa0e0c15..7f00da47b 100644 --- a/Adamant.xcodeproj/project.pbxproj +++ b/Adamant.xcodeproj/project.pbxproj @@ -42,6 +42,11 @@ 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 */; }; @@ -354,6 +359,11 @@ 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 = ""; }; @@ -694,7 +704,11 @@ 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 */, ); @@ -826,6 +840,7 @@ E993302321369B8A00CD5200 /* RichMessage.swift */, E971591921681D6900A5F904 /* TransactionStatus.swift */, E9FCA1E5218334C00005E83D /* SimpleTransactionDetails.swift */, + 648DD7A12237D9A000B811FD /* DogeTransaction.swift */, ); path = Models; sourceTree = ""; @@ -1607,6 +1622,7 @@ 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 */, @@ -1641,6 +1657,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 */, @@ -1664,6 +1681,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 */, @@ -1708,6 +1726,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 */, @@ -1828,6 +1847,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 */, diff --git a/Adamant/Models/DogeTransaction.swift b/Adamant/Models/DogeTransaction.swift new file mode 100644 index 000000000..23fc2ff68 --- /dev/null +++ b/Adamant/Models/DogeTransaction.swift @@ -0,0 +1,58 @@ +// +// DogeTransaction.swift +// Adamant +// +// Created by Anton Boyarkin on 12/03/2019. +// Copyright © 2019 Adamant. All rights reserved. +// + +import Foundation + +class DogeTransaction: TransactionDetails { + var txId: String = "" + var senderAddress: String = "" + var recipientAddress: String = "" + var dateValue: Date? + var amountValue: Decimal = 0 + var feeValue: Decimal? + var confirmationsValue: String? + var blockValue: String? = nil + var isOutgoing: Bool = false + var transactionStatus: TransactionStatus? + + static func from(_ dictionry: [String: Any], with walletAddress: String) -> DogeTransaction { + let transaction = DogeTransaction() + + if let txid = dictionry["txid"] as? String { transaction.txId = txid } + if let vin = dictionry["vin"] as? [[String: Any]], let input = vin.first, let address = input["addr"] as? String { + transaction.senderAddress = address + if address == walletAddress { + transaction.isOutgoing = true + } + } + if let vout = dictionry["vout"] as? [[String: Any]] { + let outputs = vout.filter { item -> Bool in + if let publickKey = item["scriptPubKey"] as? [String: Any], let addresses = publickKey["addresses"] as? [String], let address = addresses.first { + if transaction.isOutgoing, address != walletAddress { + return true + } else if !transaction.isOutgoing, address == walletAddress { + return true + } + } + return false + } + if let output = outputs.first, let publickKey = output["scriptPubKey"] as? [String: Any], let addresses = publickKey["addresses"] as? [String], let address = addresses.first, let valueRaw = output["value"] as? String, let value = Decimal(string: valueRaw) { + transaction.recipientAddress = address + transaction.amountValue = value + } + } + if let time = dictionry["time"] as? NSNumber { transaction.dateValue = Date(timeIntervalSince1970: time.doubleValue) } + if let fees = dictionry["fees"] as? NSNumber { transaction.feeValue = fees.decimalValue } + if let confirmations = dictionry["confirmations"] as? NSNumber { transaction.confirmationsValue = confirmations.stringValue } + if let blockhash = dictionry["blockhash"] as? String { transaction.blockValue = blockhash } + + transaction.transactionStatus = TransactionStatus.success + + return transaction + } +} diff --git a/Adamant/Wallets/Doge/DogeTransactionsViewController.swift b/Adamant/Wallets/Doge/DogeTransactionsViewController.swift index 7fa689e43..3fa145fa9 100644 --- a/Adamant/Wallets/Doge/DogeTransactionsViewController.swift +++ b/Adamant/Wallets/Doge/DogeTransactionsViewController.swift @@ -112,52 +112,3 @@ class DogeTransactionsViewController: TransactionsListViewControllerBase { return transaction.senderAddress.lowercased() == walletService.wallet?.address.lowercased() } } - -class DogeTransaction: TransactionDetails { - var txId: String = "" - var senderAddress: String = "" - var recipientAddress: String = "" - var dateValue: Date? - var amountValue: Decimal = 0 - var feeValue: Decimal? - var confirmationsValue: String? - var blockValue: String? = nil - var isOutgoing: Bool = false - var transactionStatus: TransactionStatus? - - static func from(_ dictionry: [String: Any], with walletAddress: String) -> DogeTransaction { - let transaction = DogeTransaction() - - if let txid = dictionry["txid"] as? String { transaction.txId = txid } - if let vin = dictionry["vin"] as? [[String: Any]], let input = vin.first, let address = input["addr"] as? String { - transaction.senderAddress = address - if address == walletAddress { - transaction.isOutgoing = true - } - } - if let vout = dictionry["vout"] as? [[String: Any]] { - let outputs = vout.filter { item -> Bool in - if let publickKey = item["scriptPubKey"] as? [String: Any], let addresses = publickKey["addresses"] as? [String], let address = addresses.first { - if transaction.isOutgoing, address != walletAddress { - return true - } else if !transaction.isOutgoing, address == walletAddress { - return true - } - } - return false - } - if let output = outputs.first, let publickKey = output["scriptPubKey"] as? [String: Any], let addresses = publickKey["addresses"] as? [String], let address = addresses.first, let valueRaw = output["value"] as? String, let value = Decimal(string: valueRaw) { - transaction.recipientAddress = address - transaction.amountValue = value - } - } - if let time = dictionry["time"] as? NSNumber { transaction.dateValue = Date(timeIntervalSince1970: time.doubleValue) } - if let fees = dictionry["fees"] as? NSNumber { transaction.feeValue = fees.decimalValue } - if let confirmations = dictionry["confirmations"] as? NSNumber { transaction.confirmationsValue = confirmations.stringValue } - if let blockhash = dictionry["blockhash"] as? String { transaction.blockValue = blockhash } - - transaction.transactionStatus = TransactionStatus.success - - return transaction - } -} diff --git a/Adamant/Wallets/Doge/DogeTransferViewController.swift b/Adamant/Wallets/Doge/DogeTransferViewController.swift new file mode 100644 index 000000000..b6ede386a --- /dev/null +++ b/Adamant/Wallets/Doge/DogeTransferViewController.swift @@ -0,0 +1,223 @@ +// +// DogeTransferViewController.swift +// Adamant +// +// Created by Anton Boyarkin on 12/03/2019. +// Copyright © 2019 Adamant. All rights reserved. +// + +import UIKit +import Eureka + +class DogeTransferViewController: TransferViewControllerBase { + + // MARK: Dependencies + + var chatsProvider: ChatsProvider! + + + // MARK: Properties + + override var balanceFormatter: NumberFormatter { + if let service = service { + return AdamantBalanceFormat.currencyFormatter(for: .full, currencySymbol: type(of: service).currencySymbol) + } else { + return AdamantBalanceFormat.currencyFormatterFull + } + } + + private var skipValueChange: Bool = false + + static let invalidCharacters: CharacterSet = CharacterSet.decimalDigits.inverted + + + // MARK: Send + + override func sendFunds() { + let comments: String + if let row: TextAreaRow = form.rowBy(tag: BaseRows.comments.tag), let text = row.value { + comments = text + } else { + comments = "" + } + + guard let service = service as? DogeWalletService, let recipient = recipientAddress, let amount = amount else { + return + } + + guard let dialogService = dialogService else { + return + } + + dialogService.showProgress(withMessage: String.adamantLocalized.transfer.transferProcessingMessage, userInteractionEnable: false) + + service.createTransaction(recipient: recipient, amount: amount) { [weak self] result in + guard let vc = self else { + dialogService.dismissProgress() + dialogService.showError(withMessage: String.adamantLocalized.sharedErrors.unknownError, error: nil) + return + } + + switch result { + case .success(let transaction): + // MARK: 1. Send adm report + if let reportRecipient = vc.admReportRecipient, let hash = transaction.txHash { + self?.reportTransferTo(admAddress: reportRecipient, amount: amount, comments: comments, hash: hash) + } + + // MARK: 2. Send transaction + service.sendTransaction(transaction) { result in + switch result { + case .success(let hash): + service.update() + + service.getTransaction(by: hash) { result in + switch result { + case .success(let transaction): + vc.dialogService.showSuccess(withMessage: String.adamantLocalized.transfer.transferSuccess) + + if let detailsVc = vc.router.get(scene: AdamantScene.Wallets.Doge.transactionDetails) as? DogeTransactionDetailsViewController { + 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) + } else { + vc.delegate?.transferViewController(vc, didFinishWithTransfer: transaction, detailsViewController: nil) + } + + case .failure(let error): + if case let .internalError(message, _) = error, message == "No transaction" { + vc.dialogService.showSuccess(withMessage: String.adamantLocalized.transfer.transferSuccess) + if let detailsVc = vc.router.get(scene: AdamantScene.Wallets.Doge.transactionDetails) as? DogeTransactionDetailsViewController { + 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) + } else { + vc.delegate?.transferViewController(vc, didFinishWithTransfer: transaction, detailsViewController: nil) + } + } else { + vc.dialogService.showRichError(error: error) + vc.delegate?.transferViewController(vc, didFinishWithTransfer: nil, detailsViewController: nil) + } + } + } + + case .failure(let error): + vc.dialogService.showRichError(error: error) + } + } + + case .failure(let error): + dialogService.dismissProgress() + 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 = SuffixTextRow() { + $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 + } + } + + 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/DogeWalletRoutes.swift b/Adamant/Wallets/Doge/DogeWalletRoutes.swift index 307c17b72..75b621e39 100644 --- a/Adamant/Wallets/Doge/DogeWalletRoutes.swift +++ b/Adamant/Wallets/Doge/DogeWalletRoutes.swift @@ -17,7 +17,18 @@ extension AdamantScene.Wallets { return c } - /// List of Lisk transactions + /// 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) @@ -25,7 +36,7 @@ extension AdamantScene.Wallets { return c } - /// Lisk transaction details + /// Transaction details static let transactionDetails = AdamantScene(identifier: "TransactionDetailsViewControllerBase") { r in let c = DogeTransactionDetailsViewController() c.dialogService = r.resolve(DialogService.self) diff --git a/Adamant/Wallets/Doge/DogeWalletService+RichMessageProvider.swift b/Adamant/Wallets/Doge/DogeWalletService+RichMessageProvider.swift new file mode 100644 index 000000000..eba933266 --- /dev/null +++ b/Adamant/Wallets/Doge/DogeWalletService+RichMessageProvider.swift @@ -0,0 +1,178 @@ +// +// 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 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: 1. Sender & recipient names + + let senderName: String? + let recipientName: String? + + if let address = accountService.account?.address { + if let senderId = transaction.senderId, senderId.caseInsensitiveCompare(address) == .orderedSame { + senderName = String.adamantLocalized.transactionDetails.yourAddress + } else { + senderName = transaction.chatroom?.partner?.name + } + + if let recipientId = transaction.recipientId, recipientId.caseInsensitiveCompare(address) == .orderedSame { + recipientName = String.adamantLocalized.transactionDetails.yourAddress + } else { + recipientName = transaction.chatroom?.partner?.name + } + } else if let partner = transaction.chatroom?.partner, let id = partner.address { + if transaction.senderId == id { + senderName = partner.name + recipientName = nil + } else { + recipientName = partner.name + senderName = nil + } + } else { + senderName = nil + recipientName = nil + } + + // MARK: 2. Go go transaction + + getTransaction(by: hash) { [weak self] result in + dialogService.dismissProgress() + guard let vc = self?.router.get(scene: AdamantScene.Wallets.Doge.transactionDetails) as? DogeTransactionDetailsViewController else { + return + } + + vc.service = self + vc.senderName = senderName + vc.recipientName = recipientName + vc.comment = comment + + switch result { + case .success(let transaction): + vc.transaction = transaction + + 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 + + default: + self?.dialogService.showRichError(error: error) + return + } + break + } + + DispatchQueue.main.async { + chat.navigationController?.pushViewController(vc, animated: true) + } + } + } + + // 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..ab2ab2c6f --- /dev/null +++ b/Adamant/Wallets/Doge/DogeWalletService+RichMessageProviderWithStatusCheck.swift @@ -0,0 +1,101 @@ +// +// 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 + } + + getTransaction(by: hash) { result in + switch result { + case .success(let dogeTransaction): + // MARK: Check status + guard let status = dogeTransaction.transactionStatus else { + completion(.failure(error: WalletServiceError.internalError(message: "Failed to get transaction", error: nil))) + return + } + + guard status == .success else { + completion(.success(result: status)) + return + } + + // MARK: Check address + if transaction.isOutgoing { + guard dogeTransaction.senderAddress == self.dogeWallet?.address else { + completion(.success(result: .warning)) + return + } + } else { + guard dogeTransaction.recipientAddress == self.dogeWallet?.address else { + completion(.success(result: .warning)) + return + } + } + + + + guard let sentDate = dogeTransaction.dateValue else { + let timeAgo = -1 * date.timeIntervalSinceNow + + let result: TransactionStatus + if timeAgo > 60 * 60 * 3 { + // 3h waiting for pending status + result = .failed + } else { + // Note: No info about processing transactions + result = .pending + } + completion(.success(result: result)) + return + } + + // MARK: Check date + let start = date.addingTimeInterval(-60 * 5) + let end = date.addingTimeInterval(60 * 5) + let range = start...end + + guard range.contains(sentDate) else { + completion(.success(result: .warning)) + return + } + + // MARK: Check amount + if let raw = transaction.richContent?[RichContentKeys.transfer.amount], let reported = AdamantBalanceFormat.deserializeBalance(from: raw) { + guard reported == dogeTransaction.amountValue else { + completion(.success(result: .warning)) + return + } + } + + completion(.success(result: .success)) + + case .failure(let error): + if case let .internalError(message, _) = error, message == "No transaction" { + let timeAgo = -1 * date.timeIntervalSinceNow + + let result: TransactionStatus + if timeAgo > 60 * 60 * 3 { + // 3h 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..9a649ab0a --- /dev/null +++ b/Adamant/Wallets/Doge/DogeWalletService+Send.swift @@ -0,0 +1,271 @@ +// +// 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 rawAwount = NSDecimalNumber(decimal: amount * Decimal(DogeWalletService.multiplier)).int64Value + + // MARK: Go background + defaultDispatchQueue.async { + // MARK: 2. Search for unspent transactions + self.getUnspentTransactions({ result in + switch result { + case .success(let utxos): + // MARK: 3. Create local transaction + let unsignedTx = self.createUnsignedTx(toAddress: toAddress, amount: rawAwount, changeAddress: changeAddress, utxos: utxos, lockTime: 0) + let signedTransaction = self.signTx(unsignedTx: unsignedTx, keys: [key]) + completion(.success(result: signedTransaction)) + break + case .failure(let error): + completion(.failure(error: .notEnoughMoney)) + break + } + }) + } + } + + func sendTransaction(_ transaction: BitcoinKit.Transaction, completion: @escaping (WalletServiceResult) -> Void) { + guard let raw = AdamantResources.dogeServers.randomElement(), let url = URL(string: raw) else { + fatalError("Failed to build DOGE endpoint URL") + } + + // Headers + let headers = [ + "Content-Type": "application/json" + ] + + // Request url + let endpoint = url.appendingPathComponent(DogeApiCommands.sendTransaction()) + + defaultDispatchQueue.async { + // 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: self.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: + completion(.failure(error: .internalError(message: "DOGE Wallet: server not response", error: nil))) + } + } + } + } + + // TODO: select utxos and decide fee + public func selectTx(from utxos: [UnspentTransaction], amount: Int64) -> (utxos: [UnspentTransaction], fee: Int64) { + return (utxos, NSDecimalNumber(decimal: self.transactionFee * Decimal(DogeWalletService.multiplier)).int64Value) + } + + public func createUnsignedTx(toAddress: Address, amount: Int64, changeAddress: Address, utxos: [UnspentTransaction], lockTime: UInt32 = 0) -> UnsignedTransaction { + let (utxos, fee) = selectTx(from: utxos, amount: amount) + let totalAmount: Int64 = Int64(utxos.reduce(0) { $0 + $1.output.value }) + let change: Int64 = totalAmount - amount - fee + + let toPubKeyHash: Data = toAddress.data + let changePubkeyHash: Data = changeAddress.data + + let lockingScriptTo = Script.buildPublicKeyHashOut(pubKeyHash: toPubKeyHash) + let lockingScriptChange = Script.buildPublicKeyHashOut(pubKeyHash: changePubkeyHash) + + let toOutput = TransactionOutput(value: UInt64(amount), lockingScript: lockingScriptTo) + let changeOutput = TransactionOutput(value: UInt64(change), lockingScript: lockingScriptChange) + + let unsignedInputs = utxos.map { TransactionInput(previousOutput: $0.outpoint, signatureScript: Data(), sequence: UInt32.max) } + let tx = BitcoinKit.Transaction(version: 1, inputs: unsignedInputs, outputs: [toOutput, changeOutput], lockTime: lockTime) + return UnsignedTransaction(tx: tx, utxos: utxos) + } + + public func signTx(unsignedTx: UnsignedTransaction, keys: [PrivateKey]) -> BitcoinKit.Transaction { + var inputsToSign = unsignedTx.tx.inputs + var transactionToSign: BitcoinKit.Transaction { + return BitcoinKit.Transaction(version: unsignedTx.tx.version, inputs: inputsToSign, outputs: unsignedTx.tx.outputs, lockTime: unsignedTx.tx.lockTime) + } + + // Signing + let hashType = SighashType.BTC.ALL + for (i, utxo) in unsignedTx.utxos.enumerated() { + let pubkeyHash: Data = Script.getPublicKeyHash(from: utxo.output.lockingScript) + + let keysOfUtxo: [PrivateKey] = keys.filter { $0.publicKey().pubkeyHash == pubkeyHash } + guard let key = keysOfUtxo.first else { + print("No keys to this txout : \(utxo.output.value)") + continue + } + print("Value of signing txout : \(utxo.output.value)") + + let sighash: Data = transactionToSign.signatureHash(for: utxo.output, inputIndex: i, hashType: SighashType.BTC.ALL) + let signature: Data = try! BitcoinKit.Crypto.sign(sighash, privateKey: key) + let txin = inputsToSign[i] + let pubkey = key.publicKey() + + let unlockingScript = Script.buildPublicKeyUnlockingScript(signature: signature, pubkey: pubkey, hashType: hashType) + + inputsToSign[i] = TransactionInput(previousOutput: txin.previousOutput, signatureScript: unlockingScript, sequence: txin.sequence) + } + return transactionToSign + } +} + +extension BitcoinKit.Transaction: TransactionDetails { + 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 "" + } +} + +public protocol BinaryConvertible { + static func +(lhs: Data, rhs: Self) -> Data + static func +=(lhs: inout Data, rhs: Self) +} + +public extension BinaryConvertible { + public static func +(lhs: Data, rhs: Self) -> Data { + var value = rhs + let data = Data(buffer: UnsafeBufferPointer(start: &value, count: 1)) + return lhs + data + } + + public static func +=(lhs: inout Data, rhs: Self) { + lhs = lhs + rhs + } +} + +extension UInt8 : BinaryConvertible {} +extension UInt16 : BinaryConvertible {} +extension UInt32 : BinaryConvertible {} +extension UInt64 : BinaryConvertible {} +extension Int8 : BinaryConvertible {} +extension Int16 : BinaryConvertible {} +extension Int32 : BinaryConvertible {} +extension Int64 : BinaryConvertible {} +extension Int : BinaryConvertible {} + +extension Bool : BinaryConvertible { + public static func +(lhs: Data, rhs: Bool) -> Data { + return lhs + (rhs ? UInt8(0x01) : UInt8(0x00)).littleEndian + } +} + +extension String : BinaryConvertible { + public static func +(lhs: Data, rhs: String) -> Data { + guard let data = rhs.data(using: .ascii) else { return lhs} + return lhs + data + } +} + +extension Data : BinaryConvertible { + public static func +(lhs: Data, rhs: Data) -> Data { + var data = Data() + data.append(lhs) + data.append(rhs) + return data + } +} + +enum SignError: Error { + case noPreviousOutput + case noPreviousOutputAddress + case noPrivateKey +} + +enum SerializationError: Error { + case noPreviousOutput + case noPreviousTransaction +} diff --git a/Adamant/Wallets/Doge/DogeWalletService.swift b/Adamant/Wallets/Doge/DogeWalletService.swift index 2b90120be..93138bd51 100644 --- a/Adamant/Wallets/Doge/DogeWalletService.swift +++ b/Adamant/Wallets/Doge/DogeWalletService.swift @@ -24,6 +24,14 @@ struct DogeApiCommands { static func getTransaction(txId: String) -> String { return "/tx/\(txId)" } + + static func getUnspentTransactions(address: String) -> String { + return "/addr/\(address)/utxo" + } + + static func sendTransaction() -> String { + return "/tx/send" + } } class DogeWalletService: WalletService { @@ -57,7 +65,7 @@ class DogeWalletService: WalletService { static let multiplier = 1e8 static let chunkSize = 20 - private (set) var transactionFee: Decimal = 1 // 1 DOGE per transaction + private (set) var transactionFee: Decimal = 1.0 // 1 DOGE per transaction static let kvsAddress = "doge:address" @@ -159,8 +167,48 @@ class DogeWalletService: WalletService { } func validate(address: String) -> AddressValidationResult { - // Todo - return .valid + return isValid(bitcoinAddress: address) ? .valid : .invalid + } + + private func getBase58DecodeAsBytes(address: String, length: Int) -> [UTF8.CodeUnit]? { + let b58Chars = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" + + var output: [UTF8.CodeUnit] = Array(repeating: 0, count: length) + + for i in 0.. Bool { + guard address.count >= 26 && address.count <= 35, + address.rangeOfCharacter(from: CharacterSet.alphanumerics.inverted) == nil, + let decodedAddress = getBase58DecodeAsBytes(address: address, length: 25), + decodedAddress.count >= 4 + else { return false } + + let decodedAddressNoCheckSum = Array(decodedAddress.prefix(decodedAddress.count - 4)) + let hashedSum = decodedAddressNoCheckSum.sha256().sha256() + + let checkSum = Array(decodedAddress.suffix(from: decodedAddress.count - 4)) + let hashedSumHeader = Array(hashedSum.prefix(4)) + + return hashedSumHeader == checkSum } } @@ -320,7 +368,7 @@ extension DogeWalletService { extension DogeWalletService { /// - Parameters: /// - dogeAddress: DOGE address to save into KVS - /// - adamantAddress: Owner of Lisk address + /// - 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 { @@ -420,6 +468,69 @@ extension DogeWalletService { } } + func getUnspentTransactions(_ completion: @escaping (ApiServiceResult<[UnspentTransaction]>) -> Void) { + guard let raw = AdamantResources.dogeServers.randomElement(), let url = URL(string: raw) else { + fatalError("Failed to build 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(address: 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): + + if let items = data as? [[String: Any]] { + var utxos = [UnspentTransaction]() + for item in items { + if let txid = item["txid"] as? String, + let vout = item["vout"] as? NSNumber, + let amount = item["amount"] as? NSNumber { + + let value = NSDecimalNumber(decimal: (amount.decimalValue * Decimal(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 + + print(txid, txIndex, lockScript.hex, value) + + 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)) + } else { + completion(.failure(.internalError(message: "DOGE Wallet: not valid response", error: nil))) + } + + case .failure: + completion(.failure(.internalError(message: "DOGE Wallet: server not response", error: nil))) + } + } + } + func getTransaction(by hash: String, completion: @escaping (ApiServiceResult) -> Void) { guard let raw = AdamantResources.dogeServers.randomElement(), let url = URL(string: raw) else { fatalError("Failed to build DOGE endpoint URL") From 09b117e180acd8a9e60d9a3bc32d338af520821b Mon Sep 17 00:00:00 2001 From: Anton B Date: Thu, 14 Mar 2019 17:17:16 +0300 Subject: [PATCH 05/46] Move transaction creation to lib --- .../Wallets/Doge/DogeWalletService+Send.swift | 64 ++----------------- 1 file changed, 5 insertions(+), 59 deletions(-) diff --git a/Adamant/Wallets/Doge/DogeWalletService+Send.swift b/Adamant/Wallets/Doge/DogeWalletService+Send.swift index 9a649ab0a..e8512e3f8 100644 --- a/Adamant/Wallets/Doge/DogeWalletService+Send.swift +++ b/Adamant/Wallets/Doge/DogeWalletService+Send.swift @@ -45,7 +45,8 @@ extension DogeWalletService: WalletServiceTwoStepSend { return } - let rawAwount = NSDecimalNumber(decimal: amount * Decimal(DogeWalletService.multiplier)).int64Value + let rawAmount = NSDecimalNumber(decimal: amount * Decimal(DogeWalletService.multiplier)).uint64Value + let fee = NSDecimalNumber(decimal: self.transactionFee * Decimal(DogeWalletService.multiplier)).uint64Value // MARK: Go background defaultDispatchQueue.async { @@ -54,11 +55,10 @@ extension DogeWalletService: WalletServiceTwoStepSend { switch result { case .success(let utxos): // MARK: 3. Create local transaction - let unsignedTx = self.createUnsignedTx(toAddress: toAddress, amount: rawAwount, changeAddress: changeAddress, utxos: utxos, lockTime: 0) - let signedTransaction = self.signTx(unsignedTx: unsignedTx, keys: [key]) - completion(.success(result: signedTransaction)) + let transaction = BitcoinKit.Transaction.createNewTransaction(toAddress: toAddress, amount: rawAmount, fee: fee, changeAddress: changeAddress, utxos: utxos, keys: [key]) + completion(.success(result: transaction)) break - case .failure(let error): + case .failure: completion(.failure(error: .notEnoughMoney)) break } @@ -105,60 +105,6 @@ extension DogeWalletService: WalletServiceTwoStepSend { } } } - - // TODO: select utxos and decide fee - public func selectTx(from utxos: [UnspentTransaction], amount: Int64) -> (utxos: [UnspentTransaction], fee: Int64) { - return (utxos, NSDecimalNumber(decimal: self.transactionFee * Decimal(DogeWalletService.multiplier)).int64Value) - } - - public func createUnsignedTx(toAddress: Address, amount: Int64, changeAddress: Address, utxos: [UnspentTransaction], lockTime: UInt32 = 0) -> UnsignedTransaction { - let (utxos, fee) = selectTx(from: utxos, amount: amount) - let totalAmount: Int64 = Int64(utxos.reduce(0) { $0 + $1.output.value }) - let change: Int64 = totalAmount - amount - fee - - let toPubKeyHash: Data = toAddress.data - let changePubkeyHash: Data = changeAddress.data - - let lockingScriptTo = Script.buildPublicKeyHashOut(pubKeyHash: toPubKeyHash) - let lockingScriptChange = Script.buildPublicKeyHashOut(pubKeyHash: changePubkeyHash) - - let toOutput = TransactionOutput(value: UInt64(amount), lockingScript: lockingScriptTo) - let changeOutput = TransactionOutput(value: UInt64(change), lockingScript: lockingScriptChange) - - let unsignedInputs = utxos.map { TransactionInput(previousOutput: $0.outpoint, signatureScript: Data(), sequence: UInt32.max) } - let tx = BitcoinKit.Transaction(version: 1, inputs: unsignedInputs, outputs: [toOutput, changeOutput], lockTime: lockTime) - return UnsignedTransaction(tx: tx, utxos: utxos) - } - - public func signTx(unsignedTx: UnsignedTransaction, keys: [PrivateKey]) -> BitcoinKit.Transaction { - var inputsToSign = unsignedTx.tx.inputs - var transactionToSign: BitcoinKit.Transaction { - return BitcoinKit.Transaction(version: unsignedTx.tx.version, inputs: inputsToSign, outputs: unsignedTx.tx.outputs, lockTime: unsignedTx.tx.lockTime) - } - - // Signing - let hashType = SighashType.BTC.ALL - for (i, utxo) in unsignedTx.utxos.enumerated() { - let pubkeyHash: Data = Script.getPublicKeyHash(from: utxo.output.lockingScript) - - let keysOfUtxo: [PrivateKey] = keys.filter { $0.publicKey().pubkeyHash == pubkeyHash } - guard let key = keysOfUtxo.first else { - print("No keys to this txout : \(utxo.output.value)") - continue - } - print("Value of signing txout : \(utxo.output.value)") - - let sighash: Data = transactionToSign.signatureHash(for: utxo.output, inputIndex: i, hashType: SighashType.BTC.ALL) - let signature: Data = try! BitcoinKit.Crypto.sign(sighash, privateKey: key) - let txin = inputsToSign[i] - let pubkey = key.publicKey() - - let unlockingScript = Script.buildPublicKeyUnlockingScript(signature: signature, pubkey: pubkey, hashType: hashType) - - inputsToSign[i] = TransactionInput(previousOutput: txin.previousOutput, signatureScript: unlockingScript, sequence: txin.sequence) - } - return transactionToSign - } } extension BitcoinKit.Transaction: TransactionDetails { From 60980862930c889d1b77c324deb466548b361fbd Mon Sep 17 00:00:00 2001 From: Anton B Date: Thu, 14 Mar 2019 17:27:21 +0300 Subject: [PATCH 06/46] Update podfile.lock --- Podfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Podfile.lock b/Podfile.lock index aa37676b8..a50577ce1 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -160,7 +160,7 @@ EXTERNAL SOURCES: CHECKOUT OPTIONS: BitcoinKit: - :commit: 4c0ba2fce414436e2288a354411675a12c2866dc + :commit: fbc6c7dad2e3700415a839e8d43f6ecfc7772a84 :git: https://github.com/boyarkin-anton/BitcoinKit.git Lisk: :commit: 154a3af05772136b776ff14ae05f92c8d844ea20 From 2fa6682266b8e2ec52e6449c07c169c1137fab66 Mon Sep 17 00:00:00 2001 From: Anton B Date: Thu, 14 Mar 2019 17:40:31 +0300 Subject: [PATCH 07/46] Add localization --- Adamant/Assets/l18n/de.lproj/Localizable.strings | 8 +++++++- Adamant/Assets/l18n/en.lproj/Localizable.strings | 8 +++++++- Adamant/Assets/l18n/ru.lproj/Localizable.strings | 8 +++++++- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/Adamant/Assets/l18n/de.lproj/Localizable.strings b/Adamant/Assets/l18n/de.lproj/Localizable.strings index 4d20ab34f..7673fd332 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"; @@ -172,6 +175,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" = "Konto"; 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/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" = "Делегаты"; From 2949a24bd8d1f7c6010ef746e00e88a42e8e285e Mon Sep 17 00:00:00 2001 From: Anton B Date: Thu, 14 Mar 2019 18:38:57 +0300 Subject: [PATCH 08/46] Move address validation to lib --- .../Doge/DogeTransferViewController.swift | 16 ++++- .../Wallets/Doge/DogeWalletService+Send.swift | 60 ------------------- Adamant/Wallets/Doge/DogeWalletService.swift | 43 +------------ Podfile.lock | 2 +- 4 files changed, 17 insertions(+), 104 deletions(-) diff --git a/Adamant/Wallets/Doge/DogeTransferViewController.swift b/Adamant/Wallets/Doge/DogeTransferViewController.swift index b6ede386a..503ec0cb5 100644 --- a/Adamant/Wallets/Doge/DogeTransferViewController.swift +++ b/Adamant/Wallets/Doge/DogeTransferViewController.swift @@ -161,7 +161,7 @@ class DogeTransferViewController: TransferViewControllerBase { } override func recipientRow() -> BaseRow { - let row = SuffixTextRow() { + let row = TextRow() { $0.tag = BaseRows.address.tag $0.cell.textField.placeholder = String.adamantLocalized.newChat.addressPlaceholder @@ -173,6 +173,20 @@ class DogeTransferViewController: TransferViewControllerBase { $0.disabled = true $0.cell.textField.isEnabled = false } + }.cellUpdate { (cell, row) in + cell.textField?.setStyle(.input) + cell.setStyle(.secondaryBackground) + }.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 diff --git a/Adamant/Wallets/Doge/DogeWalletService+Send.swift b/Adamant/Wallets/Doge/DogeWalletService+Send.swift index e8512e3f8..0fe7b61c8 100644 --- a/Adamant/Wallets/Doge/DogeWalletService+Send.swift +++ b/Adamant/Wallets/Doge/DogeWalletService+Send.swift @@ -155,63 +155,3 @@ extension BitcoinKit.Transaction: TransactionDetails { return "" } } - -public protocol BinaryConvertible { - static func +(lhs: Data, rhs: Self) -> Data - static func +=(lhs: inout Data, rhs: Self) -} - -public extension BinaryConvertible { - public static func +(lhs: Data, rhs: Self) -> Data { - var value = rhs - let data = Data(buffer: UnsafeBufferPointer(start: &value, count: 1)) - return lhs + data - } - - public static func +=(lhs: inout Data, rhs: Self) { - lhs = lhs + rhs - } -} - -extension UInt8 : BinaryConvertible {} -extension UInt16 : BinaryConvertible {} -extension UInt32 : BinaryConvertible {} -extension UInt64 : BinaryConvertible {} -extension Int8 : BinaryConvertible {} -extension Int16 : BinaryConvertible {} -extension Int32 : BinaryConvertible {} -extension Int64 : BinaryConvertible {} -extension Int : BinaryConvertible {} - -extension Bool : BinaryConvertible { - public static func +(lhs: Data, rhs: Bool) -> Data { - return lhs + (rhs ? UInt8(0x01) : UInt8(0x00)).littleEndian - } -} - -extension String : BinaryConvertible { - public static func +(lhs: Data, rhs: String) -> Data { - guard let data = rhs.data(using: .ascii) else { return lhs} - return lhs + data - } -} - -extension Data : BinaryConvertible { - public static func +(lhs: Data, rhs: Data) -> Data { - var data = Data() - data.append(lhs) - data.append(rhs) - return data - } -} - -enum SignError: Error { - case noPreviousOutput - case noPreviousOutputAddress - case noPrivateKey -} - -enum SerializationError: Error { - case noPreviousOutput - case noPreviousTransaction -} diff --git a/Adamant/Wallets/Doge/DogeWalletService.swift b/Adamant/Wallets/Doge/DogeWalletService.swift index 93138bd51..3cc38b150 100644 --- a/Adamant/Wallets/Doge/DogeWalletService.swift +++ b/Adamant/Wallets/Doge/DogeWalletService.swift @@ -167,48 +167,7 @@ class DogeWalletService: WalletService { } func validate(address: String) -> AddressValidationResult { - return isValid(bitcoinAddress: address) ? .valid : .invalid - } - - private func getBase58DecodeAsBytes(address: String, length: Int) -> [UTF8.CodeUnit]? { - let b58Chars = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" - - var output: [UTF8.CodeUnit] = Array(repeating: 0, count: length) - - for i in 0.. Bool { - guard address.count >= 26 && address.count <= 35, - address.rangeOfCharacter(from: CharacterSet.alphanumerics.inverted) == nil, - let decodedAddress = getBase58DecodeAsBytes(address: address, length: 25), - decodedAddress.count >= 4 - else { return false } - - let decodedAddressNoCheckSum = Array(decodedAddress.prefix(decodedAddress.count - 4)) - let hashedSum = decodedAddressNoCheckSum.sha256().sha256() - - let checkSum = Array(decodedAddress.suffix(from: decodedAddress.count - 4)) - let hashedSumHeader = Array(hashedSum.prefix(4)) - - return hashedSumHeader == checkSum + return AddressFactory.isValid(bitcoinAddress: address) ? .valid : .invalid } } diff --git a/Podfile.lock b/Podfile.lock index a50577ce1..0be50be36 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -160,7 +160,7 @@ EXTERNAL SOURCES: CHECKOUT OPTIONS: BitcoinKit: - :commit: fbc6c7dad2e3700415a839e8d43f6ecfc7772a84 + :commit: b78dd8df08bee99bdd75eef5cb5e8de0e27638e8 :git: https://github.com/boyarkin-anton/BitcoinKit.git Lisk: :commit: 154a3af05772136b776ff14ae05f92c8d844ea20 From 34839d5fa7cc2e869d2145b8be237fb9235adb99 Mon Sep 17 00:00:00 2001 From: Anton B Date: Fri, 15 Mar 2019 12:03:19 +0300 Subject: [PATCH 09/46] Load doge transactions recursivly --- .../Doge/DogeTransactionsViewController.swift | 14 ++++++-- Adamant/Wallets/Doge/DogeWalletService.swift | 36 +++++++++++++++---- 2 files changed, 42 insertions(+), 8 deletions(-) diff --git a/Adamant/Wallets/Doge/DogeTransactionsViewController.swift b/Adamant/Wallets/Doge/DogeTransactionsViewController.swift index 3fa145fa9..19a3f61cc 100644 --- a/Adamant/Wallets/Doge/DogeTransactionsViewController.swift +++ b/Adamant/Wallets/Doge/DogeTransactionsViewController.swift @@ -29,7 +29,17 @@ class DogeTransactionsViewController: TransactionsListViewControllerBase { } override func handleRefresh(_ refreshControl: UIRefreshControl) { - self.walletService.getTransactions({ (result) in + self.transactions.removeAll() + DispatchQueue.main.async { + self.tableView.reloadData() + } + + self.walletService.getTransactions(update: { transactions in + self.transactions.append(contentsOf: transactions) + DispatchQueue.main.async { + self.tableView.reloadData() + } + }, completion: { (result) in switch result { case .success(let transactions): self.transactions = transactions @@ -102,7 +112,7 @@ class DogeTransactionsViewController: TransactionsListViewControllerBase { configureCell(cell, isOutgoing: outgoing, - partnerId: partnerId ?? "", + partnerId: partnerId, partnerName: nil, amount: transaction.amountValue, date: transaction.dateValue) diff --git a/Adamant/Wallets/Doge/DogeWalletService.swift b/Adamant/Wallets/Doge/DogeWalletService.swift index 3cc38b150..8c48130bd 100644 --- a/Adamant/Wallets/Doge/DogeWalletService.swift +++ b/Adamant/Wallets/Doge/DogeWalletService.swift @@ -388,7 +388,11 @@ extension DogeWalletService { // MARK: - Transactions extension DogeWalletService { - func getTransactions(_ completion: @escaping (ApiServiceResult<[DogeTransaction]>) -> Void) { + func getTransactions(update: @escaping ([DogeTransaction]) -> Void, completion: @escaping (ApiServiceResult<[DogeTransaction]>) -> Void) { + self.getNewTransactions(update: update, completion: completion) + } + + private func getNewTransactions(from: Int = 0, prevTransactions: [DogeTransaction] = [DogeTransaction](), update: @escaping ([DogeTransaction]) -> Void, completion: @escaping (ApiServiceResult<[DogeTransaction]>) -> Void) { guard let raw = AdamantResources.dogeServers.randomElement(), let url = URL(string: raw) else { fatalError("Failed to build DOGE endpoint URL") } @@ -399,21 +403,41 @@ extension DogeWalletService { "Content-Type": "application/json" ] + let to = from + DogeWalletService.chunkSize + + let parameters = [ + "from": from, + "to": to + ] + // Request url let endpoint = url.appendingPathComponent(DogeApiCommands.getTransactions(address: address)) // MARK: Sending request - Alamofire.request(endpoint, method: .get, headers: headers).responseJSON(queue: defaultDispatchQueue) { response in + Alamofire.request(endpoint, method: .get, parameters: parameters, headers: headers).responseJSON(queue: defaultDispatchQueue) { response in switch response.result { case .success(let data): - if let result = data as? [String: Any], let items = result["items"] as? [[String: Any]] { - var transactions = [DogeTransaction]() + if let result = data as? [String: Any], let items = result["items"] as? [[String: Any]], let totalItems = result["totalItems"] as? NSNumber { + + let hasMore = to < totalItems.intValue + + var newTransactions = [DogeTransaction]() for item in items { - transactions.append(DogeTransaction.from(item, with: address)) + newTransactions.append(DogeTransaction.from(item, with: address)) + } + + update(newTransactions) + + var transactions = prevTransactions + transactions.append(contentsOf: newTransactions) + + if hasMore { + self.getNewTransactions(from: to, prevTransactions: transactions, update: update, completion: completion) + } else { + completion(.success(transactions)) } - completion(.success(transactions)) } else { completion(.failure(.internalError(message: "DOGE Wallet: not valid response", error: nil))) } From af2f965f9d0e4f4202544c4051c76aefb558f211 Mon Sep 17 00:00:00 2001 From: Pavel Anokhov Date: Thu, 28 Mar 2019 23:35:07 +0300 Subject: [PATCH 10/46] Version --- Adamant/Info.plist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Adamant/Info.plist b/Adamant/Info.plist index 3cfa6f44f..01e912bca 100644 --- a/Adamant/Info.plist +++ b/Adamant/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 1.4 CFBundleVersion - 69 + 70 LSRequiresIPhoneOS NSAppTransportSecurity From 89619ce12324ed418c1d0446515587cea636fab4 Mon Sep 17 00:00:00 2001 From: Pavel Anokhov Date: Tue, 2 Apr 2019 19:43:00 +0300 Subject: [PATCH 11/46] Cleanup Fix for annoying error "Server not yet ready". --- .../Wallets/Doge/DogeWalletService+Send.swift | 4 +- Adamant/Wallets/Doge/DogeWalletService.swift | 110 ++++++++++-------- 2 files changed, 62 insertions(+), 52 deletions(-) diff --git a/Adamant/Wallets/Doge/DogeWalletService+Send.swift b/Adamant/Wallets/Doge/DogeWalletService+Send.swift index 0fe7b61c8..a95a6b279 100644 --- a/Adamant/Wallets/Doge/DogeWalletService+Send.swift +++ b/Adamant/Wallets/Doge/DogeWalletService+Send.swift @@ -45,8 +45,8 @@ extension DogeWalletService: WalletServiceTwoStepSend { return } - let rawAmount = NSDecimalNumber(decimal: amount * Decimal(DogeWalletService.multiplier)).uint64Value - let fee = NSDecimalNumber(decimal: self.transactionFee * Decimal(DogeWalletService.multiplier)).uint64Value + let rawAmount = NSDecimalNumber(decimal: amount * DogeWalletService.multiplier).uint64Value + let fee = NSDecimalNumber(decimal: self.transactionFee * DogeWalletService.multiplier).uint64Value // MARK: Go background defaultDispatchQueue.async { diff --git a/Adamant/Wallets/Doge/DogeWalletService.swift b/Adamant/Wallets/Doge/DogeWalletService.swift index 8c48130bd..aaacaa16e 100644 --- a/Adamant/Wallets/Doge/DogeWalletService.swift +++ b/Adamant/Wallets/Doge/DogeWalletService.swift @@ -62,7 +62,7 @@ class DogeWalletService: WalletService { static var currencySymbol = "DOGE" static var currencyLogo = #imageLiteral(resourceName: "wallet_doge") - static let multiplier = 1e8 + 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 @@ -159,7 +159,16 @@ class DogeWalletService: WalletService { } case .failure(let error): - self?.dialogService.showRichError(error: 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) @@ -272,7 +281,7 @@ extension DogeWalletService { } guard let address = self.dogeWallet?.address else { - completion(.failure(error: .internalError(message: "DOGE Wallet: not found", error: nil))) + completion(.failure(error: .walletNotInitiated)) return } @@ -290,10 +299,10 @@ extension DogeWalletService { switch response.result { case .success(let data): if let raw = Decimal(string: data) { - let balance = raw / Decimal(DogeWalletService.multiplier) + let balance = raw * DogeWalletService.multiplier completion(.success(result: balance)) } else { - completion(.failure(error: .internalError(message: "DOGE Wallet: balance not found", error: nil))) + completion(.failure(error: .remoteServiceError(message: "DOGE Wallet: \(data)"))) } case .failure: @@ -397,57 +406,58 @@ extension DogeWalletService { fatalError("Failed to build DOGE endpoint URL") } - if let address = self.wallet?.address { - // Headers - let headers = [ - "Content-Type": "application/json" - ] - - let to = from + DogeWalletService.chunkSize - - let parameters = [ - "from": from, - "to": to - ] - - // Request url - let endpoint = url.appendingPathComponent(DogeApiCommands.getTransactions(address: address)) + guard let address = self.wallet?.address else { + completion(.failure(.internalError(message: "DOGE Wallet: not found", error: nil))) + return + } + + // Headers + let headers = [ + "Content-Type": "application/json" + ] + + let to = from + DogeWalletService.chunkSize + + let parameters = [ + "from": from, + "to": to + ] + + // Request url + let endpoint = url.appendingPathComponent(DogeApiCommands.getTransactions(address: address)) + + // MARK: Sending request + Alamofire.request(endpoint, method: .get, parameters: parameters, headers: headers).responseJSON(queue: defaultDispatchQueue) { response in - // MARK: Sending request - Alamofire.request(endpoint, method: .get, parameters: parameters, headers: headers).responseJSON(queue: defaultDispatchQueue) { response in + switch response.result { + case .success(let data): - switch response.result { - case .success(let data): + if let result = data as? [String: Any], let items = result["items"] as? [[String: Any]], let totalItems = result["totalItems"] as? NSNumber { - if let result = data as? [String: Any], let items = result["items"] as? [[String: Any]], let totalItems = result["totalItems"] as? NSNumber { - - let hasMore = to < totalItems.intValue - - var newTransactions = [DogeTransaction]() - for item in items { - newTransactions.append(DogeTransaction.from(item, with: address)) - } - - update(newTransactions) - - var transactions = prevTransactions - transactions.append(contentsOf: newTransactions) - - if hasMore { - self.getNewTransactions(from: to, prevTransactions: transactions, update: update, completion: completion) - } else { - completion(.success(transactions)) - } - } else { - completion(.failure(.internalError(message: "DOGE Wallet: not valid response", error: nil))) + let hasMore = to < totalItems.intValue + + var newTransactions = [DogeTransaction]() + for item in items { + newTransactions.append(DogeTransaction.from(item, with: address)) } - case .failure: - completion(.failure(.internalError(message: "DOGE Wallet: server not response", error: nil))) + update(newTransactions) + + var transactions = prevTransactions + transactions.append(contentsOf: newTransactions) + + if hasMore { + self.getNewTransactions(from: to, prevTransactions: transactions, update: update, completion: completion) + } else { + completion(.success(transactions)) + } + } else { + completion(.failure(.internalError(message: "DOGE Wallet: not valid response", error: nil))) } + + case .failure: + completion(.failure(.internalError(message: "DOGE Wallet: server not response", error: nil))) } - } else { - completion(.failure(.internalError(message: "DOGE Wallet: not found", error: nil))) } } @@ -488,7 +498,7 @@ extension DogeWalletService { let vout = item["vout"] as? NSNumber, let amount = item["amount"] as? NSNumber { - let value = NSDecimalNumber(decimal: (amount.decimalValue * Decimal(DogeWalletService.multiplier))).uint64Value + 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() From 534ffc84fb020168e25d78da368fa47e1970b283 Mon Sep 17 00:00:00 2001 From: Pavel Anokhov Date: Wed, 3 Apr 2019 22:50:38 +0300 Subject: [PATCH 12/46] DogeTransactions: Redesigned --- Adamant.xcodeproj/project.pbxproj | 4 + Adamant/Models/DogeTransaction.swift | 284 +++++++++++++++--- .../DogeGetTransactionsResponse.swift | 28 ++ .../Doge/DogeTransactionsViewController.swift | 71 +++-- Adamant/Wallets/Doge/DogeWalletService.swift | 67 ++--- 5 files changed, 351 insertions(+), 103 deletions(-) create mode 100644 Adamant/ServerResponses/DogeGetTransactionsResponse.swift diff --git a/Adamant.xcodeproj/project.pbxproj b/Adamant.xcodeproj/project.pbxproj index 6ed4bffae..ad4ed7f0d 100644 --- a/Adamant.xcodeproj/project.pbxproj +++ b/Adamant.xcodeproj/project.pbxproj @@ -135,6 +135,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 */; }; @@ -462,6 +463,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 = ""; }; @@ -898,6 +900,7 @@ E95F85792007F0260070534A /* ServerResponse.swift */, E9C51EEE20139DC600385EB7 /* TransactionIdResponse.swift */, E95F856E2007B61D0070534A /* GetPublicKeyResponse.swift */, + E933475A225539390083839E /* DogeGetTransactionsResponse.swift */, ); path = ServerResponses; sourceTree = ""; @@ -1588,6 +1591,7 @@ 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 */, diff --git a/Adamant/Models/DogeTransaction.swift b/Adamant/Models/DogeTransaction.swift index 23fc2ff68..1fd8d907c 100644 --- a/Adamant/Models/DogeTransaction.swift +++ b/Adamant/Models/DogeTransaction.swift @@ -8,51 +8,255 @@ import Foundation -class DogeTransaction: TransactionDetails { - var txId: String = "" - var senderAddress: String = "" - var recipientAddress: String = "" - var dateValue: Date? - var amountValue: Decimal = 0 - var feeValue: Decimal? - var confirmationsValue: String? - var blockValue: String? = nil - var isOutgoing: Bool = false - var transactionStatus: TransactionStatus? - - static func from(_ dictionry: [String: Any], with walletAddress: String) -> DogeTransaction { - let transaction = DogeTransaction() - - if let txid = dictionry["txid"] as? String { transaction.txId = txid } - if let vin = dictionry["vin"] as? [[String: Any]], let input = vin.first, let address = input["addr"] as? String { - transaction.senderAddress = address - if address == walletAddress { - transaction.isOutgoing = true +struct DogeTransaction: TransactionDetails { + 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? + + static func map(from dogeTransaction: DogeRawTransaction, for address: String, with blockId: String?) -> [DogeTransaction] { + var transactions = [DogeTransaction]() + + // MARK: Known values + let txId = dogeTransaction.txId + let dateValue = dogeTransaction.date + let feeValue = dogeTransaction.fee + let confirmationsValue = String(dogeTransaction.confirmations) + let transactionStatus: TransactionStatus = dogeTransaction.confirmations > 0 ? .success : .pending + + // Transfers + let myInputs = dogeTransaction.inputs.filter { $0.sender == address } + let myOutputs = dogeTransaction.outputs.filter { $0.addresses.contains(address) } + + // MARK: Inputs + if myInputs.count > 0 { + let amountValue = myInputs.map { $0.value }.reduce(0, +) + + let recipient: String + if dogeTransaction.outputs.count == 1 { + recipient = dogeTransaction.outputs.first?.addresses.first ?? "" + } else { + recipient = "\(dogeTransaction.outputs.count) recipients" } + + let inputTransaction = DogeTransaction(txId: txId, + dateValue: dateValue, + blockValue: blockId, + senderAddress: address, + recipientAddress: recipient, + amountValue: amountValue, + feeValue: feeValue, + confirmationsValue: confirmationsValue, + isOutgoing: true, + transactionStatus: transactionStatus) + + transactions.append(inputTransaction) } - if let vout = dictionry["vout"] as? [[String: Any]] { - let outputs = vout.filter { item -> Bool in - if let publickKey = item["scriptPubKey"] as? [String: Any], let addresses = publickKey["addresses"] as? [String], let address = addresses.first { - if transaction.isOutgoing, address != walletAddress { - return true - } else if !transaction.isOutgoing, address == walletAddress { - return true - } - } - return false - } - if let output = outputs.first, let publickKey = output["scriptPubKey"] as? [String: Any], let addresses = publickKey["addresses"] as? [String], let address = addresses.first, let valueRaw = output["value"] as? String, let value = Decimal(string: valueRaw) { - transaction.recipientAddress = address - transaction.amountValue = value + + // MARK: Outputs + if myOutputs.count > 0 { + let amountValue = myOutputs.map { $0.value }.reduce(0, +) + + let sender: String + if dogeTransaction.inputs.count == 1 { + sender = dogeTransaction.inputs.first?.sender ?? "" + } else { + sender = "\(dogeTransaction.inputs.count) senders" } + + let outputTransaction = DogeTransaction(txId: txId, + dateValue: dateValue, + blockValue: blockId, + senderAddress: sender, + recipientAddress: address, + amountValue: amountValue, + feeValue: feeValue, + confirmationsValue: confirmationsValue, + isOutgoing: false, + transactionStatus: transactionStatus) + + transactions.append(outputTransaction) } - if let time = dictionry["time"] as? NSNumber { transaction.dateValue = Date(timeIntervalSince1970: time.doubleValue) } - if let fees = dictionry["fees"] as? NSNumber { transaction.feeValue = fees.decimalValue } - if let confirmations = dictionry["confirmations"] as? NSNumber { transaction.confirmationsValue = confirmations.stringValue } - if let blockhash = dictionry["blockhash"] as? String { transaction.blockValue = blockhash } - transaction.transactionStatus = TransactionStatus.success + return transactions + } +} + + +// 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] +} + +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: Base + self.txId = try container.decode(String.self, forKey: .txId) + + let timeInterval = try container.decode(TimeInterval.self, forKey: .date) + self.date = Date(timeIntervalSince1970: timeInterval) + + self.valueIn = try container.decode(Decimal.self, forKey: .valueIn) + self.valueOut = try container.decode(Decimal.self, forKey: .valueOut) + self.fee = try container.decode(Decimal.self, forKey: .fee) + self.confirmations = try container.decode(Int.self, forKey: .confirmations) + self.blockHash = try container.decode(String.self, forKey: .blockHash) + + // MARK: Inputs & Outputs + + self.inputs = try container.decode([DogeInput].self, forKey: .inputs) + self.outputs = try container.decode([DogeOutput].self, forKey: .outputs) + } +} + + +// MARK: Doge internal +struct DogeInput: Decodable { + enum CodingKeys: String, CodingKey { + case sender = "addr" + case value + 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) + + self.sender = try container.decode(String.self, forKey: .sender) + self.value = try container.decode(Decimal.self, forKey: .value) + self.txId = try container.decode(String.self, forKey: .txId) + self.vOut = try container.decode(Int.self, forKey: .vOut) + } +} + +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) - return transaction + 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 +} +*/ + +/* 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/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/Wallets/Doge/DogeTransactionsViewController.swift b/Adamant/Wallets/Doge/DogeTransactionsViewController.swift index 19a3f61cc..56bf07ef0 100644 --- a/Adamant/Wallets/Doge/DogeTransactionsViewController.swift +++ b/Adamant/Wallets/Doge/DogeTransactionsViewController.swift @@ -29,37 +29,54 @@ class DogeTransactionsViewController: TransactionsListViewControllerBase { } override func handleRefresh(_ refreshControl: UIRefreshControl) { - self.transactions.removeAll() - DispatchQueue.main.async { - self.tableView.reloadData() - } + transactions.removeAll() - self.walletService.getTransactions(update: { transactions in - self.transactions.append(contentsOf: transactions) - DispatchQueue.main.async { - self.tableView.reloadData() - } - }, completion: { (result) in + walletService.getTransactions(from: 0) { [weak self] result in switch result { - case .success(let transactions): - self.transactions = transactions + case .success(let tuple): + self?.transactions = tuple.transactions + DispatchQueue.main.async { - self.tableView.reloadData() + self?.tableView.reloadData() + self?.refreshControl.endRefreshing() } - break + case .failure(let error): - if case .internalError(let message, _ ) = error { - let localizedErrorMessage = NSLocalizedString(message, comment: "TransactionList: 'Transactions not found' message.") - self.dialogService.showWarning(withMessage: localizedErrorMessage) - } else { - self.dialogService.showError(withMessage: String.adamantLocalized.transactionList.notFound, error: error) + DispatchQueue.main.async { + self?.tableView.reloadData() + self?.refreshControl.endRefreshing() } - break - } - DispatchQueue.main.async { - self.refreshControl.endRefreshing() + + self?.dialogService.showRichError(error: error) } - }) + } + +// self.walletService.getTransactions(update: { transactions in +// self.transactions.append(contentsOf: transactions) +// DispatchQueue.main.async { +// self.tableView.reloadData() +// } +// }, completion: { (result) in +// switch result { +// case .success(let transactions): +// self.transactions = transactions +// DispatchQueue.main.async { +// self.tableView.reloadData() +// } +// break +// case .failure(let error): +// if case .internalError(let message, _ ) = error { +// let localizedErrorMessage = NSLocalizedString(message, comment: "TransactionList: 'Transactions not found' message.") +// self.dialogService.showWarning(withMessage: localizedErrorMessage) +// } else { +// self.dialogService.showError(withMessage: String.adamantLocalized.transactionList.notFound, error: error) +// } +// break +// } +// DispatchQueue.main.async { +// self.refreshControl.endRefreshing() +// } +// }) } @@ -107,7 +124,7 @@ class DogeTransactionsViewController: TransactionsListViewControllerBase { } func configureCell(_ cell: TransactionTableViewCell, for transaction: DogeTransaction) { - let outgoing = isOutgoing(transaction) + let outgoing = transaction.isOutgoing let partnerId = outgoing ? transaction.recipientAddress : transaction.senderAddress configureCell(cell, @@ -117,8 +134,4 @@ class DogeTransactionsViewController: TransactionsListViewControllerBase { amount: transaction.amountValue, date: transaction.dateValue) } - - private func isOutgoing(_ transaction: DogeTransaction) -> Bool { - return transaction.senderAddress.lowercased() == walletService.wallet?.address.lowercased() - } } diff --git a/Adamant/Wallets/Doge/DogeWalletService.swift b/Adamant/Wallets/Doge/DogeWalletService.swift index aaacaa16e..51420b3e1 100644 --- a/Adamant/Wallets/Doge/DogeWalletService.swift +++ b/Adamant/Wallets/Doge/DogeWalletService.swift @@ -90,6 +90,8 @@ class DogeWalletService: WalletService { 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 @@ -397,27 +399,37 @@ extension DogeWalletService { // MARK: - Transactions extension DogeWalletService { - func getTransactions(update: @escaping ([DogeTransaction]) -> Void, completion: @escaping (ApiServiceResult<[DogeTransaction]>) -> Void) { - self.getNewTransactions(update: update, completion: completion) + 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 { DogeTransaction.map(from: $0, for: address, with: nil) }.reduce([], +) + + completion(.success((transactions: transactions, hasMore: hasMore))) + + case .failure(let error): + completion(.failure(error)) + } + } } - private func getNewTransactions(from: Int = 0, prevTransactions: [DogeTransaction] = [DogeTransaction](), update: @escaping ([DogeTransaction]) -> Void, completion: @escaping (ApiServiceResult<[DogeTransaction]>) -> Void) { + private func getTransactions(for address: String, from: Int, to: Int, completion: @escaping (ApiServiceResult) -> Void) { guard let raw = AdamantResources.dogeServers.randomElement(), let url = URL(string: raw) else { fatalError("Failed to build DOGE endpoint URL") } - guard let address = self.wallet?.address else { - completion(.failure(.internalError(message: "DOGE Wallet: not found", error: nil))) - return - } - // Headers let headers = [ "Content-Type": "application/json" ] - let to = from + DogeWalletService.chunkSize - let parameters = [ "from": from, "to": to @@ -427,34 +439,16 @@ extension DogeWalletService { let endpoint = url.appendingPathComponent(DogeApiCommands.getTransactions(address: address)) // MARK: Sending request - Alamofire.request(endpoint, method: .get, parameters: parameters, headers: headers).responseJSON(queue: defaultDispatchQueue) { response in - + Alamofire.request(endpoint, method: .get, parameters: parameters, headers: headers).responseData(queue: defaultDispatchQueue) { response in switch response.result { case .success(let data): - - if let result = data as? [String: Any], let items = result["items"] as? [[String: Any]], let totalItems = result["totalItems"] as? NSNumber { - - let hasMore = to < totalItems.intValue - - var newTransactions = [DogeTransaction]() - for item in items { - newTransactions.append(DogeTransaction.from(item, with: address)) - } - - update(newTransactions) - - var transactions = prevTransactions - transactions.append(contentsOf: newTransactions) - - if hasMore { - self.getNewTransactions(from: to, prevTransactions: transactions, update: update, completion: completion) - } else { - completion(.success(transactions)) - } - } else { + guard let dogeResponse = try? DogeWalletService.jsonDecoder.decode(DogeGetTransactionsResponse.self, from: data) else { completion(.failure(.internalError(message: "DOGE Wallet: not valid response", error: nil))) + break } + completion(.success(dogeResponse)) + case .failure: completion(.failure(.internalError(message: "DOGE Wallet: server not response", error: nil))) } @@ -524,6 +518,7 @@ extension DogeWalletService { } } + /* func getTransaction(by hash: String, completion: @escaping (ApiServiceResult) -> Void) { guard let raw = AdamantResources.dogeServers.randomElement(), let url = URL(string: raw) else { fatalError("Failed to build DOGE endpoint URL") @@ -544,7 +539,8 @@ extension DogeWalletService { switch response.result { case .success(let data): if let item = data as? [String: Any] { - completion(.success(DogeTransaction.from(item, with: address))) + let transfers = DogeTransaction.transactions(from: item, for: address) + completion(.success(transfers)) } else { completion(.failure(.internalError(message: "No transaction", error: nil))) } @@ -556,8 +552,10 @@ extension DogeWalletService { completion(.failure(.internalError(message: "DOGE Wallet: not found", error: nil))) } } + */ } +// MARK: - WalletServiceWithTransfers extension DogeWalletService: WalletServiceWithTransfers { func transferListViewController() -> UIViewController { guard let vc = router.get(scene: AdamantScene.Wallets.Doge.transactionsList) as? DogeTransactionsViewController else { @@ -569,6 +567,7 @@ extension DogeWalletService: WalletServiceWithTransfers { } } +// MARK: - Mainnet configuration class DogeMainnet: Network { override var name: String { return "livenet" From 346d741d825f36c4a446388e97eb10a7d3085d15 Mon Sep 17 00:00:00 2001 From: Pavel Anokhov Date: Fri, 5 Apr 2019 17:25:09 +0300 Subject: [PATCH 13/46] Cleanup --- Adamant.xcodeproj/project.pbxproj | 4 + Adamant/AppDelegate.swift | 4 +- Adamant/Models/SimpleTransactionDetails.swift | 2 - Adamant/Services/AdamantAccountService.swift | 8 +- Adamant/Wallets/Doge/DogeMainnet.swift | 58 +++++++++++++ .../Wallets/Doge/DogeWalletService+Send.swift | 4 +- Adamant/Wallets/Doge/DogeWalletService.swift | 81 +++++-------------- Adamant/Wallets/TransactionDetails.swift | 6 +- 8 files changed, 92 insertions(+), 75 deletions(-) create mode 100644 Adamant/Wallets/Doge/DogeMainnet.swift diff --git a/Adamant.xcodeproj/project.pbxproj b/Adamant.xcodeproj/project.pbxproj index ad4ed7f0d..0a7bdb07e 100644 --- a/Adamant.xcodeproj/project.pbxproj +++ b/Adamant.xcodeproj/project.pbxproj @@ -74,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 */; }; @@ -387,6 +388,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 = ""; }; @@ -681,6 +683,7 @@ 64E1C82B222E958C006C4DA7 /* Doge */ = { isa = PBXGroup; children = ( + E907350D2256779C00BF02CC /* DogeMainnet.swift */, 64E1C82C222E95E2006C4DA7 /* DogeWalletRoutes.swift */, 64E1C82E222E95F6006C4DA7 /* DogeWallet.swift */, 64E1C830222E9617006C4DA7 /* DogeWalletService.swift */, @@ -1613,6 +1616,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 */, diff --git a/Adamant/AppDelegate.swift b/Adamant/AppDelegate.swift index fcf87abf9..157b454b3 100644 --- a/Adamant/AppDelegate.swift +++ b/Adamant/AppDelegate.swift @@ -57,8 +57,8 @@ struct AdamantResources { "https://lisknode1.adamant.im" ] - static let dogeServers = [ - "https://dogenode1.adamant.im/api" + static let dogeServers: [URL] = [ + URL(string: "https://dogenode1.adamant.im/api")! ] // MARK: ADAMANT Addresses diff --git a/Adamant/Models/SimpleTransactionDetails.swift b/Adamant/Models/SimpleTransactionDetails.swift index 9c46ddb07..a7615b850 100644 --- a/Adamant/Models/SimpleTransactionDetails.swift +++ b/Adamant/Models/SimpleTransactionDetails.swift @@ -28,6 +28,4 @@ struct SimpleTransactionDetails: TransactionDetails { var isOutgoing: Bool var transactionStatus: TransactionStatus? - - } diff --git a/Adamant/Services/AdamantAccountService.swift b/Adamant/Services/AdamantAccountService.swift index 0656edb56..4965d0ba5 100644 --- a/Adamant/Services/AdamantAccountService.swift +++ b/Adamant/Services/AdamantAccountService.swift @@ -100,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 @@ -118,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/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/DogeWalletService+Send.swift b/Adamant/Wallets/Doge/DogeWalletService+Send.swift index a95a6b279..be8d858aa 100644 --- a/Adamant/Wallets/Doge/DogeWalletService+Send.swift +++ b/Adamant/Wallets/Doge/DogeWalletService+Send.swift @@ -67,8 +67,8 @@ extension DogeWalletService: WalletServiceTwoStepSend { } func sendTransaction(_ transaction: BitcoinKit.Transaction, completion: @escaping (WalletServiceResult) -> Void) { - guard let raw = AdamantResources.dogeServers.randomElement(), let url = URL(string: raw) else { - fatalError("Failed to build DOGE endpoint URL") + guard let url = AdamantResources.dogeServers.randomElement() else { + fatalError("Failed to get DOGE endpoint URL") } // Headers diff --git a/Adamant/Wallets/Doge/DogeWalletService.swift b/Adamant/Wallets/Doge/DogeWalletService.swift index 51420b3e1..51849df84 100644 --- a/Adamant/Wallets/Doge/DogeWalletService.swift +++ b/Adamant/Wallets/Doge/DogeWalletService.swift @@ -13,19 +13,23 @@ import BitcoinKit import BitcoinKit.Private struct DogeApiCommands { - static func balance(address: String) -> String { + static func balance(for address: String) -> String { return "/addr/\(address)/balance" } - static func getTransactions(address: String) -> String { + static func getTransactions(for address: String) -> String { return "/addrs/\(address)/txs" } - static func getTransaction(txId: String) -> String { - return "/tx/\(txId)" + static func getTransaction(by hash: String) -> String { + return "/tx/\(hash)" } - static func getUnspentTransactions(address: String) -> String { + static func getBlock(by hash: String) -> String { + return "/block/\(hash)" + } + + static func getUnspentTransactions(for address: String) -> String { return "/addr/\(address)/utxo" } @@ -278,8 +282,8 @@ extension DogeWalletService: SwinjectDependentService { // MARK: - Balances & addresses extension DogeWalletService { func getBalance(_ completion: @escaping (WalletServiceResult) -> Void) { - guard let raw = AdamantResources.dogeServers.randomElement(), let url = URL(string: raw) else { - fatalError("Failed to build DOGE endpoint URL") + guard let url = AdamantResources.dogeServers.randomElement() else { + fatalError("Failed to get DOGE endpoint URL") } guard let address = self.dogeWallet?.address else { @@ -293,7 +297,7 @@ extension DogeWalletService { ] // Request url - let endpoint = url.appendingPathComponent(DogeApiCommands.balance(address: address)) + let endpoint = url.appendingPathComponent(DogeApiCommands.balance(for: address)) // MARK: Sending request Alamofire.request(endpoint, method: .get, headers: headers).responseString(queue: defaultDispatchQueue) { response in @@ -421,8 +425,8 @@ extension DogeWalletService { } private func getTransactions(for address: String, from: Int, to: Int, completion: @escaping (ApiServiceResult) -> Void) { - guard let raw = AdamantResources.dogeServers.randomElement(), let url = URL(string: raw) else { - fatalError("Failed to build DOGE endpoint URL") + guard let url = AdamantResources.dogeServers.randomElement() else { + fatalError("Failed to get DOGE endpoint URL") } // Headers @@ -436,7 +440,7 @@ extension DogeWalletService { ] // Request url - let endpoint = url.appendingPathComponent(DogeApiCommands.getTransactions(address: address)) + 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 @@ -456,8 +460,8 @@ extension DogeWalletService { } func getUnspentTransactions(_ completion: @escaping (ApiServiceResult<[UnspentTransaction]>) -> Void) { - guard let raw = AdamantResources.dogeServers.randomElement(), let url = URL(string: raw) else { - fatalError("Failed to build DOGE endpoint URL") + guard let url = AdamantResources.dogeServers.randomElement() else { + fatalError("Failed to get DOGE endpoint URL") } guard let wallet = self.dogeWallet else { @@ -473,7 +477,7 @@ extension DogeWalletService { ] // Request url - let endpoint = url.appendingPathComponent(DogeApiCommands.getUnspentTransactions(address: address)) + let endpoint = url.appendingPathComponent(DogeApiCommands.getUnspentTransactions(for: address)) let parameters = [ "noCache": "1" @@ -566,52 +570,3 @@ extension DogeWalletService: WalletServiceWithTransfers { return vc } } - -// MARK: - Mainnet configuration -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/TransactionDetails.swift b/Adamant/Wallets/TransactionDetails.swift index 436c02ca7..b59f353ad 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 { @@ -41,7 +39,7 @@ protocol TransactionDetails { var transactionStatus: TransactionStatus? { get } } -extension TransactionDetails { +//extension TransactionDetails { // func getSummary() -> String { // return """ // Transaction #\(id) @@ -57,4 +55,4 @@ extension TransactionDetails { // URL: \(explorerUrl?.absoluteString ?? "") // """ // } -} +//} From f1e86199746d9093450d6b66223454a9383b24fc Mon Sep 17 00:00:00 2001 From: Pavel Anokhov Date: Fri, 5 Apr 2019 17:34:26 +0300 Subject: [PATCH 14/46] DOGE: Transaction details redesign --- Adamant/Models/DogeTransaction.swift | 15 +- ...DogeTransactionDetailsViewController.swift | 223 +++++++++++++++++- .../Doge/DogeTransactionsViewController.swift | 61 ++--- ...ogeWalletService+RichMessageProvider.swift | 35 --- Adamant/Wallets/Doge/DogeWalletService.swift | 80 ++++--- ...TransactionDetailsViewControllerBase.swift | 2 +- 6 files changed, 307 insertions(+), 109 deletions(-) diff --git a/Adamant/Models/DogeTransaction.swift b/Adamant/Models/DogeTransaction.swift index 1fd8d907c..8f6579cef 100644 --- a/Adamant/Models/DogeTransaction.swift +++ b/Adamant/Models/DogeTransaction.swift @@ -93,7 +93,7 @@ struct DogeTransaction: TransactionDetails { // MARK: - Raw Doge Transaction, for easy parsing -struct DogeRawTransaction { +struct DogeRawTransaction: TransactionDetails { let txId: String let date: Date @@ -106,6 +106,19 @@ struct DogeRawTransaction { let inputs: [DogeInput] let outputs: [DogeOutput] + + // MARK: Transaction Details + var feeValue: Decimal? { return fee } + var dateValue: Date? { return date } + var confirmationsValue: String? { return String(confirmations) } + var transactionStatus: TransactionStatus? { return .success } + var blockValue: String? { return nil } + + // Not used in details + var senderAddress: String { return "" } + var recipientAddress: String { return "" } + var amountValue: Decimal { return 0 } + var isOutgoing: Bool { return false } } extension DogeRawTransaction: Decodable { diff --git a/Adamant/Wallets/Doge/DogeTransactionDetailsViewController.swift b/Adamant/Wallets/Doge/DogeTransactionDetailsViewController.swift index c5e3bc75e..7eee1e334 100644 --- a/Adamant/Wallets/Doge/DogeTransactionDetailsViewController.swift +++ b/Adamant/Wallets/Doge/DogeTransactionDetailsViewController.swift @@ -7,14 +7,40 @@ // import UIKit +import Eureka class DogeTransactionDetailsViewController: TransactionDetailsViewControllerBase { + // MARK: Rows & Sections + enum DogeRows { + case inputs, totalIn, outputs, totalOut + + var tag: String { + switch self { + case .inputs: return "inputs" + case .totalIn: return "totalIn" + case .outputs: return "outputs" + case .totalOut: return "totalOut" + } + } + + var localized: String { + switch self { + case .inputs: return "Inputs" + case .totalIn: return "Total In" + case .outputs: return "Outputs" + case .totalOut: return "Total Out" + } + } + } + // MARK: - Dependencies weak var service: DogeWalletService? // MARK: - Properties + private var blockInfo: (hash: String, height: String)? = nil + private let autoupdateInterval: TimeInterval = 5.0 weak var timer: Timer? @@ -30,11 +56,26 @@ class DogeTransactionDetailsViewController: TransactionDetailsViewControllerBase currencySymbol = DogeWalletService.currencySymbol super.viewDidLoad() + if service != nil { tableView.refreshControl = refreshControl } - if service != nil { - tableView.refreshControl = refreshControl + // MARK: Cleanup rows + if let section = form.sectionBy(tag: Sections.details.tag) { + let rows: [Rows] = [.from, .to, .amount, .block] + + for row in rows { + if let r = form.rowBy(tag: row.tag), let index = section.firstIndex(of: r) { + section.remove(at: index) + } + } } + // MARK: Add rows + addRows() + + // MARK: Update block number + updateBlockNumber() + + // MARK: Start update if transaction != nil { startUpdate() } @@ -68,6 +109,12 @@ class DogeTransactionDetailsViewController: TransactionDetailsViewControllerBase self?.refreshControl.endRefreshing() } + if let blockInfo = self?.blockInfo, blockInfo.hash == trs.blockHash { + // No need to update block id + } else { + self?.updateBlockNumber() + } + case .failure(let error): self?.dialogService.showRichError(error: error) @@ -78,7 +125,7 @@ class DogeTransactionDetailsViewController: TransactionDetailsViewControllerBase } } - // MARK: - Autoupdate + // MARK: Autoupdate func startUpdate() { timer?.invalidate() @@ -106,4 +153,174 @@ class DogeTransactionDetailsViewController: TransactionDetailsViewControllerBase func stopUpdate() { timer?.invalidate() } + + // MARK: - Update block number + func updateBlockNumber() { + guard let blockHash = (transaction as? DogeRawTransaction)?.blockHash else { + blockInfo = nil + form.rowBy(tag: Rows.block.tag)?.updateCell() + return + } + + service?.getBlockId(by: blockHash) { [weak self] result in + switch result { + case .success(let height): + self?.blockInfo = (hash: blockHash, height: height) + + guard let row: LabelRow = self?.form.rowBy(tag: Rows.block.tag) else { + break + } + + DispatchQueue.main.async { + row.value = height + row.updateCell() + } + + case .failure: + break + } + } + } +} + +// MARK: - Rows +private extension DogeTransactionDetailsViewController { + func addRows() { + // MARK: Inputs + + let inputsRow = LabelRow() { + $0.disabled = true + $0.tag = DogeRows.inputs.tag + $0.title = DogeRows.inputs.localized + + if let value = (transaction as? DogeRawTransaction)?.inputs.count { + $0.value = String(value) + } else { + $0.value = TransactionDetailsViewControllerBase.awaitingValueString + } + }.cellUpdate { [weak self] (cell, row) in + cell.textLabel?.textColor = .black + + if let t = self?.transaction as? DogeRawTransaction { + row.value = String(t.inputs.count) + } else { + row.value = TransactionDetailsViewControllerBase.awaitingValueString + } + } + + let totalInRow = DecimalRow() { + $0.disabled = true + $0.tag = DogeRows.totalIn.tag + $0.title = DogeRows.totalIn.localized + $0.formatter = AdamantBalanceFormat.currencyFormatter(for: .full, currencySymbol: currencySymbol) + + $0.value = (transaction as? DogeRawTransaction)?.valueIn.doubleValue + }.cellSetup { (cell, _) in + cell.selectionStyle = .gray + }.onCellSelection { [weak self] (cell, row) in + if let value = row.value { + let text = AdamantBalanceFormat.full.format(value, withCurrencySymbol: self?.currencySymbol ?? nil) + self?.shareValue(text, from: cell) + } + }.cellUpdate { [weak self] (cell, row) in + cell.textLabel?.textColor = .black + + if let doge = self?.transaction as? DogeRawTransaction { + row.value = doge.valueIn.doubleValue + } else { + row.value = nil + } + } + + // MARK: Outputs + + let outputsRow = LabelRow() { + $0.disabled = true + $0.tag = DogeRows.outputs.tag + $0.title = DogeRows.outputs.localized + + if let value = (transaction as? DogeRawTransaction)?.outputs.count { + $0.value = String(value) + } else { + $0.value = TransactionDetailsViewControllerBase.awaitingValueString + } + }.cellUpdate { [weak self] (cell, row) in + cell.textLabel?.textColor = .black + + if let t = self?.transaction as? DogeRawTransaction { + row.value = String(t.outputs.count) + } else { + row.value = TransactionDetailsViewControllerBase.awaitingValueString + } + } + + let totalOutRow = DecimalRow() { + $0.disabled = true + $0.tag = DogeRows.totalOut.tag + $0.title = DogeRows.totalOut.localized + $0.formatter = AdamantBalanceFormat.currencyFormatter(for: .full, currencySymbol: currencySymbol) + + $0.value = (transaction as? DogeRawTransaction)?.valueOut.doubleValue + }.cellSetup { (cell, _) in + cell.selectionStyle = .gray + }.onCellSelection { [weak self] (cell, row) in + if let value = row.value { + let text = AdamantBalanceFormat.full.format(value, withCurrencySymbol: self?.currencySymbol ?? nil) + self?.shareValue(text, from: cell) + } + }.cellUpdate { [weak self] (cell, row) in + cell.textLabel?.textColor = .black + + if let doge = self?.transaction as? DogeRawTransaction { + row.value = doge.valueOut.doubleValue + } else { + row.value = nil + } + } + + let blockRow = LabelRow() { [weak self] in + $0.disabled = true + $0.tag = Rows.block.tag + $0.title = Rows.block.localized + + if let value = self?.blockInfo?.height { + $0.value = value + } else { + $0.value = TransactionDetailsViewControllerBase.awaitingValueString + } + }.cellSetup { (cell, _) in + cell.selectionStyle = .gray + }.onCellSelection { [weak self] (cell, row) in + if let text = row.value { + self?.shareValue(text, from: cell) + } + }.cellUpdate { [weak self] (cell, row) in + cell.textLabel?.textColor = .black + + if let value = self?.blockInfo?.height { + row.value = value + } else { + row.value = TransactionDetailsViewControllerBase.awaitingValueString + } + } + + if let section = form.sectionBy(tag: Sections.details.tag) { + let rows = [inputsRow, totalInRow, outputsRow, totalOutRow] + + if let row = form.rowBy(tag: Rows.date.tag) { + try! section.insert(row: rows[0], after: row) + try! section.insert(row: rows[1], after: rows[0]) + try! section.insert(row: rows[2], after: rows[1]) + try! section.insert(row: rows[3], after: rows[2]) + } else { + section.append(contentsOf: rows) + } + + if let row = form.rowBy(tag: Rows.confirmations.tag) { + try! section.insert(row: blockRow, after: row) + } else { + section.append(blockRow) + } + } + } } diff --git a/Adamant/Wallets/Doge/DogeTransactionsViewController.swift b/Adamant/Wallets/Doge/DogeTransactionsViewController.swift index 56bf07ef0..953aac39e 100644 --- a/Adamant/Wallets/Doge/DogeTransactionsViewController.swift +++ b/Adamant/Wallets/Doge/DogeTransactionsViewController.swift @@ -50,33 +50,6 @@ class DogeTransactionsViewController: TransactionsListViewControllerBase { self?.dialogService.showRichError(error: error) } } - -// self.walletService.getTransactions(update: { transactions in -// self.transactions.append(contentsOf: transactions) -// DispatchQueue.main.async { -// self.tableView.reloadData() -// } -// }, completion: { (result) in -// switch result { -// case .success(let transactions): -// self.transactions = transactions -// DispatchQueue.main.async { -// self.tableView.reloadData() -// } -// break -// case .failure(let error): -// if case .internalError(let message, _ ) = error { -// let localizedErrorMessage = NSLocalizedString(message, comment: "TransactionList: 'Transactions not found' message.") -// self.dialogService.showWarning(withMessage: localizedErrorMessage) -// } else { -// self.dialogService.showError(withMessage: String.adamantLocalized.transactionList.notFound, error: error) -// } -// break -// } -// DispatchQueue.main.async { -// self.refreshControl.endRefreshing() -// } -// }) } @@ -87,26 +60,32 @@ class DogeTransactionsViewController: TransactionsListViewControllerBase { } func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - tableView.deselectRow(at: indexPath, animated: true) - - let transaction = transactions[indexPath.row] - guard let controller = router.get(scene: AdamantScene.Wallets.Doge.transactionDetails) as? DogeTransactionDetailsViewController else { - return + fatalError("Failed to get DogeTransactionDetailsViewController") } - controller.transaction = transaction - controller.service = walletService + controller.service = self.walletService + dialogService.showProgress(withMessage: nil, userInteractionEnable: false) + let txId = transactions[indexPath.row].txId - if let address = walletService.wallet?.address { - if transaction.senderAddress.caseInsensitiveCompare(address) == .orderedSame { - controller.senderName = String.adamantLocalized.transactionDetails.yourAddress - } else if transaction.recipientAddress.caseInsensitiveCompare(address) == .orderedSame { - controller.recipientName = String.adamantLocalized.transactionDetails.yourAddress + walletService.getTransaction(by: txId) { result in + DispatchQueue.main.async { + self.tableView.deselectRow(at: indexPath, animated: true) + self.dialogService.dismissProgress() + } + + switch result { + case .success(let dogeTransaction): + controller.transaction = dogeTransaction + + DispatchQueue.main.async { + self.navigationController?.pushViewController(controller, animated: true) + } + + case .failure(let error): + self.dialogService.showRichError(error: error) } } - - navigationController?.pushViewController(controller, animated: true) } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { diff --git a/Adamant/Wallets/Doge/DogeWalletService+RichMessageProvider.swift b/Adamant/Wallets/Doge/DogeWalletService+RichMessageProvider.swift index eba933266..cefd73876 100644 --- a/Adamant/Wallets/Doge/DogeWalletService+RichMessageProvider.swift +++ b/Adamant/Wallets/Doge/DogeWalletService+RichMessageProvider.swift @@ -30,38 +30,6 @@ extension DogeWalletService: RichMessageProvider { comment = nil } - // MARK: 1. Sender & recipient names - - let senderName: String? - let recipientName: String? - - if let address = accountService.account?.address { - if let senderId = transaction.senderId, senderId.caseInsensitiveCompare(address) == .orderedSame { - senderName = String.adamantLocalized.transactionDetails.yourAddress - } else { - senderName = transaction.chatroom?.partner?.name - } - - if let recipientId = transaction.recipientId, recipientId.caseInsensitiveCompare(address) == .orderedSame { - recipientName = String.adamantLocalized.transactionDetails.yourAddress - } else { - recipientName = transaction.chatroom?.partner?.name - } - } else if let partner = transaction.chatroom?.partner, let id = partner.address { - if transaction.senderId == id { - senderName = partner.name - recipientName = nil - } else { - recipientName = partner.name - senderName = nil - } - } else { - senderName = nil - recipientName = nil - } - - // MARK: 2. Go go transaction - getTransaction(by: hash) { [weak self] result in dialogService.dismissProgress() guard let vc = self?.router.get(scene: AdamantScene.Wallets.Doge.transactionDetails) as? DogeTransactionDetailsViewController else { @@ -69,8 +37,6 @@ extension DogeWalletService: RichMessageProvider { } vc.service = self - vc.senderName = senderName - vc.recipientName = recipientName vc.comment = comment switch result { @@ -104,7 +70,6 @@ extension DogeWalletService: RichMessageProvider { self?.dialogService.showRichError(error: error) return } - break } DispatchQueue.main.async { diff --git a/Adamant/Wallets/Doge/DogeWalletService.swift b/Adamant/Wallets/Doge/DogeWalletService.swift index 51849df84..977fb71ee 100644 --- a/Adamant/Wallets/Doge/DogeWalletService.swift +++ b/Adamant/Wallets/Doge/DogeWalletService.swift @@ -522,41 +522,65 @@ extension DogeWalletService { } } - /* - func getTransaction(by hash: String, completion: @escaping (ApiServiceResult) -> Void) { - guard let raw = AdamantResources.dogeServers.randomElement(), let url = URL(string: raw) else { - fatalError("Failed to build DOGE endpoint URL") + func getTransaction(by hash: String, completion: @escaping (ApiServiceResult) -> Void) { + guard let url = AdamantResources.dogeServers.randomElement() else { + fatalError("Failed to get DOGE endpoint URL") } - if let address = self.wallet?.address { - // Headers - let headers = [ - "Content-Type": "application/json" - ] - - // Request url - let endpoint = url.appendingPathComponent(DogeApiCommands.getTransaction(txId: hash)) - - // MARK: Sending request - Alamofire.request(endpoint, method: .get, headers: headers).responseJSON(queue: defaultDispatchQueue) { response in + // 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))) + } - switch response.result { - case .success(let data): - if let item = data as? [String: Any] { - let transfers = DogeTransaction.transactions(from: item, for: address) - completion(.success(transfers)) - } else { - completion(.failure(.internalError(message: "No transaction", error: nil))) - } - case .failure: - completion(.failure(.internalError(message: "No transaction", error: nil))) + 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))) } - } else { - completion(.failure(.internalError(message: "DOGE Wallet: not found", error: nil))) } } - */ } // MARK: - WalletServiceWithTransfers diff --git a/Adamant/Wallets/TransactionDetailsViewControllerBase.swift b/Adamant/Wallets/TransactionDetailsViewControllerBase.swift index 8144eb913..ffcd56376 100644 --- a/Adamant/Wallets/TransactionDetailsViewControllerBase.swift +++ b/Adamant/Wallets/TransactionDetailsViewControllerBase.swift @@ -113,7 +113,7 @@ class TransactionDetailsViewControllerBase: FormViewController { var transaction: TransactionDetails? = nil - private static let awaitingValueString = "⏱" + static let awaitingValueString = "⏱" private lazy var currencyFormatter: NumberFormatter = { return AdamantBalanceFormat.currencyFormatter(for: .full, currencySymbol: currencySymbol) From 4371e51f10b8545e8ca61f103f2f1a7560b34b19 Mon Sep 17 00:00:00 2001 From: Pavel Anokhov Date: Sun, 7 Apr 2019 13:37:33 +0300 Subject: [PATCH 15/46] Sending DOGEs --- Adamant/Models/DogeTransaction.swift | 60 ++++++++++--- ...DogeTransactionDetailsViewController.swift | 86 +++++++++---------- .../Doge/DogeTransferViewController.swift | 62 ++++++------- .../Wallets/Doge/DogeWalletService+Send.swift | 71 +++++++-------- Adamant/Wallets/Doge/DogeWalletService.swift | 53 ++++++------ ...TransactionDetailsViewControllerBase.swift | 27 ++++-- 6 files changed, 200 insertions(+), 159 deletions(-) diff --git a/Adamant/Models/DogeTransaction.swift b/Adamant/Models/DogeTransaction.swift index 8f6579cef..f542d60c6 100644 --- a/Adamant/Models/DogeTransaction.swift +++ b/Adamant/Models/DogeTransaction.swift @@ -30,8 +30,17 @@ struct DogeTransaction: TransactionDetails { let txId = dogeTransaction.txId let dateValue = dogeTransaction.date let feeValue = dogeTransaction.fee - let confirmationsValue = String(dogeTransaction.confirmations) - let transactionStatus: TransactionStatus = dogeTransaction.confirmations > 0 ? .success : .pending + + let confirmationsValue: String? + let transactionStatus: TransactionStatus + + if let confirmations = dogeTransaction.confirmations { + confirmationsValue = String(confirmations) + transactionStatus = confirmations > 0 ? .success : .pending + } else { + confirmationsValue = nil + transactionStatus = .pending + } // Transfers let myInputs = dogeTransaction.inputs.filter { $0.sender == address } @@ -95,14 +104,14 @@ struct DogeTransaction: TransactionDetails { // MARK: - Raw Doge Transaction, for easy parsing struct DogeRawTransaction: TransactionDetails { let txId: String - let date: Date + let date: Date? let valueIn: Decimal let valueOut: Decimal let fee: Decimal - let confirmations: Int - let blockHash: String + let confirmations: Int? + let blockHash: String? let inputs: [DogeInput] let outputs: [DogeOutput] @@ -110,10 +119,17 @@ struct DogeRawTransaction: TransactionDetails { // MARK: Transaction Details var feeValue: Decimal? { return fee } var dateValue: Date? { return date } - var confirmationsValue: String? { return String(confirmations) } var transactionStatus: TransactionStatus? { return .success } var blockValue: String? { return nil } + var confirmationsValue: String? { + if let confirmations = confirmations { + return String(confirmations) + } else { + return nil + } + } + // Not used in details var senderAddress: String { return "" } var recipientAddress: String { return "" } @@ -137,17 +153,21 @@ extension DogeRawTransaction: Decodable { init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) - // MARK: Base + // MARK: Required self.txId = try container.decode(String.self, forKey: .txId) - - let timeInterval = try container.decode(TimeInterval.self, forKey: .date) - self.date = Date(timeIntervalSince1970: timeInterval) - self.valueIn = try container.decode(Decimal.self, forKey: .valueIn) self.valueOut = try container.decode(Decimal.self, forKey: .valueOut) self.fee = try container.decode(Decimal.self, forKey: .fee) - self.confirmations = try container.decode(Int.self, forKey: .confirmations) - self.blockHash = try container.decode(String.self, forKey: .blockHash) + + // 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 @@ -235,6 +255,20 @@ struct DogeOutput: Decodable { "fees": 1, "firstSeenTs": 1554298214 } + +new transaction: +{ + "txid": "60cd612335c9797ea67689b9cde4a41e20c20c1b96eb0731c59c5b0eab8bad31", + "version": 1, + "locktime": 0, + "vin": [], + "vout": [], + "valueOut": 283, + "size": 225, + "valueIn": 284, + "fees": 1 +} + */ /* Inputs diff --git a/Adamant/Wallets/Doge/DogeTransactionDetailsViewController.swift b/Adamant/Wallets/Doge/DogeTransactionDetailsViewController.swift index 7eee1e334..97a5b0821 100644 --- a/Adamant/Wallets/Doge/DogeTransactionDetailsViewController.swift +++ b/Adamant/Wallets/Doge/DogeTransactionDetailsViewController.swift @@ -94,71 +94,71 @@ class DogeTransactionDetailsViewController: TransactionDetailsViewControllerBase } @objc func refresh() { - guard let id = transaction?.txId, let service = service else { - refreshControl.endRefreshing() + updateTransaction { [weak self] error in + 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 id = transaction?.txId else { + completion?(nil) return } - + service.getTransaction(by: id) { [weak self] result in switch result { case .success(let trs): self?.transaction = trs - DispatchQueue.main.async { - self?.tableView.reloadData() - self?.refreshControl.endRefreshing() - } - if let blockInfo = self?.blockInfo, blockInfo.hash == trs.blockHash { // No need to update block id } else { self?.updateBlockNumber() } - case .failure(let error): - self?.dialogService.showRichError(error: error) - - DispatchQueue.main.async { - self?.refreshControl.endRefreshing() + DispatchQueue.main.async { [weak self] in + self?.tableView.reloadData() } + + completion?(nil) + + case .failure(let error): + completion?(error) } } } - // MARK: Autoupdate - - func startUpdate() { - timer?.invalidate() - timer = Timer.scheduledTimer(withTimeInterval: autoupdateInterval, repeats: true) { [weak self] _ in - guard let id = self?.transaction?.txId, let service = self?.service else { - return - } - - service.getTransaction(by: id) { result in - switch result { - case .success(let trs): - self?.transaction = trs - + func updateBlockNumber() { + guard let blockHash = (transaction as? DogeRawTransaction)?.blockHash else { + if blockInfo != nil { + blockInfo = nil + + if let row = form.rowBy(tag: Rows.block.tag) { DispatchQueue.main.async { - self?.tableView.reloadData() + row.updateCell() } - - case .failure: - break } } - } - } - - func stopUpdate() { - timer?.invalidate() - } - - // MARK: - Update block number - func updateBlockNumber() { - guard let blockHash = (transaction as? DogeRawTransaction)?.blockHash else { - blockInfo = nil - form.rowBy(tag: Rows.block.tag)?.updateCell() + return } diff --git a/Adamant/Wallets/Doge/DogeTransferViewController.swift b/Adamant/Wallets/Doge/DogeTransferViewController.swift index 35c8e6519..4037f80d8 100644 --- a/Adamant/Wallets/Doge/DogeTransferViewController.swift +++ b/Adamant/Wallets/Doge/DogeTransferViewController.swift @@ -76,42 +76,46 @@ class DogeTransferViewController: TransferViewControllerBase { case .success(let transaction): vc.dialogService.showSuccess(withMessage: String.adamantLocalized.transfer.transferSuccess) - if let detailsVc = vc.router.get(scene: AdamantScene.Wallets.Doge.transactionDetails) as? DogeTransactionDetailsViewController { - 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) - } else { + 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): - if case let .internalError(message, _) = error, message == "No transaction" { - vc.dialogService.showSuccess(withMessage: String.adamantLocalized.transfer.transferSuccess) - if let detailsVc = vc.router.get(scene: AdamantScene.Wallets.Doge.transactionDetails) as? DogeTransactionDetailsViewController { - 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) - } else { - vc.delegate?.transferViewController(vc, didFinishWithTransfer: transaction, detailsViewController: nil) - } - } else { + 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) } } diff --git a/Adamant/Wallets/Doge/DogeWalletService+Send.swift b/Adamant/Wallets/Doge/DogeWalletService+Send.swift index be8d858aa..fe882c65a 100644 --- a/Adamant/Wallets/Doge/DogeWalletService+Send.swift +++ b/Adamant/Wallets/Doge/DogeWalletService+Send.swift @@ -48,21 +48,18 @@ extension DogeWalletService: WalletServiceTwoStepSend { let rawAmount = NSDecimalNumber(decimal: amount * DogeWalletService.multiplier).uint64Value let fee = NSDecimalNumber(decimal: self.transactionFee * DogeWalletService.multiplier).uint64Value - // MARK: Go background - defaultDispatchQueue.async { - // MARK: 2. Search for unspent transactions - self.getUnspentTransactions({ result in - switch result { - case .success(let utxos): - // MARK: 3. Create local transaction - let transaction = BitcoinKit.Transaction.createNewTransaction(toAddress: toAddress, amount: rawAmount, fee: fee, changeAddress: changeAddress, utxos: utxos, keys: [key]) - completion(.success(result: transaction)) - break - case .failure: - completion(.failure(error: .notEnoughMoney)) - break - } - }) + // MARK: 2. Search for unspent transactions + self.getUnspentTransactions { result in + switch result { + case .success(let utxos): + // MARK: 3. Create local transaction + let transaction = BitcoinKit.Transaction.createNewTransaction(toAddress: toAddress, amount: rawAmount, fee: fee, changeAddress: changeAddress, utxos: utxos, keys: [key]) + completion(.success(result: transaction)) + break + case .failure: + completion(.failure(error: .notEnoughMoney)) + break + } } } @@ -71,37 +68,33 @@ extension DogeWalletService: WalletServiceTwoStepSend { fatalError("Failed to get DOGE endpoint URL") } + // Request url + let endpoint = url.appendingPathComponent(DogeApiCommands.sendTransaction()) + // Headers let headers = [ "Content-Type": "application/json" ] - // Request url - let endpoint = url.appendingPathComponent(DogeApiCommands.sendTransaction()) + // MARK: Prepare params + let txHex = transaction.serialized().hex - defaultDispatchQueue.async { - // 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: self.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: - completion(.failure(error: .internalError(message: "DOGE Wallet: server not response", error: nil))) + 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))) } } } diff --git a/Adamant/Wallets/Doge/DogeWalletService.swift b/Adamant/Wallets/Doge/DogeWalletService.swift index 977fb71ee..89888ac82 100644 --- a/Adamant/Wallets/Doge/DogeWalletService.swift +++ b/Adamant/Wallets/Doge/DogeWalletService.swift @@ -66,7 +66,7 @@ class DogeWalletService: WalletService { static var currencySymbol = "DOGE" static var currencyLogo = #imageLiteral(resourceName: "wallet_doge") - static let multiplier = Decimal(sign: .plus, exponent: -8, significand: 1) + 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 @@ -305,7 +305,7 @@ extension DogeWalletService { switch response.result { case .success(let data): if let raw = Decimal(string: data) { - let balance = raw * DogeWalletService.multiplier + let balance = raw / DogeWalletService.multiplier completion(.success(result: balance)) } else { completion(.failure(error: .remoteServiceError(message: "DOGE Wallet: \(data)"))) @@ -485,37 +485,36 @@ extension DogeWalletService { // 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 + } - if let items = data as? [[String: Any]] { - var utxos = [UnspentTransaction]() - for item in items { - if let txid = item["txid"] as? String, - let vout = item["vout"] as? NSNumber, - let amount = item["amount"] as? NSNumber { - - 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 - - print(txid, txIndex, lockScript.hex, value) - - let unspentOutput = TransactionOutput(value: value, lockingScript: lockScript) - let unspentOutpoint = TransactionOutPoint(hash: txHash, index: txIndex) - let utxo = UnspentTransaction(output: unspentOutput, outpoint: unspentOutpoint) - - utxos.append(utxo) - } + 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 } - completion(.success(utxos)) - } else { - completion(.failure(.internalError(message: "DOGE Wallet: not valid response", error: nil))) + + 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))) } diff --git a/Adamant/Wallets/TransactionDetailsViewControllerBase.swift b/Adamant/Wallets/TransactionDetailsViewControllerBase.swift index ffcd56376..487faf0f5 100644 --- a/Adamant/Wallets/TransactionDetailsViewControllerBase.swift +++ b/Adamant/Wallets/TransactionDetailsViewControllerBase.swift @@ -112,6 +112,12 @@ 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 + }() static let awaitingValueString = "⏱" @@ -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) From 3f201b640833eeba4f40da5e0c909922f2583bd4 Mon Sep 17 00:00:00 2001 From: Pavel Anokhov Date: Sun, 7 Apr 2019 14:13:34 +0300 Subject: [PATCH 16/46] DOGE status check fixed --- Adamant/Models/DogeTransaction.swift | 11 ++- .../RichMessageProvider.swift | 2 +- .../Stories/Chats/ChatViewController.swift | 2 + ...e+RichMessageProviderWithStatusCheck.swift | 69 ++++++++++++------- 4 files changed, 56 insertions(+), 28 deletions(-) diff --git a/Adamant/Models/DogeTransaction.swift b/Adamant/Models/DogeTransaction.swift index f542d60c6..95734d2ff 100644 --- a/Adamant/Models/DogeTransaction.swift +++ b/Adamant/Models/DogeTransaction.swift @@ -119,7 +119,16 @@ struct DogeRawTransaction: TransactionDetails { // MARK: Transaction Details var feeValue: Decimal? { return fee } var dateValue: Date? { return date } - var transactionStatus: TransactionStatus? { return .success } + var transactionStatus: TransactionStatus? { + if let confirmations = confirmations { + return confirmations > 0 ? .success : .pending + } else if let date = date { + // oldter than 15m + return date.timeIntervalSinceNow > -60 * 15 ? .pending : TransactionStatus.failed + } else { + return .pending + } + } var blockValue: String? { return nil } var confirmationsValue: String? { diff --git a/Adamant/ServiceProtocols/RichMessageProvider.swift b/Adamant/ServiceProtocols/RichMessageProvider.swift index cde72b977..783985b47 100644 --- a/Adamant/ServiceProtocols/RichMessageProvider.swift +++ b/Adamant/ServiceProtocols/RichMessageProvider.swift @@ -40,6 +40,6 @@ protocol RichMessageProviderWithStatusCheck: RichMessageProvider { extension RichMessageProviderWithStatusCheck { var delayBetweenChecks: TimeInterval { - return 10.0 + return 30.0 } } diff --git a/Adamant/Stories/Chats/ChatViewController.swift b/Adamant/Stories/Chats/ChatViewController.swift index 0b0821641..13a48b76f 100644 --- a/Adamant/Stories/Chats/ChatViewController.swift +++ b/Adamant/Stories/Chats/ChatViewController.swift @@ -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/Wallets/Doge/DogeWalletService+RichMessageProviderWithStatusCheck.swift b/Adamant/Wallets/Doge/DogeWalletService+RichMessageProviderWithStatusCheck.swift index ab2ab2c6f..762f0cc96 100644 --- a/Adamant/Wallets/Doge/DogeWalletService+RichMessageProviderWithStatusCheck.swift +++ b/Adamant/Wallets/Doge/DogeWalletService+RichMessageProviderWithStatusCheck.swift @@ -15,6 +15,11 @@ extension DogeWalletService: RichMessageProviderWithStatusCheck { return } + guard let walletAddress = dogeWallet?.address else { + completion(.failure(error: .notLogged)) + return + } + getTransaction(by: hash) { result in switch result { case .success(let dogeTransaction): @@ -29,27 +34,13 @@ extension DogeWalletService: RichMessageProviderWithStatusCheck { return } - // MARK: Check address - if transaction.isOutgoing { - guard dogeTransaction.senderAddress == self.dogeWallet?.address else { - completion(.success(result: .warning)) - return - } - } else { - guard dogeTransaction.recipientAddress == self.dogeWallet?.address else { - completion(.success(result: .warning)) - return - } - } - - - + // MARK: Check date guard let sentDate = dogeTransaction.dateValue else { let timeAgo = -1 * date.timeIntervalSinceNow let result: TransactionStatus - if timeAgo > 60 * 60 * 3 { - // 3h waiting for pending status + if timeAgo > 60 * 10 { + // 10m waiting for pending status result = .failed } else { // Note: No info about processing transactions @@ -59,7 +50,6 @@ extension DogeWalletService: RichMessageProviderWithStatusCheck { return } - // MARK: Check date let start = date.addingTimeInterval(-60 * 5) let end = date.addingTimeInterval(60 * 5) let range = start...end @@ -69,23 +59,50 @@ extension DogeWalletService: RichMessageProviderWithStatusCheck { return } - // MARK: Check amount - if let raw = transaction.richContent?[RichContentKeys.transfer.amount], let reported = AdamantBalanceFormat.deserializeBalance(from: raw) { - guard reported == dogeTransaction.amountValue 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: .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 * 60 * 3 { - // 3h waiting for pending status + if timeAgo > 60 * 10 { + // 10m waiting for pending status result = .failed } else { // Note: No info about processing transactions From 00c4cb2503ae8dfa246ec811b59bc1664cee98e5 Mon Sep 17 00:00:00 2001 From: Pavel Anokhov Date: Sun, 7 Apr 2019 15:34:04 +0300 Subject: [PATCH 17/46] DOGE: Transactions list fixed --- Adamant/Models/DogeTransaction.swift | 48 ++++++++++++++++++++-------- 1 file changed, 34 insertions(+), 14 deletions(-) diff --git a/Adamant/Models/DogeTransaction.swift b/Adamant/Models/DogeTransaction.swift index 95734d2ff..030a03bab 100644 --- a/Adamant/Models/DogeTransaction.swift +++ b/Adamant/Models/DogeTransaction.swift @@ -43,18 +43,40 @@ struct DogeTransaction: TransactionDetails { } // Transfers - let myInputs = dogeTransaction.inputs.filter { $0.sender == address } - let myOutputs = dogeTransaction.outputs.filter { $0.addresses.contains(address) } + var myInputs = dogeTransaction.inputs.filter { $0.sender == address } + var myOutputs: [DogeOutput] = dogeTransaction.outputs.filter { $0.addresses.contains(address) } + + var totalInputsValue = myInputs.map { $0.value }.reduce(0, +) - dogeTransaction.fee + var totalOutputsValue = myOutputs.map { $0.value }.reduce(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(dogeTransaction.inputs.map { $0.sender }.filter { $0 != address }) + let recipients = Set(dogeTransaction.outputs.compactMap { $0.addresses.first }.filter { $0 != address }) // MARK: Inputs if myInputs.count > 0 { - let amountValue = myInputs.map { $0.value }.reduce(0, +) - let recipient: String - if dogeTransaction.outputs.count == 1 { - recipient = dogeTransaction.outputs.first?.addresses.first ?? "" + if recipients.count == 1, let name = recipients.first { + recipient = name } else { - recipient = "\(dogeTransaction.outputs.count) recipients" + recipient = "\(recipients.count) recipients" } let inputTransaction = DogeTransaction(txId: txId, @@ -62,7 +84,7 @@ struct DogeTransaction: TransactionDetails { blockValue: blockId, senderAddress: address, recipientAddress: recipient, - amountValue: amountValue, + amountValue: totalInputsValue, feeValue: feeValue, confirmationsValue: confirmationsValue, isOutgoing: true, @@ -73,13 +95,11 @@ struct DogeTransaction: TransactionDetails { // MARK: Outputs if myOutputs.count > 0 { - let amountValue = myOutputs.map { $0.value }.reduce(0, +) - let sender: String - if dogeTransaction.inputs.count == 1 { - sender = dogeTransaction.inputs.first?.sender ?? "" + if senders.count == 1, let name = senders.first { + sender = name } else { - sender = "\(dogeTransaction.inputs.count) senders" + sender = "\(senders.count) senders" } let outputTransaction = DogeTransaction(txId: txId, @@ -87,7 +107,7 @@ struct DogeTransaction: TransactionDetails { blockValue: blockId, senderAddress: sender, recipientAddress: address, - amountValue: amountValue, + amountValue: totalOutputsValue, feeValue: feeValue, confirmationsValue: confirmationsValue, isOutgoing: false, From c856fd9fe0ab39b96b20b33d3a267caf3a16800f Mon Sep 17 00:00:00 2001 From: Pavel Anokhov Date: Sun, 7 Apr 2019 15:39:50 +0300 Subject: [PATCH 18/46] DE localisation fixes. --- Adamant/Assets/l18n/de.lproj/Localizable.strings | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Adamant/Assets/l18n/de.lproj/Localizable.strings b/Adamant/Assets/l18n/de.lproj/Localizable.strings index 7673fd332..007332211 100755 --- a/Adamant/Assets/l18n/de.lproj/Localizable.strings +++ b/Adamant/Assets/l18n/de.lproj/Localizable.strings @@ -158,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"; @@ -182,7 +182,7 @@ "AccountTab.Title" = "Konto"; /* Account tab: Delegates section title */ -"AccountTab.Section.Delegates" = "Delegate"; +"AccountTab.Section.Delegates" = "Delegierte"; /* Product name */ "ADAMANT" = "ADAMANT"; @@ -290,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"; From 404486042b95dffb34787e7042ae5d811cdb682a Mon Sep 17 00:00:00 2001 From: Pavel Anokhov Date: Sun, 7 Apr 2019 15:40:13 +0300 Subject: [PATCH 19/46] Version --- Adamant/Info.plist | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Adamant/Info.plist b/Adamant/Info.plist index 01e912bca..fe6cde92e 100644 --- a/Adamant/Info.plist +++ b/Adamant/Info.plist @@ -17,9 +17,9 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.4 + 1.5 CFBundleVersion - 70 + 71 LSRequiresIPhoneOS NSAppTransportSecurity From 35f45a5025152c28972401988c64fa43b7e152c0 Mon Sep 17 00:00:00 2001 From: Pavel Anokhov Date: Mon, 8 Apr 2019 12:29:23 +0300 Subject: [PATCH 20/46] Some project settings. --- Adamant.xcodeproj/project.pbxproj | 3 +-- .../xcshareddata/WorkspaceSettings.xcsettings | 5 +++++ 2 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 Adamant.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings diff --git a/Adamant.xcodeproj/project.pbxproj b/Adamant.xcodeproj/project.pbxproj index 0a7bdb07e..46bc4c464 100644 --- a/Adamant.xcodeproj/project.pbxproj +++ b/Adamant.xcodeproj/project.pbxproj @@ -1917,7 +1917,6 @@ PRODUCT_NAME = ADAMANT; SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 4.2; }; name = Debug; @@ -1971,7 +1970,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 @@ + + + + + From fc83f24a3e6186d7fb763ca81ec172f92127bb2b Mon Sep 17 00:00:00 2001 From: Pavel Anokhov Date: Tue, 9 Apr 2019 21:34:24 +0300 Subject: [PATCH 21/46] DOGE transaction refactored, Details improved. --- Adamant/Models/DogeTransaction.swift | 127 +++------ ...DogeTransactionDetailsViewController.swift | 267 +++--------------- .../Doge/DogeTransactionsViewController.swift | 44 ++- .../Doge/DogeTransferViewController.swift | 8 +- ...ogeWalletService+RichMessageProvider.swift | 24 +- ...e+RichMessageProviderWithStatusCheck.swift | 19 +- Adamant/Wallets/Doge/DogeWalletService.swift | 2 +- 7 files changed, 153 insertions(+), 338 deletions(-) diff --git a/Adamant/Models/DogeTransaction.swift b/Adamant/Models/DogeTransaction.swift index 030a03bab..f7baf9f07 100644 --- a/Adamant/Models/DogeTransaction.swift +++ b/Adamant/Models/DogeTransaction.swift @@ -22,19 +22,30 @@ struct DogeTransaction: TransactionDetails { let isOutgoing: Bool let transactionStatus: TransactionStatus? +} + + +// MARK: - Raw Doge Transaction, for easy parsing +struct DogeRawTransaction { + let txId: String + let date: Date? - static func map(from dogeTransaction: DogeRawTransaction, for address: String, with blockId: String?) -> [DogeTransaction] { - var transactions = [DogeTransaction]() - + 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 txId = dogeTransaction.txId - let dateValue = dogeTransaction.date - let feeValue = dogeTransaction.fee - let confirmationsValue: String? let transactionStatus: TransactionStatus - if let confirmations = dogeTransaction.confirmations { + if let confirmations = confirmations { confirmationsValue = String(confirmations) transactionStatus = confirmations > 0 ? .success : .pending } else { @@ -43,10 +54,10 @@ struct DogeTransaction: TransactionDetails { } // Transfers - var myInputs = dogeTransaction.inputs.filter { $0.sender == address } - var myOutputs: [DogeOutput] = dogeTransaction.outputs.filter { $0.addresses.contains(address) } + var myInputs = inputs.filter { $0.sender == address } + var myOutputs = outputs.filter { $0.addresses.contains(address) } - var totalInputsValue = myInputs.map { $0.value }.reduce(0, +) - dogeTransaction.fee + var totalInputsValue = myInputs.map { $0.value }.reduce(0, +) - fee var totalOutputsValue = myOutputs.map { $0.value }.reduce(0, +) if totalInputsValue > totalOutputsValue { @@ -67,8 +78,8 @@ struct DogeTransaction: TransactionDetails { } } - let senders = Set(dogeTransaction.inputs.map { $0.sender }.filter { $0 != address }) - let recipients = Set(dogeTransaction.outputs.compactMap { $0.addresses.first }.filter { $0 != address }) + let senders = Set(inputs.map { $0.sender }.filter { $0 != address }) + let recipients = Set(outputs.compactMap { $0.addresses.first }.filter { $0 != address }) // MARK: Inputs if myInputs.count > 0 { @@ -80,90 +91,40 @@ struct DogeTransaction: TransactionDetails { } let inputTransaction = DogeTransaction(txId: txId, - dateValue: dateValue, + dateValue: date, blockValue: blockId, senderAddress: address, recipientAddress: recipient, amountValue: totalInputsValue, - feeValue: feeValue, + feeValue: fee, confirmationsValue: confirmationsValue, isOutgoing: true, transactionStatus: transactionStatus) - transactions.append(inputTransaction) + return inputTransaction } // MARK: Outputs - if myOutputs.count > 0 { - let sender: String - if senders.count == 1, let name = senders.first { - sender = name - } else { - sender = "\(senders.count) senders" - } - - let outputTransaction = DogeTransaction(txId: txId, - dateValue: dateValue, - blockValue: blockId, - senderAddress: sender, - recipientAddress: address, - amountValue: totalOutputsValue, - feeValue: feeValue, - confirmationsValue: confirmationsValue, - isOutgoing: false, - transactionStatus: transactionStatus) - - transactions.append(outputTransaction) - } - - return transactions - } -} - - -// MARK: - Raw Doge Transaction, for easy parsing -struct DogeRawTransaction: TransactionDetails { - 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] - - // MARK: Transaction Details - var feeValue: Decimal? { return fee } - var dateValue: Date? { return date } - var transactionStatus: TransactionStatus? { - if let confirmations = confirmations { - return confirmations > 0 ? .success : .pending - } else if let date = date { - // oldter than 15m - return date.timeIntervalSinceNow > -60 * 15 ? .pending : TransactionStatus.failed + let sender: String + if senders.count == 1, let name = senders.first { + sender = name } else { - return .pending - } - } - var blockValue: String? { return nil } - - var confirmationsValue: String? { - if let confirmations = confirmations { - return String(confirmations) - } else { - return nil + sender = "\(senders.count) senders" } + + 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 } - - // Not used in details - var senderAddress: String { return "" } - var recipientAddress: String { return "" } - var amountValue: Decimal { return 0 } - var isOutgoing: Bool { return false } } extension DogeRawTransaction: Decodable { diff --git a/Adamant/Wallets/Doge/DogeTransactionDetailsViewController.swift b/Adamant/Wallets/Doge/DogeTransactionDetailsViewController.swift index 97a5b0821..d61e4de7b 100644 --- a/Adamant/Wallets/Doge/DogeTransactionDetailsViewController.swift +++ b/Adamant/Wallets/Doge/DogeTransactionDetailsViewController.swift @@ -10,36 +10,13 @@ import UIKit import Eureka class DogeTransactionDetailsViewController: TransactionDetailsViewControllerBase { - // MARK: Rows & Sections - enum DogeRows { - case inputs, totalIn, outputs, totalOut - - var tag: String { - switch self { - case .inputs: return "inputs" - case .totalIn: return "totalIn" - case .outputs: return "outputs" - case .totalOut: return "totalOut" - } - } - - var localized: String { - switch self { - case .inputs: return "Inputs" - case .totalIn: return "Total In" - case .outputs: return "Outputs" - case .totalOut: return "Total Out" - } - } - } - // MARK: - Dependencies weak var service: DogeWalletService? // MARK: - Properties - private var blockInfo: (hash: String, height: String)? = nil + private var cachedBlockInfo: (hash: String, height: String)? = nil private let autoupdateInterval: TimeInterval = 5.0 weak var timer: Timer? @@ -58,22 +35,7 @@ class DogeTransactionDetailsViewController: TransactionDetailsViewControllerBase super.viewDidLoad() if service != nil { tableView.refreshControl = refreshControl } - // MARK: Cleanup rows - if let section = form.sectionBy(tag: Sections.details.tag) { - let rows: [Rows] = [.from, .to, .amount, .block] - - for row in rows { - if let r = form.rowBy(tag: row.tag), let index = section.firstIndex(of: r) { - section.remove(at: index) - } - } - } - - // MARK: Add rows - addRows() - - // MARK: Update block number - updateBlockNumber() + updateTransaction() // MARK: Start update if transaction != nil { @@ -119,7 +81,7 @@ class DogeTransactionDetailsViewController: TransactionDetailsViewControllerBase // MARK: Updating methods func updateTransaction(completion: ((RichError?) -> Void)? = nil) { - guard let service = service, let id = transaction?.txId else { + guard let service = service, let address = service.wallet?.address, let id = transaction?.txId else { completion?(nil) return } @@ -127,200 +89,47 @@ class DogeTransactionDetailsViewController: TransactionDetailsViewControllerBase service.getTransaction(by: id) { [weak self] result in switch result { case .success(let trs): - self?.transaction = trs - - if let blockInfo = self?.blockInfo, blockInfo.hash == trs.blockHash { - // No need to update block id + 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?.updateBlockNumber() - } - - DispatchQueue.main.async { [weak self] in - self?.tableView.reloadData() + self?.transaction = trs.asDogeTransaction(for: address) + + DispatchQueue.main.async { [weak self] in + self?.tableView.reloadData() + } + + completion?(nil) } - completion?(nil) - case .failure(let error): completion?(error) } } } - - func updateBlockNumber() { - guard let blockHash = (transaction as? DogeRawTransaction)?.blockHash else { - if blockInfo != nil { - blockInfo = nil - - if let row = form.rowBy(tag: Rows.block.tag) { - DispatchQueue.main.async { - row.updateCell() - } - } - } - - return - } - - service?.getBlockId(by: blockHash) { [weak self] result in - switch result { - case .success(let height): - self?.blockInfo = (hash: blockHash, height: height) - - guard let row: LabelRow = self?.form.rowBy(tag: Rows.block.tag) else { - break - } - - DispatchQueue.main.async { - row.value = height - row.updateCell() - } - - case .failure: - break - } - } - } -} - -// MARK: - Rows -private extension DogeTransactionDetailsViewController { - func addRows() { - // MARK: Inputs - - let inputsRow = LabelRow() { - $0.disabled = true - $0.tag = DogeRows.inputs.tag - $0.title = DogeRows.inputs.localized - - if let value = (transaction as? DogeRawTransaction)?.inputs.count { - $0.value = String(value) - } else { - $0.value = TransactionDetailsViewControllerBase.awaitingValueString - } - }.cellUpdate { [weak self] (cell, row) in - cell.textLabel?.textColor = .black - - if let t = self?.transaction as? DogeRawTransaction { - row.value = String(t.inputs.count) - } else { - row.value = TransactionDetailsViewControllerBase.awaitingValueString - } - } - - let totalInRow = DecimalRow() { - $0.disabled = true - $0.tag = DogeRows.totalIn.tag - $0.title = DogeRows.totalIn.localized - $0.formatter = AdamantBalanceFormat.currencyFormatter(for: .full, currencySymbol: currencySymbol) - - $0.value = (transaction as? DogeRawTransaction)?.valueIn.doubleValue - }.cellSetup { (cell, _) in - cell.selectionStyle = .gray - }.onCellSelection { [weak self] (cell, row) in - if let value = row.value { - let text = AdamantBalanceFormat.full.format(value, withCurrencySymbol: self?.currencySymbol ?? nil) - self?.shareValue(text, from: cell) - } - }.cellUpdate { [weak self] (cell, row) in - cell.textLabel?.textColor = .black - - if let doge = self?.transaction as? DogeRawTransaction { - row.value = doge.valueIn.doubleValue - } else { - row.value = nil - } - } - - // MARK: Outputs - - let outputsRow = LabelRow() { - $0.disabled = true - $0.tag = DogeRows.outputs.tag - $0.title = DogeRows.outputs.localized - - if let value = (transaction as? DogeRawTransaction)?.outputs.count { - $0.value = String(value) - } else { - $0.value = TransactionDetailsViewControllerBase.awaitingValueString - } - }.cellUpdate { [weak self] (cell, row) in - cell.textLabel?.textColor = .black - - if let t = self?.transaction as? DogeRawTransaction { - row.value = String(t.outputs.count) - } else { - row.value = TransactionDetailsViewControllerBase.awaitingValueString - } - } - - let totalOutRow = DecimalRow() { - $0.disabled = true - $0.tag = DogeRows.totalOut.tag - $0.title = DogeRows.totalOut.localized - $0.formatter = AdamantBalanceFormat.currencyFormatter(for: .full, currencySymbol: currencySymbol) - - $0.value = (transaction as? DogeRawTransaction)?.valueOut.doubleValue - }.cellSetup { (cell, _) in - cell.selectionStyle = .gray - }.onCellSelection { [weak self] (cell, row) in - if let value = row.value { - let text = AdamantBalanceFormat.full.format(value, withCurrencySymbol: self?.currencySymbol ?? nil) - self?.shareValue(text, from: cell) - } - }.cellUpdate { [weak self] (cell, row) in - cell.textLabel?.textColor = .black - - if let doge = self?.transaction as? DogeRawTransaction { - row.value = doge.valueOut.doubleValue - } else { - row.value = nil - } - } - - let blockRow = LabelRow() { [weak self] in - $0.disabled = true - $0.tag = Rows.block.tag - $0.title = Rows.block.localized - - if let value = self?.blockInfo?.height { - $0.value = value - } else { - $0.value = TransactionDetailsViewControllerBase.awaitingValueString - } - }.cellSetup { (cell, _) in - cell.selectionStyle = .gray - }.onCellSelection { [weak self] (cell, row) in - if let text = row.value { - self?.shareValue(text, from: cell) - } - }.cellUpdate { [weak self] (cell, row) in - cell.textLabel?.textColor = .black - - if let value = self?.blockInfo?.height { - row.value = value - } else { - row.value = TransactionDetailsViewControllerBase.awaitingValueString - } - } - - if let section = form.sectionBy(tag: Sections.details.tag) { - let rows = [inputsRow, totalInRow, outputsRow, totalOutRow] - - if let row = form.rowBy(tag: Rows.date.tag) { - try! section.insert(row: rows[0], after: row) - try! section.insert(row: rows[1], after: rows[0]) - try! section.insert(row: rows[2], after: rows[1]) - try! section.insert(row: rows[3], after: rows[2]) - } else { - section.append(contentsOf: rows) - } - - if let row = form.rowBy(tag: Rows.confirmations.tag) { - try! section.insert(row: blockRow, after: row) - } else { - section.append(blockRow) - } - } - } } diff --git a/Adamant/Wallets/Doge/DogeTransactionsViewController.swift b/Adamant/Wallets/Doge/DogeTransactionsViewController.swift index 953aac39e..5ef71cd73 100644 --- a/Adamant/Wallets/Doge/DogeTransactionsViewController.swift +++ b/Adamant/Wallets/Doge/DogeTransactionsViewController.swift @@ -64,26 +64,52 @@ class DogeTransactionsViewController: TransactionsListViewControllerBase { fatalError("Failed to get DogeTransactionDetailsViewController") } + guard let walletService = walletService, 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) { result in - DispatchQueue.main.async { - self.tableView.deselectRow(at: indexPath, animated: true) - self.dialogService.dismissProgress() - } - switch result { case .success(let dogeTransaction): - controller.transaction = dogeTransaction + guard let blockHash = dogeTransaction.blockHash else { + controller.transaction = dogeTransaction.asDogeTransaction(for: sender) + DispatchQueue.main.async { + self.navigationController?.pushViewController(controller, animated: true) + self.tableView.deselectRow(at: indexPath, animated: true) + self.dialogService.dismissProgress() + } + break + } - DispatchQueue.main.async { - self.navigationController?.pushViewController(controller, animated: true) + walletService.getBlockId(by: blockHash) { result in + let transaction: DogeTransaction + switch result { + case .success(let id): + transaction = dogeTransaction.asDogeTransaction(for: sender, blockId: id) + + case .failure: + transaction = dogeTransaction.asDogeTransaction(for: sender) + } + + controller.transaction = transaction + + DispatchQueue.main.async { + self.tableView.deselectRow(at: indexPath, animated: true) + self.dialogService.dismissProgress() + self.navigationController?.pushViewController(controller, animated: true) + } } case .failure(let error): - self.dialogService.showRichError(error: error) + DispatchQueue.main.async { + self.tableView.deselectRow(at: indexPath, animated: true) + self.dialogService.dismissProgress() + self.dialogService.showRichError(error: error) + } } } } diff --git a/Adamant/Wallets/Doge/DogeTransferViewController.swift b/Adamant/Wallets/Doge/DogeTransferViewController.swift index 4037f80d8..8e7c62882 100644 --- a/Adamant/Wallets/Doge/DogeTransferViewController.swift +++ b/Adamant/Wallets/Doge/DogeTransferViewController.swift @@ -41,11 +41,11 @@ class DogeTransferViewController: TransferViewControllerBase { comments = "" } - guard let service = service as? DogeWalletService, let recipient = recipientAddress, let amount = amount else { + guard let service = service as? DogeWalletService, let recipient = recipientAddress, let amount = amount, let dialogService = dialogService else { return } - guard let dialogService = dialogService else { + guard let sender = service.wallet?.address else { return } @@ -73,9 +73,11 @@ class DogeTransferViewController: TransferViewControllerBase { service.getTransaction(by: hash) { result in switch result { - case .success(let transaction): + case .success(let dogeRawTransaction): vc.dialogService.showSuccess(withMessage: String.adamantLocalized.transfer.transferSuccess) + let transaction = dogeRawTransaction.asDogeTransaction(for: sender) + guard let detailsVc = vc.router.get(scene: AdamantScene.Wallets.Doge.transactionDetails) as? DogeTransactionDetailsViewController else { vc.delegate?.transferViewController(vc, didFinishWithTransfer: transaction, detailsViewController: nil) break diff --git a/Adamant/Wallets/Doge/DogeWalletService+RichMessageProvider.swift b/Adamant/Wallets/Doge/DogeWalletService+RichMessageProvider.swift index cefd73876..bce43000e 100644 --- a/Adamant/Wallets/Doge/DogeWalletService+RichMessageProvider.swift +++ b/Adamant/Wallets/Doge/DogeWalletService+RichMessageProvider.swift @@ -17,7 +17,8 @@ extension DogeWalletService: RichMessageProvider { // MARK: 0. Prepare guard let richContent = transaction.richContent, let hash = richContent[RichContentKeys.transfer.hash], - let dialogService = dialogService else { + let dialogService = dialogService, + let address = wallet?.address else { return } @@ -41,7 +42,26 @@ extension DogeWalletService: RichMessageProvider { switch result { case .success(let transaction): - vc.transaction = transaction + guard let blockHash = transaction.blockHash else { + vc.transaction = transaction.asDogeTransaction(for: address) + break + } + + self?.getBlockId(by: blockHash) { result in + switch result { + case .success(let id): + vc.transaction = transaction.asDogeTransaction(for: address, blockId: id) + + case .failure: + vc.transaction = transaction.asDogeTransaction(for: address) + } + + DispatchQueue.main.async { + chat.navigationController?.pushViewController(vc, animated: true) + } + } + + return case .failure(let error): switch error { diff --git a/Adamant/Wallets/Doge/DogeWalletService+RichMessageProviderWithStatusCheck.swift b/Adamant/Wallets/Doge/DogeWalletService+RichMessageProviderWithStatusCheck.swift index 762f0cc96..a212d1202 100644 --- a/Adamant/Wallets/Doge/DogeWalletService+RichMessageProviderWithStatusCheck.swift +++ b/Adamant/Wallets/Doge/DogeWalletService+RichMessageProviderWithStatusCheck.swift @@ -23,19 +23,14 @@ extension DogeWalletService: RichMessageProviderWithStatusCheck { getTransaction(by: hash) { result in switch result { case .success(let dogeTransaction): - // MARK: Check status - guard let status = dogeTransaction.transactionStatus else { - completion(.failure(error: WalletServiceError.internalError(message: "Failed to get transaction", error: nil))) - return - } - - guard status == .success else { - completion(.success(result: status)) + // 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.dateValue else { + guard let sentDate = dogeTransaction.date else { let timeAgo = -1 * date.timeIntervalSinceNow let result: TransactionStatus @@ -50,8 +45,10 @@ extension DogeWalletService: RichMessageProviderWithStatusCheck { return } - let start = date.addingTimeInterval(-60 * 5) - let end = date.addingTimeInterval(60 * 5) + // 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 { diff --git a/Adamant/Wallets/Doge/DogeWalletService.swift b/Adamant/Wallets/Doge/DogeWalletService.swift index 89888ac82..3179f671c 100644 --- a/Adamant/Wallets/Doge/DogeWalletService.swift +++ b/Adamant/Wallets/Doge/DogeWalletService.swift @@ -414,7 +414,7 @@ extension DogeWalletService { case .success(let doge): let hasMore = doge.to < doge.totalItems - let transactions = doge.items.map { DogeTransaction.map(from: $0, for: address, with: nil) }.reduce([], +) + let transactions = doge.items.map { $0.asDogeTransaction(for: address) } completion(.success((transactions: transactions, hasMore: hasMore))) From 7a9ab4b39df26d875bd45cd574f52414fa34c6d0 Mon Sep 17 00:00:00 2001 From: Pavel Anokhov Date: Tue, 9 Apr 2019 21:50:36 +0300 Subject: [PATCH 22/46] DOGE: Sender/recipient name --- .../Doge/DogeTransactionsViewController.swift | 18 ++++++++++++------ ...ogeWalletService+RichMessageProvider.swift | 19 ++++++++++++++----- 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/Adamant/Wallets/Doge/DogeTransactionsViewController.swift b/Adamant/Wallets/Doge/DogeTransactionsViewController.swift index 5ef71cd73..bcbb2026a 100644 --- a/Adamant/Wallets/Doge/DogeTransactionsViewController.swift +++ b/Adamant/Wallets/Doge/DogeTransactionsViewController.swift @@ -75,8 +75,17 @@ class DogeTransactionsViewController: TransactionsListViewControllerBase { walletService.getTransaction(by: txId) { result in switch result { case .success(let dogeTransaction): + let transaction = dogeTransaction.asDogeTransaction(for: sender) + + // Sender name + if transaction.senderAddress == sender { + controller.senderName = String.adamantLocalized.transactionDetails.yourAddress + } else if transaction.recipientAddress == sender { + controller.recipientName = String.adamantLocalized.transactionDetails.yourAddress + } + guard let blockHash = dogeTransaction.blockHash else { - controller.transaction = dogeTransaction.asDogeTransaction(for: sender) + controller.transaction = transaction DispatchQueue.main.async { self.navigationController?.pushViewController(controller, animated: true) self.tableView.deselectRow(at: indexPath, animated: true) @@ -86,17 +95,14 @@ class DogeTransactionsViewController: TransactionsListViewControllerBase { } walletService.getBlockId(by: blockHash) { result in - let transaction: DogeTransaction switch result { case .success(let id): - transaction = dogeTransaction.asDogeTransaction(for: sender, blockId: id) + controller.transaction = dogeTransaction.asDogeTransaction(for: sender, blockId: id) case .failure: - transaction = dogeTransaction.asDogeTransaction(for: sender) + controller.transaction = transaction } - controller.transaction = transaction - DispatchQueue.main.async { self.tableView.deselectRow(at: indexPath, animated: true) self.dialogService.dismissProgress() diff --git a/Adamant/Wallets/Doge/DogeWalletService+RichMessageProvider.swift b/Adamant/Wallets/Doge/DogeWalletService+RichMessageProvider.swift index bce43000e..afbe533bc 100644 --- a/Adamant/Wallets/Doge/DogeWalletService+RichMessageProvider.swift +++ b/Adamant/Wallets/Doge/DogeWalletService+RichMessageProvider.swift @@ -41,19 +41,28 @@ extension DogeWalletService: RichMessageProvider { vc.comment = comment switch result { - case .success(let transaction): - guard let blockHash = transaction.blockHash else { - vc.transaction = transaction.asDogeTransaction(for: address) + case .success(let dogeTransaction): + let transaction = dogeTransaction.asDogeTransaction(for: address) + + // Sender name + if transaction.senderAddress == address { + vc.senderName = String.adamantLocalized.transactionDetails.yourAddress + } else if transaction.recipientAddress == address { + vc.recipientName = String.adamantLocalized.transactionDetails.yourAddress + } + + guard let blockHash = dogeTransaction.blockHash else { + vc.transaction = transaction break } self?.getBlockId(by: blockHash) { result in switch result { case .success(let id): - vc.transaction = transaction.asDogeTransaction(for: address, blockId: id) + vc.transaction = dogeTransaction.asDogeTransaction(for: address, blockId: id) case .failure: - vc.transaction = transaction.asDogeTransaction(for: address) + vc.transaction = transaction } DispatchQueue.main.async { From cb43c735de0b5484747d9697d68acd30b0c75cdf Mon Sep 17 00:00:00 2001 From: Pavel Anokhov Date: Tue, 9 Apr 2019 22:19:33 +0300 Subject: [PATCH 23/46] DOGE: Recipients & Senders localisation. --- .../l18n/de.lproj/Localizable.stringsdict | 32 +++++++++++++++ .../l18n/en.lproj/Localizable.stringsdict | 32 +++++++++++++++ .../l18n/ru.lproj/Localizable.stringsdict | 40 +++++++++++++++++++ Adamant/Models/DogeTransaction.swift | 18 ++++++++- 4 files changed, 120 insertions(+), 2 deletions(-) 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.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.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/Models/DogeTransaction.swift b/Adamant/Models/DogeTransaction.swift index f7baf9f07..ffd5ba2ef 100644 --- a/Adamant/Models/DogeTransaction.swift +++ b/Adamant/Models/DogeTransaction.swift @@ -8,6 +8,20 @@ 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 { let txId: String let dateValue: Date? @@ -87,7 +101,7 @@ struct DogeRawTransaction { if recipients.count == 1, let name = recipients.first { recipient = name } else { - recipient = "\(recipients.count) recipients" + recipient = String.adamantLocalized.dogeTransaction.recipients(recipients.count) } let inputTransaction = DogeTransaction(txId: txId, @@ -109,7 +123,7 @@ struct DogeRawTransaction { if senders.count == 1, let name = senders.first { sender = name } else { - sender = "\(senders.count) senders" + sender = String.adamantLocalized.dogeTransaction.senders(senders.count) } let outputTransaction = DogeTransaction(txId: txId, From 8251cac56e0166f134d5345feb355c11f455c46b Mon Sep 17 00:00:00 2001 From: Pavel Anokhov Date: Tue, 9 Apr 2019 22:21:29 +0300 Subject: [PATCH 24/46] Optimisation fixed for Debug builds. --- Adamant.xcodeproj/project.pbxproj | 1 + 1 file changed, 1 insertion(+) diff --git a/Adamant.xcodeproj/project.pbxproj b/Adamant.xcodeproj/project.pbxproj index 46bc4c464..744fcef1f 100644 --- a/Adamant.xcodeproj/project.pbxproj +++ b/Adamant.xcodeproj/project.pbxproj @@ -1917,6 +1917,7 @@ PRODUCT_NAME = ADAMANT; SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 4.2; }; name = Debug; From e0d87dc1f4e3668be1e2d01030f24a00cf66868b Mon Sep 17 00:00:00 2001 From: Pavel Anokhov Date: Tue, 9 Apr 2019 22:34:29 +0300 Subject: [PATCH 25/46] New logo --- .../wallet_doge.imageset/wallet_doge.png | Bin 1002 -> 1089 bytes .../wallet_doge.imageset/wallet_doge@2x.png | Bin 2231 -> 2028 bytes .../wallet_doge.imageset/wallet_doge@3x.png | Bin 3607 -> 3398 bytes 3 files changed, 0 insertions(+), 0 deletions(-) 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 index 68e9f0609c18d98346c7e44a2dcd727020a3ba08..20e1bf4fb804a4cd5c8e41fd003e9a48af53b044 100644 GIT binary patch delta 1081 zcmV-91jhU72f+xC7k@Vh1^@s6sN2#;00001b5ch_0Itp)=>Px&`$e5n#pSv zK@`S~ySSqf1;OA&R0MbNp!o+B6-B)0$v;5_z3Cx%_AYqw<`NI09z3Y1AR-DbxF#YR zh`7WJcl~~osnD6!($ndg?h-%vW_s$qu6|#=>ZyJEsR{2%m zAH+WKjfgEbw4|(Qk^JtZ^v&w_Lnpox?~Av@PVuJLa3GFWZ3Ynuxz$(>JBkvQ6pF}RaZBFn${-G*$t;e}OJrHZnN0qYu+!q$JOnmU9QW0C z(PYI42z?{)gn#H9@pDV9F0x&GD~2#q4z&0faf;X~&Jh=itE#^Ve3a+|^=uXQiyc14 zkS$H-W``=?Qwq6`k{z&HOfu++WwEKBBCjr5AH?)gQwD`Ew) zWe!niMY^U96I8fTtgO6Z6NjL4CU|~Wz;v*L@)*UX*?L$OtGMS1oNWjf=(24XW6T^Y z6$H9~@PBz2V-!c+-F!H}h&mTrtm0UTF{jzU(*XfFHAEaj8P#lD?5ZhI&fcT# zT*{J-m^dZV;W(W&%X22zi?&;`uj$*y<8kK>;`3}#>T3qp>LRYyiMjp_4*o@PZ0UE! zD2}QkD&(s|wG&FK5WFCHLZdy99S>8N{uy zh<{0nL-ggOgjl*I_t+89wd;i@e2#KZFN)6GVFgT=OJ=I}n0E@5N1d3&F^GhOVLwo> zDC1v5=2%AU6X}YuHdE#|wr-JSStc}79OJ-7k&)2|2z_t1sd7z*Hs3~-F)_D^G~jx1 zo7g7$1`nAlpgW!tJNthQ{>)R{MOB?9Ab+|WU5IV(sUrVqpbW6WFe7*E3Z5w1s0d;aEEO9I8%41b6f9K`1e=IhDToIm zC<+pc;sp;Cl6c_}#UuWIvLoZ>?Plh^eJ?xt;Fme}+sXHTGk?1~JF_(-(rva59~EOP z#wuScMvGBVYUfQI>38upqEG23@mjpH=u*B@{1jESW%_UZRRH0)#c{D!)QgH+wZz4L zKzjRv*d)ePO65?Av&FuZxV?hQAydnPI3u(mWxE8|awe81aYp1VDVye^ZlsG-&U}_C zp1Wf4U~z>g4u4He;=0&Qp;XJ`ur$My^q1I@BrepRGkS>sjA%6#b{e`kwA6`nqLG4a zOq;T@i)v_{;8tzm$0N^-V|v64E6?<9p_m}bG1@#OHv2T4^2gEU%CA0@VTq>4qCm#Mm>4VxNgSH)IS4Z@^O;-zFr^}CO_92!sx>;0 z)|NSs=YQ5zQ}9d?x8CubM#MoD#t-4rmWxkPJP(r&6sd0rZCHLNA|k13QfT5>v39lK zdd^2&a6R|gwcF$uoVW%LaY4;c(ADH2t~azBijTOS;ViD32kNYpu4ZNGe9CeA&{Y+7 z+Z73UmI6vOR?A%NDdL`|c(Y{YYP^=DwdLAQnt$K&w1cr@PeK}&GZj3zT31No+FVu1 z@4a{yk~o*^+305vuRj!V?8$+v-M}#vaR*#gMSkpC1R-v};#?I4?UHoLiEmqSFK&Zk zJWQY-cGlH*sP+o@V(FOpEqwM0dgRY`*=m?fU2k>RB^mFwun1P!l(~X z9A7N3Lht|qXPUX1yPa> zw*NL!tu7EuMuD=Ox67uUl&MvnsEtzcJcn3B0`7FA%mZHvhMa-hdnWb=MPdlP><8P^ P00000NkvXXu0mjffVSAF 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 index 6d7df23b1335e6e18f486bbb32c46a93c0dd7991..34282fce3350cd5ee20179cfda547696a7db4532 100644 GIT binary patch delta 2027 zcmVPx+rb$FWRCodHU2Uip zMHseHvk*(YTA_47ijqDo@RraQ*auQkB;G&@i=aqI3c6qvq4Xn)KK?}3uY&5wO{^fI zBvSKM6lGM@RVoW4$t){MyJ?yBJl8wt%-J)0c6R5TbM~C`zJKuCot>R|X7_pS?9P6k zapN@FQh}p{*ZM5*EfwaJpgRbXBn}|&PYZt_{X4J=_y;I;j2B+Jx!`+1n6ue*htc5A zz)s)?U>Nvf4BIh11aw?1!4`4DI~BiS+XjJUz)YZHqSKAD{~_7)J_hatCbzqT-DI3? zB9D(x0X$T+Wq)+5amGaU!WV(rvE4g5b^~3;&r$l#z}LWP;CLWPsBuwb9W`tfsJ8+O zVl<(~#Za}%*^A^5FaU_2Gplh`kz=JMfcP=sbzq8ABWj$KlG!}hqR0kdT6WRw5^7ww zMv^7wgKQ%(Ba~E)3so?G{duUe88{_>)oKkjuBt^dQ-3^B9|8(rHdEu8X;*Dqr(t$2 zQ0PK`p-Q#wi_bj=#@&Lf9EMM{z)>FOIl|+yhUfuK0Gb__p~1(2dz#&FvOS4QUgsrA zn>2qc+BqBO2YB-Q3c$Mohns+#&_+w6osDw|cMLKw0iFPO(>0cfBjDzsrDf4+#@Rwn z$IQdPUVk8lfuDl6Wn+#`HV(RuMi2gSP!YqtzZck+jzfW!LGm<)p3(@4~~G-ZnC^Yq}Z z;?-+v-cmDX&8Tsf7=lSYrv8-xA77$5-NmEExl{?y<1UleH6>!Pi$slcsUnld>zUsQ zP=DRv;!xvUs>tO12bxC&sCX4+R+2}JODZBe{}Sjzuc+`8pKFJ z7bQ8=xTGTT^KS}J)?IW~;{;UApm-}|xzA#r5=^rTs&Td&and{?=R4$~(PtB=akd)q z((47NFSI$-I9rW)>CXkIwZ{pFn3v+4ynhuO<@w}uCA7y?q==f3xPo%3y@qO>twy{w z@2&BRMIpiW$$Jce8fTPU(jL!la=l!cPhJ%`mdaxX0UBwK3lOhJRgNetj~x^tKpnnsx6{qIZp#5c9k`6{C{YV6L!f# zccmjQZx%E1-}Y3HBW_Qbi_j05lT%u63=B1hI>^+w*2Y zfI9?4_!1o4Rn~L=v#_B&U#N$QN&AZA= zp^(HblDo+*S!!#K%Tma1`57qo8X&QYWQ(i$r%|YJjnwg5HXFtGF@MpT?@9Vqb(Z=N zxo%uwSpC2|(vV}SfMkM{wdY)b`u+PhL@d3d^E zaIye`3;`kMs;;{HS~_>TUNPRQlonzSTL=sQiz3Of61=?A?GJQsas$7glc#s;>&6P~$?>D^{0R{Z|yLTCJtVRkdhFihlzyUU4}T+E^I}C!yO^ z#`^pK+j8Iqsg+aXq?AnO*@{Is0>kM=@-NX{Wq!?kYaB$uCxJdd)HJSu_Bg5_AH0Oc zV;Or}l+Ye0E9(Z(T>&fvqM623)*eR{!eSq)uLITqpM+8Q*H+{FwX#c&V1~QHLEu$@ z^Gyvk&QESWy=29o%j9*%^}rCYA81*qakbL3=X?)*2k_Nh{*ciYfY0#rN`7fV`V&gL zr*%zfwyt(Xc)yDucp=z>IOXF4djQ@>;&d;dp$e!1sz5@4{{eZBEMkB(R|Nn7002ov JPDHLkV1gi*)-eD8 literal 2231 zcmV;o2uSydP)$M=PY znVp&Unc4R_vuDrjobA%)XT#w>66g`3rj#B1#0G-~td7i>NM9L3E>CIQT(w|_=0Q;b9 zScLlOT_~W4D=ciSZ`2CM({K{}gtjs^;QVF)6M*hOOW4sMoc*EffNJxANkFF-@n{py z7Lk|GSAcVX!tI#Cdu*K!`{7g`1Nfb|1Q-t#W-45mP+tz3#lTA7hA@=rEGk@jd|Ycs z!C^kI6gbb-*`>V-=Mt^#Z7)UBuYos!zS-Tfw^ZS>$I7)OSAZS{)&mn=otw2+;hIIr zzrH^xJ_23>g8hV0;rxZk!oc;_Cx8{esaYJFwNc@kMJR0j#i08fIMrSQ zs1CWv%w=~qI zDnrteHWg=eZI&}D>zga8Z}3$)AG!85VjWUuJs~gX7H# zAJisVS%pg+a8$u3(VTsRtM!Kih2)d#OMub9FrX|~AAZng0^A-UW>Q-h?hzYtJn``; z<`eAaf%SmBMd3*0$6_2})+)l)2Z)!d7l7}9BPHAy^@FmH%W`?J$5e(q_mMmU30t_HpWiZt#LkG&63;j+uH2u=A|<_>^6o{KQf_*>&T zQsLsFl3w->s(Clc{~Q!y55#q%!o@|UdD&Vt;lGp?HgD?Pf|qHos??YYR}&7WgATOj zzm?`WdF5-|FzZyfe1SAx{Hbs!u+$U?rMH8(X2nN^^N%#wP;Ul)^QTH=;4a zsS0Nz5hnc!G;_jG#jc#c)&&Ojnr_70yH=T*}8nQfrzcz+KTyg$pN` z%{1FS2$<(ney(4Z=BaPMb1MZE?=Jb{T*A&Lsm?fw(6+Vx4%&4x)NLx9P+$$M)H0X~ zXCf?=N(oo_Z@FG7g;WFWDj{m_dhW(0l-(X3@UOA4kJddn>B>#82Ru1!cRuv2d{XT~ z;XEhBPM~I0KTw~88FUF^-|_JSL4>lNp@6oBb(b8C1B~u!Jj2-T&{=%|N|WdPFaSD_<=vVSQCNiLh2VrG#r&;i^HF z$nB6I1!oTxPAIYwEq{+nn9ewfs9K!-C#{63a1v3qICaKZ!RSm`uAWGke#c2f)#Btv zRtY<#HzyKdt#Y1R%?8bFSK)%jvw=)XxNRz2L#%@qsBl5USsd9Qp{{hs2?f^Bo@p|H zQxz_7K-MEWp1+NmymepI#~@Lfh6>^M-_gUGR=_HThJyfKj!m zuCwwXvtNR&@>E@g3mMk`xp1m*t>Q9r%@V;H6!r9UA%X49zKfI;v zJ)2mCv!jtee-~UQ=XY=FxdtsND<-C;3TGmbKOGL&*Ymq)@~oL{^ibh4#VxTBf3NZJ z({YI{!c?emVd7X};mZJ)00RPY#!>OL8x4O+))^NUs`N6xsp3(9%TGhnn+B=nO;3aI z3AkGJ2nLOr51in~?-)Be4o<+xdk~%M|aN{uGgTSr`JFPrV zW*@-kQZ{gS*t#NT&>i^`fxdO%_tj9~>?mcMve-8m$V|z>OanQu!DRsd{YkOQFc#l2 zjKO)NT3KgY>Yx)UTjgBl;QW>+oa5nQy{kMNic)0$z$hNiG7=RoOO%Rf!^`{>fUgB^ zYARgQ5Ea(oIrQ{N*4|XOtdWv!$3M7Ccj-cfbBUHr+iyX88^Al!%%;L+4wXp5bzq$U z{O!uF!nsB)TzfvM84u)o!=4HkP6!vK4UuTP{m*48T&}QrIn9UnXkd#EiwfrxJvU!& zXnhKp1a!E$Hnwh+WunG2D_6;O42M6iKMpl4opGTenand6ReVGgOf+_-GmcS^vDKh` z1Xw0PuQM)uRQcrkAz%a`A{vLRGmcRRW87`bjj^+V#3$Ebx>DhMs|+*Q^o>_6%|ep()n(5nCd002ovPDHLk FV1m}XKV<*_ 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 index 5ddedc0c50e13fc0e14aea2b0786444872a62ce3..339a4e6d755e4ad7cd93b1ce60b2847eeb0c9866 100644 GIT binary patch literal 3398 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 dfN~od{tw^_w$d~ Date: Wed, 10 Apr 2019 14:05:54 +0300 Subject: [PATCH 26/46] Fixed transaction summary --- Adamant.xcodeproj/project.pbxproj | 4 - .../BaseTransaction+TransactionDetails.swift | 2 + Adamant/Models/DogeTransaction.swift | 2 + Adamant/Models/EthTransaction.swift | 2 + Adamant/Models/SimpleTransactionDetails.swift | 2 + .../Utilities/AdamantFormattingTools.swift | 73 ------------------- .../Wallets/Doge/DogeWalletService+Send.swift | 2 + .../Lisk/LskTransactionsViewController.swift | 4 + Adamant/Wallets/TransactionDetails.swift | 60 ++++++++++----- ...TransactionDetailsViewControllerBase.swift | 2 +- 10 files changed, 58 insertions(+), 95 deletions(-) delete mode 100644 Adamant/Utilities/AdamantFormattingTools.swift diff --git a/Adamant.xcodeproj/project.pbxproj b/Adamant.xcodeproj/project.pbxproj index 744fcef1f..5f87983f9 100644 --- a/Adamant.xcodeproj/project.pbxproj +++ b/Adamant.xcodeproj/project.pbxproj @@ -165,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 */; }; @@ -494,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 = ""; }; @@ -1034,7 +1032,6 @@ E950651F20404997008352E5 /* Utilities */ = { isa = PBXGroup; children = ( - E948E04B2027679300975D6B /* AdamantFormattingTools.swift */, E9942B83203CBFCE00C163AF /* AdamantQRTools.swift */, E9E7CDB62003994E00DFC4DB /* AdamantUtilities.swift */, E950652220404C84008352E5 /* AdamantUriTools.swift */, @@ -1781,7 +1778,6 @@ 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 */, diff --git a/Adamant/CoreData/BaseTransaction+TransactionDetails.swift b/Adamant/CoreData/BaseTransaction+TransactionDetails.swift index e98659c88..d3faecd39 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 "ADM" } + var txId: String { return transactionId ?? "" } var senderAddress: String { return senderId ?? "" } var recipientAddress: String { return recipientId ?? "" } diff --git a/Adamant/Models/DogeTransaction.swift b/Adamant/Models/DogeTransaction.swift index ffd5ba2ef..b9b94cf37 100644 --- a/Adamant/Models/DogeTransaction.swift +++ b/Adamant/Models/DogeTransaction.swift @@ -23,6 +23,8 @@ extension String.adamantLocalized { } struct DogeTransaction: TransactionDetails { + static var defaultCurrencySymbol: String? { return "DOGE" } + let txId: String let dateValue: Date? let blockValue: String? diff --git a/Adamant/Models/EthTransaction.swift b/Adamant/Models/EthTransaction.swift index 5b5e95c62..d266ba13b 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 "ETH" } + 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 a7615b850..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 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/Wallets/Doge/DogeWalletService+Send.swift b/Adamant/Wallets/Doge/DogeWalletService+Send.swift index fe882c65a..6594639c9 100644 --- a/Adamant/Wallets/Doge/DogeWalletService+Send.swift +++ b/Adamant/Wallets/Doge/DogeWalletService+Send.swift @@ -101,6 +101,8 @@ extension DogeWalletService: WalletServiceTwoStepSend { } extension BitcoinKit.Transaction: TransactionDetails { + static var defaultCurrencySymbol: String? { return "DOGE" } + var txId: String { return txID } diff --git a/Adamant/Wallets/Lisk/LskTransactionsViewController.swift b/Adamant/Wallets/Lisk/LskTransactionsViewController.swift index eb96db786..3f8214bee 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 "LSK" } + var txId: String { return id } @@ -170,6 +172,8 @@ extension Transactions.TransactionModel: TransactionDetails { } extension LocalTransaction: TransactionDetails { + static var defaultCurrencySymbol: String? { return "LSK" } + var txId: String { return id ?? "" } diff --git a/Adamant/Wallets/TransactionDetails.swift b/Adamant/Wallets/TransactionDetails.swift index b59f353ad..548c9b2ce 100644 --- a/Adamant/Wallets/TransactionDetails.swift +++ b/Adamant/Wallets/TransactionDetails.swift @@ -37,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 ?? "") -// """ -// } -//} +extension TransactionDetails { + 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 487faf0f5..e3a264e1c 100644 --- a/Adamant/Wallets/TransactionDetailsViewControllerBase.swift +++ b/Adamant/Wallets/TransactionDetailsViewControllerBase.swift @@ -559,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) } } From c08691ae30da0693658941bfb57c732e2e867091 Mon Sep 17 00:00:00 2001 From: Pavel Anokhov Date: Wed, 10 Apr 2019 14:13:45 +0300 Subject: [PATCH 27/46] Cleanup --- .../BaseTransaction+TransactionDetails.swift | 2 +- Adamant/Helpers/Decimal+adamant.swift | 4 +- Adamant/Models/DogeTransaction.swift | 2 +- Adamant/Models/EthTransaction.swift | 2 +- .../Stories/Chats/ChatViewController.swift | 2 +- Adamant/Utilities/AdamantUtilities.swift | 39 ------------------- .../Adamant/AdmTransferViewController.swift | 6 ++- .../Wallets/Adamant/AdmWalletService.swift | 5 ++- .../Wallets/Doge/DogeWalletService+Send.swift | 2 +- .../Lisk/LskTransactionsViewController.swift | 4 +- 10 files changed, 17 insertions(+), 51 deletions(-) diff --git a/Adamant/CoreData/BaseTransaction+TransactionDetails.swift b/Adamant/CoreData/BaseTransaction+TransactionDetails.swift index d3faecd39..a38564880 100644 --- a/Adamant/CoreData/BaseTransaction+TransactionDetails.swift +++ b/Adamant/CoreData/BaseTransaction+TransactionDetails.swift @@ -9,7 +9,7 @@ import Foundation extension BaseTransaction: TransactionDetails { - static var defaultCurrencySymbol: String? { return "ADM" } + static var defaultCurrencySymbol: String? { return AdmWalletService.currencySymbol } var txId: String { return transactionId ?? "" } var senderAddress: String { return senderId ?? "" } 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/Models/DogeTransaction.swift b/Adamant/Models/DogeTransaction.swift index b9b94cf37..59382758f 100644 --- a/Adamant/Models/DogeTransaction.swift +++ b/Adamant/Models/DogeTransaction.swift @@ -23,7 +23,7 @@ extension String.adamantLocalized { } struct DogeTransaction: TransactionDetails { - static var defaultCurrencySymbol: String? { return "DOGE" } + static var defaultCurrencySymbol: String? { return DogeWalletService.currencySymbol } let txId: String let dateValue: Date? diff --git a/Adamant/Models/EthTransaction.swift b/Adamant/Models/EthTransaction.swift index d266ba13b..5dbf289e6 100644 --- a/Adamant/Models/EthTransaction.swift +++ b/Adamant/Models/EthTransaction.swift @@ -134,7 +134,7 @@ extension EthTransaction: Decodable { // MARK: - TransactionDetails extension EthTransaction: TransactionDetails { - static var defaultCurrencySymbol: String? { return "ETH" } + static var defaultCurrencySymbol: String? { return EthWalletService.currencySymbol } var txId: String { return hash } var senderAddress: String { return from } diff --git a/Adamant/Stories/Chats/ChatViewController.swift b/Adamant/Stories/Chats/ChatViewController.swift index 13a48b76f..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 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/DogeWalletService+Send.swift b/Adamant/Wallets/Doge/DogeWalletService+Send.swift index 6594639c9..5a19e0b34 100644 --- a/Adamant/Wallets/Doge/DogeWalletService+Send.swift +++ b/Adamant/Wallets/Doge/DogeWalletService+Send.swift @@ -101,7 +101,7 @@ extension DogeWalletService: WalletServiceTwoStepSend { } extension BitcoinKit.Transaction: TransactionDetails { - static var defaultCurrencySymbol: String? { return "DOGE" } + static var defaultCurrencySymbol: String? { return DogeWalletService.currencySymbol } var txId: String { return txID diff --git a/Adamant/Wallets/Lisk/LskTransactionsViewController.swift b/Adamant/Wallets/Lisk/LskTransactionsViewController.swift index 3f8214bee..ad0e78c7e 100644 --- a/Adamant/Wallets/Lisk/LskTransactionsViewController.swift +++ b/Adamant/Wallets/Lisk/LskTransactionsViewController.swift @@ -117,7 +117,7 @@ class LskTransactionsViewController: TransactionsListViewControllerBase { } extension Transactions.TransactionModel: TransactionDetails { - static var defaultCurrencySymbol: String? { return "LSK" } + static var defaultCurrencySymbol: String? { return LskWalletService.currencySymbol } var txId: String { return id @@ -172,7 +172,7 @@ extension Transactions.TransactionModel: TransactionDetails { } extension LocalTransaction: TransactionDetails { - static var defaultCurrencySymbol: String? { return "LSK" } + static var defaultCurrencySymbol: String? { return LskWalletService.currencySymbol } var txId: String { return id ?? "" From 5b3d2ec5becd91cdbcf71f968402a858ac2f1c9e Mon Sep 17 00:00:00 2001 From: Pavel Anokhov Date: Wed, 10 Apr 2019 15:09:33 +0300 Subject: [PATCH 28/46] Load more doge transactions. (hard limit 200) --- .../Doge/DogeTransactionsViewController.swift | 85 ++++++++++++++++++- 1 file changed, 81 insertions(+), 4 deletions(-) diff --git a/Adamant/Wallets/Doge/DogeTransactionsViewController.swift b/Adamant/Wallets/Doge/DogeTransactionsViewController.swift index bcbb2026a..9afb331b3 100644 --- a/Adamant/Wallets/Doge/DogeTransactionsViewController.swift +++ b/Adamant/Wallets/Doge/DogeTransactionsViewController.swift @@ -7,6 +7,7 @@ // import UIKit +import ProcedureKit class DogeTransactionsViewController: TransactionsListViewControllerBase { @@ -18,23 +19,35 @@ class DogeTransactionsViewController: TransactionsListViewControllerBase { // 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() - - self.refreshControl.beginRefreshing() - currencySymbol = DogeWalletService.currencySymbol - handleRefresh(self.refreshControl) + refreshControl.beginRefreshing() + handleRefresh(refreshControl) + } + + deinit { + procedureQueue.cancelAllOperations() } override func handleRefresh(_ refreshControl: UIRefreshControl) { transactions.removeAll() + loadedTo = 0 walletService.getTransactions(from: 0) { [weak self] result in switch result { case .success(let tuple): self?.transactions = tuple.transactions + self?.loadedTo = tuple.transactions.count + + if tuple.hasMore { + self?.loadMoreTransactions(from: tuple.transactions.count) + } DispatchQueue.main.async { self?.tableView.reloadData() @@ -145,4 +158,68 @@ class DogeTransactionsViewController: TransactionsListViewControllerBase { amount: transaction.amountValue, date: transaction.dateValue) } + + // MARK: - Load more + private func loadMoreTransactions(from: Int) { + let procedure = LoadMoreDogeTransactionsProcedure(service: walletService, from: from) + print("getting from \(from)") + procedure.addDidFinishBlockObserver { [weak self] (procedure, error) in + guard let result = procedure.result else { + return + } + + guard let vc = self else { + return + } + + let total = vc.loadedTo + result.transactions.count + vc.loadedTo = total + vc.transactions.append(contentsOf: result.transactions) + + var indexPaths = [IndexPath]() + for index in from.. Date: Wed, 10 Apr 2019 15:34:56 +0300 Subject: [PATCH 29/46] Cleanup --- Adamant/ServiceProtocols/LskApiService.swift | 3 - .../AdamantLskApiService.swift | 108 ++++-------------- Adamant/Wallets/Doge/DogeWalletService.swift | 4 +- Adamant/Wallets/Lisk/LskWalletService.swift | 37 +++--- 4 files changed, 40 insertions(+), 112 deletions(-) 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/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/Wallets/Doge/DogeWalletService.swift b/Adamant/Wallets/Doge/DogeWalletService.swift index 3179f671c..b043d4ced 100644 --- a/Adamant/Wallets/Doge/DogeWalletService.swift +++ b/Adamant/Wallets/Doge/DogeWalletService.swift @@ -453,8 +453,8 @@ extension DogeWalletService { completion(.success(dogeResponse)) - case .failure: - completion(.failure(.internalError(message: "DOGE Wallet: server not response", error: nil))) + case .failure(let error): + completion(.failure(.internalError(message: "DOGE Wallet: server not responding", error: error))) } } } diff --git a/Adamant/Wallets/Lisk/LskWalletService.swift b/Adamant/Wallets/Lisk/LskWalletService.swift index 167a1103b..1c9e1fc7a 100644 --- a/Adamant/Wallets/Lisk/LskWalletService.swift +++ b/Adamant/Wallets/Lisk/LskWalletService.swift @@ -445,30 +445,27 @@ 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(.failure(error: .internalError(message: error.message, error: nil))) - break + completion(.success(result: balance.asDecimal(exponent: LskWalletService.currencyExponent))) + } else { + completion(.success(result: 0)) } + + case .error(response: let error): + completion(.failure(error: .internalError(message: error.message, error: nil))) } } - } else { - completion(.failure(error: .internalError(message: "LSK Wallet: not found", error: nil))) } } From afb1813ccf62110e5340bfdbaa0643fc3f0c4eac Mon Sep 17 00:00:00 2001 From: Pavel Anokhov Date: Wed, 10 Apr 2019 16:07:16 +0300 Subject: [PATCH 30/46] Less annoying errors. --- Adamant/Wallets/Ethereum/EthWalletService.swift | 11 ++++++++++- Adamant/Wallets/Lisk/LskWalletService.swift | 6 +++++- 2 files changed, 15 insertions(+), 2 deletions(-) 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/LskWalletService.swift b/Adamant/Wallets/Lisk/LskWalletService.swift index 1c9e1fc7a..1b361ccbc 100644 --- a/Adamant/Wallets/Lisk/LskWalletService.swift +++ b/Adamant/Wallets/Lisk/LskWalletService.swift @@ -463,7 +463,11 @@ extension LskWalletService { } case .error(response: let error): - completion(.failure(error: .internalError(message: error.message, error: nil))) + if error.message == "Unexpected Error" { + completion(.failure(error: .networkError)) + } else { + completion(.failure(error: .internalError(message: error.message, error: nil))) + } } } } From 8509c6c4562d065e4536e3eac30639dd0cb7f0ea Mon Sep 17 00:00:00 2001 From: Pavel Anokhov Date: Wed, 10 Apr 2019 16:09:18 +0300 Subject: [PATCH 31/46] Version --- Adamant/Info.plist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Adamant/Info.plist b/Adamant/Info.plist index fe6cde92e..fd9237af7 100644 --- a/Adamant/Info.plist +++ b/Adamant/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 1.5 CFBundleVersion - 71 + 72 LSRequiresIPhoneOS NSAppTransportSecurity From 5e6e0a22547d16fb76d12f63926de5c1489efc45 Mon Sep 17 00:00:00 2001 From: Pavel Anokhov Date: Wed, 10 Apr 2019 23:12:09 +0300 Subject: [PATCH 32/46] Fixed many issues with transfers to self. --- Adamant/Models/DogeTransaction.swift | 85 ++++++++++++++----- ...DogeTransactionDetailsViewController.swift | 10 ++- .../Doge/DogeTransactionsViewController.swift | 45 ++++++---- Adamant/Wallets/Doge/DogeWalletService.swift | 13 +-- 4 files changed, 107 insertions(+), 46 deletions(-) diff --git a/Adamant/Models/DogeTransaction.swift b/Adamant/Models/DogeTransaction.swift index 59382758f..715c8e039 100644 --- a/Adamant/Models/DogeTransaction.swift +++ b/Adamant/Models/DogeTransaction.swift @@ -94,18 +94,39 @@ struct DogeRawTransaction { } } - let senders = Set(inputs.map { $0.sender }.filter { $0 != address }) - let recipients = Set(outputs.compactMap { $0.addresses.first }.filter { $0 != address }) + let senders = Set(inputs.map { $0.sender } ) + let recipients = Set(outputs.compactMap { $0.addresses.first } ) - // MARK: Inputs - if myInputs.count > 0 { - let recipient: String - if recipients.count == 1, let name = recipients.first { - recipient = name + 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 { - recipient = String.adamantLocalized.dogeTransaction.recipients(recipients.count) + 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, @@ -121,13 +142,6 @@ struct DogeRawTransaction { } // MARK: Outputs - let sender: String - if senders.count == 1, let name = senders.first { - sender = name - } else { - sender = String.adamantLocalized.dogeTransaction.senders(senders.count) - } - let outputTransaction = DogeTransaction(txId: txId, dateValue: date, blockValue: blockId, @@ -161,9 +175,6 @@ extension DogeRawTransaction: Decodable { // MARK: Required self.txId = try container.decode(String.self, forKey: .txId) - self.valueIn = try container.decode(Decimal.self, forKey: .valueIn) - self.valueOut = try container.decode(Decimal.self, forKey: .valueOut) - self.fee = try container.decode(Decimal.self, forKey: .fee) // MARK: Optionals for new transactions if let timeInterval = try? container.decode(TimeInterval.self, forKey: .date) { @@ -176,9 +187,28 @@ extension DogeRawTransaction: Decodable { self.blockHash = try? container.decode(String.self, forKey: .blockHash) // MARK: Inputs & Outputs - - self.inputs = try container.decode([DogeInput].self, forKey: .inputs) + 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 + } } } @@ -200,10 +230,21 @@ struct DogeInput: Decodable { init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) - self.sender = try container.decode(String.self, forKey: .sender) - self.value = try container.decode(Decimal.self, forKey: .value) + // 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 = raw + } else { + self.value = 0 + } } } diff --git a/Adamant/Wallets/Doge/DogeTransactionDetailsViewController.swift b/Adamant/Wallets/Doge/DogeTransactionDetailsViewController.swift index d61e4de7b..94bf774af 100644 --- a/Adamant/Wallets/Doge/DogeTransactionDetailsViewController.swift +++ b/Adamant/Wallets/Doge/DogeTransactionDetailsViewController.swift @@ -57,10 +57,12 @@ class DogeTransactionDetailsViewController: TransactionDetailsViewControllerBase @objc func refresh() { updateTransaction { [weak self] error in - self?.refreshControl.endRefreshing() - - if let error = error { - self?.dialogService.showRichError(error: error) + DispatchQueue.main.async { + self?.refreshControl.endRefreshing() + + if let error = error { + self?.dialogService.showRichError(error: error) + } } } } diff --git a/Adamant/Wallets/Doge/DogeTransactionsViewController.swift b/Adamant/Wallets/Doge/DogeTransactionsViewController.swift index 9afb331b3..19317052b 100644 --- a/Adamant/Wallets/Doge/DogeTransactionsViewController.swift +++ b/Adamant/Wallets/Doge/DogeTransactionsViewController.swift @@ -77,7 +77,8 @@ class DogeTransactionsViewController: TransactionsListViewControllerBase { fatalError("Failed to get DogeTransactionDetailsViewController") } - guard let walletService = walletService, let sender = walletService.wallet?.address else { + // Hold reference + guard let sender = walletService.wallet?.address else { return } @@ -85,7 +86,11 @@ class DogeTransactionsViewController: TransactionsListViewControllerBase { dialogService.showProgress(withMessage: nil, userInteractionEnable: false) let txId = transactions[indexPath.row].txId - walletService.getTransaction(by: txId) { result in + 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) @@ -93,21 +98,24 @@ class DogeTransactionsViewController: TransactionsListViewControllerBase { // Sender name if transaction.senderAddress == sender { controller.senderName = String.adamantLocalized.transactionDetails.yourAddress - } else if transaction.recipientAddress == sender { + } + + if transaction.recipientAddress == sender { controller.recipientName = String.adamantLocalized.transactionDetails.yourAddress } + // Block Id guard let blockHash = dogeTransaction.blockHash else { controller.transaction = transaction DispatchQueue.main.async { - self.navigationController?.pushViewController(controller, animated: true) - self.tableView.deselectRow(at: indexPath, animated: true) - self.dialogService.dismissProgress() + vc.navigationController?.pushViewController(controller, animated: true) + vc.tableView.deselectRow(at: indexPath, animated: true) + vc.dialogService.dismissProgress() } break } - walletService.getBlockId(by: blockHash) { result in + vc.walletService.getBlockId(by: blockHash) { result in switch result { case .success(let id): controller.transaction = dogeTransaction.asDogeTransaction(for: sender, blockId: id) @@ -117,17 +125,17 @@ class DogeTransactionsViewController: TransactionsListViewControllerBase { } DispatchQueue.main.async { - self.tableView.deselectRow(at: indexPath, animated: true) - self.dialogService.dismissProgress() - self.navigationController?.pushViewController(controller, animated: true) + vc.tableView.deselectRow(at: indexPath, animated: true) + vc.dialogService.dismissProgress() + vc.navigationController?.pushViewController(controller, animated: true) } } case .failure(let error): DispatchQueue.main.async { - self.tableView.deselectRow(at: indexPath, animated: true) - self.dialogService.dismissProgress() - self.dialogService.showRichError(error: error) + vc.tableView.deselectRow(at: indexPath, animated: true) + vc.dialogService.dismissProgress() + vc.dialogService.showRichError(error: error) } } } @@ -151,10 +159,17 @@ class DogeTransactionsViewController: TransactionsListViewControllerBase { 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: nil, + partnerName: partnerName, amount: transaction.amountValue, date: transaction.dateValue) } @@ -162,7 +177,7 @@ class DogeTransactionsViewController: TransactionsListViewControllerBase { // MARK: - Load more private func loadMoreTransactions(from: Int) { let procedure = LoadMoreDogeTransactionsProcedure(service: walletService, from: from) - print("getting from \(from)") + procedure.addDidFinishBlockObserver { [weak self] (procedure, error) in guard let result = procedure.result else { return diff --git a/Adamant/Wallets/Doge/DogeWalletService.swift b/Adamant/Wallets/Doge/DogeWalletService.swift index b043d4ced..423b7d636 100644 --- a/Adamant/Wallets/Doge/DogeWalletService.swift +++ b/Adamant/Wallets/Doge/DogeWalletService.swift @@ -446,15 +446,18 @@ extension DogeWalletService { Alamofire.request(endpoint, method: .get, parameters: parameters, headers: headers).responseData(queue: defaultDispatchQueue) { response in switch response.result { case .success(let data): - guard let dogeResponse = try? DogeWalletService.jsonDecoder.decode(DogeGetTransactionsResponse.self, from: data) else { - completion(.failure(.internalError(message: "DOGE Wallet: not valid response", error: nil))) - break + 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))) } - completion(.success(dogeResponse)) + case .failure(let error as URLError): + completion(.failure(.networkError(error: error))) case .failure(let error): - completion(.failure(.internalError(message: "DOGE Wallet: server not responding", error: error))) + completion(.failure(.serverError(error: error.localizedDescription))) } } } From c353c317dd0f8be370f4c98a992f6046e4f11061 Mon Sep 17 00:00:00 2001 From: Pavel Anokhov Date: Thu, 11 Apr 2019 12:39:04 +0300 Subject: [PATCH 33/46] New doge wallet icon --- .../wallet_doge.imageset/wallet_doge.png | Bin 1089 -> 9093 bytes .../wallet_doge.imageset/wallet_doge@2x.png | Bin 2028 -> 22342 bytes .../wallet_doge.imageset/wallet_doge@3x.png | Bin 3398 -> 38177 bytes 3 files changed, 0 insertions(+), 0 deletions(-) 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 index 20e1bf4fb804a4cd5c8e41fd003e9a48af53b044..42fdb325517ff4a6f44d0949dabeef2a01be2e5a 100644 GIT binary patch literal 9093 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 F{{sPJp;Z6? delta 1069 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 delta 3397 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 Date: Thu, 11 Apr 2019 12:54:46 +0300 Subject: [PATCH 34/46] Fixed transfer to self value. --- Adamant/Models/DogeTransaction.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Adamant/Models/DogeTransaction.swift b/Adamant/Models/DogeTransaction.swift index 715c8e039..e3fc51057 100644 --- a/Adamant/Models/DogeTransaction.swift +++ b/Adamant/Models/DogeTransaction.swift @@ -76,6 +76,11 @@ struct DogeRawTransaction { 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 From 3b5e887ec3c0eb36625c7e467f6d85ebb63f40e4 Mon Sep 17 00:00:00 2001 From: Pavel Anokhov Date: Thu, 11 Apr 2019 13:53:05 +0300 Subject: [PATCH 35/46] Better doge transaction details, when opened from chat --- ...ogeWalletService+RichMessageProvider.swift | 87 +++++++++++++------ 1 file changed, 60 insertions(+), 27 deletions(-) diff --git a/Adamant/Wallets/Doge/DogeWalletService+RichMessageProvider.swift b/Adamant/Wallets/Doge/DogeWalletService+RichMessageProvider.swift index afbe533bc..388ce7675 100644 --- a/Adamant/Wallets/Doge/DogeWalletService+RichMessageProvider.swift +++ b/Adamant/Wallets/Doge/DogeWalletService+RichMessageProvider.swift @@ -31,46 +31,77 @@ extension DogeWalletService: RichMessageProvider { comment = nil } + // MARK: Get transaction getTransaction(by: hash) { [weak self] result in - dialogService.dismissProgress() - guard let vc = self?.router.get(scene: AdamantScene.Wallets.Doge.transactionDetails) as? DogeTransactionDetailsViewController else { + guard let vc = self?.router.get(scene: AdamantScene.Wallets.Doge.transactionDetails) as? DogeTransactionDetailsViewController, let service = self else { return } - vc.service = self + // MARK: 1. Prepare details view controller + vc.service = service vc.comment = comment switch result { - case .success(let dogeTransaction): - let transaction = dogeTransaction.asDogeTransaction(for: address) + case .success(let dogeRawTransaction): + let dogeTransaction = dogeRawTransaction.asDogeTransaction(for: address) - // Sender name - if transaction.senderAddress == address { + // MARK: 2. Self name + if dogeTransaction.senderAddress == address { vc.senderName = String.adamantLocalized.transactionDetails.yourAddress - } else if transaction.recipientAddress == address { + } + if dogeTransaction.recipientAddress == address { vc.recipientName = String.adamantLocalized.transactionDetails.yourAddress } - guard let blockHash = dogeTransaction.blockHash else { - vc.transaction = transaction - break - } + vc.transaction = dogeTransaction + + let group = DispatchGroup() - self?.getBlockId(by: blockHash) { result in - switch result { - case .success(let id): - vc.transaction = dogeTransaction.asDogeTransaction(for: address, blockId: id) + // 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 + } - case .failure: - vc.transaction = transaction + group.leave() // Leave 1 } - - DispatchQueue.main.async { - chat.navigationController?.pushViewController(vc, animated: true) + } + + // 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 } } - return + // 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 { @@ -95,15 +126,17 @@ extension DogeWalletService: RichMessageProvider { vc.transaction = failedTransaction + DispatchQueue.main.async { + dialogService.dismissProgress() + chat.navigationController?.pushViewController(vc, animated: true) + } + default: - self?.dialogService.showRichError(error: error) - return + dialogService.dismissProgress() + dialogService.showRichError(error: error) } } - DispatchQueue.main.async { - chat.navigationController?.pushViewController(vc, animated: true) - } } } From 00de942d2e749fe6217d20c1a362260e887e8ca0 Mon Sep 17 00:00:00 2001 From: Pavel Anokhov Date: Thu, 11 Apr 2019 14:02:06 +0300 Subject: [PATCH 36/46] Fixed naming for transactions to self. --- Adamant/Wallets/Doge/DogeTransferViewController.swift | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Adamant/Wallets/Doge/DogeTransferViewController.swift b/Adamant/Wallets/Doge/DogeTransferViewController.swift index 8e7c62882..e4d8dd35e 100644 --- a/Adamant/Wallets/Doge/DogeTransferViewController.swift +++ b/Adamant/Wallets/Doge/DogeTransferViewController.swift @@ -85,8 +85,14 @@ class DogeTransferViewController: TransferViewControllerBase { detailsVc.transaction = transaction detailsVc.service = service + detailsVc.senderName = String.adamantLocalized.transactionDetails.yourAddress - detailsVc.recipientName = self?.recipientName + + if let recipientName = self?.recipientName { + detailsVc.recipientName = recipientName + } else if transaction.recipientAddress == sender { + detailsVc.recipientName = String.adamantLocalized.transactionDetails.yourAddress + } if comments.count > 0 { detailsVc.comment = comments From 84ccbbe183e2ac2083d8651c0e2f3f7f4b67da59 Mon Sep 17 00:00:00 2001 From: Pavel Anokhov Date: Thu, 11 Apr 2019 19:36:02 +0300 Subject: [PATCH 37/46] richContentType.lowercased() --- .../ServiceProtocols/RichMessageProvider.swift | 1 + .../DataProviders/AdamantChatsProvider.swift | 15 ++++++++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/Adamant/ServiceProtocols/RichMessageProvider.swift b/Adamant/ServiceProtocols/RichMessageProvider.swift index 783985b47..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 } 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 From 7c7f194df43251a8936d529262cb2dc3d1672018 Mon Sep 17 00:00:00 2001 From: Pavel Anokhov Date: Thu, 11 Apr 2019 20:07:39 +0300 Subject: [PATCH 38/46] LSK wallet icon updated --- .../Icons/wallet_lsk.imageset/wallet_lsk.png | Bin 1281 -> 0 bytes .../Icons/wallet_lsk.imageset/wallet_lsk@2x.png | Bin 2684 -> 0 bytes .../Icons/wallet_lsk.imageset/wallet_lsk@3x.png | Bin 4180 -> 0 bytes .../wallet_lsk.imageset/Contents.json | 0 .../Wallets/wallet_lsk.imageset/wallet_lsk.png | Bin 0 -> 2147 bytes .../wallet_lsk.imageset/wallet_lsk@2x.png | Bin 0 -> 4474 bytes .../wallet_lsk.imageset/wallet_lsk@3x.png | Bin 0 -> 6920 bytes 7 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 Adamant/Assets/Assets.xcassets/Icons/wallet_lsk.imageset/wallet_lsk.png delete mode 100644 Adamant/Assets/Assets.xcassets/Icons/wallet_lsk.imageset/wallet_lsk@2x.png delete mode 100644 Adamant/Assets/Assets.xcassets/Icons/wallet_lsk.imageset/wallet_lsk@3x.png rename Adamant/Assets/Assets.xcassets/{Icons => Wallets}/wallet_lsk.imageset/Contents.json (100%) create mode 100644 Adamant/Assets/Assets.xcassets/Wallets/wallet_lsk.imageset/wallet_lsk.png create mode 100644 Adamant/Assets/Assets.xcassets/Wallets/wallet_lsk.imageset/wallet_lsk@2x.png create mode 100644 Adamant/Assets/Assets.xcassets/Wallets/wallet_lsk.imageset/wallet_lsk@3x.png 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 908fda752e4e519fefd7fe577250dc6e89b39006..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1281 zcmV+c1^)VpP)!u@u1X1bx1jn*3OKRSsq!I#j+&?+HY zk%fEJ+;VkQ$gDe(`00_{7qk^Y|7$r8_U_d(%d-DDvk?J_G0|Vp-bp1`Jt@bI-!2ya z)zH7jg2`-m0hm36nzy4_AD!6@EW}C_$=w1b-{L1T-7~o0(5D8^*c+$0j=)6$hAFwBI^`brm^#j+DfeHTewhh4)V+7+eO zVlET>3{0Ve&s=dBj3imw69vmskA4%sQ{XdW4)P)X813~OmaS0A4YZjU>s;7-IrvNz zT#PS3y~wg{fnd2f0OyWBCGa^ni!FrTqrZj2l84qO7Ce(2<=}H#)JF9CPbOgO{HnA zg5d4|CMOl+mj#SZtg9V7f3=WfKXHD-M?N3@epj6BpP+4^smdNi3nIlnURj&$0Kn#F zyUj_Q{rnBt2=TYb-=s|n0r(6J^2cpk51XU7`uX$=BE!B}A%d+^_$v?LFEW&;W?N$S zfZ_V5l|}&aU8c=1pRkDu7ge5`Z6wAjI!m!QPhw=tGTf1T`8#M=@_JiiYaGTFBX)GUr*j*s>zzd>aT z)8@k7&A~@5j#+{*=={ju{Rgg7#69Xi+}<->5#S*(1$I}V5lu1T4b;41W?deF)j{~C z_^QHriP~d$;9Ny~awOMKG9GAimdyGKU=rIgz&AfF|4{Il08A*k3$&SxSA9_^ui?PE zlpHRHbDNhpPU)Bl7l(DOz%NwB=YcrVu4t(;0MBFnp>YW}-d=Z!S6fNcpxglORR($n zToJUg!b!3nUfm?QC9IV2F(33c*$u>!A3nIjjw(eM$Vrku{p|g~MLB(7*uT;eRjy}# zfn@s;;G#T1IVqeR>C4Z)2wYugY{GXKAE15#W&8S&Q8^i$6zRjyegvFq(;W7)j)G$z zYcz}IY2bL0@DdV~ht3VC^B`;!`s0)Fk_{NOe>MjC&8nuI4~u>xxaM?EzkI)$%D)~; z5lEAW7p7PwHWapI2SPvd#JEIFzVA2%^&%hCwn(t*3|q4?roXLXTq-7CxZZ{aB_sRF z1S{_Nwc9)Xc58{TshJ%R=@nEa;bor#rjEmkL`m2SXSQ(yI1xFM!HE0~Y7%qn575^_ zu;c3iw=bxC4O}NNE@$Q>AHwg@&cA55G6zObLT(>lHVttHMJ@BA4m8GXMxDDDlh6vZ rU30L!z4|~8IQ5|75IW0)0Ghr(#6MF;q#5$g00000NkvXXu0mjfWP@cw 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 c6a0035e62ead11ecb86be4f378e75319464befc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2684 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 Z?*IyAMFQ5Pb literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..cf2528f8e270b4eeeefe817b7ec52f8ea76e1bd8 GIT binary patch literal 4474 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 M07*qoM6N<$f)+!cFaQ7m literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..f079d95776804c8584f8f8551795365ed7b732ed GIT binary patch literal 6920 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 Date: Thu, 11 Apr 2019 20:07:45 +0300 Subject: [PATCH 39/46] Version --- Adamant/Info.plist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Adamant/Info.plist b/Adamant/Info.plist index fd9237af7..55055523c 100644 --- a/Adamant/Info.plist +++ b/Adamant/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 1.5 CFBundleVersion - 72 + 73 LSRequiresIPhoneOS NSAppTransportSecurity From d50f6ab7e6a628a09b892c7f3ca9191dc4c313e0 Mon Sep 17 00:00:00 2001 From: Pavel Anokhov Date: Fri, 12 Apr 2019 18:07:18 +0300 Subject: [PATCH 40/46] Fixed income transfers floating points fuckups (-1 transfers to self and 4.9999999) --- Adamant/Models/DogeTransaction.swift | 4 ++-- Adamant/Wallets/Doge/DogeWallet.swift | 8 ++++++++ Adamant/Wallets/Doge/DogeWalletService.swift | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/Adamant/Models/DogeTransaction.swift b/Adamant/Models/DogeTransaction.swift index e3fc51057..cabe2a89f 100644 --- a/Adamant/Models/DogeTransaction.swift +++ b/Adamant/Models/DogeTransaction.swift @@ -222,7 +222,7 @@ extension DogeRawTransaction: Decodable { struct DogeInput: Decodable { enum CodingKeys: String, CodingKey { case sender = "addr" - case value + case value = "valueSat" case txId = "txid" case vOut = "vout" } @@ -246,7 +246,7 @@ struct DogeInput: Decodable { self.vOut = try container.decode(Int.self, forKey: .vOut) if let raw = try? container.decode(Decimal.self, forKey: .value) { - self.value = raw + self.value = Decimal(sign: .plus, exponent: DogeWalletService.currencyExponent, significand: raw) } else { self.value = 0 } diff --git a/Adamant/Wallets/Doge/DogeWallet.swift b/Adamant/Wallets/Doge/DogeWallet.swift index 95eca3abc..fe9e8b8b2 100644 --- a/Adamant/Wallets/Doge/DogeWallet.swift +++ b/Adamant/Wallets/Doge/DogeWallet.swift @@ -21,4 +21,12 @@ class DogeWallet: WalletAccount { self.publicKey = privateKey.publicKey() self.address = publicKey.toCashaddr().base58 } + + init(address: String, privateKey: PrivateKey, publicKey: PublicKey, balance: Decimal, notifications: Int) { + self.address = address + self.privateKey = privateKey + self.publicKey = publicKey + self.balance = balance + self.notifications = notifications + } } diff --git a/Adamant/Wallets/Doge/DogeWalletService.swift b/Adamant/Wallets/Doge/DogeWalletService.swift index 423b7d636..9f3193d40 100644 --- a/Adamant/Wallets/Doge/DogeWalletService.swift +++ b/Adamant/Wallets/Doge/DogeWalletService.swift @@ -65,7 +65,7 @@ class DogeWalletService: WalletService { // 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 From 9e58b5cfb033a5087a0925ee92614be30272feee Mon Sep 17 00:00:00 2001 From: Pavel Anokhov Date: Fri, 12 Apr 2019 19:42:21 +0300 Subject: [PATCH 41/46] Fixed BitcoinKit UInt64 crash --- .../Wallets/Doge/DogeTransferViewController.swift | 1 - Adamant/Wallets/Doge/DogeWalletService+Send.swift | 14 ++++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/Adamant/Wallets/Doge/DogeTransferViewController.swift b/Adamant/Wallets/Doge/DogeTransferViewController.swift index e4d8dd35e..c61881adc 100644 --- a/Adamant/Wallets/Doge/DogeTransferViewController.swift +++ b/Adamant/Wallets/Doge/DogeTransferViewController.swift @@ -133,7 +133,6 @@ class DogeTransferViewController: TransferViewControllerBase { } case .failure(let error): - dialogService.dismissProgress() dialogService.showRichError(error: error) } } diff --git a/Adamant/Wallets/Doge/DogeWalletService+Send.swift b/Adamant/Wallets/Doge/DogeWalletService+Send.swift index 5a19e0b34..fd5c2f781 100644 --- a/Adamant/Wallets/Doge/DogeWalletService+Send.swift +++ b/Adamant/Wallets/Doge/DogeWalletService+Send.swift @@ -49,16 +49,22 @@ extension DogeWalletService: WalletServiceTwoStepSend { let fee = NSDecimalNumber(decimal: self.transactionFee * DogeWalletService.multiplier).uint64Value // MARK: 2. Search for unspent transactions - self.getUnspentTransactions { result in + getUnspentTransactions { result in switch result { case .success(let utxos): - // MARK: 3. Create local transaction + // 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)) - break + case .failure: completion(.failure(error: .notEnoughMoney)) - break } } } From d37f9b7e3b297306ee43e6877c568a8982e47f45 Mon Sep 17 00:00:00 2001 From: Pavel Anokhov Date: Fri, 12 Apr 2019 20:55:18 +0300 Subject: [PATCH 42/46] Fixed doge list crash on refresh. --- .../Doge/DogeTransactionsViewController.swift | 54 ++++++++++--------- Adamant/Wallets/Doge/DogeWallet.swift | 4 +- 2 files changed, 31 insertions(+), 27 deletions(-) diff --git a/Adamant/Wallets/Doge/DogeTransactionsViewController.swift b/Adamant/Wallets/Doge/DogeTransactionsViewController.swift index 19317052b..110785b0e 100644 --- a/Adamant/Wallets/Doge/DogeTransactionsViewController.swift +++ b/Adamant/Wallets/Doge/DogeTransactionsViewController.swift @@ -36,31 +36,38 @@ class DogeTransactionsViewController: TransactionsListViewControllerBase { } override func handleRefresh(_ refreshControl: UIRefreshControl) { - transactions.removeAll() + procedureQueue.cancelAllOperations() loadedTo = 0 - walletService.getTransactions(from: 0) { [weak self] result in + walletService.getTransactions(from: loadedTo) { [weak self] result in + guard let vc = self else { + refreshControl.endRefreshing() + return + } + switch result { case .success(let tuple): - self?.transactions = tuple.transactions - self?.loadedTo = tuple.transactions.count - - if tuple.hasMore { - self?.loadMoreTransactions(from: tuple.transactions.count) - } + vc.transactions = tuple.transactions + vc.loadedTo = tuple.transactions.count DispatchQueue.main.async { - self?.tableView.reloadData() - self?.refreshControl.endRefreshing() + 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 { - self?.tableView.reloadData() - self?.refreshControl.endRefreshing() + vc.tableView.reloadData() + refreshControl.endRefreshing() } - - self?.dialogService.showRichError(error: error) } } } @@ -179,17 +186,11 @@ class DogeTransactionsViewController: TransactionsListViewControllerBase { let procedure = LoadMoreDogeTransactionsProcedure(service: walletService, from: from) procedure.addDidFinishBlockObserver { [weak self] (procedure, error) in - guard let result = procedure.result else { - return - } - - guard let vc = self else { + guard let vc = self, let result = procedure.result else { return } let total = vc.loadedTo + result.transactions.count - vc.loadedTo = total - vc.transactions.append(contentsOf: result.transactions) var indexPaths = [IndexPath]() for index in from.. Date: Fri, 12 Apr 2019 21:15:15 +0300 Subject: [PATCH 43/46] Color for eth logo. --- .../wallet_eth.imageset/wallet_eth.png | Bin 1949 -> 2295 bytes .../wallet_eth.imageset/wallet_eth@2x.png | Bin 4046 -> 4685 bytes .../wallet_eth.imageset/wallet_eth@3x.png | Bin 6323 -> 7129 bytes 3 files changed, 0 insertions(+), 0 deletions(-) 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 abb0511b3daf7f584619b9575ae1fff62777f8f8..7c44276ab2b08966ee202b3797ef0f03c50c2e7e 100644 GIT binary patch delta 2271 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 IM6N<$f^Bf7g8%>k 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 d7224b8790588a8188738552733bfea15a5cee8a..192000517ff217a9f34395b74e93f93082c710ea 100644 GIT binary patch delta 4680 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 KMNUMnLSTX~LMg`p delta 4036 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 P00000NkvXXu0mjfr~k6- literal 6323 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 Date: Fri, 12 Apr 2019 21:15:26 +0300 Subject: [PATCH 44/46] Version --- Adamant/Info.plist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Adamant/Info.plist b/Adamant/Info.plist index 55055523c..18827a24a 100644 --- a/Adamant/Info.plist +++ b/Adamant/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 1.5 CFBundleVersion - 73 + 74 LSRequiresIPhoneOS NSAppTransportSecurity From 8d263d59f65ebef0a7254a7e66c6b95962ea4587 Mon Sep 17 00:00:00 2001 From: Pavel Anokhov Date: Mon, 15 Apr 2019 17:15:05 +0300 Subject: [PATCH 45/46] Fixed ADM transfers with comments. --- .../Services/DataProviders/AdamantTransfersProvider.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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) From d6e574dae4a42ed0926d31edf8e04e752b2d0212 Mon Sep 17 00:00:00 2001 From: Pavel Anokhov Date: Mon, 15 Apr 2019 17:15:20 +0300 Subject: [PATCH 46/46] Version --- Adamant/Info.plist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Adamant/Info.plist b/Adamant/Info.plist index 18827a24a..6137ab786 100644 --- a/Adamant/Info.plist +++ b/Adamant/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 1.5 CFBundleVersion - 74 + 75 LSRequiresIPhoneOS NSAppTransportSecurity