From f2335b75753cda01dac8ad39aa1f7eab3dfe28f6 Mon Sep 17 00:00:00 2001 From: Anton B Date: Wed, 13 Jun 2018 13:50:45 +0300 Subject: [PATCH 001/175] Eth integration init --- Adamant/AppDelegate.swift | 4 ++++ Podfile | 15 +++++++++++++++ Podfile.lock | 40 +++++++++++++++++++++++++++++++++++++-- 3 files changed, 57 insertions(+), 2 deletions(-) diff --git a/Adamant/AppDelegate.swift b/Adamant/AppDelegate.swift index b6cb1f269..5546e2220 100644 --- a/Adamant/AppDelegate.swift +++ b/Adamant/AppDelegate.swift @@ -45,6 +45,10 @@ struct AdamantResources { Node(scheme: .https, host: "lake.adamant.im", port: nil), Node(scheme: .http, host: "80.211.177.181", port: nil) ] + + static let ethServers = [ + "https://api.myetherapi.com/eth" + ] // Addresses static let supportEmail = "ios@adamant.im" diff --git a/Podfile b/Podfile index 69923c6be..831719764 100644 --- a/Podfile +++ b/Podfile @@ -26,4 +26,19 @@ target 'Adamant' do pod 'EFQRCode' # QR generator pod 'QRCodeReader.swift' # QR reader + # ETH + # https://github.com/ethereum/web3.js/ + +# https://github.com/MercuryProtocol/web3.swift +# https://medium.com/mercuryprotocol/introducing-web3-swift-for-ethereum-ios-development-1e02212b662b +# pod 'web3swift', :git => 'https://github.com/MercuryProtocol/web3.swift.git', :branch => 'master' + +# https://github.com/Boilertalk/Web3.swift + pod 'Web3' + + +# https://github.com/yuzushioh/EthereumKit +# https://github.com/bankex/web3swift +# https://github.com/IndisputableLabs/Swifthereum + end diff --git a/Podfile.lock b/Podfile.lock index 5551e1916..ad69be5c4 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -1,5 +1,6 @@ PODS: - Alamofire (4.7.3) + - BigInt.swift (1.0.0) - CryptoSwift (0.10.0) - DateToolsSwift (4.0.0) - EFQRCode (4.2.2) @@ -21,7 +22,21 @@ PODS: - QRCodeReader.swift (8.2.0) - ReachabilitySwift (4.1.0) - RNCryptor (5.0.3) +<<<<<<< HEAD - Swinject (2.4.1) +======= + - secp256k1.swift (0.1.4) + - Swinject (2.4.0) + - Web3 (0.1.1): + - Web3/Core (= 0.1.1) + - Web3/HTTPExtension (= 0.1.1) + - Web3/Core (0.1.1): + - BigInt.swift (~> 1.0) + - CryptoSwift (~> 0.8) + - secp256k1.swift (~> 0.1) + - Web3/HTTPExtension (0.1.1): + - Web3/Core +>>>>>>> Eth integration init DEPENDENCIES: - Alamofire @@ -41,10 +56,12 @@ DEPENDENCIES: - ReachabilitySwift - RNCryptor - Swinject + - Web3 SPEC REPOS: https://github.com/cocoapods/specs.git: - Alamofire + - BigInt.swift - CryptoSwift - DateToolsSwift - EFQRCode @@ -60,10 +77,23 @@ SPEC REPOS: - QRCodeReader.swift - ReachabilitySwift - RNCryptor + - secp256k1.swift - Swinject + - Web3 + +EXTERNAL SOURCES: + MessageKit: + :branch: develop + :git: https://github.com/boyarkin-anton/MessageKit.git + +CHECKOUT OPTIONS: + MessageKit: + :commit: e2364d2394eddc211be6957893fbd0f726c233f0 + :git: https://github.com/boyarkin-anton/MessageKit.git SPEC CHECKSUMS: - Alamofire: c7287b6e5d7da964a70935e5db17046b7fde6568 + Alamofire: e4fa87002c137ba2d8d634d2c51fabcda0d5c223 + BigInt.swift: 1e0ddf08d82166e72dfb0fa8d40301d273081afc CryptoSwift: 6c778d69282bed3b4e975ff97a79d074f20bb011 DateToolsSwift: 875d97ff9e3a5d54abdd67a269b3f51c757b71ab EFQRCode: 203846c7b135b5f7c1d18bb7923bc32a74c8b363 @@ -79,8 +109,14 @@ SPEC CHECKSUMS: QRCodeReader.swift: 003eb32f18a5a675b936ec82ba0ff368cddbff45 ReachabilitySwift: 6849231cd4e06559f3b9ef4a97a0a0f96d41e09f RNCryptor: c93d19029dcf7ff160aca0f24d6c9e7b0d82f664 +<<<<<<< HEAD Swinject: f7f15a9672e99328c1f07f277091087917215700 +======= + secp256k1.swift: a7e7a214f6db6ce5db32cc6b2b45e5c4dd633634 + Swinject: a1364b0f66c2736bb03c1c7cab54809e16df25da + Web3: 1f0c38c8508b8487948af1cb597653b018f4b5d3 +>>>>>>> Eth integration init -PODFILE CHECKSUM: 6e128978754d16ff0e2de713d72463d4309871f9 +PODFILE CHECKSUM: 515461f4af013cf25e5baf6629f9ebac87a15c17 COCOAPODS: 1.5.3 From 506ff0a4d888a0049a7b99d4cb6285bd3688696f Mon Sep 17 00:00:00 2001 From: Anton B Date: Sun, 17 Jun 2018 18:18:39 +0300 Subject: [PATCH 002/175] Eth account create/login/balance Eth account create/login Get Eth account balance --- Adamant.xcodeproj/project.pbxproj | 34 +++++ Adamant/AppDelegate.swift | 2 + .../EthApiServiceProtocol.swift | 22 +++ .../EthApiService/EthApiService.swift | 128 ++++++++++++++++++ Adamant/Stories/Account/AccountRoutes.swift | 1 + .../Account/AccountViewController.swift | 24 ++++ Adamant/Stories/Login/LoginRoutes.swift | 1 + .../Stories/Login/LoginViewController.swift | 24 +++- Adamant/SwinjectDependencies.swift | 10 +- Podfile | 14 +- Podfile.lock | 59 ++++---- 11 files changed, 269 insertions(+), 50 deletions(-) create mode 100644 Adamant/ServiceProtocols/EthApiServiceProtocol.swift create mode 100644 Adamant/Services/EthApiService/EthApiService.swift diff --git a/Adamant.xcodeproj/project.pbxproj b/Adamant.xcodeproj/project.pbxproj index 9f07aa778..ffbb057c0 100644 --- a/Adamant.xcodeproj/project.pbxproj +++ b/Adamant.xcodeproj/project.pbxproj @@ -23,6 +23,10 @@ E90055F720EC200900D0CB2D /* SecurityViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E90055F620EC200900D0CB2D /* SecurityViewController.swift */; }; E90055F920ECD86800D0CB2D /* SecurityViewController+StayIn.swift in Sources */ = {isa = PBXBuildFile; fileRef = E90055F820ECD86800D0CB2D /* SecurityViewController+StayIn.swift */; }; E90055FB20ECE78A00D0CB2D /* SecurityViewController+notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = E90055FA20ECE78A00D0CB2D /* SecurityViewController+notifications.swift */; }; + 644D3DBB20BD5F22002EB002 /* Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = 644D3DBA20BD5F22002EB002 /* Status.swift */; }; + 64D059FF20D3116B003AD655 /* NodesListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D059FE20D3116A003AD655 /* NodesListViewController.swift */; }; + 64D05A0220D51632003AD655 /* EthApiServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D05A0120D51632003AD655 /* EthApiServiceProtocol.swift */; }; + 64D05A0420D516B4003AD655 /* EthApiService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D05A0320D516B4003AD655 /* EthApiService.swift */; }; E905D39B2048A9BD00DDB504 /* KeychainStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = E905D39A2048A9BD00DDB504 /* KeychainStore.swift */; }; E905D39D204C13B900DDB504 /* SecuredStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = E905D39C204C13B900DDB504 /* SecuredStore.swift */; }; E905D39F204C281400DDB504 /* LoginViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E905D39E204C281400DDB504 /* LoginViewController.swift */; }; @@ -219,6 +223,10 @@ 64A223D520F760BB005157CB /* Localization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Localization.swift; sourceTree = ""; }; 64D059FE20D3116A003AD655 /* NodesListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NodesListViewController.swift; sourceTree = ""; }; 64E8304F20F5FEEF006FA590 /* VotesAsset.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VotesAsset.swift; sourceTree = ""; }; + 644D3DBA20BD5F22002EB002 /* Status.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Status.swift; sourceTree = ""; }; + 64D059FE20D3116A003AD655 /* NodesListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NodesListViewController.swift; sourceTree = ""; }; + 64D05A0120D51632003AD655 /* EthApiServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EthApiServiceProtocol.swift; sourceTree = ""; }; + 64D05A0320D516B4003AD655 /* EthApiService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EthApiService.swift; sourceTree = ""; }; 871009461457F6B7B47D99C5 /* Pods-Adamant.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Adamant.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Adamant/Pods-Adamant.debug.xcconfig"; sourceTree = ""; }; 903AC078A8A55175A05E42D4 /* Pods-Adamant.testing.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Adamant.testing.xcconfig"; path = "Pods/Target Support Files/Pods-Adamant/Pods-Adamant.testing.xcconfig"; sourceTree = ""; }; C107D5CB65B4D728B9D97C0F /* Pods_Adamant.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Adamant.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -454,6 +462,14 @@ path = Delegates; sourceTree = ""; }; + 64D05A0020D515CA003AD655 /* EthApiService */ = { + isa = PBXGroup; + children = ( + 64D05A0320D516B4003AD655 /* EthApiService.swift */, + ); + path = EthApiService; + sourceTree = ""; + }; E59396A8E0053F21F768E69B /* Pods */ = { isa = PBXGroup; children = ( @@ -521,6 +537,7 @@ E93D7ABD2052CEE1005D19DC /* NotificationsService.swift */, E9A174B22057EC47003667CD /* BackgroundFetchService.swift */, E9215972206119FB0000CA5C /* ReachabilityMonitor.swift */, + 64D05A0120D51632003AD655 /* EthApiServiceProtocol.swift */, ); path = ServiceProtocols; sourceTree = ""; @@ -528,6 +545,7 @@ E913C9061FFFA92E001A83F7 /* Services */ = { isa = PBXGroup; children = ( + 64D05A0020D515CA003AD655 /* EthApiService */, E9CAE8D02018AA5000345E76 /* ApiService */, E9B3D39F201FA2090019EB36 /* DataProviders */, E9E7CD922002740500DFC4DB /* AdamantAccountService.swift */, @@ -1025,6 +1043,8 @@ inputPaths = ( "${SRCROOT}/Pods/Target Support Files/Pods-Adamant/Pods-Adamant-frameworks.sh", "${BUILT_PRODUCTS_DIR}/Alamofire/Alamofire.framework", + "${BUILT_PRODUCTS_DIR}/Alamofire-Synchronous/Alamofire_Synchronous.framework", + "${BUILT_PRODUCTS_DIR}/BigInt/BigInt.framework", "${BUILT_PRODUCTS_DIR}/CryptoSwift/CryptoSwift.framework", "${BUILT_PRODUCTS_DIR}/DateToolsSwift/DateToolsSwift.framework", "${BUILT_PRODUCTS_DIR}/EFQRCode/EFQRCode.framework", @@ -1040,11 +1060,18 @@ "${BUILT_PRODUCTS_DIR}/QRCodeReader.swift/QRCodeReader.framework", "${BUILT_PRODUCTS_DIR}/RNCryptor/RNCryptor.framework", "${BUILT_PRODUCTS_DIR}/ReachabilitySwift/Reachability.framework", + "${BUILT_PRODUCTS_DIR}/Result/Result.framework", + "${BUILT_PRODUCTS_DIR}/SipHash/SipHash.framework", "${BUILT_PRODUCTS_DIR}/Swinject/Swinject.framework", + "${BUILT_PRODUCTS_DIR}/libsodium/libsodium.framework", + "${BUILT_PRODUCTS_DIR}/secp256k1_ios/secp256k1_ios.framework", + "${BUILT_PRODUCTS_DIR}/web3swift/web3swift.framework", ); name = "[CP] Embed Pods Frameworks"; outputPaths = ( "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Alamofire.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Alamofire_Synchronous.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/BigInt.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/CryptoSwift.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DateToolsSwift.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/EFQRCode.framework", @@ -1060,7 +1087,12 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/QRCodeReader.framework", "${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}/SipHash.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Swinject.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/libsodium.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/secp256k1_ios.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/web3swift.framework", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -1132,6 +1164,7 @@ E921597B206503000000CA5C /* ButtonsStripeView.swift in Sources */, E9A03FD820DC0ABA007653A1 /* AdamantNodesSource.swift in Sources */, E9E7CD8D20026B6600DFC4DB /* DialogService.swift in Sources */, + 64D05A0420D516B4003AD655 /* EthApiService.swift in Sources */, E9E7CDB72003994E00DFC4DB /* AdamantUtilities.swift in Sources */, E9147B6320505C7500145913 /* QRCodeReader+adamant.swift in Sources */, E90055F720EC200900D0CB2D /* SecurityViewController.swift in Sources */, @@ -1190,6 +1223,7 @@ E93B0D742028B21400126346 /* ChatsProvider.swift in Sources */, E9150BA12066DA210065A985 /* ChatTransaction+CoreDataClass.swift in Sources */, E9CAE8D42018AC1800345E76 /* AdamantApi+Keys.swift in Sources */, + 64D05A0220D51632003AD655 /* EthApiServiceProtocol.swift in Sources */, E9C51EEF20139DC600385EB7 /* TransactionIdResponse.swift in Sources */, E9722066201F42BB004F2AAD /* CoreDataStack.swift in Sources */, E913C8F21FFFA51D001A83F7 /* AppDelegate.swift in Sources */, diff --git a/Adamant/AppDelegate.swift b/Adamant/AppDelegate.swift index 5546e2220..7acf5d3f5 100644 --- a/Adamant/AppDelegate.swift +++ b/Adamant/AppDelegate.swift @@ -81,6 +81,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { var accountService: AccountService! var notificationService: NotificationsService! var dialogService: DialogService! + var ethApiService: EthApiServiceProtocol! // MARK: - Lifecycle @@ -91,6 +92,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { accountService = container.resolve(AccountService.self) notificationService = container.resolve(NotificationsService.self) dialogService = container.resolve(DialogService.self) + ethApiService = container.resolve(EthApiServiceProtocol.self) // MARK: 2. Init UI window = UIWindow(frame: UIScreen.main.bounds) diff --git a/Adamant/ServiceProtocols/EthApiServiceProtocol.swift b/Adamant/ServiceProtocols/EthApiServiceProtocol.swift new file mode 100644 index 000000000..3ec865dc9 --- /dev/null +++ b/Adamant/ServiceProtocols/EthApiServiceProtocol.swift @@ -0,0 +1,22 @@ +// +// EthApiService.swift +// Adamant +// +// Created by Anton Boyarkin on 16/06/2018. +// Copyright © 2018 Adamant. All rights reserved. +// + +import Foundation +import web3swift + +protocol EthApiServiceProtocol: class { + + var account: EthAccount? { get } + + // MARK: - Accounts + func newAccount(byPassphrase passphrase: String, completion: @escaping (ApiServiceResult) -> Void) + + // MARK: - Tools + func getBalance(_ completion: @escaping (ApiServiceResult) -> Void) + func getBalance(byAddress address: String, completion: @escaping (ApiServiceResult) -> Void) +} diff --git a/Adamant/Services/EthApiService/EthApiService.swift b/Adamant/Services/EthApiService/EthApiService.swift new file mode 100644 index 000000000..ab4c0d14b --- /dev/null +++ b/Adamant/Services/EthApiService/EthApiService.swift @@ -0,0 +1,128 @@ +// +// EthApiService.swift +// Adamant +// +// Created by Anton Boyarkin on 16/06/2018. +// Copyright © 2018 Adamant. All rights reserved. +// + +import Foundation +import web3swift +import BigInt + +// MARK: - Notifications +extension Notification.Name { + struct EthApiService { + /// Raised when user has logged out. + static let userLoggedOut = Notification.Name("adamant.ethApiService.userHasLoggedOut") + + /// Raised when user has successfully logged in. + static let userLoggedIn = Notification.Name("adamant.ethApiService.userHasLoggedIn") + + private init() {} + } +} + +struct EthAccount { + let wallet: BIP32Keystore + let address: String? + var balance: BigUInt? + var balanceString: String? +} + +class EthApiService: EthApiServiceProtocol { + + // MARK: - Dependencies + var adamantCore: AdamantCore! + + // MARK: - Properties + var apiUrl: String + var web3: web3! + + private(set) var account: EthAccount? + + // MARK: - Initialization + + init(apiUrl: String) { + self.apiUrl = apiUrl + + // test network + self.apiUrl = "https://ropsten.infura.io/" + + if let url = URL(string: self.apiUrl), let web3 = Web3.new(url) { + self.web3 = web3 + } else { + print("Unable init Web3") + return + } + + let gasPriceResult = self.web3.eth.getGasPrice() + guard case .success(let gasPrice) = gasPriceResult else { return } + + print("ETH Server gas Price: \(gasPrice)") + + if let network = self.web3.provider.network { print("ETH Server network: \(network)") } + } + + func newAccount(byPassphrase passphrase: String, completion: @escaping (ApiServiceResult) -> Void) { + guard let keystore = try? BIP32Keystore(mnemonics: passphrase, + password: "", + mnemonicsPassword: "", + language: .english), + let wallet = keystore else { + completion(.failure(.internalError(message: "ETH Wallet: fail to create Keystore", error: nil))) + return + } + self.account = EthAccount(wallet: wallet, address: wallet.addresses?.first?.address, balance: nil, balanceString: nil) + if let account = self.account { + NotificationCenter.default.post(name: Notification.Name.EthApiService.userLoggedIn, object: self) + completion(.success(account)) + } else { + completion(.failure(.internalError(message: "ETH Wallet: fail to create Account", error: nil))) + } + } + + func getBalance(_ completion: @escaping (ApiServiceResult) -> Void) { + if let walletAddress = self.account?.wallet.addresses?.first { + let balanceResult = web3.eth.getBalance(address: walletAddress) + guard case .success(let balance) = balanceResult else { + completion(.failure(.internalError(message: "ETH Wallet: fail to get balance", error: nil))) + return + } + self.account?.balance = balance + + if let formattedAmount = Web3.Utils.formatToEthereumUnits(balance, + toUnits: .eth, + decimals: 5, + fallbackToScientific: true) { + self.account?.balanceString = formattedAmount + completion(.success("\(formattedAmount) ETH")) + } else { + completion(.failure(.internalError(message: "ETH Wallet: fail to get balance amount", error: nil))) + } + } else { + completion(.failure(.internalError(message: "ETH Wallet: not found", error: nil))) + } + } + + func getBalance(byAddress address: String, completion: @escaping (ApiServiceResult) -> Void) { + if let walletAddress = EthereumAddress(address) { + let balanceResult = web3.eth.getBalance(address: walletAddress) + guard case .success(let balance) = balanceResult else { + completion(.failure(.internalError(message: "ETH Wallet: fail to get balance", error: nil))) + return + } + + if let formattedAmount = Web3.Utils.formatToEthereumUnits(balance, + toUnits: .eth, + decimals: 8, + fallbackToScientific: true) { + completion(.success("\(formattedAmount) ETH")) + } else { + completion(.failure(.internalError(message: "ETH Wallet: fail to get balance amount", error: nil))) + } + } else { + completion(.failure(.internalError(message: "ETH Wallet: not found", error: nil))) + } + } +} diff --git a/Adamant/Stories/Account/AccountRoutes.swift b/Adamant/Stories/Account/AccountRoutes.swift index a6db08315..95ea1fc07 100644 --- a/Adamant/Stories/Account/AccountRoutes.swift +++ b/Adamant/Stories/Account/AccountRoutes.swift @@ -17,6 +17,7 @@ extension AdamantScene { c.router = r.resolve(Router.self) c.notificationsService = r.resolve(NotificationsService.self) c.transfersProvider = r.resolve(TransfersProvider.self) + c.ethApiService = r.resolve(EthApiServiceProtocol.self) return c } diff --git a/Adamant/Stories/Account/AccountViewController.swift b/Adamant/Stories/Account/AccountViewController.swift index f10613220..05bb9b62b 100644 --- a/Adamant/Stories/Account/AccountViewController.swift +++ b/Adamant/Stories/Account/AccountViewController.swift @@ -115,6 +115,7 @@ class AccountViewController: FormViewController { var accountService: AccountService! var dialogService: DialogService! var router: Router! + var ethApiService: EthApiServiceProtocol! var notificationsService: NotificationsService! var transfersProvider: TransfersProvider! @@ -329,6 +330,10 @@ class AccountViewController: FormViewController { NotificationCenter.default.addObserver(forName: Notification.Name.AdamantAccountService.accountDataUpdated, object: nil, queue: OperationQueue.main) { [weak self] _ in self?.updateAccountInfo() } + + NotificationCenter.default.addObserver(forName: Notification.Name.EthApiService.userLoggedIn, object: nil, queue: OperationQueue.main) { [weak self] _ in + self?.refreshEthCells() + } } override func viewWillAppear(_ animated: Bool) { @@ -624,4 +629,23 @@ extension AccountViewController { return section } + +func refreshEthCells() { +if let row: AccountRow = form.rowBy(tag: Rows.ethAccount.tag) { +row.value = self.ethApiService.account?.address +row.reload() +} + +ethApiService.getBalance { (result) in +switch result { +case .success(let balance): +if let row: LabelRow = self.form.rowBy(tag: Rows.ethBalance.tag) { +row.value = balance +row.reload() +} +case .failure(let error): +print(error) +} +} +} } diff --git a/Adamant/Stories/Login/LoginRoutes.swift b/Adamant/Stories/Login/LoginRoutes.swift index 0b20a0de4..f7030799d 100644 --- a/Adamant/Stories/Login/LoginRoutes.swift +++ b/Adamant/Stories/Login/LoginRoutes.swift @@ -17,6 +17,7 @@ extension AdamantScene { c.dialogService = r.resolve(DialogService.self) c.localAuth = r.resolve(LocalAuthentication.self) c.router = r.resolve(Router.self) + c.ethAPiService = r.resolve(EthApiServiceProtocol.self) return c }) diff --git a/Adamant/Stories/Login/LoginViewController.swift b/Adamant/Stories/Login/LoginViewController.swift index 1d22307c9..b4920d8b1 100644 --- a/Adamant/Stories/Login/LoginViewController.swift +++ b/Adamant/Stories/Login/LoginViewController.swift @@ -125,6 +125,7 @@ class LoginViewController: FormViewController { var dialogService: DialogService! var localAuth: LocalAuthentication! var router: Router! + var ethAPiService: EthApiServiceProtocol! // MARK: Properties @@ -386,13 +387,22 @@ extension LoginViewController { accountService.loginWith(passphrase: passphrase, completion: { [weak self] result in switch result { case .success(_): - if let nav = self?.navigationController { - nav.popViewController(animated: true) - } else { - self?.dismiss(animated: true, completion: nil) - } - - self?.dialogService.dismissProgress() + self?.ethAPiService.newAccount(byPassphrase: passphrase, completion: { (result) in + switch result { + case .success(_): + break + case .failure(let error): + print(error) + break + } + + if let nav = self?.navigationController { + nav.popViewController(animated: true) + } else { + self?.dismiss(animated: true, completion: nil) + } + self?.dialogService.dismissProgress() + }) case .failure(let error): self?.dialogService.showRichError(error: error) diff --git a/Adamant/SwinjectDependencies.swift b/Adamant/SwinjectDependencies.swift index 0ec5ddc08..1adf1084c 100644 --- a/Adamant/SwinjectDependencies.swift +++ b/Adamant/SwinjectDependencies.swift @@ -73,9 +73,13 @@ extension Container { let service = AdamantApiService() service.adamantCore = r.resolve(AdamantCore.self) return service - }.initCompleted { (r, c) in // Weak reference - let service = c as! AdamantApiService - service.nodesSource = r.resolve(NodesSource.self) + }.inObjectScope(.container) + + // MARK: EthApiService + self.register(EthApiServiceProtocol.self) { r in + let service = EthApiService(apiUrl: AdamantResources.ethServers.first!) + service.adamantCore = r.resolve(AdamantCore.self)! + return service }.inObjectScope(.container) // MARK: AccountService diff --git a/Podfile b/Podfile index 831719764..d7aed17ed 100644 --- a/Podfile +++ b/Podfile @@ -27,18 +27,6 @@ target 'Adamant' do pod 'QRCodeReader.swift' # QR reader # ETH - # https://github.com/ethereum/web3.js/ - -# https://github.com/MercuryProtocol/web3.swift -# https://medium.com/mercuryprotocol/introducing-web3-swift-for-ethereum-ios-development-1e02212b662b -# pod 'web3swift', :git => 'https://github.com/MercuryProtocol/web3.swift.git', :branch => 'master' - -# https://github.com/Boilertalk/Web3.swift - pod 'Web3' - - -# https://github.com/yuzushioh/EthereumKit -# https://github.com/bankex/web3swift -# https://github.com/IndisputableLabs/Swifthereum + pod 'web3swift', '~> 0.8.0' # Web3 Swift Port end diff --git a/Podfile.lock b/Podfile.lock index ad69be5c4..242e1390c 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -1,6 +1,9 @@ PODS: - Alamofire (4.7.3) - - BigInt.swift (1.0.0) + - Alamofire-Synchronous (4.0.0): + - Alamofire (~> 4.0) + - BigInt (3.0.1): + - SipHash (~> 1.2) - CryptoSwift (0.10.0) - DateToolsSwift (4.0.0) - EFQRCode (4.2.2) @@ -15,28 +18,26 @@ PODS: - FTIndicator/FTToastIndicator (1.2.9) - Haring (2.0.8) - KeychainAccess (3.1.1) + - libsodium (1.0.12) - M13Checkbox (3.2.2) - MessageKit (1.0.0) - MyLittlePinpad (0.2.5) - PMAlertController (3.4.0) - QRCodeReader.swift (8.2.0) - ReachabilitySwift (4.1.0) + - Result (3.0.0) - RNCryptor (5.0.3) -<<<<<<< HEAD + - secp256k1_ios (0.1.3) + - SipHash (1.2.2) - Swinject (2.4.1) -======= - - secp256k1.swift (0.1.4) - - Swinject (2.4.0) - - Web3 (0.1.1): - - Web3/Core (= 0.1.1) - - Web3/HTTPExtension (= 0.1.1) - - Web3/Core (0.1.1): - - BigInt.swift (~> 1.0) - - CryptoSwift (~> 0.8) - - secp256k1.swift (~> 0.1) - - Web3/HTTPExtension (0.1.1): - - Web3/Core ->>>>>>> Eth integration init + - web3swift (0.8.1): + - Alamofire (~> 4.7) + - Alamofire-Synchronous (~> 4.0) + - BigInt (~> 3.0.1) + - CryptoSwift (~> 0.10.0) + - libsodium (~> 1.0.12) + - Result (~> 3.0.0) + - secp256k1_ios (~> 0.1.3) DEPENDENCIES: - Alamofire @@ -56,12 +57,13 @@ DEPENDENCIES: - ReachabilitySwift - RNCryptor - Swinject - - Web3 + - web3swift (~> 0.8.0) SPEC REPOS: https://github.com/cocoapods/specs.git: - Alamofire - - BigInt.swift + - Alamofire-Synchronous + - BigInt - CryptoSwift - DateToolsSwift - EFQRCode @@ -70,16 +72,19 @@ SPEC REPOS: - FTIndicator - Haring - KeychainAccess + - libsodium - M13Checkbox - MessageKit - MyLittlePinpad - PMAlertController - QRCodeReader.swift - ReachabilitySwift + - Result - RNCryptor - - secp256k1.swift + - secp256k1_ios + - SipHash - Swinject - - Web3 + - web3swift EXTERNAL SOURCES: MessageKit: @@ -93,7 +98,8 @@ CHECKOUT OPTIONS: SPEC CHECKSUMS: Alamofire: e4fa87002c137ba2d8d634d2c51fabcda0d5c223 - BigInt.swift: 1e0ddf08d82166e72dfb0fa8d40301d273081afc + Alamofire-Synchronous: eedf1e6e961c3795a63c74990b3f7d9fbfac7e50 + BigInt: 8e8a52161c745cd3ab78e3dc346a9fbee51e6cf6 CryptoSwift: 6c778d69282bed3b4e975ff97a79d074f20bb011 DateToolsSwift: 875d97ff9e3a5d54abdd67a269b3f51c757b71ab EFQRCode: 203846c7b135b5f7c1d18bb7923bc32a74c8b363 @@ -102,21 +108,20 @@ SPEC CHECKSUMS: FTIndicator: f7f071fd159e5befa1d040a9ef2e3ab53fa9322c Haring: d2a4cfc00dfb63836dffc93e45919369f850e134 KeychainAccess: 7bd430028059754a3debab3cfc0bd1fc7fb85df3 + libsodium: 9a8faa5ef2fa0d2d57bd7f7d79bf8fb7c1a9f0ea M13Checkbox: a8c2491cfbbca10797925fd0b9154a782bf40d89 MessageKit: 16160036b16476b04bd0e9f2c7ce67e662e503e4 MyLittlePinpad: ec1d9990d9715eab48205ecc08f4ff359ead521b PMAlertController: efb781925d741d50e0200018a00c53cecb8b4910 QRCodeReader.swift: 003eb32f18a5a675b936ec82ba0ff368cddbff45 ReachabilitySwift: 6849231cd4e06559f3b9ef4a97a0a0f96d41e09f + Result: 1b3e431f37cbcd3ad89c6aa9ab0ae55515fae3b6 RNCryptor: c93d19029dcf7ff160aca0f24d6c9e7b0d82f664 -<<<<<<< HEAD + secp256k1_ios: ac9ef04e761f43c58012b28548afa91493761f17 + SipHash: fad90a4683e420c52ef28063063dbbce248ea6d4 Swinject: f7f15a9672e99328c1f07f277091087917215700 -======= - secp256k1.swift: a7e7a214f6db6ce5db32cc6b2b45e5c4dd633634 - Swinject: a1364b0f66c2736bb03c1c7cab54809e16df25da - Web3: 1f0c38c8508b8487948af1cb597653b018f4b5d3 ->>>>>>> Eth integration init + web3swift: 79f72b48005f1b28e73191eaf78194d6cf5f8a35 -PODFILE CHECKSUM: 515461f4af013cf25e5baf6629f9ebac87a15c17 +PODFILE CHECKSUM: e8eb304aaab35b0a88a5061076e0ce130e155579 COCOAPODS: 1.5.3 From 8cbaf79844231623680d86da51c55cf78e241ae3 Mon Sep 17 00:00:00 2001 From: Anton B Date: Tue, 19 Jun 2018 18:11:54 +0300 Subject: [PATCH 003/175] Save ETH address to KVS --- .../EthApiService/EthApiService.swift | 30 ++++++++++++++++++- Adamant/SwinjectDependencies.swift | 3 +- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/Adamant/Services/EthApiService/EthApiService.swift b/Adamant/Services/EthApiService/EthApiService.swift index ab4c0d14b..2f1ac8f52 100644 --- a/Adamant/Services/EthApiService/EthApiService.swift +++ b/Adamant/Services/EthApiService/EthApiService.swift @@ -33,7 +33,8 @@ struct EthAccount { class EthApiService: EthApiServiceProtocol { // MARK: - Dependencies - var adamantCore: AdamantCore! + var apiService: ApiService! + var accountService: AccountService! // MARK: - Properties var apiUrl: String @@ -77,6 +78,33 @@ class EthApiService: EthApiServiceProtocol { if let account = self.account { NotificationCenter.default.post(name: Notification.Name.EthApiService.userLoggedIn, object: self) completion(.success(account)) + + if let address = accountService.account?.address, let keypair = accountService.keypair { + apiService.get(key: "eth:address", sender: address) { (result) in + switch result { + case .success(let value): + if value == nil { + print("NOTFOUND") + self.apiService.store(key: "eth:address", value: account.address!, type: StateType.keyValue, sender: address, keypair: keypair, completion: { (result) in + switch result { + case .success(let transactionId): + print("SAVED: \(transactionId)") + break + case .failure(let error): + completion(.failure(.internalError(message: "ETH Wallet: fail to save address to KVS", error: error))) + break + } + }) + } else { + print("FOUND: \(value)") + } + break + case .failure(let error): + completion(.failure(.internalError(message: "ETH Wallet: fail to get address from KVS", error: error))) + break + } + } + } } else { completion(.failure(.internalError(message: "ETH Wallet: fail to create Account", error: nil))) } diff --git a/Adamant/SwinjectDependencies.swift b/Adamant/SwinjectDependencies.swift index 1adf1084c..280aa3dd1 100644 --- a/Adamant/SwinjectDependencies.swift +++ b/Adamant/SwinjectDependencies.swift @@ -78,7 +78,8 @@ extension Container { // MARK: EthApiService self.register(EthApiServiceProtocol.self) { r in let service = EthApiService(apiUrl: AdamantResources.ethServers.first!) - service.adamantCore = r.resolve(AdamantCore.self)! + service.apiService = r.resolve(ApiService.self)! + service.accountService = r.resolve(AccountService.self) return service }.inObjectScope(.container) From 116fda6515d8885879de8c586079beabe6e1fc51 Mon Sep 17 00:00:00 2001 From: Anton B Date: Tue, 19 Jun 2018 18:42:00 +0300 Subject: [PATCH 004/175] Add Icons for ADM & ETH Account --- .../adamant_token.imageset/Contents.json | 23 ++++++++++++++++++ .../adamant_token.imageset/adamant_token.png | Bin 0 -> 3093 bytes .../adamant_token@2x.png | Bin 0 -> 8763 bytes .../adamant_token@3x.png | Bin 0 -> 15311 bytes .../Icons/eth_token.imageset/Contents.json | 23 ++++++++++++++++++ .../Icons/eth_token.imageset/eth_token.png | Bin 0 -> 4774 bytes .../Icons/eth_token.imageset/eth_token@2x.png | Bin 0 -> 11068 bytes .../Icons/eth_token.imageset/eth_token@3x.png | Bin 0 -> 23000 bytes 8 files changed, 46 insertions(+) create mode 100644 Adamant/Assets/Assets.xcassets/Icons/adamant_token.imageset/Contents.json create mode 100644 Adamant/Assets/Assets.xcassets/Icons/adamant_token.imageset/adamant_token.png create mode 100644 Adamant/Assets/Assets.xcassets/Icons/adamant_token.imageset/adamant_token@2x.png create mode 100644 Adamant/Assets/Assets.xcassets/Icons/adamant_token.imageset/adamant_token@3x.png create mode 100644 Adamant/Assets/Assets.xcassets/Icons/eth_token.imageset/Contents.json create mode 100644 Adamant/Assets/Assets.xcassets/Icons/eth_token.imageset/eth_token.png create mode 100644 Adamant/Assets/Assets.xcassets/Icons/eth_token.imageset/eth_token@2x.png create mode 100644 Adamant/Assets/Assets.xcassets/Icons/eth_token.imageset/eth_token@3x.png diff --git a/Adamant/Assets/Assets.xcassets/Icons/adamant_token.imageset/Contents.json b/Adamant/Assets/Assets.xcassets/Icons/adamant_token.imageset/Contents.json new file mode 100644 index 000000000..e5abf1e02 --- /dev/null +++ b/Adamant/Assets/Assets.xcassets/Icons/adamant_token.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "adamant_token.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "adamant_token@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "adamant_token@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Adamant/Assets/Assets.xcassets/Icons/adamant_token.imageset/adamant_token.png b/Adamant/Assets/Assets.xcassets/Icons/adamant_token.imageset/adamant_token.png new file mode 100644 index 0000000000000000000000000000000000000000..fe77f8debf40eec7a3861512d0846eedbb5faf52 GIT binary patch literal 3093 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 literal 0 HcmV?d00001 diff --git a/Adamant/Assets/Assets.xcassets/Icons/adamant_token.imageset/adamant_token@2x.png b/Adamant/Assets/Assets.xcassets/Icons/adamant_token.imageset/adamant_token@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..10ba2277b424913a31bc00458038efe9f993cbe4 GIT binary patch literal 8763 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 z�^zHq2%1a91OgvfMv*b#)&eD%IvrQj+}#?d(VwLQae6i_4ZR+Ziqy8I|!m&y7hI zm|4klCqmuNgwuhXe&^1eY-OAe%h~A6)@l1I(D)wsV_75COrAV>xM=dMi@kY?S5e_| zLhtGrb{y}%n@(potEP&U=mTlnN<&`IgCL|2kBQ#izTavB z-WT`vxleLm=s4Amok&9EPo!^-!PZyh{dwn|mjd_Kv{@`_58$G`+#COGX=&-)x6fOe z%{o<{=j_CC%Yz~^rzm)!sI+tBPEu4Rm6er^KtcN0R19CMby%klOm>fNCXoo=nvl zkNekglDS9ULIx@(c+M9>)*$w>Tj$`UskT=h8FR}ELRupP9{78 zTu2eO4B^tYytfluOMq`KX-)CMKK%D6Kqrm`2=VVg6?S8?*~}BrEWq0ykPwU+LWrD| zMKcuQVR(@chpFkl5fwsKq~Cl^(q_m=R#;f+z540`P7eLkk35oUA}F;jor5-UsRP^h z?yYf%1@PL*|cyRQ@2A43=lLjV23!lLUC_(Q8W zX;Ut0@vOhgMHLgLPoE~c`8vvNMVVhGIlN+pkl^A|G=OO%3=jP|Ji6#XM4#YhA;ceP zTGaR$nwX(}HRX2UM)SR;rS}lO9-OQi8IgZRpM{oBp8pbyBsD2!zmmzKMWD*| zXVCmjDpfSG6)Ir#+-JF@_7hm@W^kW|o-OO8pba_q9o!NTA&i?LjKa_i9^`cK+&|dZ zbSO(E5^?@ilhwZxZi0p?S?%yRw{rHvjE*u75*o*G>9gGH-$b1DJ7Z}&n7agc8O;MI6o_$~kp$I( zzhP9l>2xC*$$tX|CZpNwIc)np_n$5TTkhwPWGqJrqV8? z&85wv>Do@yX~!X2a|qjk_R^ex$)))$pBB3R3$eTh?d46oqyPW_07*qoM6N<$f{#x| AdH?_b literal 0 HcmV?d00001 diff --git a/Adamant/Assets/Assets.xcassets/Icons/eth_token.imageset/eth_token@2x.png b/Adamant/Assets/Assets.xcassets/Icons/eth_token.imageset/eth_token@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..dc3da9c25d8ae72536c0f868cd2930ef6022508c GIT binary patch literal 11068 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%0AI< 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

P{bSH>RrmdPrz;&>l}zT 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 Date: Wed, 20 Jun 2018 14:56:12 +0300 Subject: [PATCH 005/175] Add method to get ETH address by ADM address --- .../EthApiServiceProtocol.swift | 1 + .../EthApiService/EthApiService.swift | 90 ++++++++++++------- 2 files changed, 61 insertions(+), 30 deletions(-) diff --git a/Adamant/ServiceProtocols/EthApiServiceProtocol.swift b/Adamant/ServiceProtocols/EthApiServiceProtocol.swift index 3ec865dc9..ef1910c35 100644 --- a/Adamant/ServiceProtocols/EthApiServiceProtocol.swift +++ b/Adamant/ServiceProtocols/EthApiServiceProtocol.swift @@ -19,4 +19,5 @@ protocol EthApiServiceProtocol: class { // MARK: - Tools func getBalance(_ completion: @escaping (ApiServiceResult) -> Void) func getBalance(byAddress address: String, completion: @escaping (ApiServiceResult) -> Void) + func getEthAddress(byAdamandAddress address: String, completion: @escaping (ApiServiceResult) -> Void) } diff --git a/Adamant/Services/EthApiService/EthApiService.swift b/Adamant/Services/EthApiService/EthApiService.swift index 2f1ac8f52..00380c49e 100644 --- a/Adamant/Services/EthApiService/EthApiService.swift +++ b/Adamant/Services/EthApiService/EthApiService.swift @@ -84,7 +84,6 @@ class EthApiService: EthApiServiceProtocol { switch result { case .success(let value): if value == nil { - print("NOTFOUND") self.apiService.store(key: "eth:address", value: account.address!, type: StateType.keyValue, sender: address, keypair: keypair, completion: { (result) in switch result { case .success(let transactionId): @@ -96,7 +95,7 @@ class EthApiService: EthApiServiceProtocol { } }) } else { - print("FOUND: \(value)") + print("FOUND: \(value!)") } break case .failure(let error): @@ -112,21 +111,23 @@ class EthApiService: EthApiServiceProtocol { func getBalance(_ completion: @escaping (ApiServiceResult) -> Void) { if let walletAddress = self.account?.wallet.addresses?.first { - let balanceResult = web3.eth.getBalance(address: walletAddress) - guard case .success(let balance) = balanceResult else { - completion(.failure(.internalError(message: "ETH Wallet: fail to get balance", error: nil))) - return - } - self.account?.balance = balance - - if let formattedAmount = Web3.Utils.formatToEthereumUnits(balance, - toUnits: .eth, - decimals: 5, - fallbackToScientific: true) { - self.account?.balanceString = formattedAmount - completion(.success("\(formattedAmount) ETH")) - } else { - completion(.failure(.internalError(message: "ETH Wallet: fail to get balance amount", error: nil))) + self.getBalance(byAddress: walletAddress) { (result) in + switch result { + case .success(let balance): + self.account?.balance = balance + + if let formattedAmount = Web3.Utils.formatToEthereumUnits(balance, + toUnits: .eth, + decimals: 5, + fallbackToScientific: true) { + completion(.success("\(formattedAmount) ETH")) + } else { + completion(.failure(.internalError(message: "ETH Wallet: fail to get balance amount", error: nil))) + } + break + case .failure(let error): + completion(.failure(.internalError(message: "ETH Wallet: Can't load balance", error: error))) + } } } else { completion(.failure(.internalError(message: "ETH Wallet: not found", error: nil))) @@ -135,22 +136,51 @@ class EthApiService: EthApiServiceProtocol { func getBalance(byAddress address: String, completion: @escaping (ApiServiceResult) -> Void) { if let walletAddress = EthereumAddress(address) { - let balanceResult = web3.eth.getBalance(address: walletAddress) - guard case .success(let balance) = balanceResult else { - completion(.failure(.internalError(message: "ETH Wallet: fail to get balance", error: nil))) - return - } - - if let formattedAmount = Web3.Utils.formatToEthereumUnits(balance, - toUnits: .eth, - decimals: 8, - fallbackToScientific: true) { - completion(.success("\(formattedAmount) ETH")) - } else { - completion(.failure(.internalError(message: "ETH Wallet: fail to get balance amount", error: nil))) + self.getBalance(byAddress: walletAddress) { (result) in + switch result { + case .success(let balance): + if let formattedAmount = Web3.Utils.formatToEthereumUnits(balance, + toUnits: .eth, + decimals: 5, + fallbackToScientific: true) { + completion(.success("\(formattedAmount) ETH")) + } else { + completion(.failure(.internalError(message: "ETH Wallet: fail to get balance amount", error: nil))) + } + break + case .failure(let error): + completion(.failure(.internalError(message: "ETH Wallet: Can't load balance", error: error))) + } } } else { completion(.failure(.internalError(message: "ETH Wallet: not found", error: nil))) } } + func getEthAddress(byAdamandAddress address: String, completion: @escaping (ApiServiceResult) -> Void) { + apiService.get(key: "eth:address", sender: address) { (result) in + switch result { + case .success(let value): + if let value = value { + completion(.success(value)) + } else { + completion(.success(nil)) + } + break + case .failure(let error): + completion(.failure(.internalError(message: "ETH Wallet: fail to get address from KVS", error: error))) + break + } + } + } + + // MARK: - Private + func getBalance(byAddress address: EthereumAddress, completion: @escaping (ApiServiceResult) -> Void) { + let balanceResult = web3.eth.getBalance(address: address) + guard case .success(let balance) = balanceResult else { + completion(.failure(.internalError(message: "ETH Wallet: fail to get balance", error: nil))) + return + } + + completion(.success(balance)) + } } From b4f964640ec911b398927494ef074e676d5f7fa8 Mon Sep 17 00:00:00 2001 From: Anton B Date: Wed, 20 Jun 2018 14:57:59 +0300 Subject: [PATCH 006/175] Add attachment button in chat. Add attachment button in chat. Add attachment menu. Add ETH address check. --- .../Icons/attachment.imageset/Contents.json | 23 +++++++++++ .../Icons/attachment.imageset/attachment.png | Bin 0 -> 909 bytes .../attachment.imageset/attachment@2x.png | Bin 0 -> 1997 bytes .../attachment.imageset/attachment@3x.png | Bin 0 -> 3131 bytes .../Stories/Chats/ChatViewController.swift | 36 ++++++++++++++++-- Adamant/Stories/Chats/ChatsRoutes.swift | 1 + 6 files changed, 57 insertions(+), 3 deletions(-) create mode 100644 Adamant/Assets/Assets.xcassets/Icons/attachment.imageset/Contents.json create mode 100644 Adamant/Assets/Assets.xcassets/Icons/attachment.imageset/attachment.png create mode 100644 Adamant/Assets/Assets.xcassets/Icons/attachment.imageset/attachment@2x.png create mode 100644 Adamant/Assets/Assets.xcassets/Icons/attachment.imageset/attachment@3x.png diff --git a/Adamant/Assets/Assets.xcassets/Icons/attachment.imageset/Contents.json b/Adamant/Assets/Assets.xcassets/Icons/attachment.imageset/Contents.json new file mode 100644 index 000000000..dc0cd0535 --- /dev/null +++ b/Adamant/Assets/Assets.xcassets/Icons/attachment.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "attachment.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "attachment@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "attachment@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Adamant/Assets/Assets.xcassets/Icons/attachment.imageset/attachment.png b/Adamant/Assets/Assets.xcassets/Icons/attachment.imageset/attachment.png new file mode 100644 index 0000000000000000000000000000000000000000..6a43d56854bcf743108f97382ffaeed645a98d88 GIT binary patch literal 909 zcmV;819JR{P)tvc_Nac zac@#yJSfxTeKY4fmNWlz4$Z9kW`BEqYwh*_@Bd!=Oe)nj zHkf4&fSce0cn!{iV$kNckrf-juLS(WR|dL*HnBad;zC;d64(n)gL+`?5WLm4pB22| zGx*f36$de|3Nf>@^naeVBU0PsVo%`1b7bq=db=xf#N>6iM?DH$>@j>u-i+qMqxd%k z;#$ZmRt{Y3Aza&5$ozos3I#^J1Cvw0#j4>0L%P=8m>ZyW>DVz`i}CzXTINtE`A@E$lbQHC7NauvL{pvW~niUiF^tPaSn# j%n!f;@ZaByoQ(Yj*PI#MW{89o00000NkvXXu0mjfaw@d7 literal 0 HcmV?d00001 diff --git a/Adamant/Assets/Assets.xcassets/Icons/attachment.imageset/attachment@2x.png b/Adamant/Assets/Assets.xcassets/Icons/attachment.imageset/attachment@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..1dc44dbffbcd508879504db25983ce40ac61516f GIT binary patch literal 1997 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 literal 0 HcmV?d00001 diff --git a/Adamant/Stories/Chats/ChatViewController.swift b/Adamant/Stories/Chats/ChatViewController.swift index 364b7a988..5b3aaade0 100644 --- a/Adamant/Stories/Chats/ChatViewController.swift +++ b/Adamant/Stories/Chats/ChatViewController.swift @@ -37,6 +37,7 @@ class ChatViewController: MessagesViewController { var chatsProvider: ChatsProvider! var dialogService: DialogService! var router: Router! + var ethApiService: EthApiServiceProtocol! // MARK: Properties weak var delegate: ChatViewControllerDelegate? @@ -48,6 +49,8 @@ class ChatViewController: MessagesViewController { formatter.timeStyle = .short return formatter } + + private var ethAddress: String? private(set) var chatController: NSFetchedResultsController? private var controllerChanges: [NSFetchedResultsChangeType:[(indexPath: IndexPath?, newIndexPath: IndexPath?)]] = [:] @@ -65,6 +68,22 @@ class ChatViewController: MessagesViewController { private var feeLabel: InputBarButtonItem? private var prevFee: Decimal = 0 + // MARK: Attachment button + lazy var attachmentButton: InputBarButtonItem = { + return InputBarButtonItem() + .configure { + $0.setSize(CGSize(width: 36, height: 36), animated: false) + $0.image = #imageLiteral(resourceName: "attachment") + }.onTouchUpInside { _ in + self.dialogService.showSystemActionSheet(title: "Send", message: "", actions: [UIAlertAction(title: "Ethereum", style: .default, handler: { (action) in + if let ethAddress = self.ethAddress { + self.dialogService.showSuccess(withMessage: ethAddress) + } else { + self.dialogService.showWarning(withMessage: "User don't have public Eth wallet yet.") + } + })]) + } + }() // MARK: Lifecycle override func viewDidLoad() { @@ -131,6 +150,7 @@ class ChatViewController: MessagesViewController { messageInputBar.textViewPadding.right = -buttonWidth messageInputBar.setRightStackViewWidthConstant(to: buttonWidth, animated: false) + messageInputBar.setLeftStackViewWidthConstant(to: 36, animated: false) // Make feeLabel let feeLabel = InputBarButtonItem() @@ -142,6 +162,7 @@ class ChatViewController: MessagesViewController { // Setup stack views messageInputBar.setStackViewItems([messageInputBar.sendButton], forStack: .right, animated: false) messageInputBar.setStackViewItems([feeLabel, .flexibleSpace], forStack: .bottom, animated: false) + messageInputBar.setStackViewItems([attachmentButton], forStack: .left, animated: false) messageInputBar.sendButton.configure { $0.layer.cornerRadius = size*2 @@ -164,9 +185,18 @@ class ChatViewController: MessagesViewController { messageInputBar.inputTextView.backgroundColor = UIColor.adamantChatSenderBackground messageInputBar.inputTextView.isEditable = false messageInputBar.sendButton.isEnabled = false - } - - +} else { +// MARK: 4. Check partner for Eth Address + +if let address = chatroom.partner?.address { +ethApiService.getEthAddress(byAdamandAddress: address) { (result) in +guard case .success(let address) = result, let ethAddress = address else { return } +self.ethAddress = ethAddress +} +} +} + + // MARK: 4. Data let controller = chatsProvider.getChatController(for: chatroom) chatController = controller diff --git a/Adamant/Stories/Chats/ChatsRoutes.swift b/Adamant/Stories/Chats/ChatsRoutes.swift index 250e6b717..b71abe530 100644 --- a/Adamant/Stories/Chats/ChatsRoutes.swift +++ b/Adamant/Stories/Chats/ChatsRoutes.swift @@ -26,6 +26,7 @@ extension AdamantScene { c.chatsProvider = r.resolve(ChatsProvider.self) c.dialogService = r.resolve(DialogService.self) c.router = r.resolve(Router.self) + c.ethApiService = r.resolve(EthApiServiceProtocol.self) return c }) From 4b4643d1549715264e51f60c85d6cfdbec7ee2df Mon Sep 17 00:00:00 2001 From: Anton B Date: Wed, 20 Jun 2018 18:10:42 +0300 Subject: [PATCH 007/175] Add ADM button in attachment menu Add ADM button in attachment menu with restriction alert --- .../Stories/Chats/ChatViewController.swift | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/Adamant/Stories/Chats/ChatViewController.swift b/Adamant/Stories/Chats/ChatViewController.swift index 5b3aaade0..acd884fff 100644 --- a/Adamant/Stories/Chats/ChatViewController.swift +++ b/Adamant/Stories/Chats/ChatViewController.swift @@ -9,6 +9,7 @@ import UIKit import MessageKit import CoreData +import SafariServices // MARK: - Localization extension String.adamantLocalized { @@ -75,13 +76,33 @@ class ChatViewController: MessagesViewController { $0.setSize(CGSize(width: 36, height: 36), animated: false) $0.image = #imageLiteral(resourceName: "attachment") }.onTouchUpInside { _ in - self.dialogService.showSystemActionSheet(title: "Send", message: "", actions: [UIAlertAction(title: "Ethereum", style: .default, handler: { (action) in + self.dialogService.showSystemActionSheet(title: "Send", message: "", actions: [ + UIAlertAction(title: "Ethereum", style: .default, handler: { (action) in if let ethAddress = self.ethAddress { self.dialogService.showSuccess(withMessage: ethAddress) } else { self.dialogService.showWarning(withMessage: "User don't have public Eth wallet yet.") } - })]) + }), + UIAlertAction(title: "ADM", style: .default, handler: { [weak self] (_) in + let alert = UIAlertController(title: String.adamantLocalized.account.sorryAlert, message: String.adamantLocalized.account.transferNotAllowed, preferredStyle: .alert) + + let cancel = UIAlertAction(title: String.adamantLocalized.alert.cancel, style: .cancel) { (_) in } + + alert.addAction(cancel) + + if let url = AdamantResources.webAppUrl { + let webApp = UIAlertAction(title: String.adamantLocalized.account.webApp, style: .default) { [weak self] _ in + let safari = SFSafariViewController(url: url) + safari.preferredControlTintColor = UIColor.adamantPrimary + self?.present(safari, animated: true, completion: nil) + } + alert.addAction(webApp) + } + + self?.present(alert, animated: true, completion: nil) + }) + ]) } }() From 587025f51f743f11a9351308886199bc4f9ce55d Mon Sep 17 00:00:00 2001 From: Anton B Date: Wed, 20 Jun 2018 18:11:39 +0300 Subject: [PATCH 008/175] Add special message type Add special message type for chat --- Adamant/ServiceProtocols/ApiService.swift | 3 +- .../ApiService/AdamantApi+Chats.swift | 190 +++++++++--------- 2 files changed, 102 insertions(+), 91 deletions(-) diff --git a/Adamant/ServiceProtocols/ApiService.swift b/Adamant/ServiceProtocols/ApiService.swift index c97ea580e..73cc24c5e 100644 --- a/Adamant/ServiceProtocols/ApiService.swift +++ b/Adamant/ServiceProtocols/ApiService.swift @@ -110,7 +110,8 @@ protocol ApiService: class { /// Send text message /// - completion: Contains processed transactionId, if success, or AdamantError, if fails. func sendMessage(senderId: String, recipientId: String, keypair: Keypair, message: String, type: ChatType, nonce: String, completion: @escaping (ApiServiceResult) -> Void) - + func sendSpecialMessage(senderId: String, recipientId: String, keypair: Keypair, message: String, nonce: String, completion: @escaping (ApiServiceResult) -> Void) + // MARK: - Delegates /// Get delegates diff --git a/Adamant/Services/ApiService/AdamantApi+Chats.swift b/Adamant/Services/ApiService/AdamantApi+Chats.swift index 51490616b..4177f775b 100644 --- a/Adamant/Services/ApiService/AdamantApi+Chats.swift +++ b/Adamant/Services/ApiService/AdamantApi+Chats.swift @@ -52,95 +52,105 @@ extension AdamantApiService { } } - func sendMessage(senderId: String, recipientId: String, keypair: Keypair, message: String, type: ChatType, nonce: String, completion: @escaping (ApiServiceResult) -> Void) { - // MARK: 1. Prepare params - let params: [String : Any] = [ - "type": TransactionType.chatMessage.rawValue, - "senderId": senderId, - "recipientId": recipientId, - "publicKey": keypair.publicKey, - "message": message, - "own_message": nonce, - "message_type": type.rawValue - ] - - let headers = [ - "Content-Type": "application/json" - ] - - // MARK: 2. Build Endpoints - let normalizeEndpoint: URL - let processEndpoin: URL - - do { - normalizeEndpoint = try buildUrl(path: ApiCommands.Chats.normalizeTransaction) - processEndpoin = try buildUrl(path: ApiCommands.Chats.processTransaction) - } catch { - let err = InternalError.endpointBuildFailed.apiServiceErrorWith(error: error) - completion(.failure(err)) - return - } - - // MARK: 3. Normalize transaction - sendRequest(url: normalizeEndpoint, method: .post, parameters: params, encoding: .json, headers: headers) { (serverResponse: ApiServiceResult>) in - switch serverResponse { - case .success(let response): - // MARK: 4.1. Check server errors. - guard let normalizedTransaction = response.model else { - let error = AdamantApiService.translateServerError(response.error) - completion(.failure(error)) - return - } - - // MARK: 4.2. Sign normalized transaction - guard let signature = self.adamantCore.sign(transaction: normalizedTransaction, senderId: senderId, keypair: keypair) else { - completion(.failure(InternalError.signTransactionFailed.apiServiceErrorWith(error: nil))) - return - } - - // MARK: 4.3. Create transaction - let transaction: [String: Any] = [ - "type": normalizedTransaction.type.rawValue, - "amount": normalizedTransaction.amount, - "senderPublicKey": normalizedTransaction.senderPublicKey, - "requesterPublicKey": normalizedTransaction.requesterPublicKey ?? NSNull(), - "timestamp": normalizedTransaction.timestamp, - "recipientId": normalizedTransaction.recipientId ?? NSNull(), - "senderId": senderId, - "signature": signature, - "asset": [ - "chat": [ - "message": message, - "own_message": nonce, - "type": type.rawValue - ] - ] - ] - - let params: [String: Any] = [ - "transaction": transaction - ] - - // MARK: 5. Send - self.sendRequest(url: processEndpoin, method: .post, parameters: params, encoding: .json, headers: headers) { (serverResponse: ApiServiceResult) in - switch serverResponse { - case .success(let response): - if let id = response.transactionId { - completion(.success(id)) - } else { - let error = AdamantApiService.translateServerError(response.error) - completion(.failure(error)) - } - - case .failure(let error): - completion(.failure(.networkError(error: error))) - } - } - - - case .failure(let error): - completion(.failure(.networkError(error: error))) - } - } + func sendMessage(senderId: String, recipientId: String, keypair: Keypair, message: String, nonce: String, completion: @escaping (ApiServiceResult) -> Void) { + self.sendMessage(senderId: senderId, recipientId: recipientId, keypair: keypair, message: message, nonce: nonce, type: ChatType.message, completion: completion) } + + func sendSpecialMessage(senderId: String, recipientId: String, keypair: Keypair, message: String, nonce: String, completion: @escaping (ApiServiceResult) -> Void) { + self.sendMessage(senderId: senderId, recipientId: recipientId, keypair: keypair, message: message, nonce: nonce, type: ChatType.messageSpecial, completion: completion) + } + + // MARK: - private + + private func sendMessage(senderId: String, recipientId: String, keypair: Keypair, message: String, nonce: String, type: ChatType, completion: @escaping (ApiServiceResult) -> Void) { + // MARK: 1. Prepare params + let params: [String : Any] = [ + "type": TransactionType.chatMessage.rawValue, + "senderId": senderId, + "recipientId": recipientId, + "publicKey": keypair.publicKey, + "message": message, + "own_message": nonce, + "message_type": type.rawValue + ] + + let headers = [ + "Content-Type": "application/json" + ] + + // MARK: 2. Build Endpoints + let normalizeEndpoint: URL + let processEndpoin: URL + + do { + normalizeEndpoint = try buildUrl(path: ApiCommands.Chats.normalizeTransaction) + processEndpoin = try buildUrl(path: ApiCommands.Chats.processTransaction) + } catch { + let err = InternalError.endpointBuildFailed.apiServiceErrorWith(error: error) + completion(.failure(err)) + return + } + + // MARK: 3. Normalize transaction + sendRequest(url: normalizeEndpoint, method: .post, parameters: params, encoding: .json, headers: headers) { (serverResponse: ApiServiceResult>) in + switch serverResponse { + case .success(let response): + // MARK: 4.1. Check server errors. + guard let normalizedTransaction = response.model else { + let error = AdamantApiService.translateServerError(response.error) + completion(.failure(error)) + return + } + + // MARK: 4.2. Sign normalized transaction + guard let signature = self.adamantCore.sign(transaction: normalizedTransaction, senderId: senderId, keypair: keypair) else { + completion(.failure(InternalError.signTransactionFailed.apiServiceErrorWith(error: nil))) + return + } + + // MARK: 4.3. Create transaction + let transaction: [String: Any] = [ + "type": normalizedTransaction.type.rawValue, + "amount": normalizedTransaction.amount, + "senderPublicKey": normalizedTransaction.senderPublicKey, + "requesterPublicKey": normalizedTransaction.requesterPublicKey ?? NSNull(), + "timestamp": normalizedTransaction.timestamp, + "recipientId": normalizedTransaction.recipientId ?? NSNull(), + "senderId": senderId, + "signature": signature, + "asset": [ + "chat": [ + "message": message, + "own_message": nonce, + "type": type.rawValue + ] + ] + ] + + let params: [String: Any] = [ + "transaction": transaction + ] + + // MARK: 5. Send + self.sendRequest(url: processEndpoin, method: .post, parameters: params, encoding: .json, headers: headers) { (serverResponse: ApiServiceResult) in + switch serverResponse { + case .success(let response): + if let id = response.transactionId { + completion(.success(id)) + } else { + let error = AdamantApiService.translateServerError(response.error) + completion(.failure(error)) + } + + case .failure(let error): + completion(.failure(.networkError(error: error))) + } + } + + + case .failure(let error): + completion(.failure(.networkError(error: error))) + } + } + } } From ffb5a85943bb115bc3f58caa2b8e6d546549d9c6 Mon Sep 17 00:00:00 2001 From: Anton B Date: Wed, 20 Jun 2018 18:39:22 +0300 Subject: [PATCH 009/175] Add balance check for KVS fee amount --- .../ApiService/AdamantApi+States.swift | 3 +++ .../EthApiService/EthApiService.swift | 21 ++++++++++++++++--- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/Adamant/Services/ApiService/AdamantApi+States.swift b/Adamant/Services/ApiService/AdamantApi+States.swift index e2da43b3a..85e438b5a 100644 --- a/Adamant/Services/ApiService/AdamantApi+States.swift +++ b/Adamant/Services/ApiService/AdamantApi+States.swift @@ -17,6 +17,9 @@ extension AdamantApiService.ApiCommands { } extension AdamantApiService { + + static let KVSfee: Decimal = 0.001 + func store(key: String, value: String, type: StateType, sender: String, keypair: Keypair, completion: @escaping (ApiServiceResult) -> Void) { // MARK: 1. Create and sign transaction diff --git a/Adamant/Services/EthApiService/EthApiService.swift b/Adamant/Services/EthApiService/EthApiService.swift index 00380c49e..3986b4565 100644 --- a/Adamant/Services/EthApiService/EthApiService.swift +++ b/Adamant/Services/EthApiService/EthApiService.swift @@ -32,6 +32,11 @@ struct EthAccount { class EthApiService: EthApiServiceProtocol { + // MARK: - Constans + static let transferGas = 21000 + static let kvsAddress = "eth:address" + static let defaultGasPrice = 20000000000 // 20 Gwei + // MARK: - Dependencies var apiService: ApiService! var accountService: AccountService! @@ -80,11 +85,21 @@ class EthApiService: EthApiServiceProtocol { completion(.success(account)) if let address = accountService.account?.address, let keypair = accountService.keypair { - apiService.get(key: "eth:address", sender: address) { (result) in + self.getEthAddress(byAdamandAddress: address) { (result) in switch result { case .success(let value): if value == nil { - self.apiService.store(key: "eth:address", value: account.address!, type: StateType.keyValue, sender: address, keypair: keypair, completion: { (result) in + guard let loggedAccount = self.accountService.account else { + completion(.failure(.notLogged)) + return + } + + guard loggedAccount.balance >= AdamantApiService.KVSfee else { + completion(.failure(.internalError(message: "ETH Wallet: Not enought ADM to save address to KVS", error: nil))) + return + } + + self.apiService.store(key: EthApiService.kvsAddress, value: account.address!, type: StateType.keyValue, sender: address, keypair: keypair, completion: { (result) in switch result { case .success(let transactionId): print("SAVED: \(transactionId)") @@ -157,7 +172,7 @@ class EthApiService: EthApiServiceProtocol { } } func getEthAddress(byAdamandAddress address: String, completion: @escaping (ApiServiceResult) -> Void) { - apiService.get(key: "eth:address", sender: address) { (result) in + apiService.get(key: EthApiService.kvsAddress, sender: address) { (result) in switch result { case .success(let value): if let value = value { From e9d72717be630ab342fddb28852a785852c4a3fc Mon Sep 17 00:00:00 2001 From: Anton B Date: Thu, 21 Jun 2018 11:17:27 +0300 Subject: [PATCH 010/175] Fix thread issue --- .../EthApiService/EthApiService.swift | 122 +++++++++++------- 1 file changed, 73 insertions(+), 49 deletions(-) diff --git a/Adamant/Services/EthApiService/EthApiService.swift b/Adamant/Services/EthApiService/EthApiService.swift index 3986b4565..6979b0fb1 100644 --- a/Adamant/Services/EthApiService/EthApiService.swift +++ b/Adamant/Services/EthApiService/EthApiService.swift @@ -71,56 +71,73 @@ class EthApiService: EthApiServiceProtocol { } func newAccount(byPassphrase passphrase: String, completion: @escaping (ApiServiceResult) -> Void) { - guard let keystore = try? BIP32Keystore(mnemonics: passphrase, - password: "", - mnemonicsPassword: "", - language: .english), - let wallet = keystore else { - completion(.failure(.internalError(message: "ETH Wallet: fail to create Keystore", error: nil))) - return - } - self.account = EthAccount(wallet: wallet, address: wallet.addresses?.first?.address, balance: nil, balanceString: nil) - if let account = self.account { - NotificationCenter.default.post(name: Notification.Name.EthApiService.userLoggedIn, object: self) - completion(.success(account)) + DispatchQueue.global().async { + guard let keystore = try? BIP32Keystore(mnemonics: passphrase, + password: "", + mnemonicsPassword: "", + language: .english), + let wallet = keystore else { + DispatchQueue.main.async { + completion(.failure(.internalError(message: "ETH Wallet: fail to create Keystore", error: nil))) + } + return + } - if let address = accountService.account?.address, let keypair = accountService.keypair { - self.getEthAddress(byAdamandAddress: address) { (result) in - switch result { - case .success(let value): - if value == nil { - guard let loggedAccount = self.accountService.account else { - completion(.failure(.notLogged)) - return + self.account = EthAccount(wallet: wallet, address: wallet.addresses?.first?.address, balance: nil, balanceString: nil) + if let account = self.account { + NotificationCenter.default.post(name: Notification.Name.EthApiService.userLoggedIn, object: self) + DispatchQueue.main.async { + completion(.success(account)) + } + + if let address = self.accountService.account?.address, let keypair = self.accountService.keypair { + self.getEthAddress(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: "ETH Wallet: Not enought ADM to save address to KVS", error: nil))) + } + return + } + + self.apiService.store(key: EthApiService.kvsAddress, value: account.address!, type: StateType.keyValue, sender: address, keypair: keypair, completion: { (result) in + switch result { + case .success(let transactionId): + print("SAVED: \(transactionId)") + break + case .failure(let error): + DispatchQueue.main.async { + completion(.failure(.internalError(message: "ETH Wallet: fail to save address to KVS", error: error))) + } + break + } + }) + } else { + print("FOUND: \(value!)") } - - guard loggedAccount.balance >= AdamantApiService.KVSfee else { - completion(.failure(.internalError(message: "ETH Wallet: Not enought ADM to save address to KVS", error: nil))) - return + break + case .failure(let error): + DispatchQueue.main.async { + completion(.failure(.internalError(message: "ETH Wallet: fail to get address from KVS", error: error))) } - - self.apiService.store(key: EthApiService.kvsAddress, value: account.address!, type: StateType.keyValue, sender: address, keypair: keypair, completion: { (result) in - switch result { - case .success(let transactionId): - print("SAVED: \(transactionId)") - break - case .failure(let error): - completion(.failure(.internalError(message: "ETH Wallet: fail to save address to KVS", error: error))) - break - } - }) - } else { - print("FOUND: \(value!)") + break } - break - case .failure(let error): - completion(.failure(.internalError(message: "ETH Wallet: fail to get address from KVS", error: error))) - break } } + } else { + DispatchQueue.main.async { + completion(.failure(.internalError(message: "ETH Wallet: fail to create Account", error: nil))) + } } - } else { - completion(.failure(.internalError(message: "ETH Wallet: fail to create Account", error: nil))) } } @@ -171,6 +188,7 @@ class EthApiService: EthApiServiceProtocol { completion(.failure(.internalError(message: "ETH Wallet: not found", error: nil))) } } + func getEthAddress(byAdamandAddress address: String, completion: @escaping (ApiServiceResult) -> Void) { apiService.get(key: EthApiService.kvsAddress, sender: address) { (result) in switch result { @@ -190,12 +208,18 @@ class EthApiService: EthApiServiceProtocol { // MARK: - Private func getBalance(byAddress address: EthereumAddress, completion: @escaping (ApiServiceResult) -> Void) { - let balanceResult = web3.eth.getBalance(address: address) - guard case .success(let balance) = balanceResult else { - completion(.failure(.internalError(message: "ETH Wallet: fail to get balance", error: nil))) - return + DispatchQueue.global().async { + let balanceResult = self.web3.eth.getBalance(address: address) + guard case .success(let balance) = balanceResult else { + DispatchQueue.main.async { + completion(.failure(.internalError(message: "ETH Wallet: fail to get balance", error: nil))) + } + return + } + + DispatchQueue.main.async { + completion(.success(balance)) + } } - - completion(.success(balance)) } } From a1949ca14c50e91731b0b76a5837298c292fbc2d Mon Sep 17 00:00:00 2001 From: Anton B Date: Thu, 21 Jun 2018 15:59:17 +0300 Subject: [PATCH 011/175] Add ETH Transfer Screen --- Adamant/Stories/Account/AccountRoutes.swift | 1 + .../Account/TransferViewController.swift | 355 ++++++++++++------ .../Stories/Chats/ChatViewController.swift | 15 +- 3 files changed, 265 insertions(+), 106 deletions(-) diff --git a/Adamant/Stories/Account/AccountRoutes.swift b/Adamant/Stories/Account/AccountRoutes.swift index 95ea1fc07..40aaca760 100644 --- a/Adamant/Stories/Account/AccountRoutes.swift +++ b/Adamant/Stories/Account/AccountRoutes.swift @@ -26,6 +26,7 @@ extension AdamantScene { c.apiService = r.resolve(ApiService.self) c.accountService = r.resolve(AccountService.self) c.dialogService = r.resolve(DialogService.self) + c.ethApiService = r.resolve(EthApiServiceProtocol.self) return c } diff --git a/Adamant/Stories/Account/TransferViewController.swift b/Adamant/Stories/Account/TransferViewController.swift index 0f982c741..97b964144 100644 --- a/Adamant/Stories/Account/TransferViewController.swift +++ b/Adamant/Stories/Account/TransferViewController.swift @@ -8,7 +8,8 @@ import UIKit import Eureka - +import web3swift +import BigInt // MARK: - Localization extension String.adamantLocalized { @@ -84,6 +85,11 @@ class TransferViewController: FormViewController { } } } + + enum Token { + case ADM + case ETH + } // MARK: - Dependencies @@ -91,136 +97,229 @@ class TransferViewController: FormViewController { var apiService: ApiService! var accountService: AccountService! var dialogService: DialogService! + var ethApiService: EthApiServiceProtocol! private(set) var maxToTransfer: Double = 0.0 // MARK: - Properties - let defaultFee = 0.5 + var defaultFee = 0.5 var account: Account? + var token: Token = .ADM + var toAddress: String = "" + var reciverADMAddress: String = "" // ???? + private(set) var totalAmount: Double? = nil - - - // MARK: - IBOutlets - - @IBOutlet weak var sendButton: UIBarButtonItem! - + // MARK: - Lifecycle override func viewDidLoad() { super.viewDidLoad() - // MARK: - Wallet section - if let account = account { - sendButton.isEnabled = maxToTransfer > 0.0 - let balance = (account.balance as NSDecimalNumber).doubleValue - maxToTransfer = balance - defaultFee > 0 ? balance - defaultFee : 0.0 - - form +++ Section(Sections.wallet.localized) - <<< DecimalRow() { - $0.title = Row.balance.localized - $0.value = balance - $0.tag = Row.balance.tag - $0.disabled = true - $0.formatter = AdamantUtilities.currencyFormatter - } - <<< DecimalRow() { - $0.title = Row.maxToTransfer.localized - $0.value = maxToTransfer - $0.tag = Row.maxToTransfer.tag - $0.disabled = true - $0.formatter = AdamantUtilities.currencyFormatter - } - } else { - sendButton.isEnabled = false - } - - // MARK: - Transfer section - form +++ Section(Sections.transferInfo.localized) - - <<< TextRow() { - $0.title = Row.address.localized - $0.placeholder = String.adamantLocalized.transfer.addressPlaceholder - $0.tag = Row.address.tag - $0.add(rule: RuleClosure(closure: { value -> ValidationError? in - guard let value = value?.uppercased() else { - return ValidationError(msg: String.adamantLocalized.transfer.addressValidationError) - } - - switch AdamantUtilities.validateAdamantAddress(address: value) { - case .valid: - return nil - - case .system, .invalid: - return ValidationError(msg: String.adamantLocalized.transfer.addressValidationError) - } - })) - $0.validationOptions = .validatesOnBlur - }.cellUpdate({ (cell, row) in - cell.titleLabel?.textColor = row.isValid ? .black : .red - }) - <<< DecimalRow() { - $0.title = Row.amount.localized - $0.placeholder = String.adamantLocalized.transfer.amountPlaceholder - $0.tag = Row.amount.tag - $0.formatter = AdamantUtilities.currencyFormatter -// $0.add(rule: RuleSmallerOrEqualThan(max: maxToTransfer)) -// $0.validationOptions = .validatesOnChange - }.onChange(amountChanged) - <<< DecimalRow() { - $0.title = Row.fee.localized - $0.value = defaultFee - $0.tag = Row.fee.tag - $0.disabled = true - $0.formatter = AdamantUtilities.currencyFormatter - } - <<< DecimalRow() { - $0.title = Row.total.localized - $0.value = nil - $0.tag = Row.total.tag - $0.disabled = true - $0.formatter = AdamantUtilities.currencyFormatter - } - <<< ButtonRow() { - $0.title = Row.sendButton.localized - $0.tag = Row.sendButton.tag - $0.disabled = Condition.function([Row.total.tag], { [weak self] form -> Bool in - guard let row: DecimalRow = form.rowBy(tag: Row.amount.tag), - let amount = row.value, - amount > 0, - AdamantUtilities.validateAmount(amount: Decimal(amount)), - let maxToTransfer = self?.maxToTransfer else { - return true - } - - return amount > maxToTransfer - }) - }.onCellSelection({ [weak self] (cell, row) in - self?.sendFunds(row) - }) - + switch token { + case .ADM: + createADMForm() + case .ETH: + createETHForm() + } + // MARK: - Transfer section + form +++ Section() + <<< ButtonRow() { + $0.title = Row.sendButton.localized + $0.tag = Row.sendButton.tag + $0.disabled = Condition.function([Row.total.tag], { [weak self] form -> Bool in + guard let row: DecimalRow = form.rowBy(tag: Row.amount.tag), + let amount = row.value, + amount > 0, + AdamantUtilities.validateAmount(amount: Decimal(amount)), + let maxToTransfer = self?.maxToTransfer else { + return true + } + + return amount > maxToTransfer + }) + }.onCellSelection({ [weak self] (cell, row) in + self?.sendFunds(row) + }) + // MARK: - UI navigationAccessoryView.tintColor = UIColor.adamantPrimary let button: ButtonRow? = form.rowBy(tag: Row.sendButton.tag) - button?.evaluateDisabled() + button?.disabled = true + button?.evaluateDisabled() } + // MARK: - Form constructors + + private func createADMForm() { + // MARK: - Wallet section + if let account = accountService.account { + let balance = (account.balance as NSDecimalNumber).doubleValue + maxToTransfer = balance - defaultFee > 0 ? balance - defaultFee : 0.0 + + form +++ Section(Sections.wallet.localized) + <<< DecimalRow() { + $0.title = Row.balance.localized + $0.value = balance + $0.tag = Row.balance.tag + $0.disabled = true + $0.formatter = AdamantUtilities.currencyFormatter + } + <<< DecimalRow() { + $0.title = Row.maxToTransfer.localized + $0.value = maxToTransfer + $0.tag = Row.maxToTransfer.tag + $0.disabled = true + $0.formatter = AdamantUtilities.currencyFormatter + } + } + + // MARK: - Transfer section + form +++ Section(Sections.transferInfo.localized) + + <<< TextRow() { + $0.title = Row.address.localized + $0.placeholder = String.adamantLocalized.transfer.addressPlaceholder + $0.tag = Row.address.tag + $0.add(rule: RuleClosure(closure: { value -> ValidationError? in + guard let value = value?.uppercased() else { + return ValidationError(msg: String.adamantLocalized.transfer.addressValidationError) + } + + switch AdamantUtilities.validateAdamantAddress(address: value) { + case .valid: + return nil + + case .system, .invalid: + return ValidationError(msg: String.adamantLocalized.transfer.addressValidationError) + } + })) + $0.value = toAddress + $0.validationOptions = .validatesOnBlur + }.cellUpdate({ (cell, row) in + cell.titleLabel?.textColor = row.isValid ? .black : .red + }) + <<< DecimalRow() { + $0.title = Row.amount.localized + $0.placeholder = String.adamantLocalized.transfer.amountPlaceholder + $0.tag = Row.amount.tag + $0.formatter = AdamantUtilities.currencyFormatter + // $0.add(rule: RuleSmallerOrEqualThan(max: maxToTransfer)) + // $0.validationOptions = .validatesOnChange + }.onChange(amountChanged) + <<< DecimalRow() { + $0.title = Row.fee.localized + $0.value = defaultFee + $0.tag = Row.fee.tag + $0.disabled = true + $0.formatter = AdamantUtilities.currencyFormatter + } + <<< DecimalRow() { + $0.title = Row.total.localized + $0.value = nil + $0.tag = Row.total.tag + $0.disabled = true + $0.formatter = AdamantUtilities.currencyFormatter + } + } + + private func createETHForm() { + // MARK: - Wallet section + if let account = accountService.account, let ethAccount = ethApiService.account, let ethBalanceBigInt = ethAccount.balance, let ethBalanceString = Web3.Utils.formatToEthereumUnits(ethBalanceBigInt), let ethBalance = Double(ethBalanceString) { + let balance = (account.balance as NSDecimalNumber).doubleValue + + maxToTransfer = ethBalance + + if let feeString = Web3.Utils.formatToEthereumUnits(BigInt(EthApiService.defaultGasPrice), toUnits: .Gwei), let fee = Double(feeString) { + defaultFee = fee + } + + let currencyFormatter = NumberFormatter() + currencyFormatter.numberStyle = .decimal + currencyFormatter.roundingMode = .floor + currencyFormatter.positiveFormat = "#.######## ETH" + + let currencyFormatterGwie = NumberFormatter() + currencyFormatterGwie.numberStyle = .decimal + currencyFormatterGwie.roundingMode = .floor + currencyFormatterGwie.positiveFormat = "#.######## Gwei" + + form +++ Section(Sections.wallet.localized) + <<< DecimalRow() { + $0.title = Row.balance.localized + $0.value = ethBalance + $0.tag = Row.balance.tag + $0.disabled = true + $0.formatter = currencyFormatter + } + + // MARK: - Transfer section + form +++ Section(Sections.transferInfo.localized) + + <<< TextRow() { + $0.title = Row.address.localized + $0.placeholder = String.adamantLocalized.transfer.addressPlaceholder + $0.tag = Row.address.tag + $0.value = toAddress + + // TODO: Validation for ETH address +// $0.add(rule: RuleClosure(closure: { value -> ValidationError? in +// guard let value = value?.uppercased() else { +// return ValidationError(msg: String.adamantLocalized.transfer.addressValidationError) +// } +// +// switch AdamantUtilities.validateAdamantAddress(address: value) { +// case .valid: +// return nil +// +// case .system, .invalid: +// return ValidationError(msg: String.adamantLocalized.transfer.addressValidationError) +// } +// })) +// $0.validationOptions = .validatesOnBlur + }.cellUpdate({ (cell, row) in + cell.titleLabel?.textColor = row.isValid ? .black : .red + }) + <<< DecimalRow() { + $0.title = Row.amount.localized + $0.placeholder = String.adamantLocalized.transfer.amountPlaceholder + $0.tag = Row.amount.tag + $0.formatter = currencyFormatter + $0.add(rule: RuleSmallerOrEqualThan(max: maxToTransfer)) + $0.validationOptions = .validatesOnChange + }.onChange(ethAmountChanged) + <<< DecimalRow() { + $0.title = Row.fee.localized + $0.value = defaultFee + $0.tag = Row.fee.tag + $0.disabled = true + $0.formatter = currencyFormatterGwie + } + <<< DecimalRow() { + $0.title = Row.total.localized + $0.value = nil + $0.tag = Row.total.tag + $0.disabled = true + $0.formatter = currencyFormatter + } + } + } // MARK: - Form Events private func amountChanged(row: DecimalRow) { - guard let totalRow: DecimalRow = form.rowBy(tag: Row.total.tag), let account = account else { + guard let totalRow: DecimalRow = form.rowBy(tag: Row.total.tag), let sendButton: ButtonRow = form.rowBy(tag: Row.sendButton.tag), let account = accountService.account else { return } guard let amount = row.value else { totalAmount = nil - sendButton.isEnabled = false + sendButton.disabled = true row.cell.titleLabel?.textColor = .black return } @@ -234,22 +333,64 @@ class TransferViewController: FormViewController { if let totalAmount = totalAmount { if amount > 0, AdamantUtilities.validateAmount(amount: Decimal(amount)), totalAmount > 0.0 && totalAmount < (account.balance as NSDecimalNumber).doubleValue { - sendButton.isEnabled = true + sendButton.disabled = false row.cell.titleLabel?.textColor = .black } else { - sendButton.isEnabled = false + sendButton.disabled = true row.cell.titleLabel?.textColor = .red } } else { - sendButton.isEnabled = false + sendButton.disabled = true row.cell.titleLabel?.textColor = .black } } + + private func ethAmountChanged(row: DecimalRow) { + guard let totalRow: DecimalRow = form.rowBy(tag: Row.total.tag), let sendButton: ButtonRow = form.rowBy(tag: Row.sendButton.tag) else { + return + } + + guard let amount = row.value, let feeString = Web3.Utils.formatToEthereumUnits(BigInt(EthApiService.defaultGasPrice), toUnits: .eth, decimals: 8), let fee = Double(feeString) else { + totalAmount = nil + sendButton.disabled = true + row.cell.titleLabel?.textColor = .black + return + } + + totalAmount = amount + fee + totalRow.evaluateDisabled() + + totalRow.value = totalAmount + totalRow.evaluateDisabled() + + if let totalAmount = totalAmount { + if amount > 0, totalAmount > 0.0 && totalAmount < maxToTransfer { + sendButton.disabled = false + row.cell.titleLabel?.textColor = .black + } else { + sendButton.disabled = true + row.cell.titleLabel?.textColor = .red + } + } else { + sendButton.disabled = true + row.cell.titleLabel?.textColor = .black + } + sendButton.evaluateDisabled() + } - // MARK: - IBActions + // MARK: - Send Actions - @IBAction func sendFunds(_ sender: Any) { + func sendFunds(_ sender: Any) { + switch token { + case .ADM: + sendADMFunds() + case .ETH: + sendETHFunds() + } + } + + func sendADMFunds() { guard let dialogService = self.dialogService, let apiService = self.apiService else { fatalError("Dependecies fatal error") } @@ -328,4 +469,8 @@ class TransferViewController: FormViewController { present(alert, animated: true, completion: nil) } + + func sendETHFunds() { + // TODO: Add send eth funds + } } diff --git a/Adamant/Stories/Chats/ChatViewController.swift b/Adamant/Stories/Chats/ChatViewController.swift index acd884fff..aaff51f45 100644 --- a/Adamant/Stories/Chats/ChatViewController.swift +++ b/Adamant/Stories/Chats/ChatViewController.swift @@ -79,7 +79,20 @@ class ChatViewController: MessagesViewController { self.dialogService.showSystemActionSheet(title: "Send", message: "", actions: [ UIAlertAction(title: "Ethereum", style: .default, handler: { (action) in if let ethAddress = self.ethAddress { - self.dialogService.showSuccess(withMessage: ethAddress) +// self.dialogService.showSuccess(withMessage: ethAddress) + // MARK: Show transfer details + guard let vc = self.router.get(scene: AdamantScene.Account.transfer) as? TransferViewController else { + fatalError("Can't get TransferViewController scene") + } + + vc.token = .ETH + vc.toAddress = ethAddress + + if let nav = self.navigationController { + nav.pushViewController(vc, animated: true) + } else { + self.present(vc, animated: true, completion: nil) + } } else { self.dialogService.showWarning(withMessage: "User don't have public Eth wallet yet.") } From edccaca0b88534998b057e6b2c133601ed260de1 Mon Sep 17 00:00:00 2001 From: Anton B Date: Thu, 21 Jun 2018 19:42:42 +0300 Subject: [PATCH 012/175] Add send ETH trasaction method --- .../EthApiServiceProtocol.swift | 2 + .../EthApiService/EthApiService.swift | 87 +++++++++++++++++-- .../Stories/Chats/ChatViewController.swift | 21 ++++- 3 files changed, 102 insertions(+), 8 deletions(-) diff --git a/Adamant/ServiceProtocols/EthApiServiceProtocol.swift b/Adamant/ServiceProtocols/EthApiServiceProtocol.swift index ef1910c35..8926e9da9 100644 --- a/Adamant/ServiceProtocols/EthApiServiceProtocol.swift +++ b/Adamant/ServiceProtocols/EthApiServiceProtocol.swift @@ -16,6 +16,8 @@ protocol EthApiServiceProtocol: class { // MARK: - Accounts func newAccount(byPassphrase passphrase: String, completion: @escaping (ApiServiceResult) -> Void) + // MARK: - Transactions + func sendFunds(toAddress address: String, amount: Double, completion: @escaping (ApiServiceResult<[String: String]>) -> Void) // MARK: - Tools func getBalance(_ completion: @escaping (ApiServiceResult) -> Void) func getBalance(byAddress address: String, completion: @escaping (ApiServiceResult) -> Void) diff --git a/Adamant/Services/EthApiService/EthApiService.swift b/Adamant/Services/EthApiService/EthApiService.swift index 6979b0fb1..3e7356ae7 100644 --- a/Adamant/Services/EthApiService/EthApiService.swift +++ b/Adamant/Services/EthApiService/EthApiService.swift @@ -141,6 +141,81 @@ class EthApiService: EthApiServiceProtocol { } } + func sendFunds(toAddress address: String, amount: Double, completion: @escaping (ApiServiceResult<[String: String]>) -> Void) { + DispatchQueue.global().async { + guard let destinationEthAddress = EthereumAddress(address) else { + DispatchQueue.main.async { + completion(.failure(.internalError(message: "ETH Wallet: Send - invalid destination address", error: nil))) + } + return + } + guard let amount = Web3.Utils.parseToBigUInt("\(amount)", units: .eth) else { + DispatchQueue.main.async { + completion(.failure(.internalError(message: "ETH Wallet: Send - invalid amount format", error: nil))) + } + return + } + + guard let ethAddressFrom = self.account?.wallet.addresses?.first else { + DispatchQueue.main.async { + completion(.failure(.internalError(message: "ETH Wallet: Send - no address found", error: nil))) + } + return + } + + guard let keystore = self.account?.wallet else { + DispatchQueue.main.async { + completion(.failure(.internalError(message: "ETH Wallet: Send - no keystore found", error: nil))) + } + return + } + + self.web3.addKeystoreManager(KeystoreManager([keystore])) + var options = Web3Options.defaultOptions() +// options.gasLimit = BigUInt(gasLimit) + options.from = ethAddressFrom + options.value = BigUInt(amount) + guard let contract = self.web3.contract(Web3.Utils.coldWalletABI, at: destinationEthAddress) else { + DispatchQueue.main.async { + completion(.failure(.internalError(message: "ETH Wallet: Send - contract loading error", error: nil))) + } + return + } + + guard let estimatedGas = contract.method(options: options)?.estimateGas(options: nil).value else { + DispatchQueue.main.async { + completion(.failure(.internalError(message: "ETH Wallet: Send - retrieving estimated gas error", error: nil))) + } + return + } + options.gasLimit = estimatedGas + guard let gasPrice = self.web3.eth.getGasPrice().value else { + DispatchQueue.main.async { + completion(.failure(.internalError(message: "ETH Wallet: Send - retrieving gas price error", error: nil))) + } + return + } + options.gasPrice = gasPrice + guard let transaction = contract.method(options: options) else { + DispatchQueue.main.async { + completion(.failure(.internalError(message: "ETH Wallet: Send - create transaction issue", error: nil))) + } + return + } + + guard let sendResult = transaction.send(password: "", options: nil).value else { + DispatchQueue.main.async { + completion(.failure(.internalError(message: "ETH Wallet: Send - sending transaction error", error: nil))) + } + return + } + + DispatchQueue.main.async { + completion(.success(sendResult)) + } + } + } + func getBalance(_ completion: @escaping (ApiServiceResult) -> Void) { if let walletAddress = self.account?.wallet.addresses?.first { self.getBalance(byAddress: walletAddress) { (result) in @@ -150,9 +225,9 @@ class EthApiService: EthApiServiceProtocol { if let formattedAmount = Web3.Utils.formatToEthereumUnits(balance, toUnits: .eth, - decimals: 5, - fallbackToScientific: true) { - completion(.success("\(formattedAmount) ETH")) + decimals: 8, + fallbackToScientific: true), let amount = Double(formattedAmount) { + completion(.success("\(amount) ETH")) } else { completion(.failure(.internalError(message: "ETH Wallet: fail to get balance amount", error: nil))) } @@ -173,9 +248,9 @@ class EthApiService: EthApiServiceProtocol { case .success(let balance): if let formattedAmount = Web3.Utils.formatToEthereumUnits(balance, toUnits: .eth, - decimals: 5, - fallbackToScientific: true) { - completion(.success("\(formattedAmount) ETH")) + decimals: 8, + fallbackToScientific: true), let amount = Double(formattedAmount) { + completion(.success("\(amount) ETH")) } else { completion(.failure(.internalError(message: "ETH Wallet: fail to get balance amount", error: nil))) } diff --git a/Adamant/Stories/Chats/ChatViewController.swift b/Adamant/Stories/Chats/ChatViewController.swift index aaff51f45..d7b176968 100644 --- a/Adamant/Stories/Chats/ChatViewController.swift +++ b/Adamant/Stories/Chats/ChatViewController.swift @@ -79,14 +79,16 @@ class ChatViewController: MessagesViewController { self.dialogService.showSystemActionSheet(title: "Send", message: "", actions: [ UIAlertAction(title: "Ethereum", style: .default, handler: { (action) in if let ethAddress = self.ethAddress { -// self.dialogService.showSuccess(withMessage: ethAddress) - // MARK: Show transfer details + // MARK: Show ETH transfer details guard let vc = self.router.get(scene: AdamantScene.Account.transfer) as? TransferViewController else { fatalError("Can't get TransferViewController scene") } vc.token = .ETH vc.toAddress = ethAddress + if let address = self.chatroom?.partner?.address { + vc.reciverADMAddress = address + } if let nav = self.navigationController { nav.pushViewController(vc, animated: true) @@ -98,6 +100,21 @@ class ChatViewController: MessagesViewController { } }), UIAlertAction(title: "ADM", style: .default, handler: { [weak self] (_) in + // MARK: Show ADM transfer details - DISABLED until end of ICO +// if let address = self?.chatroom?.partner?.address { +// guard let vc = self?.router.get(scene: AdamantScene.Account.transfer) as? TransferViewController else { +// fatalError("Can't get TransferViewController scene") +// } +// +// vc.token = .ADM +// vc.toAddress = address +// +// if let nav = self?.navigationController { +// nav.pushViewController(vc, animated: true) +// } else { +// self?.present(vc, animated: true, completion: nil) +// } +// } let alert = UIAlertController(title: String.adamantLocalized.account.sorryAlert, message: String.adamantLocalized.account.transferNotAllowed, preferredStyle: .alert) let cancel = UIAlertAction(title: String.adamantLocalized.alert.cancel, style: .cancel) { (_) in } From 898dd05ef640b3a01ef2aea52b401cc1ca954061 Mon Sep 17 00:00:00 2001 From: Anton B Date: Fri, 22 Jun 2018 13:39:04 +0300 Subject: [PATCH 013/175] Send special message in chat for ETH transaction --- .../DataProviders/ChatsProvider.swift | 1 + .../EthApiServiceProtocol.swift | 2 +- .../ApiService/AdamantApi+Chats.swift | 4 +- .../DataProviders/AdamantChatsProvider.swift | 68 +++++++++-------- .../EthApiService/EthApiService.swift | 32 +++++++- .../Account/TransferViewController.swift | 74 ++++++++++++++----- .../Stories/Chats/ChatViewController.swift | 40 +++++++++- 7 files changed, 163 insertions(+), 58 deletions(-) diff --git a/Adamant/ServiceProtocols/DataProviders/ChatsProvider.swift b/Adamant/ServiceProtocols/DataProviders/ChatsProvider.swift index 3e019144b..cb5462ae9 100644 --- a/Adamant/ServiceProtocols/DataProviders/ChatsProvider.swift +++ b/Adamant/ServiceProtocols/DataProviders/ChatsProvider.swift @@ -172,6 +172,7 @@ protocol ChatsProvider: DataProvider { // MARK: - Sending messages func sendMessage(_ message: AdamantMessage, recipientId: String, completion: @escaping (ChatsProviderResult) -> Void ) + func sendSpecialMessage(_ message: AdamantMessage, recipientId: String, completion: @escaping (ChatsProviderResult) -> Void) func retrySendMessage(_ message: MessageTransaction, completion: @escaping (ChatsProviderRetryCancelResult) -> Void) // MARK: - Delete local message diff --git a/Adamant/ServiceProtocols/EthApiServiceProtocol.swift b/Adamant/ServiceProtocols/EthApiServiceProtocol.swift index 8926e9da9..3f0b6045d 100644 --- a/Adamant/ServiceProtocols/EthApiServiceProtocol.swift +++ b/Adamant/ServiceProtocols/EthApiServiceProtocol.swift @@ -17,7 +17,7 @@ protocol EthApiServiceProtocol: class { func newAccount(byPassphrase passphrase: String, completion: @escaping (ApiServiceResult) -> Void) // MARK: - Transactions - func sendFunds(toAddress address: String, amount: Double, completion: @escaping (ApiServiceResult<[String: String]>) -> Void) + func sendFunds(toAddress address: String, amount: Double, completion: @escaping (ApiServiceResult) -> Void) // MARK: - Tools func getBalance(_ completion: @escaping (ApiServiceResult) -> Void) func getBalance(byAddress address: String, completion: @escaping (ApiServiceResult) -> Void) diff --git a/Adamant/Services/ApiService/AdamantApi+Chats.swift b/Adamant/Services/ApiService/AdamantApi+Chats.swift index 4177f775b..7bdb3e4a6 100644 --- a/Adamant/Services/ApiService/AdamantApi+Chats.swift +++ b/Adamant/Services/ApiService/AdamantApi+Chats.swift @@ -60,9 +60,7 @@ extension AdamantApiService { self.sendMessage(senderId: senderId, recipientId: recipientId, keypair: keypair, message: message, nonce: nonce, type: ChatType.messageSpecial, completion: completion) } - // MARK: - private - - private func sendMessage(senderId: String, recipientId: String, keypair: Keypair, message: String, nonce: String, type: ChatType, completion: @escaping (ApiServiceResult) -> Void) { + func sendMessage(senderId: String, recipientId: String, keypair: Keypair, message: String, nonce: String, type: ChatType, completion: @escaping (ApiServiceResult) -> Void) { // MARK: 1. Prepare params let params: [String : Any] = [ "type": TransactionType.chatMessage.rawValue, diff --git a/Adamant/Services/DataProviders/AdamantChatsProvider.swift b/Adamant/Services/DataProviders/AdamantChatsProvider.swift index 7072e5f4e..e5334a327 100644 --- a/Adamant/Services/DataProviders/AdamantChatsProvider.swift +++ b/Adamant/Services/DataProviders/AdamantChatsProvider.swift @@ -256,38 +256,46 @@ extension AdamantChatsProvider { // MARK: - Sending messages { extension AdamantChatsProvider { func sendMessage(_ message: AdamantMessage, recipientId: String, completion: @escaping (ChatsProviderResult) -> Void) { - guard let loggedAccount = accountService.account, let keypair = accountService.keypair else { - completion(.failure(.notLogged)) - return - } - - guard loggedAccount.balance >= message.fee else { - completion(.failure(.notEnoughtMoneyToSend)) - return - } - - switch validateMessage(message) { - case .isValid: - break - - case .empty: - completion(.failure(.messageNotValid(.empty))) - return - - case .tooLong: - completion(.failure(.messageNotValid(.tooLong))) - return - } - - sendingQueue.async { - switch message { - case .text(let text), .markdownText(let text): - self.sendTextMessage(text: text, senderId: loggedAccount.address, recipientId: recipientId, keypair: keypair, completion: completion) - } - } + sendMessage(message, recipientId: recipientId, type: .message, completion: completion) } + + func sendSpecialMessage(_ message: AdamantMessage, recipientId: String, completion: @escaping (ChatsProviderResult) -> Void) { + sendMessage(message, recipientId: recipientId, type: .messageSpecial, completion: completion) + } + + private func sendMessage(_ message: AdamantMessage, recipientId: String, type: ChatType, completion: @escaping (ChatsProviderResult) -> Void) { + guard let loggedAccount = accountService.account, let keypair = accountService.keypair else { + completion(.failure(.notLogged)) + return + } + + guard loggedAccount.balance >= message.fee else { + completion(.failure(.notEnoughtMoneyToSend)) + return + } + + switch validateMessage(message) { + case .isValid: + break + + case .empty: + completion(.failure(.messageNotValid(.empty))) + return + + case .tooLong: + completion(.failure(.messageNotValid(.tooLong))) + return + } + + sendingQueue.async { + switch message { + case .text(let text), .markdownText(let text): + self.sendTextMessage(text: text, senderId: loggedAccount.address, recipientId: recipientId, keypair: keypair, type: type, completion: completion) + } + } + } - private func sendTextMessage(text: String, senderId: String, recipientId: String, keypair: Keypair, completion: @escaping (ChatsProviderResult) -> Void) { + private func sendTextMessage(text: String, senderId: String, recipientId: String, keypair: Keypair, type: ChatType, completion: @escaping (ChatsProviderResult) -> Void) { // MARK: 0. Prepare let privateContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) privateContext.parent = stack.container.viewContext diff --git a/Adamant/Services/EthApiService/EthApiService.swift b/Adamant/Services/EthApiService/EthApiService.swift index 3e7356ae7..4d909943f 100644 --- a/Adamant/Services/EthApiService/EthApiService.swift +++ b/Adamant/Services/EthApiService/EthApiService.swift @@ -141,7 +141,7 @@ class EthApiService: EthApiServiceProtocol { } } - func sendFunds(toAddress address: String, amount: Double, completion: @escaping (ApiServiceResult<[String: String]>) -> Void) { + func sendFunds(toAddress address: String, amount: Double, completion: @escaping (ApiServiceResult) -> Void) { DispatchQueue.global().async { guard let destinationEthAddress = EthereumAddress(address) else { DispatchQueue.main.async { @@ -210,8 +210,34 @@ class EthApiService: EthApiServiceProtocol { return } - DispatchQueue.main.async { - completion(.success(sendResult)) + guard let hash = sendResult["txhash"] else { + DispatchQueue.main.async { + completion(.failure(.internalError(message: "ETH Wallet: Send - fail to get transaction hash", error: nil))) + } + return + } + guard let formattedAmount = Web3.Utils.formatToEthereumUnits(amount, toUnits: .wei) else { + DispatchQueue.main.async { + completion(.failure(.internalError(message: "ETH Wallet: Send - fail to get transaction amount", error: nil))) + } + return + } + + let result = ["type": "eth_transaction", "amount": formattedAmount, "hash": hash, "comments":""] + + do { + let data = try JSONEncoder().encode(result) + guard let raw = String(data: data, encoding: String.Encoding.utf8) else { + return + } + + DispatchQueue.main.async { + completion(.success(raw)) + } + } catch { + DispatchQueue.main.async { + completion(.failure(.internalError(message: "ETH Wallet: Send - wrong data issue", error: nil))) + } } } } diff --git a/Adamant/Stories/Account/TransferViewController.swift b/Adamant/Stories/Account/TransferViewController.swift index 97b964144..8252c4014 100644 --- a/Adamant/Stories/Account/TransferViewController.swift +++ b/Adamant/Stories/Account/TransferViewController.swift @@ -34,6 +34,11 @@ fileprivate extension String.adamantLocalized.alert { static let send = NSLocalizedString("TransferScene.Send", comment: "Transfer: Confirm transfer alert: Send tokens button") } +// MARK: Transfer Delegate Protocol + +protocol TransferDelegate { + func transferFinished(with data:String) +} // MARK: - class TransferViewController: FormViewController { @@ -109,9 +114,10 @@ class TransferViewController: FormViewController { var token: Token = .ADM var toAddress: String = "" - var reciverADMAddress: String = "" // ???? private(set) var totalAmount: Double? = nil + + var delegate: TransferDelegate? // MARK: - Lifecycle @@ -230,12 +236,11 @@ class TransferViewController: FormViewController { private func createETHForm() { // MARK: - Wallet section - if let account = accountService.account, let ethAccount = ethApiService.account, let ethBalanceBigInt = ethAccount.balance, let ethBalanceString = Web3.Utils.formatToEthereumUnits(ethBalanceBigInt), let ethBalance = Double(ethBalanceString) { - let balance = (account.balance as NSDecimalNumber).doubleValue + if let ethAccount = ethApiService.account, let ethBalanceBigInt = ethAccount.balance, let ethBalanceString = Web3.Utils.formatToEthereumUnits(ethBalanceBigInt), let ethBalance = Double(ethBalanceString) { maxToTransfer = ethBalance - if let feeString = Web3.Utils.formatToEthereumUnits(BigInt(EthApiService.defaultGasPrice), toUnits: .Gwei), let fee = Double(feeString) { + if let feeString = Web3.Utils.formatToEthereumUnits(BigUInt(EthApiService.defaultGasPrice * EthApiService.transferGas), toUnits: .eth, decimals: 8), let fee = Double(feeString) { defaultFee = fee } @@ -244,11 +249,6 @@ class TransferViewController: FormViewController { currencyFormatter.roundingMode = .floor currencyFormatter.positiveFormat = "#.######## ETH" - let currencyFormatterGwie = NumberFormatter() - currencyFormatterGwie.numberStyle = .decimal - currencyFormatterGwie.roundingMode = .floor - currencyFormatterGwie.positiveFormat = "#.######## Gwei" - form +++ Section(Sections.wallet.localized) <<< DecimalRow() { $0.title = Row.balance.localized @@ -298,7 +298,7 @@ class TransferViewController: FormViewController { $0.value = defaultFee $0.tag = Row.fee.tag $0.disabled = true - $0.formatter = currencyFormatterGwie + $0.formatter = currencyFormatter } <<< DecimalRow() { $0.title = Row.total.localized @@ -350,14 +350,14 @@ class TransferViewController: FormViewController { return } - guard let amount = row.value, let feeString = Web3.Utils.formatToEthereumUnits(BigInt(EthApiService.defaultGasPrice), toUnits: .eth, decimals: 8), let fee = Double(feeString) else { + guard let amount = row.value else { totalAmount = nil sendButton.disabled = true row.cell.titleLabel?.textColor = .black return } - totalAmount = amount + fee + totalAmount = amount + defaultFee totalRow.evaluateDisabled() totalRow.value = totalAmount @@ -444,11 +444,7 @@ class TransferViewController: FormViewController { self?.accountService.update() - if let nav = self?.navigationController { - nav.popViewController(animated: true) - } else { - self?.dismiss(animated: true, completion: nil) - } + self?.close() } case .failure(let error): @@ -471,6 +467,48 @@ class TransferViewController: FormViewController { } func sendETHFunds() { - // TODO: Add send eth funds + guard let recipientRow = form.rowBy(tag: Row.address.tag) as? TextRow, + let recipient = recipientRow.value, + let amountRow = form.rowBy(tag: Row.amount.tag) as? DecimalRow, + let amount = amountRow.value else { + return + } + + let alert = UIAlertController(title: String.localizedStringWithFormat(String.adamantLocalized.alert.confirmSendMessageFormat, "\(amount) ETH", recipient), message: "You can't undo this action.", preferredStyle: .alert) + let cancelAction = UIAlertAction(title: String.adamantLocalized.alert.cancel , style: .cancel, handler: nil) + let sendAction = UIAlertAction(title: String.adamantLocalized.alert.send, style: .default, handler: { _ in + self.sendEth(to: recipient, amount: amount) + }) + + alert.addAction(cancelAction) + alert.addAction(sendAction) + + present(alert, animated: true, completion: nil) + } + + private func sendEth(to recipient: String, amount: Double) { + self.dialogService.showProgress(withMessage: String.adamantLocalized.transfer.transferProcessingMessage, userInteractionEnable: false) + + self.ethApiService.sendFunds(toAddress: recipient, amount: amount) { (result) in + switch result { + case .success(let value): + print("Payload: \(value)") + + self.delegate?.transferFinished(with: value) + self.dialogService.showSuccess(withMessage: String.adamantLocalized.transfer.transferSuccess) + self.close() + + case .failure(let error): + self.dialogService.showError(withMessage: "Transrer issue", error: error) + } + } + } + + private func close() { + if let nav = self.navigationController { + nav.popViewController(animated: true) + } else { + self.dismiss(animated: true, completion: nil) + } } } diff --git a/Adamant/Stories/Chats/ChatViewController.swift b/Adamant/Stories/Chats/ChatViewController.swift index d7b176968..cf0d84173 100644 --- a/Adamant/Stories/Chats/ChatViewController.swift +++ b/Adamant/Stories/Chats/ChatViewController.swift @@ -86,9 +86,7 @@ class ChatViewController: MessagesViewController { vc.token = .ETH vc.toAddress = ethAddress - if let address = self.chatroom?.partner?.address { - vc.reciverADMAddress = address - } + vc.delegate = self if let nav = self.navigationController { nav.pushViewController(vc, animated: true) @@ -420,3 +418,39 @@ extension ChatViewController: NSFetchedResultsControllerDelegate { } } } + +extension ChatViewController: TransferDelegate { + func transferFinished(with data: String) { + if let address = chatroom?.partner?.address { + self.sendChatMessage(text: data, to: address) + } + } + + // MARK: Send Chat message with ETH transaction + private func sendChatMessage(text: String, to address: String) { + let message = AdamantMessage.text(text) + let valid = chatsProvider.validateMessage(message) + switch valid { + case .isValid: break + default: + dialogService.showToastMessage(valid.localized) + return + } + + guard text.count > 0 else { + // TODO show warning + return + } + + chatsProvider.sendSpecialMessage(.text(text), recipientId: address, completion: { [weak self] result in + switch result { + case .success: +// self?.dialogService.showSuccess(withMessage: String.adamantLocalized.transfer.transferSuccess) + break + + case .failure(let error): + self?.dialogService.showRichError(error: error) + } + }) + } +} From 66516d533faf8f554046083a58860d34941f5c02 Mon Sep 17 00:00:00 2001 From: Anton B Date: Fri, 22 Jun 2018 17:47:25 +0300 Subject: [PATCH 014/175] Render ETH Transaction in chat --- Adamant.xcodeproj/project.pbxproj | 4 + Adamant/Models/TransferType.swift | 122 ++++++++++++++++++ .../Chats/ChatListViewController.swift | 20 ++- .../Chats/ChatViewController+MessageKit.swift | 8 ++ 4 files changed, 148 insertions(+), 6 deletions(-) create mode 100644 Adamant/Models/TransferType.swift diff --git a/Adamant.xcodeproj/project.pbxproj b/Adamant.xcodeproj/project.pbxproj index ffbb057c0..72cd3e3b0 100644 --- a/Adamant.xcodeproj/project.pbxproj +++ b/Adamant.xcodeproj/project.pbxproj @@ -24,6 +24,7 @@ E90055F920ECD86800D0CB2D /* SecurityViewController+StayIn.swift in Sources */ = {isa = PBXBuildFile; fileRef = E90055F820ECD86800D0CB2D /* SecurityViewController+StayIn.swift */; }; E90055FB20ECE78A00D0CB2D /* SecurityViewController+notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = E90055FA20ECE78A00D0CB2D /* SecurityViewController+notifications.swift */; }; 644D3DBB20BD5F22002EB002 /* Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = 644D3DBA20BD5F22002EB002 /* Status.swift */; }; + 649A694720DD427200D27E91 /* TransferType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649A694620DD427200D27E91 /* TransferType.swift */; }; 64D059FF20D3116B003AD655 /* NodesListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D059FE20D3116A003AD655 /* NodesListViewController.swift */; }; 64D05A0220D51632003AD655 /* EthApiServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D05A0120D51632003AD655 /* EthApiServiceProtocol.swift */; }; 64D05A0420D516B4003AD655 /* EthApiService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D05A0320D516B4003AD655 /* EthApiService.swift */; }; @@ -224,6 +225,7 @@ 64D059FE20D3116A003AD655 /* NodesListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NodesListViewController.swift; sourceTree = ""; }; 64E8304F20F5FEEF006FA590 /* VotesAsset.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VotesAsset.swift; sourceTree = ""; }; 644D3DBA20BD5F22002EB002 /* Status.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Status.swift; sourceTree = ""; }; + 649A694620DD427200D27E91 /* TransferType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransferType.swift; sourceTree = ""; }; 64D059FE20D3116A003AD655 /* NodesListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NodesListViewController.swift; sourceTree = ""; }; 64D05A0120D51632003AD655 /* EthApiServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EthApiServiceProtocol.swift; sourceTree = ""; }; 64D05A0320D516B4003AD655 /* EthApiService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EthApiService.swift; sourceTree = ""; }; @@ -582,6 +584,7 @@ E9A03FD320DBC824007653A1 /* NodeVersion.swift */, 644EC34E20EFA77A00F40C73 /* Delegate.swift */, 64E8304F20F5FEEF006FA590 /* VotesAsset.swift */, + 649A694620DD427200D27E91 /* TransferType.swift */, ); path = Models; sourceTree = ""; @@ -1158,6 +1161,7 @@ E9CAE8D22018AA7700345E76 /* AdamantApi+Accounts.swift in Sources */, E987024920C2B1F700E393F4 /* AdamantChatsProvider+fakeMessages.swift in Sources */, E9942B84203CBFCE00C163AF /* AdamantQRTools.swift in Sources */, + 649A694720DD427200D27E91 /* TransferType.swift in Sources */, E9E7CD8F20026CD300DFC4DB /* AdamantDialogService.swift in Sources */, E9CAE8DA2018ACD300345E76 /* AdamantApi+Chats.swift in Sources */, E91947B020002393001362F8 /* AdamantApiService.swift in Sources */, diff --git a/Adamant/Models/TransferType.swift b/Adamant/Models/TransferType.swift new file mode 100644 index 000000000..a357289b9 --- /dev/null +++ b/Adamant/Models/TransferType.swift @@ -0,0 +1,122 @@ +// +// TransferType.swift +// Adamant +// +// Created by Anton Boyarkin on 22/06/2018. +// Copyright © 2018 Adamant. All rights reserved. +// + +import Foundation +import web3swift +import BigInt + +enum TransferType: String, Decodable { + case eth = "eth_transaction" + case unknown + + init(from decoder: Decoder) throws { + let label = try decoder.singleValueContainer().decode(String.self) + switch label { + case "eth_transaction": self = .eth + default: self = .unknown + } + } +} + +struct ChatTransfer: Decodable { + enum CodingKeys: String, CodingKey { + case amount + case hash + case type + case comments + } + let amount: String + let hash: String + let type: TransferType + let comments: String + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + amount = (try? container.decode(String.self, forKey: .amount)) ?? "0" + hash = (try? container.decode(String.self, forKey: .hash)) ?? "" + type = (try? container.decode(TransferType.self, forKey: .type)) ?? .unknown + comments = (try? container.decode(String.self, forKey: .comments)) ?? "" + } + + func render() -> NSAttributedString { + switch type { + case .eth: + return renderETH() + default: + return NSAttributedString(string: "") + } + } + + func renderPreview(isOutgoing: Bool) -> String { + switch type { + case .eth: + return renderETHPreview(isOutgoing: isOutgoing) + default: + return "" + } + } + + + // MARK: - Chat renderers + + private func renderETH() -> NSAttributedString { + guard let amount = BigUInt(amount) else { + return NSAttributedString(string: "") + } + + let balance: String + + if let formattedAmount = Web3.Utils.formatToEthereumUnits(amount, + toUnits: .eth, + decimals: 8, + fallbackToScientific: true), let amount = Double(formattedAmount) { + balance = "\(amount) ETH" + } else { + balance = "-- ETH" + } + + let sent = String.adamantLocalized.chat.sent + + let attributedString = NSMutableAttributedString(string: "\(sent)\n\(balance)\n\n\(String.adamantLocalized.chat.tapForDetails)") + + let rangeReference = attributedString.string as NSString + let sentRange = rangeReference.range(of: sent) + let amountRange = rangeReference.range(of: balance) + + attributedString.setAttributes([.font: UIFont.adamantPrimary(size: 14)], range: sentRange) + attributedString.setAttributes([.font: UIFont.adamantPrimary(size: 28)], range: amountRange) + + return attributedString + } + + // MARK: - Preview renderers + + func renderETHPreview(isOutgoing: Bool) -> String { + guard let amount = BigUInt(amount) else { + return "" + } + + let balance: String + + if let formattedAmount = Web3.Utils.formatToEthereumUnits(amount, + toUnits: .eth, + decimals: 8, + fallbackToScientific: true), let amount = Double(formattedAmount) { + balance = "\(amount) ETH" + } else { + balance = "-- ETH" + } + + if isOutgoing { + return String.localizedStringWithFormat(String.adamantLocalized.chatList.sentMessagePrefix, " ⬅️ \(balance)") + } else { + return "➡️ \(balance)" + } + } +} diff --git a/Adamant/Stories/Chats/ChatListViewController.swift b/Adamant/Stories/Chats/ChatListViewController.swift index 681e57741..938746d03 100644 --- a/Adamant/Stories/Chats/ChatListViewController.swift +++ b/Adamant/Stories/Chats/ChatListViewController.swift @@ -271,12 +271,20 @@ extension ChatListViewController { cell.lastMessageLabel.text = nil break } - - if message.isOutgoing { - cell.lastMessageLabel.text = String.localizedStringWithFormat(String.adamantLocalized.chatList.sentMessagePrefix, text) - } else { - cell.lastMessageLabel.text = text - } + + if message.type == ChatType.messageSpecial.rawValue { + guard let data = text.data(using: String.Encoding.utf8), let transfer = try? JSONDecoder().decode(ChatTransfer.self, from: data) else { + cell.lastMessageLabel.text = nil + break + } + cell.lastMessageLabel.text = transfer.renderPreview(isOutgoing: message.isOutgoing) + } else { + if message.isOutgoing { + cell.lastMessageLabel.text = String.localizedStringWithFormat(String.adamantLocalized.chatList.sentMessagePrefix, text) + } else { + cell.lastMessageLabel.text = text + } + } case let transfer as TransferTransaction: cell.lastMessageLabel.text = formatTransferPreview(transfer) diff --git a/Adamant/Stories/Chats/ChatViewController+MessageKit.swift b/Adamant/Stories/Chats/ChatViewController+MessageKit.swift index 39fafcc31..2e61b43c5 100644 --- a/Adamant/Stories/Chats/ChatViewController+MessageKit.swift +++ b/Adamant/Stories/Chats/ChatViewController+MessageKit.swift @@ -382,6 +382,14 @@ extension MessageTransaction: MessageType { guard let message = message else { return MessageKind.text("") } + + if type == ChatType.messageSpecial.rawValue { + guard let data = message.data(using: String.Encoding.utf8), let transfer = try? JSONDecoder().decode(ChatTransfer.self, from: data) else { + return MessageData.text("") + } + + return MessageData.attributedText(transfer.render()) + } if isMarkdown { let parser = MarkdownParser(font: UIFont.adamantChatDefault) From 5fda1afd9f4f52f3ad10eb32462051c720ed77a3 Mon Sep 17 00:00:00 2001 From: Anton B Date: Mon, 25 Jun 2018 11:56:09 +0300 Subject: [PATCH 015/175] Fix for new rich messege type --- Adamant/ServiceProtocols/DataProviders/ChatsProvider.swift | 2 +- Adamant/Services/ApiService/AdamantApi+Chats.swift | 4 ++-- Adamant/Services/DataProviders/AdamantChatsProvider.swift | 4 ++-- Adamant/Stories/Chats/ChatListViewController.swift | 2 +- Adamant/Stories/Chats/ChatViewController+MessageKit.swift | 2 +- Adamant/Stories/Chats/ChatViewController.swift | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Adamant/ServiceProtocols/DataProviders/ChatsProvider.swift b/Adamant/ServiceProtocols/DataProviders/ChatsProvider.swift index cb5462ae9..36b2305f1 100644 --- a/Adamant/ServiceProtocols/DataProviders/ChatsProvider.swift +++ b/Adamant/ServiceProtocols/DataProviders/ChatsProvider.swift @@ -172,7 +172,7 @@ protocol ChatsProvider: DataProvider { // MARK: - Sending messages func sendMessage(_ message: AdamantMessage, recipientId: String, completion: @escaping (ChatsProviderResult) -> Void ) - func sendSpecialMessage(_ message: AdamantMessage, recipientId: String, completion: @escaping (ChatsProviderResult) -> Void) + func sendRichMessage(_ message: AdamantMessage, recipientId: String, completion: @escaping (ChatsProviderResult) -> Void) func retrySendMessage(_ message: MessageTransaction, completion: @escaping (ChatsProviderRetryCancelResult) -> Void) // MARK: - Delete local message diff --git a/Adamant/Services/ApiService/AdamantApi+Chats.swift b/Adamant/Services/ApiService/AdamantApi+Chats.swift index 7bdb3e4a6..6bd5fd2ef 100644 --- a/Adamant/Services/ApiService/AdamantApi+Chats.swift +++ b/Adamant/Services/ApiService/AdamantApi+Chats.swift @@ -56,8 +56,8 @@ extension AdamantApiService { self.sendMessage(senderId: senderId, recipientId: recipientId, keypair: keypair, message: message, nonce: nonce, type: ChatType.message, completion: completion) } - func sendSpecialMessage(senderId: String, recipientId: String, keypair: Keypair, message: String, nonce: String, completion: @escaping (ApiServiceResult) -> Void) { - self.sendMessage(senderId: senderId, recipientId: recipientId, keypair: keypair, message: message, nonce: nonce, type: ChatType.messageSpecial, completion: completion) + func sendRichMessage(senderId: String, recipientId: String, keypair: Keypair, message: String, nonce: String, completion: @escaping (ApiServiceResult) -> Void) { + self.sendMessage(senderId: senderId, recipientId: recipientId, keypair: keypair, message: message, nonce: nonce, type: ChatType.richMessage, completion: completion) } func sendMessage(senderId: String, recipientId: String, keypair: Keypair, message: String, nonce: String, type: ChatType, completion: @escaping (ApiServiceResult) -> Void) { diff --git a/Adamant/Services/DataProviders/AdamantChatsProvider.swift b/Adamant/Services/DataProviders/AdamantChatsProvider.swift index e5334a327..834af0974 100644 --- a/Adamant/Services/DataProviders/AdamantChatsProvider.swift +++ b/Adamant/Services/DataProviders/AdamantChatsProvider.swift @@ -259,8 +259,8 @@ extension AdamantChatsProvider { sendMessage(message, recipientId: recipientId, type: .message, completion: completion) } - func sendSpecialMessage(_ message: AdamantMessage, recipientId: String, completion: @escaping (ChatsProviderResult) -> Void) { - sendMessage(message, recipientId: recipientId, type: .messageSpecial, completion: completion) + func sendRichMessage(_ message: AdamantMessage, recipientId: String, completion: @escaping (ChatsProviderResult) -> Void) { + sendMessage(message, recipientId: recipientId, type: .richMessage, completion: completion) } private func sendMessage(_ message: AdamantMessage, recipientId: String, type: ChatType, completion: @escaping (ChatsProviderResult) -> Void) { diff --git a/Adamant/Stories/Chats/ChatListViewController.swift b/Adamant/Stories/Chats/ChatListViewController.swift index 938746d03..48fb23742 100644 --- a/Adamant/Stories/Chats/ChatListViewController.swift +++ b/Adamant/Stories/Chats/ChatListViewController.swift @@ -272,7 +272,7 @@ extension ChatListViewController { break } - if message.type == ChatType.messageSpecial.rawValue { + if message.type == Int16(ChatType.richMessage.rawValue) { guard let data = text.data(using: String.Encoding.utf8), let transfer = try? JSONDecoder().decode(ChatTransfer.self, from: data) else { cell.lastMessageLabel.text = nil break diff --git a/Adamant/Stories/Chats/ChatViewController+MessageKit.swift b/Adamant/Stories/Chats/ChatViewController+MessageKit.swift index 2e61b43c5..caaf50758 100644 --- a/Adamant/Stories/Chats/ChatViewController+MessageKit.swift +++ b/Adamant/Stories/Chats/ChatViewController+MessageKit.swift @@ -383,7 +383,7 @@ extension MessageTransaction: MessageType { return MessageKind.text("") } - if type == ChatType.messageSpecial.rawValue { + if type == Int16(ChatType.richMessage.rawValue) { guard let data = message.data(using: String.Encoding.utf8), let transfer = try? JSONDecoder().decode(ChatTransfer.self, from: data) else { return MessageData.text("") } diff --git a/Adamant/Stories/Chats/ChatViewController.swift b/Adamant/Stories/Chats/ChatViewController.swift index cf0d84173..9b8598a79 100644 --- a/Adamant/Stories/Chats/ChatViewController.swift +++ b/Adamant/Stories/Chats/ChatViewController.swift @@ -442,7 +442,7 @@ extension ChatViewController: TransferDelegate { return } - chatsProvider.sendSpecialMessage(.text(text), recipientId: address, completion: { [weak self] result in + chatsProvider.sendRichMessage(.text(text), recipientId: address, completion: { [weak self] result in switch result { case .success: // self?.dialogService.showSuccess(withMessage: String.adamantLocalized.transfer.transferSuccess) From ffef436fe10956d58a99c04fe7fdda2a7c68f3db Mon Sep 17 00:00:00 2001 From: Anton B Date: Mon, 25 Jun 2018 18:04:15 +0300 Subject: [PATCH 016/175] Add ETH Transactions history API --- .../EthApiServiceProtocol.swift | 2 + .../EthApiService/EthApiService.swift | 175 ++++++++++++++++++ 2 files changed, 177 insertions(+) diff --git a/Adamant/ServiceProtocols/EthApiServiceProtocol.swift b/Adamant/ServiceProtocols/EthApiServiceProtocol.swift index 3f0b6045d..111489bff 100644 --- a/Adamant/ServiceProtocols/EthApiServiceProtocol.swift +++ b/Adamant/ServiceProtocols/EthApiServiceProtocol.swift @@ -18,6 +18,8 @@ protocol EthApiServiceProtocol: class { // MARK: - Transactions func sendFunds(toAddress address: String, amount: Double, completion: @escaping (ApiServiceResult) -> Void) + + func getTransactions(_ completion: @escaping (ApiServiceResult<[EthTransaction]>) -> Void) // MARK: - Tools func getBalance(_ completion: @escaping (ApiServiceResult) -> Void) func getBalance(byAddress address: String, completion: @escaping (ApiServiceResult) -> Void) diff --git a/Adamant/Services/EthApiService/EthApiService.swift b/Adamant/Services/EthApiService/EthApiService.swift index 4d909943f..e6b05a212 100644 --- a/Adamant/Services/EthApiService/EthApiService.swift +++ b/Adamant/Services/EthApiService/EthApiService.swift @@ -9,6 +9,7 @@ import Foundation import web3swift import BigInt +import Alamofire // MARK: - Notifications extension Notification.Name { @@ -45,6 +46,8 @@ class EthApiService: EthApiServiceProtocol { var apiUrl: String var web3: web3! + let transactionsHistoryService = TransactionsHistory() + private(set) var account: EthAccount? // MARK: - Initialization @@ -242,6 +245,16 @@ class EthApiService: EthApiServiceProtocol { } } + func getTransactions(_ completion: @escaping (ApiServiceResult<[EthTransaction]>) -> Void) { + if let address = self.account?.address { + transactionsHistoryService.getTransactionsHistory(address: address, completion: completion) + } else { + completion(.failure(.internalError(message: "ETH Wallet: not found", error: nil))) + } + } + + // MARK: - Tools + func getBalance(_ completion: @escaping (ApiServiceResult) -> Void) { if let walletAddress = self.account?.wallet.addresses?.first { self.getBalance(byAddress: walletAddress) { (result) in @@ -324,3 +337,165 @@ class EthApiService: EthApiServiceProtocol { } } } + +// MARK: - Transactions History API + + class TransactionsHistory { + private let baseUrl = "https://api-ropsten.etherscan.io/api" + + private let defaultResponseDispatchQueue = DispatchQueue(label: "com.adamant.response-queue", qos: .utility, attributes: [.concurrent]) + + private func buildUrl(queryItems: [URLQueryItem]? = nil) throws -> URL { + guard let url = URL(string: baseUrl), var components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { + throw AdamantApiService.InternalError.endpointBuildFailed + } + + components.queryItems = queryItems + + return try components.asURL() + } + + func getTransactionsHistory(address: String, page: Int = 1, size: Int = 50, completion: @escaping (ApiServiceResult<[EthTransaction]>) -> Void) { + let queryItems: [URLQueryItem] = [URLQueryItem(name: "module", value: "account"), + URLQueryItem(name: "action", value: "txlist"), + URLQueryItem(name: "address", value: address), + URLQueryItem(name: "page", value: "\(page)"), + URLQueryItem(name: "offset", value: "\(size)"), + URLQueryItem(name: "sort", value: "asc") +// ,URLQueryItem(name: "apikey", value: "YourApiKeyToken") + ] + + let endpoint: URL + do { + endpoint = try buildUrl(queryItems: queryItems) + } catch { + let err = AdamantApiService.InternalError.endpointBuildFailed.apiServiceErrorWith(error: error) + completion(.failure(err)) + return + } + + Alamofire.request(endpoint).responseData(queue: defaultResponseDispatchQueue) { response in + switch response.result { + case .success(let data): + do { + let model: Response = try JSONDecoder().decode(Response.self, from: data) + + if model.status == 1 { + completion(.success(model.result)) + } else { + completion(.failure(.internalError(message: model.message, error: nil))) + } + } catch { + completion(.failure(.internalError(message: "", error: error))) + } + break + case .failure(let error): + completion(.failure(.internalError(message: "", error: error))) + break + } + } + } +} + +//MARK: - Decodable Transaction structures +struct Response: Decodable { + enum CodingKeys: String, CodingKey { + case status + case message + case result + } + + let status: Int + let message: String + let result: [EthTransaction] + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let statusString = (try? container.decode(String.self, forKey: .status)) ?? "0" + status = Int(statusString) ?? 0 + message = (try? container.decode(String.self, forKey: .message)) ?? "" + result = (try? container.decode([EthTransaction].self, forKey: .result)) ?? [] + } +} + +struct EthTransaction: Decodable { + enum CodingKeys: String, CodingKey { + case date = "timeStamp" + case hash + case value + case from + case to + case gasUsed + case gasPrice + case confirmations + case isError + case receiptStatus = "txreceipt_status" + } + + let date: Date + let hash: String + let value: BigUInt + let from: String + let to: String + let gasUsed: BigUInt + let gasPrice: BigUInt + let confirmations: String + let isError: Bool + let receiptStatus: Int + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + let unixTimeStamp = Double((try? container.decode(String.self, forKey: .date)) ?? "0") ?? 0 + date = Date(timeIntervalSince1970: unixTimeStamp) + hash = (try? container.decode(String.self, forKey: .hash)) ?? "" + value = BigUInt((try? container.decode(String.self, forKey: .value)) ?? "0") ?? BigUInt(0) + from = (try? container.decode(String.self, forKey: .from)) ?? "" + to = (try? container.decode(String.self, forKey: .to)) ?? "" + gasUsed = BigUInt((try? container.decode(String.self, forKey: .gasUsed)) ?? "0") ?? BigUInt(0) + gasPrice = BigUInt((try? container.decode(String.self, forKey: .gasPrice)) ?? "0") ?? BigUInt(0) + confirmations = (try? container.decode(String.self, forKey: .confirmations)) ?? "0" + let isErrorStatus = Int((try? container.decode(String.self, forKey: .isError)) ?? "0") ?? 0 + isError = isErrorStatus == 1 ? true : false + receiptStatus = Int((try? container.decode(String.self, forKey: .receiptStatus)) ?? "0") ?? 0 + } + + func isOutgoing(_ address: String) -> Bool { + return from == address.lowercased() ? true : false + } + + func formattedValue() -> String { + if let formattedAmount = Web3.Utils.formatToEthereumUnits(value, + toUnits: .eth, + decimals: 8, + fallbackToScientific: true), let amount = Double(formattedAmount) { + return "\(amount) ETH" + } else { + return "\(value)" + } + } +} + +/* + { + "blockNumber":"3455267", + "timeStamp":"1529241530", + "hash":"0x9e2092aa9a278ebdd5cc4e37d626533ec1a480397c101add069817c0934cfa76", + "nonce":"561145", + "blockHash":"0xf828955a0911da4a2c207f96b8bffabac804eab7888ec88149ab9867db19b7dd", + "transactionIndex":"16", + "from":"0x687422eea2cb73b5d3e242ba5456b782919afc85", + "to":"0x700bc74dd49044446bcb6a25ae5e725d14538825", + "value":"1000000000000000000", + "gas":"314150", + "gasPrice":"5000000000", + "isError":"0", + "txreceipt_status":"1", + "input":"0x", + "contractAddress":"", + "cumulativeGasUsed":"381927", + "gasUsed":"21000", + "confirmations":"32316" + } + + */ From 94715689cdcd122f91953411d375ead0910f4552 Mon Sep 17 00:00:00 2001 From: Anton B Date: Mon, 25 Jun 2018 18:05:16 +0300 Subject: [PATCH 017/175] Add ETH Transactions history screen --- Adamant.xcodeproj/project.pbxproj | 4 + .../ETHTransactionsViewController.swift | 121 +++++++++++++ .../Transactions/TransactionsRoutes.swift | 10 +- .../TransactionsViewController.swift | 161 +++++++++--------- 4 files changed, 215 insertions(+), 81 deletions(-) create mode 100644 Adamant/Stories/Transactions/ETHTransactionsViewController.swift diff --git a/Adamant.xcodeproj/project.pbxproj b/Adamant.xcodeproj/project.pbxproj index 72cd3e3b0..4cbd53680 100644 --- a/Adamant.xcodeproj/project.pbxproj +++ b/Adamant.xcodeproj/project.pbxproj @@ -28,6 +28,7 @@ 64D059FF20D3116B003AD655 /* NodesListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D059FE20D3116A003AD655 /* NodesListViewController.swift */; }; 64D05A0220D51632003AD655 /* EthApiServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D05A0120D51632003AD655 /* EthApiServiceProtocol.swift */; }; 64D05A0420D516B4003AD655 /* EthApiService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D05A0320D516B4003AD655 /* EthApiService.swift */; }; + 64FA53CD20E1300B006783C9 /* ETHTransactionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64FA53CC20E1300A006783C9 /* ETHTransactionsViewController.swift */; }; E905D39B2048A9BD00DDB504 /* KeychainStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = E905D39A2048A9BD00DDB504 /* KeychainStore.swift */; }; E905D39D204C13B900DDB504 /* SecuredStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = E905D39C204C13B900DDB504 /* SecuredStore.swift */; }; E905D39F204C281400DDB504 /* LoginViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E905D39E204C281400DDB504 /* LoginViewController.swift */; }; @@ -229,6 +230,7 @@ 64D059FE20D3116A003AD655 /* NodesListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NodesListViewController.swift; sourceTree = ""; }; 64D05A0120D51632003AD655 /* EthApiServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EthApiServiceProtocol.swift; sourceTree = ""; }; 64D05A0320D516B4003AD655 /* EthApiService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EthApiService.swift; sourceTree = ""; }; + 64FA53CC20E1300A006783C9 /* ETHTransactionsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ETHTransactionsViewController.swift; sourceTree = ""; }; 871009461457F6B7B47D99C5 /* Pods-Adamant.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Adamant.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Adamant/Pods-Adamant.debug.xcconfig"; sourceTree = ""; }; 903AC078A8A55175A05E42D4 /* Pods-Adamant.testing.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Adamant.testing.xcconfig"; path = "Pods/Target Support Files/Pods-Adamant/Pods-Adamant.testing.xcconfig"; sourceTree = ""; }; C107D5CB65B4D728B9D97C0F /* Pods_Adamant.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Adamant.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -680,6 +682,7 @@ children = ( E94E7B05205D48B20042B639 /* TransactionsRoutes.swift */, E9E7CDC12003F5A400DFC4DB /* TransactionsViewController.swift */, + 64FA53CC20E1300A006783C9 /* ETHTransactionsViewController.swift */, E94E7B0B205D5E4A0042B639 /* TransactionsViewController.xib */, E9EC34132005178500C0E546 /* TransactionDetailsViewController.swift */, E94E7B0D205D5EA80042B639 /* TransactionDetailsViewController.xib */, @@ -1187,6 +1190,7 @@ E95F85C7200A9B070070534A /* ChatTableViewCell.swift in Sources */, E983AE2A20E65F3200497E1A /* AccountViewController.swift in Sources */, E9E7CD932002740500DFC4DB /* AdamantAccountService.swift in Sources */, + 64FA53CD20E1300B006783C9 /* ETHTransactionsViewController.swift in Sources */, E9147B612050599000145913 /* LoginViewController+QR.swift in Sources */, E9147B6F205088DE00145913 /* LoginViewController+Pinpad.swift in Sources */, E9FAE5E2203ED1AE008D3A6B /* ShareQrViewController.swift in Sources */, diff --git a/Adamant/Stories/Transactions/ETHTransactionsViewController.swift b/Adamant/Stories/Transactions/ETHTransactionsViewController.swift new file mode 100644 index 000000000..4d536f247 --- /dev/null +++ b/Adamant/Stories/Transactions/ETHTransactionsViewController.swift @@ -0,0 +1,121 @@ +// +// ETHTransactionsViewController.swift +// Adamant +// +// Created by Anton Boyarkin on 25/06/2018. +// Copyright © 2018 Adamant. All rights reserved. +// + +import UIKit + +class ETHTransactionsViewController: TransactionsViewController { + + // MARK: - Dependencies + var ethApiService: EthApiServiceProtocol! + var dialogService: DialogService! + var router: Router! + + // MARK: - Properties + var transactions: [EthTransaction] = [] + + override func viewDidLoad() { + super.viewDidLoad() + + tableView.dataSource = self + tableView.delegate = self + + self.refreshControl.beginRefreshing() + + handleRefresh(self.refreshControl) + } + + override func handleRefresh(_ refreshControl: UIRefreshControl) { + self.ethApiService.getTransactions({ (result) in + switch result { + case .success(let transactions): + self.transactions = transactions + DispatchQueue.main.async { + self.tableView.reloadData() + } + break + case .failure(let error): + self.dialogService.showError(withMessage: "", error: error) + break + } + DispatchQueue.main.async { + self.refreshControl.endRefreshing() + } + }) + } +} + +// MARK: - UITableView Cells +extension ETHTransactionsViewController { + private func configureCell(_ cell: TransactionTableViewCell, for transfer: EthTransaction) { + cell.accountLabel.tintColor = UIColor.adamantPrimary + cell.ammountLabel.tintColor = UIColor.adamantPrimary + cell.dateLabel.tintColor = UIColor.adamantSecondary + cell.avatarImageView.tintColor = UIColor.adamantPrimary + + guard let address = ethApiService.account?.address else { + return + } + if transfer.isOutgoing(address) { + cell.transactionType = .outcome + cell.accountLabel.text = transfer.to + } else { + cell.transactionType = .income + cell.accountLabel.text = transfer.from + } + + cell.ammountLabel.text = transfer.formattedValue() + + cell.dateLabel.text = transfer.date.humanizedDateTime() + } +} + +// MARK: - UITableView +extension ETHTransactionsViewController: UITableViewDataSource, UITableViewDelegate { + func numberOfSections(in tableView: UITableView) -> Int { + return 1 + } + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return transactions.count + } + + func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { + return UIView() + } + + func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + return cellHeight + } + + 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.Transactions.transactionDetails) as? TransactionDetailsViewController else { +// return +// } +// +// controller.transaction = transaction +// navigationController?.pushViewController(controller, animated: true) + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + guard let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, 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 + } +} diff --git a/Adamant/Stories/Transactions/TransactionsRoutes.swift b/Adamant/Stories/Transactions/TransactionsRoutes.swift index 5a240a9a7..0968a5ada 100644 --- a/Adamant/Stories/Transactions/TransactionsRoutes.swift +++ b/Adamant/Stories/Transactions/TransactionsRoutes.swift @@ -11,7 +11,7 @@ import Foundation extension AdamantScene { struct Transactions { static let transactions = AdamantScene(identifier: "TransactionsViewController", factory: { r in - let c = TransactionsViewController(nibName: "TransactionsViewController", bundle: nil) + let c = ADMTransactionsViewController(nibName: "TransactionsViewController", bundle: nil) c.accountService = r.resolve(AccountService.self) c.transfersProvider = r.resolve(TransfersProvider.self) c.dialogService = r.resolve(DialogService.self) @@ -28,6 +28,14 @@ extension AdamantScene { c.router = r.resolve(Router.self) return c }) + + static let ethTransactions = AdamantScene(identifier: "TransactionsViewController", factory: { r in + let c = ETHTransactionsViewController(nibName: "TransactionsViewController", bundle: nil) + c.ethApiService = r.resolve(EthApiServiceProtocol.self) + c.dialogService = r.resolve(DialogService.self) + c.router = r.resolve(Router.self) + return c + }) private init() {} } diff --git a/Adamant/Stories/Transactions/TransactionsViewController.swift b/Adamant/Stories/Transactions/TransactionsViewController.swift index 2f3f77c12..952bb1565 100644 --- a/Adamant/Stories/Transactions/TransactionsViewController.swift +++ b/Adamant/Stories/Transactions/TransactionsViewController.swift @@ -17,21 +17,82 @@ extension String.adamantLocalized { } } +class ADMTransactionsViewController: TransactionsViewController { + // MARK: - Dependencies + var accountService: AccountService! + var transfersProvider: TransfersProvider! + var chatsProvider: ChatsProvider! + var dialogService: DialogService! + var router: Router! + + // MARK: - Properties + var controller: NSFetchedResultsController? + + override func viewDidLoad() { + super.viewDidLoad() + + tableView.dataSource = self + tableView.delegate = self + + if accountService.account != nil { + initFetchedResultController(provider: transfersProvider) + } + + NotificationCenter.default.addObserver(forName: Notification.Name.AdamantAccountService.userLoggedIn, object: nil, queue: nil) { [weak self] notification in + self?.initFetchedResultController(provider: self?.transfersProvider) + } + + NotificationCenter.default.addObserver(forName: Notification.Name.AdamantAccountService.userLoggedOut, object: nil, queue: nil) { [weak self] _ in + self?.initFetchedResultController(provider: nil) + } + } + + /// - Parameter provider: nil to drop and reset + private func initFetchedResultController(provider: TransfersProvider?) { + controller = transfersProvider.transfersController() + controller?.delegate = self + + do { + try controller?.performFetch() + } catch { + print("There was an error performing fetch: \(error)") + controller = nil + } + + tableView.reloadData() + } + + override func handleRefresh(_ refreshControl: UIRefreshControl) { + self.transfersProvider.update { [weak self] (result) in + guard let result = result else { + DispatchQueue.main.async { + refreshControl.endRefreshing() + } + return + } + + switch result { + case .success: + DispatchQueue.main.async { + self?.tableView.reloadData() + } + break + case .failure(let error): + self?.dialogService.showRichError(error: error) + } + + DispatchQueue.main.async { + refreshControl.endRefreshing() + } + } + } +} + class TransactionsViewController: UIViewController { let cellIdentifier = "cell" let cellHeight: CGFloat = 90.0 - - // MARK: - Dependencies - var accountService: AccountService! - var transfersProvider: TransfersProvider! - var dialogService: DialogService! - var stack: CoreDataStack! - var router: Router! - - // MARK: - Properties - var controller: NSFetchedResultsController? - private lazy var refreshControl: UIRefreshControl = { + internal lazy var refreshControl: UIRefreshControl = { let refreshControl = UIRefreshControl() refreshControl.addTarget(self, action: #selector(self.handleRefresh(_:)), @@ -57,22 +118,10 @@ class TransactionsViewController: UIViewController { navigationItem.title = String.adamantLocalized.transactionList.title - if accountService.account != nil { - initFetchedResultController(provider: transfersProvider) - } - tableView.register(UINib.init(nibName: "TransactionTableViewCell", bundle: nil), forCellReuseIdentifier: cellIdentifier) - tableView.dataSource = self - tableView.delegate = self - tableView.refreshControl = refreshControl - - NotificationCenter.default.addObserver(forName: Notification.Name.AdamantAccountService.userLoggedIn, object: nil, queue: nil) { [weak self] notification in - self?.initFetchedResultController(provider: self?.transfersProvider) - } - - NotificationCenter.default.addObserver(forName: Notification.Name.AdamantAccountService.userLoggedOut, object: nil, queue: nil) { [weak self] _ in - self?.initFetchedResultController(provider: nil) - } +// tableView.dataSource = self +// tableView.delegate = self + tableView.addSubview(self.refreshControl) } override func viewWillAppear(_ animated: Bool) { @@ -86,56 +135,8 @@ class TransactionsViewController: UIViewController { } } - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - // TransactionDetails can reset this setting - if #available(iOS 11.0, *) { - navigationController?.navigationBar.prefersLargeTitles = false - } - - markTransfersAsRead() - } - - - /// - Parameter provider: nil to drop and reset - private func initFetchedResultController(provider: TransfersProvider?) { - controller = transfersProvider.transfersController() - controller?.delegate = self - - do { - try controller?.performFetch() - } catch { - print("There was an error performing fetch: \(error)") - controller = nil - } - - tableView.reloadData() - } - - @objc private func handleRefresh(_ refreshControl: UIRefreshControl) { - self.transfersProvider.update { [weak self] (result) in - guard let result = result else { - DispatchQueue.main.async { - refreshControl.endRefreshing() - } - return - } - - switch result { - case .success: - DispatchQueue.main.async { - self?.tableView.reloadData() - } - break - case .failure(let error): - self?.dialogService.showRichError(error: error) - } - - DispatchQueue.main.async { - refreshControl.endRefreshing() - } - } + @objc internal func handleRefresh(_ refreshControl: UIRefreshControl) { + } private func markTransfersAsRead() { @@ -157,7 +158,7 @@ class TransactionsViewController: UIViewController { } // MARK: - UITableView Cells -extension TransactionsViewController { +extension ADMTransactionsViewController { private func configureCell(_ cell: TransactionTableViewCell, for transfer: TransferTransaction) { cell.accountLabel.tintColor = UIColor.adamantPrimary cell.ammountLabel.tintColor = UIColor.adamantPrimary @@ -184,13 +185,13 @@ extension TransactionsViewController { // MARK: - UITableView -extension TransactionsViewController: UITableViewDataSource, UITableViewDelegate { +extension ADMTransactionsViewController: UITableViewDataSource, UITableViewDelegate { func numberOfSections(in tableView: UITableView) -> Int { return 1 } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - if let f = controller?.fetchedObjects { + if let f = controller?.fetchedObjects { return f.count } else { return 0 @@ -312,7 +313,7 @@ extension TransactionsViewController: UITableViewDataSource, UITableViewDelegate } } -extension TransactionsViewController: NSFetchedResultsControllerDelegate { +extension ADMTransactionsViewController: NSFetchedResultsControllerDelegate { func controllerWillChangeContent(_ controller: NSFetchedResultsController) { tableView.beginUpdates() } From 3d8b741d4885abe50639db4815a94a02e3cbf4fa Mon Sep 17 00:00:00 2001 From: Anton B Date: Mon, 25 Jun 2018 19:59:03 +0300 Subject: [PATCH 018/175] Add ETH Transaction Details screen --- Adamant.xcodeproj/project.pbxproj | 4 + .../ETHTransactionDetailsViewController.swift | 121 ++++++++++++++++++ .../ETHTransactionsViewController.swift | 14 +- .../Transactions/TransactionsRoutes.swift | 7 + 4 files changed, 139 insertions(+), 7 deletions(-) create mode 100644 Adamant/Stories/Transactions/ETHTransactionDetailsViewController.swift diff --git a/Adamant.xcodeproj/project.pbxproj b/Adamant.xcodeproj/project.pbxproj index 4cbd53680..ffec60697 100644 --- a/Adamant.xcodeproj/project.pbxproj +++ b/Adamant.xcodeproj/project.pbxproj @@ -29,6 +29,7 @@ 64D05A0220D51632003AD655 /* EthApiServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D05A0120D51632003AD655 /* EthApiServiceProtocol.swift */; }; 64D05A0420D516B4003AD655 /* EthApiService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D05A0320D516B4003AD655 /* EthApiService.swift */; }; 64FA53CD20E1300B006783C9 /* ETHTransactionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64FA53CC20E1300A006783C9 /* ETHTransactionsViewController.swift */; }; + 64FA53CF20E15066006783C9 /* ETHTransactionDetailsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64FA53CE20E15066006783C9 /* ETHTransactionDetailsViewController.swift */; }; E905D39B2048A9BD00DDB504 /* KeychainStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = E905D39A2048A9BD00DDB504 /* KeychainStore.swift */; }; E905D39D204C13B900DDB504 /* SecuredStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = E905D39C204C13B900DDB504 /* SecuredStore.swift */; }; E905D39F204C281400DDB504 /* LoginViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E905D39E204C281400DDB504 /* LoginViewController.swift */; }; @@ -231,6 +232,7 @@ 64D05A0120D51632003AD655 /* EthApiServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EthApiServiceProtocol.swift; sourceTree = ""; }; 64D05A0320D516B4003AD655 /* EthApiService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EthApiService.swift; sourceTree = ""; }; 64FA53CC20E1300A006783C9 /* ETHTransactionsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ETHTransactionsViewController.swift; sourceTree = ""; }; + 64FA53CE20E15066006783C9 /* ETHTransactionDetailsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ETHTransactionDetailsViewController.swift; sourceTree = ""; }; 871009461457F6B7B47D99C5 /* Pods-Adamant.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Adamant.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Adamant/Pods-Adamant.debug.xcconfig"; sourceTree = ""; }; 903AC078A8A55175A05E42D4 /* Pods-Adamant.testing.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Adamant.testing.xcconfig"; path = "Pods/Target Support Files/Pods-Adamant/Pods-Adamant.testing.xcconfig"; sourceTree = ""; }; C107D5CB65B4D728B9D97C0F /* Pods_Adamant.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Adamant.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -684,6 +686,7 @@ E9E7CDC12003F5A400DFC4DB /* TransactionsViewController.swift */, 64FA53CC20E1300A006783C9 /* ETHTransactionsViewController.swift */, E94E7B0B205D5E4A0042B639 /* TransactionsViewController.xib */, + 64FA53CE20E15066006783C9 /* ETHTransactionDetailsViewController.swift */, E9EC34132005178500C0E546 /* TransactionDetailsViewController.swift */, E94E7B0D205D5EA80042B639 /* TransactionDetailsViewController.xib */, E9E7CDC52003F6D200DFC4DB /* TransactionTableViewCell.swift */, @@ -1247,6 +1250,7 @@ E948E03B20235E2300975D6B /* SettingsRoutes.swift in Sources */, E9CAE8D82018ACA700345E76 /* AdamantApi+Transfers.swift in Sources */, E9E7CDCC20040FDC00DFC4DB /* TransactionType.swift in Sources */, + 64FA53CF20E15066006783C9 /* ETHTransactionDetailsViewController.swift in Sources */, E95F85852008CB3A0070534A /* ChatListViewController.swift in Sources */, E91947AC20001A9A001362F8 /* ApiService.swift in Sources */, E9150B9F2066DA210065A985 /* MessageTransaction+CoreDataClass.swift in Sources */, diff --git a/Adamant/Stories/Transactions/ETHTransactionDetailsViewController.swift b/Adamant/Stories/Transactions/ETHTransactionDetailsViewController.swift new file mode 100644 index 000000000..bc43f222a --- /dev/null +++ b/Adamant/Stories/Transactions/ETHTransactionDetailsViewController.swift @@ -0,0 +1,121 @@ +// +// ETHTransactionDetailsViewController.swift +// Adamant +// +// Created by Anton Boyarkin on 25/06/2018. +// Copyright © 2018 Adamant. All rights reserved. +// + +import UIKit +import Eureka +import web3swift +import BigInt + +class ETHTransactionDetailsViewController: FormViewController { + // MARK: - Rows + fileprivate enum Row: Int { + case transactionNumber = 0 + case from + case to + case date + case amount + case fee + case confirmations + case block + + var localized: String { + switch self { + case .transactionNumber: return NSLocalizedString("TransactionDetailsScene.Row.Id", comment: "Transaction details: Id row.") + case .from: return NSLocalizedString("TransactionDetailsScene.Row.From", comment: "Transaction details: sender row.") + case .to: return NSLocalizedString("TransactionDetailsScene.Row.To", comment: "Transaction details: recipient row.") + case .date: return NSLocalizedString("TransactionDetailsScene.Row.Date", comment: "Transaction details: date row.") + case .amount: return NSLocalizedString("TransactionDetailsScene.Row.Amount", comment: "Transaction details: amount row.") + case .fee: return NSLocalizedString("TransactionDetailsScene.Row.Fee", comment: "Transaction details: fee row.") + case .confirmations: return NSLocalizedString("TransactionDetailsScene.Row.Confirmations", comment: "Transaction details: confirmations row.") + case .block: return NSLocalizedString("TransactionDetailsScene.Row.Block", comment: "Transaction details: Block id row.") + } + } + } + + // MARK: - Dependencies + var dialogService: DialogService! + var ethApiService: EthApiServiceProtocol! + + // MARK: - Properties + var transaction: EthTransaction? + + override func viewDidLoad() { + super.viewDidLoad() + + guard let transaction = transaction else { + return + } + + guard let feeString = Web3.Utils.formatToEthereumUnits(BigUInt(EthApiService.defaultGasPrice * EthApiService.transferGas), toUnits: .eth, decimals: 8), let fee = Double(feeString) else { + return + } + + let currencyFormatter = NumberFormatter() + currencyFormatter.numberStyle = .decimal + currencyFormatter.roundingMode = .floor + currencyFormatter.positiveFormat = "#.######## ETH" + + // MARK: - Transfer section + form +++ Section() + + <<< TextRow() { + $0.disabled = true + $0.title = Row.transactionNumber.localized + $0.value = transaction.hash + } + <<< TextRow() { + $0.disabled = true + $0.title = Row.from.localized + $0.value = transaction.from + } + <<< TextRow() { + $0.disabled = true + $0.title = Row.to.localized + $0.value = transaction.to + } + <<< DateRow() { + $0.disabled = true + $0.title = Row.date.localized + $0.value = transaction.date + } + + <<< TextRow() { + $0.disabled = true + $0.title = Row.amount.localized +// $0.formatter = currencyFormatter + $0.value = transaction.formattedValue() + } + + <<< DecimalRow() { + $0.title = Row.fee.localized + $0.value = fee + $0.disabled = true + $0.formatter = currencyFormatter + } + <<< TextRow() { + $0.title = Row.confirmations.localized + $0.value = transaction.confirmations + $0.disabled = true + } + + // MARK: - UI + navigationAccessoryView.tintColor = UIColor.adamantPrimary + + for row in form.allRows { + row.baseCell?.textLabel?.font = UIFont.adamantPrimary(size: 17) + row.baseCell?.textLabel?.textColor = UIColor.adamantPrimary + row.baseCell?.tintColor = UIColor.adamantPrimary + } + } + + override func didReceiveMemoryWarning() { + super.didReceiveMemoryWarning() + // Dispose of any resources that can be recreated. + } + +} diff --git a/Adamant/Stories/Transactions/ETHTransactionsViewController.swift b/Adamant/Stories/Transactions/ETHTransactionsViewController.swift index 4d536f247..44c4c9def 100644 --- a/Adamant/Stories/Transactions/ETHTransactionsViewController.swift +++ b/Adamant/Stories/Transactions/ETHTransactionsViewController.swift @@ -95,14 +95,14 @@ extension ETHTransactionsViewController: UITableViewDataSource, UITableViewDeleg func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: true) -// let transaction = transactions[indexPath.row] + let transaction = transactions[indexPath.row] -// guard let controller = router.get(scene: AdamantScene.Transactions.transactionDetails) as? TransactionDetailsViewController else { -// return -// } -// -// controller.transaction = transaction -// navigationController?.pushViewController(controller, animated: true) + guard let controller = router.get(scene: AdamantScene.Transactions.ethTransactionDetails) as? ETHTransactionDetailsViewController else { + return + } + + controller.transaction = transaction + navigationController?.pushViewController(controller, animated: true) } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { diff --git a/Adamant/Stories/Transactions/TransactionsRoutes.swift b/Adamant/Stories/Transactions/TransactionsRoutes.swift index 0968a5ada..7010f126d 100644 --- a/Adamant/Stories/Transactions/TransactionsRoutes.swift +++ b/Adamant/Stories/Transactions/TransactionsRoutes.swift @@ -36,6 +36,13 @@ extension AdamantScene { c.router = r.resolve(Router.self) return c }) + + static let ethTransactionDetails = AdamantScene(identifier: "TransactionDetailsViewController", factory: { r in + let c = ETHTransactionDetailsViewController() + c.dialogService = r.resolve(DialogService.self) + c.ethApiService = r.resolve(EthApiServiceProtocol.self) + return c + }) private init() {} } From 815d54c28e24148ef9cc451ef1e1d59048e10bbf Mon Sep 17 00:00:00 2001 From: Anton B Date: Tue, 26 Jun 2018 15:14:30 +0300 Subject: [PATCH 019/175] Unify transaction details Add Transaction details protocol Unify ADM & ETH Transaction details Unify transaction details screen --- Adamant.xcodeproj/project.pbxproj | 8 +- .../EthApiService/EthApiService.swift | 170 ++++++- .../Chats/ChatViewController+MessageKit.swift | 2 +- ...BaseTransactionDetailsViewController.swift | 474 ++++++++++++++++++ .../ETHTransactionDetailsViewController.swift | 121 ----- .../ETHTransactionsViewController.swift | 2 +- .../Transactions/TransactionsRoutes.swift | 7 +- .../TransactionsViewController.swift | 99 ++-- 8 files changed, 695 insertions(+), 188 deletions(-) create mode 100644 Adamant/Stories/Transactions/BaseTransactionDetailsViewController.swift delete mode 100644 Adamant/Stories/Transactions/ETHTransactionDetailsViewController.swift diff --git a/Adamant.xcodeproj/project.pbxproj b/Adamant.xcodeproj/project.pbxproj index ffec60697..65a137a4c 100644 --- a/Adamant.xcodeproj/project.pbxproj +++ b/Adamant.xcodeproj/project.pbxproj @@ -29,7 +29,7 @@ 64D05A0220D51632003AD655 /* EthApiServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D05A0120D51632003AD655 /* EthApiServiceProtocol.swift */; }; 64D05A0420D516B4003AD655 /* EthApiService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D05A0320D516B4003AD655 /* EthApiService.swift */; }; 64FA53CD20E1300B006783C9 /* ETHTransactionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64FA53CC20E1300A006783C9 /* ETHTransactionsViewController.swift */; }; - 64FA53CF20E15066006783C9 /* ETHTransactionDetailsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64FA53CE20E15066006783C9 /* ETHTransactionDetailsViewController.swift */; }; + 64FA53D120E24942006783C9 /* BaseTransactionDetailsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64FA53D020E24941006783C9 /* BaseTransactionDetailsViewController.swift */; }; E905D39B2048A9BD00DDB504 /* KeychainStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = E905D39A2048A9BD00DDB504 /* KeychainStore.swift */; }; E905D39D204C13B900DDB504 /* SecuredStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = E905D39C204C13B900DDB504 /* SecuredStore.swift */; }; E905D39F204C281400DDB504 /* LoginViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E905D39E204C281400DDB504 /* LoginViewController.swift */; }; @@ -232,7 +232,7 @@ 64D05A0120D51632003AD655 /* EthApiServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EthApiServiceProtocol.swift; sourceTree = ""; }; 64D05A0320D516B4003AD655 /* EthApiService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EthApiService.swift; sourceTree = ""; }; 64FA53CC20E1300A006783C9 /* ETHTransactionsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ETHTransactionsViewController.swift; sourceTree = ""; }; - 64FA53CE20E15066006783C9 /* ETHTransactionDetailsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ETHTransactionDetailsViewController.swift; sourceTree = ""; }; + 64FA53D020E24941006783C9 /* BaseTransactionDetailsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseTransactionDetailsViewController.swift; sourceTree = ""; }; 871009461457F6B7B47D99C5 /* Pods-Adamant.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Adamant.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Adamant/Pods-Adamant.debug.xcconfig"; sourceTree = ""; }; 903AC078A8A55175A05E42D4 /* Pods-Adamant.testing.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Adamant.testing.xcconfig"; path = "Pods/Target Support Files/Pods-Adamant/Pods-Adamant.testing.xcconfig"; sourceTree = ""; }; C107D5CB65B4D728B9D97C0F /* Pods_Adamant.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Adamant.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -686,7 +686,7 @@ E9E7CDC12003F5A400DFC4DB /* TransactionsViewController.swift */, 64FA53CC20E1300A006783C9 /* ETHTransactionsViewController.swift */, E94E7B0B205D5E4A0042B639 /* TransactionsViewController.xib */, - 64FA53CE20E15066006783C9 /* ETHTransactionDetailsViewController.swift */, + 64FA53D020E24941006783C9 /* BaseTransactionDetailsViewController.swift */, E9EC34132005178500C0E546 /* TransactionDetailsViewController.swift */, E94E7B0D205D5EA80042B639 /* TransactionDetailsViewController.xib */, E9E7CDC52003F6D200DFC4DB /* TransactionTableViewCell.swift */, @@ -1250,7 +1250,6 @@ E948E03B20235E2300975D6B /* SettingsRoutes.swift in Sources */, E9CAE8D82018ACA700345E76 /* AdamantApi+Transfers.swift in Sources */, E9E7CDCC20040FDC00DFC4DB /* TransactionType.swift in Sources */, - 64FA53CF20E15066006783C9 /* ETHTransactionDetailsViewController.swift in Sources */, E95F85852008CB3A0070534A /* ChatListViewController.swift in Sources */, E91947AC20001A9A001362F8 /* ApiService.swift in Sources */, E9150B9F2066DA210065A985 /* MessageTransaction+CoreDataClass.swift in Sources */, @@ -1283,6 +1282,7 @@ E93D7AC02052CF63005D19DC /* AdamantNotificationService.swift in Sources */, E93B0D762028B28E00126346 /* AdamantChatsProvider.swift in Sources */, E983AE2320E6568C00497E1A /* Wallet.swift in Sources */, + 64FA53D120E24942006783C9 /* BaseTransactionDetailsViewController.swift in Sources */, E90A4943204C5ED6009F6A65 /* EurekaPassphraseRow.swift in Sources */, E965A53420B833A00041A3EA /* StateAsset.swift in Sources */, E9393FA82055C92700EE6F30 /* Decimal+adamant.swift in Sources */, diff --git a/Adamant/Services/EthApiService/EthApiService.swift b/Adamant/Services/EthApiService/EthApiService.swift index e6b05a212..63e8313be 100644 --- a/Adamant/Services/EthApiService/EthApiService.swift +++ b/Adamant/Services/EthApiService/EthApiService.swift @@ -397,6 +397,99 @@ class EthApiService: EthApiServiceProtocol { } } + +/// A standard protocol representing a Transaction details. +protocol TransactionDetailsProtocol { + + /// The identifier of the transaction. + var id: String { get } + + /// The sender of the transaction. + var senderAddress: String { get } + + /// The reciver of the transaction. + var recipientAddress: String { get } + + /// The date the transaction was sent. + var sentDate: Date { get } + + /// The amount of currency that was sent. + var amountValue: Double { get } + + /// The amount of fee that taken for transaction process. + var feeValue: Double { get } + + /// The confirmations of the transaction. + var confirmationsValue: String { get } + + /// The block of the transaction. + var block: String { get } + + /// The show explorer button. + var showGoToExplorer: Bool { get } + + /// The explorer url. + var explorerUrl: URL? { get } + + /// The show go to button. + var showGoToChat: Bool { get } + + /// The show go to button. + var chatroom: Chatroom? { get } + + /// The currency of the transaction. + var currencyCode: String { get } + +} + +extension TransactionDetailsProtocol { + + var haveChatroom: Bool { + if let chatroom = self.chatroom, let transactions = chatroom.transactions { + let messeges = transactions.first (where: { (object) -> Bool in + return !(object is TransferTransaction) + }) + + return (messeges != nil) + } + + return false + } + + var currencyFormatter: NumberFormatter { + let formatter = NumberFormatter() + formatter.numberStyle = .decimal + formatter.roundingMode = .floor + formatter.positiveFormat = "#.######## \(currencyCode)" + + return formatter + } + + func getSummary() -> String { + return """ + Transaction #\(id) + + Summary + Sender: \(senderAddress) + Recipient: \(recipientAddress) + Date: \(DateFormatter.localizedString(from: sentDate, dateStyle: .short, timeStyle: .medium)) + Amount: \(formattedAmount()) + Fee: \(formattedFee()) + Confirmations: \(String(confirmationsValue)) + Block: \(block) + URL: \(explorerUrl?.absoluteString ?? "") + """ + } + + func formattedAmount() -> String { + return currencyFormatter.string(from: NSNumber(value: amountValue)) ?? "" + } + + func formattedFee() -> String { + return currencyFormatter.string(from: NSNumber(value: feeValue)) ?? "" + } +} + //MARK: - Decodable Transaction structures struct Response: Decodable { enum CodingKeys: String, CodingKey { @@ -430,6 +523,7 @@ struct EthTransaction: Decodable { case confirmations case isError case receiptStatus = "txreceipt_status" + case blockNumber } let date: Date @@ -439,9 +533,10 @@ struct EthTransaction: Decodable { let to: String let gasUsed: BigUInt let gasPrice: BigUInt - let confirmations: String + let confirmationsValue: String let isError: Bool - let receiptStatus: Int + let receiptStatus: TransactionReceiptStatus + let blockNumber: UInt init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) @@ -454,10 +549,11 @@ struct EthTransaction: Decodable { to = (try? container.decode(String.self, forKey: .to)) ?? "" gasUsed = BigUInt((try? container.decode(String.self, forKey: .gasUsed)) ?? "0") ?? BigUInt(0) gasPrice = BigUInt((try? container.decode(String.self, forKey: .gasPrice)) ?? "0") ?? BigUInt(0) - confirmations = (try? container.decode(String.self, forKey: .confirmations)) ?? "0" + confirmationsValue = (try? container.decode(String.self, forKey: .confirmations)) ?? "0" let isErrorStatus = Int((try? container.decode(String.self, forKey: .isError)) ?? "0") ?? 0 isError = isErrorStatus == 1 ? true : false - receiptStatus = Int((try? container.decode(String.self, forKey: .receiptStatus)) ?? "0") ?? 0 + receiptStatus = (try? container.decode(TransactionReceiptStatus.self, forKey: .receiptStatus)) ?? .unknown + blockNumber = UInt((try? container.decode(String.self, forKey: .blockNumber)) ?? "0") ?? 0 } func isOutgoing(_ address: String) -> Bool { @@ -476,6 +572,72 @@ struct EthTransaction: Decodable { } } +extension EthTransaction: TransactionDetailsProtocol { + + var id: String { + return self.hash + } + + var senderAddress: String { + return self.from + } + + var recipientAddress: String { + return self.to + } + + var sentDate: Date { + return self.date + } + + var amountValue: Double { + guard let string = Web3.Utils.formatToEthereumUnits(value, toUnits: .eth, decimals: 8), let value = Double(string) else { + return 0 + } + + return value + } + + var feeValue: Double { + guard let string = Web3.Utils.formatToEthereumUnits((self.gasPrice * self.gasUsed), toUnits: .eth, decimals: 8), let value = Double(string) else { + return 0 + } + + return value + } + + var block: String { + return "\(self.blockNumber)" + } + + var showGoToExplorer: Bool { + return false + } + + var explorerUrl: URL? { + return nil + } + + var showGoToChat: Bool { + return false + } + + var chatroom: Chatroom? { + return nil + } + + var currencyCode: String { + return "ETH" + } + +} + +enum TransactionReceiptStatus: String, Decodable { + case fail = "0" + case pass = "1" + case unknown +} + /* { "blockNumber":"3455267", diff --git a/Adamant/Stories/Chats/ChatViewController+MessageKit.swift b/Adamant/Stories/Chats/ChatViewController+MessageKit.swift index caaf50758..8904e34b9 100644 --- a/Adamant/Stories/Chats/ChatViewController+MessageKit.swift +++ b/Adamant/Stories/Chats/ChatViewController+MessageKit.swift @@ -187,7 +187,7 @@ extension ChatViewController: MessageCellDelegate { switch message { case let transfer as TransferTransaction: // MARK: Show transfer details - guard let vc = router.get(scene: AdamantScene.Transactions.transactionDetails) as? TransactionDetailsViewController else { + guard let vc = router.get(scene: AdamantScene.Transactions.transactionDetails) as? BaseTransactionDetailsViewController else { fatalError("Can't get TransactionDetails scene") } diff --git a/Adamant/Stories/Transactions/BaseTransactionDetailsViewController.swift b/Adamant/Stories/Transactions/BaseTransactionDetailsViewController.swift new file mode 100644 index 000000000..101a26a94 --- /dev/null +++ b/Adamant/Stories/Transactions/BaseTransactionDetailsViewController.swift @@ -0,0 +1,474 @@ +// +// BaseTransactionDetailsViewController.swift +// Adamant +// +// Created by Anton Boyarkin on 25/06/2018. +// Copyright © 2018 Adamant. All rights reserved. +// + +import UIKit +import Eureka +import web3swift +import BigInt +import SafariServices + +class BaseTransactionDetailsViewController: FormViewController { + // MARK: - Rows + fileprivate enum Row: Int { + case transactionNumber = 0 + case from + case to + case date + case amount + case fee + case confirmations + case block + case openInExplorer + case openChat + + var tag: String { + switch self { + case .transactionNumber: return "id" + case .from: return "from" + case .to: return "to" + case .date: return "date" + case .amount: return "amount" + case .fee: return "fee" + case .confirmations: return "confirmations" + case .block: return "block" + case .openInExplorer: return "openInExplorer" + case .openChat: return "openChat" + } + } + + var localized: String { + switch self { + case .transactionNumber: return NSLocalizedString("TransactionDetailsScene.Row.Id", comment: "Transaction details: Id row.") + case .from: return NSLocalizedString("TransactionDetailsScene.Row.From", comment: "Transaction details: sender row.") + case .to: return NSLocalizedString("TransactionDetailsScene.Row.To", comment: "Transaction details: recipient row.") + case .date: return NSLocalizedString("TransactionDetailsScene.Row.Date", comment: "Transaction details: date row.") + case .amount: return NSLocalizedString("TransactionDetailsScene.Row.Amount", comment: "Transaction details: amount row.") + case .fee: return NSLocalizedString("TransactionDetailsScene.Row.Fee", comment: "Transaction details: fee row.") + case .confirmations: return NSLocalizedString("TransactionDetailsScene.Row.Confirmations", comment: "Transaction details: confirmations row.") + case .block: return NSLocalizedString("TransactionDetailsScene.Row.Block", comment: "Transaction details: Block id row.") + case .openInExplorer: return NSLocalizedString("TransactionDetailsScene.Row.Explorer", comment: "Transaction details: 'Open transaction in explorer' row.") + case .openChat: return "" + } + } + } + + // MARK: - Dependencies + var dialogService: DialogService! + + // MARK: - Properties + var transaction: TransactionDetailsProtocol? + + override func viewDidLoad() { + super.viewDidLoad() + + navigationItem.title = String.adamantLocalized.transactionDetails.title + navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .action, target: self, action: #selector(share)) + + // MARK: - Transfer section + form +++ Section() + + <<< TextRow() { + $0.disabled = true + $0.tag = Row.transactionNumber.tag + $0.title = Row.transactionNumber.localized + }.cellUpdate { cell, row in + self.updateCell(cell) + }.onCellSelection({ (cell, row) in + if let text = row.value { + self.shareValue(text) + } + }) + + <<< TextRow() { + $0.disabled = true + $0.tag = Row.from.tag + $0.title = Row.from.localized + }.cellUpdate { cell, row in + self.updateCell(cell) + }.onCellSelection({ (cell, row) in + if let text = row.value { + self.shareValue(text) + } + }) + + <<< TextRow() { + $0.disabled = true + $0.tag = Row.to.tag + $0.title = Row.to.localized + }.cellUpdate { cell, row in + self.updateCell(cell) + }.onCellSelection({ (cell, row) in + if let text = row.value { + self.shareValue(text) + } + }) + + <<< DateRow() { + $0.disabled = true + $0.tag = Row.date.tag + $0.title = Row.date.localized + }.cellUpdate { cell, row in + self.updateCell(cell) + }.onCellSelection({ (cell, row) in + if let text = cell.detailTextLabel?.text { + self.shareValue(text) + } + }) + + <<< DecimalRow() { + $0.disabled = true + $0.tag = Row.amount.tag + $0.title = Row.amount.localized + }.cellUpdate { cell, row in + self.updateCell(cell) + }.onCellSelection({ (cell, row) in + if let text = row.displayValueFor?(row.value) { + self.shareValue(text) + } + }) + + <<< DecimalRow() { + $0.disabled = true + $0.tag = Row.fee.tag + $0.title = Row.fee.localized + }.cellUpdate { cell, row in + self.updateCell(cell) + }.onCellSelection({ (cell, row) in + if let text = row.displayValueFor?(row.value) { + self.shareValue(text) + } + }) + + <<< TextRow() { + $0.disabled = true + $0.tag = Row.confirmations.tag + $0.title = Row.confirmations.localized + }.cellUpdate { cell, row in + self.updateCell(cell) + }.onCellSelection({ (cell, row) in + if let text = row.value { + self.shareValue(text) + } + }) + + <<< TextRow() { + $0.disabled = true + $0.tag = Row.block.tag + $0.title = Row.block.localized + }.cellUpdate { cell, row in + self.updateCell(cell) + }.onCellSelection({ (cell, row) in + if let text = row.value { + self.shareValue(text) + } + }) + + <<< LabelRow() { + $0.hidden = true + $0.tag = Row.openInExplorer.tag + $0.title = Row.openInExplorer.localized + } + .cellSetup({ (cell, _) in + cell.selectionStyle = .gray + }) + .cellUpdate({ (cell, _) in + if let label = cell.textLabel { + label.font = UIFont.adamantPrimary(size: 17) + label.textColor = UIColor.adamantPrimary + } + + cell.accessoryType = .disclosureIndicator + }) + .onCellSelection({ [weak self] (_, row) in + // TODO: + if let url = self?.transaction?.explorerUrl { + let safari = SFSafariViewController(url: url) + safari.preferredControlTintColor = UIColor.adamantPrimary + self?.present(safari, animated: true, completion: nil) + } + }) + + <<< LabelRow() { + $0.hidden = true + $0.tag = Row.openChat.tag + $0.title = Row.openChat.localized + } + .cellSetup({ (cell, _) in + cell.selectionStyle = .gray + }) + .cellUpdate({ (cell, _) in + if let label = cell.textLabel { + label.font = UIFont.adamantPrimary(size: 17) + label.textColor = UIColor.adamantPrimary + } + + cell.accessoryType = .disclosureIndicator + }) + .onCellSelection({ [weak self] (_, row) in + self?.goToChat() + }) + + + // MARK: - UI + navigationAccessoryView.tintColor = UIColor.adamantPrimary + + guard let transaction = transaction else { + return + } + + updateDetails(with: transaction) + } + + override func didReceiveMemoryWarning() { + super.didReceiveMemoryWarning() + // Dispose of any resources that can be recreated. + } + + func set(transaction: TransactionDetailsProtocol) { + self.transaction = transaction + updateDetails(with: transaction) + } + + private func updateDetails(with transaction: TransactionDetailsProtocol) { + let currencyFormatter = NumberFormatter() + currencyFormatter.numberStyle = .decimal + currencyFormatter.roundingMode = .floor + currencyFormatter.positiveFormat = "#.########" + currencyFormatter.positiveSuffix = " \(transaction.currencyCode)" + + if let row: TextRow = self.form.rowBy(tag: Row.transactionNumber.tag) { + row.value = transaction.id + row.reload() + } + + if let row: TextRow = self.form.rowBy(tag: Row.from.tag) { + row.value = transaction.senderAddress + row.reload() + } + + if let row: TextRow = self.form.rowBy(tag: Row.to.tag) { + row.value = transaction.recipientAddress + row.reload() + } + + if let row: DateRow = self.form.rowBy(tag: Row.date.tag) { + row.value = transaction.sentDate + row.reload() + } + + if let row: DecimalRow = self.form.rowBy(tag: Row.amount.tag) { + row.value = transaction.amountValue + row.formatter = currencyFormatter + row.reload() + } + + if let row: DecimalRow = self.form.rowBy(tag: Row.fee.tag) { + row.value = transaction.feeValue + row.formatter = currencyFormatter + row.reload() + } + + if let row: TextRow = self.form.rowBy(tag: Row.confirmations.tag) { + row.value = transaction.confirmationsValue + row.reload() + } + + if let row: TextRow = self.form.rowBy(tag: Row.block.tag) { + row.value = transaction.block + row.reload() + } + + if let row: LabelRow = self.form.rowBy(tag: Row.openInExplorer.tag) { + row.hidden = transaction.showGoToExplorer ? false : true + row.reload() + row.evaluateHidden() + } + + if let row: LabelRow = self.form.rowBy(tag: Row.openChat.tag) { + row.hidden = transaction.showGoToChat ? false : true + row.title = (transaction.haveChatroom) ? String.adamantLocalized.transactionList.toChat : String.adamantLocalized.transactionList.startChat + row.reload() + row.evaluateHidden() + } + } + + // MARK: - Actions + + @objc func share(_ sender: Any) { + guard let transaction = transaction else { + return + } + + let alert = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) + alert.addAction(UIAlertAction(title: String.adamantLocalized.alert.cancel, style: .cancel, handler: nil)) + + if let url = transaction.explorerUrl { + // URL + alert.addAction(UIAlertAction(title: String.adamantLocalized.alert.exportUrlButton, style: .default) { [weak self] _ in + let alert = UIActivityViewController(activityItems: [url], applicationActivities: nil) + self?.present(alert, animated: true, completion: nil) + }) + } + + // Description + alert.addAction(UIAlertAction(title: String.adamantLocalized.alert.exportSummaryButton, style: .default, handler: { [weak self] _ in + let text = transaction.getSummary() + let alert = UIActivityViewController(activityItems: [text], applicationActivities: nil) + self?.present(alert, animated: true, completion: nil) + })) + + present(alert, animated: true, completion: nil) + } + + func shareValue( _ value: String) { + dialogService.presentShareAlertFor(string: value, + types: [.copyToPasteboard, .share], + excludedActivityTypes: nil, + animated: true, completion: nil) + } + + func goToChat() { + + } + + // MARK: - Privare tools + private func updateCell(_ cell: BaseCell) { + cell.textLabel?.textColor = UIColor.adamantPrimary + cell.detailTextLabel?.textColor = UIColor.adamantSecondary + + let font = UIFont.adamantPrimary(size: 17) + cell.textLabel?.font = font + cell.detailTextLabel?.font = font + } +} + +class ADMTransactionDetailsViewController: BaseTransactionDetailsViewController { + + // MARK: - Dependencies + var accountService: AccountService! + var transfersProvider: TransfersProvider! + var router: Router! + + // MARK: - Properties + private let autoupdateInterval: TimeInterval = 5.0 + + weak var timer: Timer? + + // MARK: - Lifecycle + + override func viewDidLoad() { + super.viewDidLoad() + + startUpdate() + } + + deinit { + stopUpdate() + } + + override func goToChat() { + guard let vc = self.router.get(scene: AdamantScene.Chats.chat) as? ChatViewController else { + dialogService.showError(withMessage: "TransactionDetailsViewController: Failed to get ChatViewController", error: nil) + return + } + + guard let chatroom = transaction?.chatroom else { + dialogService.showError(withMessage: "TransactionDetailsViewController: Failed to get chatroom for transaction.", error: nil) + return + } + + guard let account = self.accountService.account else { + dialogService.showError(withMessage: "TransactionDetailsViewController: User not logged.", error: nil) + return + } + + vc.account = account + vc.chatroom = chatroom + vc.hidesBottomBarWhenPushed = true + + if let nav = self.navigationController { + nav.pushViewController(vc, animated: true) + } else { + self.present(vc, animated: true) + } + } + + // MARK: - Autoupdate + + func startUpdate() { + timer?.invalidate() + timer = Timer.scheduledTimer(withTimeInterval: autoupdateInterval, repeats: true) { [weak self] _ in + guard let id = self?.transaction?.id else { + return + } + + self?.transfersProvider.refreshTransfer(id: id) { result in + switch result { + case .success: + DispatchQueue.main.async { + self?.tableView.reloadData() + } + + case .failure: + return + } + } + } + } + + func stopUpdate() { + timer?.invalidate() + } +} + +extension TransferTransaction: TransactionDetailsProtocol { + var id: String { + return self.transactionId ?? "" + } + + var senderAddress: String { + return self.senderId ?? "" + } + + var recipientAddress: String { + return self.recipientId ?? "" + } + + var amountValue: Double { + return self.amount?.doubleValue ?? 0 + } + + var feeValue: Double { + return self.fee?.doubleValue ?? 0 + } + + var confirmationsValue: String { + return "\(self.confirmations)" + } + + var block: String { + return self.blockId ?? "" + } + + var showGoToExplorer: Bool { + return true + } + + var explorerUrl: URL? { + return URL(string: "https://explorer.adamant.im/tx/\(id)") + } + + var showGoToChat: Bool { + return true + } + + var currencyCode: String { + return "ADM" + } + +} diff --git a/Adamant/Stories/Transactions/ETHTransactionDetailsViewController.swift b/Adamant/Stories/Transactions/ETHTransactionDetailsViewController.swift deleted file mode 100644 index bc43f222a..000000000 --- a/Adamant/Stories/Transactions/ETHTransactionDetailsViewController.swift +++ /dev/null @@ -1,121 +0,0 @@ -// -// ETHTransactionDetailsViewController.swift -// Adamant -// -// Created by Anton Boyarkin on 25/06/2018. -// Copyright © 2018 Adamant. All rights reserved. -// - -import UIKit -import Eureka -import web3swift -import BigInt - -class ETHTransactionDetailsViewController: FormViewController { - // MARK: - Rows - fileprivate enum Row: Int { - case transactionNumber = 0 - case from - case to - case date - case amount - case fee - case confirmations - case block - - var localized: String { - switch self { - case .transactionNumber: return NSLocalizedString("TransactionDetailsScene.Row.Id", comment: "Transaction details: Id row.") - case .from: return NSLocalizedString("TransactionDetailsScene.Row.From", comment: "Transaction details: sender row.") - case .to: return NSLocalizedString("TransactionDetailsScene.Row.To", comment: "Transaction details: recipient row.") - case .date: return NSLocalizedString("TransactionDetailsScene.Row.Date", comment: "Transaction details: date row.") - case .amount: return NSLocalizedString("TransactionDetailsScene.Row.Amount", comment: "Transaction details: amount row.") - case .fee: return NSLocalizedString("TransactionDetailsScene.Row.Fee", comment: "Transaction details: fee row.") - case .confirmations: return NSLocalizedString("TransactionDetailsScene.Row.Confirmations", comment: "Transaction details: confirmations row.") - case .block: return NSLocalizedString("TransactionDetailsScene.Row.Block", comment: "Transaction details: Block id row.") - } - } - } - - // MARK: - Dependencies - var dialogService: DialogService! - var ethApiService: EthApiServiceProtocol! - - // MARK: - Properties - var transaction: EthTransaction? - - override func viewDidLoad() { - super.viewDidLoad() - - guard let transaction = transaction else { - return - } - - guard let feeString = Web3.Utils.formatToEthereumUnits(BigUInt(EthApiService.defaultGasPrice * EthApiService.transferGas), toUnits: .eth, decimals: 8), let fee = Double(feeString) else { - return - } - - let currencyFormatter = NumberFormatter() - currencyFormatter.numberStyle = .decimal - currencyFormatter.roundingMode = .floor - currencyFormatter.positiveFormat = "#.######## ETH" - - // MARK: - Transfer section - form +++ Section() - - <<< TextRow() { - $0.disabled = true - $0.title = Row.transactionNumber.localized - $0.value = transaction.hash - } - <<< TextRow() { - $0.disabled = true - $0.title = Row.from.localized - $0.value = transaction.from - } - <<< TextRow() { - $0.disabled = true - $0.title = Row.to.localized - $0.value = transaction.to - } - <<< DateRow() { - $0.disabled = true - $0.title = Row.date.localized - $0.value = transaction.date - } - - <<< TextRow() { - $0.disabled = true - $0.title = Row.amount.localized -// $0.formatter = currencyFormatter - $0.value = transaction.formattedValue() - } - - <<< DecimalRow() { - $0.title = Row.fee.localized - $0.value = fee - $0.disabled = true - $0.formatter = currencyFormatter - } - <<< TextRow() { - $0.title = Row.confirmations.localized - $0.value = transaction.confirmations - $0.disabled = true - } - - // MARK: - UI - navigationAccessoryView.tintColor = UIColor.adamantPrimary - - for row in form.allRows { - row.baseCell?.textLabel?.font = UIFont.adamantPrimary(size: 17) - row.baseCell?.textLabel?.textColor = UIColor.adamantPrimary - row.baseCell?.tintColor = UIColor.adamantPrimary - } - } - - override func didReceiveMemoryWarning() { - super.didReceiveMemoryWarning() - // Dispose of any resources that can be recreated. - } - -} diff --git a/Adamant/Stories/Transactions/ETHTransactionsViewController.swift b/Adamant/Stories/Transactions/ETHTransactionsViewController.swift index 44c4c9def..eee71037a 100644 --- a/Adamant/Stories/Transactions/ETHTransactionsViewController.swift +++ b/Adamant/Stories/Transactions/ETHTransactionsViewController.swift @@ -97,7 +97,7 @@ extension ETHTransactionsViewController: UITableViewDataSource, UITableViewDeleg let transaction = transactions[indexPath.row] - guard let controller = router.get(scene: AdamantScene.Transactions.ethTransactionDetails) as? ETHTransactionDetailsViewController else { + guard let controller = router.get(scene: AdamantScene.Transactions.ethTransactionDetails) as? BaseTransactionDetailsViewController else { return } diff --git a/Adamant/Stories/Transactions/TransactionsRoutes.swift b/Adamant/Stories/Transactions/TransactionsRoutes.swift index 7010f126d..d483a45d0 100644 --- a/Adamant/Stories/Transactions/TransactionsRoutes.swift +++ b/Adamant/Stories/Transactions/TransactionsRoutes.swift @@ -21,7 +21,7 @@ extension AdamantScene { }) static let transactionDetails = AdamantScene(identifier: "TransactionDetailsViewController", factory: { r in - let c = TransactionDetailsViewController(nibName: "TransactionDetailsViewController", bundle: nil) + let c = ADMTransactionDetailsViewController() c.accountService = r.resolve(AccountService.self) c.dialogService = r.resolve(DialogService.self) c.transfersProvider = r.resolve(TransfersProvider.self) @@ -37,10 +37,9 @@ extension AdamantScene { return c }) - static let ethTransactionDetails = AdamantScene(identifier: "TransactionDetailsViewController", factory: { r in - let c = ETHTransactionDetailsViewController() + static let ethTransactionDetails = AdamantScene(identifier: "BaseTransactionDetailsViewController", factory: { r in + let c = BaseTransactionDetailsViewController() c.dialogService = r.resolve(DialogService.self) - c.ethApiService = r.resolve(EthApiServiceProtocol.self) return c }) diff --git a/Adamant/Stories/Transactions/TransactionsViewController.swift b/Adamant/Stories/Transactions/TransactionsViewController.swift index 952bb1565..0bd32e81d 100644 --- a/Adamant/Stories/Transactions/TransactionsViewController.swift +++ b/Adamant/Stories/Transactions/TransactionsViewController.swift @@ -17,6 +17,51 @@ extension String.adamantLocalized { } } +class TransactionsViewController: UIViewController { + let cellIdentifier = "cell" + let cellHeight: CGFloat = 90.0 + + internal lazy var refreshControl: UIRefreshControl = { + let refreshControl = UIRefreshControl() + refreshControl.addTarget(self, action: + #selector(self.handleRefresh(_:)), + for: UIControlEvents.valueChanged) + refreshControl.tintColor = UIColor.adamantPrimary + + return refreshControl + }() + + + // MARK: - IBOutlets + @IBOutlet weak var tableView: UITableView! + + + // MARK: - Lifecycle + + override func viewDidLoad() { + super.viewDidLoad() + navigationItem.title = String.adamantLocalized.transactionList.title + + tableView.register(UINib.init(nibName: "TransactionTableViewCell", bundle: nil), forCellReuseIdentifier: cellIdentifier) + tableView.addSubview(self.refreshControl) + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + if let indexPath = tableView.indexPathForSelectedRow { + tableView.deselectRow(at: indexPath, animated: animated) + } + + if tableView.isEditing { + tableView.setEditing(false, animated: false) + } + } + + @objc internal func handleRefresh(_ refreshControl: UIRefreshControl) { + + } +} + class ADMTransactionsViewController: TransactionsViewController { // MARK: - Dependencies var accountService: AccountService! @@ -85,58 +130,6 @@ class ADMTransactionsViewController: TransactionsViewController { refreshControl.endRefreshing() } } - } -} - -class TransactionsViewController: UIViewController { - let cellIdentifier = "cell" - let cellHeight: CGFloat = 90.0 - - internal lazy var refreshControl: UIRefreshControl = { - let refreshControl = UIRefreshControl() - refreshControl.addTarget(self, action: - #selector(self.handleRefresh(_:)), - for: UIControlEvents.valueChanged) - refreshControl.tintColor = UIColor.adamantPrimary - - return refreshControl - }() - - - // MARK: - IBOutlets - @IBOutlet weak var tableView: UITableView! - - - // MARK: - Lifecycle - - override func viewDidLoad() { - super.viewDidLoad() - - if #available(iOS 11.0, *) { - navigationController?.navigationBar.prefersLargeTitles = false - } - - navigationItem.title = String.adamantLocalized.transactionList.title - - tableView.register(UINib.init(nibName: "TransactionTableViewCell", bundle: nil), forCellReuseIdentifier: cellIdentifier) -// tableView.dataSource = self -// tableView.delegate = self - tableView.addSubview(self.refreshControl) - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - if let indexPath = tableView.indexPathForSelectedRow { - tableView.deselectRow(at: indexPath, animated: animated) - } - - if tableView.isEditing { - tableView.setEditing(false, animated: false) - } - } - - @objc internal func handleRefresh(_ refreshControl: UIRefreshControl) { - } private func markTransfersAsRead() { @@ -212,7 +205,7 @@ extension ADMTransactionsViewController: UITableViewDataSource, UITableViewDeleg return } - guard let controller = router.get(scene: AdamantScene.Transactions.transactionDetails) as? TransactionDetailsViewController else { + guard let controller = router.get(scene: AdamantScene.Transactions.transactionDetails) as? BaseTransactionDetailsViewController else { return } From 6cc7f558599ed4636b1a7ce7d737771bd9e40ac4 Mon Sep 17 00:00:00 2001 From: Anton B Date: Tue, 26 Jun 2018 19:50:21 +0300 Subject: [PATCH 020/175] Show Transaction details from chat Show Transaction details from chat Load Transaction details from Ethereum network --- Adamant.xcodeproj/project.pbxproj | 8 + Adamant/Models/EthTransaction.swift | 258 +++++++++++++++ .../EthApiServiceProtocol.swift | 2 + .../TransactionDetailsProtocol.swift | 103 ++++++ .../EthApiService/EthApiService.swift | 301 +++--------------- .../Chats/ChatViewController+MessageKit.swift | 30 ++ 6 files changed, 437 insertions(+), 265 deletions(-) create mode 100644 Adamant/Models/EthTransaction.swift create mode 100644 Adamant/ServiceProtocols/TransactionDetailsProtocol.swift diff --git a/Adamant.xcodeproj/project.pbxproj b/Adamant.xcodeproj/project.pbxproj index 65a137a4c..b85d86c9e 100644 --- a/Adamant.xcodeproj/project.pbxproj +++ b/Adamant.xcodeproj/project.pbxproj @@ -25,6 +25,8 @@ E90055FB20ECE78A00D0CB2D /* SecurityViewController+notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = E90055FA20ECE78A00D0CB2D /* SecurityViewController+notifications.swift */; }; 644D3DBB20BD5F22002EB002 /* Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = 644D3DBA20BD5F22002EB002 /* Status.swift */; }; 649A694720DD427200D27E91 /* TransferType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649A694620DD427200D27E91 /* TransferType.swift */; }; + 64BD2B7520E2814B00E2CD36 /* EthTransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64BD2B7420E2814B00E2CD36 /* EthTransaction.swift */; }; + 64BD2B7720E2820300E2CD36 /* TransactionDetailsProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64BD2B7620E2820300E2CD36 /* TransactionDetailsProtocol.swift */; }; 64D059FF20D3116B003AD655 /* NodesListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D059FE20D3116A003AD655 /* NodesListViewController.swift */; }; 64D05A0220D51632003AD655 /* EthApiServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D05A0120D51632003AD655 /* EthApiServiceProtocol.swift */; }; 64D05A0420D516B4003AD655 /* EthApiService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D05A0320D516B4003AD655 /* EthApiService.swift */; }; @@ -228,6 +230,8 @@ 64E8304F20F5FEEF006FA590 /* VotesAsset.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VotesAsset.swift; sourceTree = ""; }; 644D3DBA20BD5F22002EB002 /* Status.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Status.swift; sourceTree = ""; }; 649A694620DD427200D27E91 /* TransferType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransferType.swift; sourceTree = ""; }; + 64BD2B7420E2814B00E2CD36 /* EthTransaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EthTransaction.swift; sourceTree = ""; }; + 64BD2B7620E2820300E2CD36 /* TransactionDetailsProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionDetailsProtocol.swift; sourceTree = ""; }; 64D059FE20D3116A003AD655 /* NodesListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NodesListViewController.swift; sourceTree = ""; }; 64D05A0120D51632003AD655 /* EthApiServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EthApiServiceProtocol.swift; sourceTree = ""; }; 64D05A0320D516B4003AD655 /* EthApiService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EthApiService.swift; sourceTree = ""; }; @@ -544,6 +548,7 @@ E9A174B22057EC47003667CD /* BackgroundFetchService.swift */, E9215972206119FB0000CA5C /* ReachabilityMonitor.swift */, 64D05A0120D51632003AD655 /* EthApiServiceProtocol.swift */, + 64BD2B7620E2820300E2CD36 /* TransactionDetailsProtocol.swift */, ); path = ServiceProtocols; sourceTree = ""; @@ -589,6 +594,7 @@ 644EC34E20EFA77A00F40C73 /* Delegate.swift */, 64E8304F20F5FEEF006FA590 /* VotesAsset.swift */, 649A694620DD427200D27E91 /* TransferType.swift */, + 64BD2B7420E2814B00E2CD36 /* EthTransaction.swift */, ); path = Models; sourceTree = ""; @@ -1185,6 +1191,7 @@ E9150BA22066DA210065A985 /* ChatTransaction+CoreDataProperties.swift in Sources */, E9204B5020C94C4A00F3B9AB /* Date+humanizedString.swift in Sources */, E9E7CD9120026FA100DFC4DB /* SwinjectDependencies.swift in Sources */, + 64BD2B7520E2814B00E2CD36 /* EthTransaction.swift in Sources */, E9E7CD8B20026B0600DFC4DB /* AccountService.swift in Sources */, E9150B992066DA210065A985 /* TransferTransaction+CoreDataClass.swift in Sources */, E9150BA02066DA210065A985 /* MessageTransaction+CoreDataProperties.swift in Sources */, @@ -1283,6 +1290,7 @@ E93B0D762028B28E00126346 /* AdamantChatsProvider.swift in Sources */, E983AE2320E6568C00497E1A /* Wallet.swift in Sources */, 64FA53D120E24942006783C9 /* BaseTransactionDetailsViewController.swift in Sources */, + 64BD2B7720E2820300E2CD36 /* TransactionDetailsProtocol.swift in Sources */, E90A4943204C5ED6009F6A65 /* EurekaPassphraseRow.swift in Sources */, E965A53420B833A00041A3EA /* StateAsset.swift in Sources */, E9393FA82055C92700EE6F30 /* Decimal+adamant.swift in Sources */, diff --git a/Adamant/Models/EthTransaction.swift b/Adamant/Models/EthTransaction.swift new file mode 100644 index 000000000..9f712e2e5 --- /dev/null +++ b/Adamant/Models/EthTransaction.swift @@ -0,0 +1,258 @@ +// +// EthTransaction.swift +// Adamant +// +// Created by Anton Boyarkin on 26/06/2018. +// Copyright © 2018 Adamant. All rights reserved. +// + +import Foundation +import web3swift +import BigInt + +struct Response: Decodable { + enum CodingKeys: String, CodingKey { + case status + case message + case result + } + + let status: Int + let message: String + let result: [EthTransaction] + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let statusString = (try? container.decode(String.self, forKey: .status)) ?? "0" + status = Int(statusString) ?? 0 + message = (try? container.decode(String.self, forKey: .message)) ?? "" + result = (try? container.decode([EthTransaction].self, forKey: .result)) ?? [] + } +} + +enum TransactionReceiptStatus: String, Decodable { + case fail = "0" + case pass = "1" + case unknown +} + +struct EthTransaction: Decodable { + enum CodingKeys: String, CodingKey { + case date = "timeStamp" + case hash + case value + case from + case to + case gasUsed + case gasPrice + case confirmations + case isError + case receiptStatus = "txreceipt_status" + case blockNumber + } + + let date: Date + let hash: String + let value: BigUInt + let from: String + let to: String + let gasUsed: BigUInt + let gasPrice: BigUInt + let confirmationsValue: String + let isError: Bool + let receiptStatus: TransactionReceiptStatus + let blockNumber: UInt + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + let unixTimeStamp = Double((try? container.decode(String.self, forKey: .date)) ?? "0") ?? 0 + date = Date(timeIntervalSince1970: unixTimeStamp) + hash = (try? container.decode(String.self, forKey: .hash)) ?? "" + value = BigUInt((try? container.decode(String.self, forKey: .value)) ?? "0") ?? BigUInt(0) + from = (try? container.decode(String.self, forKey: .from)) ?? "" + to = (try? container.decode(String.self, forKey: .to)) ?? "" + gasUsed = BigUInt((try? container.decode(String.self, forKey: .gasUsed)) ?? "0") ?? BigUInt(0) + gasPrice = BigUInt((try? container.decode(String.self, forKey: .gasPrice)) ?? "0") ?? BigUInt(0) + confirmationsValue = (try? container.decode(String.self, forKey: .confirmations)) ?? "0" + let isErrorStatus = Int((try? container.decode(String.self, forKey: .isError)) ?? "0") ?? 0 + isError = isErrorStatus == 1 ? true : false + receiptStatus = (try? container.decode(TransactionReceiptStatus.self, forKey: .receiptStatus)) ?? .unknown + blockNumber = UInt((try? container.decode(String.self, forKey: .blockNumber)) ?? "0") ?? 0 + } + + func isOutgoing(_ address: String) -> Bool { + return from == address.lowercased() ? true : false + } + + func formattedValue() -> String { + if let formattedAmount = Web3.Utils.formatToEthereumUnits(value, + toUnits: .eth, + decimals: 8, + fallbackToScientific: true), let amount = Double(formattedAmount) { + return "\(amount) ETH" + } else { + return "\(value)" + } + } +} + +extension EthTransaction: TransactionDetailsProtocol { + + var id: String { + return self.hash + } + + var senderAddress: String { + return self.from + } + + var recipientAddress: String { + return self.to + } + + var sentDate: Date { + return self.date + } + + var amountValue: Double { + guard let string = Web3.Utils.formatToEthereumUnits(value, toUnits: .eth, decimals: 8), let value = Double(string) else { + return 0 + } + + return value + } + + var feeValue: Double { + guard let string = Web3.Utils.formatToEthereumUnits((self.gasPrice * self.gasUsed), toUnits: .eth, decimals: 8), let value = Double(string) else { + return 0 + } + + return value + } + + var block: String { + return "\(self.blockNumber)" + } + + var showGoToExplorer: Bool { + return false + } + + var explorerUrl: URL? { + return nil + } + + var showGoToChat: Bool { + return false + } + + var chatroom: Chatroom? { + return nil + } + + var currencyCode: String { + return "ETH" + } + +} + +/* + { + "blockNumber":"3455267", + "timeStamp":"1529241530", + "hash":"0x9e2092aa9a278ebdd5cc4e37d626533ec1a480397c101add069817c0934cfa76", + "nonce":"561145", + "blockHash":"0xf828955a0911da4a2c207f96b8bffabac804eab7888ec88149ab9867db19b7dd", + "transactionIndex":"16", + "from":"0x687422eea2cb73b5d3e242ba5456b782919afc85", + "to":"0x700bc74dd49044446bcb6a25ae5e725d14538825", + "value":"1000000000000000000", + "gas":"314150", + "gasPrice":"5000000000", + "isError":"0", + "txreceipt_status":"1", + "input":"0x", + "contractAddress":"", + "cumulativeGasUsed":"381927", + "gasUsed":"21000", + "confirmations":"32316" + } + + */ + +struct web3EthTransaction { + let transaction: EthereumTransaction + let transactionBlock: Block? +} + +extension web3EthTransaction: TransactionDetailsProtocol { + var id: String { + return self.transaction.txhash ?? "" + } + + var senderAddress: String { + return self.transaction.sender?.address ?? "" + } + + var recipientAddress: String { + return self.transaction.to.address + } + + var sentDate: Date { + if let timestamp = self.transactionBlock?.timestamp { + return timestamp + } else { + return Date() + } + } + + var amountValue: Double { + guard let string = Web3.Utils.formatToEthereumUnits(self.transaction.value, toUnits: .eth, decimals: 8), let value = Double(string) else { + return 0 + } + + return value + } + + var feeValue: Double { + guard let string = Web3.Utils.formatToEthereumUnits((self.transaction.gasPrice * self.transaction.gasLimit), toUnits: .eth, decimals: 8), let value = Double(string) else { + return 0 + } + + return value + } + + var confirmationsValue: String { + return "--" + } + + var block: String { + if let number = self.transactionBlock?.number { + return "\(number)" + } else { + return "--" + } + } + + var showGoToExplorer: Bool { + return false + } + + var explorerUrl: URL? { + return nil + } + + var showGoToChat: Bool { + return false + } + + var chatroom: Chatroom? { + return nil + } + + var currencyCode: String { + return "ETH" + } + +} diff --git a/Adamant/ServiceProtocols/EthApiServiceProtocol.swift b/Adamant/ServiceProtocols/EthApiServiceProtocol.swift index 111489bff..8b0fc89eb 100644 --- a/Adamant/ServiceProtocols/EthApiServiceProtocol.swift +++ b/Adamant/ServiceProtocols/EthApiServiceProtocol.swift @@ -20,6 +20,8 @@ protocol EthApiServiceProtocol: class { func sendFunds(toAddress address: String, amount: Double, completion: @escaping (ApiServiceResult) -> Void) func getTransactions(_ completion: @escaping (ApiServiceResult<[EthTransaction]>) -> Void) + func getTransaction(byHash hash: String, completion: @escaping (ApiServiceResult) -> Void) + // MARK: - Tools func getBalance(_ completion: @escaping (ApiServiceResult) -> Void) func getBalance(byAddress address: String, completion: @escaping (ApiServiceResult) -> Void) diff --git a/Adamant/ServiceProtocols/TransactionDetailsProtocol.swift b/Adamant/ServiceProtocols/TransactionDetailsProtocol.swift new file mode 100644 index 000000000..ba4db1e15 --- /dev/null +++ b/Adamant/ServiceProtocols/TransactionDetailsProtocol.swift @@ -0,0 +1,103 @@ +// +// TransactionDetailsProtocol.swift +// Adamant +// +// Created by Anton Boyarkin on 26/06/2018. +// Copyright © 2018 Adamant. All rights reserved. +// + +import Foundation +import web3swift +import BigInt + +/// A standard protocol representing a Transaction details. +protocol TransactionDetailsProtocol { + + /// The identifier of the transaction. + var id: String { get } + + /// The sender of the transaction. + var senderAddress: String { get } + + /// The reciver of the transaction. + var recipientAddress: String { get } + + /// The date the transaction was sent. + var sentDate: Date { get } + + /// The amount of currency that was sent. + var amountValue: Double { get } + + /// The amount of fee that taken for transaction process. + var feeValue: Double { get } + + /// The confirmations of the transaction. + var confirmationsValue: String { get } + + /// The block of the transaction. + var block: String { get } + + /// The show explorer button. + var showGoToExplorer: Bool { get } + + /// The explorer url. + var explorerUrl: URL? { get } + + /// The show go to button. + var showGoToChat: Bool { get } + + /// The show go to button. + var chatroom: Chatroom? { get } + + /// The currency of the transaction. + var currencyCode: String { get } + +} + +extension TransactionDetailsProtocol { + + var haveChatroom: Bool { + if let chatroom = self.chatroom, let transactions = chatroom.transactions { + let messeges = transactions.first (where: { (object) -> Bool in + return !(object is TransferTransaction) + }) + + return (messeges != nil) + } + + return false + } + + var currencyFormatter: NumberFormatter { + let formatter = NumberFormatter() + formatter.numberStyle = .decimal + formatter.roundingMode = .floor + formatter.positiveFormat = "#.######## \(currencyCode)" + + return formatter + } + + func getSummary() -> String { + return """ + Transaction #\(id) + + Summary + Sender: \(senderAddress) + Recipient: \(recipientAddress) + Date: \(DateFormatter.localizedString(from: sentDate, dateStyle: .short, timeStyle: .medium)) + Amount: \(formattedAmount()) + Fee: \(formattedFee()) + Confirmations: \(String(confirmationsValue)) + Block: \(block) + URL: \(explorerUrl?.absoluteString ?? "") + """ + } + + func formattedAmount() -> String { + return currencyFormatter.string(from: NSNumber(value: amountValue)) ?? "" + } + + func formattedFee() -> String { + return currencyFormatter.string(from: NSNumber(value: feeValue)) ?? "" + } +} diff --git a/Adamant/Services/EthApiService/EthApiService.swift b/Adamant/Services/EthApiService/EthApiService.swift index 63e8313be..bdd898034 100644 --- a/Adamant/Services/EthApiService/EthApiService.swift +++ b/Adamant/Services/EthApiService/EthApiService.swift @@ -253,6 +253,42 @@ class EthApiService: EthApiServiceProtocol { } } + func getTransaction(byHash hash: String, completion: @escaping (ApiServiceResult) -> Void) { + DispatchQueue.global().async { + let result = self.web3.eth.getTransactionDetails(hash) + switch result { + case .success(let transaction): + if let number = transaction.blockNumber { +// let resultReceipt = self.web3.eth.getTransactionReceipt(hash) +// if case .success(let receipt) = resultReceipt { +// print(receipt) +// } + + let result = self.web3.eth.getBlockByNumber(number) + guard case .success(let block) = result else { + DispatchQueue.main.async { + completion(.success(web3EthTransaction(transaction: transaction.transaction, transactionBlock: nil))) + } + return + } + DispatchQueue.main.async { + completion(.success(web3EthTransaction(transaction: transaction.transaction, transactionBlock: block))) + } + } else { + DispatchQueue.main.async { + completion(.success(web3EthTransaction(transaction: transaction.transaction, transactionBlock: nil))) + } + } + break + case .failure(let error): + DispatchQueue.main.async { + completion(.failure(.internalError(message: "ETH Wallet: fail to load transaction details", error: error))) + } + break + } + } + } + // MARK: - Tools func getBalance(_ completion: @escaping (ApiServiceResult) -> Void) { @@ -396,268 +432,3 @@ class EthApiService: EthApiServiceProtocol { } } } - - -/// A standard protocol representing a Transaction details. -protocol TransactionDetailsProtocol { - - /// The identifier of the transaction. - var id: String { get } - - /// The sender of the transaction. - var senderAddress: String { get } - - /// The reciver of the transaction. - var recipientAddress: String { get } - - /// The date the transaction was sent. - var sentDate: Date { get } - - /// The amount of currency that was sent. - var amountValue: Double { get } - - /// The amount of fee that taken for transaction process. - var feeValue: Double { get } - - /// The confirmations of the transaction. - var confirmationsValue: String { get } - - /// The block of the transaction. - var block: String { get } - - /// The show explorer button. - var showGoToExplorer: Bool { get } - - /// The explorer url. - var explorerUrl: URL? { get } - - /// The show go to button. - var showGoToChat: Bool { get } - - /// The show go to button. - var chatroom: Chatroom? { get } - - /// The currency of the transaction. - var currencyCode: String { get } - -} - -extension TransactionDetailsProtocol { - - var haveChatroom: Bool { - if let chatroom = self.chatroom, let transactions = chatroom.transactions { - let messeges = transactions.first (where: { (object) -> Bool in - return !(object is TransferTransaction) - }) - - return (messeges != nil) - } - - return false - } - - var currencyFormatter: NumberFormatter { - let formatter = NumberFormatter() - formatter.numberStyle = .decimal - formatter.roundingMode = .floor - formatter.positiveFormat = "#.######## \(currencyCode)" - - return formatter - } - - func getSummary() -> String { - return """ - Transaction #\(id) - - Summary - Sender: \(senderAddress) - Recipient: \(recipientAddress) - Date: \(DateFormatter.localizedString(from: sentDate, dateStyle: .short, timeStyle: .medium)) - Amount: \(formattedAmount()) - Fee: \(formattedFee()) - Confirmations: \(String(confirmationsValue)) - Block: \(block) - URL: \(explorerUrl?.absoluteString ?? "") - """ - } - - func formattedAmount() -> String { - return currencyFormatter.string(from: NSNumber(value: amountValue)) ?? "" - } - - func formattedFee() -> String { - return currencyFormatter.string(from: NSNumber(value: feeValue)) ?? "" - } -} - -//MARK: - Decodable Transaction structures -struct Response: Decodable { - enum CodingKeys: String, CodingKey { - case status - case message - case result - } - - let status: Int - let message: String - let result: [EthTransaction] - - init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - let statusString = (try? container.decode(String.self, forKey: .status)) ?? "0" - status = Int(statusString) ?? 0 - message = (try? container.decode(String.self, forKey: .message)) ?? "" - result = (try? container.decode([EthTransaction].self, forKey: .result)) ?? [] - } -} - -struct EthTransaction: Decodable { - enum CodingKeys: String, CodingKey { - case date = "timeStamp" - case hash - case value - case from - case to - case gasUsed - case gasPrice - case confirmations - case isError - case receiptStatus = "txreceipt_status" - case blockNumber - } - - let date: Date - let hash: String - let value: BigUInt - let from: String - let to: String - let gasUsed: BigUInt - let gasPrice: BigUInt - let confirmationsValue: String - let isError: Bool - let receiptStatus: TransactionReceiptStatus - let blockNumber: UInt - - init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - - let unixTimeStamp = Double((try? container.decode(String.self, forKey: .date)) ?? "0") ?? 0 - date = Date(timeIntervalSince1970: unixTimeStamp) - hash = (try? container.decode(String.self, forKey: .hash)) ?? "" - value = BigUInt((try? container.decode(String.self, forKey: .value)) ?? "0") ?? BigUInt(0) - from = (try? container.decode(String.self, forKey: .from)) ?? "" - to = (try? container.decode(String.self, forKey: .to)) ?? "" - gasUsed = BigUInt((try? container.decode(String.self, forKey: .gasUsed)) ?? "0") ?? BigUInt(0) - gasPrice = BigUInt((try? container.decode(String.self, forKey: .gasPrice)) ?? "0") ?? BigUInt(0) - confirmationsValue = (try? container.decode(String.self, forKey: .confirmations)) ?? "0" - let isErrorStatus = Int((try? container.decode(String.self, forKey: .isError)) ?? "0") ?? 0 - isError = isErrorStatus == 1 ? true : false - receiptStatus = (try? container.decode(TransactionReceiptStatus.self, forKey: .receiptStatus)) ?? .unknown - blockNumber = UInt((try? container.decode(String.self, forKey: .blockNumber)) ?? "0") ?? 0 - } - - func isOutgoing(_ address: String) -> Bool { - return from == address.lowercased() ? true : false - } - - func formattedValue() -> String { - if let formattedAmount = Web3.Utils.formatToEthereumUnits(value, - toUnits: .eth, - decimals: 8, - fallbackToScientific: true), let amount = Double(formattedAmount) { - return "\(amount) ETH" - } else { - return "\(value)" - } - } -} - -extension EthTransaction: TransactionDetailsProtocol { - - var id: String { - return self.hash - } - - var senderAddress: String { - return self.from - } - - var recipientAddress: String { - return self.to - } - - var sentDate: Date { - return self.date - } - - var amountValue: Double { - guard let string = Web3.Utils.formatToEthereumUnits(value, toUnits: .eth, decimals: 8), let value = Double(string) else { - return 0 - } - - return value - } - - var feeValue: Double { - guard let string = Web3.Utils.formatToEthereumUnits((self.gasPrice * self.gasUsed), toUnits: .eth, decimals: 8), let value = Double(string) else { - return 0 - } - - return value - } - - var block: String { - return "\(self.blockNumber)" - } - - var showGoToExplorer: Bool { - return false - } - - var explorerUrl: URL? { - return nil - } - - var showGoToChat: Bool { - return false - } - - var chatroom: Chatroom? { - return nil - } - - var currencyCode: String { - return "ETH" - } - -} - -enum TransactionReceiptStatus: String, Decodable { - case fail = "0" - case pass = "1" - case unknown -} - -/* - { - "blockNumber":"3455267", - "timeStamp":"1529241530", - "hash":"0x9e2092aa9a278ebdd5cc4e37d626533ec1a480397c101add069817c0934cfa76", - "nonce":"561145", - "blockHash":"0xf828955a0911da4a2c207f96b8bffabac804eab7888ec88149ab9867db19b7dd", - "transactionIndex":"16", - "from":"0x687422eea2cb73b5d3e242ba5456b782919afc85", - "to":"0x700bc74dd49044446bcb6a25ae5e725d14538825", - "value":"1000000000000000000", - "gas":"314150", - "gasPrice":"5000000000", - "isError":"0", - "txreceipt_status":"1", - "input":"0x", - "contractAddress":"", - "cumulativeGasUsed":"381927", - "gasUsed":"21000", - "confirmations":"32316" - } - - */ diff --git a/Adamant/Stories/Chats/ChatViewController+MessageKit.swift b/Adamant/Stories/Chats/ChatViewController+MessageKit.swift index 8904e34b9..61c68df74 100644 --- a/Adamant/Stories/Chats/ChatViewController+MessageKit.swift +++ b/Adamant/Stories/Chats/ChatViewController+MessageKit.swift @@ -202,6 +202,36 @@ extension ChatViewController: MessageCellDelegate { case let message as MessageTransaction: // MARK: Show Retry/Cancel action sheet guard message.messageStatus == .failed else { + + if message.type == UInt16(ChatType.richMessage.rawValue) { + guard let data = message.message?.data(using: String.Encoding.utf8), let transfer = try? JSONDecoder().decode(ChatTransfer.self, from: data), transfer.type == .eth else { + break + } + + guard let vc = router.get(scene: AdamantScene.Transactions.transactionDetails) as? BaseTransactionDetailsViewController else { + break + } + self.dialogService.showProgress(withMessage: String.adamantLocalized.transfer.transferProcessingMessage, userInteractionEnable: false) + + self.ethApiService.getTransaction(byHash: transfer.hash) { (result) in + switch result { + case .success(let transaction): + vc.set(transaction: transaction) + self.dialogService.dismissProgress() + break + case .failure(let error): + self.dialogService.showError(withMessage: "Transrer issue", error: error) + break + } + } + + if let nav = navigationController { + nav.pushViewController(vc, animated: true) + } else { + present(vc, animated: true, completion: nil) + } + } + break } From c10578c569dfc476a485b1af49aa557aad7fb348 Mon Sep 17 00:00:00 2001 From: Anton B Date: Tue, 26 Jun 2018 23:47:05 +0300 Subject: [PATCH 021/175] Optimisation & Refactoring Optimize Base TransationViewController Make History transaction API url auto switch to used ethereum network --- Adamant.xcodeproj/project.pbxproj | 4 + Adamant/AppDelegate.swift | 3 +- Adamant/Models/EthTransaction.swift | 4 - .../TransactionDetailsProtocol.swift | 4 + .../EthApiService/EthApiService.swift | 25 +- .../ADMTransactionsViewController.swift | 234 ++++++++++++++ ...BaseTransactionDetailsViewController.swift | 3 + .../ETHTransactionsViewController.swift | 48 +-- .../TransactionsViewController.swift | 288 +++--------------- 9 files changed, 310 insertions(+), 303 deletions(-) create mode 100644 Adamant/Stories/Transactions/ADMTransactionsViewController.swift diff --git a/Adamant.xcodeproj/project.pbxproj b/Adamant.xcodeproj/project.pbxproj index b85d86c9e..c795da1a3 100644 --- a/Adamant.xcodeproj/project.pbxproj +++ b/Adamant.xcodeproj/project.pbxproj @@ -30,6 +30,7 @@ 64D059FF20D3116B003AD655 /* NodesListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D059FE20D3116A003AD655 /* NodesListViewController.swift */; }; 64D05A0220D51632003AD655 /* EthApiServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D05A0120D51632003AD655 /* EthApiServiceProtocol.swift */; }; 64D05A0420D516B4003AD655 /* EthApiService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D05A0320D516B4003AD655 /* EthApiService.swift */; }; + 64F085D920E2D7600006DE68 /* ADMTransactionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64F085D820E2D7600006DE68 /* ADMTransactionsViewController.swift */; }; 64FA53CD20E1300B006783C9 /* ETHTransactionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64FA53CC20E1300A006783C9 /* ETHTransactionsViewController.swift */; }; 64FA53D120E24942006783C9 /* BaseTransactionDetailsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64FA53D020E24941006783C9 /* BaseTransactionDetailsViewController.swift */; }; E905D39B2048A9BD00DDB504 /* KeychainStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = E905D39A2048A9BD00DDB504 /* KeychainStore.swift */; }; @@ -235,6 +236,7 @@ 64D059FE20D3116A003AD655 /* NodesListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NodesListViewController.swift; sourceTree = ""; }; 64D05A0120D51632003AD655 /* EthApiServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EthApiServiceProtocol.swift; sourceTree = ""; }; 64D05A0320D516B4003AD655 /* EthApiService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EthApiService.swift; sourceTree = ""; }; + 64F085D820E2D7600006DE68 /* ADMTransactionsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ADMTransactionsViewController.swift; sourceTree = ""; }; 64FA53CC20E1300A006783C9 /* ETHTransactionsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ETHTransactionsViewController.swift; sourceTree = ""; }; 64FA53D020E24941006783C9 /* BaseTransactionDetailsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseTransactionDetailsViewController.swift; sourceTree = ""; }; 871009461457F6B7B47D99C5 /* Pods-Adamant.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Adamant.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Adamant/Pods-Adamant.debug.xcconfig"; sourceTree = ""; }; @@ -690,6 +692,7 @@ children = ( E94E7B05205D48B20042B639 /* TransactionsRoutes.swift */, E9E7CDC12003F5A400DFC4DB /* TransactionsViewController.swift */, + 64F085D820E2D7600006DE68 /* ADMTransactionsViewController.swift */, 64FA53CC20E1300A006783C9 /* ETHTransactionsViewController.swift */, E94E7B0B205D5E4A0042B639 /* TransactionsViewController.xib */, 64FA53D020E24941006783C9 /* BaseTransactionDetailsViewController.swift */, @@ -1193,6 +1196,7 @@ E9E7CD9120026FA100DFC4DB /* SwinjectDependencies.swift in Sources */, 64BD2B7520E2814B00E2CD36 /* EthTransaction.swift in Sources */, E9E7CD8B20026B0600DFC4DB /* AccountService.swift in Sources */, + 64F085D920E2D7600006DE68 /* ADMTransactionsViewController.swift in Sources */, E9150B992066DA210065A985 /* TransferTransaction+CoreDataClass.swift in Sources */, E9150BA02066DA210065A985 /* MessageTransaction+CoreDataProperties.swift in Sources */, E9B3D39A201F90570019EB36 /* AccountsProvider.swift in Sources */, diff --git a/Adamant/AppDelegate.swift b/Adamant/AppDelegate.swift index 7acf5d3f5..001673283 100644 --- a/Adamant/AppDelegate.swift +++ b/Adamant/AppDelegate.swift @@ -47,7 +47,8 @@ struct AdamantResources { ] static let ethServers = [ - "https://api.myetherapi.com/eth" +// "https://api.myetherapi.com/eth" + "https://ropsten.infura.io/" // test network ] // Addresses diff --git a/Adamant/Models/EthTransaction.swift b/Adamant/Models/EthTransaction.swift index 9f712e2e5..14b2553be 100644 --- a/Adamant/Models/EthTransaction.swift +++ b/Adamant/Models/EthTransaction.swift @@ -81,10 +81,6 @@ struct EthTransaction: Decodable { blockNumber = UInt((try? container.decode(String.self, forKey: .blockNumber)) ?? "0") ?? 0 } - func isOutgoing(_ address: String) -> Bool { - return from == address.lowercased() ? true : false - } - func formattedValue() -> String { if let formattedAmount = Web3.Utils.formatToEthereumUnits(value, toUnits: .eth, diff --git a/Adamant/ServiceProtocols/TransactionDetailsProtocol.swift b/Adamant/ServiceProtocols/TransactionDetailsProtocol.swift index ba4db1e15..664d6273a 100644 --- a/Adamant/ServiceProtocols/TransactionDetailsProtocol.swift +++ b/Adamant/ServiceProtocols/TransactionDetailsProtocol.swift @@ -56,6 +56,10 @@ protocol TransactionDetailsProtocol { extension TransactionDetailsProtocol { + func isOutgoing(_ address: String) -> Bool { + return senderAddress == address.lowercased() ? true : false + } + var haveChatroom: Bool { if let chatroom = self.chatroom, let transactions = chatroom.transactions { let messeges = transactions.first (where: { (object) -> Bool in diff --git a/Adamant/Services/EthApiService/EthApiService.swift b/Adamant/Services/EthApiService/EthApiService.swift index bdd898034..61a3ddebe 100644 --- a/Adamant/Services/EthApiService/EthApiService.swift +++ b/Adamant/Services/EthApiService/EthApiService.swift @@ -46,7 +46,7 @@ class EthApiService: EthApiServiceProtocol { var apiUrl: String var web3: web3! - let transactionsHistoryService = TransactionsHistory() + var transactionsHistoryService: TransactionsHistory! private(set) var account: EthAccount? @@ -55,9 +55,6 @@ class EthApiService: EthApiServiceProtocol { init(apiUrl: String) { self.apiUrl = apiUrl - // test network - self.apiUrl = "https://ropsten.infura.io/" - if let url = URL(string: self.apiUrl), let web3 = Web3.new(url) { self.web3 = web3 } else { @@ -70,7 +67,14 @@ class EthApiService: EthApiServiceProtocol { print("ETH Server gas Price: \(gasPrice)") - if let network = self.web3.provider.network { print("ETH Server network: \(network)") } + guard let network = self.web3.provider.network else { + print("Unable get ETH Server network") + self.transactionsHistoryService = TransactionsHistory(network: "") + return + } + print("ETH Server network: \(network)") + + self.transactionsHistoryService = TransactionsHistory(network: "\(network)") } func newAccount(byPassphrase passphrase: String, completion: @escaping (ApiServiceResult) -> Void) { @@ -376,8 +380,15 @@ class EthApiService: EthApiServiceProtocol { // MARK: - Transactions History API - class TransactionsHistory { - private let baseUrl = "https://api-ropsten.etherscan.io/api" +class TransactionsHistory { + private var networkSuffix = "" + private var baseUrl: String { return "https://api\(networkSuffix).etherscan.io/api" } + + init(network: String) { + if network != "\(Networks.Mainnet)" { + networkSuffix = "-\(network)" + } + } private let defaultResponseDispatchQueue = DispatchQueue(label: "com.adamant.response-queue", qos: .utility, attributes: [.concurrent]) diff --git a/Adamant/Stories/Transactions/ADMTransactionsViewController.swift b/Adamant/Stories/Transactions/ADMTransactionsViewController.swift new file mode 100644 index 000000000..44255fcb4 --- /dev/null +++ b/Adamant/Stories/Transactions/ADMTransactionsViewController.swift @@ -0,0 +1,234 @@ +// +// ADMTransactionsViewController.swift +// Adamant +// +// Created by Anton Boyarkin on 26/06/2018. +// Copyright © 2018 Adamant. All rights reserved. +// + +import UIKit +import CoreData + +class ADMTransactionsViewController: TransactionsViewController { + // MARK: - Dependencies + var accountService: AccountService! + var transfersProvider: TransfersProvider! + var chatsProvider: ChatsProvider! + var dialogService: DialogService! + var router: Router! + + // MARK: - Properties + var controller: NSFetchedResultsController? + + override func viewDidLoad() { + super.viewDidLoad() + + if accountService.account != nil { + initFetchedResultController(provider: transfersProvider) + } + + NotificationCenter.default.addObserver(forName: Notification.Name.AdamantAccountService.userLoggedIn, object: nil, queue: nil) { [weak self] notification in + self?.initFetchedResultController(provider: self?.transfersProvider) + } + + NotificationCenter.default.addObserver(forName: Notification.Name.AdamantAccountService.userLoggedOut, object: nil, queue: nil) { [weak self] _ in + self?.initFetchedResultController(provider: nil) + } + } + + /// - Parameter provider: nil to drop and reset + private func initFetchedResultController(provider: TransfersProvider?) { + controller = transfersProvider.transfersController() + controller?.delegate = self + + do { + try controller?.performFetch() + } catch { + print("There was an error performing fetch: \(error)") + controller = nil + } + + tableView.reloadData() + } + + override func handleRefresh(_ refreshControl: UIRefreshControl) { + self.transfersProvider.update { [weak self] (result) in + guard let result = result else { + DispatchQueue.main.async { + refreshControl.endRefreshing() + } + return + } + + switch result { + case .success: + DispatchQueue.main.async { + self?.tableView.reloadData() + } + break + case .failure(let error): + self?.dialogService.showRichError(error: error) + } + + DispatchQueue.main.async { + refreshControl.endRefreshing() + } + } + } +} + +// MARK: - UITableView +extension ADMTransactionsViewController { + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + if let f = controller?.fetchedObjects { + return f.count + } else { + return 0 + } + } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + guard let transaction = controller?.object(at: indexPath) else { + tableView.deselectRow(at: indexPath, animated: true) + return + } + + guard let controller = router.get(scene: AdamantScene.Transactions.transactionDetails) as? BaseTransactionDetailsViewController else { + return + } + + controller.transaction = transaction + navigationController?.pushViewController(controller, animated: true) + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + guard let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as? TransactionTableViewCell, + let transfer = controller?.object(at: indexPath) else { + // TODO: Display & Log error + return UITableViewCell(style: .default, reuseIdentifier: "cell") + } + + cell.accessoryType = .disclosureIndicator + + configureCell(cell, for: transfer) + return cell + } + + func tableView(_ tableView: UITableView, editActionsForRowAt: IndexPath) -> [UITableViewRowAction]? { + guard let transfer = controller?.object(at: editActionsForRowAt), let chatroom = transfer.partner?.chatroom, let transactions = chatroom.transactions else { + return nil + } + + let messeges = transactions.first (where: { (object) -> Bool in + return !(object is TransferTransaction) + }) + + let title = (messeges != nil) ? String.adamantLocalized.transactionList.toChat : String.adamantLocalized.transactionList.startChat + + let toChat = UITableViewRowAction(style: .normal, title: title) { action, index in + guard let vc = self.router.get(scene: AdamantScene.Chats.chat) as? ChatViewController else { + // TODO: Log this + return + } + + guard let account = self.accountService.account else { + return + } + + vc.account = account + vc.chatroom = chatroom + vc.hidesBottomBarWhenPushed = true + + if let nav = self.navigationController { + nav.pushViewController(vc, animated: true) + } else { + self.present(vc, animated: true) + } + } + + toChat.backgroundColor = UIColor.adamantPrimary + + return [toChat] + } + + func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { + return true + } + + @available(iOS 11.0, *) + func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { + guard let transfer = controller?.object(at: indexPath), let chatroom = transfer.partner?.chatroom, let transactions = chatroom.transactions else { + return nil + } + + let messeges = transactions.first (where: { (object) -> Bool in + return !(object is TransferTransaction) + }) + + let title = (messeges != nil) ? String.adamantLocalized.transactionList.toChat : String.adamantLocalized.transactionList.startChat + + let toChat = UIContextualAction(style: .normal, title: title, handler: { (ac:UIContextualAction, view:UIView, success:(Bool) -> Void) in + guard let vc = self.router.get(scene: AdamantScene.Chats.chat) as? ChatViewController else { + // TODO: Log this + return + } + + guard let account = self.accountService.account else { + return + } + + vc.account = account + vc.chatroom = chatroom + vc.hidesBottomBarWhenPushed = true + + if let nav = self.navigationController { + nav.pushViewController(vc, animated: true) + } else { + self.present(vc, animated: true) + } + }) + + toChat.image = (messeges != nil) ? #imageLiteral(resourceName: "chats_tab") : #imageLiteral(resourceName: "Chat") + toChat.backgroundColor = UIColor.adamantPrimary + return UISwipeActionsConfiguration(actions: [toChat]) + } +} + +extension ADMTransactionsViewController: NSFetchedResultsControllerDelegate { + func controllerWillChangeContent(_ controller: NSFetchedResultsController) { + tableView.beginUpdates() + } + + func controllerDidChangeContent(_ controller: NSFetchedResultsController) { + tableView.endUpdates() + } + + func controller(_ controller: NSFetchedResultsController, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) { + switch type { + case .insert: + if let newIndexPath = newIndexPath { + tableView.insertRows(at: [newIndexPath], with: .automatic) + + if let transfer = anObject as? TransferTransaction { + transfer.isUnread = false + } + } + + case .delete: + if let indexPath = indexPath { + tableView.deleteRows(at: [indexPath], with: .automatic) + } + + case .update: + if let indexPath = indexPath, + let cell = self.tableView.cellForRow(at: indexPath) as? TransactionTableViewCell, + let transfer = anObject as? TransferTransaction { + configureCell(cell, for: transfer) + } + + default: + break + } + } +} diff --git a/Adamant/Stories/Transactions/BaseTransactionDetailsViewController.swift b/Adamant/Stories/Transactions/BaseTransactionDetailsViewController.swift index 101a26a94..3f3908ff2 100644 --- a/Adamant/Stories/Transactions/BaseTransactionDetailsViewController.swift +++ b/Adamant/Stories/Transactions/BaseTransactionDetailsViewController.swift @@ -471,4 +471,7 @@ extension TransferTransaction: TransactionDetailsProtocol { return "ADM" } + func isOutgoing(_ address: String) -> Bool { + return self.isOutgoing + } } diff --git a/Adamant/Stories/Transactions/ETHTransactionsViewController.swift b/Adamant/Stories/Transactions/ETHTransactionsViewController.swift index eee71037a..97ec03343 100644 --- a/Adamant/Stories/Transactions/ETHTransactionsViewController.swift +++ b/Adamant/Stories/Transactions/ETHTransactionsViewController.swift @@ -21,9 +21,6 @@ class ETHTransactionsViewController: TransactionsViewController { override func viewDidLoad() { super.viewDidLoad() - tableView.dataSource = self - tableView.delegate = self - self.refreshControl.beginRefreshing() handleRefresh(self.refreshControl) @@ -47,51 +44,22 @@ class ETHTransactionsViewController: TransactionsViewController { } }) } -} - -// MARK: - UITableView Cells -extension ETHTransactionsViewController { - private func configureCell(_ cell: TransactionTableViewCell, for transfer: EthTransaction) { - cell.accountLabel.tintColor = UIColor.adamantPrimary - cell.ammountLabel.tintColor = UIColor.adamantPrimary - cell.dateLabel.tintColor = UIColor.adamantSecondary - cell.avatarImageView.tintColor = UIColor.adamantPrimary - + + override func currentAddress() -> String { guard let address = ethApiService.account?.address else { - return + return "" } - if transfer.isOutgoing(address) { - cell.transactionType = .outcome - cell.accountLabel.text = transfer.to - } else { - cell.transactionType = .income - cell.accountLabel.text = transfer.from - } - - cell.ammountLabel.text = transfer.formattedValue() - - cell.dateLabel.text = transfer.date.humanizedDateTime() + return address } } // MARK: - UITableView -extension ETHTransactionsViewController: UITableViewDataSource, UITableViewDelegate { - func numberOfSections(in tableView: UITableView) -> Int { - return 1 - } - - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { +extension ETHTransactionsViewController { + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return transactions.count } - func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { - return UIView() - } - - func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - return cellHeight - } - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: true) @@ -105,7 +73,7 @@ extension ETHTransactionsViewController: UITableViewDataSource, UITableViewDeleg navigationController?.pushViewController(controller, animated: true) } - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { guard let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as? TransactionTableViewCell else { // TODO: Display & Log error return UITableViewCell(style: .default, reuseIdentifier: "cell") diff --git a/Adamant/Stories/Transactions/TransactionsViewController.swift b/Adamant/Stories/Transactions/TransactionsViewController.swift index 0bd32e81d..732e506d7 100644 --- a/Adamant/Stories/Transactions/TransactionsViewController.swift +++ b/Adamant/Stories/Transactions/TransactionsViewController.swift @@ -42,6 +42,9 @@ class TransactionsViewController: UIViewController { super.viewDidLoad() navigationItem.title = String.adamantLocalized.transactionList.title + tableView.dataSource = self + tableView.delegate = self + tableView.register(UINib.init(nibName: "TransactionTableViewCell", bundle: nil), forCellReuseIdentifier: cellIdentifier) tableView.addSubview(self.refreshControl) } @@ -60,76 +63,31 @@ class TransactionsViewController: UIViewController { @objc internal func handleRefresh(_ refreshControl: UIRefreshControl) { } + + internal func currentAddress() -> String { + return "" + } } -class ADMTransactionsViewController: TransactionsViewController { - // MARK: - Dependencies - var accountService: AccountService! - var transfersProvider: TransfersProvider! - var chatsProvider: ChatsProvider! - var dialogService: DialogService! - var router: Router! - - // MARK: - Properties - var controller: NSFetchedResultsController? - - override func viewDidLoad() { - super.viewDidLoad() - - tableView.dataSource = self - tableView.delegate = self - - if accountService.account != nil { - initFetchedResultController(provider: transfersProvider) - } - - NotificationCenter.default.addObserver(forName: Notification.Name.AdamantAccountService.userLoggedIn, object: nil, queue: nil) { [weak self] notification in - self?.initFetchedResultController(provider: self?.transfersProvider) - } +// MARK: - UITableView Cells +extension TransactionsViewController { + internal func configureCell(_ cell: TransactionTableViewCell, for transfer: TransactionDetailsProtocol) { + cell.accountLabel.tintColor = UIColor.adamantPrimary + cell.ammountLabel.tintColor = UIColor.adamantPrimary + cell.dateLabel.tintColor = UIColor.adamantSecondary + cell.avatarImageView.tintColor = UIColor.adamantPrimary - NotificationCenter.default.addObserver(forName: Notification.Name.AdamantAccountService.userLoggedOut, object: nil, queue: nil) { [weak self] _ in - self?.initFetchedResultController(provider: nil) + if transfer.isOutgoing(currentAddress()) { + cell.transactionType = .outcome + cell.accountLabel.text = transfer.recipientAddress + } else { + cell.transactionType = .income + cell.accountLabel.text = transfer.senderAddress } - } - - /// - Parameter provider: nil to drop and reset - private func initFetchedResultController(provider: TransfersProvider?) { - controller = transfersProvider.transfersController() - controller?.delegate = self - do { - try controller?.performFetch() - } catch { - print("There was an error performing fetch: \(error)") - controller = nil - } + cell.ammountLabel.text = transfer.formattedAmount() - tableView.reloadData() - } - - override func handleRefresh(_ refreshControl: UIRefreshControl) { - self.transfersProvider.update { [weak self] (result) in - guard let result = result else { - DispatchQueue.main.async { - refreshControl.endRefreshing() - } - return - } - - switch result { - case .success: - DispatchQueue.main.async { - self?.tableView.reloadData() - } - break - case .failure(let error): - self?.dialogService.showRichError(error: error) - } - - DispatchQueue.main.async { - refreshControl.endRefreshing() - } - } + cell.dateLabel.text = transfer.sentDate.humanizedDateTime() } private func markTransfersAsRead() { @@ -150,198 +108,26 @@ class ADMTransactionsViewController: TransactionsViewController { } } -// MARK: - UITableView Cells -extension ADMTransactionsViewController { - private func configureCell(_ cell: TransactionTableViewCell, for transfer: TransferTransaction) { - cell.accountLabel.tintColor = UIColor.adamantPrimary - cell.ammountLabel.tintColor = UIColor.adamantPrimary - cell.dateLabel.tintColor = UIColor.adamantSecondary - cell.avatarImageView.tintColor = UIColor.adamantPrimary - - if transfer.isOutgoing { - cell.transactionType = .outcome - cell.accountLabel.text = transfer.recipientId - } else { - cell.transactionType = .income - cell.accountLabel.text = transfer.senderId - } - - if let amount = transfer.amount { - cell.ammountLabel.text = AdamantUtilities.format(balance: amount) - } - - if let date = transfer.date as Date? { - cell.dateLabel.text = date.humanizedDateTime() - } - } -} - - // MARK: - UITableView -extension ADMTransactionsViewController: UITableViewDataSource, UITableViewDelegate { - func numberOfSections(in tableView: UITableView) -> Int { - return 1 - } - - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - if let f = controller?.fetchedObjects { - return f.count - } else { - return 0 - } - } - - func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { - return UIView() - } - - func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - return cellHeight - } - - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - guard let transaction = controller?.object(at: indexPath) else { - tableView.deselectRow(at: indexPath, animated: true) - return - } - - guard let controller = router.get(scene: AdamantScene.Transactions.transactionDetails) as? BaseTransactionDetailsViewController else { - return - } - - controller.transaction = transaction - navigationController?.pushViewController(controller, animated: true) - } - - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - guard let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as? TransactionTableViewCell, - let transfer = controller?.object(at: indexPath) else { - // TODO: Display & Log error - return UITableViewCell(style: .default, reuseIdentifier: "cell") - } - - cell.accessoryType = .disclosureIndicator - - configureCell(cell, for: transfer) - return cell - } +extension TransactionsViewController: UITableViewDataSource, UITableViewDelegate { + func numberOfSections(in tableView: UITableView) -> Int { + return 1 + } - func tableView(_ tableView: UITableView, editActionsForRowAt: IndexPath) -> [UITableViewRowAction]? { - guard let transfer = controller?.object(at: editActionsForRowAt), let chatroom = transfer.partner?.chatroom, let transactions = chatroom.transactions else { - return nil - } - - let messeges = transactions.first (where: { (object) -> Bool in - return !(object is TransferTransaction) - }) - - let title = (messeges != nil) ? String.adamantLocalized.transactionList.toChat : String.adamantLocalized.transactionList.startChat - - let toChat = UITableViewRowAction(style: .normal, title: title) { action, index in - guard let vc = self.router.get(scene: AdamantScene.Chats.chat) as? ChatViewController else { - // TODO: Log this - return - } - - guard let account = self.accountService.account else { - return - } - - vc.account = account - vc.chatroom = chatroom - vc.hidesBottomBarWhenPushed = true - - if let nav = self.navigationController { - nav.pushViewController(vc, animated: true) - } else { - self.present(vc, animated: true) - } - } - - toChat.backgroundColor = UIColor.adamantPrimary - - return [toChat] + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return 0 } - func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { - return true + func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { + return UIView() } - @available(iOS 11.0, *) - func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { - guard let transfer = controller?.object(at: indexPath), let chatroom = transfer.partner?.chatroom, let transactions = chatroom.transactions else { - return nil - } - - let messeges = transactions.first (where: { (object) -> Bool in - return !(object is TransferTransaction) - }) - - let title = (messeges != nil) ? String.adamantLocalized.transactionList.toChat : String.adamantLocalized.transactionList.startChat - - let toChat = UIContextualAction(style: .normal, title: title, handler: { (ac:UIContextualAction, view:UIView, success:(Bool) -> Void) in - guard let vc = self.router.get(scene: AdamantScene.Chats.chat) as? ChatViewController else { - // TODO: Log this - return - } - - guard let account = self.accountService.account else { - return - } - - vc.account = account - vc.chatroom = chatroom - vc.hidesBottomBarWhenPushed = true - - if let nav = self.navigationController { - nav.pushViewController(vc, animated: true) - } else { - self.present(vc, animated: true) - } - }) - - toChat.image = (messeges != nil) ? #imageLiteral(resourceName: "chats_tab") : #imageLiteral(resourceName: "Chat") - toChat.backgroundColor = UIColor.adamantPrimary - return UISwipeActionsConfiguration(actions: [toChat]) + func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + return cellHeight } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + return UITableViewCell(style: .default, reuseIdentifier: "cell") + } + } - -extension ADMTransactionsViewController: NSFetchedResultsControllerDelegate { - func controllerWillChangeContent(_ controller: NSFetchedResultsController) { - tableView.beginUpdates() - } - - func controllerDidChangeContent(_ controller: NSFetchedResultsController) { - tableView.endUpdates() - } - - func controller(_ controller: NSFetchedResultsController, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) { - switch type { - case .insert: - if let newIndexPath = newIndexPath { - tableView.insertRows(at: [newIndexPath], with: .automatic) - - if let transfer = anObject as? TransferTransaction { - transfer.isUnread = false - } - } - - case .delete: - if let indexPath = indexPath { - tableView.deleteRows(at: [indexPath], with: .automatic) - } - - case .update: - if let indexPath = indexPath, - let cell = self.tableView.cellForRow(at: indexPath) as? TransactionTableViewCell, - let transfer = anObject as? TransferTransaction { - configureCell(cell, for: transfer) - } - - default: - break - } - } -} - - From d21931816cb46f82dc45be695e3411f857d0b4c9 Mon Sep 17 00:00:00 2001 From: Anton B Date: Wed, 27 Jun 2018 10:24:43 +0300 Subject: [PATCH 022/175] ETH Transaction details confiramrions amount fix --- Adamant/Models/EthTransaction.swift | 8 +++++++- .../Services/EthApiService/EthApiService.swift | 17 ++++++++++------- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/Adamant/Models/EthTransaction.swift b/Adamant/Models/EthTransaction.swift index 14b2553be..814ca6359 100644 --- a/Adamant/Models/EthTransaction.swift +++ b/Adamant/Models/EthTransaction.swift @@ -180,6 +180,7 @@ extension EthTransaction: TransactionDetailsProtocol { struct web3EthTransaction { let transaction: EthereumTransaction let transactionBlock: Block? + let lastBlockNumber: BigUInt? } extension web3EthTransaction: TransactionDetailsProtocol { @@ -220,7 +221,12 @@ extension web3EthTransaction: TransactionDetailsProtocol { } var confirmationsValue: String { - return "--" + if let blockNumber = self.transactionBlock?.number, let lastBlockNumber = self.lastBlockNumber { + let confirmations = lastBlockNumber - blockNumber + return "\(confirmations)" + } else { + return "--" + } } var block: String { diff --git a/Adamant/Services/EthApiService/EthApiService.swift b/Adamant/Services/EthApiService/EthApiService.swift index 61a3ddebe..0620b94bf 100644 --- a/Adamant/Services/EthApiService/EthApiService.swift +++ b/Adamant/Services/EthApiService/EthApiService.swift @@ -263,24 +263,27 @@ class EthApiService: EthApiServiceProtocol { switch result { case .success(let transaction): if let number = transaction.blockNumber { -// let resultReceipt = self.web3.eth.getTransactionReceipt(hash) -// if case .success(let receipt) = resultReceipt { -// print(receipt) -// } + let resultBlockNumber = self.web3.eth.getBlockNumber() + guard case .success(let blockNumber) = resultBlockNumber else { + DispatchQueue.main.async { + completion(.success(web3EthTransaction(transaction: transaction.transaction, transactionBlock: nil, lastBlockNumber: nil))) + } + return + } let result = self.web3.eth.getBlockByNumber(number) guard case .success(let block) = result else { DispatchQueue.main.async { - completion(.success(web3EthTransaction(transaction: transaction.transaction, transactionBlock: nil))) + completion(.success(web3EthTransaction(transaction: transaction.transaction, transactionBlock: nil, lastBlockNumber: blockNumber))) } return } DispatchQueue.main.async { - completion(.success(web3EthTransaction(transaction: transaction.transaction, transactionBlock: block))) + completion(.success(web3EthTransaction(transaction: transaction.transaction, transactionBlock: block, lastBlockNumber: blockNumber))) } } else { DispatchQueue.main.async { - completion(.success(web3EthTransaction(transaction: transaction.transaction, transactionBlock: nil))) + completion(.success(web3EthTransaction(transaction: transaction.transaction, transactionBlock: nil, lastBlockNumber: nil))) } } break From 7a93c68901d0524d75fc4274b59087aceddd6ceb Mon Sep 17 00:00:00 2001 From: Anton B Date: Wed, 27 Jun 2018 10:47:32 +0300 Subject: [PATCH 023/175] Localization update --- Adamant/Assets/l18n/de.lproj/Localizable.strings | 3 +++ Adamant/Assets/l18n/en.lproj/Localizable.strings | 3 +++ Adamant/Assets/l18n/ru.lproj/Localizable.strings | 3 +++ .../Stories/Account/TransferViewController.swift | 4 +++- .../Chats/ChatViewController+MessageKit.swift | 3 ++- Adamant/Stories/Chats/ChatViewController.swift | 2 +- .../BaseTransactionDetailsViewController.swift | 13 +++++++++++++ .../TransactionDetailsViewController.swift | 13 ------------- 8 files changed, 28 insertions(+), 16 deletions(-) diff --git a/Adamant/Assets/l18n/de.lproj/Localizable.strings b/Adamant/Assets/l18n/de.lproj/Localizable.strings index fea8bf670..33ec2982a 100755 --- a/Adamant/Assets/l18n/de.lproj/Localizable.strings +++ b/Adamant/Assets/l18n/de.lproj/Localizable.strings @@ -511,6 +511,9 @@ /* Export transaction: 'Share transaction URL' button */ "TransactionDetailsScene.Share.URL" = "URL"; +/* Transaction details: 'Requesting Data' progress message. */ +"TransactionDetailsScene.RequestingData" = "Requesting Data.."; + /* TransfersProvider: Transaction not found error. %@ for transaction's ID */ "TransfersProvider.Error.TransactionNotFoundFormat" = "Transaktion mit der ID %@ nicht gefunden"; diff --git a/Adamant/Assets/l18n/en.lproj/Localizable.strings b/Adamant/Assets/l18n/en.lproj/Localizable.strings index e014c9d30..54dff5d64 100755 --- a/Adamant/Assets/l18n/en.lproj/Localizable.strings +++ b/Adamant/Assets/l18n/en.lproj/Localizable.strings @@ -619,6 +619,9 @@ /* Export transaction: 'Share transaction URL' button */ "TransactionDetailsScene.Share.URL" = "URL"; +/* Transaction details: 'Requesting Data' progress message. */ +"TransactionDetailsScene.RequestingData" = "Requesting Data.."; + /* TransfersProvider: Transaction not found error. %@ for transaction's ID */ "TransfersProvider.Error.TransactionNotFoundFormat" = "Transaction with id %@ not found"; diff --git a/Adamant/Assets/l18n/ru.lproj/Localizable.strings b/Adamant/Assets/l18n/ru.lproj/Localizable.strings index e4ca6429b..6da967b54 100644 --- a/Adamant/Assets/l18n/ru.lproj/Localizable.strings +++ b/Adamant/Assets/l18n/ru.lproj/Localizable.strings @@ -619,6 +619,9 @@ /* Export transaction: 'Share transaction URL' button */ "TransactionDetailsScene.Share.URL" = "URL"; +/* Transaction details: 'Requesting Data' progress message. */ +"TransactionDetailsScene.RequestingData" = "Запрашиваем данные.."; + /* TransfersProvider: Transaction not found error. %@ for transaction's ID */ "TransfersProvider.Error.TransactionNotFoundFormat" = "Транзакция с идентификатором %@ не найдена"; diff --git a/Adamant/Stories/Account/TransferViewController.swift b/Adamant/Stories/Account/TransferViewController.swift index 8252c4014..8a9eb6005 100644 --- a/Adamant/Stories/Account/TransferViewController.swift +++ b/Adamant/Stories/Account/TransferViewController.swift @@ -24,6 +24,8 @@ extension String.adamantLocalized { static let transferProcessingMessage = NSLocalizedString("TransferScene.SendingFundsProgress", comment: "Transfer: Processing message") static let transferSuccess = NSLocalizedString("TransferScene.TransferSuccessMessage", comment: "Transfer: Tokens transfered successfully message") + + static let send = NSLocalizedString("TransferScene.Row.Send", comment: "Transfer: Send button") private init() { } } @@ -74,7 +76,7 @@ class TransferViewController: FormViewController { case .address: return NSLocalizedString("TransferScene.Row.Recipient", comment: "Transfer: recipient address") case .fee: return NSLocalizedString("TransferScene.Row.TransactionFee", comment: "Transfer: transfer fee") case .total: return NSLocalizedString("TransferScene.Row.Total", comment: "Transfer: total amount of transaction: money to transfer adding fee") - case .sendButton: return NSLocalizedString("TransferScene.Row.Send", comment: "Transfer: Send button") + case .sendButton: return String.adamantLocalized.transfer.send } } } diff --git a/Adamant/Stories/Chats/ChatViewController+MessageKit.swift b/Adamant/Stories/Chats/ChatViewController+MessageKit.swift index 61c68df74..b35a23085 100644 --- a/Adamant/Stories/Chats/ChatViewController+MessageKit.swift +++ b/Adamant/Stories/Chats/ChatViewController+MessageKit.swift @@ -211,7 +211,8 @@ extension ChatViewController: MessageCellDelegate { guard let vc = router.get(scene: AdamantScene.Transactions.transactionDetails) as? BaseTransactionDetailsViewController else { break } - self.dialogService.showProgress(withMessage: String.adamantLocalized.transfer.transferProcessingMessage, userInteractionEnable: false) + + self.dialogService.showProgress(withMessage: String.adamantLocalized.transactionDetails.requestingDataProgressMessage, userInteractionEnable: false) self.ethApiService.getTransaction(byHash: transfer.hash) { (result) in switch result { diff --git a/Adamant/Stories/Chats/ChatViewController.swift b/Adamant/Stories/Chats/ChatViewController.swift index 9b8598a79..aa1f4dd36 100644 --- a/Adamant/Stories/Chats/ChatViewController.swift +++ b/Adamant/Stories/Chats/ChatViewController.swift @@ -76,7 +76,7 @@ class ChatViewController: MessagesViewController { $0.setSize(CGSize(width: 36, height: 36), animated: false) $0.image = #imageLiteral(resourceName: "attachment") }.onTouchUpInside { _ in - self.dialogService.showSystemActionSheet(title: "Send", message: "", actions: [ + self.dialogService.showSystemActionSheet(title: String.adamantLocalized.transfer.send, message: "", actions: [ UIAlertAction(title: "Ethereum", style: .default, handler: { (action) in if let ethAddress = self.ethAddress { // MARK: Show ETH transfer details diff --git a/Adamant/Stories/Transactions/BaseTransactionDetailsViewController.swift b/Adamant/Stories/Transactions/BaseTransactionDetailsViewController.swift index 3f3908ff2..240b529d7 100644 --- a/Adamant/Stories/Transactions/BaseTransactionDetailsViewController.swift +++ b/Adamant/Stories/Transactions/BaseTransactionDetailsViewController.swift @@ -12,6 +12,19 @@ import web3swift import BigInt import SafariServices +// MARK: - Localization +extension String.adamantLocalized { + struct transactionDetails { + static let title = NSLocalizedString("TransactionDetailsScene.Title", comment: "Transaction details: scene title") + static let requestingDataProgressMessage = NSLocalizedString("TransactionDetailsScene.RequestingData", comment: "Transaction details: 'Requesting Data' progress message.") + } +} + +extension String.adamantLocalized.alert { + static let exportUrlButton = NSLocalizedString("TransactionDetailsScene.Share.URL", comment: "Export transaction: 'Share transaction URL' button") + static let exportSummaryButton = NSLocalizedString("TransactionDetailsScene.Share.Summary", comment: "Export transaction: 'Share transaction summary' button") +} + class BaseTransactionDetailsViewController: FormViewController { // MARK: - Rows fileprivate enum Row: Int { diff --git a/Adamant/Stories/Transactions/TransactionDetailsViewController.swift b/Adamant/Stories/Transactions/TransactionDetailsViewController.swift index 068509620..e09176767 100644 --- a/Adamant/Stories/Transactions/TransactionDetailsViewController.swift +++ b/Adamant/Stories/Transactions/TransactionDetailsViewController.swift @@ -10,19 +10,6 @@ import UIKit import SafariServices -// MARK: - Localization -extension String.adamantLocalized { - struct transactionDetails { - static let title = NSLocalizedString("TransactionDetailsScene.Title", comment: "Transaction details: scene title") - } -} - -extension String.adamantLocalized.alert { - static let exportUrlButton = NSLocalizedString("TransactionDetailsScene.Share.URL", comment: "Export transaction: 'Share transaction URL' button") - static let exportSummaryButton = NSLocalizedString("TransactionDetailsScene.Share.Summary", comment: "Export transaction: 'Share transaction summary' button") -} - - // MARK: - class TransactionDetailsViewController: UIViewController { // MARK: - Rows From 7236a2d4345bd6d374bcf66dbdc37437558d1be4 Mon Sep 17 00:00:00 2001 From: Anton B Date: Wed, 27 Jun 2018 10:55:57 +0300 Subject: [PATCH 024/175] Cleanup --- Adamant.xcodeproj/project.pbxproj | 7 +- .../TransactionDetailsViewController.swift | 336 ------------------ .../TransactionDetailsViewController.xib | 39 -- 3 files changed, 1 insertion(+), 381 deletions(-) delete mode 100644 Adamant/Stories/Transactions/TransactionDetailsViewController.swift delete mode 100644 Adamant/Stories/Transactions/TransactionDetailsViewController.xib diff --git a/Adamant.xcodeproj/project.pbxproj b/Adamant.xcodeproj/project.pbxproj index c795da1a3..6456ec574 100644 --- a/Adamant.xcodeproj/project.pbxproj +++ b/Adamant.xcodeproj/project.pbxproj @@ -101,7 +101,6 @@ E94E7B06205D48B20042B639 /* TransactionsRoutes.swift in Sources */ = {isa = PBXBuildFile; fileRef = E94E7B05205D48B20042B639 /* TransactionsRoutes.swift */; }; E94E7B08205D4CB80042B639 /* SharedRoutes.swift in Sources */ = {isa = PBXBuildFile; fileRef = E94E7B07205D4CB80042B639 /* SharedRoutes.swift */; }; E94E7B0C205D5E4A0042B639 /* TransactionsViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = E94E7B0B205D5E4A0042B639 /* TransactionsViewController.xib */; }; - E94E7B0E205D5EA80042B639 /* TransactionDetailsViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = E94E7B0D205D5EA80042B639 /* TransactionDetailsViewController.xib */; }; E9502740202E257E002C1098 /* RepeaterService.swift in Sources */ = {isa = PBXBuildFile; fileRef = E950273F202E257E002C1098 /* RepeaterService.swift */; }; E950652120404BF0008352E5 /* AdamantUriBuilding.swift in Sources */ = {isa = PBXBuildFile; fileRef = E950652020404BF0008352E5 /* AdamantUriBuilding.swift */; }; E950652320404C84008352E5 /* AdamantUriTools.swift in Sources */ = {isa = PBXBuildFile; fileRef = E950652220404C84008352E5 /* AdamantUriTools.swift */; }; @@ -188,7 +187,6 @@ E9E7CDC82003F6D200DFC4DB /* TransactionTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = E9E7CDC62003F6D200DFC4DB /* TransactionTableViewCell.xib */; }; E9E7CDCA20040CC200DFC4DB /* Transaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E7CDC920040CC200DFC4DB /* Transaction.swift */; }; E9E7CDCC20040FDC00DFC4DB /* TransactionType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E7CDCB20040FDC00DFC4DB /* TransactionType.swift */; }; - E9EC34142005178500C0E546 /* TransactionDetailsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9EC34132005178500C0E546 /* TransactionDetailsViewController.swift */; }; E9EC3415200524CA00C0E546 /* Exo+2_100_normal.ttf in Resources */ = {isa = PBXBuildFile; fileRef = E91947A3200010DD001362F8 /* Exo+2_100_normal.ttf */; }; E9EC3416200524CA00C0E546 /* Exo+2_300_normal.ttf in Resources */ = {isa = PBXBuildFile; fileRef = E91947A4200010DE001362F8 /* Exo+2_300_normal.ttf */; }; E9EC3417200524CA00C0E546 /* Exo+2_400_italic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = E919479F200010DC001362F8 /* Exo+2_400_italic.ttf */; }; @@ -333,7 +331,6 @@ E94E7B05205D48B20042B639 /* TransactionsRoutes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionsRoutes.swift; sourceTree = ""; }; E94E7B07205D4CB80042B639 /* SharedRoutes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedRoutes.swift; sourceTree = ""; }; E94E7B0B205D5E4A0042B639 /* TransactionsViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TransactionsViewController.xib; sourceTree = ""; }; - E94E7B0D205D5EA80042B639 /* TransactionDetailsViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TransactionDetailsViewController.xib; sourceTree = ""; }; E950273F202E257E002C1098 /* RepeaterService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepeaterService.swift; sourceTree = ""; }; E950652020404BF0008352E5 /* AdamantUriBuilding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantUriBuilding.swift; sourceTree = ""; }; E950652220404C84008352E5 /* AdamantUriTools.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantUriTools.swift; sourceTree = ""; }; @@ -423,7 +420,6 @@ E9E7CDC62003F6D200DFC4DB /* TransactionTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TransactionTableViewCell.xib; sourceTree = ""; }; E9E7CDC920040CC200DFC4DB /* Transaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Transaction.swift; sourceTree = ""; }; E9E7CDCB20040FDC00DFC4DB /* TransactionType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionType.swift; sourceTree = ""; }; - E9EC34132005178500C0E546 /* TransactionDetailsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionDetailsViewController.swift; sourceTree = ""; }; E9EC342020052ABB00C0E546 /* TransferViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransferViewController.swift; sourceTree = ""; }; E9EC344420066D4A00C0E546 /* AdamantTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AdamantTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; E9EC344620066D4A00C0E546 /* AddressValidationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddressValidationTests.swift; sourceTree = ""; }; @@ -696,8 +692,6 @@ 64FA53CC20E1300A006783C9 /* ETHTransactionsViewController.swift */, E94E7B0B205D5E4A0042B639 /* TransactionsViewController.xib */, 64FA53D020E24941006783C9 /* BaseTransactionDetailsViewController.swift */, - E9EC34132005178500C0E546 /* TransactionDetailsViewController.swift */, - E94E7B0D205D5EA80042B639 /* TransactionDetailsViewController.xib */, E9E7CDC52003F6D200DFC4DB /* TransactionTableViewCell.swift */, E9E7CDC62003F6D200DFC4DB /* TransactionTableViewCell.xib */, ); @@ -1028,6 +1022,7 @@ E95CB45E205D7F9600A7218E /* Localizable.stringsdict in Resources */, E94E7B0E205D5EA80042B639 /* TransactionDetailsViewController.xib in Resources */, E91E5BF420DAF3AB00B06B3C /* NodeCell.xib in Resources */, + E91E5BF420DAF3AB00B06B3C /* NodeRow.xib in Resources */, E9EC341D200524CA00C0E546 /* Roboto_400_normal.ttf in Resources */, E9A174B920587B84003667CD /* notification.mp3 in Resources */, E9EC3418200524CA00C0E546 /* Exo+2_400_normal.ttf in Resources */, diff --git a/Adamant/Stories/Transactions/TransactionDetailsViewController.swift b/Adamant/Stories/Transactions/TransactionDetailsViewController.swift deleted file mode 100644 index e09176767..000000000 --- a/Adamant/Stories/Transactions/TransactionDetailsViewController.swift +++ /dev/null @@ -1,336 +0,0 @@ -// -// TransactionDetailsViewController.swift -// Adamant -// -// Created by Anokhov Pavel on 09.01.2018. -// Copyright © 2018 Adamant. All rights reserved. -// - -import UIKit -import SafariServices - - -// MARK: - -class TransactionDetailsViewController: UIViewController { - // MARK: - Rows - fileprivate enum Row: Int { - case transactionNumber = 0 - case from - case to - case date - case amount - case fee - case confirmations - case block - case openInExplorer - case openChat // if transaction.chatroom.isHidden, numberOfRowsInSection will return total-1 - - static let total = 10 - - var localized: String { - switch self { - case .transactionNumber: return NSLocalizedString("TransactionDetailsScene.Row.Id", comment: "Transaction details: Id row.") - case .from: return NSLocalizedString("TransactionDetailsScene.Row.From", comment: "Transaction details: sender row.") - case .to: return NSLocalizedString("TransactionDetailsScene.Row.To", comment: "Transaction details: recipient row.") - case .date: return NSLocalizedString("TransactionDetailsScene.Row.Date", comment: "Transaction details: date row.") - case .amount: return NSLocalizedString("TransactionDetailsScene.Row.Amount", comment: "Transaction details: amount row.") - case .fee: return NSLocalizedString("TransactionDetailsScene.Row.Fee", comment: "Transaction details: fee row.") - case .confirmations: return NSLocalizedString("TransactionDetailsScene.Row.Confirmations", comment: "Transaction details: confirmations row.") - case .block: return NSLocalizedString("TransactionDetailsScene.Row.Block", comment: "Transaction details: Block id row.") - case .openInExplorer: return NSLocalizedString("TransactionDetailsScene.Row.Explorer", comment: "Transaction details: 'Open transaction in explorer' row.") - case .openChat: return "" - } - } - - var image: UIImage? { - switch self { - case .openInExplorer: return #imageLiteral(resourceName: "row_icon_placeholder") - case .openChat: return #imageLiteral(resourceName: "row_icon_placeholder") - - default: return nil - } - } - } - - // MARK: - Dependencies - var accountService: AccountService! - var dialogService: DialogService! - var transfersProvider: TransfersProvider! - var router: Router! - - // MARK: - Properties - private let cellIdentifier = "cell" - var transaction: TransferTransaction? - var explorerUrl: URL! - var haveChatroom = false - - private let autoupdateInterval: TimeInterval = 5.0 - - weak var timer: Timer? - - // MARK: - IBOutlets - @IBOutlet weak var tableView: UITableView! - - - // MARK: - Lifecycle - - override func viewDidLoad() { - if #available(iOS 11.0, *) { - navigationController?.navigationBar.prefersLargeTitles = true - } - - navigationItem.title = String.adamantLocalized.transactionDetails.title - navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .action, target: self, action: #selector(share)) - tableView.dataSource = self - tableView.delegate = self - - if let transaction = transaction { - if let chatroom = transaction.partner?.chatroom, let transactions = chatroom.transactions { - let messeges = transactions.first (where: { (object) -> Bool in - return !(object is TransferTransaction) - }) - - haveChatroom = (messeges != nil) - } - - tableView.reloadData() - - if let id = transaction.transactionId { - explorerUrl = URL(string: "https://explorer.adamant.im/tx/\(id)") - } - } else { - self.navigationItem.rightBarButtonItems = nil - } - - startUpdate() - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - if let indexPath = tableView.indexPathForSelectedRow { - tableView.deselectRow(at: indexPath, animated: animated) - } - } - - deinit { - stopUpdate() - } - - - // MARK: - IBActions - - @IBAction func share(_ sender: Any) { - guard let transaction = transaction, let url = explorerUrl else { - return - } - - let alert = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) - alert.addAction(UIAlertAction(title: String.adamantLocalized.alert.cancel, style: .cancel, handler: nil)) - - // URL - alert.addAction(UIAlertAction(title: String.adamantLocalized.alert.exportUrlButton, style: .default) { [weak self] _ in - let alert = UIActivityViewController(activityItems: [url], applicationActivities: nil) - self?.present(alert, animated: true, completion: nil) - }) - - // Description - alert.addAction(UIAlertAction(title: String.adamantLocalized.alert.exportSummaryButton, style: .default, handler: { [weak self] _ in - let text = AdamantFormattingTools.summaryFor(transaction: transaction, url: url) - let alert = UIActivityViewController(activityItems: [text], applicationActivities: nil) - self?.present(alert, animated: true, completion: nil) - })) - - present(alert, animated: true, completion: nil) - } - - - -} - - -// MARK: - UITableView -extension TransactionDetailsViewController: UITableViewDataSource, UITableViewDelegate { - func numberOfSections(in tableView: UITableView) -> Int { - return 1 - } - - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - if transaction != nil { - guard let hidden = transaction?.chatroom?.isHidden, !hidden else { - return Row.total - 1 - } - - return Row.total - } else { - return 0 - } - } - - func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { - return UIView() - } - - func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - return 50 - } - - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - guard let row = Row(rawValue: indexPath.row) else { - tableView.deselectRow(at: indexPath, animated: true) - return - } - - switch row { - case .openInExplorer: - if let url = explorerUrl { - let safari = SFSafariViewController(url: url) - safari.preferredControlTintColor = UIColor.adamantPrimary - present(safari, animated: true, completion: nil) - } - - case .openChat: - // TODO: Log errors - guard let vc = self.router.get(scene: AdamantScene.Chats.chat) as? ChatViewController else { - dialogService.showError(withMessage: "TransactionDetailsViewController: Failed to get ChatViewController", error: nil) - break - } - - guard let chatroom = transaction?.partner?.chatroom else { - dialogService.showError(withMessage: "TransactionDetailsViewController: Failed to get chatroom for transaction.", error: nil) - break - } - - guard let account = self.accountService.account else { - dialogService.showError(withMessage: "TransactionDetailsViewController: User not logged.", error: nil) - break - } - - vc.account = account - vc.chatroom = chatroom - vc.hidesBottomBarWhenPushed = true - - if let nav = self.navigationController { - nav.pushViewController(vc, animated: true) - } else { - self.present(vc, animated: true) - } - - default: - let share: String - if row == .date, let date = transaction?.date as Date? { - share = DateFormatter.localizedString(from: date, dateStyle: .medium, timeStyle: .medium) - - } else if let cell = tableView.cellForRow(at: indexPath), let details = cell.detailTextLabel?.text { - share = details - } else { - tableView.deselectRow(at: indexPath, animated: true) - break - } - - dialogService.presentShareAlertFor(string: share, - types: [.copyToPasteboard, .share], - excludedActivityTypes: nil, - animated: true) - { - tableView.deselectRow(at: indexPath, animated: true) - } - } - } -} - - -// MARK: - UITableView Cells -extension TransactionDetailsViewController { - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - guard let transaction = transaction, let row = Row(rawValue: indexPath.row) else { - // TODO: Display & Log error - return UITableViewCell(style: .default, reuseIdentifier: cellIdentifier) - } - - let cell: UITableViewCell - if let c = tableView.dequeueReusableCell(withIdentifier: cellIdentifier) { - cell = c - cell.accessoryType = .none - cell.imageView?.image = nil - } else { - cell = UITableViewCell(style: .value1, reuseIdentifier: cellIdentifier) - } - - cell.textLabel?.text = row.localized - cell.imageView?.image = row.image - - switch row { - case .amount: - if let amount = transaction.amount { - cell.detailTextLabel?.text = AdamantUtilities.format(balance: amount) - } - - case .date: - if let date = transaction.date as Date? { - cell.detailTextLabel?.text = date.humanizedDateTimeFull() - } - - case .confirmations: - cell.detailTextLabel?.text = String(transaction.confirmations) - - case .fee: - if let fee = transaction.fee { - cell.detailTextLabel?.text = AdamantUtilities.format(balance: fee) - } - - case .transactionNumber: - if let id = transaction.transactionId { - cell.detailTextLabel?.text = String(id) - } - - case .from: - cell.detailTextLabel?.text = transaction.senderId - - case .to: - cell.detailTextLabel?.text = transaction.recipientId - - case .block: - cell.detailTextLabel?.text = transaction.blockId - - case .openInExplorer: - cell.detailTextLabel?.text = nil - cell.accessoryType = .disclosureIndicator - case .openChat: - cell.textLabel?.text = (self.haveChatroom) ? String.adamantLocalized.transactionList.toChat : String.adamantLocalized.transactionList.startChat - cell.detailTextLabel?.text = nil - cell.accessoryType = .disclosureIndicator -// cell.imageView?.image = (haveChatroom) ? #imageLiteral(resourceName: "chats_tab") : #imageLiteral(resourceName: "Chat") - } - - return cell - } -} - - -// MARK: - Autoupdate -extension TransactionDetailsViewController { - func startUpdate() { - timer?.invalidate() - timer = Timer.scheduledTimer(withTimeInterval: autoupdateInterval, repeats: true) { [weak self] _ in - guard let id = self?.transaction?.transactionId else { - return - } - - self?.transfersProvider.refreshTransfer(id: id) { result in - switch result { - case .success: - DispatchQueue.main.async { - self?.tableView.reloadData() - } - - case .failure: - return - } - } - } - } - - func stopUpdate() { - timer?.invalidate() - } -} diff --git a/Adamant/Stories/Transactions/TransactionDetailsViewController.xib b/Adamant/Stories/Transactions/TransactionDetailsViewController.xib deleted file mode 100644 index 6f359e85e..000000000 --- a/Adamant/Stories/Transactions/TransactionDetailsViewController.xib +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - From cf516d06cfe2362aa161c0cda6fb8528aa65a93f Mon Sep 17 00:00:00 2001 From: Anton B Date: Wed, 27 Jun 2018 10:56:27 +0300 Subject: [PATCH 025/175] Switch to Ethereum Mainnet API --- Adamant/AppDelegate.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Adamant/AppDelegate.swift b/Adamant/AppDelegate.swift index 001673283..e5500d348 100644 --- a/Adamant/AppDelegate.swift +++ b/Adamant/AppDelegate.swift @@ -47,8 +47,8 @@ struct AdamantResources { ] static let ethServers = [ -// "https://api.myetherapi.com/eth" - "https://ropsten.infura.io/" // test network + "https://api.myetherapi.com/eth" +// "https://ropsten.infura.io/" // test network ] // Addresses From 58fe19665511027ad5f4703a400dc26eebdb3f49 Mon Sep 17 00:00:00 2001 From: Anton B Date: Wed, 27 Jun 2018 11:36:16 +0300 Subject: [PATCH 026/175] Improve errors handler --- Adamant/Assets/l18n/de.lproj/Localizable.strings | 3 --- Adamant/Assets/l18n/en.lproj/Localizable.strings | 9 +++++++++ Adamant/Assets/l18n/ru.lproj/Localizable.strings | 9 +++++++++ .../Stories/Account/TransferViewController.swift | 14 +++++++++++--- .../ETHTransactionsViewController.swift | 7 ++++++- .../Transactions/TransactionsViewController.swift | 1 + 6 files changed, 36 insertions(+), 7 deletions(-) diff --git a/Adamant/Assets/l18n/de.lproj/Localizable.strings b/Adamant/Assets/l18n/de.lproj/Localizable.strings index 33ec2982a..fea8bf670 100755 --- a/Adamant/Assets/l18n/de.lproj/Localizable.strings +++ b/Adamant/Assets/l18n/de.lproj/Localizable.strings @@ -511,9 +511,6 @@ /* Export transaction: 'Share transaction URL' button */ "TransactionDetailsScene.Share.URL" = "URL"; -/* Transaction details: 'Requesting Data' progress message. */ -"TransactionDetailsScene.RequestingData" = "Requesting Data.."; - /* TransfersProvider: Transaction not found error. %@ for transaction's ID */ "TransfersProvider.Error.TransactionNotFoundFormat" = "Transaktion mit der ID %@ nicht gefunden"; diff --git a/Adamant/Assets/l18n/en.lproj/Localizable.strings b/Adamant/Assets/l18n/en.lproj/Localizable.strings index 54dff5d64..34c268577 100755 --- a/Adamant/Assets/l18n/en.lproj/Localizable.strings +++ b/Adamant/Assets/l18n/en.lproj/Localizable.strings @@ -583,6 +583,12 @@ /* TransactionList: Start Chat button */ "TransactionListScene.StartChat" = "Start Chat"; +/* TransactionList: 'Transactions not found' message. */ +"TransactionListScene.Error.NotFound" = "Transactions not found"; + +/* TransactionList: 'Transactions not found' localize error message from API. */ +"No transactions found" = "Transactions not found"; + /* Transaction details: scene title */ "TransactionDetailsScene.Title" = "Details"; @@ -673,6 +679,9 @@ /* Transfer: Confirm transfer alert: Send tokens button */ "TransferScene.Send" = "Send"; +/* Transfer: Confirm transfer alert: 'Can't Undo' message */ +"TransferScene.CantUndo" = "You can't undo this action."; + /* Transfer: Confirm transfer %1$@ tokens to %2$@ message. Note two variables: at runtime %1$@ will be amount (with ADM suffix), and %2$@ will be recipient address. You can use address before amount with this so called 'position tokens'. */ "TransferScene.SendConfirmFormat" = "Send %1$@ to %2$@?"; diff --git a/Adamant/Assets/l18n/ru.lproj/Localizable.strings b/Adamant/Assets/l18n/ru.lproj/Localizable.strings index 6da967b54..21e1f64e4 100644 --- a/Adamant/Assets/l18n/ru.lproj/Localizable.strings +++ b/Adamant/Assets/l18n/ru.lproj/Localizable.strings @@ -583,6 +583,12 @@ /* TransactionList: Start Chat button */ "TransactionListScene.StartChat" = "Начать чат"; +/* TransactionList: 'Transactions not found' message. */ +"TransactionListScene.Error.NotFound" = "Транзакции не найдены"; + +/* TransactionList: 'Transactions not found' localize error message from API. */ +"No transactions found" = "Транзакции не найдены"; + /* Transaction details: scene title */ "TransactionDetailsScene.Title" = "Подробности"; @@ -673,6 +679,9 @@ /* Transfer: Confirm transfer alert: Send tokens button */ "TransferScene.Send" = "Отправить"; +/* Transfer: Confirm transfer alert: 'Can't Undo' message */ +"TransferScene.CantUndo" = "Вы не сможете отменить это действие."; + /* Transfer: Confirm transfer %1$@ tokens to %2$@ message. Note two variables: at runtime %1$@ will be amount (with ADM suffix), and %2$@ will be recipient address. You can use address before amount with this so called 'position tokens'. */ "TransferScene.SendConfirmFormat" = "Отправить %1$@ получателю %2$@?"; diff --git a/Adamant/Stories/Account/TransferViewController.swift b/Adamant/Stories/Account/TransferViewController.swift index 8a9eb6005..ba061ce3c 100644 --- a/Adamant/Stories/Account/TransferViewController.swift +++ b/Adamant/Stories/Account/TransferViewController.swift @@ -25,7 +25,9 @@ extension String.adamantLocalized { static let transferProcessingMessage = NSLocalizedString("TransferScene.SendingFundsProgress", comment: "Transfer: Processing message") static let transferSuccess = NSLocalizedString("TransferScene.TransferSuccessMessage", comment: "Transfer: Tokens transfered successfully message") - static let send = NSLocalizedString("TransferScene.Row.Send", comment: "Transfer: Send button") + static let send = NSLocalizedString("TransferScene.Send", comment: "Transfer: Send button") + + static let cantUndo = NSLocalizedString("TransferScene.CantUndo", comment: "Transfer: Send button") private init() { } } @@ -355,6 +357,7 @@ class TransferViewController: FormViewController { guard let amount = row.value else { totalAmount = nil sendButton.disabled = true + sendButton.evaluateDisabled() row.cell.titleLabel?.textColor = .black return } @@ -429,7 +432,7 @@ class TransferViewController: FormViewController { return } - let alert = UIAlertController(title: String.localizedStringWithFormat(String.adamantLocalized.alert.confirmSendMessageFormat, "\(amount) \(AdamantUtilities.currencyCode)", recipient), message: "You can't undo this action.", preferredStyle: .alert) + let alert = UIAlertController(title: String.localizedStringWithFormat(String.adamantLocalized.alert.confirmSendMessageFormat, "\(amount) \(AdamantUtilities.currencyCode)", recipient), message: String.adamantLocalized.transfer.cantUndo, preferredStyle: .alert) let cancelAction = UIAlertAction(title: String.adamantLocalized.alert.cancel , style: .cancel, handler: nil) let sendAction = UIAlertAction(title: String.adamantLocalized.alert.send, style: .default, handler: { _ in dialogService.showProgress(withMessage: String.adamantLocalized.transfer.transferProcessingMessage, userInteractionEnable: false) @@ -476,7 +479,12 @@ class TransferViewController: FormViewController { return } - let alert = UIAlertController(title: String.localizedStringWithFormat(String.adamantLocalized.alert.confirmSendMessageFormat, "\(amount) ETH", recipient), message: "You can't undo this action.", preferredStyle: .alert) + guard let totalAmount = totalAmount, totalAmount <= maxToTransfer else { + dialogService.showWarning(withMessage: String.adamantLocalized.transfer.amountTooHigh) + return + } + + let alert = UIAlertController(title: String.localizedStringWithFormat(String.adamantLocalized.alert.confirmSendMessageFormat, "\(amount) ETH", recipient), message: String.adamantLocalized.transfer.cantUndo, preferredStyle: .alert) let cancelAction = UIAlertAction(title: String.adamantLocalized.alert.cancel , style: .cancel, handler: nil) let sendAction = UIAlertAction(title: String.adamantLocalized.alert.send, style: .default, handler: { _ in self.sendEth(to: recipient, amount: amount) diff --git a/Adamant/Stories/Transactions/ETHTransactionsViewController.swift b/Adamant/Stories/Transactions/ETHTransactionsViewController.swift index 97ec03343..2f76e5c12 100644 --- a/Adamant/Stories/Transactions/ETHTransactionsViewController.swift +++ b/Adamant/Stories/Transactions/ETHTransactionsViewController.swift @@ -36,7 +36,12 @@ class ETHTransactionsViewController: TransactionsViewController { } break case .failure(let error): - self.dialogService.showError(withMessage: "", error: 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 { diff --git a/Adamant/Stories/Transactions/TransactionsViewController.swift b/Adamant/Stories/Transactions/TransactionsViewController.swift index 732e506d7..e85beba7f 100644 --- a/Adamant/Stories/Transactions/TransactionsViewController.swift +++ b/Adamant/Stories/Transactions/TransactionsViewController.swift @@ -14,6 +14,7 @@ extension String.adamantLocalized { static let title = NSLocalizedString("TransactionListScene.Title", comment: "TransactionList: scene title") static let toChat = NSLocalizedString("TransactionListScene.ToChat", comment: "TransactionList: To Chat button") static let startChat = NSLocalizedString("TransactionListScene.StartChat", comment: "TransactionList: Start Chat button") + static let notFound = NSLocalizedString("TransactionListScene.Error.NotFound", comment: "TransactionList: 'Transactions not found' message.") } } From 7eb95f506f90cb102d89289422b1ea89caccaecb Mon Sep 17 00:00:00 2001 From: Anton B Date: Wed, 27 Jun 2018 11:49:23 +0300 Subject: [PATCH 027/175] Add strings file for translation --- Adamant.xcodeproj/project.pbxproj | 4 ++++ Adamant/Assets/eth_l18n.strings | 16 ++++++++++++++++ 2 files changed, 20 insertions(+) create mode 100644 Adamant/Assets/eth_l18n.strings diff --git a/Adamant.xcodeproj/project.pbxproj b/Adamant.xcodeproj/project.pbxproj index 6456ec574..a098ce4d7 100644 --- a/Adamant.xcodeproj/project.pbxproj +++ b/Adamant.xcodeproj/project.pbxproj @@ -24,6 +24,7 @@ E90055F920ECD86800D0CB2D /* SecurityViewController+StayIn.swift in Sources */ = {isa = PBXBuildFile; fileRef = E90055F820ECD86800D0CB2D /* SecurityViewController+StayIn.swift */; }; E90055FB20ECE78A00D0CB2D /* SecurityViewController+notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = E90055FA20ECE78A00D0CB2D /* SecurityViewController+notifications.swift */; }; 644D3DBB20BD5F22002EB002 /* Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = 644D3DBA20BD5F22002EB002 /* Status.swift */; }; + 642361BE20E3869D0061559E /* eth_l18n.strings in Resources */ = {isa = PBXBuildFile; fileRef = 642361BD20E3869D0061559E /* eth_l18n.strings */; }; 649A694720DD427200D27E91 /* TransferType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649A694620DD427200D27E91 /* TransferType.swift */; }; 64BD2B7520E2814B00E2CD36 /* EthTransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64BD2B7420E2814B00E2CD36 /* EthTransaction.swift */; }; 64BD2B7720E2820300E2CD36 /* TransactionDetailsProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64BD2B7620E2820300E2CD36 /* TransactionDetailsProtocol.swift */; }; @@ -228,6 +229,7 @@ 64D059FE20D3116A003AD655 /* NodesListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NodesListViewController.swift; sourceTree = ""; }; 64E8304F20F5FEEF006FA590 /* VotesAsset.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VotesAsset.swift; sourceTree = ""; }; 644D3DBA20BD5F22002EB002 /* Status.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Status.swift; sourceTree = ""; }; + 642361BD20E3869D0061559E /* eth_l18n.strings */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; path = eth_l18n.strings; sourceTree = ""; }; 649A694620DD427200D27E91 /* TransferType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransferType.swift; sourceTree = ""; }; 64BD2B7420E2814B00E2CD36 /* EthTransaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EthTransaction.swift; sourceTree = ""; }; 64BD2B7620E2820300E2CD36 /* TransactionDetailsProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionDetailsProtocol.swift; sourceTree = ""; }; @@ -629,6 +631,7 @@ E9A174B820587B83003667CD /* notification.mp3 */, E9256F752039A9A200DE86E9 /* LaunchScreen.storyboard */, E9C51EEC2011416E00385EB7 /* adamant-core.js */, + 642361BD20E3869D0061559E /* eth_l18n.strings */, ); path = Assets; sourceTree = ""; @@ -1006,6 +1009,7 @@ E9C51EED2011416E00385EB7 /* adamant-core.js in Resources */, E90A4945204C6204009F6A65 /* PassphraseCell.xib in Resources */, E941CCDF20E7B70200C96220 /* WalletCollectionViewCell.xib in Resources */, + 642361BE20E3869D0061559E /* eth_l18n.strings in Resources */, E9E7CDC82003F6D200DFC4DB /* TransactionTableViewCell.xib in Resources */, E9EC3417200524CA00C0E546 /* Exo+2_400_italic.ttf in Resources */, E94E7B01205D3F090042B639 /* ChatListViewController.xib in Resources */, diff --git a/Adamant/Assets/eth_l18n.strings b/Adamant/Assets/eth_l18n.strings new file mode 100644 index 000000000..4b2b3d9f3 --- /dev/null +++ b/Adamant/Assets/eth_l18n.strings @@ -0,0 +1,16 @@ +/* + eth_l18n.strings + Adamant + + Created by Anton Boyarkin on 27/06/2018. + Copyright © 2018 Adamant. All rights reserved. +*/ + +/* TransactionList: 'Transactions not found' message. */ +"TransactionListScene.Error.NotFound" = "Transactions not found"; + +/* TransactionList: 'Transactions not found' localize error message from API. */ +"No transactions found" = "Transactions not found"; + +/* Transaction details: 'Requesting Data' progress message. */ +"TransactionDetailsScene.RequestingData" = "Requesting Data.."; From e5f8f43c1619aea4f75021bc442ab056ae56eade Mon Sep 17 00:00:00 2001 From: Anton B Date: Wed, 27 Jun 2018 14:23:27 +0300 Subject: [PATCH 028/175] Minor Improvments Change Eth node url Add explorer url for eth trandaction details Add eth address validation --- Adamant/AppDelegate.swift | 4 +- Adamant/Models/EthTransaction.swift | 4 +- .../Account/TransferViewController.swift | 49 ++++++++++--------- 3 files changed, 31 insertions(+), 26 deletions(-) diff --git a/Adamant/AppDelegate.swift b/Adamant/AppDelegate.swift index e5500d348..3456b34d8 100644 --- a/Adamant/AppDelegate.swift +++ b/Adamant/AppDelegate.swift @@ -47,8 +47,8 @@ struct AdamantResources { ] static let ethServers = [ - "https://api.myetherapi.com/eth" -// "https://ropsten.infura.io/" // test network +// "https://ethnode1.adamant.im/" + "https://ropsten.infura.io/" // test network ] // Addresses diff --git a/Adamant/Models/EthTransaction.swift b/Adamant/Models/EthTransaction.swift index 814ca6359..cbe1c8108 100644 --- a/Adamant/Models/EthTransaction.swift +++ b/Adamant/Models/EthTransaction.swift @@ -132,11 +132,11 @@ extension EthTransaction: TransactionDetailsProtocol { } var showGoToExplorer: Bool { - return false + return true } var explorerUrl: URL? { - return nil + return URL(string: "https://etherscan.io/tx/\(id)") } var showGoToChat: Bool { diff --git a/Adamant/Stories/Account/TransferViewController.swift b/Adamant/Stories/Account/TransferViewController.swift index ba061ce3c..d0577f3d5 100644 --- a/Adamant/Stories/Account/TransferViewController.swift +++ b/Adamant/Stories/Account/TransferViewController.swift @@ -197,15 +197,17 @@ class TransferViewController: FormViewController { $0.placeholder = String.adamantLocalized.transfer.addressPlaceholder $0.tag = Row.address.tag $0.add(rule: RuleClosure(closure: { value -> ValidationError? in - guard let value = value?.uppercased() else { + guard let value = value?.lowercased() else { return ValidationError(msg: String.adamantLocalized.transfer.addressValidationError) } - switch AdamantUtilities.validateAdamantAddress(address: value) { - case .valid: - return nil - - case .system, .invalid: + if let walletAddress = EthereumAddress(value) { + if walletAddress.isValid { + return nil + } else { + return ValidationError(msg: String.adamantLocalized.transfer.addressValidationError) + } + } else { return ValidationError(msg: String.adamantLocalized.transfer.addressValidationError) } })) @@ -270,22 +272,20 @@ class TransferViewController: FormViewController { $0.placeholder = String.adamantLocalized.transfer.addressPlaceholder $0.tag = Row.address.tag $0.value = toAddress - - // TODO: Validation for ETH address -// $0.add(rule: RuleClosure(closure: { value -> ValidationError? in -// guard let value = value?.uppercased() else { -// return ValidationError(msg: String.adamantLocalized.transfer.addressValidationError) -// } -// -// switch AdamantUtilities.validateAdamantAddress(address: value) { -// case .valid: -// return nil -// -// case .system, .invalid: -// return ValidationError(msg: String.adamantLocalized.transfer.addressValidationError) -// } -// })) -// $0.validationOptions = .validatesOnBlur + $0.add(rule: RuleClosure(closure: { value -> ValidationError? in + guard let value = value?.uppercased() else { + return ValidationError(msg: String.adamantLocalized.transfer.addressValidationError) + } + + switch AdamantUtilities.validateAdamantAddress(address: value) { + case .valid: + return nil + + case .system, .invalid: + return ValidationError(msg: String.adamantLocalized.transfer.addressValidationError) + } + })) + $0.validationOptions = .validatesOnBlur }.cellUpdate({ (cell, row) in cell.titleLabel?.textColor = row.isValid ? .black : .red }) @@ -479,6 +479,11 @@ class TransferViewController: FormViewController { return } + guard recipientRow.isValid else { + dialogService.showWarning(withMessage: (recipientRow.validationErrors.first?.msg) ?? "Invalid Address") + return + } + guard let totalAmount = totalAmount, totalAmount <= maxToTransfer else { dialogService.showWarning(withMessage: String.adamantLocalized.transfer.amountTooHigh) return From dd3b0862749e85ec1005fe146c54e8777afcf6b4 Mon Sep 17 00:00:00 2001 From: Anton B Date: Fri, 29 Jun 2018 12:33:44 +0300 Subject: [PATCH 029/175] Switch back to ETH Mainnet --- Adamant/AppDelegate.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Adamant/AppDelegate.swift b/Adamant/AppDelegate.swift index 3456b34d8..6bb652733 100644 --- a/Adamant/AppDelegate.swift +++ b/Adamant/AppDelegate.swift @@ -47,8 +47,8 @@ struct AdamantResources { ] static let ethServers = [ -// "https://ethnode1.adamant.im/" - "https://ropsten.infura.io/" // test network + "https://ethnode1.adamant.im/" +// "https://ropsten.infura.io/" // test network ] // Addresses From fe553c00ceaea7be0e414a0520c223ca67e2d93c Mon Sep 17 00:00:00 2001 From: Anton B Date: Fri, 29 Jun 2018 13:04:08 +0300 Subject: [PATCH 030/175] Fix Transaction screen address validation --- .../Account/TransferViewController.swift | 36 ++++++++++--------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/Adamant/Stories/Account/TransferViewController.swift b/Adamant/Stories/Account/TransferViewController.swift index d0577f3d5..0e11b9ed0 100644 --- a/Adamant/Stories/Account/TransferViewController.swift +++ b/Adamant/Stories/Account/TransferViewController.swift @@ -197,17 +197,15 @@ class TransferViewController: FormViewController { $0.placeholder = String.adamantLocalized.transfer.addressPlaceholder $0.tag = Row.address.tag $0.add(rule: RuleClosure(closure: { value -> ValidationError? in - guard let value = value?.lowercased() else { + guard let value = value?.uppercased() else { return ValidationError(msg: String.adamantLocalized.transfer.addressValidationError) } - if let walletAddress = EthereumAddress(value) { - if walletAddress.isValid { - return nil - } else { - return ValidationError(msg: String.adamantLocalized.transfer.addressValidationError) - } - } else { + switch AdamantUtilities.validateAdamantAddress(address: value) { + case .valid: + return nil + + case .system, .invalid: return ValidationError(msg: String.adamantLocalized.transfer.addressValidationError) } })) @@ -221,8 +219,8 @@ class TransferViewController: FormViewController { $0.placeholder = String.adamantLocalized.transfer.amountPlaceholder $0.tag = Row.amount.tag $0.formatter = AdamantUtilities.currencyFormatter - // $0.add(rule: RuleSmallerOrEqualThan(max: maxToTransfer)) - // $0.validationOptions = .validatesOnChange + $0.add(rule: RuleSmallerOrEqualThan(max: maxToTransfer)) + $0.validationOptions = .validatesOnChange }.onChange(amountChanged) <<< DecimalRow() { $0.title = Row.fee.localized @@ -273,15 +271,17 @@ class TransferViewController: FormViewController { $0.tag = Row.address.tag $0.value = toAddress $0.add(rule: RuleClosure(closure: { value -> ValidationError? in - guard let value = value?.uppercased() else { + guard let value = value?.lowercased() else { return ValidationError(msg: String.adamantLocalized.transfer.addressValidationError) } - - switch AdamantUtilities.validateAdamantAddress(address: value) { - case .valid: - return nil - - case .system, .invalid: + + if let walletAddress = EthereumAddress(value) { + if walletAddress.isValid { + return nil + } else { + return ValidationError(msg: String.adamantLocalized.transfer.addressValidationError) + } + } else { return ValidationError(msg: String.adamantLocalized.transfer.addressValidationError) } })) @@ -324,6 +324,7 @@ class TransferViewController: FormViewController { guard let amount = row.value else { totalAmount = nil sendButton.disabled = true + sendButton.evaluateDisabled() row.cell.titleLabel?.textColor = .black return } @@ -347,6 +348,7 @@ class TransferViewController: FormViewController { sendButton.disabled = true row.cell.titleLabel?.textColor = .black } + sendButton.evaluateDisabled() } private func ethAmountChanged(row: DecimalRow) { From 930cd7761a306bda279d786b984d5ebd732bbe0a Mon Sep 17 00:00:00 2001 From: Anton B Date: Fri, 29 Jun 2018 13:09:50 +0300 Subject: [PATCH 031/175] Podfile.lock Update --- Podfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Podfile.lock b/Podfile.lock index 242e1390c..60bf5abb9 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -122,6 +122,6 @@ SPEC CHECKSUMS: Swinject: f7f15a9672e99328c1f07f277091087917215700 web3swift: 79f72b48005f1b28e73191eaf78194d6cf5f8a35 -PODFILE CHECKSUM: e8eb304aaab35b0a88a5061076e0ce130e155579 +PODFILE CHECKSUM: 982f53dbc54c6aa3ca1ce0750bf6f1448a4f1684 COCOAPODS: 1.5.3 From d6b63f27bc166aef137a25ae1e2c4b63464be06f Mon Sep 17 00:00:00 2001 From: Anton B Date: Sat, 30 Jun 2018 10:17:04 +0300 Subject: [PATCH 032/175] Show ADM Transactions screen from chat --- .../ApiService/AdamantApi+Transfers.swift | 33 +++++++---- .../Stories/Chats/ChatViewController.swift | 56 +++++++++---------- 2 files changed, 50 insertions(+), 39 deletions(-) diff --git a/Adamant/Services/ApiService/AdamantApi+Transfers.swift b/Adamant/Services/ApiService/AdamantApi+Transfers.swift index b43f7722e..3e38483f2 100644 --- a/Adamant/Services/ApiService/AdamantApi+Transfers.swift +++ b/Adamant/Services/ApiService/AdamantApi+Transfers.swift @@ -10,10 +10,13 @@ import Foundation extension AdamantApiService { func transferFunds(sender: String, recipient: String, amount: Decimal, keypair: Keypair, completion: @escaping (ApiServiceResult) -> Void) { + + let amount = amount.shiftedToAdamant() * 100000000 + // MARK: 1. Prepare params let params: [String : Any] = [ "type": TransactionType.send.rawValue, - "amount": amount.shiftedToAdamant(), + "amount": amount,//amount.shiftedToAdamant(), "recipientId": recipient, "senderId": sender, "publicKey": keypair.publicKey @@ -53,14 +56,14 @@ extension AdamantApiService { // MARK: 4.2. Create transaction let transaction: [String: Any] = [ - "type": TransactionType.send.rawValue, - "amount": amount.shiftedToAdamant(), - "senderPublicKey": keypair.publicKey, - "requesterPublicKey": normalizedTransaction.requesterPublicKey ?? NSNull(), - "timestamp": normalizedTransaction.timestamp, - "recipientId": recipient, - "senderId": sender, - "signature": signature + "type": normalizedTransaction.type.rawValue, + "amount": amount, + "senderPublicKey": normalizedTransaction.senderPublicKey, + "requesterPublicKey": normalizedTransaction.requesterPublicKey ?? NSNull(), + "timestamp": normalizedTransaction.timestamp, + "recipientId": normalizedTransaction.recipientId ?? NSNull(), + "senderId": sender, + "signature": signature ] let params: [String: Any] = [ @@ -70,8 +73,16 @@ extension AdamantApiService { // MARK: 5. Send self.sendRequest(url: processEndpoin, method: .post, parameters: params, encoding: .json, headers: headers) { (response: ApiServiceResult) in switch response { - case .success(_): - completion(.success(true)) + case .success(let result): + if result.success { + completion(.success(true)) + } else { + if let error = result.error { + completion(.failure(.internalError(message: error, error: nil))) + } else { + completion(.failure(.internalError(message: "Unknown Error", error: nil))) + } + } case .failure(let error): completion(.failure(error)) diff --git a/Adamant/Stories/Chats/ChatViewController.swift b/Adamant/Stories/Chats/ChatViewController.swift index aa1f4dd36..56ad04925 100644 --- a/Adamant/Stories/Chats/ChatViewController.swift +++ b/Adamant/Stories/Chats/ChatViewController.swift @@ -99,36 +99,36 @@ class ChatViewController: MessagesViewController { }), UIAlertAction(title: "ADM", style: .default, handler: { [weak self] (_) in // MARK: Show ADM transfer details - DISABLED until end of ICO -// if let address = self?.chatroom?.partner?.address { -// guard let vc = self?.router.get(scene: AdamantScene.Account.transfer) as? TransferViewController else { -// fatalError("Can't get TransferViewController scene") -// } -// -// vc.token = .ADM -// vc.toAddress = address -// -// if let nav = self?.navigationController { -// nav.pushViewController(vc, animated: true) -// } else { -// self?.present(vc, animated: true, completion: nil) -// } -// } - let alert = UIAlertController(title: String.adamantLocalized.account.sorryAlert, message: String.adamantLocalized.account.transferNotAllowed, preferredStyle: .alert) - - let cancel = UIAlertAction(title: String.adamantLocalized.alert.cancel, style: .cancel) { (_) in } - - alert.addAction(cancel) - - if let url = AdamantResources.webAppUrl { - let webApp = UIAlertAction(title: String.adamantLocalized.account.webApp, style: .default) { [weak self] _ in - let safari = SFSafariViewController(url: url) - safari.preferredControlTintColor = UIColor.adamantPrimary - self?.present(safari, animated: true, completion: nil) + if let address = self?.chatroom?.partner?.address { + guard let vc = self?.router.get(scene: AdamantScene.Account.transfer) as? TransferViewController else { + fatalError("Can't get TransferViewController scene") + } + + vc.token = .ADM + vc.toAddress = address + + if let nav = self?.navigationController { + nav.pushViewController(vc, animated: true) + } else { + self?.present(vc, animated: true, completion: nil) } - alert.addAction(webApp) } - - self?.present(alert, animated: true, completion: nil) +// let alert = UIAlertController(title: String.adamantLocalized.account.sorryAlert, message: String.adamantLocalized.account.transferNotAllowed, preferredStyle: .alert) +// +// let cancel = UIAlertAction(title: String.adamantLocalized.alert.cancel, style: .cancel) { (_) in } +// +// alert.addAction(cancel) +// +// if let url = AdamantResources.webAppUrl { +// let webApp = UIAlertAction(title: String.adamantLocalized.account.webApp, style: .default) { [weak self] _ in +// let safari = SFSafariViewController(url: url) +// safari.preferredControlTintColor = UIColor.adamantPrimary +// self?.present(safari, animated: true, completion: nil) +// } +// alert.addAction(webApp) +// } +// +// self?.present(alert, animated: true, completion: nil) }) ]) } From e17380d1bda44a36b0a2e824e49c987bbb8f3776 Mon Sep 17 00:00:00 2001 From: Anton B Date: Mon, 2 Jul 2018 14:43:12 +0300 Subject: [PATCH 033/175] Fix ADM transaction signing issue --- Adamant/Helpers/Decimal+adamant.swift | 2 +- .../ApiService/AdamantApi+Transfers.swift | 8 +-- Adamant/Services/JSAdamantCore.swift | 3 +- .../Stories/Chats/ChatViewController.swift | 60 +++++++------------ 4 files changed, 28 insertions(+), 45 deletions(-) diff --git a/Adamant/Helpers/Decimal+adamant.swift b/Adamant/Helpers/Decimal+adamant.swift index 851122731..851f15cfc 100644 --- a/Adamant/Helpers/Decimal+adamant.swift +++ b/Adamant/Helpers/Decimal+adamant.swift @@ -14,6 +14,6 @@ extension Decimal { } func shiftedToAdamant() -> Decimal { - return Decimal(sign: self.isSignMinus ? .minus : .plus, exponent: 0, significand: self) + return self * 100000000 } } diff --git a/Adamant/Services/ApiService/AdamantApi+Transfers.swift b/Adamant/Services/ApiService/AdamantApi+Transfers.swift index 3e38483f2..212e0b9d2 100644 --- a/Adamant/Services/ApiService/AdamantApi+Transfers.swift +++ b/Adamant/Services/ApiService/AdamantApi+Transfers.swift @@ -11,12 +11,10 @@ import Foundation extension AdamantApiService { func transferFunds(sender: String, recipient: String, amount: Decimal, keypair: Keypair, completion: @escaping (ApiServiceResult) -> Void) { - let amount = amount.shiftedToAdamant() * 100000000 - - // MARK: 1. Prepare params + // MARK: 1. Prepare params let params: [String : Any] = [ "type": TransactionType.send.rawValue, - "amount": amount,//amount.shiftedToAdamant(), + "amount": amount.shiftedToAdamant(), "recipientId": recipient, "senderId": sender, "publicKey": keypair.publicKey @@ -57,7 +55,7 @@ extension AdamantApiService { // MARK: 4.2. Create transaction let transaction: [String: Any] = [ "type": normalizedTransaction.type.rawValue, - "amount": amount, + "amount": normalizedTransaction.amount.shiftedToAdamant(), "senderPublicKey": normalizedTransaction.senderPublicKey, "requesterPublicKey": normalizedTransaction.requesterPublicKey ?? NSNull(), "timestamp": normalizedTransaction.timestamp, diff --git a/Adamant/Services/JSAdamantCore.swift b/Adamant/Services/JSAdamantCore.swift index 12d17bb9d..127bebb3d 100644 --- a/Adamant/Services/JSAdamantCore.swift +++ b/Adamant/Services/JSAdamantCore.swift @@ -268,6 +268,7 @@ extension JSAdamantCore { asset.votes = vote.votes } + let amount = NSDecimalNumber(decimal:t.amount.shiftedToAdamant()).uint64Value let jsTransaction = JSTransaction(id: 0, height: 0, blockId: 0, @@ -277,7 +278,7 @@ extension JSAdamantCore { senderId: senderId, recipientId: t.recipientId, recipientPublicKey: t.requesterPublicKey, - amount: (t.amount.shiftedToAdamant() as NSDecimalNumber).uint64Value, + amount: amount, fee: 0, signature: "", confirmations: 0, diff --git a/Adamant/Stories/Chats/ChatViewController.swift b/Adamant/Stories/Chats/ChatViewController.swift index 56ad04925..9f1d3941f 100644 --- a/Adamant/Stories/Chats/ChatViewController.swift +++ b/Adamant/Stories/Chats/ChatViewController.swift @@ -77,60 +77,44 @@ class ChatViewController: MessagesViewController { $0.image = #imageLiteral(resourceName: "attachment") }.onTouchUpInside { _ in self.dialogService.showSystemActionSheet(title: String.adamantLocalized.transfer.send, message: "", actions: [ - UIAlertAction(title: "Ethereum", style: .default, handler: { (action) in - if let ethAddress = self.ethAddress { - // MARK: Show ETH transfer details - guard let vc = self.router.get(scene: AdamantScene.Account.transfer) as? TransferViewController else { - fatalError("Can't get TransferViewController scene") - } - - vc.token = .ETH - vc.toAddress = ethAddress - vc.delegate = self - - if let nav = self.navigationController { - nav.pushViewController(vc, animated: true) + UIAlertAction(title: "Ethereum", style: .default, handler: { [weak self] (_) in + if let ethAddress = self?.ethAddress { + // MARK: Show ETH transfer + guard let vc = self?.router.get(scene: AdamantScene.Account.transfer) as? TransferViewController else { + fatalError("Can't get TransferViewController scene") + } + + vc.token = .ETH + vc.toAddress = ethAddress + vc.delegate = self + + if let nav = self?.navigationController { + nav.pushViewController(vc, animated: true) + } else { + self?.present(vc, animated: true, completion: nil) + } } else { - self.present(vc, animated: true, completion: nil) + self?.dialogService.showWarning(withMessage: "User don't have public Eth wallet yet.") } - } else { - self.dialogService.showWarning(withMessage: "User don't have public Eth wallet yet.") - } - }), + }), UIAlertAction(title: "ADM", style: .default, handler: { [weak self] (_) in - // MARK: Show ADM transfer details - DISABLED until end of ICO + // MARK: Show ADM transfer details if let address = self?.chatroom?.partner?.address { guard let vc = self?.router.get(scene: AdamantScene.Account.transfer) as? TransferViewController else { fatalError("Can't get TransferViewController scene") } - + vc.token = .ADM vc.toAddress = address - + if let nav = self?.navigationController { nav.pushViewController(vc, animated: true) } else { self?.present(vc, animated: true, completion: nil) } } -// let alert = UIAlertController(title: String.adamantLocalized.account.sorryAlert, message: String.adamantLocalized.account.transferNotAllowed, preferredStyle: .alert) -// -// let cancel = UIAlertAction(title: String.adamantLocalized.alert.cancel, style: .cancel) { (_) in } -// -// alert.addAction(cancel) -// -// if let url = AdamantResources.webAppUrl { -// let webApp = UIAlertAction(title: String.adamantLocalized.account.webApp, style: .default) { [weak self] _ in -// let safari = SFSafariViewController(url: url) -// safari.preferredControlTintColor = UIColor.adamantPrimary -// self?.present(safari, animated: true, completion: nil) -// } -// alert.addAction(webApp) -// } -// -// self?.present(alert, animated: true, completion: nil) }) - ]) + ]) } }() From 91cb35c0350fc13dab4d4c40f3e9e7815349c856 Mon Sep 17 00:00:00 2001 From: Anton B Date: Tue, 17 Jul 2018 12:50:58 +0300 Subject: [PATCH 034/175] Base Lisk integration Create Lisk account on start Save Lisk addres to KVS Add Lisk account balance method Add Lisk account details on account screen: Address, balance Add Transfer lisk funds method Add Transfer Lisk tokens option from acount screen --- Adamant.xcodeproj/project.pbxproj | 20 +- Adamant/Info.plist | 5 + .../LskApiServiceProtocol.swift | 24 ++ .../EthApiService.swift | 0 .../TokensApiService/LskApiService.swift | 209 ++++++++++++++++++ Adamant/Stories/Account/AccountRoutes.swift | 2 + .../Account/AccountViewController.swift | 5 + .../Account/TransferViewController.swift | 125 ++++++++++- Adamant/Stories/Login/LoginRoutes.swift | 1 + .../Stories/Login/LoginViewController.swift | 12 +- Adamant/SwinjectDependencies.swift | 10 +- Podfile | 3 + Podfile.lock | 19 +- 13 files changed, 428 insertions(+), 7 deletions(-) create mode 100644 Adamant/ServiceProtocols/LskApiServiceProtocol.swift rename Adamant/Services/{EthApiService => TokensApiService}/EthApiService.swift (100%) create mode 100644 Adamant/Services/TokensApiService/LskApiService.swift diff --git a/Adamant.xcodeproj/project.pbxproj b/Adamant.xcodeproj/project.pbxproj index a098ce4d7..5c8abc181 100644 --- a/Adamant.xcodeproj/project.pbxproj +++ b/Adamant.xcodeproj/project.pbxproj @@ -26,6 +26,8 @@ 644D3DBB20BD5F22002EB002 /* Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = 644D3DBA20BD5F22002EB002 /* Status.swift */; }; 642361BE20E3869D0061559E /* eth_l18n.strings in Resources */ = {isa = PBXBuildFile; fileRef = 642361BD20E3869D0061559E /* eth_l18n.strings */; }; 649A694720DD427200D27E91 /* TransferType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649A694620DD427200D27E91 /* TransferType.swift */; }; + 64A223D820F7A08E005157CB /* LskApiServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64A223D720F7A08E005157CB /* LskApiServiceProtocol.swift */; }; + 64A223DA20F7A14B005157CB /* LskApiService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64A223D920F7A14B005157CB /* LskApiService.swift */; }; 64BD2B7520E2814B00E2CD36 /* EthTransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64BD2B7420E2814B00E2CD36 /* EthTransaction.swift */; }; 64BD2B7720E2820300E2CD36 /* TransactionDetailsProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64BD2B7620E2820300E2CD36 /* TransactionDetailsProtocol.swift */; }; 64D059FF20D3116B003AD655 /* NodesListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D059FE20D3116A003AD655 /* NodesListViewController.swift */; }; @@ -231,6 +233,8 @@ 644D3DBA20BD5F22002EB002 /* Status.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Status.swift; sourceTree = ""; }; 642361BD20E3869D0061559E /* eth_l18n.strings */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; path = eth_l18n.strings; sourceTree = ""; }; 649A694620DD427200D27E91 /* TransferType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransferType.swift; sourceTree = ""; }; + 64A223D720F7A08E005157CB /* LskApiServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LskApiServiceProtocol.swift; sourceTree = ""; }; + 64A223D920F7A14B005157CB /* LskApiService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LskApiService.swift; sourceTree = ""; }; 64BD2B7420E2814B00E2CD36 /* EthTransaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EthTransaction.swift; sourceTree = ""; }; 64BD2B7620E2820300E2CD36 /* TransactionDetailsProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionDetailsProtocol.swift; sourceTree = ""; }; 64D059FE20D3116A003AD655 /* NodesListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NodesListViewController.swift; sourceTree = ""; }; @@ -472,12 +476,13 @@ path = Delegates; sourceTree = ""; }; - 64D05A0020D515CA003AD655 /* EthApiService */ = { + 64D05A0020D515CA003AD655 /* TokensApiService */ = { isa = PBXGroup; children = ( 64D05A0320D516B4003AD655 /* EthApiService.swift */, + 64A223D920F7A14B005157CB /* LskApiService.swift */, ); - path = EthApiService; + path = TokensApiService; sourceTree = ""; }; E59396A8E0053F21F768E69B /* Pods */ = { @@ -548,6 +553,7 @@ E9A174B22057EC47003667CD /* BackgroundFetchService.swift */, E9215972206119FB0000CA5C /* ReachabilityMonitor.swift */, 64D05A0120D51632003AD655 /* EthApiServiceProtocol.swift */, + 64A223D720F7A08E005157CB /* LskApiServiceProtocol.swift */, 64BD2B7620E2820300E2CD36 /* TransactionDetailsProtocol.swift */, ); path = ServiceProtocols; @@ -556,7 +562,7 @@ E913C9061FFFA92E001A83F7 /* Services */ = { isa = PBXGroup; children = ( - 64D05A0020D515CA003AD655 /* EthApiService */, + 64D05A0020D515CA003AD655 /* TokensApiService */, E9CAE8D02018AA5000345E76 /* ApiService */, E9B3D39F201FA2090019EB36 /* DataProviders */, E9E7CD922002740500DFC4DB /* AdamantAccountService.swift */, @@ -1065,12 +1071,14 @@ "${BUILT_PRODUCTS_DIR}/CryptoSwift/CryptoSwift.framework", "${BUILT_PRODUCTS_DIR}/DateToolsSwift/DateToolsSwift.framework", "${BUILT_PRODUCTS_DIR}/EFQRCode/EFQRCode.framework", + "${BUILT_PRODUCTS_DIR}/Ed25519/Ed25519.framework", "${BUILT_PRODUCTS_DIR}/Eureka/Eureka.framework", "${BUILT_PRODUCTS_DIR}/FTIndicator/FTIndicator.framework", "${BUILT_PRODUCTS_DIR}/FreakingSimpleRoundImageView/FreakingSimpleRoundImageView.framework", "${BUILT_PRODUCTS_DIR}/Haring/Haring.framework", "${BUILT_PRODUCTS_DIR}/KeychainAccess/KeychainAccess.framework", "${BUILT_PRODUCTS_DIR}/M13Checkbox/M13Checkbox.framework", + "${BUILT_PRODUCTS_DIR}/Lisk/Lisk.framework", "${BUILT_PRODUCTS_DIR}/MessageKit/MessageKit.framework", "${BUILT_PRODUCTS_DIR}/MyLittlePinpad/MyLittlePinpad.framework", "${BUILT_PRODUCTS_DIR}/PMAlertController/PMAlertController.framework", @@ -1080,6 +1088,7 @@ "${BUILT_PRODUCTS_DIR}/Result/Result.framework", "${BUILT_PRODUCTS_DIR}/SipHash/SipHash.framework", "${BUILT_PRODUCTS_DIR}/Swinject/Swinject.framework", + "${BUILT_PRODUCTS_DIR}/libCEd25519/CEd25519.framework", "${BUILT_PRODUCTS_DIR}/libsodium/libsodium.framework", "${BUILT_PRODUCTS_DIR}/secp256k1_ios/secp256k1_ios.framework", "${BUILT_PRODUCTS_DIR}/web3swift/web3swift.framework", @@ -1092,12 +1101,14 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/CryptoSwift.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DateToolsSwift.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/EFQRCode.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Ed25519.framework", "${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}/Haring.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/KeychainAccess.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/M13Checkbox.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Lisk.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MessageKit.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MyLittlePinpad.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/PMAlertController.framework", @@ -1107,6 +1118,7 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Result.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SipHash.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Swinject.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/CEd25519.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/libsodium.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/secp256k1_ios.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/web3swift.framework", @@ -1176,6 +1188,7 @@ E987024920C2B1F700E393F4 /* AdamantChatsProvider+fakeMessages.swift in Sources */, E9942B84203CBFCE00C163AF /* AdamantQRTools.swift in Sources */, 649A694720DD427200D27E91 /* TransferType.swift in Sources */, + 64A223D820F7A08E005157CB /* LskApiServiceProtocol.swift in Sources */, E9E7CD8F20026CD300DFC4DB /* AdamantDialogService.swift in Sources */, E9CAE8DA2018ACD300345E76 /* AdamantApi+Chats.swift in Sources */, E91947B020002393001362F8 /* AdamantApiService.swift in Sources */, @@ -1190,6 +1203,7 @@ E9150B9D2066DA210065A985 /* Chatroom+CoreDataClass.swift in Sources */, E9942B80203C058C00C163AF /* QRGeneratorViewController.swift in Sources */, E921597520611A6A0000CA5C /* AdamantReachability.swift in Sources */, + 64A223DA20F7A14B005157CB /* LskApiService.swift in Sources */, E9150BA22066DA210065A985 /* ChatTransaction+CoreDataProperties.swift in Sources */, E9204B5020C94C4A00F3B9AB /* Date+humanizedString.swift in Sources */, E9E7CD9120026FA100DFC4DB /* SwinjectDependencies.swift in Sources */, diff --git a/Adamant/Info.plist b/Adamant/Info.plist index 38665f36f..d840dc19d 100644 --- a/Adamant/Info.plist +++ b/Adamant/Info.plist @@ -22,6 +22,11 @@ 40 LSRequiresIPhoneOS + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + NSCameraUsageDescription The camera needed to scan QR codes for addresses and passphrases NSFaceIDUsageDescription diff --git a/Adamant/ServiceProtocols/LskApiServiceProtocol.swift b/Adamant/ServiceProtocols/LskApiServiceProtocol.swift new file mode 100644 index 000000000..29dcd8e99 --- /dev/null +++ b/Adamant/ServiceProtocols/LskApiServiceProtocol.swift @@ -0,0 +1,24 @@ +// +// LskApiServerProtocol.swift +// Adamant +// +// Created by Anton Boyarkin on 12/07/2018. +// Copyright © 2018 Adamant. All rights reserved. +// + +import Foundation + +protocol LskApiServiceProtocol: class { + + var account: LskAccount? { get } + + // MARK: - Accounts + func newAccount(byPassphrase passphrase: String, completion: @escaping (ApiServiceResult) -> Void) + + // MARK: - Transactions + func sendFunds(toAddress address: String, amount: Double, completion: @escaping (ApiServiceResult) -> Void) + + // MARK: - Tools + func getBalance(_ completion: @escaping (ApiServiceResult) -> Void) + func getLskAddress(byAdamandAddress address: String, completion: @escaping (ApiServiceResult) -> Void) +} diff --git a/Adamant/Services/EthApiService/EthApiService.swift b/Adamant/Services/TokensApiService/EthApiService.swift similarity index 100% rename from Adamant/Services/EthApiService/EthApiService.swift rename to Adamant/Services/TokensApiService/EthApiService.swift diff --git a/Adamant/Services/TokensApiService/LskApiService.swift b/Adamant/Services/TokensApiService/LskApiService.swift new file mode 100644 index 000000000..b2cd03373 --- /dev/null +++ b/Adamant/Services/TokensApiService/LskApiService.swift @@ -0,0 +1,209 @@ +// +// LskApiService.swift +// Adamant +// +// Created by Anton Boyarkin on 12/07/2018. +// Copyright © 2018 Adamant. All rights reserved. +// + +import Foundation +import BigInt +import Lisk +import Ed25519 +import web3swift + +// MARK: - Notifications +extension Notification.Name { + struct LskApiService { + /// Raised when user has logged out. + static let userLoggedOut = Notification.Name("adamant.lskApiService.userHasLoggedOut") + + /// Raised when user has successfully logged in. + static let userLoggedIn = Notification.Name("adamant.lskApiService.userHasLoggedIn") + + private init() {} + } +} + +struct LskAccount { + let keys: KeyPair + let address: String + var balance: BigUInt? + var balanceString: String? +} + +class LskApiService: LskApiServiceProtocol { + + // MARK: - Constans + static let kvsAddress = "lsk:address" + static let defaultFee = 0.1 + + // MARK: - Dependencies + var apiService: ApiService! + var accountService: AccountService! + + // MARK: - Properties + private(set) var account: LskAccount? + + private var accountApi: Accounts! + private var transactionApi: Transactions! + + init() { + accountApi = Accounts(client: .testnet) + transactionApi = Transactions(client: .testnet) + + NotificationCenter.default.addObserver(forName: Notification.Name.AdamantAccountService.userLoggedOut, object: nil, queue: OperationQueue.main) { [weak self] _ in + self?.account = nil + } + } + + func newAccount(byPassphrase passphrase: String, completion: @escaping (ApiServiceResult) -> Void) { + do { + let keys = try Crypto.keyPair(fromPassphrase: passphrase) + let address = Crypto.address(fromPublicKey: keys.publicKeyString) + let account = LskAccount(keys: keys, address: address, balance: BigUInt(0), balanceString: "0 LSK") + 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) + + 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: LskApiService.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 sendFunds(toAddress address: String, amount: Double, completion: @escaping (ApiServiceResult) -> Void) { + if let keys = self.account?.keys { + Transactions(client: .testnet).transfer(lsk: amount, to: address, keyPair: keys) { response in + switch response { + case .success(let result): + print(result.data.hashValue) + print(result.data.message) + completion(.success(result.data.message)) + case .error(let error): + print("ERROR: " + error.message) + completion(.failure(.internalError(message: error.message, error: nil))) + } + } + } + } + + // 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)" + } + } + + completion(.success("\(self.account?.balanceString ?? "--") LSK")) + + break + case .error(response: let error): + print(error) + completion(.failure(.serverError(error: error.message))) + break + } + } + } else { + completion(.failure(.internalError(message: "LSK Wallet: not found", error: nil))) + } + } + + func getLskAddress(byAdamandAddress address: String, completion: @escaping (ApiServiceResult) -> Void) { + apiService.get(key: LskApiService.kvsAddress, sender: address, completion: completion) + } + + func fromRawLsk(value: BigUInt) -> String { + if let formattedAmount = Web3.Utils.formatToPrecision(value, numberDecimals: 8, formattingDecimals: 8, decimalSeparator: ".", fallbackToScientific: false) { + return formattedAmount + } else { + return "--" + } + } + + func toRawLsk(value: Double) -> BigInt { + return BigInt(value) * BigInt(10).power(8) + } + + private static let addressRegexString = "^([0-9]{2,22})L$" + private static let addressRegex = try! NSRegularExpression(pattern: addressRegexString, options: []) + private static let maxAddressNumber = BigUInt("18446744073709551615")! + + /// Rules are simple: + /// + /// - Tailing uppercase L + /// - From 2 to 22 numbers + /// - Address number lower 18446744073709551615 + /// - No leading or trailing whitespaces + static func validateAddress(address: String) -> AdamantUtilities.AddressValidationResult { + let value = address.replacingOccurrences(of: "L", with: "") + + if validate(string: address, with: addressRegex), let number = BigUInt(value), number < maxAddressNumber { + return .valid + } else { + return .invalid + } + } + + private static func validate(string: String, with regex: NSRegularExpression) -> Bool { + let matches = regex.matches(in: string, options: [], range: NSRange(location: 0, length: string.count)) + + return matches.count == 1 + } +} diff --git a/Adamant/Stories/Account/AccountRoutes.swift b/Adamant/Stories/Account/AccountRoutes.swift index 40aaca760..5945451c6 100644 --- a/Adamant/Stories/Account/AccountRoutes.swift +++ b/Adamant/Stories/Account/AccountRoutes.swift @@ -18,6 +18,7 @@ extension AdamantScene { c.notificationsService = r.resolve(NotificationsService.self) c.transfersProvider = r.resolve(TransfersProvider.self) c.ethApiService = r.resolve(EthApiServiceProtocol.self) + c.lskApiService = r.resolve(LskApiServiceProtocol.self) return c } @@ -27,6 +28,7 @@ extension AdamantScene { c.accountService = r.resolve(AccountService.self) c.dialogService = r.resolve(DialogService.self) c.ethApiService = r.resolve(EthApiServiceProtocol.self) + c.lskApiService = r.resolve(LskApiServiceProtocol.self) return c } diff --git a/Adamant/Stories/Account/AccountViewController.swift b/Adamant/Stories/Account/AccountViewController.swift index 05bb9b62b..c2b311b37 100644 --- a/Adamant/Stories/Account/AccountViewController.swift +++ b/Adamant/Stories/Account/AccountViewController.swift @@ -116,6 +116,7 @@ class AccountViewController: FormViewController { var dialogService: DialogService! var router: Router! var ethApiService: EthApiServiceProtocol! + var lskApiService: LskApiServiceProtocol! var notificationsService: NotificationsService! var transfersProvider: TransfersProvider! @@ -334,6 +335,10 @@ class AccountViewController: FormViewController { NotificationCenter.default.addObserver(forName: Notification.Name.EthApiService.userLoggedIn, object: nil, queue: OperationQueue.main) { [weak self] _ in self?.refreshEthCells() } + + NotificationCenter.default.addObserver(forName: Notification.Name.LskApiService.userLoggedIn, object: nil, queue: OperationQueue.main) { [weak self] _ in + self?.refreshLskCells() + } } override func viewWillAppear(_ animated: Bool) { diff --git a/Adamant/Stories/Account/TransferViewController.swift b/Adamant/Stories/Account/TransferViewController.swift index 0e11b9ed0..e83713249 100644 --- a/Adamant/Stories/Account/TransferViewController.swift +++ b/Adamant/Stories/Account/TransferViewController.swift @@ -98,6 +98,7 @@ class TransferViewController: FormViewController { enum Token { case ADM case ETH + case LSK } @@ -107,6 +108,7 @@ class TransferViewController: FormViewController { var accountService: AccountService! var dialogService: DialogService! var ethApiService: EthApiServiceProtocol! + var lskApiService: LskApiServiceProtocol! private(set) var maxToTransfer: Double = 0.0 @@ -134,6 +136,8 @@ class TransferViewController: FormViewController { createADMForm() case .ETH: createETHForm() + case .LSK: + createLSKForm() } // MARK: - Transfer section @@ -239,7 +243,6 @@ class TransferViewController: FormViewController { } private func createETHForm() { - // MARK: - Wallet section if let ethAccount = ethApiService.account, let ethBalanceBigInt = ethAccount.balance, let ethBalanceString = Web3.Utils.formatToEthereumUnits(ethBalanceBigInt), let ethBalance = Double(ethBalanceString) { maxToTransfer = ethBalance @@ -313,6 +316,75 @@ class TransferViewController: FormViewController { } } } + + private func createLSKForm() { + if let account = lskApiService.account, let balanceString = account.balanceString, let balance = Double(balanceString) { + + maxToTransfer = balance + defaultFee = LskApiService.defaultFee + + let currencyFormatter = NumberFormatter() + currencyFormatter.numberStyle = .decimal + currencyFormatter.roundingMode = .floor + currencyFormatter.positiveFormat = "#.######## LSK" + + form +++ Section(Sections.wallet.localized) + <<< DecimalRow() { + $0.title = Row.balance.localized + $0.value = balance + $0.tag = Row.balance.tag + $0.disabled = true + $0.formatter = currencyFormatter + } + + // MARK: - Transfer section + form +++ Section(Sections.transferInfo.localized) + + <<< TextRow() { + $0.title = Row.address.localized + $0.placeholder = String.adamantLocalized.transfer.addressPlaceholder + $0.tag = Row.address.tag + $0.value = toAddress + $0.add(rule: RuleClosure(closure: { value -> ValidationError? in + guard let value = value?.uppercased() else { + return ValidationError(msg: String.adamantLocalized.transfer.addressValidationError) + } + switch LskApiService.validateAddress(address: value) { + case .valid: + return nil + + case .system, .invalid: + return ValidationError(msg: String.adamantLocalized.transfer.addressValidationError) + } + })) + $0.validationOptions = .validatesOnBlur + }.cellUpdate({ (cell, row) in + cell.titleLabel?.textColor = row.isValid ? .black : .red + }) + <<< DecimalRow() { + $0.title = Row.amount.localized + $0.placeholder = String.adamantLocalized.transfer.amountPlaceholder + $0.tag = Row.amount.tag + $0.formatter = currencyFormatter + $0.add(rule: RuleSmallerOrEqualThan(max: maxToTransfer)) + $0.validationOptions = .validatesOnChange + }.onChange(ethAmountChanged) + <<< DecimalRow() { + $0.title = Row.fee.localized + $0.value = defaultFee + $0.tag = Row.fee.tag + $0.disabled = true + $0.formatter = currencyFormatter + } + <<< DecimalRow() { + $0.title = Row.total.localized + $0.value = nil + $0.tag = Row.total.tag + $0.disabled = true + $0.formatter = currencyFormatter + } + } + } // MARK: - Form Events @@ -394,6 +466,8 @@ class TransferViewController: FormViewController { sendADMFunds() case .ETH: sendETHFunds() + case .LSK: + sendLSKFunds() } } @@ -503,6 +577,37 @@ class TransferViewController: FormViewController { present(alert, animated: true, completion: nil) } + func sendLSKFunds() { + guard let recipientRow = form.rowBy(tag: Row.address.tag) as? TextRow, + let recipient = recipientRow.value, + let amountRow = form.rowBy(tag: Row.amount.tag) as? DecimalRow, + let amount = amountRow.value else { + return + } + + guard recipientRow.isValid else { + dialogService.showWarning(withMessage: (recipientRow.validationErrors.first?.msg) ?? "Invalid Address") + return + } + + guard let totalAmount = totalAmount, totalAmount <= maxToTransfer else { + dialogService.showWarning(withMessage: String.adamantLocalized.transfer.amountTooHigh) + return + } + + let alert = UIAlertController(title: String.localizedStringWithFormat(String.adamantLocalized.alert.confirmSendMessageFormat, "\(amount) LSK", recipient), message: String.adamantLocalized.transfer.cantUndo, preferredStyle: .alert) + let cancelAction = UIAlertAction(title: String.adamantLocalized.alert.cancel , style: .cancel, handler: nil) + let sendAction = UIAlertAction(title: String.adamantLocalized.alert.send, style: .default, handler: { _ in + self.sendLsk(to: recipient, amount: amount) + }) + + alert.addAction(cancelAction) + alert.addAction(sendAction) + + present(alert, animated: true, completion: nil) + } + + // MARK: - Private private func sendEth(to recipient: String, amount: Double) { self.dialogService.showProgress(withMessage: String.adamantLocalized.transfer.transferProcessingMessage, userInteractionEnable: false) @@ -521,6 +626,24 @@ class TransferViewController: FormViewController { } } + private func sendLsk(to recipient: String, amount: Double) { + self.dialogService.showProgress(withMessage: String.adamantLocalized.transfer.transferProcessingMessage, userInteractionEnable: false) + + self.lskApiService.sendFunds(toAddress: recipient, amount: amount) { (result) in + switch result { + case .success(let value): + print("Payload: \(value)") + + self.delegate?.transferFinished(with: value) + self.dialogService.showSuccess(withMessage: String.adamantLocalized.transfer.transferSuccess) + self.close() + + case .failure(let error): + self.dialogService.showError(withMessage: "Transrer issue", error: error) + } + } + } + private func close() { if let nav = self.navigationController { nav.popViewController(animated: true) diff --git a/Adamant/Stories/Login/LoginRoutes.swift b/Adamant/Stories/Login/LoginRoutes.swift index f7030799d..f09eaf55e 100644 --- a/Adamant/Stories/Login/LoginRoutes.swift +++ b/Adamant/Stories/Login/LoginRoutes.swift @@ -18,6 +18,7 @@ extension AdamantScene { c.localAuth = r.resolve(LocalAuthentication.self) c.router = r.resolve(Router.self) c.ethAPiService = r.resolve(EthApiServiceProtocol.self) + c.lskAPiService = r.resolve(LskApiServiceProtocol.self) return c }) diff --git a/Adamant/Stories/Login/LoginViewController.swift b/Adamant/Stories/Login/LoginViewController.swift index b4920d8b1..fbb8441bf 100644 --- a/Adamant/Stories/Login/LoginViewController.swift +++ b/Adamant/Stories/Login/LoginViewController.swift @@ -126,7 +126,7 @@ class LoginViewController: FormViewController { var localAuth: LocalAuthentication! var router: Router! var ethAPiService: EthApiServiceProtocol! - + var lskAPiService: LskApiServiceProtocol! // MARK: Properties private var hideNewPassphrase: Bool = true @@ -403,6 +403,16 @@ extension LoginViewController { } self?.dialogService.dismissProgress() }) + + self?.lskAPiService.newAccount(byPassphrase: passphrase, completion: { (result) in + switch result { + case .success(_): + break + case .failure(let error): + print(error) + break + } + }) case .failure(let error): self?.dialogService.showRichError(error: error) diff --git a/Adamant/SwinjectDependencies.swift b/Adamant/SwinjectDependencies.swift index 280aa3dd1..bb35aa1e5 100644 --- a/Adamant/SwinjectDependencies.swift +++ b/Adamant/SwinjectDependencies.swift @@ -75,13 +75,21 @@ extension Container { return service }.inObjectScope(.container) - // MARK: EthApiService + // MARK: Ethereum ApiService self.register(EthApiServiceProtocol.self) { r in let service = EthApiService(apiUrl: AdamantResources.ethServers.first!) service.apiService = r.resolve(ApiService.self)! service.accountService = r.resolve(AccountService.self) return service }.inObjectScope(.container) + + // MARK: Lisk ApiService + self.register(LskApiServiceProtocol.self) { r in + let service = LskApiService() + service.apiService = r.resolve(ApiService.self)! + service.accountService = r.resolve(AccountService.self) + return service + }.inObjectScope(.container) // MARK: AccountService self.register(AccountService.self) { r in diff --git a/Podfile b/Podfile index d7aed17ed..2920d12f2 100644 --- a/Podfile +++ b/Podfile @@ -28,5 +28,8 @@ target 'Adamant' do # ETH pod 'web3swift', '~> 0.8.0' # Web3 Swift Port + + # LSK + pod 'Lisk', :git => 'https://github.com/boyarkin-anton/lisk-swift.git', :branch => 'dev' end diff --git a/Podfile.lock b/Podfile.lock index 60bf5abb9..57a688678 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -6,6 +6,8 @@ PODS: - SipHash (~> 1.2) - CryptoSwift (0.10.0) - DateToolsSwift (4.0.0) + - Ed25519 (1.1.0): + - libCEd25519 (~> 1.1.0) - EFQRCode (4.2.2) - Eureka (4.2.0) - FreakingSimpleRoundImageView (1.2.3) @@ -18,7 +20,10 @@ PODS: - FTIndicator/FTToastIndicator (1.2.9) - Haring (2.0.8) - KeychainAccess (3.1.1) + - libCEd25519 (1.1.0) - libsodium (1.0.12) + - Lisk (1.0.0): + - Ed25519 (~> 1.1.0) - M13Checkbox (3.2.2) - MessageKit (1.0.0) - MyLittlePinpad (0.2.5) @@ -49,6 +54,7 @@ DEPENDENCIES: - FTIndicator - Haring - KeychainAccess + - Lisk (from `https://github.com/boyarkin-anton/lisk-swift.git`, branch `dev`) - M13Checkbox - MessageKit - MyLittlePinpad @@ -66,12 +72,14 @@ SPEC REPOS: - BigInt - CryptoSwift - DateToolsSwift + - Ed25519 - EFQRCode - Eureka - FreakingSimpleRoundImageView - FTIndicator - Haring - KeychainAccess + - libCEd25519 - libsodium - M13Checkbox - MessageKit @@ -87,11 +95,17 @@ SPEC REPOS: - web3swift EXTERNAL SOURCES: + Lisk: + :branch: dev + :git: https://github.com/boyarkin-anton/lisk-swift.git MessageKit: :branch: develop :git: https://github.com/boyarkin-anton/MessageKit.git CHECKOUT OPTIONS: + Lisk: + :commit: cfc6aaa0a1713dcab0e96cd7f6a7e17c7f85f9fa + :git: https://github.com/boyarkin-anton/lisk-swift.git MessageKit: :commit: e2364d2394eddc211be6957893fbd0f726c233f0 :git: https://github.com/boyarkin-anton/MessageKit.git @@ -102,13 +116,16 @@ SPEC CHECKSUMS: BigInt: 8e8a52161c745cd3ab78e3dc346a9fbee51e6cf6 CryptoSwift: 6c778d69282bed3b4e975ff97a79d074f20bb011 DateToolsSwift: 875d97ff9e3a5d54abdd67a269b3f51c757b71ab + Ed25519: d84c847d8ffbb2f6fec71479abbb8fa3dfe12578 EFQRCode: 203846c7b135b5f7c1d18bb7923bc32a74c8b363 Eureka: 0748c7bbb2560130e43c7bfa83e99c98854a1f42 FreakingSimpleRoundImageView: 66a0f0fa1722faf63b662ba61f8441b09436a428 FTIndicator: f7f071fd159e5befa1d040a9ef2e3ab53fa9322c Haring: d2a4cfc00dfb63836dffc93e45919369f850e134 KeychainAccess: 7bd430028059754a3debab3cfc0bd1fc7fb85df3 + libCEd25519: 45de00a0944cb2d51e64fcef67bd9989c42d20ca libsodium: 9a8faa5ef2fa0d2d57bd7f7d79bf8fb7c1a9f0ea + Lisk: a900424110c208c425102ee8b6fd60d9b6b878c3 M13Checkbox: a8c2491cfbbca10797925fd0b9154a782bf40d89 MessageKit: 16160036b16476b04bd0e9f2c7ce67e662e503e4 MyLittlePinpad: ec1d9990d9715eab48205ecc08f4ff359ead521b @@ -122,6 +139,6 @@ SPEC CHECKSUMS: Swinject: f7f15a9672e99328c1f07f277091087917215700 web3swift: 79f72b48005f1b28e73191eaf78194d6cf5f8a35 -PODFILE CHECKSUM: 982f53dbc54c6aa3ca1ce0750bf6f1448a4f1684 +PODFILE CHECKSUM: ff39280a050c4af02c429afa499fcfae47a49cf2 COCOAPODS: 1.5.3 From d643448f42cd4f631ca215f8e75364477dba5c63 Mon Sep 17 00:00:00 2001 From: Anton B Date: Tue, 17 Jul 2018 17:31:38 +0300 Subject: [PATCH 035/175] Add Lisk Transaction history and Details screens --- Adamant.xcodeproj/project.pbxproj | 4 + .../LskApiServiceProtocol.swift | 2 + .../TransactionDetailsProtocol.swift | 2 +- .../TokensApiService/LskApiService.swift | 18 +- .../LSKTransactionsViewController.swift | 156 ++++++++++++++++++ .../Transactions/TransactionsRoutes.swift | 10 +- Podfile.lock | 2 +- 7 files changed, 190 insertions(+), 4 deletions(-) create mode 100644 Adamant/Stories/Transactions/LSKTransactionsViewController.swift diff --git a/Adamant.xcodeproj/project.pbxproj b/Adamant.xcodeproj/project.pbxproj index 5c8abc181..c51786fac 100644 --- a/Adamant.xcodeproj/project.pbxproj +++ b/Adamant.xcodeproj/project.pbxproj @@ -33,6 +33,7 @@ 64D059FF20D3116B003AD655 /* NodesListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D059FE20D3116A003AD655 /* NodesListViewController.swift */; }; 64D05A0220D51632003AD655 /* EthApiServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D05A0120D51632003AD655 /* EthApiServiceProtocol.swift */; }; 64D05A0420D516B4003AD655 /* EthApiService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D05A0320D516B4003AD655 /* EthApiService.swift */; }; + 64EE46B220FE0C8D00194DDA /* LSKTransactionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64EE46B120FE0C8D00194DDA /* LSKTransactionsViewController.swift */; }; 64F085D920E2D7600006DE68 /* ADMTransactionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64F085D820E2D7600006DE68 /* ADMTransactionsViewController.swift */; }; 64FA53CD20E1300B006783C9 /* ETHTransactionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64FA53CC20E1300A006783C9 /* ETHTransactionsViewController.swift */; }; 64FA53D120E24942006783C9 /* BaseTransactionDetailsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64FA53D020E24941006783C9 /* BaseTransactionDetailsViewController.swift */; }; @@ -240,6 +241,7 @@ 64D059FE20D3116A003AD655 /* NodesListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NodesListViewController.swift; sourceTree = ""; }; 64D05A0120D51632003AD655 /* EthApiServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EthApiServiceProtocol.swift; sourceTree = ""; }; 64D05A0320D516B4003AD655 /* EthApiService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EthApiService.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 = ""; }; 64FA53CC20E1300A006783C9 /* ETHTransactionsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ETHTransactionsViewController.swift; sourceTree = ""; }; 64FA53D020E24941006783C9 /* BaseTransactionDetailsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseTransactionDetailsViewController.swift; sourceTree = ""; }; @@ -699,6 +701,7 @@ E9E7CDC12003F5A400DFC4DB /* TransactionsViewController.swift */, 64F085D820E2D7600006DE68 /* ADMTransactionsViewController.swift */, 64FA53CC20E1300A006783C9 /* ETHTransactionsViewController.swift */, + 64EE46B120FE0C8D00194DDA /* LSKTransactionsViewController.swift */, E94E7B0B205D5E4A0042B639 /* TransactionsViewController.xib */, 64FA53D020E24941006783C9 /* BaseTransactionDetailsViewController.swift */, E9E7CDC52003F6D200DFC4DB /* TransactionTableViewCell.swift */, @@ -1316,6 +1319,7 @@ E9EC342120052ABB00C0E546 /* TransferViewController.swift in Sources */, E921534E20EE1E8700C0843F /* EurekaAlertLabelRow.swift in Sources */, E9A03FD420DBC824007653A1 /* NodeVersion.swift in Sources */, + 64EE46B220FE0C8D00194DDA /* LSKTransactionsViewController.swift in Sources */, E93EB09F20DA3FA4001F9601 /* NodesEditorRoutes.swift in Sources */, E9EC34142005178500C0E546 /* TransactionDetailsViewController.swift in Sources */, E98FC34220F9209900032D65 /* UIColor+adamant.swift in Sources */, diff --git a/Adamant/ServiceProtocols/LskApiServiceProtocol.swift b/Adamant/ServiceProtocols/LskApiServiceProtocol.swift index 29dcd8e99..e59d6d66b 100644 --- a/Adamant/ServiceProtocols/LskApiServiceProtocol.swift +++ b/Adamant/ServiceProtocols/LskApiServiceProtocol.swift @@ -7,6 +7,7 @@ // import Foundation +import Lisk protocol LskApiServiceProtocol: class { @@ -17,6 +18,7 @@ protocol LskApiServiceProtocol: class { // MARK: - Transactions func sendFunds(toAddress address: String, amount: Double, completion: @escaping (ApiServiceResult) -> Void) + func getTransactions(_ completion: @escaping (ApiServiceResult<[Transactions.TransactionModel]>) -> Void) // MARK: - Tools func getBalance(_ completion: @escaping (ApiServiceResult) -> Void) diff --git a/Adamant/ServiceProtocols/TransactionDetailsProtocol.swift b/Adamant/ServiceProtocols/TransactionDetailsProtocol.swift index 664d6273a..b86290bea 100644 --- a/Adamant/ServiceProtocols/TransactionDetailsProtocol.swift +++ b/Adamant/ServiceProtocols/TransactionDetailsProtocol.swift @@ -57,7 +57,7 @@ protocol TransactionDetailsProtocol { extension TransactionDetailsProtocol { func isOutgoing(_ address: String) -> Bool { - return senderAddress == address.lowercased() ? true : false + return senderAddress.lowercased() == address.lowercased() ? true : false } var haveChatroom: Bool { diff --git a/Adamant/Services/TokensApiService/LskApiService.swift b/Adamant/Services/TokensApiService/LskApiService.swift index b2cd03373..140ceadfb 100644 --- a/Adamant/Services/TokensApiService/LskApiService.swift +++ b/Adamant/Services/TokensApiService/LskApiService.swift @@ -120,7 +120,7 @@ class LskApiService: LskApiServiceProtocol { func sendFunds(toAddress address: String, amount: Double, completion: @escaping (ApiServiceResult) -> Void) { if let keys = self.account?.keys { - Transactions(client: .testnet).transfer(lsk: amount, to: address, keyPair: keys) { response in + transactionApi.transfer(lsk: amount, to: address, keyPair: keys) { response in switch response { case .success(let result): print(result.data.hashValue) @@ -134,6 +134,22 @@ class LskApiService: LskApiServiceProtocol { } } + func getTransactions(_ completion: @escaping (ApiServiceResult<[Transactions.TransactionModel]>) -> Void) { + if let address = self.account?.address { + transactionApi.transactions(senderIdOrRecipientId: address, limit: 100, offset: 0) { (response) in + switch response { + case .success(response: let result): + completion(.success(result.data)) + break + case .error(response: let error): + print("ERROR: " + error.message) + completion(.failure(.internalError(message: error.message, error: nil))) + break + } + } + } + } + // MARK: - Tools func getBalance(_ completion: @escaping (ApiServiceResult) -> Void) { if let address = self.account?.address { diff --git a/Adamant/Stories/Transactions/LSKTransactionsViewController.swift b/Adamant/Stories/Transactions/LSKTransactionsViewController.swift new file mode 100644 index 000000000..b8ecf5526 --- /dev/null +++ b/Adamant/Stories/Transactions/LSKTransactionsViewController.swift @@ -0,0 +1,156 @@ +// +// LSKTransactionsViewController.swift +// Adamant +// +// Created by Anton Boyarkin on 17/07/2018. +// Copyright © 2018 Adamant. All rights reserved. +// + +import UIKit +import Lisk +import web3swift +import BigInt + +class LSKTransactionsViewController: TransactionsViewController { + + // MARK: - Dependencies + var lskApiService: LskApiServiceProtocol! + var dialogService: DialogService! + var router: Router! + + // MARK: - Properties + var transactions: [Transactions.TransactionModel] = [] + + override func viewDidLoad() { + super.viewDidLoad() + + self.refreshControl.beginRefreshing() + + handleRefresh(self.refreshControl) + } + + override func handleRefresh(_ refreshControl: UIRefreshControl) { + self.lskApiService.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() + } + }) + } + + override func currentAddress() -> String { + guard let address = lskApiService.account?.address else { + return "" + } + return address + } + +} + +// MARK: - UITableView +extension LSKTransactionsViewController { + + 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.Transactions.ethTransactionDetails) as? BaseTransactionDetailsViewController else { + return + } + + controller.transaction = transaction + navigationController?.pushViewController(controller, animated: true) + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + guard let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, 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 + } +} + +extension Transactions.TransactionModel: TransactionDetailsProtocol { + var senderAddress: String { + return self.senderId + } + + var recipientAddress: String { + return self.recipientId ?? "" + } + + var sentDate: Date { + return Date(timeIntervalSince1970: TimeInterval(self.timestamp) + Constants.Time.epochSeconds) + } + + var amountValue: Double { + guard let string = Web3.Utils.formatToPrecision(BigUInt(self.amount) ?? BigUInt(0), numberDecimals: 8, formattingDecimals: 8, decimalSeparator: ".", fallbackToScientific: false), let value = Double(string) else { + return 0 + } + + return value + } + + var feeValue: Double { + guard let string = Web3.Utils.formatToPrecision(BigUInt(self.fee) ?? BigUInt(0), numberDecimals: 8, formattingDecimals: 8, decimalSeparator: ".", fallbackToScientific: false), let value = Double(string) else { + return 0 + } + + return value + } + + var confirmationsValue: String { + return "\(self.confirmations)" + } + + var block: String { + return self.blockId + } + + var showGoToExplorer: Bool { + return true + } + + var explorerUrl: URL? { + return URL(string: "https://testnet-explorer.lisk.io/tx/\(id)") + } + + var showGoToChat: Bool { + return false + } + + var chatroom: Chatroom? { + return nil + } + + var currencyCode: String { + return "LSK" + } +} diff --git a/Adamant/Stories/Transactions/TransactionsRoutes.swift b/Adamant/Stories/Transactions/TransactionsRoutes.swift index d483a45d0..eb902c111 100644 --- a/Adamant/Stories/Transactions/TransactionsRoutes.swift +++ b/Adamant/Stories/Transactions/TransactionsRoutes.swift @@ -29,7 +29,7 @@ extension AdamantScene { return c }) - static let ethTransactions = AdamantScene(identifier: "TransactionsViewController", factory: { r in + static let ethTransactions = AdamantScene(identifier: "ETHTransactionsViewController", factory: { r in let c = ETHTransactionsViewController(nibName: "TransactionsViewController", bundle: nil) c.ethApiService = r.resolve(EthApiServiceProtocol.self) c.dialogService = r.resolve(DialogService.self) @@ -37,6 +37,14 @@ extension AdamantScene { return c }) + static let lskTransactions = AdamantScene(identifier: "LSKTransactionsViewController", factory: { r in + let c = LSKTransactionsViewController(nibName: "TransactionsViewController", bundle: nil) + c.lskApiService = r.resolve(LskApiServiceProtocol.self) + c.dialogService = r.resolve(DialogService.self) + c.router = r.resolve(Router.self) + return c + }) + static let ethTransactionDetails = AdamantScene(identifier: "BaseTransactionDetailsViewController", factory: { r in let c = BaseTransactionDetailsViewController() c.dialogService = r.resolve(DialogService.self) diff --git a/Podfile.lock b/Podfile.lock index 57a688678..bd7db7f2e 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -104,7 +104,7 @@ EXTERNAL SOURCES: CHECKOUT OPTIONS: Lisk: - :commit: cfc6aaa0a1713dcab0e96cd7f6a7e17c7f85f9fa + :commit: e2014efcdd81d4c58db0c8887f97dd786de6470f :git: https://github.com/boyarkin-anton/lisk-swift.git MessageKit: :commit: e2364d2394eddc211be6957893fbd0f726c233f0 From d6ebd58c1cfcc316c679511281cedeed180d1f83 Mon Sep 17 00:00:00 2001 From: Anton B Date: Wed, 18 Jul 2018 16:14:18 +0300 Subject: [PATCH 036/175] fix sort for Lisk transactions hidtory --- Adamant/Services/TokensApiService/LskApiService.swift | 2 +- Podfile.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Adamant/Services/TokensApiService/LskApiService.swift b/Adamant/Services/TokensApiService/LskApiService.swift index 140ceadfb..2ff90271d 100644 --- a/Adamant/Services/TokensApiService/LskApiService.swift +++ b/Adamant/Services/TokensApiService/LskApiService.swift @@ -136,7 +136,7 @@ class LskApiService: LskApiServiceProtocol { func getTransactions(_ completion: @escaping (ApiServiceResult<[Transactions.TransactionModel]>) -> Void) { if let address = self.account?.address { - transactionApi.transactions(senderIdOrRecipientId: address, limit: 100, offset: 0) { (response) in + transactionApi.transactions(senderIdOrRecipientId: address, limit: 100, offset: 0, sort: APIRequest.Sort("timestamp", direction: .descending)) { (response) in switch response { case .success(response: let result): completion(.success(result.data)) diff --git a/Podfile.lock b/Podfile.lock index bd7db7f2e..ea112cb25 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -104,7 +104,7 @@ EXTERNAL SOURCES: CHECKOUT OPTIONS: Lisk: - :commit: e2014efcdd81d4c58db0c8887f97dd786de6470f + :commit: 952fea02d6076668f68eb252ec5bd8bf4e656960 :git: https://github.com/boyarkin-anton/lisk-swift.git MessageKit: :commit: e2364d2394eddc211be6957893fbd0f726c233f0 From ead2a35da09437b3084d555569d7a4347240d687 Mon Sep 17 00:00:00 2001 From: Anton B Date: Wed, 18 Jul 2018 16:19:08 +0300 Subject: [PATCH 037/175] Sending LSK tokens in chat --- Adamant/Models/TransferType.swift | 53 +++++++++++++- .../LskApiServiceProtocol.swift | 1 + .../TokensApiService/LskApiService.swift | 73 ++++++++++++++++--- .../Chats/ChatViewController+MessageKit.swift | 41 ++++++++--- .../Stories/Chats/ChatViewController.swift | 55 ++++++++++---- Adamant/Stories/Chats/ChatsRoutes.swift | 1 + 6 files changed, 185 insertions(+), 39 deletions(-) diff --git a/Adamant/Models/TransferType.swift b/Adamant/Models/TransferType.swift index a357289b9..7a38dd708 100644 --- a/Adamant/Models/TransferType.swift +++ b/Adamant/Models/TransferType.swift @@ -12,12 +12,14 @@ import BigInt enum TransferType: String, Decodable { case eth = "eth_transaction" + case lsk = "lsk_transaction" case unknown init(from decoder: Decoder) throws { let label = try decoder.singleValueContainer().decode(String.self) switch label { case "eth_transaction": self = .eth + case "lsk_transaction": self = .lsk default: self = .unknown } } @@ -48,6 +50,7 @@ struct ChatTransfer: Decodable { switch type { case .eth: return renderETH() + case .lsk: return renderLSK() default: return NSAttributedString(string: "") } @@ -57,12 +60,13 @@ struct ChatTransfer: Decodable { switch type { case .eth: return renderETHPreview(isOutgoing: isOutgoing) + case .lsk: + return renderLSKPreview(isOutgoing: isOutgoing) default: return "" } } - // MARK: - Chat renderers private func renderETH() -> NSAttributedString { @@ -95,6 +99,33 @@ struct ChatTransfer: Decodable { return attributedString } + func renderLSK() -> NSAttributedString { + guard let amount = BigUInt(amount) else { + return NSAttributedString(string: "") + } + + let balance: String + + if let formattedAmount = Web3.Utils.formatToPrecision(amount, numberDecimals: 8, formattingDecimals: 8), let amount = Double(formattedAmount) { + balance = "\(amount) LSK" + } else { + balance = "-- LSK" + } + + let sent = String.adamantLocalized.chat.sent + + let attributedString = NSMutableAttributedString(string: "\(sent)\n\(balance)\n\n\(String.adamantLocalized.chat.tapForDetails)") + + let rangeReference = attributedString.string as NSString + let sentRange = rangeReference.range(of: sent) + let amountRange = rangeReference.range(of: balance) + + attributedString.setAttributes([.font: UIFont.adamantPrimary(size: 14)], range: sentRange) + attributedString.setAttributes([.font: UIFont.adamantPrimary(size: 28)], range: amountRange) + + return attributedString + } + // MARK: - Preview renderers func renderETHPreview(isOutgoing: Bool) -> String { @@ -119,4 +150,24 @@ struct ChatTransfer: Decodable { return "➡️ \(balance)" } } + + func renderLSKPreview(isOutgoing: Bool) -> String { + guard let amount = BigUInt(amount) else { + return "" + } + + let balance: String + + if let formattedAmount = Web3.Utils.formatToPrecision(amount, numberDecimals: 8, formattingDecimals: 8), let amount = Double(formattedAmount) { + balance = "\(amount) LSK" + } else { + balance = "-- LSK" + } + + if isOutgoing { + return String.localizedStringWithFormat(String.adamantLocalized.chatList.sentMessagePrefix, " ⬅️ \(balance)") + } else { + return "➡️ \(balance)" + } + } } diff --git a/Adamant/ServiceProtocols/LskApiServiceProtocol.swift b/Adamant/ServiceProtocols/LskApiServiceProtocol.swift index e59d6d66b..19dec62fd 100644 --- a/Adamant/ServiceProtocols/LskApiServiceProtocol.swift +++ b/Adamant/ServiceProtocols/LskApiServiceProtocol.swift @@ -19,6 +19,7 @@ protocol LskApiServiceProtocol: class { // MARK: - Transactions func sendFunds(toAddress address: String, amount: Double, completion: @escaping (ApiServiceResult) -> Void) func getTransactions(_ completion: @escaping (ApiServiceResult<[Transactions.TransactionModel]>) -> Void) + func getTransaction(byHash hash: String, completion: @escaping (ApiServiceResult) -> Void) // MARK: - Tools func getBalance(_ completion: @escaping (ApiServiceResult) -> Void) diff --git a/Adamant/Services/TokensApiService/LskApiService.swift b/Adamant/Services/TokensApiService/LskApiService.swift index 2ff90271d..fc96e7974 100644 --- a/Adamant/Services/TokensApiService/LskApiService.swift +++ b/Adamant/Services/TokensApiService/LskApiService.swift @@ -61,7 +61,7 @@ class LskApiService: LskApiServiceProtocol { do { let keys = try Crypto.keyPair(fromPassphrase: passphrase) let address = Crypto.address(fromPublicKey: keys.publicKeyString) - let account = LskAccount(keys: keys, address: address, balance: BigUInt(0), balanceString: "0 LSK") + let account = LskAccount(keys: keys, address: address, balance: BigUInt(0), balanceString: "0") self.account = account // print(address) completion(.success(account)) @@ -120,16 +120,43 @@ class LskApiService: LskApiServiceProtocol { func sendFunds(toAddress address: String, amount: Double, completion: @escaping (ApiServiceResult) -> Void) { if let keys = self.account?.keys { - transactionApi.transfer(lsk: amount, to: address, keyPair: keys) { response in - switch response { - case .success(let result): - print(result.data.hashValue) - print(result.data.message) - completion(.success(result.data.message)) - case .error(let error): - print("ERROR: " + error.message) - completion(.failure(.internalError(message: error.message, error: nil))) + + do { + let transaction = LocalTransaction(.transfer, lsk: amount, recipientId: address) + let signedTransaction = try transaction.signed(keyPair: keys) + + transactionApi.submit(signedTransaction: signedTransaction) { response in + switch response { + case .success(let result): + print(result.data.hashValue) + print(result.data.message) + + let amount = self.toRawLsk(value: amount) + + if let id = signedTransaction.id { + let result = ["type": "lsk_transaction", "amount": amount, "hash": id, "comments":""] + + do { + let data = try JSONEncoder().encode(result) + guard let raw = String(data: data, encoding: String.Encoding.utf8) else { + return + } + completion(.success(raw)) + } catch { + completion(.failure(.internalError(message: "LSK Wallet: Send - wrong data issue", error: nil))) + } + } else { + completion(.failure(.internalError(message: "LSK Wallet: Send - wrong data issue", error: nil))) + } + + + case .error(let error): + print("ERROR: " + error.message) + completion(.failure(.internalError(message: error.message, error: nil))) + } } + } catch { + completion(.failure(.internalError(message: error.localizedDescription, error: error))) } } } @@ -150,6 +177,24 @@ class LskApiService: LskApiServiceProtocol { } } + func getTransaction(byHash hash: String, completion: @escaping (ApiServiceResult) -> Void) { + transactionApi.transactions(id: hash, limit: 1, offset: 0) { (response) in + switch response { + case .success(response: let result): + if let transaction = result.data.first { + completion(.success(transaction)) + } else { + completion(.failure(.internalError(message: "No transaction", error: nil))) + } + break + case .error(response: let error): + print("ERROR: " + error.message) + completion(.failure(.internalError(message: error.message, error: nil))) + break + } + } + } + // MARK: - Tools func getBalance(_ completion: @escaping (ApiServiceResult) -> Void) { if let address = self.account?.address { @@ -193,8 +238,12 @@ class LskApiService: LskApiServiceProtocol { } } - func toRawLsk(value: Double) -> BigInt { - return BigInt(value) * BigInt(10).power(8) + func toRawLsk(value: Double) -> String { + if let formattedAmount = Web3.Utils.parseToBigUInt("\(value)", decimals: 8) { + return "\(formattedAmount)" + } else { + return "--" + } } private static let addressRegexString = "^([0-9]{2,22})L$" diff --git a/Adamant/Stories/Chats/ChatViewController+MessageKit.swift b/Adamant/Stories/Chats/ChatViewController+MessageKit.swift index b35a23085..77b11305a 100644 --- a/Adamant/Stories/Chats/ChatViewController+MessageKit.swift +++ b/Adamant/Stories/Chats/ChatViewController+MessageKit.swift @@ -204,7 +204,7 @@ extension ChatViewController: MessageCellDelegate { guard message.messageStatus == .failed else { if message.type == UInt16(ChatType.richMessage.rawValue) { - guard let data = message.message?.data(using: String.Encoding.utf8), let transfer = try? JSONDecoder().decode(ChatTransfer.self, from: data), transfer.type == .eth else { + guard let data = message.message?.data(using: String.Encoding.utf8), let transfer = try? JSONDecoder().decode(ChatTransfer.self, from: data), transfer.type != .unknown else { break } @@ -213,17 +213,36 @@ extension ChatViewController: MessageCellDelegate { } self.dialogService.showProgress(withMessage: String.adamantLocalized.transactionDetails.requestingDataProgressMessage, userInteractionEnable: false) - - self.ethApiService.getTransaction(byHash: transfer.hash) { (result) in - switch result { - case .success(let transaction): - vc.set(transaction: transaction) - self.dialogService.dismissProgress() - break - case .failure(let error): - self.dialogService.showError(withMessage: "Transrer issue", error: error) - break + + switch transfer.type { + case .eth: + self.ethApiService.getTransaction(byHash: transfer.hash) { (result) in + switch result { + case .success(let transaction): + vc.set(transaction: transaction) + self.dialogService.dismissProgress() + break + case .failure(let error): + self.dialogService.showError(withMessage: "Transrer issue", error: error) + break + } } + break + + case .lsk: + self.lskApiService.getTransaction(byHash: transfer.hash) { (result) in + switch result { + case .success(let transaction): + vc.set(transaction: transaction) + self.dialogService.dismissProgress() + break + case .failure(let error): + self.dialogService.showError(withMessage: "Transrer issue", error: error) + break + } + } + break + default: break } if let nav = navigationController { diff --git a/Adamant/Stories/Chats/ChatViewController.swift b/Adamant/Stories/Chats/ChatViewController.swift index 9f1d3941f..6b54fedf9 100644 --- a/Adamant/Stories/Chats/ChatViewController.swift +++ b/Adamant/Stories/Chats/ChatViewController.swift @@ -39,6 +39,7 @@ class ChatViewController: MessagesViewController { var dialogService: DialogService! var router: Router! var ethApiService: EthApiServiceProtocol! + var lskApiService: LskApiServiceProtocol! // MARK: Properties weak var delegate: ChatViewControllerDelegate? @@ -52,6 +53,7 @@ class ChatViewController: MessagesViewController { } private var ethAddress: String? + private var lskAddress: String? private(set) var chatController: NSFetchedResultsController? private var controllerChanges: [NSFetchedResultsChangeType:[(indexPath: IndexPath?, newIndexPath: IndexPath?)]] = [:] @@ -77,6 +79,23 @@ class ChatViewController: MessagesViewController { $0.image = #imageLiteral(resourceName: "attachment") }.onTouchUpInside { _ in self.dialogService.showSystemActionSheet(title: String.adamantLocalized.transfer.send, message: "", actions: [ + UIAlertAction(title: "ADM", style: .default, handler: { [weak self] (_) in + // MARK: Show ADM transfer details + if let address = self?.chatroom?.partner?.address { + guard let vc = self?.router.get(scene: AdamantScene.Account.transfer) as? TransferViewController else { + fatalError("Can't get TransferViewController scene") + } + + vc.token = .ADM + vc.toAddress = address + + if let nav = self?.navigationController { + nav.pushViewController(vc, animated: true) + } else { + self?.present(vc, animated: true, completion: nil) + } + } + }), UIAlertAction(title: "Ethereum", style: .default, handler: { [weak self] (_) in if let ethAddress = self?.ethAddress { // MARK: Show ETH transfer @@ -97,21 +116,24 @@ class ChatViewController: MessagesViewController { self?.dialogService.showWarning(withMessage: "User don't have public Eth wallet yet.") } }), - UIAlertAction(title: "ADM", style: .default, handler: { [weak self] (_) in - // MARK: Show ADM transfer details - if let address = self?.chatroom?.partner?.address { + UIAlertAction(title: "Lisk", style: .default, handler: { [weak self] (_) in + if let address = self?.lskAddress { + // MARK: Show ETH transfer guard let vc = self?.router.get(scene: AdamantScene.Account.transfer) as? TransferViewController else { fatalError("Can't get TransferViewController scene") } - vc.token = .ADM + vc.token = .LSK vc.toAddress = address + vc.delegate = self if let nav = self?.navigationController { nav.pushViewController(vc, animated: true) } else { self?.present(vc, animated: true, completion: nil) } + } else { + self?.dialogService.showWarning(withMessage: "User don't have public Lisk wallet yet.") } }) ]) @@ -218,17 +240,20 @@ class ChatViewController: MessagesViewController { messageInputBar.inputTextView.backgroundColor = UIColor.adamantChatSenderBackground messageInputBar.inputTextView.isEditable = false messageInputBar.sendButton.isEnabled = false -} else { -// MARK: 4. Check partner for Eth Address - -if let address = chatroom.partner?.address { -ethApiService.getEthAddress(byAdamandAddress: address) { (result) in -guard case .success(let address) = result, let ethAddress = address else { return } -self.ethAddress = ethAddress -} -} -} - + } else { + // MARK: 4. Check partner for Eth Address + + if let address = chatroom.partner?.address { + ethApiService.getEthAddress(byAdamandAddress: address) { (result) in + guard case .success(let address) = result, let ethAddress = address else { return } + self.ethAddress = ethAddress + } + lskApiService.getLskAddress(byAdamandAddress: address) { (result) in + guard case .success(let address) = result, let lskAddress = address else { return } + self.lskAddress = lskAddress + } + } + } // MARK: 4. Data let controller = chatsProvider.getChatController(for: chatroom) diff --git a/Adamant/Stories/Chats/ChatsRoutes.swift b/Adamant/Stories/Chats/ChatsRoutes.swift index b71abe530..2acaf7b40 100644 --- a/Adamant/Stories/Chats/ChatsRoutes.swift +++ b/Adamant/Stories/Chats/ChatsRoutes.swift @@ -27,6 +27,7 @@ extension AdamantScene { c.dialogService = r.resolve(DialogService.self) c.router = r.resolve(Router.self) c.ethApiService = r.resolve(EthApiServiceProtocol.self) + c.lskApiService = r.resolve(LskApiServiceProtocol.self) return c }) From 9fea742e63cf2db3bd8cb17559f60b5c1de87f2c Mon Sep 17 00:00:00 2001 From: Anton B Date: Wed, 18 Jul 2018 20:24:18 +0300 Subject: [PATCH 038/175] Rebase on develop branch with New design Rebase Resolse conflicts --- Adamant.xcodeproj/project.pbxproj | 32 +++++++--------- Adamant/Models/EthTransaction.swift | 4 +- Adamant/Models/TransferType.swift | 8 ++-- Adamant/ServiceProtocols/ApiService.swift | 1 - .../ApiService/AdamantApi+Chats.swift | 8 ++-- .../ApiService/AdamantApiService.swift | 1 + .../Account/AccountViewController.swift | 38 +++++++++---------- .../Chats/ChatViewController+MessageKit.swift | 4 +- .../ADMTransactionsViewController.swift | 18 +++++++++ ...BaseTransactionDetailsViewController.swift | 6 +-- .../TransactionsViewController.swift | 17 --------- Adamant/SwinjectDependencies.swift | 5 ++- Podfile.lock | 10 +---- 13 files changed, 72 insertions(+), 80 deletions(-) diff --git a/Adamant.xcodeproj/project.pbxproj b/Adamant.xcodeproj/project.pbxproj index c51786fac..63638c5fc 100644 --- a/Adamant.xcodeproj/project.pbxproj +++ b/Adamant.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* Begin PBXBuildFile section */ 4411402421C8B290703B13EB /* Pods_Adamant.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C107D5CB65B4D728B9D97C0F /* Pods_Adamant.framework */; }; + 642361BE20E3869D0061559E /* eth_l18n.strings in Resources */ = {isa = PBXBuildFile; fileRef = 642361BD20E3869D0061559E /* eth_l18n.strings */; }; 644EC34D20EFA60900F40C73 /* AdamantApi+Delegates.swift in Sources */ = {isa = PBXBuildFile; fileRef = 644EC34C20EFA60900F40C73 /* AdamantApi+Delegates.swift */; }; 644EC34F20EFA77A00F40C73 /* Delegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 644EC34E20EFA77A00F40C73 /* Delegate.swift */; }; 644EC35220EFA9A300F40C73 /* DelegateRoutes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 644EC35120EFA9A300F40C73 /* DelegateRoutes.swift */; }; @@ -16,16 +17,8 @@ 644EC35B20EFB8E900F40C73 /* AdamantDelegateCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 644EC35920EFB8E900F40C73 /* AdamantDelegateCell.swift */; }; 644EC35C20EFB8E900F40C73 /* AdamantDelegateCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 644EC35A20EFB8E900F40C73 /* AdamantDelegateCell.xib */; }; 644EC35E20F34F1E00F40C73 /* DelegateDetailsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 644EC35D20F34F1E00F40C73 /* DelegateDetailsViewController.swift */; }; - 64A223D620F760BB005157CB /* Localization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64A223D520F760BB005157CB /* Localization.swift */; }; - 64D059FF20D3116B003AD655 /* NodesListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D059FE20D3116A003AD655 /* NodesListViewController.swift */; }; - 64E8305020F5FEEF006FA590 /* VotesAsset.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64E8304F20F5FEEF006FA590 /* VotesAsset.swift */; }; - E90055F520EBF5DA00D0CB2D /* AboutViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E90055F420EBF5DA00D0CB2D /* AboutViewController.swift */; }; - E90055F720EC200900D0CB2D /* SecurityViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E90055F620EC200900D0CB2D /* SecurityViewController.swift */; }; - E90055F920ECD86800D0CB2D /* SecurityViewController+StayIn.swift in Sources */ = {isa = PBXBuildFile; fileRef = E90055F820ECD86800D0CB2D /* SecurityViewController+StayIn.swift */; }; - E90055FB20ECE78A00D0CB2D /* SecurityViewController+notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = E90055FA20ECE78A00D0CB2D /* SecurityViewController+notifications.swift */; }; - 644D3DBB20BD5F22002EB002 /* Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = 644D3DBA20BD5F22002EB002 /* Status.swift */; }; - 642361BE20E3869D0061559E /* eth_l18n.strings in Resources */ = {isa = PBXBuildFile; fileRef = 642361BD20E3869D0061559E /* eth_l18n.strings */; }; 649A694720DD427200D27E91 /* TransferType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649A694620DD427200D27E91 /* TransferType.swift */; }; + 64A223D620F760BB005157CB /* Localization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64A223D520F760BB005157CB /* Localization.swift */; }; 64A223D820F7A08E005157CB /* LskApiServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64A223D720F7A08E005157CB /* LskApiServiceProtocol.swift */; }; 64A223DA20F7A14B005157CB /* LskApiService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64A223D920F7A14B005157CB /* LskApiService.swift */; }; 64BD2B7520E2814B00E2CD36 /* EthTransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64BD2B7420E2814B00E2CD36 /* EthTransaction.swift */; }; @@ -33,10 +26,15 @@ 64D059FF20D3116B003AD655 /* NodesListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D059FE20D3116A003AD655 /* NodesListViewController.swift */; }; 64D05A0220D51632003AD655 /* EthApiServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D05A0120D51632003AD655 /* EthApiServiceProtocol.swift */; }; 64D05A0420D516B4003AD655 /* EthApiService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D05A0320D516B4003AD655 /* EthApiService.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 */; }; 64FA53CD20E1300B006783C9 /* ETHTransactionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64FA53CC20E1300A006783C9 /* ETHTransactionsViewController.swift */; }; 64FA53D120E24942006783C9 /* BaseTransactionDetailsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64FA53D020E24941006783C9 /* BaseTransactionDetailsViewController.swift */; }; + E90055F520EBF5DA00D0CB2D /* AboutViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E90055F420EBF5DA00D0CB2D /* AboutViewController.swift */; }; + E90055F720EC200900D0CB2D /* SecurityViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E90055F620EC200900D0CB2D /* SecurityViewController.swift */; }; + E90055F920ECD86800D0CB2D /* SecurityViewController+StayIn.swift in Sources */ = {isa = PBXBuildFile; fileRef = E90055F820ECD86800D0CB2D /* SecurityViewController+StayIn.swift */; }; + E90055FB20ECE78A00D0CB2D /* SecurityViewController+notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = E90055FA20ECE78A00D0CB2D /* SecurityViewController+notifications.swift */; }; E905D39B2048A9BD00DDB504 /* KeychainStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = E905D39A2048A9BD00DDB504 /* KeychainStore.swift */; }; E905D39D204C13B900DDB504 /* SecuredStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = E905D39C204C13B900DDB504 /* SecuredStore.swift */; }; E905D39F204C281400DDB504 /* LoginViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E905D39E204C281400DDB504 /* LoginViewController.swift */; }; @@ -220,6 +218,7 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 642361BD20E3869D0061559E /* eth_l18n.strings */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; path = eth_l18n.strings; sourceTree = ""; }; 644EC34C20EFA60900F40C73 /* AdamantApi+Delegates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AdamantApi+Delegates.swift"; sourceTree = ""; }; 644EC34E20EFA77A00F40C73 /* Delegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Delegate.swift; sourceTree = ""; }; 644EC35120EFA9A300F40C73 /* DelegateRoutes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DelegateRoutes.swift; sourceTree = ""; }; @@ -228,12 +227,8 @@ 644EC35920EFB8E900F40C73 /* AdamantDelegateCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantDelegateCell.swift; sourceTree = ""; }; 644EC35A20EFB8E900F40C73 /* AdamantDelegateCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AdamantDelegateCell.xib; sourceTree = ""; }; 644EC35D20F34F1E00F40C73 /* DelegateDetailsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DelegateDetailsViewController.swift; sourceTree = ""; }; - 64A223D520F760BB005157CB /* Localization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Localization.swift; sourceTree = ""; }; - 64D059FE20D3116A003AD655 /* NodesListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NodesListViewController.swift; sourceTree = ""; }; - 64E8304F20F5FEEF006FA590 /* VotesAsset.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VotesAsset.swift; sourceTree = ""; }; - 644D3DBA20BD5F22002EB002 /* Status.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Status.swift; sourceTree = ""; }; - 642361BD20E3869D0061559E /* eth_l18n.strings */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; path = eth_l18n.strings; sourceTree = ""; }; 649A694620DD427200D27E91 /* TransferType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransferType.swift; sourceTree = ""; }; + 64A223D520F760BB005157CB /* Localization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Localization.swift; sourceTree = ""; }; 64A223D720F7A08E005157CB /* LskApiServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LskApiServiceProtocol.swift; sourceTree = ""; }; 64A223D920F7A14B005157CB /* LskApiService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LskApiService.swift; sourceTree = ""; }; 64BD2B7420E2814B00E2CD36 /* EthTransaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EthTransaction.swift; sourceTree = ""; }; @@ -241,6 +236,7 @@ 64D059FE20D3116A003AD655 /* NodesListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NodesListViewController.swift; sourceTree = ""; }; 64D05A0120D51632003AD655 /* EthApiServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EthApiServiceProtocol.swift; sourceTree = ""; }; 64D05A0320D516B4003AD655 /* EthApiService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EthApiService.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 = ""; }; 64FA53CC20E1300A006783C9 /* ETHTransactionsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ETHTransactionsViewController.swift; sourceTree = ""; }; @@ -1033,9 +1029,8 @@ E9EC341A200524CA00C0E546 /* Exo+2_700_normal.ttf in Resources */, E95CB456205D77B500A7218E /* Localizable.strings in Resources */, E95CB45E205D7F9600A7218E /* Localizable.stringsdict in Resources */, - E94E7B0E205D5EA80042B639 /* TransactionDetailsViewController.xib in Resources */, E91E5BF420DAF3AB00B06B3C /* NodeCell.xib in Resources */, - E91E5BF420DAF3AB00B06B3C /* NodeRow.xib in Resources */, + E91E5BF420DAF3AB00B06B3C /* NodeCell.xib in Resources */, E9EC341D200524CA00C0E546 /* Roboto_400_normal.ttf in Resources */, E9A174B920587B84003667CD /* notification.mp3 in Resources */, E9EC3418200524CA00C0E546 /* Exo+2_400_normal.ttf in Resources */, @@ -1080,8 +1075,8 @@ "${BUILT_PRODUCTS_DIR}/FreakingSimpleRoundImageView/FreakingSimpleRoundImageView.framework", "${BUILT_PRODUCTS_DIR}/Haring/Haring.framework", "${BUILT_PRODUCTS_DIR}/KeychainAccess/KeychainAccess.framework", - "${BUILT_PRODUCTS_DIR}/M13Checkbox/M13Checkbox.framework", "${BUILT_PRODUCTS_DIR}/Lisk/Lisk.framework", + "${BUILT_PRODUCTS_DIR}/M13Checkbox/M13Checkbox.framework", "${BUILT_PRODUCTS_DIR}/MessageKit/MessageKit.framework", "${BUILT_PRODUCTS_DIR}/MyLittlePinpad/MyLittlePinpad.framework", "${BUILT_PRODUCTS_DIR}/PMAlertController/PMAlertController.framework", @@ -1110,8 +1105,8 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FreakingSimpleRoundImageView.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Haring.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/KeychainAccess.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/M13Checkbox.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Lisk.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/M13Checkbox.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MessageKit.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MyLittlePinpad.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/PMAlertController.framework", @@ -1321,7 +1316,6 @@ E9A03FD420DBC824007653A1 /* NodeVersion.swift in Sources */, 64EE46B220FE0C8D00194DDA /* LSKTransactionsViewController.swift in Sources */, E93EB09F20DA3FA4001F9601 /* NodesEditorRoutes.swift in Sources */, - E9EC34142005178500C0E546 /* TransactionDetailsViewController.swift in Sources */, E98FC34220F9209900032D65 /* UIColor+adamant.swift in Sources */, E948E04C2027679300975D6B /* AdamantFormattingTools.swift in Sources */, E9E7CDB12002B97B00DFC4DB /* AccountRoutes.swift in Sources */, diff --git a/Adamant/Models/EthTransaction.swift b/Adamant/Models/EthTransaction.swift index cbe1c8108..46a55a5c9 100644 --- a/Adamant/Models/EthTransaction.swift +++ b/Adamant/Models/EthTransaction.swift @@ -179,7 +179,7 @@ extension EthTransaction: TransactionDetailsProtocol { struct web3EthTransaction { let transaction: EthereumTransaction - let transactionBlock: Block? + let transactionBlock: web3swift.Block? let lastBlockNumber: BigUInt? } @@ -222,7 +222,7 @@ extension web3EthTransaction: TransactionDetailsProtocol { var confirmationsValue: String { if let blockNumber = self.transactionBlock?.number, let lastBlockNumber = self.lastBlockNumber { - let confirmations = lastBlockNumber - blockNumber + let confirmations = lastBlockNumber - BigUInt(blockNumber) return "\(confirmations)" } else { return "--" diff --git a/Adamant/Models/TransferType.swift b/Adamant/Models/TransferType.swift index 7a38dd708..32f89a72d 100644 --- a/Adamant/Models/TransferType.swift +++ b/Adamant/Models/TransferType.swift @@ -93,8 +93,8 @@ struct ChatTransfer: Decodable { let sentRange = rangeReference.range(of: sent) let amountRange = rangeReference.range(of: balance) - attributedString.setAttributes([.font: UIFont.adamantPrimary(size: 14)], range: sentRange) - attributedString.setAttributes([.font: UIFont.adamantPrimary(size: 28)], range: amountRange) + attributedString.setAttributes([.font: UIFont.adamantPrimary(ofSize: 14)], range: sentRange) + attributedString.setAttributes([.font: UIFont.adamantPrimary(ofSize: 28)], range: amountRange) return attributedString } @@ -120,8 +120,8 @@ struct ChatTransfer: Decodable { let sentRange = rangeReference.range(of: sent) let amountRange = rangeReference.range(of: balance) - attributedString.setAttributes([.font: UIFont.adamantPrimary(size: 14)], range: sentRange) - attributedString.setAttributes([.font: UIFont.adamantPrimary(size: 28)], range: amountRange) + attributedString.setAttributes([.font: UIFont.adamantPrimary(ofSize: 14)], range: sentRange) + attributedString.setAttributes([.font: UIFont.adamantPrimary(ofSize: 28)], range: amountRange) return attributedString } diff --git a/Adamant/ServiceProtocols/ApiService.swift b/Adamant/ServiceProtocols/ApiService.swift index 73cc24c5e..8f4a1cb2b 100644 --- a/Adamant/ServiceProtocols/ApiService.swift +++ b/Adamant/ServiceProtocols/ApiService.swift @@ -110,7 +110,6 @@ protocol ApiService: class { /// Send text message /// - completion: Contains processed transactionId, if success, or AdamantError, if fails. func sendMessage(senderId: String, recipientId: String, keypair: Keypair, message: String, type: ChatType, nonce: String, completion: @escaping (ApiServiceResult) -> Void) - func sendSpecialMessage(senderId: String, recipientId: String, keypair: Keypair, message: String, nonce: String, completion: @escaping (ApiServiceResult) -> Void) // MARK: - Delegates diff --git a/Adamant/Services/ApiService/AdamantApi+Chats.swift b/Adamant/Services/ApiService/AdamantApi+Chats.swift index 6bd5fd2ef..9bcb4a6bf 100644 --- a/Adamant/Services/ApiService/AdamantApi+Chats.swift +++ b/Adamant/Services/ApiService/AdamantApi+Chats.swift @@ -51,16 +51,16 @@ extension AdamantApiService { } } } - + func sendMessage(senderId: String, recipientId: String, keypair: Keypair, message: String, nonce: String, completion: @escaping (ApiServiceResult) -> Void) { - self.sendMessage(senderId: senderId, recipientId: recipientId, keypair: keypair, message: message, nonce: nonce, type: ChatType.message, completion: completion) + self.sendMessage(senderId: senderId, recipientId: recipientId, keypair: keypair, message: message, type: ChatType.message, nonce: nonce, completion: completion) } func sendRichMessage(senderId: String, recipientId: String, keypair: Keypair, message: String, nonce: String, completion: @escaping (ApiServiceResult) -> Void) { - self.sendMessage(senderId: senderId, recipientId: recipientId, keypair: keypair, message: message, nonce: nonce, type: ChatType.richMessage, completion: completion) + self.sendMessage(senderId: senderId, recipientId: recipientId, keypair: keypair, message: message, type: ChatType.richMessage, nonce: nonce, completion: completion) } - func sendMessage(senderId: String, recipientId: String, keypair: Keypair, message: String, nonce: String, type: ChatType, completion: @escaping (ApiServiceResult) -> Void) { + func sendMessage(senderId: String, recipientId: String, keypair: Keypair, message: String, type: ChatType, nonce: String, completion: @escaping (ApiServiceResult) -> Void) { // MARK: 1. Prepare params let params: [String : Any] = [ "type": TransactionType.chatMessage.rawValue, diff --git a/Adamant/Services/ApiService/AdamantApiService.swift b/Adamant/Services/ApiService/AdamantApiService.swift index 0841dd3ab..8311d9a49 100644 --- a/Adamant/Services/ApiService/AdamantApiService.swift +++ b/Adamant/Services/ApiService/AdamantApiService.swift @@ -10,6 +10,7 @@ import Foundation import Alamofire class AdamantApiService: ApiService { + // MARK: - Shared constants struct ApiCommands { diff --git a/Adamant/Stories/Account/AccountViewController.swift b/Adamant/Stories/Account/AccountViewController.swift index c2b311b37..28b2a2acc 100644 --- a/Adamant/Stories/Account/AccountViewController.swift +++ b/Adamant/Stories/Account/AccountViewController.swift @@ -337,7 +337,7 @@ class AccountViewController: FormViewController { } NotificationCenter.default.addObserver(forName: Notification.Name.LskApiService.userLoggedIn, object: nil, queue: OperationQueue.main) { [weak self] _ in - self?.refreshLskCells() +// self?.refreshLskCells() } } @@ -635,22 +635,22 @@ extension AccountViewController { return section } -func refreshEthCells() { -if let row: AccountRow = form.rowBy(tag: Rows.ethAccount.tag) { -row.value = self.ethApiService.account?.address -row.reload() -} - -ethApiService.getBalance { (result) in -switch result { -case .success(let balance): -if let row: LabelRow = self.form.rowBy(tag: Rows.ethBalance.tag) { -row.value = balance -row.reload() -} -case .failure(let error): -print(error) -} -} -} + func refreshEthCells() { +// if let row: AccountRow = form.rowBy(tag: Rows.ethAccount.tag) { +// row.value = self.ethApiService.account?.address +// row.reload() +// } +// +// ethApiService.getBalance { (result) in +// switch result { +// case .success(let balance): +// if let row: LabelRow = self.form.rowBy(tag: Rows.ethBalance.tag) { +// row.value = balance +// row.reload() +// } +// case .failure(let error): +// print(error) +// } +// } + } } diff --git a/Adamant/Stories/Chats/ChatViewController+MessageKit.swift b/Adamant/Stories/Chats/ChatViewController+MessageKit.swift index 77b11305a..138e832d3 100644 --- a/Adamant/Stories/Chats/ChatViewController+MessageKit.swift +++ b/Adamant/Stories/Chats/ChatViewController+MessageKit.swift @@ -435,10 +435,10 @@ extension MessageTransaction: MessageType { if type == Int16(ChatType.richMessage.rawValue) { guard let data = message.data(using: String.Encoding.utf8), let transfer = try? JSONDecoder().decode(ChatTransfer.self, from: data) else { - return MessageData.text("") + return MessageKind.text("") } - return MessageData.attributedText(transfer.render()) + return MessageKind.attributedText(transfer.render()) } if isMarkdown { diff --git a/Adamant/Stories/Transactions/ADMTransactionsViewController.swift b/Adamant/Stories/Transactions/ADMTransactionsViewController.swift index 44255fcb4..7488ee21d 100644 --- a/Adamant/Stories/Transactions/ADMTransactionsViewController.swift +++ b/Adamant/Stories/Transactions/ADMTransactionsViewController.swift @@ -15,6 +15,7 @@ class ADMTransactionsViewController: TransactionsViewController { var transfersProvider: TransfersProvider! var chatsProvider: ChatsProvider! var dialogService: DialogService! + var stack: CoreDataStack! var router: Router! // MARK: - Properties @@ -75,6 +76,23 @@ class ADMTransactionsViewController: TransactionsViewController { } } } + + private func markTransfersAsRead() { + let privateContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) + privateContext.parent = stack.container.viewContext + + let request = NSFetchRequest(entityName: TransferTransaction.entityName) + request.predicate = NSPredicate(format: "isUnread == true") + request.sortDescriptors = [NSSortDescriptor(key: "transactionId", ascending: false)] + + if let result = try? privateContext.fetch(request) { + result.forEach { $0.isUnread = false } + + if privateContext.hasChanges { + try? privateContext.save() + } + } + } } // MARK: - UITableView diff --git a/Adamant/Stories/Transactions/BaseTransactionDetailsViewController.swift b/Adamant/Stories/Transactions/BaseTransactionDetailsViewController.swift index 240b529d7..518d63f7b 100644 --- a/Adamant/Stories/Transactions/BaseTransactionDetailsViewController.swift +++ b/Adamant/Stories/Transactions/BaseTransactionDetailsViewController.swift @@ -191,7 +191,7 @@ class BaseTransactionDetailsViewController: FormViewController { }) .cellUpdate({ (cell, _) in if let label = cell.textLabel { - label.font = UIFont.adamantPrimary(size: 17) + label.font = UIFont.adamantPrimary(ofSize: 17) label.textColor = UIColor.adamantPrimary } @@ -216,7 +216,7 @@ class BaseTransactionDetailsViewController: FormViewController { }) .cellUpdate({ (cell, _) in if let label = cell.textLabel { - label.font = UIFont.adamantPrimary(size: 17) + label.font = UIFont.adamantPrimary(ofSize: 17) label.textColor = UIColor.adamantPrimary } @@ -354,7 +354,7 @@ class BaseTransactionDetailsViewController: FormViewController { cell.textLabel?.textColor = UIColor.adamantPrimary cell.detailTextLabel?.textColor = UIColor.adamantSecondary - let font = UIFont.adamantPrimary(size: 17) + let font = UIFont.adamantPrimary(ofSize: 17) cell.textLabel?.font = font cell.detailTextLabel?.font = font } diff --git a/Adamant/Stories/Transactions/TransactionsViewController.swift b/Adamant/Stories/Transactions/TransactionsViewController.swift index e85beba7f..0302d3c45 100644 --- a/Adamant/Stories/Transactions/TransactionsViewController.swift +++ b/Adamant/Stories/Transactions/TransactionsViewController.swift @@ -90,23 +90,6 @@ extension TransactionsViewController { cell.dateLabel.text = transfer.sentDate.humanizedDateTime() } - - private func markTransfersAsRead() { - let privateContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) - privateContext.parent = stack.container.viewContext - - let request = NSFetchRequest(entityName: TransferTransaction.entityName) - request.predicate = NSPredicate(format: "isUnread == true") - request.sortDescriptors = [NSSortDescriptor(key: "transactionId", ascending: false)] - - if let result = try? privateContext.fetch(request) { - result.forEach { $0.isUnread = false } - - if privateContext.hasChanges { - try? privateContext.save() - } - } - } } // MARK: - UITableView diff --git a/Adamant/SwinjectDependencies.swift b/Adamant/SwinjectDependencies.swift index bb35aa1e5..4b6748899 100644 --- a/Adamant/SwinjectDependencies.swift +++ b/Adamant/SwinjectDependencies.swift @@ -73,7 +73,10 @@ extension Container { let service = AdamantApiService() service.adamantCore = r.resolve(AdamantCore.self) return service - }.inObjectScope(.container) + }.initCompleted { (r, c) in // Weak reference + let service = c as! AdamantApiService + service.nodesSource = r.resolve(NodesSource.self) + }.inObjectScope(.container) // MARK: Ethereum ApiService self.register(EthApiServiceProtocol.self) { r in diff --git a/Podfile.lock b/Podfile.lock index ea112cb25..faea507e7 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -98,20 +98,14 @@ EXTERNAL SOURCES: Lisk: :branch: dev :git: https://github.com/boyarkin-anton/lisk-swift.git - MessageKit: - :branch: develop - :git: https://github.com/boyarkin-anton/MessageKit.git CHECKOUT OPTIONS: Lisk: :commit: 952fea02d6076668f68eb252ec5bd8bf4e656960 :git: https://github.com/boyarkin-anton/lisk-swift.git - MessageKit: - :commit: e2364d2394eddc211be6957893fbd0f726c233f0 - :git: https://github.com/boyarkin-anton/MessageKit.git SPEC CHECKSUMS: - Alamofire: e4fa87002c137ba2d8d634d2c51fabcda0d5c223 + Alamofire: c7287b6e5d7da964a70935e5db17046b7fde6568 Alamofire-Synchronous: eedf1e6e961c3795a63c74990b3f7d9fbfac7e50 BigInt: 8e8a52161c745cd3ab78e3dc346a9fbee51e6cf6 CryptoSwift: 6c778d69282bed3b4e975ff97a79d074f20bb011 @@ -139,6 +133,6 @@ SPEC CHECKSUMS: Swinject: f7f15a9672e99328c1f07f277091087917215700 web3swift: 79f72b48005f1b28e73191eaf78194d6cf5f8a35 -PODFILE CHECKSUM: ff39280a050c4af02c429afa499fcfae47a49cf2 +PODFILE CHECKSUM: f167498be8316d6db16767ee403235192ed374dd COCOAPODS: 1.5.3 From 81cb6916cce92f257a3a4b9f9cc4b933e3cd38e3 Mon Sep 17 00:00:00 2001 From: Anton B Date: Sun, 22 Jul 2018 18:45:44 +0300 Subject: [PATCH 039/175] Integrate tokens in new account screen --- .../Icons/wallet_lsk.imageset/Contents.json | 23 ++ .../Icons/wallet_lsk.imageset/wallet_lsk.png | Bin 0 -> 1281 bytes .../wallet_lsk.imageset/wallet_lsk@2x.png | Bin 0 -> 2684 bytes .../wallet_lsk.imageset/wallet_lsk@3x.png | Bin 0 -> 4180 bytes .../Assets/l18n/de.lproj/Localizable.strings | 3 + .../Assets/l18n/en.lproj/Localizable.strings | 3 + .../Assets/l18n/ru.lproj/Localizable.strings | 3 + Adamant/Models/Wallet.swift | 26 +- .../TokensApiService/EthApiService.swift | 3 +- .../Account/AccountViewController.swift | 277 ++++++++++++++++-- 10 files changed, 297 insertions(+), 41 deletions(-) create mode 100644 Adamant/Assets/Assets.xcassets/Icons/wallet_lsk.imageset/Contents.json create mode 100644 Adamant/Assets/Assets.xcassets/Icons/wallet_lsk.imageset/wallet_lsk.png create mode 100644 Adamant/Assets/Assets.xcassets/Icons/wallet_lsk.imageset/wallet_lsk@2x.png create mode 100644 Adamant/Assets/Assets.xcassets/Icons/wallet_lsk.imageset/wallet_lsk@3x.png diff --git a/Adamant/Assets/Assets.xcassets/Icons/wallet_lsk.imageset/Contents.json b/Adamant/Assets/Assets.xcassets/Icons/wallet_lsk.imageset/Contents.json new file mode 100644 index 000000000..850b37580 --- /dev/null +++ b/Adamant/Assets/Assets.xcassets/Icons/wallet_lsk.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "wallet_lsk.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "wallet_lsk@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "wallet_lsk@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file 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 new file mode 100644 index 0000000000000000000000000000000000000000..908fda752e4e519fefd7fe577250dc6e89b39006 GIT binary patch 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 literal 0 HcmV?d00001 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 new file mode 100644 index 0000000000000000000000000000000000000000..c6a0035e62ead11ecb86be4f378e75319464befc GIT binary patch literal 2684 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 eH)pgORsIJ@ez3oG_JLaf0000 0 { + alertLabel.text = String(count) + } else { + alertLabel.isHidden = true + } + } + }.cellUpdate({ (cell, _) in + cell.accessoryType = .disclosureIndicator + }).onCellSelection({ [weak self] (_, _) in + guard let vc = self?.router.get(scene: AdamantScene.Transactions.ethTransactions), let nav = self?.navigationController else { + return + } + + nav.pushViewController(vc, animated: true) + }) + + // Send Tokens + <<< LabelRow() { + $0.title = Rows.sendTokens.localized + $0.cell.imageView?.image = #imageLiteral(resourceName: "row_icon_placeholder") + $0.cell.selectionStyle = .gray + }.cellUpdate({ (cell, _) in + cell.accessoryType = .disclosureIndicator + }) + .onCellSelection({ [weak self] (_, row) in + guard let vc = self?.router.get(scene: AdamantScene.Account.transfer) as? TransferViewController else { + fatalError("Can't get TransferViewController scene") + } + + vc.token = .ETH + + if let nav = self?.navigationController { + nav.pushViewController(vc, animated: true) + } else { + self?.present(vc, animated: true, completion: nil) + } + }) + + case .lisk: + // Account + section <<< LabelRow() { + $0.title = "" + }.cellUpdate({ [weak self] (cell, row) in + row.title = self?.lskApiService.account?.address + cell.accessoryType = .disclosureIndicator + }).onCellSelection({ [weak self] (_, row) in + guard let address = self?.lskApiService.account?.address else { + return + } + + self?.dialogService.presentShareAlertFor(string: address, + types: [.copyToPasteboard, .share, .generateQr(sharingTip: address)], + excludedActivityTypes: ShareContentType.address.excludedActivityTypes, + animated: true, + completion: { + guard let indexPath = row.indexPath else { + return + } + self?.tableView.deselectRow(at: indexPath, animated: true) + }) + }) + + // Balance + section <<< AlertLabelRow() { [weak self] in + $0.title = Rows.balanceLsk.localized + $0.tag = Rows.balanceLsk.tag + $0.value = wallet.formattedFull + $0.cell.imageView?.image = #imageLiteral(resourceName: "row_icon_placeholder") + $0.cell.selectionStyle = .gray + + if let alertLabel = $0.cell.alertLabel { + alertLabel.backgroundColor = UIColor.adamantPrimary + alertLabel.textColor = UIColor.white + alertLabel.clipsToBounds = true + alertLabel.textInsets = UIEdgeInsets(top: 1, left: 5, bottom: 1, right: 5) + + if let count = self?.transfersController?.fetchedObjects?.count, count > 0 { + alertLabel.text = String(count) + } else { + alertLabel.isHidden = true + } + } + }.cellUpdate({ (cell, _) in + cell.accessoryType = .disclosureIndicator + }).onCellSelection({ [weak self] (_, _) in + guard let vc = self?.router.get(scene: AdamantScene.Transactions.lskTransactions), let nav = self?.navigationController else { + return + } + + nav.pushViewController(vc, animated: true) + }) + + // Send Tokens + <<< LabelRow() { + $0.title = Rows.sendTokens.localized +// $0.tag = Rows.sendTokens.tag + $0.cell.imageView?.image = #imageLiteral(resourceName: "row_icon_placeholder") + $0.cell.selectionStyle = .gray + }.cellUpdate({ (cell, _) in + cell.accessoryType = .disclosureIndicator + }) + .onCellSelection({ [weak self] (_, row) in + guard let vc = self?.router.get(scene: AdamantScene.Account.transfer) as? TransferViewController else { + fatalError("Can't get TransferViewController scene") + } + + vc.token = .LSK + + if let nav = self?.navigationController { + nav.pushViewController(vc, animated: true) + } else { + self?.present(vc, animated: true, completion: nil) + } + }) } return section } func refreshEthCells() { -// if let row: AccountRow = form.rowBy(tag: Rows.ethAccount.tag) { -// row.value = self.ethApiService.account?.address -// row.reload() -// } -// -// ethApiService.getBalance { (result) in -// switch result { -// case .success(let balance): -// if let row: LabelRow = self.form.rowBy(tag: Rows.ethBalance.tag) { -// row.value = balance -// row.reload() -// } -// case .failure(let error): -// print(error) -// } -// } + ethApiService.getBalance { (result) in + switch result { + case .success(_): + if let wallets = self.wallets, let balanceString = self.ethApiService.account?.balanceString, let balance = Double(balanceString) { + + for i in 0.. Date: Mon, 23 Jul 2018 16:16:01 +0300 Subject: [PATCH 040/175] Add comment for ETH&LSK token transfer in chat Add comment for ETH&LSK token transfer in chat Cange amount fromat in rich messege for ETH&LSK token transfer in chat --- .../Assets/l18n/en.lproj/Localizable.strings | 3 + .../Assets/l18n/ru.lproj/Localizable.strings | 3 + Adamant/Models/TransferType.swift | 112 +++--------------- .../EthApiServiceProtocol.swift | 3 + .../LskApiServiceProtocol.swift | 3 + .../DataProviders/AdamantChatsProvider.swift | 1 - .../TokensApiService/EthApiService.swift | 103 ++++++++++++++-- .../TokensApiService/LskApiService.swift | 34 +++++- .../Account/TransferViewController.swift | 105 +++++++++++++--- 9 files changed, 244 insertions(+), 123 deletions(-) diff --git a/Adamant/Assets/l18n/en.lproj/Localizable.strings b/Adamant/Assets/l18n/en.lproj/Localizable.strings index df5bdb941..0bac72794 100755 --- a/Adamant/Assets/l18n/en.lproj/Localizable.strings +++ b/Adamant/Assets/l18n/en.lproj/Localizable.strings @@ -673,6 +673,9 @@ /* Transfer: transfer fee */ "TransferScene.Row.TransactionFee" = "Fee"; +/* Transfer: comment for transfer in chat */ +"TransferScene.Row.Comments" = "Comments"; + /* Transfer: 'Transfer info' section */ "TransferScene.Section.TransferInfo" = "Transfer Info"; diff --git a/Adamant/Assets/l18n/ru.lproj/Localizable.strings b/Adamant/Assets/l18n/ru.lproj/Localizable.strings index 3741de9b5..592c4f4e4 100644 --- a/Adamant/Assets/l18n/ru.lproj/Localizable.strings +++ b/Adamant/Assets/l18n/ru.lproj/Localizable.strings @@ -673,6 +673,9 @@ /* Transfer: transfer fee */ "TransferScene.Row.TransactionFee" = "Комиссия"; +/* Transfer: comment for transfer in chat */ +"TransferScene.Row.Comments" = "Комментарии"; + /* Transfer: 'Transfer info' section */ "TransferScene.Section.TransferInfo" = "Перевод"; diff --git a/Adamant/Models/TransferType.swift b/Adamant/Models/TransferType.swift index 32f89a72d..92cbdb5b0 100644 --- a/Adamant/Models/TransferType.swift +++ b/Adamant/Models/TransferType.swift @@ -23,6 +23,14 @@ enum TransferType: String, Decodable { default: self = .unknown } } + + var currencySymbol: String { + switch self { + case .eth: return "ETH" + case .lsk: return "LSK" + default: return "" + } + } } struct ChatTransfer: Decodable { @@ -47,74 +55,21 @@ struct ChatTransfer: Decodable { } func render() -> NSAttributedString { - switch type { - case .eth: - return renderETH() - case .lsk: return renderLSK() - default: + guard type != .unknown else { return NSAttributedString(string: "") } - } - - func renderPreview(isOutgoing: Bool) -> String { - switch type { - case .eth: - return renderETHPreview(isOutgoing: isOutgoing) - case .lsk: - return renderLSKPreview(isOutgoing: isOutgoing) - default: - return "" - } - } - - // MARK: - Chat renderers - - private func renderETH() -> NSAttributedString { - guard let amount = BigUInt(amount) else { - return NSAttributedString(string: "") - } - - let balance: String - if let formattedAmount = Web3.Utils.formatToEthereumUnits(amount, - toUnits: .eth, - decimals: 8, - fallbackToScientific: true), let amount = Double(formattedAmount) { - balance = "\(amount) ETH" - } else { - balance = "-- ETH" - } + let balance = "\(amount) \(type.currencySymbol)" let sent = String.adamantLocalized.chat.sent - let attributedString = NSMutableAttributedString(string: "\(sent)\n\(balance)\n\n\(String.adamantLocalized.chat.tapForDetails)") - - let rangeReference = attributedString.string as NSString - let sentRange = rangeReference.range(of: sent) - let amountRange = rangeReference.range(of: balance) - - attributedString.setAttributes([.font: UIFont.adamantPrimary(ofSize: 14)], range: sentRange) - attributedString.setAttributes([.font: UIFont.adamantPrimary(ofSize: 28)], range: amountRange) - - return attributedString - } - - func renderLSK() -> NSAttributedString { - guard let amount = BigUInt(amount) else { - return NSAttributedString(string: "") - } - - let balance: String + var attributedString = NSMutableAttributedString(string: "\(sent)\n\(balance)") - if let formattedAmount = Web3.Utils.formatToPrecision(amount, numberDecimals: 8, formattingDecimals: 8), let amount = Double(formattedAmount) { - balance = "\(amount) LSK" - } else { - balance = "-- LSK" + if comments != "" { + attributedString.append(NSAttributedString(string: "\n\(comments)")) } - let sent = String.adamantLocalized.chat.sent - - let attributedString = NSMutableAttributedString(string: "\(sent)\n\(balance)\n\n\(String.adamantLocalized.chat.tapForDetails)") + attributedString.append(NSAttributedString(string: "\n\n\(String.adamantLocalized.chat.tapForDetails)")) let rangeReference = attributedString.string as NSString let sentRange = rangeReference.range(of: sent) @@ -126,43 +81,8 @@ struct ChatTransfer: Decodable { return attributedString } - // MARK: - Preview renderers - - func renderETHPreview(isOutgoing: Bool) -> String { - guard let amount = BigUInt(amount) else { - return "" - } - - let balance: String - - if let formattedAmount = Web3.Utils.formatToEthereumUnits(amount, - toUnits: .eth, - decimals: 8, - fallbackToScientific: true), let amount = Double(formattedAmount) { - balance = "\(amount) ETH" - } else { - balance = "-- ETH" - } - - if isOutgoing { - return String.localizedStringWithFormat(String.adamantLocalized.chatList.sentMessagePrefix, " ⬅️ \(balance)") - } else { - return "➡️ \(balance)" - } - } - - func renderLSKPreview(isOutgoing: Bool) -> String { - guard let amount = BigUInt(amount) else { - return "" - } - - let balance: String - - if let formattedAmount = Web3.Utils.formatToPrecision(amount, numberDecimals: 8, formattingDecimals: 8), let amount = Double(formattedAmount) { - balance = "\(amount) LSK" - } else { - balance = "-- LSK" - } + func renderPreview(isOutgoing: Bool) -> String { + let balance = "\(amount) \(type.currencySymbol)" if isOutgoing { return String.localizedStringWithFormat(String.adamantLocalized.chatList.sentMessagePrefix, " ⬅️ \(balance)") diff --git a/Adamant/ServiceProtocols/EthApiServiceProtocol.swift b/Adamant/ServiceProtocols/EthApiServiceProtocol.swift index 8b0fc89eb..9ceeaaf23 100644 --- a/Adamant/ServiceProtocols/EthApiServiceProtocol.swift +++ b/Adamant/ServiceProtocols/EthApiServiceProtocol.swift @@ -17,6 +17,9 @@ protocol EthApiServiceProtocol: class { 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: TransactionIntermediate, completion: @escaping (ApiServiceResult) -> Void) + func sendFunds(toAddress address: String, amount: Double, completion: @escaping (ApiServiceResult) -> Void) func getTransactions(_ completion: @escaping (ApiServiceResult<[EthTransaction]>) -> Void) diff --git a/Adamant/ServiceProtocols/LskApiServiceProtocol.swift b/Adamant/ServiceProtocols/LskApiServiceProtocol.swift index 19dec62fd..0837746c5 100644 --- a/Adamant/ServiceProtocols/LskApiServiceProtocol.swift +++ b/Adamant/ServiceProtocols/LskApiServiceProtocol.swift @@ -17,6 +17,9 @@ protocol LskApiServiceProtocol: class { 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) + func sendFunds(toAddress address: String, amount: Double, completion: @escaping (ApiServiceResult) -> Void) func getTransactions(_ completion: @escaping (ApiServiceResult<[Transactions.TransactionModel]>) -> Void) func getTransaction(byHash hash: String, completion: @escaping (ApiServiceResult) -> Void) diff --git a/Adamant/Services/DataProviders/AdamantChatsProvider.swift b/Adamant/Services/DataProviders/AdamantChatsProvider.swift index 834af0974..d061af7de 100644 --- a/Adamant/Services/DataProviders/AdamantChatsProvider.swift +++ b/Adamant/Services/DataProviders/AdamantChatsProvider.swift @@ -337,7 +337,6 @@ extension AdamantChatsProvider { // MARK: 3. Create chat transaction - let type = ChatType.message let transaction = MessageTransaction(entity: MessageTransaction.entity(), insertInto: privateContext) transaction.date = Date() as NSDate transaction.recipientId = recipientId diff --git a/Adamant/Services/TokensApiService/EthApiService.swift b/Adamant/Services/TokensApiService/EthApiService.swift index 44b920cd8..423d1ee73 100644 --- a/Adamant/Services/TokensApiService/EthApiService.swift +++ b/Adamant/Services/TokensApiService/EthApiService.swift @@ -97,6 +97,8 @@ class EthApiService: EthApiServiceProtocol { completion(.success(account)) } + self.getBalance({ _ in }) + if let address = self.accountService.account?.address, let keypair = self.accountService.keypair { self.getEthAddress(byAdamandAddress: address) { (result) in switch result { @@ -147,6 +149,97 @@ class EthApiService: EthApiServiceProtocol { } } } + func createTransaction(toAddress address: String, amount: Double, completion: @escaping (ApiServiceResult) -> Void) { + DispatchQueue.global().async { + guard let destinationEthAddress = EthereumAddress(address) else { + DispatchQueue.main.async { + completion(.failure(.internalError(message: "ETH Wallet: Send - invalid destination address", error: nil))) + } + return + } + guard let amount = Web3.Utils.parseToBigUInt("\(amount)", units: .eth) else { + DispatchQueue.main.async { + completion(.failure(.internalError(message: "ETH Wallet: Send - invalid amount format", error: nil))) + } + return + } + + guard let ethAddressFrom = self.account?.wallet.addresses?.first else { + DispatchQueue.main.async { + completion(.failure(.internalError(message: "ETH Wallet: Send - no address found", error: nil))) + } + return + } + + guard let keystore = self.account?.wallet else { + DispatchQueue.main.async { + completion(.failure(.internalError(message: "ETH Wallet: Send - no keystore found", error: nil))) + } + return + } + + self.web3.addKeystoreManager(KeystoreManager([keystore])) + var options = Web3Options.defaultOptions() + // options.gasLimit = BigUInt(gasLimit) + options.from = ethAddressFrom + options.value = BigUInt(amount) + guard let contract = self.web3.contract(Web3.Utils.coldWalletABI, at: destinationEthAddress) else { + DispatchQueue.main.async { + completion(.failure(.internalError(message: "ETH Wallet: Send - contract loading error", error: nil))) + } + return + } + + guard let estimatedGas = contract.method(options: options)?.estimateGas(options: nil).value else { + DispatchQueue.main.async { + completion(.failure(.internalError(message: "ETH Wallet: Send - retrieving estimated gas error", error: nil))) + } + return + } + options.gasLimit = estimatedGas + guard let gasPrice = self.web3.eth.getGasPrice().value else { + DispatchQueue.main.async { + completion(.failure(.internalError(message: "ETH Wallet: Send - retrieving gas price error", error: nil))) + } + return + } + options.gasPrice = gasPrice + guard var intermediate = contract.method(options: options) else { + DispatchQueue.main.async { + completion(.failure(.internalError(message: "ETH Wallet: Send - create transaction issue", error: nil))) + } + return + } + + + DispatchQueue.main.async { + completion(.success(intermediate)) + } + } + } + + func sendTransaction(transaction: TransactionIntermediate, completion: @escaping (ApiServiceResult) -> Void) { + DispatchQueue.global().async { + let sendResult = transaction.send(password: "", options: nil) + + guard let sendValue = sendResult.value else { + DispatchQueue.main.async { + completion(.failure(.internalError(message: "ETH Wallet: Send - sending transaction error", error: nil))) + } + return + } + + guard let hash = sendValue["txhash"] else { + DispatchQueue.main.async { + completion(.failure(.internalError(message: "ETH Wallet: Send - fail to get transaction hash", error: nil))) + } + return + } + DispatchQueue.main.async { + completion(.success(hash)) + } + } + } func sendFunds(toAddress address: String, amount: Double, completion: @escaping (ApiServiceResult) -> Void) { DispatchQueue.global().async { @@ -223,14 +316,8 @@ class EthApiService: EthApiServiceProtocol { } return } - guard let formattedAmount = Web3.Utils.formatToEthereumUnits(amount, toUnits: .wei) else { - DispatchQueue.main.async { - completion(.failure(.internalError(message: "ETH Wallet: Send - fail to get transaction amount", error: nil))) - } - return - } - let result = ["type": "eth_transaction", "amount": formattedAmount, "hash": hash, "comments":""] + let result = ["type": "eth_transaction", "amount": "\(amount)", "hash": hash, "comments":""] do { let data = try JSONEncoder().encode(result) @@ -412,7 +499,7 @@ class TransactionsHistory { URLQueryItem(name: "address", value: address), URLQueryItem(name: "page", value: "\(page)"), URLQueryItem(name: "offset", value: "\(size)"), - URLQueryItem(name: "sort", value: "asc") + URLQueryItem(name: "sort", value: "desc") // ,URLQueryItem(name: "apikey", value: "YourApiKeyToken") ] diff --git a/Adamant/Services/TokensApiService/LskApiService.swift b/Adamant/Services/TokensApiService/LskApiService.swift index fc96e7974..78d4a9ef8 100644 --- a/Adamant/Services/TokensApiService/LskApiService.swift +++ b/Adamant/Services/TokensApiService/LskApiService.swift @@ -73,6 +73,8 @@ class LskApiService: LskApiServiceProtocol { 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 { @@ -118,6 +120,34 @@ class LskApiService: LskApiServiceProtocol { } } + func createTransaction(toAddress address: String, amount: Double, completion: @escaping (ApiServiceResult) -> Void) { + if let keys = self.account?.keys { + do { + let transaction = LocalTransaction(.transfer, lsk: amount, recipientId: address) + let signedTransaction = try transaction.signed(keyPair: keys) + + completion(.success(signedTransaction)) + } catch { + completion(.failure(.internalError(message: error.localizedDescription, error: error))) + } + } + } + + func sendTransaction(transaction: LocalTransaction, completion: @escaping (ApiServiceResult) -> Void) { + transactionApi.submit(signedTransaction: transaction) { response in + switch response { + case .success(let result): + print(result.data.hashValue) + print(result.data.message) + + completion(.success(transaction.id ?? "")) + case .error(let error): + print("ERROR: " + error.message) + completion(.failure(.internalError(message: error.message, error: nil))) + } + } + } + func sendFunds(toAddress address: String, amount: Double, completion: @escaping (ApiServiceResult) -> Void) { if let keys = self.account?.keys { @@ -131,10 +161,8 @@ class LskApiService: LskApiServiceProtocol { print(result.data.hashValue) print(result.data.message) - let amount = self.toRawLsk(value: amount) - if let id = signedTransaction.id { - let result = ["type": "lsk_transaction", "amount": amount, "hash": id, "comments":""] + let result = ["type": "lsk_transaction", "amount": "\(amount)", "hash": id, "comments":""] do { let data = try JSONEncoder().encode(result) diff --git a/Adamant/Stories/Account/TransferViewController.swift b/Adamant/Stories/Account/TransferViewController.swift index e83713249..2bd609505 100644 --- a/Adamant/Stories/Account/TransferViewController.swift +++ b/Adamant/Stories/Account/TransferViewController.swift @@ -56,6 +56,7 @@ class TransferViewController: FormViewController { case address case fee case total + case comments case sendButton var tag: String { @@ -66,6 +67,7 @@ class TransferViewController: FormViewController { case .address: return "recipient" case .fee: return "fee" case .total: return "total" + case .comments: return "comments" case .sendButton: return "send" } } @@ -78,6 +80,7 @@ class TransferViewController: FormViewController { case .address: return NSLocalizedString("TransferScene.Row.Recipient", comment: "Transfer: recipient address") case .fee: return NSLocalizedString("TransferScene.Row.TransactionFee", comment: "Transfer: transfer fee") case .total: return NSLocalizedString("TransferScene.Row.Total", comment: "Transfer: total amount of transaction: money to transfer adding fee") + case .comments: return NSLocalizedString("TransferScene.Row.Comments", comment: "Transfer: total amount of transaction: money to transfer adding fee") case .sendButton: return String.adamantLocalized.transfer.send } } @@ -139,6 +142,15 @@ class TransferViewController: FormViewController { case .LSK: createLSKForm() } + + if delegate != nil { + form +++ Section(Row.comments.localized) + <<< TextAreaRow() { + $0.title = Row.comments.localized + $0.tag = Row.comments.tag + $0.placeholder = Row.comments.localized + } + } // MARK: - Transfer section form +++ Section() @@ -611,17 +623,51 @@ class TransferViewController: FormViewController { private func sendEth(to recipient: String, amount: Double) { self.dialogService.showProgress(withMessage: String.adamantLocalized.transfer.transferProcessingMessage, userInteractionEnable: false) - self.ethApiService.sendFunds(toAddress: recipient, amount: amount) { (result) in + self.ethApiService.createTransaction(toAddress: recipient, amount: amount) { (result) in switch result { - case .success(let value): - print("Payload: \(value)") - - self.delegate?.transferFinished(with: value) - self.dialogService.showSuccess(withMessage: String.adamantLocalized.transfer.transferSuccess) - self.close() - + case .success(let transaction): + self.ethApiService.sendTransaction(transaction: transaction, completion: { (result) in + switch result { + case .success(let txHash): + DispatchQueue.global().async { + print("TxHash: \(txHash)") + + var message = ["type": "eth_transaction", "amount": "\(amount)", "hash": txHash, "comments":""] + + if let commentsRow = self.form.rowBy(tag: Row.comments.tag) as? TextAreaRow, + let comments = commentsRow.value { + message["comments"] = comments + } + + do { + let data = try JSONEncoder().encode(message) + guard let raw = String(data: data, encoding: String.Encoding.utf8) else { + return + } + + print("Payload: \(raw)") + DispatchQueue.main.async { + self.delegate?.transferFinished(with: raw) + self.dialogService.showSuccess(withMessage: String.adamantLocalized.transfer.transferSuccess) + self.close() + } + } catch { + DispatchQueue.main.async { + self.dialogService.showError(withMessage: "ETH Wallet: Send - wrong data issue", error: nil) + } + } + } + + break + case .failure(let error): + self.dialogService.showError(withMessage: "Transrer issue", error: error) + break + } + }) + break case .failure(let error): self.dialogService.showError(withMessage: "Transrer issue", error: error) + break } } } @@ -629,17 +675,46 @@ class TransferViewController: FormViewController { private func sendLsk(to recipient: String, amount: Double) { self.dialogService.showProgress(withMessage: String.adamantLocalized.transfer.transferProcessingMessage, userInteractionEnable: false) - self.lskApiService.sendFunds(toAddress: recipient, amount: amount) { (result) in + self.lskApiService.createTransaction(toAddress: recipient, amount: amount) { (result) in switch result { - case .success(let value): - print("Payload: \(value)") - - self.delegate?.transferFinished(with: value) - self.dialogService.showSuccess(withMessage: String.adamantLocalized.transfer.transferSuccess) - self.close() + case .success(let transaction): + if let id = transaction.id { + var message = ["type": "lsk_transaction", "amount": "\(amount)", "hash": id, "comments":""] + + if let commentsRow = self.form.rowBy(tag: Row.comments.tag) as? TextAreaRow, + let comments = commentsRow.value { + message["comments"] = comments + } + + do { + let data = try JSONEncoder().encode(message) + guard let raw = String(data: data, encoding: String.Encoding.utf8) else { + return + } + print("Payload: \(raw)") + self.delegate?.transferFinished(with: raw) + + self.lskApiService.sendTransaction(transaction: transaction, completion: { (result) in + switch result { + case .success(let hash): + print("Hash: \(hash)") + self.dialogService.showSuccess(withMessage: String.adamantLocalized.transfer.transferSuccess) + self.close() + case .failure(let error): + self.dialogService.showError(withMessage: "Transrer issue", error: error) + } + }) + } catch { + self.dialogService.showError(withMessage: "Transrer issue", error: nil) + } + } else { + self.dialogService.showError(withMessage: "Transrer issue", error: nil) + } + break case .failure(let error): self.dialogService.showError(withMessage: "Transrer issue", error: error) + break } } } From b9a24f1b1eb0baab816aea2ee7dea4148700f85a Mon Sep 17 00:00:00 2001 From: Anton B Date: Mon, 30 Jul 2018 17:19:49 +0300 Subject: [PATCH 041/175] Migrate KeyPair creation to native --- Adamant/Services/JSAdamantCore+Native.swift | 89 +++++++++++++++++++-- Adamant/Services/JSAdamantCore.swift | 18 ++--- Podfile | 10 ++- Podfile.lock | 14 +++- 4 files changed, 111 insertions(+), 20 deletions(-) diff --git a/Adamant/Services/JSAdamantCore+Native.swift b/Adamant/Services/JSAdamantCore+Native.swift index a637d0db8..5d86b7ca7 100644 --- a/Adamant/Services/JSAdamantCore+Native.swift +++ b/Adamant/Services/JSAdamantCore+Native.swift @@ -8,8 +8,56 @@ import Foundation import libsodium +import BigInt +import CryptoSwift extension JSAdamantCore { + + func createKeypairFor(rawHash: [UInt8]) -> Keypair? { + guard let keypair = makeKeypairFrom(seed: rawHash) else { + print("Unable create Keypair from seed") + return nil + } + + return Keypair(publicKey: keypair.publicKey.hexString(), privateKey: keypair.privateKey.hexString()) + } + + func createHashFor(passphrase: String) -> String? { + guard let hash = createPassPhraseHash(passphrase: passphrase) else { + print("Unable create hash from passphrase") + return nil + } + + return hash.hexString() + } + + func createKeypairFor(passphrase: String) -> Keypair? { + guard let hash = createPassPhraseHash(passphrase: passphrase) else { + print("Unable create hash from passphrase") + return nil + } + + guard let keypair = makeKeypairFrom(seed: hash) else { + print("Unable create Keypair from seed") + return nil + } + + return Keypair(publicKey: keypair.publicKey.hexString(), privateKey: keypair.privateKey.hexString()) + } + + func createPassPhraseHash(passphrase: String) -> [UInt8]? { + guard let seed = createSeed(passphrase) else { + print("FAIL to create Seed from passphrase bytes") + return nil + } + + guard let hash = hashSHA256(seed) else { + print("FAIL to create SHA256 from seed") + return nil + } + + return hash + } func encodeValue(_ value: [String: Any], privateKey privateKeyHex: String) -> (message: String, nonce: String)? { let data = ["payload": value] @@ -18,7 +66,7 @@ extension JSAdamantCore { let message = padded.bytes let privateKey = privateKeyHex.hexBytes() - guard let hash = hashSHA256(privateKey: privateKey) else { + guard let hash = hashSHA256(privateKey) else { print("FAIL to create SHA256 of private key") return nil } @@ -44,7 +92,7 @@ extension JSAdamantCore { let nonce = rawNonce.hexBytes() let privateKey = privateKeyHex.hexBytes() - guard let hash = hashSHA256(privateKey: privateKey) else { + guard let hash = hashSHA256(privateKey) else { print("FAIL to create SHA256 of private key") return nil } @@ -91,19 +139,19 @@ extension JSAdamantCore { return message } - private func hashSHA256(privateKey: Bytes) -> Bytes? { + private func hashSHA256(_ input: Bytes) -> Bytes? { var hash = Bytes(count: HashSHA256Bytes) guard .SUCCESS == crypto_hash_sha256( &hash, - privateKey, UInt64(privateKey.count) + input, UInt64(input.count) ).exitCode else { return nil } return hash } private func ed2curve(privateKey: Bytes) -> Bytes? { - var secretKey = Bytes(count: SecretKeyBytes) + var secretKey = Bytes(count: SecretCurveKeyBytes) guard .SUCCESS == crypto_sign_ed25519_sk_to_curve25519( &secretKey, @@ -118,14 +166,41 @@ extension JSAdamantCore { randombytes_buf(&nonce, NonceBytes) return nonce } + + private func createSeed(_ passphrase: String) -> [UInt8]? { + let password = passphrase.decomposedStringWithCompatibilityMapping + let salt = ("mnemonic").decomposedStringWithCompatibilityMapping + + if let seed = try? PKCS5.PBKDF2(password: password.bytes, salt: salt.bytes, iterations: 2048, keyLength: 64, variant: HMAC.Variant.sha512).calculate() { + return seed + } else { + return nil + } + } + + private func makeKeypairFrom(seed: Bytes) -> (publicKey: Bytes, privateKey: Bytes)? { + var publicKey = Bytes(count: PublicKeyBytes) + var privateKey = Bytes(count: SecretKeyBytes) + + guard .SUCCESS == crypto_sign_seed_keypair( + &publicKey, + &privateKey, + seed + ).exitCode else { return nil } + + return (publicKey: publicKey, privateKey: privateKey) + } } // MARK:- Helpers public let HashSHA256Bytes = Int(crypto_hash_sha256_bytes()) -public let SecretKeyBytes = Int(crypto_scalarmult_curve25519_bytes()) +public let SecretCurveKeyBytes = Int(crypto_scalarmult_curve25519_bytes()) public let MacBytes = Int(crypto_secretbox_macbytes()) public let NonceBytes = Int(crypto_secretbox_noncebytes()) -public var KeyBytes = Int(crypto_secretbox_keybytes()) +public let KeyBytes = Int(crypto_secretbox_keybytes()) +public let SeedBytes = Int(crypto_sign_seedbytes()) +public let PublicKeyBytes = Int(crypto_sign_publickeybytes()) +public let SecretKeyBytes = Int(crypto_sign_secretkeybytes()) public typealias Bytes = Array diff --git a/Adamant/Services/JSAdamantCore.swift b/Adamant/Services/JSAdamantCore.swift index 12d17bb9d..a3b844a52 100644 --- a/Adamant/Services/JSAdamantCore.swift +++ b/Adamant/Services/JSAdamantCore.swift @@ -203,7 +203,7 @@ extension JSAdamantCore { // MARK: - Keys extension JSAdamantCore { - func createKeypairFor(rawHash: [UInt8]) -> Keypair? { + func js_createKeypairFor(rawHash: [UInt8]) -> Keypair? { let jsHash = convertToJsHash(rawHash) if let keypairRaw = call(function: .makeKeypair, with: [jsHash]), @@ -218,15 +218,15 @@ extension JSAdamantCore { } } - func createKeypairFor(passphrase: String) -> Keypair? { - guard let hash = createHashFor(passphrase: passphrase), hash.count > 0 else { - return nil - } - - return createKeypairFor(rawHash: AdamantUtilities.getBytes(from: hash)) - } + func js_createKeypairFor(passphrase: String) -> Keypair? { + guard let hash = js_createHashFor(passphrase: passphrase), hash.count > 0 else { + return nil + } + + return createKeypairFor(rawHash: AdamantUtilities.getBytes(from: hash)) + } - func createHashFor(passphrase: String) -> String? { + func js_createHashFor(passphrase: String) -> String? { let hash: String? if let jsHash = call(function: .createPassPhraseHash, with: [passphrase]), !jsHash.isUndefined, diff --git a/Podfile b/Podfile index d73218f89..a184fa690 100644 --- a/Podfile +++ b/Podfile @@ -6,8 +6,6 @@ target 'Adamant' do pod 'Alamofire' # Network pod 'KeychainAccess' # Keychain - pod 'RNCryptor' # Cryptor - pod 'CryptoSwift' # MD5 hash pod 'Swinject' # Dependency Injection pod 'ReachabilitySwift' # Network status pod 'Haring' # Markdown parser @@ -26,6 +24,12 @@ target 'Adamant' do pod 'QRCodeReader.swift' # QR reader # Crypto - pod 'libsodium' + pod 'RNCryptor' # Cryptor + pod 'CryptoSwift' # MD5 hash + pod 'libsodium' # Sodium crypto library + + # Utility + pod 'ByteBackpacker' # Utility to pack value types into a Byte array + pod 'BigInt' # Big integer number lib end diff --git a/Podfile.lock b/Podfile.lock index 9f82ee4a8..7c7af184c 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -1,5 +1,8 @@ PODS: - Alamofire (4.7.3) + - BigInt (3.1.0): + - SipHash (~> 1.2) + - ByteBackpacker (1.2.1) - CryptoSwift (0.10.0) - DateToolsSwift (4.0.0) - EFQRCode (4.2.2) @@ -21,10 +24,13 @@ PODS: - QRCodeReader.swift (8.2.0) - ReachabilitySwift (4.1.0) - RNCryptor (5.0.3) + - SipHash (1.2.2) - Swinject (2.4.1) DEPENDENCIES: - Alamofire + - BigInt + - ByteBackpacker - CryptoSwift - DateToolsSwift - EFQRCode @@ -45,6 +51,8 @@ DEPENDENCIES: SPEC REPOS: https://github.com/cocoapods/specs.git: - Alamofire + - BigInt + - ByteBackpacker - CryptoSwift - DateToolsSwift - EFQRCode @@ -60,10 +68,13 @@ SPEC REPOS: - QRCodeReader.swift - ReachabilitySwift - RNCryptor + - SipHash - Swinject SPEC CHECKSUMS: Alamofire: c7287b6e5d7da964a70935e5db17046b7fde6568 + BigInt: 76b5dfdfa3e2e478d4ffdf161aeede5502e2742f + ByteBackpacker: df001da117faacdbf09b69a116ec480f6651c9ec CryptoSwift: 6c778d69282bed3b4e975ff97a79d074f20bb011 DateToolsSwift: 875d97ff9e3a5d54abdd67a269b3f51c757b71ab EFQRCode: 203846c7b135b5f7c1d18bb7923bc32a74c8b363 @@ -79,8 +90,9 @@ SPEC CHECKSUMS: QRCodeReader.swift: 003eb32f18a5a675b936ec82ba0ff368cddbff45 ReachabilitySwift: 6849231cd4e06559f3b9ef4a97a0a0f96d41e09f RNCryptor: c93d19029dcf7ff160aca0f24d6c9e7b0d82f664 + SipHash: fad90a4683e420c52ef28063063dbbce248ea6d4 Swinject: f7f15a9672e99328c1f07f277091087917215700 -PODFILE CHECKSUM: fde2e99336bc59c015a61b7d2cb1ac5b48462187 +PODFILE CHECKSUM: 0944be8d3423ca0ce3da6f94bdfcfb0eaf7cf75b COCOAPODS: 1.5.3 From d74e4cf0e658b46e1a48b9c8af67d8c7b179c057 Mon Sep 17 00:00:00 2001 From: Anton B Date: Mon, 30 Jul 2018 18:35:24 +0300 Subject: [PATCH 042/175] Native decryption for chat messages --- Adamant/Services/JSAdamantCore+Native.swift | 59 +++++++++++++++++++++ Adamant/Services/JSAdamantCore.swift | 2 +- 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/Adamant/Services/JSAdamantCore+Native.swift b/Adamant/Services/JSAdamantCore+Native.swift index 5d86b7ca7..29f7aa151 100644 --- a/Adamant/Services/JSAdamantCore+Native.swift +++ b/Adamant/Services/JSAdamantCore+Native.swift @@ -58,6 +58,30 @@ extension JSAdamantCore { return hash } + func decodeMessage(rawMessage: String, rawNonce: String, senderPublicKey senderKeyHex: String, privateKey privateKeyHex: String) -> String? { + let message = rawMessage.hexBytes() + let nonce = rawNonce.hexBytes() + let senderKey = senderKeyHex.hexBytes() + let privateKey = privateKeyHex.hexBytes() + + guard let publicKey = ed2curve(publicKey: senderKey) else { + print("FAIL to create ed2curve publick key from SHA256") + return nil + } + + guard let secretKey = ed2curve(privateKey: privateKey) else { + print("FAIL to create ed2curve secret key from SHA256") + return nil + } + + guard let decrepted = open(authenticatedCipherText: message, senderPublicKey: publicKey, recipientSecretKey: secretKey, nonce: nonce) else { + print("FAIL to decrypt") + return nil + } + + return decrepted.utf8String + } + func encodeValue(_ value: [String: Any], privateKey privateKeyHex: String) -> (message: String, nonce: String)? { let data = ["payload": value] @@ -125,6 +149,26 @@ extension JSAdamantCore { return (authenticatedCipherText: authenticatedCipherText, nonce: nonce) } + private func open(authenticatedCipherText: Bytes, senderPublicKey: Bytes, recipientSecretKey: Bytes, nonce: Bytes) -> Bytes? { + guard nonce.count == BoxNonceBytes, + authenticatedCipherText.count >= BoxMacBytes, + senderPublicKey.count == BoxPublicKeyBytes, + recipientSecretKey.count == BoxSecretKeyBytes + else { return nil } + + var message = Bytes(count: authenticatedCipherText.count - BoxMacBytes) + + guard .SUCCESS == crypto_box_open_easy( + &message, + authenticatedCipherText, UInt64(authenticatedCipherText.count), + nonce, + senderPublicKey, + recipientSecretKey + ).exitCode else { return nil } + + return message + } + private func open(authenticatedCipherText: Bytes, secretKey: Bytes, nonce: Bytes) -> Bytes? { guard authenticatedCipherText.count >= MacBytes else { return nil } var message = Bytes(count: authenticatedCipherText.count - MacBytes) @@ -150,6 +194,17 @@ extension JSAdamantCore { return hash } + private func ed2curve(publicKey: Bytes) -> Bytes? { + var publicED2Key = Bytes(count: SecretCurveKeyBytes) + + guard .SUCCESS == crypto_sign_ed25519_pk_to_curve25519( + &publicED2Key, + publicKey + ).exitCode else { return nil } + + return publicED2Key + } + private func ed2curve(privateKey: Bytes) -> Bytes? { var secretKey = Bytes(count: SecretCurveKeyBytes) @@ -196,11 +251,15 @@ extension JSAdamantCore { public let HashSHA256Bytes = Int(crypto_hash_sha256_bytes()) public let SecretCurveKeyBytes = Int(crypto_scalarmult_curve25519_bytes()) public let MacBytes = Int(crypto_secretbox_macbytes()) +public let BoxMacBytes = Int(crypto_box_macbytes()) public let NonceBytes = Int(crypto_secretbox_noncebytes()) +public var BoxNonceBytes = Int(crypto_box_noncebytes()) public let KeyBytes = Int(crypto_secretbox_keybytes()) public let SeedBytes = Int(crypto_sign_seedbytes()) public let PublicKeyBytes = Int(crypto_sign_publickeybytes()) public let SecretKeyBytes = Int(crypto_sign_secretkeybytes()) +public let BoxPublicKeyBytes = Int(crypto_box_publickeybytes()) +public let BoxSecretKeyBytes = Int(crypto_box_secretkeybytes()) public typealias Bytes = Array diff --git a/Adamant/Services/JSAdamantCore.swift b/Adamant/Services/JSAdamantCore.swift index a3b844a52..a9b17729b 100644 --- a/Adamant/Services/JSAdamantCore.swift +++ b/Adamant/Services/JSAdamantCore.swift @@ -314,7 +314,7 @@ extension JSAdamantCore { return encodedMessage } - func decodeMessage(rawMessage: String, rawNonce: String, senderPublicKey senderKeyHex: String, privateKey privateKeyHex: String) -> String? { + func js_decodeMessage(rawMessage: String, rawNonce: String, senderPublicKey senderKeyHex: String, privateKey privateKeyHex: String) -> String? { let message = convertToJsHash(AdamantUtilities.getBytes(from: rawMessage)) let nonce = convertToJsHash(AdamantUtilities.getBytes(from: rawNonce)) let senderKey = convertToJsHash(AdamantUtilities.getBytes(from: senderKeyHex)) From 9ec297916637ce121c1e3c6d0ff466291c070296 Mon Sep 17 00:00:00 2001 From: Anton B Date: Tue, 31 Jul 2018 11:40:39 +0300 Subject: [PATCH 043/175] Native encryption for chat messages --- Adamant/Services/JSAdamantCore+Native.swift | 55 +++++++++++++++++++-- Adamant/Services/JSAdamantCore.swift | 2 +- 2 files changed, 52 insertions(+), 5 deletions(-) diff --git a/Adamant/Services/JSAdamantCore+Native.swift b/Adamant/Services/JSAdamantCore+Native.swift index 29f7aa151..30103b0df 100644 --- a/Adamant/Services/JSAdamantCore+Native.swift +++ b/Adamant/Services/JSAdamantCore+Native.swift @@ -58,6 +58,33 @@ extension JSAdamantCore { return hash } + + func encodeMessage(_ message: String, recipientPublicKey publicKey: String, privateKey privateKeyHex: String) -> (message: String, nonce: String)? { + let message = message.bytes + let recipientKey = publicKey.hexBytes() + let privateKey = privateKeyHex.hexBytes() + + guard let publicKey = ed2curve(publicKey: recipientKey) else { + print("FAIL to create ed2curve publick key from SHA256") + return nil + } + + guard let secretKey = ed2curve(privateKey: privateKey) else { + print("FAIL to create ed2curve secret key from SHA256") + return nil + } + + guard let encrypted = seal(message: message, recipientPublicKey: publicKey, senderSecretKey: secretKey) else { + print("FAIL to encrypt") + return nil + } + + let encryptedMessage = encrypted.authenticatedCipherText.hexString() + let nonce = encrypted.nonce.hexString() + + return (message: encryptedMessage, nonce: nonce) + } + func decodeMessage(rawMessage: String, rawNonce: String, senderPublicKey senderKeyHex: String, privateKey privateKeyHex: String) -> String? { let message = rawMessage.hexBytes() let nonce = rawNonce.hexBytes() @@ -134,10 +161,30 @@ extension JSAdamantCore { return decrepted.utf8String } + private func seal(message: Bytes, recipientPublicKey: Bytes, senderSecretKey: Bytes) -> (authenticatedCipherText: Bytes, nonce: Bytes)? { + guard recipientPublicKey.count == BoxPublicKeyBytes, + senderSecretKey.count == BoxSecretKeyBytes + else { return nil } + + var authenticatedCipherText = Bytes(count: message.count + BoxMacBytes) + let nonce = self.nonce(BoxNonceBytes) + + guard .SUCCESS == crypto_box_easy( + &authenticatedCipherText, + message, + CUnsignedLongLong(message.count), + nonce, + recipientPublicKey, + senderSecretKey + ).exitCode else { return nil } + + return (authenticatedCipherText: authenticatedCipherText, nonce: nonce) + } + private func seal(message: Bytes, secretKey: Bytes) -> (authenticatedCipherText: Bytes, nonce: Bytes)? { guard secretKey.count == KeyBytes else { return nil } var authenticatedCipherText = Bytes(count: message.count + MacBytes) - let nonce = self.nonce() + let nonce = self.nonce(NonceBytes) guard .SUCCESS == crypto_secretbox_easy ( &authenticatedCipherText, @@ -216,9 +263,9 @@ extension JSAdamantCore { return secretKey } - private func nonce() -> Bytes { - var nonce = Bytes(count: NonceBytes) - randombytes_buf(&nonce, NonceBytes) + private func nonce(_ count: Int) -> Bytes { + var nonce = Bytes(count: count) + randombytes_buf(&nonce, count) return nonce } diff --git a/Adamant/Services/JSAdamantCore.swift b/Adamant/Services/JSAdamantCore.swift index a9b17729b..b2c9a3eb9 100644 --- a/Adamant/Services/JSAdamantCore.swift +++ b/Adamant/Services/JSAdamantCore.swift @@ -300,7 +300,7 @@ extension JSAdamantCore { // MARK: - Messages extension JSAdamantCore { - func encodeMessage(_ message: String, recipientPublicKey publicKey: String, privateKey privateKeyHex: String) -> (message: String, nonce: String)? { + func js_encodeMessage(_ message: String, recipientPublicKey publicKey: String, privateKey privateKeyHex: String) -> (message: String, nonce: String)? { let privateKey = AdamantUtilities.getBytes(from: privateKeyHex) let encodedMessage: (String, String)? From e9f94257d2b0c57d3bc74873400a6737e7929e86 Mon Sep 17 00:00:00 2001 From: Anton B Date: Tue, 31 Jul 2018 18:35:39 +0300 Subject: [PATCH 044/175] Native sign for chat messages --- Adamant.xcodeproj/project.pbxproj | 2 + Adamant/Services/JSAdamantCore+Native.swift | 116 +++++++++++++++++++- Adamant/Services/JSAdamantCore.swift | 2 +- Podfile | 1 - Podfile.lock | 10 +- 5 files changed, 119 insertions(+), 12 deletions(-) diff --git a/Adamant.xcodeproj/project.pbxproj b/Adamant.xcodeproj/project.pbxproj index 919388e36..ceee9321a 100644 --- a/Adamant.xcodeproj/project.pbxproj +++ b/Adamant.xcodeproj/project.pbxproj @@ -1034,6 +1034,7 @@ inputPaths = ( "${SRCROOT}/Pods/Target Support Files/Pods-Adamant/Pods-Adamant-frameworks.sh", "${BUILT_PRODUCTS_DIR}/Alamofire/Alamofire.framework", + "${BUILT_PRODUCTS_DIR}/ByteBackpacker/ByteBackpacker.framework", "${BUILT_PRODUCTS_DIR}/CryptoSwift/CryptoSwift.framework", "${BUILT_PRODUCTS_DIR}/DateToolsSwift/DateToolsSwift.framework", "${BUILT_PRODUCTS_DIR}/EFQRCode/EFQRCode.framework", @@ -1054,6 +1055,7 @@ name = "[CP] Embed Pods Frameworks"; outputPaths = ( "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Alamofire.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", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/EFQRCode.framework", diff --git a/Adamant/Services/JSAdamantCore+Native.swift b/Adamant/Services/JSAdamantCore+Native.swift index 30103b0df..fd2ac14ac 100644 --- a/Adamant/Services/JSAdamantCore+Native.swift +++ b/Adamant/Services/JSAdamantCore+Native.swift @@ -8,8 +8,8 @@ import Foundation import libsodium -import BigInt import CryptoSwift +import ByteBackpacker extension JSAdamantCore { @@ -59,6 +59,37 @@ extension JSAdamantCore { return hash } + func sign(transaction: SignableTransaction, senderId: String, keypair: Keypair) -> String? { + let privateKey = keypair.privateKey.hexBytes() + + let data = transaction.bytes + guard let hash = hashSHA256(data) else { + print("FAIL to create SHA256 of transaction") + return nil + } + + guard let signature = signature(message: hash, secretKey: privateKey) else { + print("FAIL to sign of transaction") + return nil + } + + return signature.hexString() + } + + public func signature(message: Bytes, secretKey: Bytes) -> Bytes? { + guard secretKey.count == SecretKeyBytes else { return nil } + var signature = Array(count: SignBytes) + + guard .SUCCESS == crypto_sign_detached ( + &signature, + nil, + message, UInt64(message.count), + secretKey + ).exitCode else { return nil } + + return signature + } + func encodeMessage(_ message: String, recipientPublicKey publicKey: String, privateKey privateKeyHex: String) -> (message: String, nonce: String)? { let message = message.bytes let recipientKey = publicKey.hexBytes() @@ -307,6 +338,7 @@ public let PublicKeyBytes = Int(crypto_sign_publickeybytes()) public let SecretKeyBytes = Int(crypto_sign_secretkeybytes()) public let BoxPublicKeyBytes = Int(crypto_box_publickeybytes()) public let BoxSecretKeyBytes = Int(crypto_box_secretkeybytes()) +public let SignBytes = Int(crypto_sign_bytes()) public typealias Bytes = Array @@ -420,3 +452,85 @@ func JSONStringify(value: AnyObject, prettyPrinted: Bool = false) -> String { return "" } + +// MARK: - Bytes +extension SignableTransaction { + + var bytes: [UInt8] { + return + typeBytes + + timestampBytes + + senderPublicKeyBytes + + requesterPublicKeyBytes + + recipientIdBytes + + amountBytes + + assetBytes + + signatureBytes + + signSignatureBytes + } + + var typeBytes: [UInt8] { + return [UInt8(type.rawValue)] + } + + var timestampBytes: [UInt8] { + return ByteBackpacker.pack(UInt32(timestamp), byteOrder: .littleEndian) + } + + var senderPublicKeyBytes: [UInt8] { + return senderPublicKey.hexBytes() + } + + var requesterPublicKeyBytes: [UInt8] { + return requesterPublicKey?.hexBytes() ?? [] + } + + var recipientIdBytes: [UInt8] { + guard + let value = recipientId?.replacingOccurrences(of: "U", with: ""), + let number = UInt64(value) else { return Bytes(count: 8) } + return ByteBackpacker.pack(number, byteOrder: .bigEndian) + } + + var amountBytes: [UInt8] { + let value = (self.amount.shiftedToAdamant() as NSDecimalNumber).uint64Value + let bytes = ByteBackpacker.pack(value, byteOrder: .littleEndian) + return bytes + } + + var signatureBytes: [UInt8] { + return [] + } + + var signSignatureBytes: [UInt8] { + return [] + } + + var assetBytes: [UInt8] { + switch type { + case .chatMessage: + guard let msg = asset.chat?.message, let own = asset.chat?.ownMessage, let type = asset.chat?.type else { return [] } + + return msg.hexBytes() + own.hexBytes() + ByteBackpacker.pack(UInt32(type.rawValue), byteOrder: .littleEndian) + + case .state: + // TODO: ____ + return [] + + case .vote: + guard + let votes = asset.votes?.votes + else { return [] } + + var bytes = [UInt8]() + for vote in votes { + bytes += vote.bytes + } + + return bytes + + default: + return [] + } + } +} diff --git a/Adamant/Services/JSAdamantCore.swift b/Adamant/Services/JSAdamantCore.swift index b2c9a3eb9..9a2cf9311 100644 --- a/Adamant/Services/JSAdamantCore.swift +++ b/Adamant/Services/JSAdamantCore.swift @@ -256,7 +256,7 @@ extension JSAdamantCore { // MARK: - Transactions extension JSAdamantCore { - func sign(transaction t: SignableTransaction, senderId: String, keypair: Keypair) -> String? { + func js_sign(transaction t: SignableTransaction, senderId: String, keypair: Keypair) -> String? { let asset = JSAsset() if let chat = t.asset.chat { asset.chat = JSChat(type: Int(chat.type.rawValue), message: chat.message, own_message: chat.ownMessage) diff --git a/Podfile b/Podfile index a184fa690..e41527905 100644 --- a/Podfile +++ b/Podfile @@ -30,6 +30,5 @@ target 'Adamant' do # Utility pod 'ByteBackpacker' # Utility to pack value types into a Byte array - pod 'BigInt' # Big integer number lib end diff --git a/Podfile.lock b/Podfile.lock index 7c7af184c..4c09ec2b2 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -1,7 +1,5 @@ PODS: - Alamofire (4.7.3) - - BigInt (3.1.0): - - SipHash (~> 1.2) - ByteBackpacker (1.2.1) - CryptoSwift (0.10.0) - DateToolsSwift (4.0.0) @@ -24,12 +22,10 @@ PODS: - QRCodeReader.swift (8.2.0) - ReachabilitySwift (4.1.0) - RNCryptor (5.0.3) - - SipHash (1.2.2) - Swinject (2.4.1) DEPENDENCIES: - Alamofire - - BigInt - ByteBackpacker - CryptoSwift - DateToolsSwift @@ -51,7 +47,6 @@ DEPENDENCIES: SPEC REPOS: https://github.com/cocoapods/specs.git: - Alamofire - - BigInt - ByteBackpacker - CryptoSwift - DateToolsSwift @@ -68,12 +63,10 @@ SPEC REPOS: - QRCodeReader.swift - ReachabilitySwift - RNCryptor - - SipHash - Swinject SPEC CHECKSUMS: Alamofire: c7287b6e5d7da964a70935e5db17046b7fde6568 - BigInt: 76b5dfdfa3e2e478d4ffdf161aeede5502e2742f ByteBackpacker: df001da117faacdbf09b69a116ec480f6651c9ec CryptoSwift: 6c778d69282bed3b4e975ff97a79d074f20bb011 DateToolsSwift: 875d97ff9e3a5d54abdd67a269b3f51c757b71ab @@ -90,9 +83,8 @@ SPEC CHECKSUMS: QRCodeReader.swift: 003eb32f18a5a675b936ec82ba0ff368cddbff45 ReachabilitySwift: 6849231cd4e06559f3b9ef4a97a0a0f96d41e09f RNCryptor: c93d19029dcf7ff160aca0f24d6c9e7b0d82f664 - SipHash: fad90a4683e420c52ef28063063dbbce248ea6d4 Swinject: f7f15a9672e99328c1f07f277091087917215700 -PODFILE CHECKSUM: 0944be8d3423ca0ce3da6f94bdfcfb0eaf7cf75b +PODFILE CHECKSUM: 98c70943a4bd95a5caff40dbfa88d2a08b2d29e3 COCOAPODS: 1.5.3 From 9600b366693d834ded3624bf004f9a2cb586f224 Mon Sep 17 00:00:00 2001 From: Anton B Date: Wed, 1 Aug 2018 11:52:13 +0300 Subject: [PATCH 045/175] add native sign for kvs transactions --- Adamant/Services/JSAdamantCore+Native.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Adamant/Services/JSAdamantCore+Native.swift b/Adamant/Services/JSAdamantCore+Native.swift index fd2ac14ac..6c7f9b003 100644 --- a/Adamant/Services/JSAdamantCore+Native.swift +++ b/Adamant/Services/JSAdamantCore+Native.swift @@ -514,8 +514,9 @@ extension SignableTransaction { return msg.hexBytes() + own.hexBytes() + ByteBackpacker.pack(UInt32(type.rawValue), byteOrder: .littleEndian) case .state: - // TODO: ____ - return [] + guard let key = asset.state?.key, let value = asset.state?.value, let type = asset.state?.type else { return [] } + + return value.bytes + key.bytes + ByteBackpacker.pack(UInt32(type.rawValue), byteOrder: .littleEndian) case .vote: guard From a395ff60914c272ff23cb9e46a3cfbdb2f2955d6 Mon Sep 17 00:00:00 2001 From: Anton B Date: Wed, 1 Aug 2018 13:00:08 +0300 Subject: [PATCH 046/175] Native passphrase generation --- Adamant.xcodeproj/project.pbxproj | 4 + Adamant/Services/JSAdamantCore+Native.swift | 16 +- Adamant/Utilities/Mnemonic.swift | 2120 +++++++++++++++++++ 3 files changed, 2132 insertions(+), 8 deletions(-) create mode 100644 Adamant/Utilities/Mnemonic.swift diff --git a/Adamant.xcodeproj/project.pbxproj b/Adamant.xcodeproj/project.pbxproj index ceee9321a..eb25b70ea 100644 --- a/Adamant.xcodeproj/project.pbxproj +++ b/Adamant.xcodeproj/project.pbxproj @@ -19,6 +19,7 @@ 644EC35E20F34F1E00F40C73 /* DelegateDetailsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 644EC35D20F34F1E00F40C73 /* DelegateDetailsViewController.swift */; }; 6455E9F121075D3600B2E94C /* AddressBookService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6455E9F021075D3600B2E94C /* AddressBookService.swift */; }; 6455E9F321075D8000B2E94C /* AdamantAddressBookService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6455E9F221075D8000B2E94C /* AdamantAddressBookService.swift */; }; + 649E9A152111B3C200686B01 /* Mnemonic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649E9A142111B3C200686B01 /* Mnemonic.swift */; }; 64A223D620F760BB005157CB /* Localization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64A223D520F760BB005157CB /* Localization.swift */; }; 64D059FF20D3116B003AD655 /* NodesListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D059FE20D3116A003AD655 /* NodesListViewController.swift */; }; 64E8305020F5FEEF006FA590 /* VotesAsset.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64E8304F20F5FEEF006FA590 /* VotesAsset.swift */; }; @@ -222,6 +223,7 @@ 644EC35D20F34F1E00F40C73 /* DelegateDetailsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DelegateDetailsViewController.swift; sourceTree = ""; }; 6455E9F021075D3600B2E94C /* AddressBookService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddressBookService.swift; sourceTree = ""; }; 6455E9F221075D8000B2E94C /* AdamantAddressBookService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantAddressBookService.swift; sourceTree = ""; }; + 649E9A142111B3C200686B01 /* Mnemonic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mnemonic.swift; sourceTree = ""; }; 64A223D520F760BB005157CB /* Localization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Localization.swift; sourceTree = ""; }; 64D059FE20D3116A003AD655 /* NodesListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NodesListViewController.swift; sourceTree = ""; }; 64E8304F20F5FEEF006FA590 /* VotesAsset.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VotesAsset.swift; sourceTree = ""; }; @@ -684,6 +686,7 @@ E9942B83203CBFCE00C163AF /* AdamantQRTools.swift */, E9E7CDB62003994E00DFC4DB /* AdamantUtilities.swift */, E950652220404C84008352E5 /* AdamantUriTools.swift */, + 649E9A142111B3C200686B01 /* Mnemonic.swift */, ); path = Utilities; sourceTree = ""; @@ -1209,6 +1212,7 @@ E913C8F21FFFA51D001A83F7 /* AppDelegate.swift in Sources */, E905D39B2048A9BD00DDB504 /* KeychainStore.swift in Sources */, E9E7CDCA20040CC200DFC4DB /* Transaction.swift in Sources */, + 649E9A152111B3C200686B01 /* Mnemonic.swift in Sources */, E95F85692006AB9D0070534A /* NormalizedTransaction.swift in Sources */, E9150B9B2066DA210065A985 /* BaseTransaction+CoreDataClass.swift in Sources */, E9150B9A2066DA210065A985 /* TransferTransaction+CoreDataProperties.swift in Sources */, diff --git a/Adamant/Services/JSAdamantCore+Native.swift b/Adamant/Services/JSAdamantCore+Native.swift index 6c7f9b003..6542eff06 100644 --- a/Adamant/Services/JSAdamantCore+Native.swift +++ b/Adamant/Services/JSAdamantCore+Native.swift @@ -59,6 +59,13 @@ extension JSAdamantCore { return hash } + func generateNewPassphrase() -> String { + if let passphrase = try? Mnemonic.generate().joined(separator: " ") { + return passphrase + } + return "" + } + func sign(transaction: SignableTransaction, senderId: String, keypair: Keypair) -> String? { let privateKey = keypair.privateKey.hexBytes() @@ -301,14 +308,7 @@ extension JSAdamantCore { } private func createSeed(_ passphrase: String) -> [UInt8]? { - let password = passphrase.decomposedStringWithCompatibilityMapping - let salt = ("mnemonic").decomposedStringWithCompatibilityMapping - - if let seed = try? PKCS5.PBKDF2(password: password.bytes, salt: salt.bytes, iterations: 2048, keyLength: 64, variant: HMAC.Variant.sha512).calculate() { - return seed - } else { - return nil - } + return Mnemonic.seed(passphrase: passphrase) } private func makeKeypairFrom(seed: Bytes) -> (publicKey: Bytes, privateKey: Bytes)? { diff --git a/Adamant/Utilities/Mnemonic.swift b/Adamant/Utilities/Mnemonic.swift new file mode 100644 index 000000000..15dab5a4b --- /dev/null +++ b/Adamant/Utilities/Mnemonic.swift @@ -0,0 +1,2120 @@ +// +// Mnemonic.swift +// Adamant +// +// Created by Anton Boyarkin on 01/08/2018. +// Copyright © 2018 Adamant. All rights reserved. +// + +import Foundation +import CryptoSwift + +public struct Mnemonic { + + public static func generate() throws -> [String] { + let byteCount = 16 + var bytes = Data(count: byteCount) + let status = bytes.withUnsafeMutableBytes { SecRandomCopyBytes(kSecRandomDefault, byteCount, $0) } + guard status == errSecSuccess else { throw MnemonicError.randomBytesError } + return generate(entropy: bytes) + } + + static func generate(entropy : Data) -> [String] { + let list = WordList.english + var bin = String(entropy.flatMap { ("00000000" + String($0, radix:2)).suffix(8) }) + + let hash = entropy.sha256() + let bits = entropy.count * 8 + let cs = bits / 32 + + let hashbits = String(hash.flatMap { ("00000000" + String($0, radix:2)).suffix(8) }) + let checksum = String(hashbits.prefix(cs)) + bin += checksum + + var mnemonic = [String]() + for i in 0..<(bin.count / 11) { + let wi = Int(bin[bin.index(bin.startIndex, offsetBy: i * 11).. [UInt8]? { + let mnemonic = m.joined(separator: " ") + let salt = ("mnemonic" + passphrase) + + return seed(passphrase: mnemonic, salt: salt) + } + + public static func seed(passphrase: String, salt: String = "mnemonic") -> [UInt8]? { + let password = passphrase.decomposedStringWithCompatibilityMapping + let salt = salt.decomposedStringWithCompatibilityMapping + + if let seed = try? PKCS5.PBKDF2(password: password.bytes, salt: salt.bytes, iterations: 2048, keyLength: 64, variant: HMAC.Variant.sha512).calculate() { + return seed + } else { + return nil + } + } +} + +public enum MnemonicError : Error { + case randomBytesError +} + +class WordList { + static var english: [String.SubSequence] = { + let words = + """ + abandon + ability + able + about + above + absent + absorb + abstract + absurd + abuse + access + accident + account + accuse + achieve + acid + acoustic + acquire + across + act + action + actor + actress + actual + adapt + add + addict + address + adjust + admit + adult + advance + advice + aerobic + affair + afford + afraid + again + age + agent + agree + ahead + aim + air + airport + aisle + alarm + album + alcohol + alert + alien + all + alley + allow + almost + alone + alpha + already + also + alter + always + amateur + amazing + among + amount + amused + analyst + anchor + ancient + anger + angle + angry + animal + ankle + announce + annual + another + answer + antenna + antique + anxiety + any + apart + apology + appear + apple + approve + april + arch + arctic + area + arena + argue + arm + armed + armor + army + around + arrange + arrest + arrive + arrow + art + artefact + artist + artwork + ask + aspect + assault + asset + assist + assume + asthma + athlete + atom + attack + attend + attitude + attract + auction + audit + august + aunt + author + auto + autumn + average + avocado + avoid + awake + aware + away + awesome + awful + awkward + axis + baby + bachelor + bacon + badge + bag + balance + balcony + ball + bamboo + banana + banner + bar + barely + bargain + barrel + base + basic + basket + battle + beach + bean + beauty + because + become + beef + before + begin + behave + behind + believe + below + belt + bench + benefit + best + betray + better + between + beyond + bicycle + bid + bike + bind + biology + bird + birth + bitter + black + blade + blame + blanket + blast + bleak + bless + blind + blood + blossom + blouse + blue + blur + blush + board + boat + body + boil + bomb + bone + bonus + book + boost + border + boring + borrow + boss + bottom + bounce + box + boy + bracket + brain + brand + brass + brave + bread + breeze + brick + bridge + brief + bright + bring + brisk + broccoli + broken + bronze + broom + brother + brown + brush + bubble + buddy + budget + buffalo + build + bulb + bulk + bullet + bundle + bunker + burden + burger + burst + bus + business + busy + butter + buyer + buzz + cabbage + cabin + cable + cactus + cage + cake + call + calm + camera + camp + can + canal + cancel + candy + cannon + canoe + canvas + canyon + capable + capital + captain + car + carbon + card + cargo + carpet + carry + cart + case + cash + casino + castle + casual + cat + catalog + catch + category + cattle + caught + cause + caution + cave + ceiling + celery + cement + census + century + cereal + certain + chair + chalk + champion + change + chaos + chapter + charge + chase + chat + cheap + check + cheese + chef + cherry + chest + chicken + chief + child + chimney + choice + choose + chronic + chuckle + chunk + churn + cigar + cinnamon + circle + citizen + city + civil + claim + clap + clarify + claw + clay + clean + clerk + clever + click + client + cliff + climb + clinic + clip + clock + clog + close + cloth + cloud + clown + club + clump + cluster + clutch + coach + coast + coconut + code + coffee + coil + coin + collect + color + column + combine + come + comfort + comic + common + company + concert + conduct + confirm + congress + connect + consider + control + convince + cook + cool + copper + copy + coral + core + corn + correct + cost + cotton + couch + country + couple + course + cousin + cover + coyote + crack + cradle + craft + cram + crane + crash + crater + crawl + crazy + cream + credit + creek + crew + cricket + crime + crisp + critic + crop + cross + crouch + crowd + crucial + cruel + cruise + crumble + crunch + crush + cry + crystal + cube + culture + cup + cupboard + curious + current + curtain + curve + cushion + custom + cute + cycle + dad + damage + damp + dance + danger + daring + dash + daughter + dawn + day + deal + debate + debris + decade + december + decide + decline + decorate + decrease + deer + defense + define + defy + degree + delay + deliver + demand + demise + denial + dentist + deny + depart + depend + deposit + depth + deputy + derive + describe + desert + design + desk + despair + destroy + detail + detect + develop + device + devote + diagram + dial + diamond + diary + dice + diesel + diet + differ + digital + dignity + dilemma + dinner + dinosaur + direct + dirt + disagree + discover + disease + dish + dismiss + disorder + display + distance + divert + divide + divorce + dizzy + doctor + document + dog + doll + dolphin + domain + donate + donkey + donor + door + dose + double + dove + draft + dragon + drama + drastic + draw + dream + dress + drift + drill + drink + drip + drive + drop + drum + dry + duck + dumb + dune + during + dust + dutch + duty + dwarf + dynamic + eager + eagle + early + earn + earth + easily + east + easy + echo + ecology + economy + edge + edit + educate + effort + egg + eight + either + elbow + elder + electric + elegant + element + elephant + elevator + elite + else + embark + embody + embrace + emerge + emotion + employ + empower + empty + enable + enact + end + endless + endorse + enemy + energy + enforce + engage + engine + enhance + enjoy + enlist + enough + enrich + enroll + ensure + enter + entire + entry + envelope + episode + equal + equip + era + erase + erode + erosion + error + erupt + escape + essay + essence + estate + eternal + ethics + evidence + evil + evoke + evolve + exact + example + excess + exchange + excite + exclude + excuse + execute + exercise + exhaust + exhibit + exile + exist + exit + exotic + expand + expect + expire + explain + expose + express + extend + extra + eye + eyebrow + fabric + face + faculty + fade + faint + faith + fall + false + fame + family + famous + fan + fancy + fantasy + farm + fashion + fat + fatal + father + fatigue + fault + favorite + feature + february + federal + fee + feed + feel + female + fence + festival + fetch + fever + few + fiber + fiction + field + figure + file + film + filter + final + find + fine + finger + finish + fire + firm + first + fiscal + fish + fit + fitness + fix + flag + flame + flash + flat + flavor + flee + flight + flip + float + flock + floor + flower + fluid + flush + fly + foam + focus + fog + foil + fold + follow + food + foot + force + forest + forget + fork + fortune + forum + forward + fossil + foster + found + fox + fragile + frame + frequent + fresh + friend + fringe + frog + front + frost + frown + frozen + fruit + fuel + fun + funny + furnace + fury + future + gadget + gain + galaxy + gallery + game + gap + garage + garbage + garden + garlic + garment + gas + gasp + gate + gather + gauge + gaze + general + genius + genre + gentle + genuine + gesture + ghost + giant + gift + giggle + ginger + giraffe + girl + give + glad + glance + glare + glass + glide + glimpse + globe + gloom + glory + glove + glow + glue + goat + goddess + gold + good + goose + gorilla + gospel + gossip + govern + gown + grab + grace + grain + grant + grape + grass + gravity + great + green + grid + grief + grit + grocery + group + grow + grunt + guard + guess + guide + guilt + guitar + gun + gym + habit + hair + half + hammer + hamster + hand + happy + harbor + hard + harsh + harvest + hat + have + hawk + hazard + head + health + heart + heavy + hedgehog + height + hello + helmet + help + hen + hero + hidden + high + hill + hint + hip + hire + history + hobby + hockey + hold + hole + holiday + hollow + home + honey + hood + hope + horn + horror + horse + hospital + host + hotel + hour + hover + hub + huge + human + humble + humor + hundred + hungry + hunt + hurdle + hurry + hurt + husband + hybrid + ice + icon + idea + identify + idle + ignore + ill + illegal + illness + image + imitate + immense + immune + impact + impose + improve + impulse + inch + include + income + increase + index + indicate + indoor + industry + infant + inflict + inform + inhale + inherit + initial + inject + injury + inmate + inner + innocent + input + inquiry + insane + insect + inside + inspire + install + intact + interest + into + invest + invite + involve + iron + island + isolate + issue + item + ivory + jacket + jaguar + jar + jazz + jealous + jeans + jelly + jewel + job + join + joke + journey + joy + judge + juice + jump + jungle + junior + junk + just + kangaroo + keen + keep + ketchup + key + kick + kid + kidney + kind + kingdom + kiss + kit + kitchen + kite + kitten + kiwi + knee + knife + knock + know + lab + label + labor + ladder + lady + lake + lamp + language + laptop + large + later + latin + laugh + laundry + lava + law + lawn + lawsuit + layer + lazy + leader + leaf + learn + leave + lecture + left + leg + legal + legend + leisure + lemon + lend + length + lens + leopard + lesson + letter + level + liar + liberty + library + license + life + lift + light + like + limb + limit + link + lion + liquid + list + little + live + lizard + load + loan + lobster + local + lock + logic + lonely + long + loop + lottery + loud + lounge + love + loyal + lucky + luggage + lumber + lunar + lunch + luxury + lyrics + machine + mad + magic + magnet + maid + mail + main + major + make + mammal + man + manage + mandate + mango + mansion + manual + maple + marble + march + margin + marine + market + marriage + mask + mass + master + match + material + math + matrix + matter + maximum + maze + meadow + mean + measure + meat + mechanic + medal + media + melody + melt + member + memory + mention + menu + mercy + merge + merit + merry + mesh + message + metal + method + middle + midnight + milk + million + mimic + mind + minimum + minor + minute + miracle + mirror + misery + miss + mistake + mix + mixed + mixture + mobile + model + modify + mom + moment + monitor + monkey + monster + month + moon + moral + more + morning + mosquito + mother + motion + motor + mountain + mouse + move + movie + much + muffin + mule + multiply + muscle + museum + mushroom + music + must + mutual + myself + mystery + myth + naive + name + napkin + narrow + nasty + nation + nature + near + neck + need + negative + neglect + neither + nephew + nerve + nest + net + network + neutral + never + news + next + nice + night + noble + noise + nominee + noodle + normal + north + nose + notable + note + nothing + notice + novel + now + nuclear + number + nurse + nut + oak + obey + object + oblige + obscure + observe + obtain + obvious + occur + ocean + october + odor + off + offer + office + often + oil + okay + old + olive + olympic + omit + once + one + onion + online + only + open + opera + opinion + oppose + option + orange + orbit + orchard + order + ordinary + organ + orient + original + orphan + ostrich + other + outdoor + outer + output + outside + oval + oven + over + own + owner + oxygen + oyster + ozone + pact + paddle + page + pair + palace + palm + panda + panel + panic + panther + paper + parade + parent + park + parrot + party + pass + patch + path + patient + patrol + pattern + pause + pave + payment + peace + peanut + pear + peasant + pelican + pen + penalty + pencil + people + pepper + perfect + permit + person + pet + phone + photo + phrase + physical + piano + picnic + picture + piece + pig + pigeon + pill + pilot + pink + pioneer + pipe + pistol + pitch + pizza + place + planet + plastic + plate + play + please + pledge + pluck + plug + plunge + poem + poet + point + polar + pole + police + pond + pony + pool + popular + portion + position + possible + post + potato + pottery + poverty + powder + power + practice + praise + predict + prefer + prepare + present + pretty + prevent + price + pride + primary + print + priority + prison + private + prize + problem + process + produce + profit + program + project + promote + proof + property + prosper + protect + proud + provide + public + pudding + pull + pulp + pulse + pumpkin + punch + pupil + puppy + purchase + purity + purpose + purse + push + put + puzzle + pyramid + quality + quantum + quarter + question + quick + quit + quiz + quote + rabbit + raccoon + race + rack + radar + radio + rail + rain + raise + rally + ramp + ranch + random + range + rapid + rare + rate + rather + raven + raw + razor + ready + real + reason + rebel + rebuild + recall + receive + recipe + record + recycle + reduce + reflect + reform + refuse + region + regret + regular + reject + relax + release + relief + rely + remain + remember + remind + remove + render + renew + rent + reopen + repair + repeat + replace + report + require + rescue + resemble + resist + resource + response + result + retire + retreat + return + reunion + reveal + review + reward + rhythm + rib + ribbon + rice + rich + ride + ridge + rifle + right + rigid + ring + riot + ripple + risk + ritual + rival + river + road + roast + robot + robust + rocket + romance + roof + rookie + room + rose + rotate + rough + round + route + royal + rubber + rude + rug + rule + run + runway + rural + sad + saddle + sadness + safe + sail + salad + salmon + salon + salt + salute + same + sample + sand + satisfy + satoshi + sauce + sausage + save + say + scale + scan + scare + scatter + scene + scheme + school + science + scissors + scorpion + scout + scrap + screen + script + scrub + sea + search + season + seat + second + secret + section + security + seed + seek + segment + select + sell + seminar + senior + sense + sentence + series + service + session + settle + setup + seven + shadow + shaft + shallow + share + shed + shell + sheriff + shield + shift + shine + ship + shiver + shock + shoe + shoot + shop + short + shoulder + shove + shrimp + shrug + shuffle + shy + sibling + sick + side + siege + sight + sign + silent + silk + silly + silver + similar + simple + since + sing + siren + sister + situate + six + size + skate + sketch + ski + skill + skin + skirt + skull + slab + slam + sleep + slender + slice + slide + slight + slim + slogan + slot + slow + slush + small + smart + smile + smoke + smooth + snack + snake + snap + sniff + snow + soap + soccer + social + sock + soda + soft + solar + soldier + solid + solution + solve + someone + song + soon + sorry + sort + soul + sound + soup + source + south + space + spare + spatial + spawn + speak + special + speed + spell + spend + sphere + spice + spider + spike + spin + spirit + split + spoil + sponsor + spoon + sport + spot + spray + spread + spring + spy + square + squeeze + squirrel + stable + stadium + staff + stage + stairs + stamp + stand + start + state + stay + steak + steel + stem + step + stereo + stick + still + sting + stock + stomach + stone + stool + story + stove + strategy + street + strike + strong + struggle + student + stuff + stumble + style + subject + submit + subway + success + such + sudden + suffer + sugar + suggest + suit + summer + sun + sunny + sunset + super + supply + supreme + sure + surface + surge + surprise + surround + survey + suspect + sustain + swallow + swamp + swap + swarm + swear + sweet + swift + swim + swing + switch + sword + symbol + symptom + syrup + system + table + tackle + tag + tail + talent + talk + tank + tape + target + task + taste + tattoo + taxi + teach + team + tell + ten + tenant + tennis + tent + term + test + text + thank + that + theme + then + theory + there + they + thing + this + thought + three + thrive + throw + thumb + thunder + ticket + tide + tiger + tilt + timber + time + tiny + tip + tired + tissue + title + toast + tobacco + today + toddler + toe + together + toilet + token + tomato + tomorrow + tone + tongue + tonight + tool + tooth + top + topic + topple + torch + tornado + tortoise + toss + total + tourist + toward + tower + town + toy + track + trade + traffic + tragic + train + transfer + trap + trash + travel + tray + treat + tree + trend + trial + tribe + trick + trigger + trim + trip + trophy + trouble + truck + true + truly + trumpet + trust + truth + try + tube + tuition + tumble + tuna + tunnel + turkey + turn + turtle + twelve + twenty + twice + twin + twist + two + type + typical + ugly + umbrella + unable + unaware + uncle + uncover + under + undo + unfair + unfold + unhappy + uniform + unique + unit + universe + unknown + unlock + until + unusual + unveil + update + upgrade + uphold + upon + upper + upset + urban + urge + usage + use + used + useful + useless + usual + utility + vacant + vacuum + vague + valid + valley + valve + van + vanish + vapor + various + vast + vault + vehicle + velvet + vendor + venture + venue + verb + verify + version + very + vessel + veteran + viable + vibrant + vicious + victory + video + view + village + vintage + violin + virtual + virus + visa + visit + visual + vital + vivid + vocal + voice + void + volcano + volume + vote + voyage + wage + wagon + wait + walk + wall + walnut + want + warfare + warm + warrior + wash + wasp + waste + water + wave + way + wealth + weapon + wear + weasel + weather + web + wedding + weekend + weird + welcome + west + wet + whale + what + wheat + wheel + when + where + whip + whisper + wide + width + wife + wild + will + win + window + wine + wing + wink + winner + winter + wire + wisdom + wise + wish + witness + wolf + woman + wonder + wood + wool + word + work + world + worry + worth + wrap + wreck + wrestle + wrist + write + wrong + yard + year + yellow + you + young + youth + zebra + zero + zone + zoo + """ + return words.split(separator: "\n") + }() +} From 77b1bffdfadaf9867e1a4b93cab40718cd562f0f Mon Sep 17 00:00:00 2001 From: Anton B Date: Wed, 1 Aug 2018 13:22:27 +0300 Subject: [PATCH 047/175] Remove JS core & cleanup --- Adamant.xcodeproj/project.pbxproj | 16 +- Adamant/AppDelegate.swift | 1 - Adamant/Assets/adamant-core.js | 66587 ---------------- ...+Native.swift => AdamantCoreService.swift} | 41 +- Adamant/Services/JSAdamantCore.swift | 333 - Adamant/SwinjectDependencies.swift | 5 +- 6 files changed, 11 insertions(+), 66972 deletions(-) delete mode 100644 Adamant/Assets/adamant-core.js rename Adamant/Services/{JSAdamantCore+Native.swift => AdamantCoreService.swift} (94%) delete mode 100644 Adamant/Services/JSAdamantCore.swift diff --git a/Adamant.xcodeproj/project.pbxproj b/Adamant.xcodeproj/project.pbxproj index eb25b70ea..b9187d6f3 100644 --- a/Adamant.xcodeproj/project.pbxproj +++ b/Adamant.xcodeproj/project.pbxproj @@ -8,7 +8,7 @@ /* Begin PBXBuildFile section */ 4411402421C8B290703B13EB /* Pods_Adamant.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C107D5CB65B4D728B9D97C0F /* Pods_Adamant.framework */; }; - 643ED0B12109F4BD005A9FDA /* JSAdamantCore+Native.swift in Sources */ = {isa = PBXBuildFile; fileRef = 643ED0B02109F4BD005A9FDA /* JSAdamantCore+Native.swift */; }; + 643ED0B12109F4BD005A9FDA /* AdamantCoreService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 643ED0B02109F4BD005A9FDA /* AdamantCoreService.swift */; }; 644EC34D20EFA60900F40C73 /* AdamantApi+Delegates.swift in Sources */ = {isa = PBXBuildFile; fileRef = 644EC34C20EFA60900F40C73 /* AdamantApi+Delegates.swift */; }; 644EC34F20EFA77A00F40C73 /* Delegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 644EC34E20EFA77A00F40C73 /* Delegate.swift */; }; 644EC35220EFA9A300F40C73 /* DelegateRoutes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 644EC35120EFA9A300F40C73 /* DelegateRoutes.swift */; }; @@ -39,7 +39,6 @@ E913C8F91FFFA51D001A83F7 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E913C8F81FFFA51D001A83F7 /* Assets.xcassets */; }; E913C9081FFFA943001A83F7 /* AdamantCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = E913C9071FFFA943001A83F7 /* AdamantCore.swift */; }; E913C90D1FFFA99B001A83F7 /* Keypair.swift in Sources */ = {isa = PBXBuildFile; fileRef = E913C90C1FFFA99B001A83F7 /* Keypair.swift */; }; - E913C9171FFFAF76001A83F7 /* JSAdamantCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = E913C9161FFFAF76001A83F7 /* JSAdamantCore.swift */; }; E9147B5F20500E9300145913 /* MyLittlePinpad+adamant.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9147B5E20500E9300145913 /* MyLittlePinpad+adamant.swift */; }; E9147B612050599000145913 /* LoginViewController+QR.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9147B602050599000145913 /* LoginViewController+QR.swift */; }; E9147B6320505C7500145913 /* QRCodeReader+adamant.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9147B6220505C7500145913 /* QRCodeReader+adamant.swift */; }; @@ -157,7 +156,6 @@ E9B3D3A1201FA26B0019EB36 /* AdamantAccountsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9B3D3A0201FA26B0019EB36 /* AdamantAccountsProvider.swift */; }; E9B3D3A9202082450019EB36 /* AdamantTransfersProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9B3D3A8202082450019EB36 /* AdamantTransfersProvider.swift */; }; E9C51ECF200E2D1100385EB7 /* FeeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9C51ECE200E2D1100385EB7 /* FeeTests.swift */; }; - E9C51EED2011416E00385EB7 /* adamant-core.js in Resources */ = {isa = PBXBuildFile; fileRef = E9C51EEC2011416E00385EB7 /* adamant-core.js */; }; E9C51EEF20139DC600385EB7 /* TransactionIdResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9C51EEE20139DC600385EB7 /* TransactionIdResponse.swift */; }; E9C51EF12013F18000385EB7 /* NewChatViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9C51EF02013F18000385EB7 /* NewChatViewController.swift */; }; E9CAE8D22018AA7700345E76 /* AdamantApi+Accounts.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9CAE8D12018AA7700345E76 /* AdamantApi+Accounts.swift */; }; @@ -212,7 +210,7 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ - 643ED0B02109F4BD005A9FDA /* JSAdamantCore+Native.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JSAdamantCore+Native.swift"; sourceTree = ""; }; + 643ED0B02109F4BD005A9FDA /* AdamantCoreService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantCoreService.swift; sourceTree = ""; }; 644EC34C20EFA60900F40C73 /* AdamantApi+Delegates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AdamantApi+Delegates.swift"; sourceTree = ""; }; 644EC34E20EFA77A00F40C73 /* Delegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Delegate.swift; sourceTree = ""; }; 644EC35120EFA9A300F40C73 /* DelegateRoutes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DelegateRoutes.swift; sourceTree = ""; }; @@ -254,7 +252,6 @@ E913C8FD1FFFA51E001A83F7 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; E913C9071FFFA943001A83F7 /* AdamantCore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantCore.swift; sourceTree = ""; }; E913C90C1FFFA99B001A83F7 /* Keypair.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Keypair.swift; sourceTree = ""; }; - E913C9161FFFAF76001A83F7 /* JSAdamantCore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSAdamantCore.swift; sourceTree = ""; }; E9147B5E20500E9300145913 /* MyLittlePinpad+adamant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MyLittlePinpad+adamant.swift"; sourceTree = ""; }; E9147B602050599000145913 /* LoginViewController+QR.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LoginViewController+QR.swift"; sourceTree = ""; }; E9147B6220505C7500145913 /* QRCodeReader+adamant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "QRCodeReader+adamant.swift"; sourceTree = ""; }; @@ -386,7 +383,6 @@ E9B3D3A0201FA26B0019EB36 /* AdamantAccountsProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantAccountsProvider.swift; sourceTree = ""; }; E9B3D3A8202082450019EB36 /* AdamantTransfersProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantTransfersProvider.swift; sourceTree = ""; }; E9C51ECE200E2D1100385EB7 /* FeeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeeTests.swift; sourceTree = ""; }; - E9C51EEC2011416E00385EB7 /* adamant-core.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = "adamant-core.js"; sourceTree = ""; }; E9C51EEE20139DC600385EB7 /* TransactionIdResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionIdResponse.swift; sourceTree = ""; }; E9C51EF02013F18000385EB7 /* NewChatViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewChatViewController.swift; sourceTree = ""; }; E9CAE8D12018AA7700345E76 /* AdamantApi+Accounts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AdamantApi+Accounts.swift"; sourceTree = ""; }; @@ -547,8 +543,7 @@ E9A03FD720DC0ABA007653A1 /* AdamantNodesSource.swift */, E93D7ABF2052CF63005D19DC /* AdamantNotificationService.swift */, E921597420611A6A0000CA5C /* AdamantReachability.swift */, - E913C9161FFFAF76001A83F7 /* JSAdamantCore.swift */, - 643ED0B02109F4BD005A9FDA /* JSAdamantCore+Native.swift */, + 643ED0B02109F4BD005A9FDA /* AdamantCoreService.swift */, E905D39A2048A9BD00DDB504 /* KeychainStore.swift */, E950273F202E257E002C1098 /* RepeaterService.swift */, E9E7CDB42002BA6900DFC4DB /* SwinjectedRouter.swift */, @@ -610,7 +605,6 @@ E913C8F81FFFA51D001A83F7 /* Assets.xcassets */, E9A174B820587B83003667CD /* notification.mp3 */, E9256F752039A9A200DE86E9 /* LaunchScreen.storyboard */, - E9C51EEC2011416E00385EB7 /* adamant-core.js */, ); path = Assets; sourceTree = ""; @@ -985,7 +979,6 @@ 644EC35C20EFB8E900F40C73 /* AdamantDelegateCell.xib in Resources */, E9942B89203D9ECA00C163AF /* QrCell.xib in Resources */, E921597D2065031D0000CA5C /* ButtonsStripe.xib in Resources */, - E9C51EED2011416E00385EB7 /* adamant-core.js in Resources */, E90A4945204C6204009F6A65 /* PassphraseCell.xib in Resources */, E941CCDF20E7B70200C96220 /* WalletCollectionViewCell.xib in Resources */, E9E7CDC82003F6D200DFC4DB /* TransactionTableViewCell.xib in Resources */, @@ -1201,7 +1194,7 @@ E93EFE13200D1156000BB482 /* ChatViewController.swift in Sources */, E927171E20C04614002BB9A6 /* UIColor+hex.swift in Sources */, E9B3D39E201F99F40019EB36 /* DataProvider.swift in Sources */, - 643ED0B12109F4BD005A9FDA /* JSAdamantCore+Native.swift in Sources */, + 643ED0B12109F4BD005A9FDA /* AdamantCoreService.swift in Sources */, E93EB0A320DA4CCA001F9601 /* Node.swift in Sources */, E9E7CDC02003AF6D00DFC4DB /* AdamantCellFactory.swift in Sources */, E93B0D742028B21400126346 /* ChatsProvider.swift in Sources */, @@ -1250,7 +1243,6 @@ E98FC34420F920BD00032D65 /* UIFont+adamant.swift in Sources */, 644EC35720EFAAB700F40C73 /* DelegatesListViewController.swift in Sources */, E9C51EF12013F18000385EB7 /* NewChatViewController.swift in Sources */, - E913C9171FFFAF76001A83F7 /* JSAdamantCore.swift in Sources */, E9502740202E257E002C1098 /* RepeaterService.swift in Sources */, E93D7AC02052CF63005D19DC /* AdamantNotificationService.swift in Sources */, E93B0D762028B28E00126346 /* AdamantChatsProvider.swift in Sources */, diff --git a/Adamant/AppDelegate.swift b/Adamant/AppDelegate.swift index d4fe36f64..a99ec72f6 100644 --- a/Adamant/AppDelegate.swift +++ b/Adamant/AppDelegate.swift @@ -36,7 +36,6 @@ extension StoreKey { // MARK: - Resources struct AdamantResources { - static let jsCore = Bundle.main.url(forResource: "adamant-core", withExtension: "js")! static let coreDataModel = Bundle.main.url(forResource: "ChatModels", withExtension: "momd")! static let nodes: [Node] = [ diff --git a/Adamant/Assets/adamant-core.js b/Adamant/Assets/adamant-core.js deleted file mode 100644 index 9307a0077..000000000 --- a/Adamant/Assets/adamant-core.js +++ /dev/null @@ -1,66587 +0,0 @@ -var adamant_core = -/******/ (function(modules) { // webpackBootstrap -/******/ // The module cache -/******/ var installedModules = {}; -/******/ -/******/ // The require function -/******/ function __webpack_require__(moduleId) { -/******/ -/******/ // Check if module is in cache -/******/ if(installedModules[moduleId]) { -/******/ return installedModules[moduleId].exports; -/******/ } -/******/ // Create a new module (and put it into the cache) -/******/ var module = installedModules[moduleId] = { -/******/ i: moduleId, -/******/ l: false, -/******/ exports: {} -/******/ }; -/******/ -/******/ // Execute the module function -/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); -/******/ -/******/ // Flag the module as loaded -/******/ module.l = true; -/******/ -/******/ // Return the exports of the module -/******/ return module.exports; -/******/ } -/******/ -/******/ -/******/ // expose the modules object (__webpack_modules__) -/******/ __webpack_require__.m = modules; -/******/ -/******/ // expose the module cache -/******/ __webpack_require__.c = installedModules; -/******/ -/******/ // define getter function for harmony exports -/******/ __webpack_require__.d = function(exports, name, getter) { -/******/ if(!__webpack_require__.o(exports, name)) { -/******/ Object.defineProperty(exports, name, { -/******/ configurable: false, -/******/ enumerable: true, -/******/ get: getter -/******/ }); -/******/ } -/******/ }; -/******/ -/******/ // getDefaultExport function for compatibility with non-harmony modules -/******/ __webpack_require__.n = function(module) { -/******/ var getter = module && module.__esModule ? -/******/ function getDefault() { return module['default']; } : -/******/ function getModuleExports() { return module; }; -/******/ __webpack_require__.d(getter, 'a', getter); -/******/ return getter; -/******/ }; -/******/ -/******/ // Object.prototype.hasOwnProperty.call -/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; -/******/ -/******/ // __webpack_public_path__ -/******/ __webpack_require__.p = ""; -/******/ -/******/ // Load entry module and return exports -/******/ return __webpack_require__(__webpack_require__.s = 113); -/******/ }) -/************************************************************************/ -/******/ ([ -/* 0 */ -/***/ (function(module, exports, __webpack_require__) { - -"use strict"; -/* WEBPACK VAR INJECTION */(function(global) {/*! - * The buffer module from node.js, for the browser. - * - * @author Feross Aboukhadijeh - * @license MIT - */ -/* eslint-disable no-proto */ - - - -var base64 = __webpack_require__(114) -var ieee754 = __webpack_require__(115) -var isArray = __webpack_require__(72) - -exports.Buffer = Buffer -exports.SlowBuffer = SlowBuffer -exports.INSPECT_MAX_BYTES = 50 - -/** - * If `Buffer.TYPED_ARRAY_SUPPORT`: - * === true Use Uint8Array implementation (fastest) - * === false Use Object implementation (most compatible, even IE6) - * - * Browsers that support typed arrays are IE 10+, Firefox 4+, Chrome 7+, Safari 5.1+, - * Opera 11.6+, iOS 4.2+. - * - * Due to various browser bugs, sometimes the Object implementation will be used even - * when the browser supports typed arrays. - * - * Note: - * - * - Firefox 4-29 lacks support for adding new properties to `Uint8Array` instances, - * See: https://bugzilla.mozilla.org/show_bug.cgi?id=695438. - * - * - Chrome 9-10 is missing the `TypedArray.prototype.subarray` function. - * - * - IE10 has a broken `TypedArray.prototype.subarray` function which returns arrays of - * incorrect length in some situations. - - * We detect these buggy browsers and set `Buffer.TYPED_ARRAY_SUPPORT` to `false` so they - * get the Object implementation, which is slower but behaves correctly. - */ -Buffer.TYPED_ARRAY_SUPPORT = global.TYPED_ARRAY_SUPPORT !== undefined - ? global.TYPED_ARRAY_SUPPORT - : typedArraySupport() - -/* - * Export kMaxLength after typed array support is determined. - */ -exports.kMaxLength = kMaxLength() - -function typedArraySupport () { - try { - var arr = new Uint8Array(1) - arr.__proto__ = {__proto__: Uint8Array.prototype, foo: function () { return 42 }} - return arr.foo() === 42 && // typed array instances can be augmented - typeof arr.subarray === 'function' && // chrome 9-10 lack `subarray` - arr.subarray(1, 1).byteLength === 0 // ie10 has broken `subarray` - } catch (e) { - return false - } -} - -function kMaxLength () { - return Buffer.TYPED_ARRAY_SUPPORT - ? 0x7fffffff - : 0x3fffffff -} - -function createBuffer (that, length) { - if (kMaxLength() < length) { - throw new RangeError('Invalid typed array length') - } - if (Buffer.TYPED_ARRAY_SUPPORT) { - // Return an augmented `Uint8Array` instance, for best performance - that = new Uint8Array(length) - that.__proto__ = Buffer.prototype - } else { - // Fallback: Return an object instance of the Buffer class - if (that === null) { - that = new Buffer(length) - } - that.length = length - } - - return that -} - -/** - * The Buffer constructor returns instances of `Uint8Array` that have their - * prototype changed to `Buffer.prototype`. Furthermore, `Buffer` is a subclass of - * `Uint8Array`, so the returned instances will have all the node `Buffer` methods - * and the `Uint8Array` methods. Square bracket notation works as expected -- it - * returns a single octet. - * - * The `Uint8Array` prototype remains unmodified. - */ - -function Buffer (arg, encodingOrOffset, length) { - if (!Buffer.TYPED_ARRAY_SUPPORT && !(this instanceof Buffer)) { - return new Buffer(arg, encodingOrOffset, length) - } - - // Common case. - if (typeof arg === 'number') { - if (typeof encodingOrOffset === 'string') { - throw new Error( - 'If encoding is specified then the first argument must be a string' - ) - } - return allocUnsafe(this, arg) - } - return from(this, arg, encodingOrOffset, length) -} - -Buffer.poolSize = 8192 // not used by this implementation - -// TODO: Legacy, not needed anymore. Remove in next major version. -Buffer._augment = function (arr) { - arr.__proto__ = Buffer.prototype - return arr -} - -function from (that, value, encodingOrOffset, length) { - if (typeof value === 'number') { - throw new TypeError('"value" argument must not be a number') - } - - if (typeof ArrayBuffer !== 'undefined' && value instanceof ArrayBuffer) { - return fromArrayBuffer(that, value, encodingOrOffset, length) - } - - if (typeof value === 'string') { - return fromString(that, value, encodingOrOffset) - } - - return fromObject(that, value) -} - -/** - * Functionally equivalent to Buffer(arg, encoding) but throws a TypeError - * if value is a number. - * Buffer.from(str[, encoding]) - * Buffer.from(array) - * Buffer.from(buffer) - * Buffer.from(arrayBuffer[, byteOffset[, length]]) - **/ -Buffer.from = function (value, encodingOrOffset, length) { - return from(null, value, encodingOrOffset, length) -} - -if (Buffer.TYPED_ARRAY_SUPPORT) { - Buffer.prototype.__proto__ = Uint8Array.prototype - Buffer.__proto__ = Uint8Array - if (typeof Symbol !== 'undefined' && Symbol.species && - Buffer[Symbol.species] === Buffer) { - // Fix subarray() in ES2016. See: https://github.com/feross/buffer/pull/97 - Object.defineProperty(Buffer, Symbol.species, { - value: null, - configurable: true - }) - } -} - -function assertSize (size) { - if (typeof size !== 'number') { - throw new TypeError('"size" argument must be a number') - } else if (size < 0) { - throw new RangeError('"size" argument must not be negative') - } -} - -function alloc (that, size, fill, encoding) { - assertSize(size) - if (size <= 0) { - return createBuffer(that, size) - } - if (fill !== undefined) { - // Only pay attention to encoding if it's a string. This - // prevents accidentally sending in a number that would - // be interpretted as a start offset. - return typeof encoding === 'string' - ? createBuffer(that, size).fill(fill, encoding) - : createBuffer(that, size).fill(fill) - } - return createBuffer(that, size) -} - -/** - * Creates a new filled Buffer instance. - * alloc(size[, fill[, encoding]]) - **/ -Buffer.alloc = function (size, fill, encoding) { - return alloc(null, size, fill, encoding) -} - -function allocUnsafe (that, size) { - assertSize(size) - that = createBuffer(that, size < 0 ? 0 : checked(size) | 0) - if (!Buffer.TYPED_ARRAY_SUPPORT) { - for (var i = 0; i < size; ++i) { - that[i] = 0 - } - } - return that -} - -/** - * Equivalent to Buffer(num), by default creates a non-zero-filled Buffer instance. - * */ -Buffer.allocUnsafe = function (size) { - return allocUnsafe(null, size) -} -/** - * Equivalent to SlowBuffer(num), by default creates a non-zero-filled Buffer instance. - */ -Buffer.allocUnsafeSlow = function (size) { - return allocUnsafe(null, size) -} - -function fromString (that, string, encoding) { - if (typeof encoding !== 'string' || encoding === '') { - encoding = 'utf8' - } - - if (!Buffer.isEncoding(encoding)) { - throw new TypeError('"encoding" must be a valid string encoding') - } - - var length = byteLength(string, encoding) | 0 - that = createBuffer(that, length) - - var actual = that.write(string, encoding) - - if (actual !== length) { - // Writing a hex string, for example, that contains invalid characters will - // cause everything after the first invalid character to be ignored. (e.g. - // 'abxxcd' will be treated as 'ab') - that = that.slice(0, actual) - } - - return that -} - -function fromArrayLike (that, array) { - var length = array.length < 0 ? 0 : checked(array.length) | 0 - that = createBuffer(that, length) - for (var i = 0; i < length; i += 1) { - that[i] = array[i] & 255 - } - return that -} - -function fromArrayBuffer (that, array, byteOffset, length) { - array.byteLength // this throws if `array` is not a valid ArrayBuffer - - if (byteOffset < 0 || array.byteLength < byteOffset) { - throw new RangeError('\'offset\' is out of bounds') - } - - if (array.byteLength < byteOffset + (length || 0)) { - throw new RangeError('\'length\' is out of bounds') - } - - if (byteOffset === undefined && length === undefined) { - array = new Uint8Array(array) - } else if (length === undefined) { - array = new Uint8Array(array, byteOffset) - } else { - array = new Uint8Array(array, byteOffset, length) - } - - if (Buffer.TYPED_ARRAY_SUPPORT) { - // Return an augmented `Uint8Array` instance, for best performance - that = array - that.__proto__ = Buffer.prototype - } else { - // Fallback: Return an object instance of the Buffer class - that = fromArrayLike(that, array) - } - return that -} - -function fromObject (that, obj) { - if (Buffer.isBuffer(obj)) { - var len = checked(obj.length) | 0 - that = createBuffer(that, len) - - if (that.length === 0) { - return that - } - - obj.copy(that, 0, 0, len) - return that - } - - if (obj) { - if ((typeof ArrayBuffer !== 'undefined' && - obj.buffer instanceof ArrayBuffer) || 'length' in obj) { - if (typeof obj.length !== 'number' || isnan(obj.length)) { - return createBuffer(that, 0) - } - return fromArrayLike(that, obj) - } - - if (obj.type === 'Buffer' && isArray(obj.data)) { - return fromArrayLike(that, obj.data) - } - } - - throw new TypeError('First argument must be a string, Buffer, ArrayBuffer, Array, or array-like object.') -} - -function checked (length) { - // Note: cannot use `length < kMaxLength()` here because that fails when - // length is NaN (which is otherwise coerced to zero.) - if (length >= kMaxLength()) { - throw new RangeError('Attempt to allocate Buffer larger than maximum ' + - 'size: 0x' + kMaxLength().toString(16) + ' bytes') - } - return length | 0 -} - -function SlowBuffer (length) { - if (+length != length) { // eslint-disable-line eqeqeq - length = 0 - } - return Buffer.alloc(+length) -} - -Buffer.isBuffer = function isBuffer (b) { - return !!(b != null && b._isBuffer) -} - -Buffer.compare = function compare (a, b) { - if (!Buffer.isBuffer(a) || !Buffer.isBuffer(b)) { - throw new TypeError('Arguments must be Buffers') - } - - if (a === b) return 0 - - var x = a.length - var y = b.length - - for (var i = 0, len = Math.min(x, y); i < len; ++i) { - if (a[i] !== b[i]) { - x = a[i] - y = b[i] - break - } - } - - if (x < y) return -1 - if (y < x) return 1 - return 0 -} - -Buffer.isEncoding = function isEncoding (encoding) { - switch (String(encoding).toLowerCase()) { - case 'hex': - case 'utf8': - case 'utf-8': - case 'ascii': - case 'latin1': - case 'binary': - case 'base64': - case 'ucs2': - case 'ucs-2': - case 'utf16le': - case 'utf-16le': - return true - default: - return false - } -} - -Buffer.concat = function concat (list, length) { - if (!isArray(list)) { - throw new TypeError('"list" argument must be an Array of Buffers') - } - - if (list.length === 0) { - return Buffer.alloc(0) - } - - var i - if (length === undefined) { - length = 0 - for (i = 0; i < list.length; ++i) { - length += list[i].length - } - } - - var buffer = Buffer.allocUnsafe(length) - var pos = 0 - for (i = 0; i < list.length; ++i) { - var buf = list[i] - if (!Buffer.isBuffer(buf)) { - throw new TypeError('"list" argument must be an Array of Buffers') - } - buf.copy(buffer, pos) - pos += buf.length - } - return buffer -} - -function byteLength (string, encoding) { - if (Buffer.isBuffer(string)) { - return string.length - } - if (typeof ArrayBuffer !== 'undefined' && typeof ArrayBuffer.isView === 'function' && - (ArrayBuffer.isView(string) || string instanceof ArrayBuffer)) { - return string.byteLength - } - if (typeof string !== 'string') { - string = '' + string - } - - var len = string.length - if (len === 0) return 0 - - // Use a for loop to avoid recursion - var loweredCase = false - for (;;) { - switch (encoding) { - case 'ascii': - case 'latin1': - case 'binary': - return len - case 'utf8': - case 'utf-8': - case undefined: - return utf8ToBytes(string).length - case 'ucs2': - case 'ucs-2': - case 'utf16le': - case 'utf-16le': - return len * 2 - case 'hex': - return len >>> 1 - case 'base64': - return base64ToBytes(string).length - default: - if (loweredCase) return utf8ToBytes(string).length // assume utf8 - encoding = ('' + encoding).toLowerCase() - loweredCase = true - } - } -} -Buffer.byteLength = byteLength - -function slowToString (encoding, start, end) { - var loweredCase = false - - // No need to verify that "this.length <= MAX_UINT32" since it's a read-only - // property of a typed array. - - // This behaves neither like String nor Uint8Array in that we set start/end - // to their upper/lower bounds if the value passed is out of range. - // undefined is handled specially as per ECMA-262 6th Edition, - // Section 13.3.3.7 Runtime Semantics: KeyedBindingInitialization. - if (start === undefined || start < 0) { - start = 0 - } - // Return early if start > this.length. Done here to prevent potential uint32 - // coercion fail below. - if (start > this.length) { - return '' - } - - if (end === undefined || end > this.length) { - end = this.length - } - - if (end <= 0) { - return '' - } - - // Force coersion to uint32. This will also coerce falsey/NaN values to 0. - end >>>= 0 - start >>>= 0 - - if (end <= start) { - return '' - } - - if (!encoding) encoding = 'utf8' - - while (true) { - switch (encoding) { - case 'hex': - return hexSlice(this, start, end) - - case 'utf8': - case 'utf-8': - return utf8Slice(this, start, end) - - case 'ascii': - return asciiSlice(this, start, end) - - case 'latin1': - case 'binary': - return latin1Slice(this, start, end) - - case 'base64': - return base64Slice(this, start, end) - - case 'ucs2': - case 'ucs-2': - case 'utf16le': - case 'utf-16le': - return utf16leSlice(this, start, end) - - default: - if (loweredCase) throw new TypeError('Unknown encoding: ' + encoding) - encoding = (encoding + '').toLowerCase() - loweredCase = true - } - } -} - -// The property is used by `Buffer.isBuffer` and `is-buffer` (in Safari 5-7) to detect -// Buffer instances. -Buffer.prototype._isBuffer = true - -function swap (b, n, m) { - var i = b[n] - b[n] = b[m] - b[m] = i -} - -Buffer.prototype.swap16 = function swap16 () { - var len = this.length - if (len % 2 !== 0) { - throw new RangeError('Buffer size must be a multiple of 16-bits') - } - for (var i = 0; i < len; i += 2) { - swap(this, i, i + 1) - } - return this -} - -Buffer.prototype.swap32 = function swap32 () { - var len = this.length - if (len % 4 !== 0) { - throw new RangeError('Buffer size must be a multiple of 32-bits') - } - for (var i = 0; i < len; i += 4) { - swap(this, i, i + 3) - swap(this, i + 1, i + 2) - } - return this -} - -Buffer.prototype.swap64 = function swap64 () { - var len = this.length - if (len % 8 !== 0) { - throw new RangeError('Buffer size must be a multiple of 64-bits') - } - for (var i = 0; i < len; i += 8) { - swap(this, i, i + 7) - swap(this, i + 1, i + 6) - swap(this, i + 2, i + 5) - swap(this, i + 3, i + 4) - } - return this -} - -Buffer.prototype.toString = function toString () { - var length = this.length | 0 - if (length === 0) return '' - if (arguments.length === 0) return utf8Slice(this, 0, length) - return slowToString.apply(this, arguments) -} - -Buffer.prototype.equals = function equals (b) { - if (!Buffer.isBuffer(b)) throw new TypeError('Argument must be a Buffer') - if (this === b) return true - return Buffer.compare(this, b) === 0 -} - -Buffer.prototype.inspect = function inspect () { - var str = '' - var max = exports.INSPECT_MAX_BYTES - if (this.length > 0) { - str = this.toString('hex', 0, max).match(/.{2}/g).join(' ') - if (this.length > max) str += ' ... ' - } - return '' -} - -Buffer.prototype.compare = function compare (target, start, end, thisStart, thisEnd) { - if (!Buffer.isBuffer(target)) { - throw new TypeError('Argument must be a Buffer') - } - - if (start === undefined) { - start = 0 - } - if (end === undefined) { - end = target ? target.length : 0 - } - if (thisStart === undefined) { - thisStart = 0 - } - if (thisEnd === undefined) { - thisEnd = this.length - } - - if (start < 0 || end > target.length || thisStart < 0 || thisEnd > this.length) { - throw new RangeError('out of range index') - } - - if (thisStart >= thisEnd && start >= end) { - return 0 - } - if (thisStart >= thisEnd) { - return -1 - } - if (start >= end) { - return 1 - } - - start >>>= 0 - end >>>= 0 - thisStart >>>= 0 - thisEnd >>>= 0 - - if (this === target) return 0 - - var x = thisEnd - thisStart - var y = end - start - var len = Math.min(x, y) - - var thisCopy = this.slice(thisStart, thisEnd) - var targetCopy = target.slice(start, end) - - for (var i = 0; i < len; ++i) { - if (thisCopy[i] !== targetCopy[i]) { - x = thisCopy[i] - y = targetCopy[i] - break - } - } - - if (x < y) return -1 - if (y < x) return 1 - return 0 -} - -// Finds either the first index of `val` in `buffer` at offset >= `byteOffset`, -// OR the last index of `val` in `buffer` at offset <= `byteOffset`. -// -// Arguments: -// - buffer - a Buffer to search -// - val - a string, Buffer, or number -// - byteOffset - an index into `buffer`; will be clamped to an int32 -// - encoding - an optional encoding, relevant is val is a string -// - dir - true for indexOf, false for lastIndexOf -function bidirectionalIndexOf (buffer, val, byteOffset, encoding, dir) { - // Empty buffer means no match - if (buffer.length === 0) return -1 - - // Normalize byteOffset - if (typeof byteOffset === 'string') { - encoding = byteOffset - byteOffset = 0 - } else if (byteOffset > 0x7fffffff) { - byteOffset = 0x7fffffff - } else if (byteOffset < -0x80000000) { - byteOffset = -0x80000000 - } - byteOffset = +byteOffset // Coerce to Number. - if (isNaN(byteOffset)) { - // byteOffset: it it's undefined, null, NaN, "foo", etc, search whole buffer - byteOffset = dir ? 0 : (buffer.length - 1) - } - - // Normalize byteOffset: negative offsets start from the end of the buffer - if (byteOffset < 0) byteOffset = buffer.length + byteOffset - if (byteOffset >= buffer.length) { - if (dir) return -1 - else byteOffset = buffer.length - 1 - } else if (byteOffset < 0) { - if (dir) byteOffset = 0 - else return -1 - } - - // Normalize val - if (typeof val === 'string') { - val = Buffer.from(val, encoding) - } - - // Finally, search either indexOf (if dir is true) or lastIndexOf - if (Buffer.isBuffer(val)) { - // Special case: looking for empty string/buffer always fails - if (val.length === 0) { - return -1 - } - return arrayIndexOf(buffer, val, byteOffset, encoding, dir) - } else if (typeof val === 'number') { - val = val & 0xFF // Search for a byte value [0-255] - if (Buffer.TYPED_ARRAY_SUPPORT && - typeof Uint8Array.prototype.indexOf === 'function') { - if (dir) { - return Uint8Array.prototype.indexOf.call(buffer, val, byteOffset) - } else { - return Uint8Array.prototype.lastIndexOf.call(buffer, val, byteOffset) - } - } - return arrayIndexOf(buffer, [ val ], byteOffset, encoding, dir) - } - - throw new TypeError('val must be string, number or Buffer') -} - -function arrayIndexOf (arr, val, byteOffset, encoding, dir) { - var indexSize = 1 - var arrLength = arr.length - var valLength = val.length - - if (encoding !== undefined) { - encoding = String(encoding).toLowerCase() - if (encoding === 'ucs2' || encoding === 'ucs-2' || - encoding === 'utf16le' || encoding === 'utf-16le') { - if (arr.length < 2 || val.length < 2) { - return -1 - } - indexSize = 2 - arrLength /= 2 - valLength /= 2 - byteOffset /= 2 - } - } - - function read (buf, i) { - if (indexSize === 1) { - return buf[i] - } else { - return buf.readUInt16BE(i * indexSize) - } - } - - var i - if (dir) { - var foundIndex = -1 - for (i = byteOffset; i < arrLength; i++) { - if (read(arr, i) === read(val, foundIndex === -1 ? 0 : i - foundIndex)) { - if (foundIndex === -1) foundIndex = i - if (i - foundIndex + 1 === valLength) return foundIndex * indexSize - } else { - if (foundIndex !== -1) i -= i - foundIndex - foundIndex = -1 - } - } - } else { - if (byteOffset + valLength > arrLength) byteOffset = arrLength - valLength - for (i = byteOffset; i >= 0; i--) { - var found = true - for (var j = 0; j < valLength; j++) { - if (read(arr, i + j) !== read(val, j)) { - found = false - break - } - } - if (found) return i - } - } - - return -1 -} - -Buffer.prototype.includes = function includes (val, byteOffset, encoding) { - return this.indexOf(val, byteOffset, encoding) !== -1 -} - -Buffer.prototype.indexOf = function indexOf (val, byteOffset, encoding) { - return bidirectionalIndexOf(this, val, byteOffset, encoding, true) -} - -Buffer.prototype.lastIndexOf = function lastIndexOf (val, byteOffset, encoding) { - return bidirectionalIndexOf(this, val, byteOffset, encoding, false) -} - -function hexWrite (buf, string, offset, length) { - offset = Number(offset) || 0 - var remaining = buf.length - offset - if (!length) { - length = remaining - } else { - length = Number(length) - if (length > remaining) { - length = remaining - } - } - - // must be an even number of digits - var strLen = string.length - if (strLen % 2 !== 0) throw new TypeError('Invalid hex string') - - if (length > strLen / 2) { - length = strLen / 2 - } - for (var i = 0; i < length; ++i) { - var parsed = parseInt(string.substr(i * 2, 2), 16) - if (isNaN(parsed)) return i - buf[offset + i] = parsed - } - return i -} - -function utf8Write (buf, string, offset, length) { - return blitBuffer(utf8ToBytes(string, buf.length - offset), buf, offset, length) -} - -function asciiWrite (buf, string, offset, length) { - return blitBuffer(asciiToBytes(string), buf, offset, length) -} - -function latin1Write (buf, string, offset, length) { - return asciiWrite(buf, string, offset, length) -} - -function base64Write (buf, string, offset, length) { - return blitBuffer(base64ToBytes(string), buf, offset, length) -} - -function ucs2Write (buf, string, offset, length) { - return blitBuffer(utf16leToBytes(string, buf.length - offset), buf, offset, length) -} - -Buffer.prototype.write = function write (string, offset, length, encoding) { - // Buffer#write(string) - if (offset === undefined) { - encoding = 'utf8' - length = this.length - offset = 0 - // Buffer#write(string, encoding) - } else if (length === undefined && typeof offset === 'string') { - encoding = offset - length = this.length - offset = 0 - // Buffer#write(string, offset[, length][, encoding]) - } else if (isFinite(offset)) { - offset = offset | 0 - if (isFinite(length)) { - length = length | 0 - if (encoding === undefined) encoding = 'utf8' - } else { - encoding = length - length = undefined - } - // legacy write(string, encoding, offset, length) - remove in v0.13 - } else { - throw new Error( - 'Buffer.write(string, encoding, offset[, length]) is no longer supported' - ) - } - - var remaining = this.length - offset - if (length === undefined || length > remaining) length = remaining - - if ((string.length > 0 && (length < 0 || offset < 0)) || offset > this.length) { - throw new RangeError('Attempt to write outside buffer bounds') - } - - if (!encoding) encoding = 'utf8' - - var loweredCase = false - for (;;) { - switch (encoding) { - case 'hex': - return hexWrite(this, string, offset, length) - - case 'utf8': - case 'utf-8': - return utf8Write(this, string, offset, length) - - case 'ascii': - return asciiWrite(this, string, offset, length) - - case 'latin1': - case 'binary': - return latin1Write(this, string, offset, length) - - case 'base64': - // Warning: maxLength not taken into account in base64Write - return base64Write(this, string, offset, length) - - case 'ucs2': - case 'ucs-2': - case 'utf16le': - case 'utf-16le': - return ucs2Write(this, string, offset, length) - - default: - if (loweredCase) throw new TypeError('Unknown encoding: ' + encoding) - encoding = ('' + encoding).toLowerCase() - loweredCase = true - } - } -} - -Buffer.prototype.toJSON = function toJSON () { - return { - type: 'Buffer', - data: Array.prototype.slice.call(this._arr || this, 0) - } -} - -function base64Slice (buf, start, end) { - if (start === 0 && end === buf.length) { - return base64.fromByteArray(buf) - } else { - return base64.fromByteArray(buf.slice(start, end)) - } -} - -function utf8Slice (buf, start, end) { - end = Math.min(buf.length, end) - var res = [] - - var i = start - while (i < end) { - var firstByte = buf[i] - var codePoint = null - var bytesPerSequence = (firstByte > 0xEF) ? 4 - : (firstByte > 0xDF) ? 3 - : (firstByte > 0xBF) ? 2 - : 1 - - if (i + bytesPerSequence <= end) { - var secondByte, thirdByte, fourthByte, tempCodePoint - - switch (bytesPerSequence) { - case 1: - if (firstByte < 0x80) { - codePoint = firstByte - } - break - case 2: - secondByte = buf[i + 1] - if ((secondByte & 0xC0) === 0x80) { - tempCodePoint = (firstByte & 0x1F) << 0x6 | (secondByte & 0x3F) - if (tempCodePoint > 0x7F) { - codePoint = tempCodePoint - } - } - break - case 3: - secondByte = buf[i + 1] - thirdByte = buf[i + 2] - if ((secondByte & 0xC0) === 0x80 && (thirdByte & 0xC0) === 0x80) { - tempCodePoint = (firstByte & 0xF) << 0xC | (secondByte & 0x3F) << 0x6 | (thirdByte & 0x3F) - if (tempCodePoint > 0x7FF && (tempCodePoint < 0xD800 || tempCodePoint > 0xDFFF)) { - codePoint = tempCodePoint - } - } - break - case 4: - secondByte = buf[i + 1] - thirdByte = buf[i + 2] - fourthByte = buf[i + 3] - if ((secondByte & 0xC0) === 0x80 && (thirdByte & 0xC0) === 0x80 && (fourthByte & 0xC0) === 0x80) { - tempCodePoint = (firstByte & 0xF) << 0x12 | (secondByte & 0x3F) << 0xC | (thirdByte & 0x3F) << 0x6 | (fourthByte & 0x3F) - if (tempCodePoint > 0xFFFF && tempCodePoint < 0x110000) { - codePoint = tempCodePoint - } - } - } - } - - if (codePoint === null) { - // we did not generate a valid codePoint so insert a - // replacement char (U+FFFD) and advance only 1 byte - codePoint = 0xFFFD - bytesPerSequence = 1 - } else if (codePoint > 0xFFFF) { - // encode to utf16 (surrogate pair dance) - codePoint -= 0x10000 - res.push(codePoint >>> 10 & 0x3FF | 0xD800) - codePoint = 0xDC00 | codePoint & 0x3FF - } - - res.push(codePoint) - i += bytesPerSequence - } - - return decodeCodePointsArray(res) -} - -// Based on http://stackoverflow.com/a/22747272/680742, the browser with -// the lowest limit is Chrome, with 0x10000 args. -// We go 1 magnitude less, for safety -var MAX_ARGUMENTS_LENGTH = 0x1000 - -function decodeCodePointsArray (codePoints) { - var len = codePoints.length - if (len <= MAX_ARGUMENTS_LENGTH) { - return String.fromCharCode.apply(String, codePoints) // avoid extra slice() - } - - // Decode in chunks to avoid "call stack size exceeded". - var res = '' - var i = 0 - while (i < len) { - res += String.fromCharCode.apply( - String, - codePoints.slice(i, i += MAX_ARGUMENTS_LENGTH) - ) - } - return res -} - -function asciiSlice (buf, start, end) { - var ret = '' - end = Math.min(buf.length, end) - - for (var i = start; i < end; ++i) { - ret += String.fromCharCode(buf[i] & 0x7F) - } - return ret -} - -function latin1Slice (buf, start, end) { - var ret = '' - end = Math.min(buf.length, end) - - for (var i = start; i < end; ++i) { - ret += String.fromCharCode(buf[i]) - } - return ret -} - -function hexSlice (buf, start, end) { - var len = buf.length - - if (!start || start < 0) start = 0 - if (!end || end < 0 || end > len) end = len - - var out = '' - for (var i = start; i < end; ++i) { - out += toHex(buf[i]) - } - return out -} - -function utf16leSlice (buf, start, end) { - var bytes = buf.slice(start, end) - var res = '' - for (var i = 0; i < bytes.length; i += 2) { - res += String.fromCharCode(bytes[i] + bytes[i + 1] * 256) - } - return res -} - -Buffer.prototype.slice = function slice (start, end) { - var len = this.length - start = ~~start - end = end === undefined ? len : ~~end - - if (start < 0) { - start += len - if (start < 0) start = 0 - } else if (start > len) { - start = len - } - - if (end < 0) { - end += len - if (end < 0) end = 0 - } else if (end > len) { - end = len - } - - if (end < start) end = start - - var newBuf - if (Buffer.TYPED_ARRAY_SUPPORT) { - newBuf = this.subarray(start, end) - newBuf.__proto__ = Buffer.prototype - } else { - var sliceLen = end - start - newBuf = new Buffer(sliceLen, undefined) - for (var i = 0; i < sliceLen; ++i) { - newBuf[i] = this[i + start] - } - } - - return newBuf -} - -/* - * Need to make sure that buffer isn't trying to write out of bounds. - */ -function checkOffset (offset, ext, length) { - if ((offset % 1) !== 0 || offset < 0) throw new RangeError('offset is not uint') - if (offset + ext > length) throw new RangeError('Trying to access beyond buffer length') -} - -Buffer.prototype.readUIntLE = function readUIntLE (offset, byteLength, noAssert) { - offset = offset | 0 - byteLength = byteLength | 0 - if (!noAssert) checkOffset(offset, byteLength, this.length) - - var val = this[offset] - var mul = 1 - var i = 0 - while (++i < byteLength && (mul *= 0x100)) { - val += this[offset + i] * mul - } - - return val -} - -Buffer.prototype.readUIntBE = function readUIntBE (offset, byteLength, noAssert) { - offset = offset | 0 - byteLength = byteLength | 0 - if (!noAssert) { - checkOffset(offset, byteLength, this.length) - } - - var val = this[offset + --byteLength] - var mul = 1 - while (byteLength > 0 && (mul *= 0x100)) { - val += this[offset + --byteLength] * mul - } - - return val -} - -Buffer.prototype.readUInt8 = function readUInt8 (offset, noAssert) { - if (!noAssert) checkOffset(offset, 1, this.length) - return this[offset] -} - -Buffer.prototype.readUInt16LE = function readUInt16LE (offset, noAssert) { - if (!noAssert) checkOffset(offset, 2, this.length) - return this[offset] | (this[offset + 1] << 8) -} - -Buffer.prototype.readUInt16BE = function readUInt16BE (offset, noAssert) { - if (!noAssert) checkOffset(offset, 2, this.length) - return (this[offset] << 8) | this[offset + 1] -} - -Buffer.prototype.readUInt32LE = function readUInt32LE (offset, noAssert) { - if (!noAssert) checkOffset(offset, 4, this.length) - - return ((this[offset]) | - (this[offset + 1] << 8) | - (this[offset + 2] << 16)) + - (this[offset + 3] * 0x1000000) -} - -Buffer.prototype.readUInt32BE = function readUInt32BE (offset, noAssert) { - if (!noAssert) checkOffset(offset, 4, this.length) - - return (this[offset] * 0x1000000) + - ((this[offset + 1] << 16) | - (this[offset + 2] << 8) | - this[offset + 3]) -} - -Buffer.prototype.readIntLE = function readIntLE (offset, byteLength, noAssert) { - offset = offset | 0 - byteLength = byteLength | 0 - if (!noAssert) checkOffset(offset, byteLength, this.length) - - var val = this[offset] - var mul = 1 - var i = 0 - while (++i < byteLength && (mul *= 0x100)) { - val += this[offset + i] * mul - } - mul *= 0x80 - - if (val >= mul) val -= Math.pow(2, 8 * byteLength) - - return val -} - -Buffer.prototype.readIntBE = function readIntBE (offset, byteLength, noAssert) { - offset = offset | 0 - byteLength = byteLength | 0 - if (!noAssert) checkOffset(offset, byteLength, this.length) - - var i = byteLength - var mul = 1 - var val = this[offset + --i] - while (i > 0 && (mul *= 0x100)) { - val += this[offset + --i] * mul - } - mul *= 0x80 - - if (val >= mul) val -= Math.pow(2, 8 * byteLength) - - return val -} - -Buffer.prototype.readInt8 = function readInt8 (offset, noAssert) { - if (!noAssert) checkOffset(offset, 1, this.length) - if (!(this[offset] & 0x80)) return (this[offset]) - return ((0xff - this[offset] + 1) * -1) -} - -Buffer.prototype.readInt16LE = function readInt16LE (offset, noAssert) { - if (!noAssert) checkOffset(offset, 2, this.length) - var val = this[offset] | (this[offset + 1] << 8) - return (val & 0x8000) ? val | 0xFFFF0000 : val -} - -Buffer.prototype.readInt16BE = function readInt16BE (offset, noAssert) { - if (!noAssert) checkOffset(offset, 2, this.length) - var val = this[offset + 1] | (this[offset] << 8) - return (val & 0x8000) ? val | 0xFFFF0000 : val -} - -Buffer.prototype.readInt32LE = function readInt32LE (offset, noAssert) { - if (!noAssert) checkOffset(offset, 4, this.length) - - return (this[offset]) | - (this[offset + 1] << 8) | - (this[offset + 2] << 16) | - (this[offset + 3] << 24) -} - -Buffer.prototype.readInt32BE = function readInt32BE (offset, noAssert) { - if (!noAssert) checkOffset(offset, 4, this.length) - - return (this[offset] << 24) | - (this[offset + 1] << 16) | - (this[offset + 2] << 8) | - (this[offset + 3]) -} - -Buffer.prototype.readFloatLE = function readFloatLE (offset, noAssert) { - if (!noAssert) checkOffset(offset, 4, this.length) - return ieee754.read(this, offset, true, 23, 4) -} - -Buffer.prototype.readFloatBE = function readFloatBE (offset, noAssert) { - if (!noAssert) checkOffset(offset, 4, this.length) - return ieee754.read(this, offset, false, 23, 4) -} - -Buffer.prototype.readDoubleLE = function readDoubleLE (offset, noAssert) { - if (!noAssert) checkOffset(offset, 8, this.length) - return ieee754.read(this, offset, true, 52, 8) -} - -Buffer.prototype.readDoubleBE = function readDoubleBE (offset, noAssert) { - if (!noAssert) checkOffset(offset, 8, this.length) - return ieee754.read(this, offset, false, 52, 8) -} - -function checkInt (buf, value, offset, ext, max, min) { - if (!Buffer.isBuffer(buf)) throw new TypeError('"buffer" argument must be a Buffer instance') - if (value > max || value < min) throw new RangeError('"value" argument is out of bounds') - if (offset + ext > buf.length) throw new RangeError('Index out of range') -} - -Buffer.prototype.writeUIntLE = function writeUIntLE (value, offset, byteLength, noAssert) { - value = +value - offset = offset | 0 - byteLength = byteLength | 0 - if (!noAssert) { - var maxBytes = Math.pow(2, 8 * byteLength) - 1 - checkInt(this, value, offset, byteLength, maxBytes, 0) - } - - var mul = 1 - var i = 0 - this[offset] = value & 0xFF - while (++i < byteLength && (mul *= 0x100)) { - this[offset + i] = (value / mul) & 0xFF - } - - return offset + byteLength -} - -Buffer.prototype.writeUIntBE = function writeUIntBE (value, offset, byteLength, noAssert) { - value = +value - offset = offset | 0 - byteLength = byteLength | 0 - if (!noAssert) { - var maxBytes = Math.pow(2, 8 * byteLength) - 1 - checkInt(this, value, offset, byteLength, maxBytes, 0) - } - - var i = byteLength - 1 - var mul = 1 - this[offset + i] = value & 0xFF - while (--i >= 0 && (mul *= 0x100)) { - this[offset + i] = (value / mul) & 0xFF - } - - return offset + byteLength -} - -Buffer.prototype.writeUInt8 = function writeUInt8 (value, offset, noAssert) { - value = +value - offset = offset | 0 - if (!noAssert) checkInt(this, value, offset, 1, 0xff, 0) - if (!Buffer.TYPED_ARRAY_SUPPORT) value = Math.floor(value) - this[offset] = (value & 0xff) - return offset + 1 -} - -function objectWriteUInt16 (buf, value, offset, littleEndian) { - if (value < 0) value = 0xffff + value + 1 - for (var i = 0, j = Math.min(buf.length - offset, 2); i < j; ++i) { - buf[offset + i] = (value & (0xff << (8 * (littleEndian ? i : 1 - i)))) >>> - (littleEndian ? i : 1 - i) * 8 - } -} - -Buffer.prototype.writeUInt16LE = function writeUInt16LE (value, offset, noAssert) { - value = +value - offset = offset | 0 - if (!noAssert) checkInt(this, value, offset, 2, 0xffff, 0) - if (Buffer.TYPED_ARRAY_SUPPORT) { - this[offset] = (value & 0xff) - this[offset + 1] = (value >>> 8) - } else { - objectWriteUInt16(this, value, offset, true) - } - return offset + 2 -} - -Buffer.prototype.writeUInt16BE = function writeUInt16BE (value, offset, noAssert) { - value = +value - offset = offset | 0 - if (!noAssert) checkInt(this, value, offset, 2, 0xffff, 0) - if (Buffer.TYPED_ARRAY_SUPPORT) { - this[offset] = (value >>> 8) - this[offset + 1] = (value & 0xff) - } else { - objectWriteUInt16(this, value, offset, false) - } - return offset + 2 -} - -function objectWriteUInt32 (buf, value, offset, littleEndian) { - if (value < 0) value = 0xffffffff + value + 1 - for (var i = 0, j = Math.min(buf.length - offset, 4); i < j; ++i) { - buf[offset + i] = (value >>> (littleEndian ? i : 3 - i) * 8) & 0xff - } -} - -Buffer.prototype.writeUInt32LE = function writeUInt32LE (value, offset, noAssert) { - value = +value - offset = offset | 0 - if (!noAssert) checkInt(this, value, offset, 4, 0xffffffff, 0) - if (Buffer.TYPED_ARRAY_SUPPORT) { - this[offset + 3] = (value >>> 24) - this[offset + 2] = (value >>> 16) - this[offset + 1] = (value >>> 8) - this[offset] = (value & 0xff) - } else { - objectWriteUInt32(this, value, offset, true) - } - return offset + 4 -} - -Buffer.prototype.writeUInt32BE = function writeUInt32BE (value, offset, noAssert) { - value = +value - offset = offset | 0 - if (!noAssert) checkInt(this, value, offset, 4, 0xffffffff, 0) - if (Buffer.TYPED_ARRAY_SUPPORT) { - this[offset] = (value >>> 24) - this[offset + 1] = (value >>> 16) - this[offset + 2] = (value >>> 8) - this[offset + 3] = (value & 0xff) - } else { - objectWriteUInt32(this, value, offset, false) - } - return offset + 4 -} - -Buffer.prototype.writeIntLE = function writeIntLE (value, offset, byteLength, noAssert) { - value = +value - offset = offset | 0 - if (!noAssert) { - var limit = Math.pow(2, 8 * byteLength - 1) - - checkInt(this, value, offset, byteLength, limit - 1, -limit) - } - - var i = 0 - var mul = 1 - var sub = 0 - this[offset] = value & 0xFF - while (++i < byteLength && (mul *= 0x100)) { - if (value < 0 && sub === 0 && this[offset + i - 1] !== 0) { - sub = 1 - } - this[offset + i] = ((value / mul) >> 0) - sub & 0xFF - } - - return offset + byteLength -} - -Buffer.prototype.writeIntBE = function writeIntBE (value, offset, byteLength, noAssert) { - value = +value - offset = offset | 0 - if (!noAssert) { - var limit = Math.pow(2, 8 * byteLength - 1) - - checkInt(this, value, offset, byteLength, limit - 1, -limit) - } - - var i = byteLength - 1 - var mul = 1 - var sub = 0 - this[offset + i] = value & 0xFF - while (--i >= 0 && (mul *= 0x100)) { - if (value < 0 && sub === 0 && this[offset + i + 1] !== 0) { - sub = 1 - } - this[offset + i] = ((value / mul) >> 0) - sub & 0xFF - } - - return offset + byteLength -} - -Buffer.prototype.writeInt8 = function writeInt8 (value, offset, noAssert) { - value = +value - offset = offset | 0 - if (!noAssert) checkInt(this, value, offset, 1, 0x7f, -0x80) - if (!Buffer.TYPED_ARRAY_SUPPORT) value = Math.floor(value) - if (value < 0) value = 0xff + value + 1 - this[offset] = (value & 0xff) - return offset + 1 -} - -Buffer.prototype.writeInt16LE = function writeInt16LE (value, offset, noAssert) { - value = +value - offset = offset | 0 - if (!noAssert) checkInt(this, value, offset, 2, 0x7fff, -0x8000) - if (Buffer.TYPED_ARRAY_SUPPORT) { - this[offset] = (value & 0xff) - this[offset + 1] = (value >>> 8) - } else { - objectWriteUInt16(this, value, offset, true) - } - return offset + 2 -} - -Buffer.prototype.writeInt16BE = function writeInt16BE (value, offset, noAssert) { - value = +value - offset = offset | 0 - if (!noAssert) checkInt(this, value, offset, 2, 0x7fff, -0x8000) - if (Buffer.TYPED_ARRAY_SUPPORT) { - this[offset] = (value >>> 8) - this[offset + 1] = (value & 0xff) - } else { - objectWriteUInt16(this, value, offset, false) - } - return offset + 2 -} - -Buffer.prototype.writeInt32LE = function writeInt32LE (value, offset, noAssert) { - value = +value - offset = offset | 0 - if (!noAssert) checkInt(this, value, offset, 4, 0x7fffffff, -0x80000000) - if (Buffer.TYPED_ARRAY_SUPPORT) { - this[offset] = (value & 0xff) - this[offset + 1] = (value >>> 8) - this[offset + 2] = (value >>> 16) - this[offset + 3] = (value >>> 24) - } else { - objectWriteUInt32(this, value, offset, true) - } - return offset + 4 -} - -Buffer.prototype.writeInt32BE = function writeInt32BE (value, offset, noAssert) { - value = +value - offset = offset | 0 - if (!noAssert) checkInt(this, value, offset, 4, 0x7fffffff, -0x80000000) - if (value < 0) value = 0xffffffff + value + 1 - if (Buffer.TYPED_ARRAY_SUPPORT) { - this[offset] = (value >>> 24) - this[offset + 1] = (value >>> 16) - this[offset + 2] = (value >>> 8) - this[offset + 3] = (value & 0xff) - } else { - objectWriteUInt32(this, value, offset, false) - } - return offset + 4 -} - -function checkIEEE754 (buf, value, offset, ext, max, min) { - if (offset + ext > buf.length) throw new RangeError('Index out of range') - if (offset < 0) throw new RangeError('Index out of range') -} - -function writeFloat (buf, value, offset, littleEndian, noAssert) { - if (!noAssert) { - checkIEEE754(buf, value, offset, 4, 3.4028234663852886e+38, -3.4028234663852886e+38) - } - ieee754.write(buf, value, offset, littleEndian, 23, 4) - return offset + 4 -} - -Buffer.prototype.writeFloatLE = function writeFloatLE (value, offset, noAssert) { - return writeFloat(this, value, offset, true, noAssert) -} - -Buffer.prototype.writeFloatBE = function writeFloatBE (value, offset, noAssert) { - return writeFloat(this, value, offset, false, noAssert) -} - -function writeDouble (buf, value, offset, littleEndian, noAssert) { - if (!noAssert) { - checkIEEE754(buf, value, offset, 8, 1.7976931348623157E+308, -1.7976931348623157E+308) - } - ieee754.write(buf, value, offset, littleEndian, 52, 8) - return offset + 8 -} - -Buffer.prototype.writeDoubleLE = function writeDoubleLE (value, offset, noAssert) { - return writeDouble(this, value, offset, true, noAssert) -} - -Buffer.prototype.writeDoubleBE = function writeDoubleBE (value, offset, noAssert) { - return writeDouble(this, value, offset, false, noAssert) -} - -// copy(targetBuffer, targetStart=0, sourceStart=0, sourceEnd=buffer.length) -Buffer.prototype.copy = function copy (target, targetStart, start, end) { - if (!start) start = 0 - if (!end && end !== 0) end = this.length - if (targetStart >= target.length) targetStart = target.length - if (!targetStart) targetStart = 0 - if (end > 0 && end < start) end = start - - // Copy 0 bytes; we're done - if (end === start) return 0 - if (target.length === 0 || this.length === 0) return 0 - - // Fatal error conditions - if (targetStart < 0) { - throw new RangeError('targetStart out of bounds') - } - if (start < 0 || start >= this.length) throw new RangeError('sourceStart out of bounds') - if (end < 0) throw new RangeError('sourceEnd out of bounds') - - // Are we oob? - if (end > this.length) end = this.length - if (target.length - targetStart < end - start) { - end = target.length - targetStart + start - } - - var len = end - start - var i - - if (this === target && start < targetStart && targetStart < end) { - // descending copy from end - for (i = len - 1; i >= 0; --i) { - target[i + targetStart] = this[i + start] - } - } else if (len < 1000 || !Buffer.TYPED_ARRAY_SUPPORT) { - // ascending copy from start - for (i = 0; i < len; ++i) { - target[i + targetStart] = this[i + start] - } - } else { - Uint8Array.prototype.set.call( - target, - this.subarray(start, start + len), - targetStart - ) - } - - return len -} - -// Usage: -// buffer.fill(number[, offset[, end]]) -// buffer.fill(buffer[, offset[, end]]) -// buffer.fill(string[, offset[, end]][, encoding]) -Buffer.prototype.fill = function fill (val, start, end, encoding) { - // Handle string cases: - if (typeof val === 'string') { - if (typeof start === 'string') { - encoding = start - start = 0 - end = this.length - } else if (typeof end === 'string') { - encoding = end - end = this.length - } - if (val.length === 1) { - var code = val.charCodeAt(0) - if (code < 256) { - val = code - } - } - if (encoding !== undefined && typeof encoding !== 'string') { - throw new TypeError('encoding must be a string') - } - if (typeof encoding === 'string' && !Buffer.isEncoding(encoding)) { - throw new TypeError('Unknown encoding: ' + encoding) - } - } else if (typeof val === 'number') { - val = val & 255 - } - - // Invalid ranges are not set to a default, so can range check early. - if (start < 0 || this.length < start || this.length < end) { - throw new RangeError('Out of range index') - } - - if (end <= start) { - return this - } - - start = start >>> 0 - end = end === undefined ? this.length : end >>> 0 - - if (!val) val = 0 - - var i - if (typeof val === 'number') { - for (i = start; i < end; ++i) { - this[i] = val - } - } else { - var bytes = Buffer.isBuffer(val) - ? val - : utf8ToBytes(new Buffer(val, encoding).toString()) - var len = bytes.length - for (i = 0; i < end - start; ++i) { - this[i + start] = bytes[i % len] - } - } - - return this -} - -// HELPER FUNCTIONS -// ================ - -var INVALID_BASE64_RE = /[^+\/0-9A-Za-z-_]/g - -function base64clean (str) { - // Node strips out invalid characters like \n and \t from the string, base64-js does not - str = stringtrim(str).replace(INVALID_BASE64_RE, '') - // Node converts strings with length < 2 to '' - if (str.length < 2) return '' - // Node allows for non-padded base64 strings (missing trailing ===), base64-js does not - while (str.length % 4 !== 0) { - str = str + '=' - } - return str -} - -function stringtrim (str) { - if (str.trim) return str.trim() - return str.replace(/^\s+|\s+$/g, '') -} - -function toHex (n) { - if (n < 16) return '0' + n.toString(16) - return n.toString(16) -} - -function utf8ToBytes (string, units) { - units = units || Infinity - var codePoint - var length = string.length - var leadSurrogate = null - var bytes = [] - - for (var i = 0; i < length; ++i) { - codePoint = string.charCodeAt(i) - - // is surrogate component - if (codePoint > 0xD7FF && codePoint < 0xE000) { - // last char was a lead - if (!leadSurrogate) { - // no lead yet - if (codePoint > 0xDBFF) { - // unexpected trail - if ((units -= 3) > -1) bytes.push(0xEF, 0xBF, 0xBD) - continue - } else if (i + 1 === length) { - // unpaired lead - if ((units -= 3) > -1) bytes.push(0xEF, 0xBF, 0xBD) - continue - } - - // valid lead - leadSurrogate = codePoint - - continue - } - - // 2 leads in a row - if (codePoint < 0xDC00) { - if ((units -= 3) > -1) bytes.push(0xEF, 0xBF, 0xBD) - leadSurrogate = codePoint - continue - } - - // valid surrogate pair - codePoint = (leadSurrogate - 0xD800 << 10 | codePoint - 0xDC00) + 0x10000 - } else if (leadSurrogate) { - // valid bmp char, but last char was a lead - if ((units -= 3) > -1) bytes.push(0xEF, 0xBF, 0xBD) - } - - leadSurrogate = null - - // encode utf8 - if (codePoint < 0x80) { - if ((units -= 1) < 0) break - bytes.push(codePoint) - } else if (codePoint < 0x800) { - if ((units -= 2) < 0) break - bytes.push( - codePoint >> 0x6 | 0xC0, - codePoint & 0x3F | 0x80 - ) - } else if (codePoint < 0x10000) { - if ((units -= 3) < 0) break - bytes.push( - codePoint >> 0xC | 0xE0, - codePoint >> 0x6 & 0x3F | 0x80, - codePoint & 0x3F | 0x80 - ) - } else if (codePoint < 0x110000) { - if ((units -= 4) < 0) break - bytes.push( - codePoint >> 0x12 | 0xF0, - codePoint >> 0xC & 0x3F | 0x80, - codePoint >> 0x6 & 0x3F | 0x80, - codePoint & 0x3F | 0x80 - ) - } else { - throw new Error('Invalid code point') - } - } - - return bytes -} - -function asciiToBytes (str) { - var byteArray = [] - for (var i = 0; i < str.length; ++i) { - // Node's code seems to be doing this and not & 0x7F.. - byteArray.push(str.charCodeAt(i) & 0xFF) - } - return byteArray -} - -function utf16leToBytes (str, units) { - var c, hi, lo - var byteArray = [] - for (var i = 0; i < str.length; ++i) { - if ((units -= 2) < 0) break - - c = str.charCodeAt(i) - hi = c >> 8 - lo = c % 256 - byteArray.push(lo) - byteArray.push(hi) - } - - return byteArray -} - -function base64ToBytes (str) { - return base64.toByteArray(base64clean(str)) -} - -function blitBuffer (src, dst, offset, length) { - for (var i = 0; i < length; ++i) { - if ((i + offset >= dst.length) || (i >= src.length)) break - dst[i + offset] = src[i] - } - return i -} - -function isnan (val) { - return val !== val // eslint-disable-line no-self-compare -} - -/* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(11))) - -/***/ }), -/* 1 */ -/***/ (function(module, exports) { - -if (typeof Object.create === 'function') { - // implementation from standard node.js 'util' module - module.exports = function inherits(ctor, superCtor) { - ctor.super_ = superCtor - ctor.prototype = Object.create(superCtor.prototype, { - constructor: { - value: ctor, - enumerable: false, - writable: true, - configurable: true - } - }); - }; -} else { - // old school shim for old browsers - module.exports = function inherits(ctor, superCtor) { - ctor.super_ = superCtor - var TempCtor = function () {} - TempCtor.prototype = superCtor.prototype - ctor.prototype = new TempCtor() - ctor.prototype.constructor = ctor - } -} - - -/***/ }), -/* 2 */ -/***/ (function(module, exports, __webpack_require__) { - -/* WEBPACK VAR INJECTION */(function(global, module) {var __WEBPACK_AMD_DEFINE_RESULT__;/** - * @license - * Lodash - * Copyright JS Foundation and other contributors - * Released under MIT license - * Based on Underscore.js 1.8.3 - * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors - */ -;(function() { - - /** Used as a safe reference for `undefined` in pre-ES5 environments. */ - var undefined; - - /** Used as the semantic version number. */ - var VERSION = '4.17.4'; - - /** Used as the size to enable large array optimizations. */ - var LARGE_ARRAY_SIZE = 200; - - /** Error message constants. */ - var CORE_ERROR_TEXT = 'Unsupported core-js use. Try https://npms.io/search?q=ponyfill.', - FUNC_ERROR_TEXT = 'Expected a function'; - - /** Used to stand-in for `undefined` hash values. */ - var HASH_UNDEFINED = '__lodash_hash_undefined__'; - - /** Used as the maximum memoize cache size. */ - var MAX_MEMOIZE_SIZE = 500; - - /** Used as the internal argument placeholder. */ - var PLACEHOLDER = '__lodash_placeholder__'; - - /** Used to compose bitmasks for cloning. */ - var CLONE_DEEP_FLAG = 1, - CLONE_FLAT_FLAG = 2, - CLONE_SYMBOLS_FLAG = 4; - - /** Used to compose bitmasks for value comparisons. */ - var COMPARE_PARTIAL_FLAG = 1, - COMPARE_UNORDERED_FLAG = 2; - - /** Used to compose bitmasks for function metadata. */ - var WRAP_BIND_FLAG = 1, - WRAP_BIND_KEY_FLAG = 2, - WRAP_CURRY_BOUND_FLAG = 4, - WRAP_CURRY_FLAG = 8, - WRAP_CURRY_RIGHT_FLAG = 16, - WRAP_PARTIAL_FLAG = 32, - WRAP_PARTIAL_RIGHT_FLAG = 64, - WRAP_ARY_FLAG = 128, - WRAP_REARG_FLAG = 256, - WRAP_FLIP_FLAG = 512; - - /** Used as default options for `_.truncate`. */ - var DEFAULT_TRUNC_LENGTH = 30, - DEFAULT_TRUNC_OMISSION = '...'; - - /** Used to detect hot functions by number of calls within a span of milliseconds. */ - var HOT_COUNT = 800, - HOT_SPAN = 16; - - /** Used to indicate the type of lazy iteratees. */ - var LAZY_FILTER_FLAG = 1, - LAZY_MAP_FLAG = 2, - LAZY_WHILE_FLAG = 3; - - /** Used as references for various `Number` constants. */ - var INFINITY = 1 / 0, - MAX_SAFE_INTEGER = 9007199254740991, - MAX_INTEGER = 1.7976931348623157e+308, - NAN = 0 / 0; - - /** Used as references for the maximum length and index of an array. */ - var MAX_ARRAY_LENGTH = 4294967295, - MAX_ARRAY_INDEX = MAX_ARRAY_LENGTH - 1, - HALF_MAX_ARRAY_LENGTH = MAX_ARRAY_LENGTH >>> 1; - - /** Used to associate wrap methods with their bit flags. */ - var wrapFlags = [ - ['ary', WRAP_ARY_FLAG], - ['bind', WRAP_BIND_FLAG], - ['bindKey', WRAP_BIND_KEY_FLAG], - ['curry', WRAP_CURRY_FLAG], - ['curryRight', WRAP_CURRY_RIGHT_FLAG], - ['flip', WRAP_FLIP_FLAG], - ['partial', WRAP_PARTIAL_FLAG], - ['partialRight', WRAP_PARTIAL_RIGHT_FLAG], - ['rearg', WRAP_REARG_FLAG] - ]; - - /** `Object#toString` result references. */ - var argsTag = '[object Arguments]', - arrayTag = '[object Array]', - asyncTag = '[object AsyncFunction]', - boolTag = '[object Boolean]', - dateTag = '[object Date]', - domExcTag = '[object DOMException]', - errorTag = '[object Error]', - funcTag = '[object Function]', - genTag = '[object GeneratorFunction]', - mapTag = '[object Map]', - numberTag = '[object Number]', - nullTag = '[object Null]', - objectTag = '[object Object]', - promiseTag = '[object Promise]', - proxyTag = '[object Proxy]', - regexpTag = '[object RegExp]', - setTag = '[object Set]', - stringTag = '[object String]', - symbolTag = '[object Symbol]', - undefinedTag = '[object Undefined]', - weakMapTag = '[object WeakMap]', - weakSetTag = '[object WeakSet]'; - - var arrayBufferTag = '[object ArrayBuffer]', - dataViewTag = '[object DataView]', - float32Tag = '[object Float32Array]', - float64Tag = '[object Float64Array]', - int8Tag = '[object Int8Array]', - int16Tag = '[object Int16Array]', - int32Tag = '[object Int32Array]', - uint8Tag = '[object Uint8Array]', - uint8ClampedTag = '[object Uint8ClampedArray]', - uint16Tag = '[object Uint16Array]', - uint32Tag = '[object Uint32Array]'; - - /** Used to match empty string literals in compiled template source. */ - var reEmptyStringLeading = /\b__p \+= '';/g, - reEmptyStringMiddle = /\b(__p \+=) '' \+/g, - reEmptyStringTrailing = /(__e\(.*?\)|\b__t\)) \+\n'';/g; - - /** Used to match HTML entities and HTML characters. */ - var reEscapedHtml = /&(?:amp|lt|gt|quot|#39);/g, - reUnescapedHtml = /[&<>"']/g, - reHasEscapedHtml = RegExp(reEscapedHtml.source), - reHasUnescapedHtml = RegExp(reUnescapedHtml.source); - - /** Used to match template delimiters. */ - var reEscape = /<%-([\s\S]+?)%>/g, - reEvaluate = /<%([\s\S]+?)%>/g, - reInterpolate = /<%=([\s\S]+?)%>/g; - - /** Used to match property names within property paths. */ - var reIsDeepProp = /\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/, - reIsPlainProp = /^\w*$/, - reLeadingDot = /^\./, - rePropName = /[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g; - - /** - * Used to match `RegExp` - * [syntax characters](http://ecma-international.org/ecma-262/7.0/#sec-patterns). - */ - var reRegExpChar = /[\\^$.*+?()[\]{}|]/g, - reHasRegExpChar = RegExp(reRegExpChar.source); - - /** Used to match leading and trailing whitespace. */ - var reTrim = /^\s+|\s+$/g, - reTrimStart = /^\s+/, - reTrimEnd = /\s+$/; - - /** Used to match wrap detail comments. */ - var reWrapComment = /\{(?:\n\/\* \[wrapped with .+\] \*\/)?\n?/, - reWrapDetails = /\{\n\/\* \[wrapped with (.+)\] \*/, - reSplitDetails = /,? & /; - - /** Used to match words composed of alphanumeric characters. */ - var reAsciiWord = /[^\x00-\x2f\x3a-\x40\x5b-\x60\x7b-\x7f]+/g; - - /** Used to match backslashes in property paths. */ - var reEscapeChar = /\\(\\)?/g; - - /** - * Used to match - * [ES template delimiters](http://ecma-international.org/ecma-262/7.0/#sec-template-literal-lexical-components). - */ - var reEsTemplate = /\$\{([^\\}]*(?:\\.[^\\}]*)*)\}/g; - - /** Used to match `RegExp` flags from their coerced string values. */ - var reFlags = /\w*$/; - - /** Used to detect bad signed hexadecimal string values. */ - var reIsBadHex = /^[-+]0x[0-9a-f]+$/i; - - /** Used to detect binary string values. */ - var reIsBinary = /^0b[01]+$/i; - - /** Used to detect host constructors (Safari). */ - var reIsHostCtor = /^\[object .+?Constructor\]$/; - - /** Used to detect octal string values. */ - var reIsOctal = /^0o[0-7]+$/i; - - /** Used to detect unsigned integer values. */ - var reIsUint = /^(?:0|[1-9]\d*)$/; - - /** Used to match Latin Unicode letters (excluding mathematical operators). */ - var reLatin = /[\xc0-\xd6\xd8-\xf6\xf8-\xff\u0100-\u017f]/g; - - /** Used to ensure capturing order of template delimiters. */ - var reNoMatch = /($^)/; - - /** Used to match unescaped characters in compiled string literals. */ - var reUnescapedString = /['\n\r\u2028\u2029\\]/g; - - /** Used to compose unicode character classes. */ - var rsAstralRange = '\\ud800-\\udfff', - rsComboMarksRange = '\\u0300-\\u036f', - reComboHalfMarksRange = '\\ufe20-\\ufe2f', - rsComboSymbolsRange = '\\u20d0-\\u20ff', - rsComboRange = rsComboMarksRange + reComboHalfMarksRange + rsComboSymbolsRange, - rsDingbatRange = '\\u2700-\\u27bf', - rsLowerRange = 'a-z\\xdf-\\xf6\\xf8-\\xff', - rsMathOpRange = '\\xac\\xb1\\xd7\\xf7', - rsNonCharRange = '\\x00-\\x2f\\x3a-\\x40\\x5b-\\x60\\x7b-\\xbf', - rsPunctuationRange = '\\u2000-\\u206f', - rsSpaceRange = ' \\t\\x0b\\f\\xa0\\ufeff\\n\\r\\u2028\\u2029\\u1680\\u180e\\u2000\\u2001\\u2002\\u2003\\u2004\\u2005\\u2006\\u2007\\u2008\\u2009\\u200a\\u202f\\u205f\\u3000', - rsUpperRange = 'A-Z\\xc0-\\xd6\\xd8-\\xde', - rsVarRange = '\\ufe0e\\ufe0f', - rsBreakRange = rsMathOpRange + rsNonCharRange + rsPunctuationRange + rsSpaceRange; - - /** Used to compose unicode capture groups. */ - var rsApos = "['\u2019]", - rsAstral = '[' + rsAstralRange + ']', - rsBreak = '[' + rsBreakRange + ']', - rsCombo = '[' + rsComboRange + ']', - rsDigits = '\\d+', - rsDingbat = '[' + rsDingbatRange + ']', - rsLower = '[' + rsLowerRange + ']', - rsMisc = '[^' + rsAstralRange + rsBreakRange + rsDigits + rsDingbatRange + rsLowerRange + rsUpperRange + ']', - rsFitz = '\\ud83c[\\udffb-\\udfff]', - rsModifier = '(?:' + rsCombo + '|' + rsFitz + ')', - rsNonAstral = '[^' + rsAstralRange + ']', - rsRegional = '(?:\\ud83c[\\udde6-\\uddff]){2}', - rsSurrPair = '[\\ud800-\\udbff][\\udc00-\\udfff]', - rsUpper = '[' + rsUpperRange + ']', - rsZWJ = '\\u200d'; - - /** Used to compose unicode regexes. */ - var rsMiscLower = '(?:' + rsLower + '|' + rsMisc + ')', - rsMiscUpper = '(?:' + rsUpper + '|' + rsMisc + ')', - rsOptContrLower = '(?:' + rsApos + '(?:d|ll|m|re|s|t|ve))?', - rsOptContrUpper = '(?:' + rsApos + '(?:D|LL|M|RE|S|T|VE))?', - reOptMod = rsModifier + '?', - rsOptVar = '[' + rsVarRange + ']?', - rsOptJoin = '(?:' + rsZWJ + '(?:' + [rsNonAstral, rsRegional, rsSurrPair].join('|') + ')' + rsOptVar + reOptMod + ')*', - rsOrdLower = '\\d*(?:(?:1st|2nd|3rd|(?![123])\\dth)\\b)', - rsOrdUpper = '\\d*(?:(?:1ST|2ND|3RD|(?![123])\\dTH)\\b)', - rsSeq = rsOptVar + reOptMod + rsOptJoin, - rsEmoji = '(?:' + [rsDingbat, rsRegional, rsSurrPair].join('|') + ')' + rsSeq, - rsSymbol = '(?:' + [rsNonAstral + rsCombo + '?', rsCombo, rsRegional, rsSurrPair, rsAstral].join('|') + ')'; - - /** Used to match apostrophes. */ - var reApos = RegExp(rsApos, 'g'); - - /** - * Used to match [combining diacritical marks](https://en.wikipedia.org/wiki/Combining_Diacritical_Marks) and - * [combining diacritical marks for symbols](https://en.wikipedia.org/wiki/Combining_Diacritical_Marks_for_Symbols). - */ - var reComboMark = RegExp(rsCombo, 'g'); - - /** Used to match [string symbols](https://mathiasbynens.be/notes/javascript-unicode). */ - var reUnicode = RegExp(rsFitz + '(?=' + rsFitz + ')|' + rsSymbol + rsSeq, 'g'); - - /** Used to match complex or compound words. */ - var reUnicodeWord = RegExp([ - rsUpper + '?' + rsLower + '+' + rsOptContrLower + '(?=' + [rsBreak, rsUpper, '$'].join('|') + ')', - rsMiscUpper + '+' + rsOptContrUpper + '(?=' + [rsBreak, rsUpper + rsMiscLower, '$'].join('|') + ')', - rsUpper + '?' + rsMiscLower + '+' + rsOptContrLower, - rsUpper + '+' + rsOptContrUpper, - rsOrdUpper, - rsOrdLower, - rsDigits, - rsEmoji - ].join('|'), 'g'); - - /** Used to detect strings with [zero-width joiners or code points from the astral planes](http://eev.ee/blog/2015/09/12/dark-corners-of-unicode/). */ - var reHasUnicode = RegExp('[' + rsZWJ + rsAstralRange + rsComboRange + rsVarRange + ']'); - - /** Used to detect strings that need a more robust regexp to match words. */ - var reHasUnicodeWord = /[a-z][A-Z]|[A-Z]{2,}[a-z]|[0-9][a-zA-Z]|[a-zA-Z][0-9]|[^a-zA-Z0-9 ]/; - - /** Used to assign default `context` object properties. */ - var contextProps = [ - 'Array', 'Buffer', 'DataView', 'Date', 'Error', 'Float32Array', 'Float64Array', - 'Function', 'Int8Array', 'Int16Array', 'Int32Array', 'Map', 'Math', 'Object', - 'Promise', 'RegExp', 'Set', 'String', 'Symbol', 'TypeError', 'Uint8Array', - 'Uint8ClampedArray', 'Uint16Array', 'Uint32Array', 'WeakMap', - '_', 'clearTimeout', 'isFinite', 'parseInt', 'setTimeout' - ]; - - /** Used to make template sourceURLs easier to identify. */ - var templateCounter = -1; - - /** Used to identify `toStringTag` values of typed arrays. */ - var typedArrayTags = {}; - typedArrayTags[float32Tag] = typedArrayTags[float64Tag] = - typedArrayTags[int8Tag] = typedArrayTags[int16Tag] = - typedArrayTags[int32Tag] = typedArrayTags[uint8Tag] = - typedArrayTags[uint8ClampedTag] = typedArrayTags[uint16Tag] = - typedArrayTags[uint32Tag] = true; - typedArrayTags[argsTag] = typedArrayTags[arrayTag] = - typedArrayTags[arrayBufferTag] = typedArrayTags[boolTag] = - typedArrayTags[dataViewTag] = typedArrayTags[dateTag] = - typedArrayTags[errorTag] = typedArrayTags[funcTag] = - typedArrayTags[mapTag] = typedArrayTags[numberTag] = - typedArrayTags[objectTag] = typedArrayTags[regexpTag] = - typedArrayTags[setTag] = typedArrayTags[stringTag] = - typedArrayTags[weakMapTag] = false; - - /** Used to identify `toStringTag` values supported by `_.clone`. */ - var cloneableTags = {}; - cloneableTags[argsTag] = cloneableTags[arrayTag] = - cloneableTags[arrayBufferTag] = cloneableTags[dataViewTag] = - cloneableTags[boolTag] = cloneableTags[dateTag] = - cloneableTags[float32Tag] = cloneableTags[float64Tag] = - cloneableTags[int8Tag] = cloneableTags[int16Tag] = - cloneableTags[int32Tag] = cloneableTags[mapTag] = - cloneableTags[numberTag] = cloneableTags[objectTag] = - cloneableTags[regexpTag] = cloneableTags[setTag] = - cloneableTags[stringTag] = cloneableTags[symbolTag] = - cloneableTags[uint8Tag] = cloneableTags[uint8ClampedTag] = - cloneableTags[uint16Tag] = cloneableTags[uint32Tag] = true; - cloneableTags[errorTag] = cloneableTags[funcTag] = - cloneableTags[weakMapTag] = false; - - /** Used to map Latin Unicode letters to basic Latin letters. */ - var deburredLetters = { - // Latin-1 Supplement block. - '\xc0': 'A', '\xc1': 'A', '\xc2': 'A', '\xc3': 'A', '\xc4': 'A', '\xc5': 'A', - '\xe0': 'a', '\xe1': 'a', '\xe2': 'a', '\xe3': 'a', '\xe4': 'a', '\xe5': 'a', - '\xc7': 'C', '\xe7': 'c', - '\xd0': 'D', '\xf0': 'd', - '\xc8': 'E', '\xc9': 'E', '\xca': 'E', '\xcb': 'E', - '\xe8': 'e', '\xe9': 'e', '\xea': 'e', '\xeb': 'e', - '\xcc': 'I', '\xcd': 'I', '\xce': 'I', '\xcf': 'I', - '\xec': 'i', '\xed': 'i', '\xee': 'i', '\xef': 'i', - '\xd1': 'N', '\xf1': 'n', - '\xd2': 'O', '\xd3': 'O', '\xd4': 'O', '\xd5': 'O', '\xd6': 'O', '\xd8': 'O', - '\xf2': 'o', '\xf3': 'o', '\xf4': 'o', '\xf5': 'o', '\xf6': 'o', '\xf8': 'o', - '\xd9': 'U', '\xda': 'U', '\xdb': 'U', '\xdc': 'U', - '\xf9': 'u', '\xfa': 'u', '\xfb': 'u', '\xfc': 'u', - '\xdd': 'Y', '\xfd': 'y', '\xff': 'y', - '\xc6': 'Ae', '\xe6': 'ae', - '\xde': 'Th', '\xfe': 'th', - '\xdf': 'ss', - // Latin Extended-A block. - '\u0100': 'A', '\u0102': 'A', '\u0104': 'A', - '\u0101': 'a', '\u0103': 'a', '\u0105': 'a', - '\u0106': 'C', '\u0108': 'C', '\u010a': 'C', '\u010c': 'C', - '\u0107': 'c', '\u0109': 'c', '\u010b': 'c', '\u010d': 'c', - '\u010e': 'D', '\u0110': 'D', '\u010f': 'd', '\u0111': 'd', - '\u0112': 'E', '\u0114': 'E', '\u0116': 'E', '\u0118': 'E', '\u011a': 'E', - '\u0113': 'e', '\u0115': 'e', '\u0117': 'e', '\u0119': 'e', '\u011b': 'e', - '\u011c': 'G', '\u011e': 'G', '\u0120': 'G', '\u0122': 'G', - '\u011d': 'g', '\u011f': 'g', '\u0121': 'g', '\u0123': 'g', - '\u0124': 'H', '\u0126': 'H', '\u0125': 'h', '\u0127': 'h', - '\u0128': 'I', '\u012a': 'I', '\u012c': 'I', '\u012e': 'I', '\u0130': 'I', - '\u0129': 'i', '\u012b': 'i', '\u012d': 'i', '\u012f': 'i', '\u0131': 'i', - '\u0134': 'J', '\u0135': 'j', - '\u0136': 'K', '\u0137': 'k', '\u0138': 'k', - '\u0139': 'L', '\u013b': 'L', '\u013d': 'L', '\u013f': 'L', '\u0141': 'L', - '\u013a': 'l', '\u013c': 'l', '\u013e': 'l', '\u0140': 'l', '\u0142': 'l', - '\u0143': 'N', '\u0145': 'N', '\u0147': 'N', '\u014a': 'N', - '\u0144': 'n', '\u0146': 'n', '\u0148': 'n', '\u014b': 'n', - '\u014c': 'O', '\u014e': 'O', '\u0150': 'O', - '\u014d': 'o', '\u014f': 'o', '\u0151': 'o', - '\u0154': 'R', '\u0156': 'R', '\u0158': 'R', - '\u0155': 'r', '\u0157': 'r', '\u0159': 'r', - '\u015a': 'S', '\u015c': 'S', '\u015e': 'S', '\u0160': 'S', - '\u015b': 's', '\u015d': 's', '\u015f': 's', '\u0161': 's', - '\u0162': 'T', '\u0164': 'T', '\u0166': 'T', - '\u0163': 't', '\u0165': 't', '\u0167': 't', - '\u0168': 'U', '\u016a': 'U', '\u016c': 'U', '\u016e': 'U', '\u0170': 'U', '\u0172': 'U', - '\u0169': 'u', '\u016b': 'u', '\u016d': 'u', '\u016f': 'u', '\u0171': 'u', '\u0173': 'u', - '\u0174': 'W', '\u0175': 'w', - '\u0176': 'Y', '\u0177': 'y', '\u0178': 'Y', - '\u0179': 'Z', '\u017b': 'Z', '\u017d': 'Z', - '\u017a': 'z', '\u017c': 'z', '\u017e': 'z', - '\u0132': 'IJ', '\u0133': 'ij', - '\u0152': 'Oe', '\u0153': 'oe', - '\u0149': "'n", '\u017f': 's' - }; - - /** Used to map characters to HTML entities. */ - var htmlEscapes = { - '&': '&', - '<': '<', - '>': '>', - '"': '"', - "'": ''' - }; - - /** Used to map HTML entities to characters. */ - var htmlUnescapes = { - '&': '&', - '<': '<', - '>': '>', - '"': '"', - ''': "'" - }; - - /** Used to escape characters for inclusion in compiled string literals. */ - var stringEscapes = { - '\\': '\\', - "'": "'", - '\n': 'n', - '\r': 'r', - '\u2028': 'u2028', - '\u2029': 'u2029' - }; - - /** Built-in method references without a dependency on `root`. */ - var freeParseFloat = parseFloat, - freeParseInt = parseInt; - - /** Detect free variable `global` from Node.js. */ - var freeGlobal = typeof global == 'object' && global && global.Object === Object && global; - - /** Detect free variable `self`. */ - var freeSelf = typeof self == 'object' && self && self.Object === Object && self; - - /** Used as a reference to the global object. */ - var root = freeGlobal || freeSelf || Function('return this')(); - - /** Detect free variable `exports`. */ - var freeExports = typeof exports == 'object' && exports && !exports.nodeType && exports; - - /** Detect free variable `module`. */ - var freeModule = freeExports && typeof module == 'object' && module && !module.nodeType && module; - - /** Detect the popular CommonJS extension `module.exports`. */ - var moduleExports = freeModule && freeModule.exports === freeExports; - - /** Detect free variable `process` from Node.js. */ - var freeProcess = moduleExports && freeGlobal.process; - - /** Used to access faster Node.js helpers. */ - var nodeUtil = (function() { - try { - return freeProcess && freeProcess.binding && freeProcess.binding('util'); - } catch (e) {} - }()); - - /* Node.js helper references. */ - var nodeIsArrayBuffer = nodeUtil && nodeUtil.isArrayBuffer, - nodeIsDate = nodeUtil && nodeUtil.isDate, - nodeIsMap = nodeUtil && nodeUtil.isMap, - nodeIsRegExp = nodeUtil && nodeUtil.isRegExp, - nodeIsSet = nodeUtil && nodeUtil.isSet, - nodeIsTypedArray = nodeUtil && nodeUtil.isTypedArray; - - /*--------------------------------------------------------------------------*/ - - /** - * Adds the key-value `pair` to `map`. - * - * @private - * @param {Object} map The map to modify. - * @param {Array} pair The key-value pair to add. - * @returns {Object} Returns `map`. - */ - function addMapEntry(map, pair) { - // Don't return `map.set` because it's not chainable in IE 11. - map.set(pair[0], pair[1]); - return map; - } - - /** - * Adds `value` to `set`. - * - * @private - * @param {Object} set The set to modify. - * @param {*} value The value to add. - * @returns {Object} Returns `set`. - */ - function addSetEntry(set, value) { - // Don't return `set.add` because it's not chainable in IE 11. - set.add(value); - return set; - } - - /** - * A faster alternative to `Function#apply`, this function invokes `func` - * with the `this` binding of `thisArg` and the arguments of `args`. - * - * @private - * @param {Function} func The function to invoke. - * @param {*} thisArg The `this` binding of `func`. - * @param {Array} args The arguments to invoke `func` with. - * @returns {*} Returns the result of `func`. - */ - function apply(func, thisArg, args) { - switch (args.length) { - case 0: return func.call(thisArg); - case 1: return func.call(thisArg, args[0]); - case 2: return func.call(thisArg, args[0], args[1]); - case 3: return func.call(thisArg, args[0], args[1], args[2]); - } - return func.apply(thisArg, args); - } - - /** - * A specialized version of `baseAggregator` for arrays. - * - * @private - * @param {Array} [array] The array to iterate over. - * @param {Function} setter The function to set `accumulator` values. - * @param {Function} iteratee The iteratee to transform keys. - * @param {Object} accumulator The initial aggregated object. - * @returns {Function} Returns `accumulator`. - */ - function arrayAggregator(array, setter, iteratee, accumulator) { - var index = -1, - length = array == null ? 0 : array.length; - - while (++index < length) { - var value = array[index]; - setter(accumulator, value, iteratee(value), array); - } - return accumulator; - } - - /** - * A specialized version of `_.forEach` for arrays without support for - * iteratee shorthands. - * - * @private - * @param {Array} [array] The array to iterate over. - * @param {Function} iteratee The function invoked per iteration. - * @returns {Array} Returns `array`. - */ - function arrayEach(array, iteratee) { - var index = -1, - length = array == null ? 0 : array.length; - - while (++index < length) { - if (iteratee(array[index], index, array) === false) { - break; - } - } - return array; - } - - /** - * A specialized version of `_.forEachRight` for arrays without support for - * iteratee shorthands. - * - * @private - * @param {Array} [array] The array to iterate over. - * @param {Function} iteratee The function invoked per iteration. - * @returns {Array} Returns `array`. - */ - function arrayEachRight(array, iteratee) { - var length = array == null ? 0 : array.length; - - while (length--) { - if (iteratee(array[length], length, array) === false) { - break; - } - } - return array; - } - - /** - * A specialized version of `_.every` for arrays without support for - * iteratee shorthands. - * - * @private - * @param {Array} [array] The array to iterate over. - * @param {Function} predicate The function invoked per iteration. - * @returns {boolean} Returns `true` if all elements pass the predicate check, - * else `false`. - */ - function arrayEvery(array, predicate) { - var index = -1, - length = array == null ? 0 : array.length; - - while (++index < length) { - if (!predicate(array[index], index, array)) { - return false; - } - } - return true; - } - - /** - * A specialized version of `_.filter` for arrays without support for - * iteratee shorthands. - * - * @private - * @param {Array} [array] The array to iterate over. - * @param {Function} predicate The function invoked per iteration. - * @returns {Array} Returns the new filtered array. - */ - function arrayFilter(array, predicate) { - var index = -1, - length = array == null ? 0 : array.length, - resIndex = 0, - result = []; - - while (++index < length) { - var value = array[index]; - if (predicate(value, index, array)) { - result[resIndex++] = value; - } - } - return result; - } - - /** - * A specialized version of `_.includes` for arrays without support for - * specifying an index to search from. - * - * @private - * @param {Array} [array] The array to inspect. - * @param {*} target The value to search for. - * @returns {boolean} Returns `true` if `target` is found, else `false`. - */ - function arrayIncludes(array, value) { - var length = array == null ? 0 : array.length; - return !!length && baseIndexOf(array, value, 0) > -1; - } - - /** - * This function is like `arrayIncludes` except that it accepts a comparator. - * - * @private - * @param {Array} [array] The array to inspect. - * @param {*} target The value to search for. - * @param {Function} comparator The comparator invoked per element. - * @returns {boolean} Returns `true` if `target` is found, else `false`. - */ - function arrayIncludesWith(array, value, comparator) { - var index = -1, - length = array == null ? 0 : array.length; - - while (++index < length) { - if (comparator(value, array[index])) { - return true; - } - } - return false; - } - - /** - * A specialized version of `_.map` for arrays without support for iteratee - * shorthands. - * - * @private - * @param {Array} [array] The array to iterate over. - * @param {Function} iteratee The function invoked per iteration. - * @returns {Array} Returns the new mapped array. - */ - function arrayMap(array, iteratee) { - var index = -1, - length = array == null ? 0 : array.length, - result = Array(length); - - while (++index < length) { - result[index] = iteratee(array[index], index, array); - } - return result; - } - - /** - * Appends the elements of `values` to `array`. - * - * @private - * @param {Array} array The array to modify. - * @param {Array} values The values to append. - * @returns {Array} Returns `array`. - */ - function arrayPush(array, values) { - var index = -1, - length = values.length, - offset = array.length; - - while (++index < length) { - array[offset + index] = values[index]; - } - return array; - } - - /** - * A specialized version of `_.reduce` for arrays without support for - * iteratee shorthands. - * - * @private - * @param {Array} [array] The array to iterate over. - * @param {Function} iteratee The function invoked per iteration. - * @param {*} [accumulator] The initial value. - * @param {boolean} [initAccum] Specify using the first element of `array` as - * the initial value. - * @returns {*} Returns the accumulated value. - */ - function arrayReduce(array, iteratee, accumulator, initAccum) { - var index = -1, - length = array == null ? 0 : array.length; - - if (initAccum && length) { - accumulator = array[++index]; - } - while (++index < length) { - accumulator = iteratee(accumulator, array[index], index, array); - } - return accumulator; - } - - /** - * A specialized version of `_.reduceRight` for arrays without support for - * iteratee shorthands. - * - * @private - * @param {Array} [array] The array to iterate over. - * @param {Function} iteratee The function invoked per iteration. - * @param {*} [accumulator] The initial value. - * @param {boolean} [initAccum] Specify using the last element of `array` as - * the initial value. - * @returns {*} Returns the accumulated value. - */ - function arrayReduceRight(array, iteratee, accumulator, initAccum) { - var length = array == null ? 0 : array.length; - if (initAccum && length) { - accumulator = array[--length]; - } - while (length--) { - accumulator = iteratee(accumulator, array[length], length, array); - } - return accumulator; - } - - /** - * A specialized version of `_.some` for arrays without support for iteratee - * shorthands. - * - * @private - * @param {Array} [array] The array to iterate over. - * @param {Function} predicate The function invoked per iteration. - * @returns {boolean} Returns `true` if any element passes the predicate check, - * else `false`. - */ - function arraySome(array, predicate) { - var index = -1, - length = array == null ? 0 : array.length; - - while (++index < length) { - if (predicate(array[index], index, array)) { - return true; - } - } - return false; - } - - /** - * Gets the size of an ASCII `string`. - * - * @private - * @param {string} string The string inspect. - * @returns {number} Returns the string size. - */ - var asciiSize = baseProperty('length'); - - /** - * Converts an ASCII `string` to an array. - * - * @private - * @param {string} string The string to convert. - * @returns {Array} Returns the converted array. - */ - function asciiToArray(string) { - return string.split(''); - } - - /** - * Splits an ASCII `string` into an array of its words. - * - * @private - * @param {string} The string to inspect. - * @returns {Array} Returns the words of `string`. - */ - function asciiWords(string) { - return string.match(reAsciiWord) || []; - } - - /** - * The base implementation of methods like `_.findKey` and `_.findLastKey`, - * without support for iteratee shorthands, which iterates over `collection` - * using `eachFunc`. - * - * @private - * @param {Array|Object} collection The collection to inspect. - * @param {Function} predicate The function invoked per iteration. - * @param {Function} eachFunc The function to iterate over `collection`. - * @returns {*} Returns the found element or its key, else `undefined`. - */ - function baseFindKey(collection, predicate, eachFunc) { - var result; - eachFunc(collection, function(value, key, collection) { - if (predicate(value, key, collection)) { - result = key; - return false; - } - }); - return result; - } - - /** - * The base implementation of `_.findIndex` and `_.findLastIndex` without - * support for iteratee shorthands. - * - * @private - * @param {Array} array The array to inspect. - * @param {Function} predicate The function invoked per iteration. - * @param {number} fromIndex The index to search from. - * @param {boolean} [fromRight] Specify iterating from right to left. - * @returns {number} Returns the index of the matched value, else `-1`. - */ - function baseFindIndex(array, predicate, fromIndex, fromRight) { - var length = array.length, - index = fromIndex + (fromRight ? 1 : -1); - - while ((fromRight ? index-- : ++index < length)) { - if (predicate(array[index], index, array)) { - return index; - } - } - return -1; - } - - /** - * The base implementation of `_.indexOf` without `fromIndex` bounds checks. - * - * @private - * @param {Array} array The array to inspect. - * @param {*} value The value to search for. - * @param {number} fromIndex The index to search from. - * @returns {number} Returns the index of the matched value, else `-1`. - */ - function baseIndexOf(array, value, fromIndex) { - return value === value - ? strictIndexOf(array, value, fromIndex) - : baseFindIndex(array, baseIsNaN, fromIndex); - } - - /** - * This function is like `baseIndexOf` except that it accepts a comparator. - * - * @private - * @param {Array} array The array to inspect. - * @param {*} value The value to search for. - * @param {number} fromIndex The index to search from. - * @param {Function} comparator The comparator invoked per element. - * @returns {number} Returns the index of the matched value, else `-1`. - */ - function baseIndexOfWith(array, value, fromIndex, comparator) { - var index = fromIndex - 1, - length = array.length; - - while (++index < length) { - if (comparator(array[index], value)) { - return index; - } - } - return -1; - } - - /** - * The base implementation of `_.isNaN` without support for number objects. - * - * @private - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if `value` is `NaN`, else `false`. - */ - function baseIsNaN(value) { - return value !== value; - } - - /** - * The base implementation of `_.mean` and `_.meanBy` without support for - * iteratee shorthands. - * - * @private - * @param {Array} array The array to iterate over. - * @param {Function} iteratee The function invoked per iteration. - * @returns {number} Returns the mean. - */ - function baseMean(array, iteratee) { - var length = array == null ? 0 : array.length; - return length ? (baseSum(array, iteratee) / length) : NAN; - } - - /** - * The base implementation of `_.property` without support for deep paths. - * - * @private - * @param {string} key The key of the property to get. - * @returns {Function} Returns the new accessor function. - */ - function baseProperty(key) { - return function(object) { - return object == null ? undefined : object[key]; - }; - } - - /** - * The base implementation of `_.propertyOf` without support for deep paths. - * - * @private - * @param {Object} object The object to query. - * @returns {Function} Returns the new accessor function. - */ - function basePropertyOf(object) { - return function(key) { - return object == null ? undefined : object[key]; - }; - } - - /** - * The base implementation of `_.reduce` and `_.reduceRight`, without support - * for iteratee shorthands, which iterates over `collection` using `eachFunc`. - * - * @private - * @param {Array|Object} collection The collection to iterate over. - * @param {Function} iteratee The function invoked per iteration. - * @param {*} accumulator The initial value. - * @param {boolean} initAccum Specify using the first or last element of - * `collection` as the initial value. - * @param {Function} eachFunc The function to iterate over `collection`. - * @returns {*} Returns the accumulated value. - */ - function baseReduce(collection, iteratee, accumulator, initAccum, eachFunc) { - eachFunc(collection, function(value, index, collection) { - accumulator = initAccum - ? (initAccum = false, value) - : iteratee(accumulator, value, index, collection); - }); - return accumulator; - } - - /** - * The base implementation of `_.sortBy` which uses `comparer` to define the - * sort order of `array` and replaces criteria objects with their corresponding - * values. - * - * @private - * @param {Array} array The array to sort. - * @param {Function} comparer The function to define sort order. - * @returns {Array} Returns `array`. - */ - function baseSortBy(array, comparer) { - var length = array.length; - - array.sort(comparer); - while (length--) { - array[length] = array[length].value; - } - return array; - } - - /** - * The base implementation of `_.sum` and `_.sumBy` without support for - * iteratee shorthands. - * - * @private - * @param {Array} array The array to iterate over. - * @param {Function} iteratee The function invoked per iteration. - * @returns {number} Returns the sum. - */ - function baseSum(array, iteratee) { - var result, - index = -1, - length = array.length; - - while (++index < length) { - var current = iteratee(array[index]); - if (current !== undefined) { - result = result === undefined ? current : (result + current); - } - } - return result; - } - - /** - * The base implementation of `_.times` without support for iteratee shorthands - * or max array length checks. - * - * @private - * @param {number} n The number of times to invoke `iteratee`. - * @param {Function} iteratee The function invoked per iteration. - * @returns {Array} Returns the array of results. - */ - function baseTimes(n, iteratee) { - var index = -1, - result = Array(n); - - while (++index < n) { - result[index] = iteratee(index); - } - return result; - } - - /** - * The base implementation of `_.toPairs` and `_.toPairsIn` which creates an array - * of key-value pairs for `object` corresponding to the property names of `props`. - * - * @private - * @param {Object} object The object to query. - * @param {Array} props The property names to get values for. - * @returns {Object} Returns the key-value pairs. - */ - function baseToPairs(object, props) { - return arrayMap(props, function(key) { - return [key, object[key]]; - }); - } - - /** - * The base implementation of `_.unary` without support for storing metadata. - * - * @private - * @param {Function} func The function to cap arguments for. - * @returns {Function} Returns the new capped function. - */ - function baseUnary(func) { - return function(value) { - return func(value); - }; - } - - /** - * The base implementation of `_.values` and `_.valuesIn` which creates an - * array of `object` property values corresponding to the property names - * of `props`. - * - * @private - * @param {Object} object The object to query. - * @param {Array} props The property names to get values for. - * @returns {Object} Returns the array of property values. - */ - function baseValues(object, props) { - return arrayMap(props, function(key) { - return object[key]; - }); - } - - /** - * Checks if a `cache` value for `key` exists. - * - * @private - * @param {Object} cache The cache to query. - * @param {string} key The key of the entry to check. - * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`. - */ - function cacheHas(cache, key) { - return cache.has(key); - } - - /** - * Used by `_.trim` and `_.trimStart` to get the index of the first string symbol - * that is not found in the character symbols. - * - * @private - * @param {Array} strSymbols The string symbols to inspect. - * @param {Array} chrSymbols The character symbols to find. - * @returns {number} Returns the index of the first unmatched string symbol. - */ - function charsStartIndex(strSymbols, chrSymbols) { - var index = -1, - length = strSymbols.length; - - while (++index < length && baseIndexOf(chrSymbols, strSymbols[index], 0) > -1) {} - return index; - } - - /** - * Used by `_.trim` and `_.trimEnd` to get the index of the last string symbol - * that is not found in the character symbols. - * - * @private - * @param {Array} strSymbols The string symbols to inspect. - * @param {Array} chrSymbols The character symbols to find. - * @returns {number} Returns the index of the last unmatched string symbol. - */ - function charsEndIndex(strSymbols, chrSymbols) { - var index = strSymbols.length; - - while (index-- && baseIndexOf(chrSymbols, strSymbols[index], 0) > -1) {} - return index; - } - - /** - * Gets the number of `placeholder` occurrences in `array`. - * - * @private - * @param {Array} array The array to inspect. - * @param {*} placeholder The placeholder to search for. - * @returns {number} Returns the placeholder count. - */ - function countHolders(array, placeholder) { - var length = array.length, - result = 0; - - while (length--) { - if (array[length] === placeholder) { - ++result; - } - } - return result; - } - - /** - * Used by `_.deburr` to convert Latin-1 Supplement and Latin Extended-A - * letters to basic Latin letters. - * - * @private - * @param {string} letter The matched letter to deburr. - * @returns {string} Returns the deburred letter. - */ - var deburrLetter = basePropertyOf(deburredLetters); - - /** - * Used by `_.escape` to convert characters to HTML entities. - * - * @private - * @param {string} chr The matched character to escape. - * @returns {string} Returns the escaped character. - */ - var escapeHtmlChar = basePropertyOf(htmlEscapes); - - /** - * Used by `_.template` to escape characters for inclusion in compiled string literals. - * - * @private - * @param {string} chr The matched character to escape. - * @returns {string} Returns the escaped character. - */ - function escapeStringChar(chr) { - return '\\' + stringEscapes[chr]; - } - - /** - * Gets the value at `key` of `object`. - * - * @private - * @param {Object} [object] The object to query. - * @param {string} key The key of the property to get. - * @returns {*} Returns the property value. - */ - function getValue(object, key) { - return object == null ? undefined : object[key]; - } - - /** - * Checks if `string` contains Unicode symbols. - * - * @private - * @param {string} string The string to inspect. - * @returns {boolean} Returns `true` if a symbol is found, else `false`. - */ - function hasUnicode(string) { - return reHasUnicode.test(string); - } - - /** - * Checks if `string` contains a word composed of Unicode symbols. - * - * @private - * @param {string} string The string to inspect. - * @returns {boolean} Returns `true` if a word is found, else `false`. - */ - function hasUnicodeWord(string) { - return reHasUnicodeWord.test(string); - } - - /** - * Converts `iterator` to an array. - * - * @private - * @param {Object} iterator The iterator to convert. - * @returns {Array} Returns the converted array. - */ - function iteratorToArray(iterator) { - var data, - result = []; - - while (!(data = iterator.next()).done) { - result.push(data.value); - } - return result; - } - - /** - * Converts `map` to its key-value pairs. - * - * @private - * @param {Object} map The map to convert. - * @returns {Array} Returns the key-value pairs. - */ - function mapToArray(map) { - var index = -1, - result = Array(map.size); - - map.forEach(function(value, key) { - result[++index] = [key, value]; - }); - return result; - } - - /** - * Creates a unary function that invokes `func` with its argument transformed. - * - * @private - * @param {Function} func The function to wrap. - * @param {Function} transform The argument transform. - * @returns {Function} Returns the new function. - */ - function overArg(func, transform) { - return function(arg) { - return func(transform(arg)); - }; - } - - /** - * Replaces all `placeholder` elements in `array` with an internal placeholder - * and returns an array of their indexes. - * - * @private - * @param {Array} array The array to modify. - * @param {*} placeholder The placeholder to replace. - * @returns {Array} Returns the new array of placeholder indexes. - */ - function replaceHolders(array, placeholder) { - var index = -1, - length = array.length, - resIndex = 0, - result = []; - - while (++index < length) { - var value = array[index]; - if (value === placeholder || value === PLACEHOLDER) { - array[index] = PLACEHOLDER; - result[resIndex++] = index; - } - } - return result; - } - - /** - * Converts `set` to an array of its values. - * - * @private - * @param {Object} set The set to convert. - * @returns {Array} Returns the values. - */ - function setToArray(set) { - var index = -1, - result = Array(set.size); - - set.forEach(function(value) { - result[++index] = value; - }); - return result; - } - - /** - * Converts `set` to its value-value pairs. - * - * @private - * @param {Object} set The set to convert. - * @returns {Array} Returns the value-value pairs. - */ - function setToPairs(set) { - var index = -1, - result = Array(set.size); - - set.forEach(function(value) { - result[++index] = [value, value]; - }); - return result; - } - - /** - * A specialized version of `_.indexOf` which performs strict equality - * comparisons of values, i.e. `===`. - * - * @private - * @param {Array} array The array to inspect. - * @param {*} value The value to search for. - * @param {number} fromIndex The index to search from. - * @returns {number} Returns the index of the matched value, else `-1`. - */ - function strictIndexOf(array, value, fromIndex) { - var index = fromIndex - 1, - length = array.length; - - while (++index < length) { - if (array[index] === value) { - return index; - } - } - return -1; - } - - /** - * A specialized version of `_.lastIndexOf` which performs strict equality - * comparisons of values, i.e. `===`. - * - * @private - * @param {Array} array The array to inspect. - * @param {*} value The value to search for. - * @param {number} fromIndex The index to search from. - * @returns {number} Returns the index of the matched value, else `-1`. - */ - function strictLastIndexOf(array, value, fromIndex) { - var index = fromIndex + 1; - while (index--) { - if (array[index] === value) { - return index; - } - } - return index; - } - - /** - * Gets the number of symbols in `string`. - * - * @private - * @param {string} string The string to inspect. - * @returns {number} Returns the string size. - */ - function stringSize(string) { - return hasUnicode(string) - ? unicodeSize(string) - : asciiSize(string); - } - - /** - * Converts `string` to an array. - * - * @private - * @param {string} string The string to convert. - * @returns {Array} Returns the converted array. - */ - function stringToArray(string) { - return hasUnicode(string) - ? unicodeToArray(string) - : asciiToArray(string); - } - - /** - * Used by `_.unescape` to convert HTML entities to characters. - * - * @private - * @param {string} chr The matched character to unescape. - * @returns {string} Returns the unescaped character. - */ - var unescapeHtmlChar = basePropertyOf(htmlUnescapes); - - /** - * Gets the size of a Unicode `string`. - * - * @private - * @param {string} string The string inspect. - * @returns {number} Returns the string size. - */ - function unicodeSize(string) { - var result = reUnicode.lastIndex = 0; - while (reUnicode.test(string)) { - ++result; - } - return result; - } - - /** - * Converts a Unicode `string` to an array. - * - * @private - * @param {string} string The string to convert. - * @returns {Array} Returns the converted array. - */ - function unicodeToArray(string) { - return string.match(reUnicode) || []; - } - - /** - * Splits a Unicode `string` into an array of its words. - * - * @private - * @param {string} The string to inspect. - * @returns {Array} Returns the words of `string`. - */ - function unicodeWords(string) { - return string.match(reUnicodeWord) || []; - } - - /*--------------------------------------------------------------------------*/ - - /** - * Create a new pristine `lodash` function using the `context` object. - * - * @static - * @memberOf _ - * @since 1.1.0 - * @category Util - * @param {Object} [context=root] The context object. - * @returns {Function} Returns a new `lodash` function. - * @example - * - * _.mixin({ 'foo': _.constant('foo') }); - * - * var lodash = _.runInContext(); - * lodash.mixin({ 'bar': lodash.constant('bar') }); - * - * _.isFunction(_.foo); - * // => true - * _.isFunction(_.bar); - * // => false - * - * lodash.isFunction(lodash.foo); - * // => false - * lodash.isFunction(lodash.bar); - * // => true - * - * // Create a suped-up `defer` in Node.js. - * var defer = _.runInContext({ 'setTimeout': setImmediate }).defer; - */ - var runInContext = (function runInContext(context) { - context = context == null ? root : _.defaults(root.Object(), context, _.pick(root, contextProps)); - - /** Built-in constructor references. */ - var Array = context.Array, - Date = context.Date, - Error = context.Error, - Function = context.Function, - Math = context.Math, - Object = context.Object, - RegExp = context.RegExp, - String = context.String, - TypeError = context.TypeError; - - /** Used for built-in method references. */ - var arrayProto = Array.prototype, - funcProto = Function.prototype, - objectProto = Object.prototype; - - /** Used to detect overreaching core-js shims. */ - var coreJsData = context['__core-js_shared__']; - - /** Used to resolve the decompiled source of functions. */ - var funcToString = funcProto.toString; - - /** Used to check objects for own properties. */ - var hasOwnProperty = objectProto.hasOwnProperty; - - /** Used to generate unique IDs. */ - var idCounter = 0; - - /** Used to detect methods masquerading as native. */ - var maskSrcKey = (function() { - var uid = /[^.]+$/.exec(coreJsData && coreJsData.keys && coreJsData.keys.IE_PROTO || ''); - return uid ? ('Symbol(src)_1.' + uid) : ''; - }()); - - /** - * Used to resolve the - * [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring) - * of values. - */ - var nativeObjectToString = objectProto.toString; - - /** Used to infer the `Object` constructor. */ - var objectCtorString = funcToString.call(Object); - - /** Used to restore the original `_` reference in `_.noConflict`. */ - var oldDash = root._; - - /** Used to detect if a method is native. */ - var reIsNative = RegExp('^' + - funcToString.call(hasOwnProperty).replace(reRegExpChar, '\\$&') - .replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g, '$1.*?') + '$' - ); - - /** Built-in value references. */ - var Buffer = moduleExports ? context.Buffer : undefined, - Symbol = context.Symbol, - Uint8Array = context.Uint8Array, - allocUnsafe = Buffer ? Buffer.allocUnsafe : undefined, - getPrototype = overArg(Object.getPrototypeOf, Object), - objectCreate = Object.create, - propertyIsEnumerable = objectProto.propertyIsEnumerable, - splice = arrayProto.splice, - spreadableSymbol = Symbol ? Symbol.isConcatSpreadable : undefined, - symIterator = Symbol ? Symbol.iterator : undefined, - symToStringTag = Symbol ? Symbol.toStringTag : undefined; - - var defineProperty = (function() { - try { - var func = getNative(Object, 'defineProperty'); - func({}, '', {}); - return func; - } catch (e) {} - }()); - - /** Mocked built-ins. */ - var ctxClearTimeout = context.clearTimeout !== root.clearTimeout && context.clearTimeout, - ctxNow = Date && Date.now !== root.Date.now && Date.now, - ctxSetTimeout = context.setTimeout !== root.setTimeout && context.setTimeout; - - /* Built-in method references for those with the same name as other `lodash` methods. */ - var nativeCeil = Math.ceil, - nativeFloor = Math.floor, - nativeGetSymbols = Object.getOwnPropertySymbols, - nativeIsBuffer = Buffer ? Buffer.isBuffer : undefined, - nativeIsFinite = context.isFinite, - nativeJoin = arrayProto.join, - nativeKeys = overArg(Object.keys, Object), - nativeMax = Math.max, - nativeMin = Math.min, - nativeNow = Date.now, - nativeParseInt = context.parseInt, - nativeRandom = Math.random, - nativeReverse = arrayProto.reverse; - - /* Built-in method references that are verified to be native. */ - var DataView = getNative(context, 'DataView'), - Map = getNative(context, 'Map'), - Promise = getNative(context, 'Promise'), - Set = getNative(context, 'Set'), - WeakMap = getNative(context, 'WeakMap'), - nativeCreate = getNative(Object, 'create'); - - /** Used to store function metadata. */ - var metaMap = WeakMap && new WeakMap; - - /** Used to lookup unminified function names. */ - var realNames = {}; - - /** Used to detect maps, sets, and weakmaps. */ - var dataViewCtorString = toSource(DataView), - mapCtorString = toSource(Map), - promiseCtorString = toSource(Promise), - setCtorString = toSource(Set), - weakMapCtorString = toSource(WeakMap); - - /** Used to convert symbols to primitives and strings. */ - var symbolProto = Symbol ? Symbol.prototype : undefined, - symbolValueOf = symbolProto ? symbolProto.valueOf : undefined, - symbolToString = symbolProto ? symbolProto.toString : undefined; - - /*------------------------------------------------------------------------*/ - - /** - * Creates a `lodash` object which wraps `value` to enable implicit method - * chain sequences. Methods that operate on and return arrays, collections, - * and functions can be chained together. Methods that retrieve a single value - * or may return a primitive value will automatically end the chain sequence - * and return the unwrapped value. Otherwise, the value must be unwrapped - * with `_#value`. - * - * Explicit chain sequences, which must be unwrapped with `_#value`, may be - * enabled using `_.chain`. - * - * The execution of chained methods is lazy, that is, it's deferred until - * `_#value` is implicitly or explicitly called. - * - * Lazy evaluation allows several methods to support shortcut fusion. - * Shortcut fusion is an optimization to merge iteratee calls; this avoids - * the creation of intermediate arrays and can greatly reduce the number of - * iteratee executions. Sections of a chain sequence qualify for shortcut - * fusion if the section is applied to an array and iteratees accept only - * one argument. The heuristic for whether a section qualifies for shortcut - * fusion is subject to change. - * - * Chaining is supported in custom builds as long as the `_#value` method is - * directly or indirectly included in the build. - * - * In addition to lodash methods, wrappers have `Array` and `String` methods. - * - * The wrapper `Array` methods are: - * `concat`, `join`, `pop`, `push`, `shift`, `sort`, `splice`, and `unshift` - * - * The wrapper `String` methods are: - * `replace` and `split` - * - * The wrapper methods that support shortcut fusion are: - * `at`, `compact`, `drop`, `dropRight`, `dropWhile`, `filter`, `find`, - * `findLast`, `head`, `initial`, `last`, `map`, `reject`, `reverse`, `slice`, - * `tail`, `take`, `takeRight`, `takeRightWhile`, `takeWhile`, and `toArray` - * - * The chainable wrapper methods are: - * `after`, `ary`, `assign`, `assignIn`, `assignInWith`, `assignWith`, `at`, - * `before`, `bind`, `bindAll`, `bindKey`, `castArray`, `chain`, `chunk`, - * `commit`, `compact`, `concat`, `conforms`, `constant`, `countBy`, `create`, - * `curry`, `debounce`, `defaults`, `defaultsDeep`, `defer`, `delay`, - * `difference`, `differenceBy`, `differenceWith`, `drop`, `dropRight`, - * `dropRightWhile`, `dropWhile`, `extend`, `extendWith`, `fill`, `filter`, - * `flatMap`, `flatMapDeep`, `flatMapDepth`, `flatten`, `flattenDeep`, - * `flattenDepth`, `flip`, `flow`, `flowRight`, `fromPairs`, `functions`, - * `functionsIn`, `groupBy`, `initial`, `intersection`, `intersectionBy`, - * `intersectionWith`, `invert`, `invertBy`, `invokeMap`, `iteratee`, `keyBy`, - * `keys`, `keysIn`, `map`, `mapKeys`, `mapValues`, `matches`, `matchesProperty`, - * `memoize`, `merge`, `mergeWith`, `method`, `methodOf`, `mixin`, `negate`, - * `nthArg`, `omit`, `omitBy`, `once`, `orderBy`, `over`, `overArgs`, - * `overEvery`, `overSome`, `partial`, `partialRight`, `partition`, `pick`, - * `pickBy`, `plant`, `property`, `propertyOf`, `pull`, `pullAll`, `pullAllBy`, - * `pullAllWith`, `pullAt`, `push`, `range`, `rangeRight`, `rearg`, `reject`, - * `remove`, `rest`, `reverse`, `sampleSize`, `set`, `setWith`, `shuffle`, - * `slice`, `sort`, `sortBy`, `splice`, `spread`, `tail`, `take`, `takeRight`, - * `takeRightWhile`, `takeWhile`, `tap`, `throttle`, `thru`, `toArray`, - * `toPairs`, `toPairsIn`, `toPath`, `toPlainObject`, `transform`, `unary`, - * `union`, `unionBy`, `unionWith`, `uniq`, `uniqBy`, `uniqWith`, `unset`, - * `unshift`, `unzip`, `unzipWith`, `update`, `updateWith`, `values`, - * `valuesIn`, `without`, `wrap`, `xor`, `xorBy`, `xorWith`, `zip`, - * `zipObject`, `zipObjectDeep`, and `zipWith` - * - * The wrapper methods that are **not** chainable by default are: - * `add`, `attempt`, `camelCase`, `capitalize`, `ceil`, `clamp`, `clone`, - * `cloneDeep`, `cloneDeepWith`, `cloneWith`, `conformsTo`, `deburr`, - * `defaultTo`, `divide`, `each`, `eachRight`, `endsWith`, `eq`, `escape`, - * `escapeRegExp`, `every`, `find`, `findIndex`, `findKey`, `findLast`, - * `findLastIndex`, `findLastKey`, `first`, `floor`, `forEach`, `forEachRight`, - * `forIn`, `forInRight`, `forOwn`, `forOwnRight`, `get`, `gt`, `gte`, `has`, - * `hasIn`, `head`, `identity`, `includes`, `indexOf`, `inRange`, `invoke`, - * `isArguments`, `isArray`, `isArrayBuffer`, `isArrayLike`, `isArrayLikeObject`, - * `isBoolean`, `isBuffer`, `isDate`, `isElement`, `isEmpty`, `isEqual`, - * `isEqualWith`, `isError`, `isFinite`, `isFunction`, `isInteger`, `isLength`, - * `isMap`, `isMatch`, `isMatchWith`, `isNaN`, `isNative`, `isNil`, `isNull`, - * `isNumber`, `isObject`, `isObjectLike`, `isPlainObject`, `isRegExp`, - * `isSafeInteger`, `isSet`, `isString`, `isUndefined`, `isTypedArray`, - * `isWeakMap`, `isWeakSet`, `join`, `kebabCase`, `last`, `lastIndexOf`, - * `lowerCase`, `lowerFirst`, `lt`, `lte`, `max`, `maxBy`, `mean`, `meanBy`, - * `min`, `minBy`, `multiply`, `noConflict`, `noop`, `now`, `nth`, `pad`, - * `padEnd`, `padStart`, `parseInt`, `pop`, `random`, `reduce`, `reduceRight`, - * `repeat`, `result`, `round`, `runInContext`, `sample`, `shift`, `size`, - * `snakeCase`, `some`, `sortedIndex`, `sortedIndexBy`, `sortedLastIndex`, - * `sortedLastIndexBy`, `startCase`, `startsWith`, `stubArray`, `stubFalse`, - * `stubObject`, `stubString`, `stubTrue`, `subtract`, `sum`, `sumBy`, - * `template`, `times`, `toFinite`, `toInteger`, `toJSON`, `toLength`, - * `toLower`, `toNumber`, `toSafeInteger`, `toString`, `toUpper`, `trim`, - * `trimEnd`, `trimStart`, `truncate`, `unescape`, `uniqueId`, `upperCase`, - * `upperFirst`, `value`, and `words` - * - * @name _ - * @constructor - * @category Seq - * @param {*} value The value to wrap in a `lodash` instance. - * @returns {Object} Returns the new `lodash` wrapper instance. - * @example - * - * function square(n) { - * return n * n; - * } - * - * var wrapped = _([1, 2, 3]); - * - * // Returns an unwrapped value. - * wrapped.reduce(_.add); - * // => 6 - * - * // Returns a wrapped value. - * var squares = wrapped.map(square); - * - * _.isArray(squares); - * // => false - * - * _.isArray(squares.value()); - * // => true - */ - function lodash(value) { - if (isObjectLike(value) && !isArray(value) && !(value instanceof LazyWrapper)) { - if (value instanceof LodashWrapper) { - return value; - } - if (hasOwnProperty.call(value, '__wrapped__')) { - return wrapperClone(value); - } - } - return new LodashWrapper(value); - } - - /** - * The base implementation of `_.create` without support for assigning - * properties to the created object. - * - * @private - * @param {Object} proto The object to inherit from. - * @returns {Object} Returns the new object. - */ - var baseCreate = (function() { - function object() {} - return function(proto) { - if (!isObject(proto)) { - return {}; - } - if (objectCreate) { - return objectCreate(proto); - } - object.prototype = proto; - var result = new object; - object.prototype = undefined; - return result; - }; - }()); - - /** - * The function whose prototype chain sequence wrappers inherit from. - * - * @private - */ - function baseLodash() { - // No operation performed. - } - - /** - * The base constructor for creating `lodash` wrapper objects. - * - * @private - * @param {*} value The value to wrap. - * @param {boolean} [chainAll] Enable explicit method chain sequences. - */ - function LodashWrapper(value, chainAll) { - this.__wrapped__ = value; - this.__actions__ = []; - this.__chain__ = !!chainAll; - this.__index__ = 0; - this.__values__ = undefined; - } - - /** - * By default, the template delimiters used by lodash are like those in - * embedded Ruby (ERB) as well as ES2015 template strings. Change the - * following template settings to use alternative delimiters. - * - * @static - * @memberOf _ - * @type {Object} - */ - lodash.templateSettings = { - - /** - * Used to detect `data` property values to be HTML-escaped. - * - * @memberOf _.templateSettings - * @type {RegExp} - */ - 'escape': reEscape, - - /** - * Used to detect code to be evaluated. - * - * @memberOf _.templateSettings - * @type {RegExp} - */ - 'evaluate': reEvaluate, - - /** - * Used to detect `data` property values to inject. - * - * @memberOf _.templateSettings - * @type {RegExp} - */ - 'interpolate': reInterpolate, - - /** - * Used to reference the data object in the template text. - * - * @memberOf _.templateSettings - * @type {string} - */ - 'variable': '', - - /** - * Used to import variables into the compiled template. - * - * @memberOf _.templateSettings - * @type {Object} - */ - 'imports': { - - /** - * A reference to the `lodash` function. - * - * @memberOf _.templateSettings.imports - * @type {Function} - */ - '_': lodash - } - }; - - // Ensure wrappers are instances of `baseLodash`. - lodash.prototype = baseLodash.prototype; - lodash.prototype.constructor = lodash; - - LodashWrapper.prototype = baseCreate(baseLodash.prototype); - LodashWrapper.prototype.constructor = LodashWrapper; - - /*------------------------------------------------------------------------*/ - - /** - * Creates a lazy wrapper object which wraps `value` to enable lazy evaluation. - * - * @private - * @constructor - * @param {*} value The value to wrap. - */ - function LazyWrapper(value) { - this.__wrapped__ = value; - this.__actions__ = []; - this.__dir__ = 1; - this.__filtered__ = false; - this.__iteratees__ = []; - this.__takeCount__ = MAX_ARRAY_LENGTH; - this.__views__ = []; - } - - /** - * Creates a clone of the lazy wrapper object. - * - * @private - * @name clone - * @memberOf LazyWrapper - * @returns {Object} Returns the cloned `LazyWrapper` object. - */ - function lazyClone() { - var result = new LazyWrapper(this.__wrapped__); - result.__actions__ = copyArray(this.__actions__); - result.__dir__ = this.__dir__; - result.__filtered__ = this.__filtered__; - result.__iteratees__ = copyArray(this.__iteratees__); - result.__takeCount__ = this.__takeCount__; - result.__views__ = copyArray(this.__views__); - return result; - } - - /** - * Reverses the direction of lazy iteration. - * - * @private - * @name reverse - * @memberOf LazyWrapper - * @returns {Object} Returns the new reversed `LazyWrapper` object. - */ - function lazyReverse() { - if (this.__filtered__) { - var result = new LazyWrapper(this); - result.__dir__ = -1; - result.__filtered__ = true; - } else { - result = this.clone(); - result.__dir__ *= -1; - } - return result; - } - - /** - * Extracts the unwrapped value from its lazy wrapper. - * - * @private - * @name value - * @memberOf LazyWrapper - * @returns {*} Returns the unwrapped value. - */ - function lazyValue() { - var array = this.__wrapped__.value(), - dir = this.__dir__, - isArr = isArray(array), - isRight = dir < 0, - arrLength = isArr ? array.length : 0, - view = getView(0, arrLength, this.__views__), - start = view.start, - end = view.end, - length = end - start, - index = isRight ? end : (start - 1), - iteratees = this.__iteratees__, - iterLength = iteratees.length, - resIndex = 0, - takeCount = nativeMin(length, this.__takeCount__); - - if (!isArr || (!isRight && arrLength == length && takeCount == length)) { - return baseWrapperValue(array, this.__actions__); - } - var result = []; - - outer: - while (length-- && resIndex < takeCount) { - index += dir; - - var iterIndex = -1, - value = array[index]; - - while (++iterIndex < iterLength) { - var data = iteratees[iterIndex], - iteratee = data.iteratee, - type = data.type, - computed = iteratee(value); - - if (type == LAZY_MAP_FLAG) { - value = computed; - } else if (!computed) { - if (type == LAZY_FILTER_FLAG) { - continue outer; - } else { - break outer; - } - } - } - result[resIndex++] = value; - } - return result; - } - - // Ensure `LazyWrapper` is an instance of `baseLodash`. - LazyWrapper.prototype = baseCreate(baseLodash.prototype); - LazyWrapper.prototype.constructor = LazyWrapper; - - /*------------------------------------------------------------------------*/ - - /** - * Creates a hash object. - * - * @private - * @constructor - * @param {Array} [entries] The key-value pairs to cache. - */ - function Hash(entries) { - var index = -1, - length = entries == null ? 0 : entries.length; - - this.clear(); - while (++index < length) { - var entry = entries[index]; - this.set(entry[0], entry[1]); - } - } - - /** - * Removes all key-value entries from the hash. - * - * @private - * @name clear - * @memberOf Hash - */ - function hashClear() { - this.__data__ = nativeCreate ? nativeCreate(null) : {}; - this.size = 0; - } - - /** - * Removes `key` and its value from the hash. - * - * @private - * @name delete - * @memberOf Hash - * @param {Object} hash The hash to modify. - * @param {string} key The key of the value to remove. - * @returns {boolean} Returns `true` if the entry was removed, else `false`. - */ - function hashDelete(key) { - var result = this.has(key) && delete this.__data__[key]; - this.size -= result ? 1 : 0; - return result; - } - - /** - * Gets the hash value for `key`. - * - * @private - * @name get - * @memberOf Hash - * @param {string} key The key of the value to get. - * @returns {*} Returns the entry value. - */ - function hashGet(key) { - var data = this.__data__; - if (nativeCreate) { - var result = data[key]; - return result === HASH_UNDEFINED ? undefined : result; - } - return hasOwnProperty.call(data, key) ? data[key] : undefined; - } - - /** - * Checks if a hash value for `key` exists. - * - * @private - * @name has - * @memberOf Hash - * @param {string} key The key of the entry to check. - * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`. - */ - function hashHas(key) { - var data = this.__data__; - return nativeCreate ? (data[key] !== undefined) : hasOwnProperty.call(data, key); - } - - /** - * Sets the hash `key` to `value`. - * - * @private - * @name set - * @memberOf Hash - * @param {string} key The key of the value to set. - * @param {*} value The value to set. - * @returns {Object} Returns the hash instance. - */ - function hashSet(key, value) { - var data = this.__data__; - this.size += this.has(key) ? 0 : 1; - data[key] = (nativeCreate && value === undefined) ? HASH_UNDEFINED : value; - return this; - } - - // Add methods to `Hash`. - Hash.prototype.clear = hashClear; - Hash.prototype['delete'] = hashDelete; - Hash.prototype.get = hashGet; - Hash.prototype.has = hashHas; - Hash.prototype.set = hashSet; - - /*------------------------------------------------------------------------*/ - - /** - * Creates an list cache object. - * - * @private - * @constructor - * @param {Array} [entries] The key-value pairs to cache. - */ - function ListCache(entries) { - var index = -1, - length = entries == null ? 0 : entries.length; - - this.clear(); - while (++index < length) { - var entry = entries[index]; - this.set(entry[0], entry[1]); - } - } - - /** - * Removes all key-value entries from the list cache. - * - * @private - * @name clear - * @memberOf ListCache - */ - function listCacheClear() { - this.__data__ = []; - this.size = 0; - } - - /** - * Removes `key` and its value from the list cache. - * - * @private - * @name delete - * @memberOf ListCache - * @param {string} key The key of the value to remove. - * @returns {boolean} Returns `true` if the entry was removed, else `false`. - */ - function listCacheDelete(key) { - var data = this.__data__, - index = assocIndexOf(data, key); - - if (index < 0) { - return false; - } - var lastIndex = data.length - 1; - if (index == lastIndex) { - data.pop(); - } else { - splice.call(data, index, 1); - } - --this.size; - return true; - } - - /** - * Gets the list cache value for `key`. - * - * @private - * @name get - * @memberOf ListCache - * @param {string} key The key of the value to get. - * @returns {*} Returns the entry value. - */ - function listCacheGet(key) { - var data = this.__data__, - index = assocIndexOf(data, key); - - return index < 0 ? undefined : data[index][1]; - } - - /** - * Checks if a list cache value for `key` exists. - * - * @private - * @name has - * @memberOf ListCache - * @param {string} key The key of the entry to check. - * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`. - */ - function listCacheHas(key) { - return assocIndexOf(this.__data__, key) > -1; - } - - /** - * Sets the list cache `key` to `value`. - * - * @private - * @name set - * @memberOf ListCache - * @param {string} key The key of the value to set. - * @param {*} value The value to set. - * @returns {Object} Returns the list cache instance. - */ - function listCacheSet(key, value) { - var data = this.__data__, - index = assocIndexOf(data, key); - - if (index < 0) { - ++this.size; - data.push([key, value]); - } else { - data[index][1] = value; - } - return this; - } - - // Add methods to `ListCache`. - ListCache.prototype.clear = listCacheClear; - ListCache.prototype['delete'] = listCacheDelete; - ListCache.prototype.get = listCacheGet; - ListCache.prototype.has = listCacheHas; - ListCache.prototype.set = listCacheSet; - - /*------------------------------------------------------------------------*/ - - /** - * Creates a map cache object to store key-value pairs. - * - * @private - * @constructor - * @param {Array} [entries] The key-value pairs to cache. - */ - function MapCache(entries) { - var index = -1, - length = entries == null ? 0 : entries.length; - - this.clear(); - while (++index < length) { - var entry = entries[index]; - this.set(entry[0], entry[1]); - } - } - - /** - * Removes all key-value entries from the map. - * - * @private - * @name clear - * @memberOf MapCache - */ - function mapCacheClear() { - this.size = 0; - this.__data__ = { - 'hash': new Hash, - 'map': new (Map || ListCache), - 'string': new Hash - }; - } - - /** - * Removes `key` and its value from the map. - * - * @private - * @name delete - * @memberOf MapCache - * @param {string} key The key of the value to remove. - * @returns {boolean} Returns `true` if the entry was removed, else `false`. - */ - function mapCacheDelete(key) { - var result = getMapData(this, key)['delete'](key); - this.size -= result ? 1 : 0; - return result; - } - - /** - * Gets the map value for `key`. - * - * @private - * @name get - * @memberOf MapCache - * @param {string} key The key of the value to get. - * @returns {*} Returns the entry value. - */ - function mapCacheGet(key) { - return getMapData(this, key).get(key); - } - - /** - * Checks if a map value for `key` exists. - * - * @private - * @name has - * @memberOf MapCache - * @param {string} key The key of the entry to check. - * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`. - */ - function mapCacheHas(key) { - return getMapData(this, key).has(key); - } - - /** - * Sets the map `key` to `value`. - * - * @private - * @name set - * @memberOf MapCache - * @param {string} key The key of the value to set. - * @param {*} value The value to set. - * @returns {Object} Returns the map cache instance. - */ - function mapCacheSet(key, value) { - var data = getMapData(this, key), - size = data.size; - - data.set(key, value); - this.size += data.size == size ? 0 : 1; - return this; - } - - // Add methods to `MapCache`. - MapCache.prototype.clear = mapCacheClear; - MapCache.prototype['delete'] = mapCacheDelete; - MapCache.prototype.get = mapCacheGet; - MapCache.prototype.has = mapCacheHas; - MapCache.prototype.set = mapCacheSet; - - /*------------------------------------------------------------------------*/ - - /** - * - * Creates an array cache object to store unique values. - * - * @private - * @constructor - * @param {Array} [values] The values to cache. - */ - function SetCache(values) { - var index = -1, - length = values == null ? 0 : values.length; - - this.__data__ = new MapCache; - while (++index < length) { - this.add(values[index]); - } - } - - /** - * Adds `value` to the array cache. - * - * @private - * @name add - * @memberOf SetCache - * @alias push - * @param {*} value The value to cache. - * @returns {Object} Returns the cache instance. - */ - function setCacheAdd(value) { - this.__data__.set(value, HASH_UNDEFINED); - return this; - } - - /** - * Checks if `value` is in the array cache. - * - * @private - * @name has - * @memberOf SetCache - * @param {*} value The value to search for. - * @returns {number} Returns `true` if `value` is found, else `false`. - */ - function setCacheHas(value) { - return this.__data__.has(value); - } - - // Add methods to `SetCache`. - SetCache.prototype.add = SetCache.prototype.push = setCacheAdd; - SetCache.prototype.has = setCacheHas; - - /*------------------------------------------------------------------------*/ - - /** - * Creates a stack cache object to store key-value pairs. - * - * @private - * @constructor - * @param {Array} [entries] The key-value pairs to cache. - */ - function Stack(entries) { - var data = this.__data__ = new ListCache(entries); - this.size = data.size; - } - - /** - * Removes all key-value entries from the stack. - * - * @private - * @name clear - * @memberOf Stack - */ - function stackClear() { - this.__data__ = new ListCache; - this.size = 0; - } - - /** - * Removes `key` and its value from the stack. - * - * @private - * @name delete - * @memberOf Stack - * @param {string} key The key of the value to remove. - * @returns {boolean} Returns `true` if the entry was removed, else `false`. - */ - function stackDelete(key) { - var data = this.__data__, - result = data['delete'](key); - - this.size = data.size; - return result; - } - - /** - * Gets the stack value for `key`. - * - * @private - * @name get - * @memberOf Stack - * @param {string} key The key of the value to get. - * @returns {*} Returns the entry value. - */ - function stackGet(key) { - return this.__data__.get(key); - } - - /** - * Checks if a stack value for `key` exists. - * - * @private - * @name has - * @memberOf Stack - * @param {string} key The key of the entry to check. - * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`. - */ - function stackHas(key) { - return this.__data__.has(key); - } - - /** - * Sets the stack `key` to `value`. - * - * @private - * @name set - * @memberOf Stack - * @param {string} key The key of the value to set. - * @param {*} value The value to set. - * @returns {Object} Returns the stack cache instance. - */ - function stackSet(key, value) { - var data = this.__data__; - if (data instanceof ListCache) { - var pairs = data.__data__; - if (!Map || (pairs.length < LARGE_ARRAY_SIZE - 1)) { - pairs.push([key, value]); - this.size = ++data.size; - return this; - } - data = this.__data__ = new MapCache(pairs); - } - data.set(key, value); - this.size = data.size; - return this; - } - - // Add methods to `Stack`. - Stack.prototype.clear = stackClear; - Stack.prototype['delete'] = stackDelete; - Stack.prototype.get = stackGet; - Stack.prototype.has = stackHas; - Stack.prototype.set = stackSet; - - /*------------------------------------------------------------------------*/ - - /** - * Creates an array of the enumerable property names of the array-like `value`. - * - * @private - * @param {*} value The value to query. - * @param {boolean} inherited Specify returning inherited property names. - * @returns {Array} Returns the array of property names. - */ - function arrayLikeKeys(value, inherited) { - var isArr = isArray(value), - isArg = !isArr && isArguments(value), - isBuff = !isArr && !isArg && isBuffer(value), - isType = !isArr && !isArg && !isBuff && isTypedArray(value), - skipIndexes = isArr || isArg || isBuff || isType, - result = skipIndexes ? baseTimes(value.length, String) : [], - length = result.length; - - for (var key in value) { - if ((inherited || hasOwnProperty.call(value, key)) && - !(skipIndexes && ( - // Safari 9 has enumerable `arguments.length` in strict mode. - key == 'length' || - // Node.js 0.10 has enumerable non-index properties on buffers. - (isBuff && (key == 'offset' || key == 'parent')) || - // PhantomJS 2 has enumerable non-index properties on typed arrays. - (isType && (key == 'buffer' || key == 'byteLength' || key == 'byteOffset')) || - // Skip index properties. - isIndex(key, length) - ))) { - result.push(key); - } - } - return result; - } - - /** - * A specialized version of `_.sample` for arrays. - * - * @private - * @param {Array} array The array to sample. - * @returns {*} Returns the random element. - */ - function arraySample(array) { - var length = array.length; - return length ? array[baseRandom(0, length - 1)] : undefined; - } - - /** - * A specialized version of `_.sampleSize` for arrays. - * - * @private - * @param {Array} array The array to sample. - * @param {number} n The number of elements to sample. - * @returns {Array} Returns the random elements. - */ - function arraySampleSize(array, n) { - return shuffleSelf(copyArray(array), baseClamp(n, 0, array.length)); - } - - /** - * A specialized version of `_.shuffle` for arrays. - * - * @private - * @param {Array} array The array to shuffle. - * @returns {Array} Returns the new shuffled array. - */ - function arrayShuffle(array) { - return shuffleSelf(copyArray(array)); - } - - /** - * This function is like `assignValue` except that it doesn't assign - * `undefined` values. - * - * @private - * @param {Object} object The object to modify. - * @param {string} key The key of the property to assign. - * @param {*} value The value to assign. - */ - function assignMergeValue(object, key, value) { - if ((value !== undefined && !eq(object[key], value)) || - (value === undefined && !(key in object))) { - baseAssignValue(object, key, value); - } - } - - /** - * Assigns `value` to `key` of `object` if the existing value is not equivalent - * using [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero) - * for equality comparisons. - * - * @private - * @param {Object} object The object to modify. - * @param {string} key The key of the property to assign. - * @param {*} value The value to assign. - */ - function assignValue(object, key, value) { - var objValue = object[key]; - if (!(hasOwnProperty.call(object, key) && eq(objValue, value)) || - (value === undefined && !(key in object))) { - baseAssignValue(object, key, value); - } - } - - /** - * Gets the index at which the `key` is found in `array` of key-value pairs. - * - * @private - * @param {Array} array The array to inspect. - * @param {*} key The key to search for. - * @returns {number} Returns the index of the matched value, else `-1`. - */ - function assocIndexOf(array, key) { - var length = array.length; - while (length--) { - if (eq(array[length][0], key)) { - return length; - } - } - return -1; - } - - /** - * Aggregates elements of `collection` on `accumulator` with keys transformed - * by `iteratee` and values set by `setter`. - * - * @private - * @param {Array|Object} collection The collection to iterate over. - * @param {Function} setter The function to set `accumulator` values. - * @param {Function} iteratee The iteratee to transform keys. - * @param {Object} accumulator The initial aggregated object. - * @returns {Function} Returns `accumulator`. - */ - function baseAggregator(collection, setter, iteratee, accumulator) { - baseEach(collection, function(value, key, collection) { - setter(accumulator, value, iteratee(value), collection); - }); - return accumulator; - } - - /** - * The base implementation of `_.assign` without support for multiple sources - * or `customizer` functions. - * - * @private - * @param {Object} object The destination object. - * @param {Object} source The source object. - * @returns {Object} Returns `object`. - */ - function baseAssign(object, source) { - return object && copyObject(source, keys(source), object); - } - - /** - * The base implementation of `_.assignIn` without support for multiple sources - * or `customizer` functions. - * - * @private - * @param {Object} object The destination object. - * @param {Object} source The source object. - * @returns {Object} Returns `object`. - */ - function baseAssignIn(object, source) { - return object && copyObject(source, keysIn(source), object); - } - - /** - * The base implementation of `assignValue` and `assignMergeValue` without - * value checks. - * - * @private - * @param {Object} object The object to modify. - * @param {string} key The key of the property to assign. - * @param {*} value The value to assign. - */ - function baseAssignValue(object, key, value) { - if (key == '__proto__' && defineProperty) { - defineProperty(object, key, { - 'configurable': true, - 'enumerable': true, - 'value': value, - 'writable': true - }); - } else { - object[key] = value; - } - } - - /** - * The base implementation of `_.at` without support for individual paths. - * - * @private - * @param {Object} object The object to iterate over. - * @param {string[]} paths The property paths to pick. - * @returns {Array} Returns the picked elements. - */ - function baseAt(object, paths) { - var index = -1, - length = paths.length, - result = Array(length), - skip = object == null; - - while (++index < length) { - result[index] = skip ? undefined : get(object, paths[index]); - } - return result; - } - - /** - * The base implementation of `_.clamp` which doesn't coerce arguments. - * - * @private - * @param {number} number The number to clamp. - * @param {number} [lower] The lower bound. - * @param {number} upper The upper bound. - * @returns {number} Returns the clamped number. - */ - function baseClamp(number, lower, upper) { - if (number === number) { - if (upper !== undefined) { - number = number <= upper ? number : upper; - } - if (lower !== undefined) { - number = number >= lower ? number : lower; - } - } - return number; - } - - /** - * The base implementation of `_.clone` and `_.cloneDeep` which tracks - * traversed objects. - * - * @private - * @param {*} value The value to clone. - * @param {boolean} bitmask The bitmask flags. - * 1 - Deep clone - * 2 - Flatten inherited properties - * 4 - Clone symbols - * @param {Function} [customizer] The function to customize cloning. - * @param {string} [key] The key of `value`. - * @param {Object} [object] The parent object of `value`. - * @param {Object} [stack] Tracks traversed objects and their clone counterparts. - * @returns {*} Returns the cloned value. - */ - function baseClone(value, bitmask, customizer, key, object, stack) { - var result, - isDeep = bitmask & CLONE_DEEP_FLAG, - isFlat = bitmask & CLONE_FLAT_FLAG, - isFull = bitmask & CLONE_SYMBOLS_FLAG; - - if (customizer) { - result = object ? customizer(value, key, object, stack) : customizer(value); - } - if (result !== undefined) { - return result; - } - if (!isObject(value)) { - return value; - } - var isArr = isArray(value); - if (isArr) { - result = initCloneArray(value); - if (!isDeep) { - return copyArray(value, result); - } - } else { - var tag = getTag(value), - isFunc = tag == funcTag || tag == genTag; - - if (isBuffer(value)) { - return cloneBuffer(value, isDeep); - } - if (tag == objectTag || tag == argsTag || (isFunc && !object)) { - result = (isFlat || isFunc) ? {} : initCloneObject(value); - if (!isDeep) { - return isFlat - ? copySymbolsIn(value, baseAssignIn(result, value)) - : copySymbols(value, baseAssign(result, value)); - } - } else { - if (!cloneableTags[tag]) { - return object ? value : {}; - } - result = initCloneByTag(value, tag, baseClone, isDeep); - } - } - // Check for circular references and return its corresponding clone. - stack || (stack = new Stack); - var stacked = stack.get(value); - if (stacked) { - return stacked; - } - stack.set(value, result); - - var keysFunc = isFull - ? (isFlat ? getAllKeysIn : getAllKeys) - : (isFlat ? keysIn : keys); - - var props = isArr ? undefined : keysFunc(value); - arrayEach(props || value, function(subValue, key) { - if (props) { - key = subValue; - subValue = value[key]; - } - // Recursively populate clone (susceptible to call stack limits). - assignValue(result, key, baseClone(subValue, bitmask, customizer, key, value, stack)); - }); - return result; - } - - /** - * The base implementation of `_.conforms` which doesn't clone `source`. - * - * @private - * @param {Object} source The object of property predicates to conform to. - * @returns {Function} Returns the new spec function. - */ - function baseConforms(source) { - var props = keys(source); - return function(object) { - return baseConformsTo(object, source, props); - }; - } - - /** - * The base implementation of `_.conformsTo` which accepts `props` to check. - * - * @private - * @param {Object} object The object to inspect. - * @param {Object} source The object of property predicates to conform to. - * @returns {boolean} Returns `true` if `object` conforms, else `false`. - */ - function baseConformsTo(object, source, props) { - var length = props.length; - if (object == null) { - return !length; - } - object = Object(object); - while (length--) { - var key = props[length], - predicate = source[key], - value = object[key]; - - if ((value === undefined && !(key in object)) || !predicate(value)) { - return false; - } - } - return true; - } - - /** - * The base implementation of `_.delay` and `_.defer` which accepts `args` - * to provide to `func`. - * - * @private - * @param {Function} func The function to delay. - * @param {number} wait The number of milliseconds to delay invocation. - * @param {Array} args The arguments to provide to `func`. - * @returns {number|Object} Returns the timer id or timeout object. - */ - function baseDelay(func, wait, args) { - if (typeof func != 'function') { - throw new TypeError(FUNC_ERROR_TEXT); - } - return setTimeout(function() { func.apply(undefined, args); }, wait); - } - - /** - * The base implementation of methods like `_.difference` without support - * for excluding multiple arrays or iteratee shorthands. - * - * @private - * @param {Array} array The array to inspect. - * @param {Array} values The values to exclude. - * @param {Function} [iteratee] The iteratee invoked per element. - * @param {Function} [comparator] The comparator invoked per element. - * @returns {Array} Returns the new array of filtered values. - */ - function baseDifference(array, values, iteratee, comparator) { - var index = -1, - includes = arrayIncludes, - isCommon = true, - length = array.length, - result = [], - valuesLength = values.length; - - if (!length) { - return result; - } - if (iteratee) { - values = arrayMap(values, baseUnary(iteratee)); - } - if (comparator) { - includes = arrayIncludesWith; - isCommon = false; - } - else if (values.length >= LARGE_ARRAY_SIZE) { - includes = cacheHas; - isCommon = false; - values = new SetCache(values); - } - outer: - while (++index < length) { - var value = array[index], - computed = iteratee == null ? value : iteratee(value); - - value = (comparator || value !== 0) ? value : 0; - if (isCommon && computed === computed) { - var valuesIndex = valuesLength; - while (valuesIndex--) { - if (values[valuesIndex] === computed) { - continue outer; - } - } - result.push(value); - } - else if (!includes(values, computed, comparator)) { - result.push(value); - } - } - return result; - } - - /** - * The base implementation of `_.forEach` without support for iteratee shorthands. - * - * @private - * @param {Array|Object} collection The collection to iterate over. - * @param {Function} iteratee The function invoked per iteration. - * @returns {Array|Object} Returns `collection`. - */ - var baseEach = createBaseEach(baseForOwn); - - /** - * The base implementation of `_.forEachRight` without support for iteratee shorthands. - * - * @private - * @param {Array|Object} collection The collection to iterate over. - * @param {Function} iteratee The function invoked per iteration. - * @returns {Array|Object} Returns `collection`. - */ - var baseEachRight = createBaseEach(baseForOwnRight, true); - - /** - * The base implementation of `_.every` without support for iteratee shorthands. - * - * @private - * @param {Array|Object} collection The collection to iterate over. - * @param {Function} predicate The function invoked per iteration. - * @returns {boolean} Returns `true` if all elements pass the predicate check, - * else `false` - */ - function baseEvery(collection, predicate) { - var result = true; - baseEach(collection, function(value, index, collection) { - result = !!predicate(value, index, collection); - return result; - }); - return result; - } - - /** - * The base implementation of methods like `_.max` and `_.min` which accepts a - * `comparator` to determine the extremum value. - * - * @private - * @param {Array} array The array to iterate over. - * @param {Function} iteratee The iteratee invoked per iteration. - * @param {Function} comparator The comparator used to compare values. - * @returns {*} Returns the extremum value. - */ - function baseExtremum(array, iteratee, comparator) { - var index = -1, - length = array.length; - - while (++index < length) { - var value = array[index], - current = iteratee(value); - - if (current != null && (computed === undefined - ? (current === current && !isSymbol(current)) - : comparator(current, computed) - )) { - var computed = current, - result = value; - } - } - return result; - } - - /** - * The base implementation of `_.fill` without an iteratee call guard. - * - * @private - * @param {Array} array The array to fill. - * @param {*} value The value to fill `array` with. - * @param {number} [start=0] The start position. - * @param {number} [end=array.length] The end position. - * @returns {Array} Returns `array`. - */ - function baseFill(array, value, start, end) { - var length = array.length; - - start = toInteger(start); - if (start < 0) { - start = -start > length ? 0 : (length + start); - } - end = (end === undefined || end > length) ? length : toInteger(end); - if (end < 0) { - end += length; - } - end = start > end ? 0 : toLength(end); - while (start < end) { - array[start++] = value; - } - return array; - } - - /** - * The base implementation of `_.filter` without support for iteratee shorthands. - * - * @private - * @param {Array|Object} collection The collection to iterate over. - * @param {Function} predicate The function invoked per iteration. - * @returns {Array} Returns the new filtered array. - */ - function baseFilter(collection, predicate) { - var result = []; - baseEach(collection, function(value, index, collection) { - if (predicate(value, index, collection)) { - result.push(value); - } - }); - return result; - } - - /** - * The base implementation of `_.flatten` with support for restricting flattening. - * - * @private - * @param {Array} array The array to flatten. - * @param {number} depth The maximum recursion depth. - * @param {boolean} [predicate=isFlattenable] The function invoked per iteration. - * @param {boolean} [isStrict] Restrict to values that pass `predicate` checks. - * @param {Array} [result=[]] The initial result value. - * @returns {Array} Returns the new flattened array. - */ - function baseFlatten(array, depth, predicate, isStrict, result) { - var index = -1, - length = array.length; - - predicate || (predicate = isFlattenable); - result || (result = []); - - while (++index < length) { - var value = array[index]; - if (depth > 0 && predicate(value)) { - if (depth > 1) { - // Recursively flatten arrays (susceptible to call stack limits). - baseFlatten(value, depth - 1, predicate, isStrict, result); - } else { - arrayPush(result, value); - } - } else if (!isStrict) { - result[result.length] = value; - } - } - return result; - } - - /** - * The base implementation of `baseForOwn` which iterates over `object` - * properties returned by `keysFunc` and invokes `iteratee` for each property. - * Iteratee functions may exit iteration early by explicitly returning `false`. - * - * @private - * @param {Object} object The object to iterate over. - * @param {Function} iteratee The function invoked per iteration. - * @param {Function} keysFunc The function to get the keys of `object`. - * @returns {Object} Returns `object`. - */ - var baseFor = createBaseFor(); - - /** - * This function is like `baseFor` except that it iterates over properties - * in the opposite order. - * - * @private - * @param {Object} object The object to iterate over. - * @param {Function} iteratee The function invoked per iteration. - * @param {Function} keysFunc The function to get the keys of `object`. - * @returns {Object} Returns `object`. - */ - var baseForRight = createBaseFor(true); - - /** - * The base implementation of `_.forOwn` without support for iteratee shorthands. - * - * @private - * @param {Object} object The object to iterate over. - * @param {Function} iteratee The function invoked per iteration. - * @returns {Object} Returns `object`. - */ - function baseForOwn(object, iteratee) { - return object && baseFor(object, iteratee, keys); - } - - /** - * The base implementation of `_.forOwnRight` without support for iteratee shorthands. - * - * @private - * @param {Object} object The object to iterate over. - * @param {Function} iteratee The function invoked per iteration. - * @returns {Object} Returns `object`. - */ - function baseForOwnRight(object, iteratee) { - return object && baseForRight(object, iteratee, keys); - } - - /** - * The base implementation of `_.functions` which creates an array of - * `object` function property names filtered from `props`. - * - * @private - * @param {Object} object The object to inspect. - * @param {Array} props The property names to filter. - * @returns {Array} Returns the function names. - */ - function baseFunctions(object, props) { - return arrayFilter(props, function(key) { - return isFunction(object[key]); - }); - } - - /** - * The base implementation of `_.get` without support for default values. - * - * @private - * @param {Object} object The object to query. - * @param {Array|string} path The path of the property to get. - * @returns {*} Returns the resolved value. - */ - function baseGet(object, path) { - path = castPath(path, object); - - var index = 0, - length = path.length; - - while (object != null && index < length) { - object = object[toKey(path[index++])]; - } - return (index && index == length) ? object : undefined; - } - - /** - * The base implementation of `getAllKeys` and `getAllKeysIn` which uses - * `keysFunc` and `symbolsFunc` to get the enumerable property names and - * symbols of `object`. - * - * @private - * @param {Object} object The object to query. - * @param {Function} keysFunc The function to get the keys of `object`. - * @param {Function} symbolsFunc The function to get the symbols of `object`. - * @returns {Array} Returns the array of property names and symbols. - */ - function baseGetAllKeys(object, keysFunc, symbolsFunc) { - var result = keysFunc(object); - return isArray(object) ? result : arrayPush(result, symbolsFunc(object)); - } - - /** - * The base implementation of `getTag` without fallbacks for buggy environments. - * - * @private - * @param {*} value The value to query. - * @returns {string} Returns the `toStringTag`. - */ - function baseGetTag(value) { - if (value == null) { - return value === undefined ? undefinedTag : nullTag; - } - return (symToStringTag && symToStringTag in Object(value)) - ? getRawTag(value) - : objectToString(value); - } - - /** - * The base implementation of `_.gt` which doesn't coerce arguments. - * - * @private - * @param {*} value The value to compare. - * @param {*} other The other value to compare. - * @returns {boolean} Returns `true` if `value` is greater than `other`, - * else `false`. - */ - function baseGt(value, other) { - return value > other; - } - - /** - * The base implementation of `_.has` without support for deep paths. - * - * @private - * @param {Object} [object] The object to query. - * @param {Array|string} key The key to check. - * @returns {boolean} Returns `true` if `key` exists, else `false`. - */ - function baseHas(object, key) { - return object != null && hasOwnProperty.call(object, key); - } - - /** - * The base implementation of `_.hasIn` without support for deep paths. - * - * @private - * @param {Object} [object] The object to query. - * @param {Array|string} key The key to check. - * @returns {boolean} Returns `true` if `key` exists, else `false`. - */ - function baseHasIn(object, key) { - return object != null && key in Object(object); - } - - /** - * The base implementation of `_.inRange` which doesn't coerce arguments. - * - * @private - * @param {number} number The number to check. - * @param {number} start The start of the range. - * @param {number} end The end of the range. - * @returns {boolean} Returns `true` if `number` is in the range, else `false`. - */ - function baseInRange(number, start, end) { - return number >= nativeMin(start, end) && number < nativeMax(start, end); - } - - /** - * The base implementation of methods like `_.intersection`, without support - * for iteratee shorthands, that accepts an array of arrays to inspect. - * - * @private - * @param {Array} arrays The arrays to inspect. - * @param {Function} [iteratee] The iteratee invoked per element. - * @param {Function} [comparator] The comparator invoked per element. - * @returns {Array} Returns the new array of shared values. - */ - function baseIntersection(arrays, iteratee, comparator) { - var includes = comparator ? arrayIncludesWith : arrayIncludes, - length = arrays[0].length, - othLength = arrays.length, - othIndex = othLength, - caches = Array(othLength), - maxLength = Infinity, - result = []; - - while (othIndex--) { - var array = arrays[othIndex]; - if (othIndex && iteratee) { - array = arrayMap(array, baseUnary(iteratee)); - } - maxLength = nativeMin(array.length, maxLength); - caches[othIndex] = !comparator && (iteratee || (length >= 120 && array.length >= 120)) - ? new SetCache(othIndex && array) - : undefined; - } - array = arrays[0]; - - var index = -1, - seen = caches[0]; - - outer: - while (++index < length && result.length < maxLength) { - var value = array[index], - computed = iteratee ? iteratee(value) : value; - - value = (comparator || value !== 0) ? value : 0; - if (!(seen - ? cacheHas(seen, computed) - : includes(result, computed, comparator) - )) { - othIndex = othLength; - while (--othIndex) { - var cache = caches[othIndex]; - if (!(cache - ? cacheHas(cache, computed) - : includes(arrays[othIndex], computed, comparator)) - ) { - continue outer; - } - } - if (seen) { - seen.push(computed); - } - result.push(value); - } - } - return result; - } - - /** - * The base implementation of `_.invert` and `_.invertBy` which inverts - * `object` with values transformed by `iteratee` and set by `setter`. - * - * @private - * @param {Object} object The object to iterate over. - * @param {Function} setter The function to set `accumulator` values. - * @param {Function} iteratee The iteratee to transform values. - * @param {Object} accumulator The initial inverted object. - * @returns {Function} Returns `accumulator`. - */ - function baseInverter(object, setter, iteratee, accumulator) { - baseForOwn(object, function(value, key, object) { - setter(accumulator, iteratee(value), key, object); - }); - return accumulator; - } - - /** - * The base implementation of `_.invoke` without support for individual - * method arguments. - * - * @private - * @param {Object} object The object to query. - * @param {Array|string} path The path of the method to invoke. - * @param {Array} args The arguments to invoke the method with. - * @returns {*} Returns the result of the invoked method. - */ - function baseInvoke(object, path, args) { - path = castPath(path, object); - object = parent(object, path); - var func = object == null ? object : object[toKey(last(path))]; - return func == null ? undefined : apply(func, object, args); - } - - /** - * The base implementation of `_.isArguments`. - * - * @private - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if `value` is an `arguments` object, - */ - function baseIsArguments(value) { - return isObjectLike(value) && baseGetTag(value) == argsTag; - } - - /** - * The base implementation of `_.isArrayBuffer` without Node.js optimizations. - * - * @private - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if `value` is an array buffer, else `false`. - */ - function baseIsArrayBuffer(value) { - return isObjectLike(value) && baseGetTag(value) == arrayBufferTag; - } - - /** - * The base implementation of `_.isDate` without Node.js optimizations. - * - * @private - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if `value` is a date object, else `false`. - */ - function baseIsDate(value) { - return isObjectLike(value) && baseGetTag(value) == dateTag; - } - - /** - * The base implementation of `_.isEqual` which supports partial comparisons - * and tracks traversed objects. - * - * @private - * @param {*} value The value to compare. - * @param {*} other The other value to compare. - * @param {boolean} bitmask The bitmask flags. - * 1 - Unordered comparison - * 2 - Partial comparison - * @param {Function} [customizer] The function to customize comparisons. - * @param {Object} [stack] Tracks traversed `value` and `other` objects. - * @returns {boolean} Returns `true` if the values are equivalent, else `false`. - */ - function baseIsEqual(value, other, bitmask, customizer, stack) { - if (value === other) { - return true; - } - if (value == null || other == null || (!isObjectLike(value) && !isObjectLike(other))) { - return value !== value && other !== other; - } - return baseIsEqualDeep(value, other, bitmask, customizer, baseIsEqual, stack); - } - - /** - * A specialized version of `baseIsEqual` for arrays and objects which performs - * deep comparisons and tracks traversed objects enabling objects with circular - * references to be compared. - * - * @private - * @param {Object} object The object to compare. - * @param {Object} other The other object to compare. - * @param {number} bitmask The bitmask flags. See `baseIsEqual` for more details. - * @param {Function} customizer The function to customize comparisons. - * @param {Function} equalFunc The function to determine equivalents of values. - * @param {Object} [stack] Tracks traversed `object` and `other` objects. - * @returns {boolean} Returns `true` if the objects are equivalent, else `false`. - */ - function baseIsEqualDeep(object, other, bitmask, customizer, equalFunc, stack) { - var objIsArr = isArray(object), - othIsArr = isArray(other), - objTag = objIsArr ? arrayTag : getTag(object), - othTag = othIsArr ? arrayTag : getTag(other); - - objTag = objTag == argsTag ? objectTag : objTag; - othTag = othTag == argsTag ? objectTag : othTag; - - var objIsObj = objTag == objectTag, - othIsObj = othTag == objectTag, - isSameTag = objTag == othTag; - - if (isSameTag && isBuffer(object)) { - if (!isBuffer(other)) { - return false; - } - objIsArr = true; - objIsObj = false; - } - if (isSameTag && !objIsObj) { - stack || (stack = new Stack); - return (objIsArr || isTypedArray(object)) - ? equalArrays(object, other, bitmask, customizer, equalFunc, stack) - : equalByTag(object, other, objTag, bitmask, customizer, equalFunc, stack); - } - if (!(bitmask & COMPARE_PARTIAL_FLAG)) { - var objIsWrapped = objIsObj && hasOwnProperty.call(object, '__wrapped__'), - othIsWrapped = othIsObj && hasOwnProperty.call(other, '__wrapped__'); - - if (objIsWrapped || othIsWrapped) { - var objUnwrapped = objIsWrapped ? object.value() : object, - othUnwrapped = othIsWrapped ? other.value() : other; - - stack || (stack = new Stack); - return equalFunc(objUnwrapped, othUnwrapped, bitmask, customizer, stack); - } - } - if (!isSameTag) { - return false; - } - stack || (stack = new Stack); - return equalObjects(object, other, bitmask, customizer, equalFunc, stack); - } - - /** - * The base implementation of `_.isMap` without Node.js optimizations. - * - * @private - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if `value` is a map, else `false`. - */ - function baseIsMap(value) { - return isObjectLike(value) && getTag(value) == mapTag; - } - - /** - * The base implementation of `_.isMatch` without support for iteratee shorthands. - * - * @private - * @param {Object} object The object to inspect. - * @param {Object} source The object of property values to match. - * @param {Array} matchData The property names, values, and compare flags to match. - * @param {Function} [customizer] The function to customize comparisons. - * @returns {boolean} Returns `true` if `object` is a match, else `false`. - */ - function baseIsMatch(object, source, matchData, customizer) { - var index = matchData.length, - length = index, - noCustomizer = !customizer; - - if (object == null) { - return !length; - } - object = Object(object); - while (index--) { - var data = matchData[index]; - if ((noCustomizer && data[2]) - ? data[1] !== object[data[0]] - : !(data[0] in object) - ) { - return false; - } - } - while (++index < length) { - data = matchData[index]; - var key = data[0], - objValue = object[key], - srcValue = data[1]; - - if (noCustomizer && data[2]) { - if (objValue === undefined && !(key in object)) { - return false; - } - } else { - var stack = new Stack; - if (customizer) { - var result = customizer(objValue, srcValue, key, object, source, stack); - } - if (!(result === undefined - ? baseIsEqual(srcValue, objValue, COMPARE_PARTIAL_FLAG | COMPARE_UNORDERED_FLAG, customizer, stack) - : result - )) { - return false; - } - } - } - return true; - } - - /** - * The base implementation of `_.isNative` without bad shim checks. - * - * @private - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if `value` is a native function, - * else `false`. - */ - function baseIsNative(value) { - if (!isObject(value) || isMasked(value)) { - return false; - } - var pattern = isFunction(value) ? reIsNative : reIsHostCtor; - return pattern.test(toSource(value)); - } - - /** - * The base implementation of `_.isRegExp` without Node.js optimizations. - * - * @private - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if `value` is a regexp, else `false`. - */ - function baseIsRegExp(value) { - return isObjectLike(value) && baseGetTag(value) == regexpTag; - } - - /** - * The base implementation of `_.isSet` without Node.js optimizations. - * - * @private - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if `value` is a set, else `false`. - */ - function baseIsSet(value) { - return isObjectLike(value) && getTag(value) == setTag; - } - - /** - * The base implementation of `_.isTypedArray` without Node.js optimizations. - * - * @private - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if `value` is a typed array, else `false`. - */ - function baseIsTypedArray(value) { - return isObjectLike(value) && - isLength(value.length) && !!typedArrayTags[baseGetTag(value)]; - } - - /** - * The base implementation of `_.iteratee`. - * - * @private - * @param {*} [value=_.identity] The value to convert to an iteratee. - * @returns {Function} Returns the iteratee. - */ - function baseIteratee(value) { - // Don't store the `typeof` result in a variable to avoid a JIT bug in Safari 9. - // See https://bugs.webkit.org/show_bug.cgi?id=156034 for more details. - if (typeof value == 'function') { - return value; - } - if (value == null) { - return identity; - } - if (typeof value == 'object') { - return isArray(value) - ? baseMatchesProperty(value[0], value[1]) - : baseMatches(value); - } - return property(value); - } - - /** - * The base implementation of `_.keys` which doesn't treat sparse arrays as dense. - * - * @private - * @param {Object} object The object to query. - * @returns {Array} Returns the array of property names. - */ - function baseKeys(object) { - if (!isPrototype(object)) { - return nativeKeys(object); - } - var result = []; - for (var key in Object(object)) { - if (hasOwnProperty.call(object, key) && key != 'constructor') { - result.push(key); - } - } - return result; - } - - /** - * The base implementation of `_.keysIn` which doesn't treat sparse arrays as dense. - * - * @private - * @param {Object} object The object to query. - * @returns {Array} Returns the array of property names. - */ - function baseKeysIn(object) { - if (!isObject(object)) { - return nativeKeysIn(object); - } - var isProto = isPrototype(object), - result = []; - - for (var key in object) { - if (!(key == 'constructor' && (isProto || !hasOwnProperty.call(object, key)))) { - result.push(key); - } - } - return result; - } - - /** - * The base implementation of `_.lt` which doesn't coerce arguments. - * - * @private - * @param {*} value The value to compare. - * @param {*} other The other value to compare. - * @returns {boolean} Returns `true` if `value` is less than `other`, - * else `false`. - */ - function baseLt(value, other) { - return value < other; - } - - /** - * The base implementation of `_.map` without support for iteratee shorthands. - * - * @private - * @param {Array|Object} collection The collection to iterate over. - * @param {Function} iteratee The function invoked per iteration. - * @returns {Array} Returns the new mapped array. - */ - function baseMap(collection, iteratee) { - var index = -1, - result = isArrayLike(collection) ? Array(collection.length) : []; - - baseEach(collection, function(value, key, collection) { - result[++index] = iteratee(value, key, collection); - }); - return result; - } - - /** - * The base implementation of `_.matches` which doesn't clone `source`. - * - * @private - * @param {Object} source The object of property values to match. - * @returns {Function} Returns the new spec function. - */ - function baseMatches(source) { - var matchData = getMatchData(source); - if (matchData.length == 1 && matchData[0][2]) { - return matchesStrictComparable(matchData[0][0], matchData[0][1]); - } - return function(object) { - return object === source || baseIsMatch(object, source, matchData); - }; - } - - /** - * The base implementation of `_.matchesProperty` which doesn't clone `srcValue`. - * - * @private - * @param {string} path The path of the property to get. - * @param {*} srcValue The value to match. - * @returns {Function} Returns the new spec function. - */ - function baseMatchesProperty(path, srcValue) { - if (isKey(path) && isStrictComparable(srcValue)) { - return matchesStrictComparable(toKey(path), srcValue); - } - return function(object) { - var objValue = get(object, path); - return (objValue === undefined && objValue === srcValue) - ? hasIn(object, path) - : baseIsEqual(srcValue, objValue, COMPARE_PARTIAL_FLAG | COMPARE_UNORDERED_FLAG); - }; - } - - /** - * The base implementation of `_.merge` without support for multiple sources. - * - * @private - * @param {Object} object The destination object. - * @param {Object} source The source object. - * @param {number} srcIndex The index of `source`. - * @param {Function} [customizer] The function to customize merged values. - * @param {Object} [stack] Tracks traversed source values and their merged - * counterparts. - */ - function baseMerge(object, source, srcIndex, customizer, stack) { - if (object === source) { - return; - } - baseFor(source, function(srcValue, key) { - if (isObject(srcValue)) { - stack || (stack = new Stack); - baseMergeDeep(object, source, key, srcIndex, baseMerge, customizer, stack); - } - else { - var newValue = customizer - ? customizer(object[key], srcValue, (key + ''), object, source, stack) - : undefined; - - if (newValue === undefined) { - newValue = srcValue; - } - assignMergeValue(object, key, newValue); - } - }, keysIn); - } - - /** - * A specialized version of `baseMerge` for arrays and objects which performs - * deep merges and tracks traversed objects enabling objects with circular - * references to be merged. - * - * @private - * @param {Object} object The destination object. - * @param {Object} source The source object. - * @param {string} key The key of the value to merge. - * @param {number} srcIndex The index of `source`. - * @param {Function} mergeFunc The function to merge values. - * @param {Function} [customizer] The function to customize assigned values. - * @param {Object} [stack] Tracks traversed source values and their merged - * counterparts. - */ - function baseMergeDeep(object, source, key, srcIndex, mergeFunc, customizer, stack) { - var objValue = object[key], - srcValue = source[key], - stacked = stack.get(srcValue); - - if (stacked) { - assignMergeValue(object, key, stacked); - return; - } - var newValue = customizer - ? customizer(objValue, srcValue, (key + ''), object, source, stack) - : undefined; - - var isCommon = newValue === undefined; - - if (isCommon) { - var isArr = isArray(srcValue), - isBuff = !isArr && isBuffer(srcValue), - isTyped = !isArr && !isBuff && isTypedArray(srcValue); - - newValue = srcValue; - if (isArr || isBuff || isTyped) { - if (isArray(objValue)) { - newValue = objValue; - } - else if (isArrayLikeObject(objValue)) { - newValue = copyArray(objValue); - } - else if (isBuff) { - isCommon = false; - newValue = cloneBuffer(srcValue, true); - } - else if (isTyped) { - isCommon = false; - newValue = cloneTypedArray(srcValue, true); - } - else { - newValue = []; - } - } - else if (isPlainObject(srcValue) || isArguments(srcValue)) { - newValue = objValue; - if (isArguments(objValue)) { - newValue = toPlainObject(objValue); - } - else if (!isObject(objValue) || (srcIndex && isFunction(objValue))) { - newValue = initCloneObject(srcValue); - } - } - else { - isCommon = false; - } - } - if (isCommon) { - // Recursively merge objects and arrays (susceptible to call stack limits). - stack.set(srcValue, newValue); - mergeFunc(newValue, srcValue, srcIndex, customizer, stack); - stack['delete'](srcValue); - } - assignMergeValue(object, key, newValue); - } - - /** - * The base implementation of `_.nth` which doesn't coerce arguments. - * - * @private - * @param {Array} array The array to query. - * @param {number} n The index of the element to return. - * @returns {*} Returns the nth element of `array`. - */ - function baseNth(array, n) { - var length = array.length; - if (!length) { - return; - } - n += n < 0 ? length : 0; - return isIndex(n, length) ? array[n] : undefined; - } - - /** - * The base implementation of `_.orderBy` without param guards. - * - * @private - * @param {Array|Object} collection The collection to iterate over. - * @param {Function[]|Object[]|string[]} iteratees The iteratees to sort by. - * @param {string[]} orders The sort orders of `iteratees`. - * @returns {Array} Returns the new sorted array. - */ - function baseOrderBy(collection, iteratees, orders) { - var index = -1; - iteratees = arrayMap(iteratees.length ? iteratees : [identity], baseUnary(getIteratee())); - - var result = baseMap(collection, function(value, key, collection) { - var criteria = arrayMap(iteratees, function(iteratee) { - return iteratee(value); - }); - return { 'criteria': criteria, 'index': ++index, 'value': value }; - }); - - return baseSortBy(result, function(object, other) { - return compareMultiple(object, other, orders); - }); - } - - /** - * The base implementation of `_.pick` without support for individual - * property identifiers. - * - * @private - * @param {Object} object The source object. - * @param {string[]} paths The property paths to pick. - * @returns {Object} Returns the new object. - */ - function basePick(object, paths) { - return basePickBy(object, paths, function(value, path) { - return hasIn(object, path); - }); - } - - /** - * The base implementation of `_.pickBy` without support for iteratee shorthands. - * - * @private - * @param {Object} object The source object. - * @param {string[]} paths The property paths to pick. - * @param {Function} predicate The function invoked per property. - * @returns {Object} Returns the new object. - */ - function basePickBy(object, paths, predicate) { - var index = -1, - length = paths.length, - result = {}; - - while (++index < length) { - var path = paths[index], - value = baseGet(object, path); - - if (predicate(value, path)) { - baseSet(result, castPath(path, object), value); - } - } - return result; - } - - /** - * A specialized version of `baseProperty` which supports deep paths. - * - * @private - * @param {Array|string} path The path of the property to get. - * @returns {Function} Returns the new accessor function. - */ - function basePropertyDeep(path) { - return function(object) { - return baseGet(object, path); - }; - } - - /** - * The base implementation of `_.pullAllBy` without support for iteratee - * shorthands. - * - * @private - * @param {Array} array The array to modify. - * @param {Array} values The values to remove. - * @param {Function} [iteratee] The iteratee invoked per element. - * @param {Function} [comparator] The comparator invoked per element. - * @returns {Array} Returns `array`. - */ - function basePullAll(array, values, iteratee, comparator) { - var indexOf = comparator ? baseIndexOfWith : baseIndexOf, - index = -1, - length = values.length, - seen = array; - - if (array === values) { - values = copyArray(values); - } - if (iteratee) { - seen = arrayMap(array, baseUnary(iteratee)); - } - while (++index < length) { - var fromIndex = 0, - value = values[index], - computed = iteratee ? iteratee(value) : value; - - while ((fromIndex = indexOf(seen, computed, fromIndex, comparator)) > -1) { - if (seen !== array) { - splice.call(seen, fromIndex, 1); - } - splice.call(array, fromIndex, 1); - } - } - return array; - } - - /** - * The base implementation of `_.pullAt` without support for individual - * indexes or capturing the removed elements. - * - * @private - * @param {Array} array The array to modify. - * @param {number[]} indexes The indexes of elements to remove. - * @returns {Array} Returns `array`. - */ - function basePullAt(array, indexes) { - var length = array ? indexes.length : 0, - lastIndex = length - 1; - - while (length--) { - var index = indexes[length]; - if (length == lastIndex || index !== previous) { - var previous = index; - if (isIndex(index)) { - splice.call(array, index, 1); - } else { - baseUnset(array, index); - } - } - } - return array; - } - - /** - * The base implementation of `_.random` without support for returning - * floating-point numbers. - * - * @private - * @param {number} lower The lower bound. - * @param {number} upper The upper bound. - * @returns {number} Returns the random number. - */ - function baseRandom(lower, upper) { - return lower + nativeFloor(nativeRandom() * (upper - lower + 1)); - } - - /** - * The base implementation of `_.range` and `_.rangeRight` which doesn't - * coerce arguments. - * - * @private - * @param {number} start The start of the range. - * @param {number} end The end of the range. - * @param {number} step The value to increment or decrement by. - * @param {boolean} [fromRight] Specify iterating from right to left. - * @returns {Array} Returns the range of numbers. - */ - function baseRange(start, end, step, fromRight) { - var index = -1, - length = nativeMax(nativeCeil((end - start) / (step || 1)), 0), - result = Array(length); - - while (length--) { - result[fromRight ? length : ++index] = start; - start += step; - } - return result; - } - - /** - * The base implementation of `_.repeat` which doesn't coerce arguments. - * - * @private - * @param {string} string The string to repeat. - * @param {number} n The number of times to repeat the string. - * @returns {string} Returns the repeated string. - */ - function baseRepeat(string, n) { - var result = ''; - if (!string || n < 1 || n > MAX_SAFE_INTEGER) { - return result; - } - // Leverage the exponentiation by squaring algorithm for a faster repeat. - // See https://en.wikipedia.org/wiki/Exponentiation_by_squaring for more details. - do { - if (n % 2) { - result += string; - } - n = nativeFloor(n / 2); - if (n) { - string += string; - } - } while (n); - - return result; - } - - /** - * The base implementation of `_.rest` which doesn't validate or coerce arguments. - * - * @private - * @param {Function} func The function to apply a rest parameter to. - * @param {number} [start=func.length-1] The start position of the rest parameter. - * @returns {Function} Returns the new function. - */ - function baseRest(func, start) { - return setToString(overRest(func, start, identity), func + ''); - } - - /** - * The base implementation of `_.sample`. - * - * @private - * @param {Array|Object} collection The collection to sample. - * @returns {*} Returns the random element. - */ - function baseSample(collection) { - return arraySample(values(collection)); - } - - /** - * The base implementation of `_.sampleSize` without param guards. - * - * @private - * @param {Array|Object} collection The collection to sample. - * @param {number} n The number of elements to sample. - * @returns {Array} Returns the random elements. - */ - function baseSampleSize(collection, n) { - var array = values(collection); - return shuffleSelf(array, baseClamp(n, 0, array.length)); - } - - /** - * The base implementation of `_.set`. - * - * @private - * @param {Object} object The object to modify. - * @param {Array|string} path The path of the property to set. - * @param {*} value The value to set. - * @param {Function} [customizer] The function to customize path creation. - * @returns {Object} Returns `object`. - */ - function baseSet(object, path, value, customizer) { - if (!isObject(object)) { - return object; - } - path = castPath(path, object); - - var index = -1, - length = path.length, - lastIndex = length - 1, - nested = object; - - while (nested != null && ++index < length) { - var key = toKey(path[index]), - newValue = value; - - if (index != lastIndex) { - var objValue = nested[key]; - newValue = customizer ? customizer(objValue, key, nested) : undefined; - if (newValue === undefined) { - newValue = isObject(objValue) - ? objValue - : (isIndex(path[index + 1]) ? [] : {}); - } - } - assignValue(nested, key, newValue); - nested = nested[key]; - } - return object; - } - - /** - * The base implementation of `setData` without support for hot loop shorting. - * - * @private - * @param {Function} func The function to associate metadata with. - * @param {*} data The metadata. - * @returns {Function} Returns `func`. - */ - var baseSetData = !metaMap ? identity : function(func, data) { - metaMap.set(func, data); - return func; - }; - - /** - * The base implementation of `setToString` without support for hot loop shorting. - * - * @private - * @param {Function} func The function to modify. - * @param {Function} string The `toString` result. - * @returns {Function} Returns `func`. - */ - var baseSetToString = !defineProperty ? identity : function(func, string) { - return defineProperty(func, 'toString', { - 'configurable': true, - 'enumerable': false, - 'value': constant(string), - 'writable': true - }); - }; - - /** - * The base implementation of `_.shuffle`. - * - * @private - * @param {Array|Object} collection The collection to shuffle. - * @returns {Array} Returns the new shuffled array. - */ - function baseShuffle(collection) { - return shuffleSelf(values(collection)); - } - - /** - * The base implementation of `_.slice` without an iteratee call guard. - * - * @private - * @param {Array} array The array to slice. - * @param {number} [start=0] The start position. - * @param {number} [end=array.length] The end position. - * @returns {Array} Returns the slice of `array`. - */ - function baseSlice(array, start, end) { - var index = -1, - length = array.length; - - if (start < 0) { - start = -start > length ? 0 : (length + start); - } - end = end > length ? length : end; - if (end < 0) { - end += length; - } - length = start > end ? 0 : ((end - start) >>> 0); - start >>>= 0; - - var result = Array(length); - while (++index < length) { - result[index] = array[index + start]; - } - return result; - } - - /** - * The base implementation of `_.some` without support for iteratee shorthands. - * - * @private - * @param {Array|Object} collection The collection to iterate over. - * @param {Function} predicate The function invoked per iteration. - * @returns {boolean} Returns `true` if any element passes the predicate check, - * else `false`. - */ - function baseSome(collection, predicate) { - var result; - - baseEach(collection, function(value, index, collection) { - result = predicate(value, index, collection); - return !result; - }); - return !!result; - } - - /** - * The base implementation of `_.sortedIndex` and `_.sortedLastIndex` which - * performs a binary search of `array` to determine the index at which `value` - * should be inserted into `array` in order to maintain its sort order. - * - * @private - * @param {Array} array The sorted array to inspect. - * @param {*} value The value to evaluate. - * @param {boolean} [retHighest] Specify returning the highest qualified index. - * @returns {number} Returns the index at which `value` should be inserted - * into `array`. - */ - function baseSortedIndex(array, value, retHighest) { - var low = 0, - high = array == null ? low : array.length; - - if (typeof value == 'number' && value === value && high <= HALF_MAX_ARRAY_LENGTH) { - while (low < high) { - var mid = (low + high) >>> 1, - computed = array[mid]; - - if (computed !== null && !isSymbol(computed) && - (retHighest ? (computed <= value) : (computed < value))) { - low = mid + 1; - } else { - high = mid; - } - } - return high; - } - return baseSortedIndexBy(array, value, identity, retHighest); - } - - /** - * The base implementation of `_.sortedIndexBy` and `_.sortedLastIndexBy` - * which invokes `iteratee` for `value` and each element of `array` to compute - * their sort ranking. The iteratee is invoked with one argument; (value). - * - * @private - * @param {Array} array The sorted array to inspect. - * @param {*} value The value to evaluate. - * @param {Function} iteratee The iteratee invoked per element. - * @param {boolean} [retHighest] Specify returning the highest qualified index. - * @returns {number} Returns the index at which `value` should be inserted - * into `array`. - */ - function baseSortedIndexBy(array, value, iteratee, retHighest) { - value = iteratee(value); - - var low = 0, - high = array == null ? 0 : array.length, - valIsNaN = value !== value, - valIsNull = value === null, - valIsSymbol = isSymbol(value), - valIsUndefined = value === undefined; - - while (low < high) { - var mid = nativeFloor((low + high) / 2), - computed = iteratee(array[mid]), - othIsDefined = computed !== undefined, - othIsNull = computed === null, - othIsReflexive = computed === computed, - othIsSymbol = isSymbol(computed); - - if (valIsNaN) { - var setLow = retHighest || othIsReflexive; - } else if (valIsUndefined) { - setLow = othIsReflexive && (retHighest || othIsDefined); - } else if (valIsNull) { - setLow = othIsReflexive && othIsDefined && (retHighest || !othIsNull); - } else if (valIsSymbol) { - setLow = othIsReflexive && othIsDefined && !othIsNull && (retHighest || !othIsSymbol); - } else if (othIsNull || othIsSymbol) { - setLow = false; - } else { - setLow = retHighest ? (computed <= value) : (computed < value); - } - if (setLow) { - low = mid + 1; - } else { - high = mid; - } - } - return nativeMin(high, MAX_ARRAY_INDEX); - } - - /** - * The base implementation of `_.sortedUniq` and `_.sortedUniqBy` without - * support for iteratee shorthands. - * - * @private - * @param {Array} array The array to inspect. - * @param {Function} [iteratee] The iteratee invoked per element. - * @returns {Array} Returns the new duplicate free array. - */ - function baseSortedUniq(array, iteratee) { - var index = -1, - length = array.length, - resIndex = 0, - result = []; - - while (++index < length) { - var value = array[index], - computed = iteratee ? iteratee(value) : value; - - if (!index || !eq(computed, seen)) { - var seen = computed; - result[resIndex++] = value === 0 ? 0 : value; - } - } - return result; - } - - /** - * The base implementation of `_.toNumber` which doesn't ensure correct - * conversions of binary, hexadecimal, or octal string values. - * - * @private - * @param {*} value The value to process. - * @returns {number} Returns the number. - */ - function baseToNumber(value) { - if (typeof value == 'number') { - return value; - } - if (isSymbol(value)) { - return NAN; - } - return +value; - } - - /** - * The base implementation of `_.toString` which doesn't convert nullish - * values to empty strings. - * - * @private - * @param {*} value The value to process. - * @returns {string} Returns the string. - */ - function baseToString(value) { - // Exit early for strings to avoid a performance hit in some environments. - if (typeof value == 'string') { - return value; - } - if (isArray(value)) { - // Recursively convert values (susceptible to call stack limits). - return arrayMap(value, baseToString) + ''; - } - if (isSymbol(value)) { - return symbolToString ? symbolToString.call(value) : ''; - } - var result = (value + ''); - return (result == '0' && (1 / value) == -INFINITY) ? '-0' : result; - } - - /** - * The base implementation of `_.uniqBy` without support for iteratee shorthands. - * - * @private - * @param {Array} array The array to inspect. - * @param {Function} [iteratee] The iteratee invoked per element. - * @param {Function} [comparator] The comparator invoked per element. - * @returns {Array} Returns the new duplicate free array. - */ - function baseUniq(array, iteratee, comparator) { - var index = -1, - includes = arrayIncludes, - length = array.length, - isCommon = true, - result = [], - seen = result; - - if (comparator) { - isCommon = false; - includes = arrayIncludesWith; - } - else if (length >= LARGE_ARRAY_SIZE) { - var set = iteratee ? null : createSet(array); - if (set) { - return setToArray(set); - } - isCommon = false; - includes = cacheHas; - seen = new SetCache; - } - else { - seen = iteratee ? [] : result; - } - outer: - while (++index < length) { - var value = array[index], - computed = iteratee ? iteratee(value) : value; - - value = (comparator || value !== 0) ? value : 0; - if (isCommon && computed === computed) { - var seenIndex = seen.length; - while (seenIndex--) { - if (seen[seenIndex] === computed) { - continue outer; - } - } - if (iteratee) { - seen.push(computed); - } - result.push(value); - } - else if (!includes(seen, computed, comparator)) { - if (seen !== result) { - seen.push(computed); - } - result.push(value); - } - } - return result; - } - - /** - * The base implementation of `_.unset`. - * - * @private - * @param {Object} object The object to modify. - * @param {Array|string} path The property path to unset. - * @returns {boolean} Returns `true` if the property is deleted, else `false`. - */ - function baseUnset(object, path) { - path = castPath(path, object); - object = parent(object, path); - return object == null || delete object[toKey(last(path))]; - } - - /** - * The base implementation of `_.update`. - * - * @private - * @param {Object} object The object to modify. - * @param {Array|string} path The path of the property to update. - * @param {Function} updater The function to produce the updated value. - * @param {Function} [customizer] The function to customize path creation. - * @returns {Object} Returns `object`. - */ - function baseUpdate(object, path, updater, customizer) { - return baseSet(object, path, updater(baseGet(object, path)), customizer); - } - - /** - * The base implementation of methods like `_.dropWhile` and `_.takeWhile` - * without support for iteratee shorthands. - * - * @private - * @param {Array} array The array to query. - * @param {Function} predicate The function invoked per iteration. - * @param {boolean} [isDrop] Specify dropping elements instead of taking them. - * @param {boolean} [fromRight] Specify iterating from right to left. - * @returns {Array} Returns the slice of `array`. - */ - function baseWhile(array, predicate, isDrop, fromRight) { - var length = array.length, - index = fromRight ? length : -1; - - while ((fromRight ? index-- : ++index < length) && - predicate(array[index], index, array)) {} - - return isDrop - ? baseSlice(array, (fromRight ? 0 : index), (fromRight ? index + 1 : length)) - : baseSlice(array, (fromRight ? index + 1 : 0), (fromRight ? length : index)); - } - - /** - * The base implementation of `wrapperValue` which returns the result of - * performing a sequence of actions on the unwrapped `value`, where each - * successive action is supplied the return value of the previous. - * - * @private - * @param {*} value The unwrapped value. - * @param {Array} actions Actions to perform to resolve the unwrapped value. - * @returns {*} Returns the resolved value. - */ - function baseWrapperValue(value, actions) { - var result = value; - if (result instanceof LazyWrapper) { - result = result.value(); - } - return arrayReduce(actions, function(result, action) { - return action.func.apply(action.thisArg, arrayPush([result], action.args)); - }, result); - } - - /** - * The base implementation of methods like `_.xor`, without support for - * iteratee shorthands, that accepts an array of arrays to inspect. - * - * @private - * @param {Array} arrays The arrays to inspect. - * @param {Function} [iteratee] The iteratee invoked per element. - * @param {Function} [comparator] The comparator invoked per element. - * @returns {Array} Returns the new array of values. - */ - function baseXor(arrays, iteratee, comparator) { - var length = arrays.length; - if (length < 2) { - return length ? baseUniq(arrays[0]) : []; - } - var index = -1, - result = Array(length); - - while (++index < length) { - var array = arrays[index], - othIndex = -1; - - while (++othIndex < length) { - if (othIndex != index) { - result[index] = baseDifference(result[index] || array, arrays[othIndex], iteratee, comparator); - } - } - } - return baseUniq(baseFlatten(result, 1), iteratee, comparator); - } - - /** - * This base implementation of `_.zipObject` which assigns values using `assignFunc`. - * - * @private - * @param {Array} props The property identifiers. - * @param {Array} values The property values. - * @param {Function} assignFunc The function to assign values. - * @returns {Object} Returns the new object. - */ - function baseZipObject(props, values, assignFunc) { - var index = -1, - length = props.length, - valsLength = values.length, - result = {}; - - while (++index < length) { - var value = index < valsLength ? values[index] : undefined; - assignFunc(result, props[index], value); - } - return result; - } - - /** - * Casts `value` to an empty array if it's not an array like object. - * - * @private - * @param {*} value The value to inspect. - * @returns {Array|Object} Returns the cast array-like object. - */ - function castArrayLikeObject(value) { - return isArrayLikeObject(value) ? value : []; - } - - /** - * Casts `value` to `identity` if it's not a function. - * - * @private - * @param {*} value The value to inspect. - * @returns {Function} Returns cast function. - */ - function castFunction(value) { - return typeof value == 'function' ? value : identity; - } - - /** - * Casts `value` to a path array if it's not one. - * - * @private - * @param {*} value The value to inspect. - * @param {Object} [object] The object to query keys on. - * @returns {Array} Returns the cast property path array. - */ - function castPath(value, object) { - if (isArray(value)) { - return value; - } - return isKey(value, object) ? [value] : stringToPath(toString(value)); - } - - /** - * A `baseRest` alias which can be replaced with `identity` by module - * replacement plugins. - * - * @private - * @type {Function} - * @param {Function} func The function to apply a rest parameter to. - * @returns {Function} Returns the new function. - */ - var castRest = baseRest; - - /** - * Casts `array` to a slice if it's needed. - * - * @private - * @param {Array} array The array to inspect. - * @param {number} start The start position. - * @param {number} [end=array.length] The end position. - * @returns {Array} Returns the cast slice. - */ - function castSlice(array, start, end) { - var length = array.length; - end = end === undefined ? length : end; - return (!start && end >= length) ? array : baseSlice(array, start, end); - } - - /** - * A simple wrapper around the global [`clearTimeout`](https://mdn.io/clearTimeout). - * - * @private - * @param {number|Object} id The timer id or timeout object of the timer to clear. - */ - var clearTimeout = ctxClearTimeout || function(id) { - return root.clearTimeout(id); - }; - - /** - * Creates a clone of `buffer`. - * - * @private - * @param {Buffer} buffer The buffer to clone. - * @param {boolean} [isDeep] Specify a deep clone. - * @returns {Buffer} Returns the cloned buffer. - */ - function cloneBuffer(buffer, isDeep) { - if (isDeep) { - return buffer.slice(); - } - var length = buffer.length, - result = allocUnsafe ? allocUnsafe(length) : new buffer.constructor(length); - - buffer.copy(result); - return result; - } - - /** - * Creates a clone of `arrayBuffer`. - * - * @private - * @param {ArrayBuffer} arrayBuffer The array buffer to clone. - * @returns {ArrayBuffer} Returns the cloned array buffer. - */ - function cloneArrayBuffer(arrayBuffer) { - var result = new arrayBuffer.constructor(arrayBuffer.byteLength); - new Uint8Array(result).set(new Uint8Array(arrayBuffer)); - return result; - } - - /** - * Creates a clone of `dataView`. - * - * @private - * @param {Object} dataView The data view to clone. - * @param {boolean} [isDeep] Specify a deep clone. - * @returns {Object} Returns the cloned data view. - */ - function cloneDataView(dataView, isDeep) { - var buffer = isDeep ? cloneArrayBuffer(dataView.buffer) : dataView.buffer; - return new dataView.constructor(buffer, dataView.byteOffset, dataView.byteLength); - } - - /** - * Creates a clone of `map`. - * - * @private - * @param {Object} map The map to clone. - * @param {Function} cloneFunc The function to clone values. - * @param {boolean} [isDeep] Specify a deep clone. - * @returns {Object} Returns the cloned map. - */ - function cloneMap(map, isDeep, cloneFunc) { - var array = isDeep ? cloneFunc(mapToArray(map), CLONE_DEEP_FLAG) : mapToArray(map); - return arrayReduce(array, addMapEntry, new map.constructor); - } - - /** - * Creates a clone of `regexp`. - * - * @private - * @param {Object} regexp The regexp to clone. - * @returns {Object} Returns the cloned regexp. - */ - function cloneRegExp(regexp) { - var result = new regexp.constructor(regexp.source, reFlags.exec(regexp)); - result.lastIndex = regexp.lastIndex; - return result; - } - - /** - * Creates a clone of `set`. - * - * @private - * @param {Object} set The set to clone. - * @param {Function} cloneFunc The function to clone values. - * @param {boolean} [isDeep] Specify a deep clone. - * @returns {Object} Returns the cloned set. - */ - function cloneSet(set, isDeep, cloneFunc) { - var array = isDeep ? cloneFunc(setToArray(set), CLONE_DEEP_FLAG) : setToArray(set); - return arrayReduce(array, addSetEntry, new set.constructor); - } - - /** - * Creates a clone of the `symbol` object. - * - * @private - * @param {Object} symbol The symbol object to clone. - * @returns {Object} Returns the cloned symbol object. - */ - function cloneSymbol(symbol) { - return symbolValueOf ? Object(symbolValueOf.call(symbol)) : {}; - } - - /** - * Creates a clone of `typedArray`. - * - * @private - * @param {Object} typedArray The typed array to clone. - * @param {boolean} [isDeep] Specify a deep clone. - * @returns {Object} Returns the cloned typed array. - */ - function cloneTypedArray(typedArray, isDeep) { - var buffer = isDeep ? cloneArrayBuffer(typedArray.buffer) : typedArray.buffer; - return new typedArray.constructor(buffer, typedArray.byteOffset, typedArray.length); - } - - /** - * Compares values to sort them in ascending order. - * - * @private - * @param {*} value The value to compare. - * @param {*} other The other value to compare. - * @returns {number} Returns the sort order indicator for `value`. - */ - function compareAscending(value, other) { - if (value !== other) { - var valIsDefined = value !== undefined, - valIsNull = value === null, - valIsReflexive = value === value, - valIsSymbol = isSymbol(value); - - var othIsDefined = other !== undefined, - othIsNull = other === null, - othIsReflexive = other === other, - othIsSymbol = isSymbol(other); - - if ((!othIsNull && !othIsSymbol && !valIsSymbol && value > other) || - (valIsSymbol && othIsDefined && othIsReflexive && !othIsNull && !othIsSymbol) || - (valIsNull && othIsDefined && othIsReflexive) || - (!valIsDefined && othIsReflexive) || - !valIsReflexive) { - return 1; - } - if ((!valIsNull && !valIsSymbol && !othIsSymbol && value < other) || - (othIsSymbol && valIsDefined && valIsReflexive && !valIsNull && !valIsSymbol) || - (othIsNull && valIsDefined && valIsReflexive) || - (!othIsDefined && valIsReflexive) || - !othIsReflexive) { - return -1; - } - } - return 0; - } - - /** - * Used by `_.orderBy` to compare multiple properties of a value to another - * and stable sort them. - * - * If `orders` is unspecified, all values are sorted in ascending order. Otherwise, - * specify an order of "desc" for descending or "asc" for ascending sort order - * of corresponding values. - * - * @private - * @param {Object} object The object to compare. - * @param {Object} other The other object to compare. - * @param {boolean[]|string[]} orders The order to sort by for each property. - * @returns {number} Returns the sort order indicator for `object`. - */ - function compareMultiple(object, other, orders) { - var index = -1, - objCriteria = object.criteria, - othCriteria = other.criteria, - length = objCriteria.length, - ordersLength = orders.length; - - while (++index < length) { - var result = compareAscending(objCriteria[index], othCriteria[index]); - if (result) { - if (index >= ordersLength) { - return result; - } - var order = orders[index]; - return result * (order == 'desc' ? -1 : 1); - } - } - // Fixes an `Array#sort` bug in the JS engine embedded in Adobe applications - // that causes it, under certain circumstances, to provide the same value for - // `object` and `other`. See https://github.com/jashkenas/underscore/pull/1247 - // for more details. - // - // This also ensures a stable sort in V8 and other engines. - // See https://bugs.chromium.org/p/v8/issues/detail?id=90 for more details. - return object.index - other.index; - } - - /** - * Creates an array that is the composition of partially applied arguments, - * placeholders, and provided arguments into a single array of arguments. - * - * @private - * @param {Array} args The provided arguments. - * @param {Array} partials The arguments to prepend to those provided. - * @param {Array} holders The `partials` placeholder indexes. - * @params {boolean} [isCurried] Specify composing for a curried function. - * @returns {Array} Returns the new array of composed arguments. - */ - function composeArgs(args, partials, holders, isCurried) { - var argsIndex = -1, - argsLength = args.length, - holdersLength = holders.length, - leftIndex = -1, - leftLength = partials.length, - rangeLength = nativeMax(argsLength - holdersLength, 0), - result = Array(leftLength + rangeLength), - isUncurried = !isCurried; - - while (++leftIndex < leftLength) { - result[leftIndex] = partials[leftIndex]; - } - while (++argsIndex < holdersLength) { - if (isUncurried || argsIndex < argsLength) { - result[holders[argsIndex]] = args[argsIndex]; - } - } - while (rangeLength--) { - result[leftIndex++] = args[argsIndex++]; - } - return result; - } - - /** - * This function is like `composeArgs` except that the arguments composition - * is tailored for `_.partialRight`. - * - * @private - * @param {Array} args The provided arguments. - * @param {Array} partials The arguments to append to those provided. - * @param {Array} holders The `partials` placeholder indexes. - * @params {boolean} [isCurried] Specify composing for a curried function. - * @returns {Array} Returns the new array of composed arguments. - */ - function composeArgsRight(args, partials, holders, isCurried) { - var argsIndex = -1, - argsLength = args.length, - holdersIndex = -1, - holdersLength = holders.length, - rightIndex = -1, - rightLength = partials.length, - rangeLength = nativeMax(argsLength - holdersLength, 0), - result = Array(rangeLength + rightLength), - isUncurried = !isCurried; - - while (++argsIndex < rangeLength) { - result[argsIndex] = args[argsIndex]; - } - var offset = argsIndex; - while (++rightIndex < rightLength) { - result[offset + rightIndex] = partials[rightIndex]; - } - while (++holdersIndex < holdersLength) { - if (isUncurried || argsIndex < argsLength) { - result[offset + holders[holdersIndex]] = args[argsIndex++]; - } - } - return result; - } - - /** - * Copies the values of `source` to `array`. - * - * @private - * @param {Array} source The array to copy values from. - * @param {Array} [array=[]] The array to copy values to. - * @returns {Array} Returns `array`. - */ - function copyArray(source, array) { - var index = -1, - length = source.length; - - array || (array = Array(length)); - while (++index < length) { - array[index] = source[index]; - } - return array; - } - - /** - * Copies properties of `source` to `object`. - * - * @private - * @param {Object} source The object to copy properties from. - * @param {Array} props The property identifiers to copy. - * @param {Object} [object={}] The object to copy properties to. - * @param {Function} [customizer] The function to customize copied values. - * @returns {Object} Returns `object`. - */ - function copyObject(source, props, object, customizer) { - var isNew = !object; - object || (object = {}); - - var index = -1, - length = props.length; - - while (++index < length) { - var key = props[index]; - - var newValue = customizer - ? customizer(object[key], source[key], key, object, source) - : undefined; - - if (newValue === undefined) { - newValue = source[key]; - } - if (isNew) { - baseAssignValue(object, key, newValue); - } else { - assignValue(object, key, newValue); - } - } - return object; - } - - /** - * Copies own symbols of `source` to `object`. - * - * @private - * @param {Object} source The object to copy symbols from. - * @param {Object} [object={}] The object to copy symbols to. - * @returns {Object} Returns `object`. - */ - function copySymbols(source, object) { - return copyObject(source, getSymbols(source), object); - } - - /** - * Copies own and inherited symbols of `source` to `object`. - * - * @private - * @param {Object} source The object to copy symbols from. - * @param {Object} [object={}] The object to copy symbols to. - * @returns {Object} Returns `object`. - */ - function copySymbolsIn(source, object) { - return copyObject(source, getSymbolsIn(source), object); - } - - /** - * Creates a function like `_.groupBy`. - * - * @private - * @param {Function} setter The function to set accumulator values. - * @param {Function} [initializer] The accumulator object initializer. - * @returns {Function} Returns the new aggregator function. - */ - function createAggregator(setter, initializer) { - return function(collection, iteratee) { - var func = isArray(collection) ? arrayAggregator : baseAggregator, - accumulator = initializer ? initializer() : {}; - - return func(collection, setter, getIteratee(iteratee, 2), accumulator); - }; - } - - /** - * Creates a function like `_.assign`. - * - * @private - * @param {Function} assigner The function to assign values. - * @returns {Function} Returns the new assigner function. - */ - function createAssigner(assigner) { - return baseRest(function(object, sources) { - var index = -1, - length = sources.length, - customizer = length > 1 ? sources[length - 1] : undefined, - guard = length > 2 ? sources[2] : undefined; - - customizer = (assigner.length > 3 && typeof customizer == 'function') - ? (length--, customizer) - : undefined; - - if (guard && isIterateeCall(sources[0], sources[1], guard)) { - customizer = length < 3 ? undefined : customizer; - length = 1; - } - object = Object(object); - while (++index < length) { - var source = sources[index]; - if (source) { - assigner(object, source, index, customizer); - } - } - return object; - }); - } - - /** - * Creates a `baseEach` or `baseEachRight` function. - * - * @private - * @param {Function} eachFunc The function to iterate over a collection. - * @param {boolean} [fromRight] Specify iterating from right to left. - * @returns {Function} Returns the new base function. - */ - function createBaseEach(eachFunc, fromRight) { - return function(collection, iteratee) { - if (collection == null) { - return collection; - } - if (!isArrayLike(collection)) { - return eachFunc(collection, iteratee); - } - var length = collection.length, - index = fromRight ? length : -1, - iterable = Object(collection); - - while ((fromRight ? index-- : ++index < length)) { - if (iteratee(iterable[index], index, iterable) === false) { - break; - } - } - return collection; - }; - } - - /** - * Creates a base function for methods like `_.forIn` and `_.forOwn`. - * - * @private - * @param {boolean} [fromRight] Specify iterating from right to left. - * @returns {Function} Returns the new base function. - */ - function createBaseFor(fromRight) { - return function(object, iteratee, keysFunc) { - var index = -1, - iterable = Object(object), - props = keysFunc(object), - length = props.length; - - while (length--) { - var key = props[fromRight ? length : ++index]; - if (iteratee(iterable[key], key, iterable) === false) { - break; - } - } - return object; - }; - } - - /** - * Creates a function that wraps `func` to invoke it with the optional `this` - * binding of `thisArg`. - * - * @private - * @param {Function} func The function to wrap. - * @param {number} bitmask The bitmask flags. See `createWrap` for more details. - * @param {*} [thisArg] The `this` binding of `func`. - * @returns {Function} Returns the new wrapped function. - */ - function createBind(func, bitmask, thisArg) { - var isBind = bitmask & WRAP_BIND_FLAG, - Ctor = createCtor(func); - - function wrapper() { - var fn = (this && this !== root && this instanceof wrapper) ? Ctor : func; - return fn.apply(isBind ? thisArg : this, arguments); - } - return wrapper; - } - - /** - * Creates a function like `_.lowerFirst`. - * - * @private - * @param {string} methodName The name of the `String` case method to use. - * @returns {Function} Returns the new case function. - */ - function createCaseFirst(methodName) { - return function(string) { - string = toString(string); - - var strSymbols = hasUnicode(string) - ? stringToArray(string) - : undefined; - - var chr = strSymbols - ? strSymbols[0] - : string.charAt(0); - - var trailing = strSymbols - ? castSlice(strSymbols, 1).join('') - : string.slice(1); - - return chr[methodName]() + trailing; - }; - } - - /** - * Creates a function like `_.camelCase`. - * - * @private - * @param {Function} callback The function to combine each word. - * @returns {Function} Returns the new compounder function. - */ - function createCompounder(callback) { - return function(string) { - return arrayReduce(words(deburr(string).replace(reApos, '')), callback, ''); - }; - } - - /** - * Creates a function that produces an instance of `Ctor` regardless of - * whether it was invoked as part of a `new` expression or by `call` or `apply`. - * - * @private - * @param {Function} Ctor The constructor to wrap. - * @returns {Function} Returns the new wrapped function. - */ - function createCtor(Ctor) { - return function() { - // Use a `switch` statement to work with class constructors. See - // http://ecma-international.org/ecma-262/7.0/#sec-ecmascript-function-objects-call-thisargument-argumentslist - // for more details. - var args = arguments; - switch (args.length) { - case 0: return new Ctor; - case 1: return new Ctor(args[0]); - case 2: return new Ctor(args[0], args[1]); - case 3: return new Ctor(args[0], args[1], args[2]); - case 4: return new Ctor(args[0], args[1], args[2], args[3]); - case 5: return new Ctor(args[0], args[1], args[2], args[3], args[4]); - case 6: return new Ctor(args[0], args[1], args[2], args[3], args[4], args[5]); - case 7: return new Ctor(args[0], args[1], args[2], args[3], args[4], args[5], args[6]); - } - var thisBinding = baseCreate(Ctor.prototype), - result = Ctor.apply(thisBinding, args); - - // Mimic the constructor's `return` behavior. - // See https://es5.github.io/#x13.2.2 for more details. - return isObject(result) ? result : thisBinding; - }; - } - - /** - * Creates a function that wraps `func` to enable currying. - * - * @private - * @param {Function} func The function to wrap. - * @param {number} bitmask The bitmask flags. See `createWrap` for more details. - * @param {number} arity The arity of `func`. - * @returns {Function} Returns the new wrapped function. - */ - function createCurry(func, bitmask, arity) { - var Ctor = createCtor(func); - - function wrapper() { - var length = arguments.length, - args = Array(length), - index = length, - placeholder = getHolder(wrapper); - - while (index--) { - args[index] = arguments[index]; - } - var holders = (length < 3 && args[0] !== placeholder && args[length - 1] !== placeholder) - ? [] - : replaceHolders(args, placeholder); - - length -= holders.length; - if (length < arity) { - return createRecurry( - func, bitmask, createHybrid, wrapper.placeholder, undefined, - args, holders, undefined, undefined, arity - length); - } - var fn = (this && this !== root && this instanceof wrapper) ? Ctor : func; - return apply(fn, this, args); - } - return wrapper; - } - - /** - * Creates a `_.find` or `_.findLast` function. - * - * @private - * @param {Function} findIndexFunc The function to find the collection index. - * @returns {Function} Returns the new find function. - */ - function createFind(findIndexFunc) { - return function(collection, predicate, fromIndex) { - var iterable = Object(collection); - if (!isArrayLike(collection)) { - var iteratee = getIteratee(predicate, 3); - collection = keys(collection); - predicate = function(key) { return iteratee(iterable[key], key, iterable); }; - } - var index = findIndexFunc(collection, predicate, fromIndex); - return index > -1 ? iterable[iteratee ? collection[index] : index] : undefined; - }; - } - - /** - * Creates a `_.flow` or `_.flowRight` function. - * - * @private - * @param {boolean} [fromRight] Specify iterating from right to left. - * @returns {Function} Returns the new flow function. - */ - function createFlow(fromRight) { - return flatRest(function(funcs) { - var length = funcs.length, - index = length, - prereq = LodashWrapper.prototype.thru; - - if (fromRight) { - funcs.reverse(); - } - while (index--) { - var func = funcs[index]; - if (typeof func != 'function') { - throw new TypeError(FUNC_ERROR_TEXT); - } - if (prereq && !wrapper && getFuncName(func) == 'wrapper') { - var wrapper = new LodashWrapper([], true); - } - } - index = wrapper ? index : length; - while (++index < length) { - func = funcs[index]; - - var funcName = getFuncName(func), - data = funcName == 'wrapper' ? getData(func) : undefined; - - if (data && isLaziable(data[0]) && - data[1] == (WRAP_ARY_FLAG | WRAP_CURRY_FLAG | WRAP_PARTIAL_FLAG | WRAP_REARG_FLAG) && - !data[4].length && data[9] == 1 - ) { - wrapper = wrapper[getFuncName(data[0])].apply(wrapper, data[3]); - } else { - wrapper = (func.length == 1 && isLaziable(func)) - ? wrapper[funcName]() - : wrapper.thru(func); - } - } - return function() { - var args = arguments, - value = args[0]; - - if (wrapper && args.length == 1 && isArray(value)) { - return wrapper.plant(value).value(); - } - var index = 0, - result = length ? funcs[index].apply(this, args) : value; - - while (++index < length) { - result = funcs[index].call(this, result); - } - return result; - }; - }); - } - - /** - * Creates a function that wraps `func` to invoke it with optional `this` - * binding of `thisArg`, partial application, and currying. - * - * @private - * @param {Function|string} func The function or method name to wrap. - * @param {number} bitmask The bitmask flags. See `createWrap` for more details. - * @param {*} [thisArg] The `this` binding of `func`. - * @param {Array} [partials] The arguments to prepend to those provided to - * the new function. - * @param {Array} [holders] The `partials` placeholder indexes. - * @param {Array} [partialsRight] The arguments to append to those provided - * to the new function. - * @param {Array} [holdersRight] The `partialsRight` placeholder indexes. - * @param {Array} [argPos] The argument positions of the new function. - * @param {number} [ary] The arity cap of `func`. - * @param {number} [arity] The arity of `func`. - * @returns {Function} Returns the new wrapped function. - */ - function createHybrid(func, bitmask, thisArg, partials, holders, partialsRight, holdersRight, argPos, ary, arity) { - var isAry = bitmask & WRAP_ARY_FLAG, - isBind = bitmask & WRAP_BIND_FLAG, - isBindKey = bitmask & WRAP_BIND_KEY_FLAG, - isCurried = bitmask & (WRAP_CURRY_FLAG | WRAP_CURRY_RIGHT_FLAG), - isFlip = bitmask & WRAP_FLIP_FLAG, - Ctor = isBindKey ? undefined : createCtor(func); - - function wrapper() { - var length = arguments.length, - args = Array(length), - index = length; - - while (index--) { - args[index] = arguments[index]; - } - if (isCurried) { - var placeholder = getHolder(wrapper), - holdersCount = countHolders(args, placeholder); - } - if (partials) { - args = composeArgs(args, partials, holders, isCurried); - } - if (partialsRight) { - args = composeArgsRight(args, partialsRight, holdersRight, isCurried); - } - length -= holdersCount; - if (isCurried && length < arity) { - var newHolders = replaceHolders(args, placeholder); - return createRecurry( - func, bitmask, createHybrid, wrapper.placeholder, thisArg, - args, newHolders, argPos, ary, arity - length - ); - } - var thisBinding = isBind ? thisArg : this, - fn = isBindKey ? thisBinding[func] : func; - - length = args.length; - if (argPos) { - args = reorder(args, argPos); - } else if (isFlip && length > 1) { - args.reverse(); - } - if (isAry && ary < length) { - args.length = ary; - } - if (this && this !== root && this instanceof wrapper) { - fn = Ctor || createCtor(fn); - } - return fn.apply(thisBinding, args); - } - return wrapper; - } - - /** - * Creates a function like `_.invertBy`. - * - * @private - * @param {Function} setter The function to set accumulator values. - * @param {Function} toIteratee The function to resolve iteratees. - * @returns {Function} Returns the new inverter function. - */ - function createInverter(setter, toIteratee) { - return function(object, iteratee) { - return baseInverter(object, setter, toIteratee(iteratee), {}); - }; - } - - /** - * Creates a function that performs a mathematical operation on two values. - * - * @private - * @param {Function} operator The function to perform the operation. - * @param {number} [defaultValue] The value used for `undefined` arguments. - * @returns {Function} Returns the new mathematical operation function. - */ - function createMathOperation(operator, defaultValue) { - return function(value, other) { - var result; - if (value === undefined && other === undefined) { - return defaultValue; - } - if (value !== undefined) { - result = value; - } - if (other !== undefined) { - if (result === undefined) { - return other; - } - if (typeof value == 'string' || typeof other == 'string') { - value = baseToString(value); - other = baseToString(other); - } else { - value = baseToNumber(value); - other = baseToNumber(other); - } - result = operator(value, other); - } - return result; - }; - } - - /** - * Creates a function like `_.over`. - * - * @private - * @param {Function} arrayFunc The function to iterate over iteratees. - * @returns {Function} Returns the new over function. - */ - function createOver(arrayFunc) { - return flatRest(function(iteratees) { - iteratees = arrayMap(iteratees, baseUnary(getIteratee())); - return baseRest(function(args) { - var thisArg = this; - return arrayFunc(iteratees, function(iteratee) { - return apply(iteratee, thisArg, args); - }); - }); - }); - } - - /** - * Creates the padding for `string` based on `length`. The `chars` string - * is truncated if the number of characters exceeds `length`. - * - * @private - * @param {number} length The padding length. - * @param {string} [chars=' '] The string used as padding. - * @returns {string} Returns the padding for `string`. - */ - function createPadding(length, chars) { - chars = chars === undefined ? ' ' : baseToString(chars); - - var charsLength = chars.length; - if (charsLength < 2) { - return charsLength ? baseRepeat(chars, length) : chars; - } - var result = baseRepeat(chars, nativeCeil(length / stringSize(chars))); - return hasUnicode(chars) - ? castSlice(stringToArray(result), 0, length).join('') - : result.slice(0, length); - } - - /** - * Creates a function that wraps `func` to invoke it with the `this` binding - * of `thisArg` and `partials` prepended to the arguments it receives. - * - * @private - * @param {Function} func The function to wrap. - * @param {number} bitmask The bitmask flags. See `createWrap` for more details. - * @param {*} thisArg The `this` binding of `func`. - * @param {Array} partials The arguments to prepend to those provided to - * the new function. - * @returns {Function} Returns the new wrapped function. - */ - function createPartial(func, bitmask, thisArg, partials) { - var isBind = bitmask & WRAP_BIND_FLAG, - Ctor = createCtor(func); - - function wrapper() { - var argsIndex = -1, - argsLength = arguments.length, - leftIndex = -1, - leftLength = partials.length, - args = Array(leftLength + argsLength), - fn = (this && this !== root && this instanceof wrapper) ? Ctor : func; - - while (++leftIndex < leftLength) { - args[leftIndex] = partials[leftIndex]; - } - while (argsLength--) { - args[leftIndex++] = arguments[++argsIndex]; - } - return apply(fn, isBind ? thisArg : this, args); - } - return wrapper; - } - - /** - * Creates a `_.range` or `_.rangeRight` function. - * - * @private - * @param {boolean} [fromRight] Specify iterating from right to left. - * @returns {Function} Returns the new range function. - */ - function createRange(fromRight) { - return function(start, end, step) { - if (step && typeof step != 'number' && isIterateeCall(start, end, step)) { - end = step = undefined; - } - // Ensure the sign of `-0` is preserved. - start = toFinite(start); - if (end === undefined) { - end = start; - start = 0; - } else { - end = toFinite(end); - } - step = step === undefined ? (start < end ? 1 : -1) : toFinite(step); - return baseRange(start, end, step, fromRight); - }; - } - - /** - * Creates a function that performs a relational operation on two values. - * - * @private - * @param {Function} operator The function to perform the operation. - * @returns {Function} Returns the new relational operation function. - */ - function createRelationalOperation(operator) { - return function(value, other) { - if (!(typeof value == 'string' && typeof other == 'string')) { - value = toNumber(value); - other = toNumber(other); - } - return operator(value, other); - }; - } - - /** - * Creates a function that wraps `func` to continue currying. - * - * @private - * @param {Function} func The function to wrap. - * @param {number} bitmask The bitmask flags. See `createWrap` for more details. - * @param {Function} wrapFunc The function to create the `func` wrapper. - * @param {*} placeholder The placeholder value. - * @param {*} [thisArg] The `this` binding of `func`. - * @param {Array} [partials] The arguments to prepend to those provided to - * the new function. - * @param {Array} [holders] The `partials` placeholder indexes. - * @param {Array} [argPos] The argument positions of the new function. - * @param {number} [ary] The arity cap of `func`. - * @param {number} [arity] The arity of `func`. - * @returns {Function} Returns the new wrapped function. - */ - function createRecurry(func, bitmask, wrapFunc, placeholder, thisArg, partials, holders, argPos, ary, arity) { - var isCurry = bitmask & WRAP_CURRY_FLAG, - newHolders = isCurry ? holders : undefined, - newHoldersRight = isCurry ? undefined : holders, - newPartials = isCurry ? partials : undefined, - newPartialsRight = isCurry ? undefined : partials; - - bitmask |= (isCurry ? WRAP_PARTIAL_FLAG : WRAP_PARTIAL_RIGHT_FLAG); - bitmask &= ~(isCurry ? WRAP_PARTIAL_RIGHT_FLAG : WRAP_PARTIAL_FLAG); - - if (!(bitmask & WRAP_CURRY_BOUND_FLAG)) { - bitmask &= ~(WRAP_BIND_FLAG | WRAP_BIND_KEY_FLAG); - } - var newData = [ - func, bitmask, thisArg, newPartials, newHolders, newPartialsRight, - newHoldersRight, argPos, ary, arity - ]; - - var result = wrapFunc.apply(undefined, newData); - if (isLaziable(func)) { - setData(result, newData); - } - result.placeholder = placeholder; - return setWrapToString(result, func, bitmask); - } - - /** - * Creates a function like `_.round`. - * - * @private - * @param {string} methodName The name of the `Math` method to use when rounding. - * @returns {Function} Returns the new round function. - */ - function createRound(methodName) { - var func = Math[methodName]; - return function(number, precision) { - number = toNumber(number); - precision = precision == null ? 0 : nativeMin(toInteger(precision), 292); - if (precision) { - // Shift with exponential notation to avoid floating-point issues. - // See [MDN](https://mdn.io/round#Examples) for more details. - var pair = (toString(number) + 'e').split('e'), - value = func(pair[0] + 'e' + (+pair[1] + precision)); - - pair = (toString(value) + 'e').split('e'); - return +(pair[0] + 'e' + (+pair[1] - precision)); - } - return func(number); - }; - } - - /** - * Creates a set object of `values`. - * - * @private - * @param {Array} values The values to add to the set. - * @returns {Object} Returns the new set. - */ - var createSet = !(Set && (1 / setToArray(new Set([,-0]))[1]) == INFINITY) ? noop : function(values) { - return new Set(values); - }; - - /** - * Creates a `_.toPairs` or `_.toPairsIn` function. - * - * @private - * @param {Function} keysFunc The function to get the keys of a given object. - * @returns {Function} Returns the new pairs function. - */ - function createToPairs(keysFunc) { - return function(object) { - var tag = getTag(object); - if (tag == mapTag) { - return mapToArray(object); - } - if (tag == setTag) { - return setToPairs(object); - } - return baseToPairs(object, keysFunc(object)); - }; - } - - /** - * Creates a function that either curries or invokes `func` with optional - * `this` binding and partially applied arguments. - * - * @private - * @param {Function|string} func The function or method name to wrap. - * @param {number} bitmask The bitmask flags. - * 1 - `_.bind` - * 2 - `_.bindKey` - * 4 - `_.curry` or `_.curryRight` of a bound function - * 8 - `_.curry` - * 16 - `_.curryRight` - * 32 - `_.partial` - * 64 - `_.partialRight` - * 128 - `_.rearg` - * 256 - `_.ary` - * 512 - `_.flip` - * @param {*} [thisArg] The `this` binding of `func`. - * @param {Array} [partials] The arguments to be partially applied. - * @param {Array} [holders] The `partials` placeholder indexes. - * @param {Array} [argPos] The argument positions of the new function. - * @param {number} [ary] The arity cap of `func`. - * @param {number} [arity] The arity of `func`. - * @returns {Function} Returns the new wrapped function. - */ - function createWrap(func, bitmask, thisArg, partials, holders, argPos, ary, arity) { - var isBindKey = bitmask & WRAP_BIND_KEY_FLAG; - if (!isBindKey && typeof func != 'function') { - throw new TypeError(FUNC_ERROR_TEXT); - } - var length = partials ? partials.length : 0; - if (!length) { - bitmask &= ~(WRAP_PARTIAL_FLAG | WRAP_PARTIAL_RIGHT_FLAG); - partials = holders = undefined; - } - ary = ary === undefined ? ary : nativeMax(toInteger(ary), 0); - arity = arity === undefined ? arity : toInteger(arity); - length -= holders ? holders.length : 0; - - if (bitmask & WRAP_PARTIAL_RIGHT_FLAG) { - var partialsRight = partials, - holdersRight = holders; - - partials = holders = undefined; - } - var data = isBindKey ? undefined : getData(func); - - var newData = [ - func, bitmask, thisArg, partials, holders, partialsRight, holdersRight, - argPos, ary, arity - ]; - - if (data) { - mergeData(newData, data); - } - func = newData[0]; - bitmask = newData[1]; - thisArg = newData[2]; - partials = newData[3]; - holders = newData[4]; - arity = newData[9] = newData[9] === undefined - ? (isBindKey ? 0 : func.length) - : nativeMax(newData[9] - length, 0); - - if (!arity && bitmask & (WRAP_CURRY_FLAG | WRAP_CURRY_RIGHT_FLAG)) { - bitmask &= ~(WRAP_CURRY_FLAG | WRAP_CURRY_RIGHT_FLAG); - } - if (!bitmask || bitmask == WRAP_BIND_FLAG) { - var result = createBind(func, bitmask, thisArg); - } else if (bitmask == WRAP_CURRY_FLAG || bitmask == WRAP_CURRY_RIGHT_FLAG) { - result = createCurry(func, bitmask, arity); - } else if ((bitmask == WRAP_PARTIAL_FLAG || bitmask == (WRAP_BIND_FLAG | WRAP_PARTIAL_FLAG)) && !holders.length) { - result = createPartial(func, bitmask, thisArg, partials); - } else { - result = createHybrid.apply(undefined, newData); - } - var setter = data ? baseSetData : setData; - return setWrapToString(setter(result, newData), func, bitmask); - } - - /** - * Used by `_.defaults` to customize its `_.assignIn` use to assign properties - * of source objects to the destination object for all destination properties - * that resolve to `undefined`. - * - * @private - * @param {*} objValue The destination value. - * @param {*} srcValue The source value. - * @param {string} key The key of the property to assign. - * @param {Object} object The parent object of `objValue`. - * @returns {*} Returns the value to assign. - */ - function customDefaultsAssignIn(objValue, srcValue, key, object) { - if (objValue === undefined || - (eq(objValue, objectProto[key]) && !hasOwnProperty.call(object, key))) { - return srcValue; - } - return objValue; - } - - /** - * Used by `_.defaultsDeep` to customize its `_.merge` use to merge source - * objects into destination objects that are passed thru. - * - * @private - * @param {*} objValue The destination value. - * @param {*} srcValue The source value. - * @param {string} key The key of the property to merge. - * @param {Object} object The parent object of `objValue`. - * @param {Object} source The parent object of `srcValue`. - * @param {Object} [stack] Tracks traversed source values and their merged - * counterparts. - * @returns {*} Returns the value to assign. - */ - function customDefaultsMerge(objValue, srcValue, key, object, source, stack) { - if (isObject(objValue) && isObject(srcValue)) { - // Recursively merge objects and arrays (susceptible to call stack limits). - stack.set(srcValue, objValue); - baseMerge(objValue, srcValue, undefined, customDefaultsMerge, stack); - stack['delete'](srcValue); - } - return objValue; - } - - /** - * Used by `_.omit` to customize its `_.cloneDeep` use to only clone plain - * objects. - * - * @private - * @param {*} value The value to inspect. - * @param {string} key The key of the property to inspect. - * @returns {*} Returns the uncloned value or `undefined` to defer cloning to `_.cloneDeep`. - */ - function customOmitClone(value) { - return isPlainObject(value) ? undefined : value; - } - - /** - * A specialized version of `baseIsEqualDeep` for arrays with support for - * partial deep comparisons. - * - * @private - * @param {Array} array The array to compare. - * @param {Array} other The other array to compare. - * @param {number} bitmask The bitmask flags. See `baseIsEqual` for more details. - * @param {Function} customizer The function to customize comparisons. - * @param {Function} equalFunc The function to determine equivalents of values. - * @param {Object} stack Tracks traversed `array` and `other` objects. - * @returns {boolean} Returns `true` if the arrays are equivalent, else `false`. - */ - function equalArrays(array, other, bitmask, customizer, equalFunc, stack) { - var isPartial = bitmask & COMPARE_PARTIAL_FLAG, - arrLength = array.length, - othLength = other.length; - - if (arrLength != othLength && !(isPartial && othLength > arrLength)) { - return false; - } - // Assume cyclic values are equal. - var stacked = stack.get(array); - if (stacked && stack.get(other)) { - return stacked == other; - } - var index = -1, - result = true, - seen = (bitmask & COMPARE_UNORDERED_FLAG) ? new SetCache : undefined; - - stack.set(array, other); - stack.set(other, array); - - // Ignore non-index properties. - while (++index < arrLength) { - var arrValue = array[index], - othValue = other[index]; - - if (customizer) { - var compared = isPartial - ? customizer(othValue, arrValue, index, other, array, stack) - : customizer(arrValue, othValue, index, array, other, stack); - } - if (compared !== undefined) { - if (compared) { - continue; - } - result = false; - break; - } - // Recursively compare arrays (susceptible to call stack limits). - if (seen) { - if (!arraySome(other, function(othValue, othIndex) { - if (!cacheHas(seen, othIndex) && - (arrValue === othValue || equalFunc(arrValue, othValue, bitmask, customizer, stack))) { - return seen.push(othIndex); - } - })) { - result = false; - break; - } - } else if (!( - arrValue === othValue || - equalFunc(arrValue, othValue, bitmask, customizer, stack) - )) { - result = false; - break; - } - } - stack['delete'](array); - stack['delete'](other); - return result; - } - - /** - * A specialized version of `baseIsEqualDeep` for comparing objects of - * the same `toStringTag`. - * - * **Note:** This function only supports comparing values with tags of - * `Boolean`, `Date`, `Error`, `Number`, `RegExp`, or `String`. - * - * @private - * @param {Object} object The object to compare. - * @param {Object} other The other object to compare. - * @param {string} tag The `toStringTag` of the objects to compare. - * @param {number} bitmask The bitmask flags. See `baseIsEqual` for more details. - * @param {Function} customizer The function to customize comparisons. - * @param {Function} equalFunc The function to determine equivalents of values. - * @param {Object} stack Tracks traversed `object` and `other` objects. - * @returns {boolean} Returns `true` if the objects are equivalent, else `false`. - */ - function equalByTag(object, other, tag, bitmask, customizer, equalFunc, stack) { - switch (tag) { - case dataViewTag: - if ((object.byteLength != other.byteLength) || - (object.byteOffset != other.byteOffset)) { - return false; - } - object = object.buffer; - other = other.buffer; - - case arrayBufferTag: - if ((object.byteLength != other.byteLength) || - !equalFunc(new Uint8Array(object), new Uint8Array(other))) { - return false; - } - return true; - - case boolTag: - case dateTag: - case numberTag: - // Coerce booleans to `1` or `0` and dates to milliseconds. - // Invalid dates are coerced to `NaN`. - return eq(+object, +other); - - case errorTag: - return object.name == other.name && object.message == other.message; - - case regexpTag: - case stringTag: - // Coerce regexes to strings and treat strings, primitives and objects, - // as equal. See http://www.ecma-international.org/ecma-262/7.0/#sec-regexp.prototype.tostring - // for more details. - return object == (other + ''); - - case mapTag: - var convert = mapToArray; - - case setTag: - var isPartial = bitmask & COMPARE_PARTIAL_FLAG; - convert || (convert = setToArray); - - if (object.size != other.size && !isPartial) { - return false; - } - // Assume cyclic values are equal. - var stacked = stack.get(object); - if (stacked) { - return stacked == other; - } - bitmask |= COMPARE_UNORDERED_FLAG; - - // Recursively compare objects (susceptible to call stack limits). - stack.set(object, other); - var result = equalArrays(convert(object), convert(other), bitmask, customizer, equalFunc, stack); - stack['delete'](object); - return result; - - case symbolTag: - if (symbolValueOf) { - return symbolValueOf.call(object) == symbolValueOf.call(other); - } - } - return false; - } - - /** - * A specialized version of `baseIsEqualDeep` for objects with support for - * partial deep comparisons. - * - * @private - * @param {Object} object The object to compare. - * @param {Object} other The other object to compare. - * @param {number} bitmask The bitmask flags. See `baseIsEqual` for more details. - * @param {Function} customizer The function to customize comparisons. - * @param {Function} equalFunc The function to determine equivalents of values. - * @param {Object} stack Tracks traversed `object` and `other` objects. - * @returns {boolean} Returns `true` if the objects are equivalent, else `false`. - */ - function equalObjects(object, other, bitmask, customizer, equalFunc, stack) { - var isPartial = bitmask & COMPARE_PARTIAL_FLAG, - objProps = getAllKeys(object), - objLength = objProps.length, - othProps = getAllKeys(other), - othLength = othProps.length; - - if (objLength != othLength && !isPartial) { - return false; - } - var index = objLength; - while (index--) { - var key = objProps[index]; - if (!(isPartial ? key in other : hasOwnProperty.call(other, key))) { - return false; - } - } - // Assume cyclic values are equal. - var stacked = stack.get(object); - if (stacked && stack.get(other)) { - return stacked == other; - } - var result = true; - stack.set(object, other); - stack.set(other, object); - - var skipCtor = isPartial; - while (++index < objLength) { - key = objProps[index]; - var objValue = object[key], - othValue = other[key]; - - if (customizer) { - var compared = isPartial - ? customizer(othValue, objValue, key, other, object, stack) - : customizer(objValue, othValue, key, object, other, stack); - } - // Recursively compare objects (susceptible to call stack limits). - if (!(compared === undefined - ? (objValue === othValue || equalFunc(objValue, othValue, bitmask, customizer, stack)) - : compared - )) { - result = false; - break; - } - skipCtor || (skipCtor = key == 'constructor'); - } - if (result && !skipCtor) { - var objCtor = object.constructor, - othCtor = other.constructor; - - // Non `Object` object instances with different constructors are not equal. - if (objCtor != othCtor && - ('constructor' in object && 'constructor' in other) && - !(typeof objCtor == 'function' && objCtor instanceof objCtor && - typeof othCtor == 'function' && othCtor instanceof othCtor)) { - result = false; - } - } - stack['delete'](object); - stack['delete'](other); - return result; - } - - /** - * A specialized version of `baseRest` which flattens the rest array. - * - * @private - * @param {Function} func The function to apply a rest parameter to. - * @returns {Function} Returns the new function. - */ - function flatRest(func) { - return setToString(overRest(func, undefined, flatten), func + ''); - } - - /** - * Creates an array of own enumerable property names and symbols of `object`. - * - * @private - * @param {Object} object The object to query. - * @returns {Array} Returns the array of property names and symbols. - */ - function getAllKeys(object) { - return baseGetAllKeys(object, keys, getSymbols); - } - - /** - * Creates an array of own and inherited enumerable property names and - * symbols of `object`. - * - * @private - * @param {Object} object The object to query. - * @returns {Array} Returns the array of property names and symbols. - */ - function getAllKeysIn(object) { - return baseGetAllKeys(object, keysIn, getSymbolsIn); - } - - /** - * Gets metadata for `func`. - * - * @private - * @param {Function} func The function to query. - * @returns {*} Returns the metadata for `func`. - */ - var getData = !metaMap ? noop : function(func) { - return metaMap.get(func); - }; - - /** - * Gets the name of `func`. - * - * @private - * @param {Function} func The function to query. - * @returns {string} Returns the function name. - */ - function getFuncName(func) { - var result = (func.name + ''), - array = realNames[result], - length = hasOwnProperty.call(realNames, result) ? array.length : 0; - - while (length--) { - var data = array[length], - otherFunc = data.func; - if (otherFunc == null || otherFunc == func) { - return data.name; - } - } - return result; - } - - /** - * Gets the argument placeholder value for `func`. - * - * @private - * @param {Function} func The function to inspect. - * @returns {*} Returns the placeholder value. - */ - function getHolder(func) { - var object = hasOwnProperty.call(lodash, 'placeholder') ? lodash : func; - return object.placeholder; - } - - /** - * Gets the appropriate "iteratee" function. If `_.iteratee` is customized, - * this function returns the custom method, otherwise it returns `baseIteratee`. - * If arguments are provided, the chosen function is invoked with them and - * its result is returned. - * - * @private - * @param {*} [value] The value to convert to an iteratee. - * @param {number} [arity] The arity of the created iteratee. - * @returns {Function} Returns the chosen function or its result. - */ - function getIteratee() { - var result = lodash.iteratee || iteratee; - result = result === iteratee ? baseIteratee : result; - return arguments.length ? result(arguments[0], arguments[1]) : result; - } - - /** - * Gets the data for `map`. - * - * @private - * @param {Object} map The map to query. - * @param {string} key The reference key. - * @returns {*} Returns the map data. - */ - function getMapData(map, key) { - var data = map.__data__; - return isKeyable(key) - ? data[typeof key == 'string' ? 'string' : 'hash'] - : data.map; - } - - /** - * Gets the property names, values, and compare flags of `object`. - * - * @private - * @param {Object} object The object to query. - * @returns {Array} Returns the match data of `object`. - */ - function getMatchData(object) { - var result = keys(object), - length = result.length; - - while (length--) { - var key = result[length], - value = object[key]; - - result[length] = [key, value, isStrictComparable(value)]; - } - return result; - } - - /** - * Gets the native function at `key` of `object`. - * - * @private - * @param {Object} object The object to query. - * @param {string} key The key of the method to get. - * @returns {*} Returns the function if it's native, else `undefined`. - */ - function getNative(object, key) { - var value = getValue(object, key); - return baseIsNative(value) ? value : undefined; - } - - /** - * A specialized version of `baseGetTag` which ignores `Symbol.toStringTag` values. - * - * @private - * @param {*} value The value to query. - * @returns {string} Returns the raw `toStringTag`. - */ - function getRawTag(value) { - var isOwn = hasOwnProperty.call(value, symToStringTag), - tag = value[symToStringTag]; - - try { - value[symToStringTag] = undefined; - var unmasked = true; - } catch (e) {} - - var result = nativeObjectToString.call(value); - if (unmasked) { - if (isOwn) { - value[symToStringTag] = tag; - } else { - delete value[symToStringTag]; - } - } - return result; - } - - /** - * Creates an array of the own enumerable symbols of `object`. - * - * @private - * @param {Object} object The object to query. - * @returns {Array} Returns the array of symbols. - */ - var getSymbols = !nativeGetSymbols ? stubArray : function(object) { - if (object == null) { - return []; - } - object = Object(object); - return arrayFilter(nativeGetSymbols(object), function(symbol) { - return propertyIsEnumerable.call(object, symbol); - }); - }; - - /** - * Creates an array of the own and inherited enumerable symbols of `object`. - * - * @private - * @param {Object} object The object to query. - * @returns {Array} Returns the array of symbols. - */ - var getSymbolsIn = !nativeGetSymbols ? stubArray : function(object) { - var result = []; - while (object) { - arrayPush(result, getSymbols(object)); - object = getPrototype(object); - } - return result; - }; - - /** - * Gets the `toStringTag` of `value`. - * - * @private - * @param {*} value The value to query. - * @returns {string} Returns the `toStringTag`. - */ - var getTag = baseGetTag; - - // Fallback for data views, maps, sets, and weak maps in IE 11 and promises in Node.js < 6. - if ((DataView && getTag(new DataView(new ArrayBuffer(1))) != dataViewTag) || - (Map && getTag(new Map) != mapTag) || - (Promise && getTag(Promise.resolve()) != promiseTag) || - (Set && getTag(new Set) != setTag) || - (WeakMap && getTag(new WeakMap) != weakMapTag)) { - getTag = function(value) { - var result = baseGetTag(value), - Ctor = result == objectTag ? value.constructor : undefined, - ctorString = Ctor ? toSource(Ctor) : ''; - - if (ctorString) { - switch (ctorString) { - case dataViewCtorString: return dataViewTag; - case mapCtorString: return mapTag; - case promiseCtorString: return promiseTag; - case setCtorString: return setTag; - case weakMapCtorString: return weakMapTag; - } - } - return result; - }; - } - - /** - * Gets the view, applying any `transforms` to the `start` and `end` positions. - * - * @private - * @param {number} start The start of the view. - * @param {number} end The end of the view. - * @param {Array} transforms The transformations to apply to the view. - * @returns {Object} Returns an object containing the `start` and `end` - * positions of the view. - */ - function getView(start, end, transforms) { - var index = -1, - length = transforms.length; - - while (++index < length) { - var data = transforms[index], - size = data.size; - - switch (data.type) { - case 'drop': start += size; break; - case 'dropRight': end -= size; break; - case 'take': end = nativeMin(end, start + size); break; - case 'takeRight': start = nativeMax(start, end - size); break; - } - } - return { 'start': start, 'end': end }; - } - - /** - * Extracts wrapper details from the `source` body comment. - * - * @private - * @param {string} source The source to inspect. - * @returns {Array} Returns the wrapper details. - */ - function getWrapDetails(source) { - var match = source.match(reWrapDetails); - return match ? match[1].split(reSplitDetails) : []; - } - - /** - * Checks if `path` exists on `object`. - * - * @private - * @param {Object} object The object to query. - * @param {Array|string} path The path to check. - * @param {Function} hasFunc The function to check properties. - * @returns {boolean} Returns `true` if `path` exists, else `false`. - */ - function hasPath(object, path, hasFunc) { - path = castPath(path, object); - - var index = -1, - length = path.length, - result = false; - - while (++index < length) { - var key = toKey(path[index]); - if (!(result = object != null && hasFunc(object, key))) { - break; - } - object = object[key]; - } - if (result || ++index != length) { - return result; - } - length = object == null ? 0 : object.length; - return !!length && isLength(length) && isIndex(key, length) && - (isArray(object) || isArguments(object)); - } - - /** - * Initializes an array clone. - * - * @private - * @param {Array} array The array to clone. - * @returns {Array} Returns the initialized clone. - */ - function initCloneArray(array) { - var length = array.length, - result = array.constructor(length); - - // Add properties assigned by `RegExp#exec`. - if (length && typeof array[0] == 'string' && hasOwnProperty.call(array, 'index')) { - result.index = array.index; - result.input = array.input; - } - return result; - } - - /** - * Initializes an object clone. - * - * @private - * @param {Object} object The object to clone. - * @returns {Object} Returns the initialized clone. - */ - function initCloneObject(object) { - return (typeof object.constructor == 'function' && !isPrototype(object)) - ? baseCreate(getPrototype(object)) - : {}; - } - - /** - * Initializes an object clone based on its `toStringTag`. - * - * **Note:** This function only supports cloning values with tags of - * `Boolean`, `Date`, `Error`, `Number`, `RegExp`, or `String`. - * - * @private - * @param {Object} object The object to clone. - * @param {string} tag The `toStringTag` of the object to clone. - * @param {Function} cloneFunc The function to clone values. - * @param {boolean} [isDeep] Specify a deep clone. - * @returns {Object} Returns the initialized clone. - */ - function initCloneByTag(object, tag, cloneFunc, isDeep) { - var Ctor = object.constructor; - switch (tag) { - case arrayBufferTag: - return cloneArrayBuffer(object); - - case boolTag: - case dateTag: - return new Ctor(+object); - - case dataViewTag: - return cloneDataView(object, isDeep); - - case float32Tag: case float64Tag: - case int8Tag: case int16Tag: case int32Tag: - case uint8Tag: case uint8ClampedTag: case uint16Tag: case uint32Tag: - return cloneTypedArray(object, isDeep); - - case mapTag: - return cloneMap(object, isDeep, cloneFunc); - - case numberTag: - case stringTag: - return new Ctor(object); - - case regexpTag: - return cloneRegExp(object); - - case setTag: - return cloneSet(object, isDeep, cloneFunc); - - case symbolTag: - return cloneSymbol(object); - } - } - - /** - * Inserts wrapper `details` in a comment at the top of the `source` body. - * - * @private - * @param {string} source The source to modify. - * @returns {Array} details The details to insert. - * @returns {string} Returns the modified source. - */ - function insertWrapDetails(source, details) { - var length = details.length; - if (!length) { - return source; - } - var lastIndex = length - 1; - details[lastIndex] = (length > 1 ? '& ' : '') + details[lastIndex]; - details = details.join(length > 2 ? ', ' : ' '); - return source.replace(reWrapComment, '{\n/* [wrapped with ' + details + '] */\n'); - } - - /** - * Checks if `value` is a flattenable `arguments` object or array. - * - * @private - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if `value` is flattenable, else `false`. - */ - function isFlattenable(value) { - return isArray(value) || isArguments(value) || - !!(spreadableSymbol && value && value[spreadableSymbol]); - } - - /** - * Checks if `value` is a valid array-like index. - * - * @private - * @param {*} value The value to check. - * @param {number} [length=MAX_SAFE_INTEGER] The upper bounds of a valid index. - * @returns {boolean} Returns `true` if `value` is a valid index, else `false`. - */ - function isIndex(value, length) { - length = length == null ? MAX_SAFE_INTEGER : length; - return !!length && - (typeof value == 'number' || reIsUint.test(value)) && - (value > -1 && value % 1 == 0 && value < length); - } - - /** - * Checks if the given arguments are from an iteratee call. - * - * @private - * @param {*} value The potential iteratee value argument. - * @param {*} index The potential iteratee index or key argument. - * @param {*} object The potential iteratee object argument. - * @returns {boolean} Returns `true` if the arguments are from an iteratee call, - * else `false`. - */ - function isIterateeCall(value, index, object) { - if (!isObject(object)) { - return false; - } - var type = typeof index; - if (type == 'number' - ? (isArrayLike(object) && isIndex(index, object.length)) - : (type == 'string' && index in object) - ) { - return eq(object[index], value); - } - return false; - } - - /** - * Checks if `value` is a property name and not a property path. - * - * @private - * @param {*} value The value to check. - * @param {Object} [object] The object to query keys on. - * @returns {boolean} Returns `true` if `value` is a property name, else `false`. - */ - function isKey(value, object) { - if (isArray(value)) { - return false; - } - var type = typeof value; - if (type == 'number' || type == 'symbol' || type == 'boolean' || - value == null || isSymbol(value)) { - return true; - } - return reIsPlainProp.test(value) || !reIsDeepProp.test(value) || - (object != null && value in Object(object)); - } - - /** - * Checks if `value` is suitable for use as unique object key. - * - * @private - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if `value` is suitable, else `false`. - */ - function isKeyable(value) { - var type = typeof value; - return (type == 'string' || type == 'number' || type == 'symbol' || type == 'boolean') - ? (value !== '__proto__') - : (value === null); - } - - /** - * Checks if `func` has a lazy counterpart. - * - * @private - * @param {Function} func The function to check. - * @returns {boolean} Returns `true` if `func` has a lazy counterpart, - * else `false`. - */ - function isLaziable(func) { - var funcName = getFuncName(func), - other = lodash[funcName]; - - if (typeof other != 'function' || !(funcName in LazyWrapper.prototype)) { - return false; - } - if (func === other) { - return true; - } - var data = getData(other); - return !!data && func === data[0]; - } - - /** - * Checks if `func` has its source masked. - * - * @private - * @param {Function} func The function to check. - * @returns {boolean} Returns `true` if `func` is masked, else `false`. - */ - function isMasked(func) { - return !!maskSrcKey && (maskSrcKey in func); - } - - /** - * Checks if `func` is capable of being masked. - * - * @private - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if `func` is maskable, else `false`. - */ - var isMaskable = coreJsData ? isFunction : stubFalse; - - /** - * Checks if `value` is likely a prototype object. - * - * @private - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if `value` is a prototype, else `false`. - */ - function isPrototype(value) { - var Ctor = value && value.constructor, - proto = (typeof Ctor == 'function' && Ctor.prototype) || objectProto; - - return value === proto; - } - - /** - * Checks if `value` is suitable for strict equality comparisons, i.e. `===`. - * - * @private - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if `value` if suitable for strict - * equality comparisons, else `false`. - */ - function isStrictComparable(value) { - return value === value && !isObject(value); - } - - /** - * A specialized version of `matchesProperty` for source values suitable - * for strict equality comparisons, i.e. `===`. - * - * @private - * @param {string} key The key of the property to get. - * @param {*} srcValue The value to match. - * @returns {Function} Returns the new spec function. - */ - function matchesStrictComparable(key, srcValue) { - return function(object) { - if (object == null) { - return false; - } - return object[key] === srcValue && - (srcValue !== undefined || (key in Object(object))); - }; - } - - /** - * A specialized version of `_.memoize` which clears the memoized function's - * cache when it exceeds `MAX_MEMOIZE_SIZE`. - * - * @private - * @param {Function} func The function to have its output memoized. - * @returns {Function} Returns the new memoized function. - */ - function memoizeCapped(func) { - var result = memoize(func, function(key) { - if (cache.size === MAX_MEMOIZE_SIZE) { - cache.clear(); - } - return key; - }); - - var cache = result.cache; - return result; - } - - /** - * Merges the function metadata of `source` into `data`. - * - * Merging metadata reduces the number of wrappers used to invoke a function. - * This is possible because methods like `_.bind`, `_.curry`, and `_.partial` - * may be applied regardless of execution order. Methods like `_.ary` and - * `_.rearg` modify function arguments, making the order in which they are - * executed important, preventing the merging of metadata. However, we make - * an exception for a safe combined case where curried functions have `_.ary` - * and or `_.rearg` applied. - * - * @private - * @param {Array} data The destination metadata. - * @param {Array} source The source metadata. - * @returns {Array} Returns `data`. - */ - function mergeData(data, source) { - var bitmask = data[1], - srcBitmask = source[1], - newBitmask = bitmask | srcBitmask, - isCommon = newBitmask < (WRAP_BIND_FLAG | WRAP_BIND_KEY_FLAG | WRAP_ARY_FLAG); - - var isCombo = - ((srcBitmask == WRAP_ARY_FLAG) && (bitmask == WRAP_CURRY_FLAG)) || - ((srcBitmask == WRAP_ARY_FLAG) && (bitmask == WRAP_REARG_FLAG) && (data[7].length <= source[8])) || - ((srcBitmask == (WRAP_ARY_FLAG | WRAP_REARG_FLAG)) && (source[7].length <= source[8]) && (bitmask == WRAP_CURRY_FLAG)); - - // Exit early if metadata can't be merged. - if (!(isCommon || isCombo)) { - return data; - } - // Use source `thisArg` if available. - if (srcBitmask & WRAP_BIND_FLAG) { - data[2] = source[2]; - // Set when currying a bound function. - newBitmask |= bitmask & WRAP_BIND_FLAG ? 0 : WRAP_CURRY_BOUND_FLAG; - } - // Compose partial arguments. - var value = source[3]; - if (value) { - var partials = data[3]; - data[3] = partials ? composeArgs(partials, value, source[4]) : value; - data[4] = partials ? replaceHolders(data[3], PLACEHOLDER) : source[4]; - } - // Compose partial right arguments. - value = source[5]; - if (value) { - partials = data[5]; - data[5] = partials ? composeArgsRight(partials, value, source[6]) : value; - data[6] = partials ? replaceHolders(data[5], PLACEHOLDER) : source[6]; - } - // Use source `argPos` if available. - value = source[7]; - if (value) { - data[7] = value; - } - // Use source `ary` if it's smaller. - if (srcBitmask & WRAP_ARY_FLAG) { - data[8] = data[8] == null ? source[8] : nativeMin(data[8], source[8]); - } - // Use source `arity` if one is not provided. - if (data[9] == null) { - data[9] = source[9]; - } - // Use source `func` and merge bitmasks. - data[0] = source[0]; - data[1] = newBitmask; - - return data; - } - - /** - * This function is like - * [`Object.keys`](http://ecma-international.org/ecma-262/7.0/#sec-object.keys) - * except that it includes inherited enumerable properties. - * - * @private - * @param {Object} object The object to query. - * @returns {Array} Returns the array of property names. - */ - function nativeKeysIn(object) { - var result = []; - if (object != null) { - for (var key in Object(object)) { - result.push(key); - } - } - return result; - } - - /** - * Converts `value` to a string using `Object.prototype.toString`. - * - * @private - * @param {*} value The value to convert. - * @returns {string} Returns the converted string. - */ - function objectToString(value) { - return nativeObjectToString.call(value); - } - - /** - * A specialized version of `baseRest` which transforms the rest array. - * - * @private - * @param {Function} func The function to apply a rest parameter to. - * @param {number} [start=func.length-1] The start position of the rest parameter. - * @param {Function} transform The rest array transform. - * @returns {Function} Returns the new function. - */ - function overRest(func, start, transform) { - start = nativeMax(start === undefined ? (func.length - 1) : start, 0); - return function() { - var args = arguments, - index = -1, - length = nativeMax(args.length - start, 0), - array = Array(length); - - while (++index < length) { - array[index] = args[start + index]; - } - index = -1; - var otherArgs = Array(start + 1); - while (++index < start) { - otherArgs[index] = args[index]; - } - otherArgs[start] = transform(array); - return apply(func, this, otherArgs); - }; - } - - /** - * Gets the parent value at `path` of `object`. - * - * @private - * @param {Object} object The object to query. - * @param {Array} path The path to get the parent value of. - * @returns {*} Returns the parent value. - */ - function parent(object, path) { - return path.length < 2 ? object : baseGet(object, baseSlice(path, 0, -1)); - } - - /** - * Reorder `array` according to the specified indexes where the element at - * the first index is assigned as the first element, the element at - * the second index is assigned as the second element, and so on. - * - * @private - * @param {Array} array The array to reorder. - * @param {Array} indexes The arranged array indexes. - * @returns {Array} Returns `array`. - */ - function reorder(array, indexes) { - var arrLength = array.length, - length = nativeMin(indexes.length, arrLength), - oldArray = copyArray(array); - - while (length--) { - var index = indexes[length]; - array[length] = isIndex(index, arrLength) ? oldArray[index] : undefined; - } - return array; - } - - /** - * Sets metadata for `func`. - * - * **Note:** If this function becomes hot, i.e. is invoked a lot in a short - * period of time, it will trip its breaker and transition to an identity - * function to avoid garbage collection pauses in V8. See - * [V8 issue 2070](https://bugs.chromium.org/p/v8/issues/detail?id=2070) - * for more details. - * - * @private - * @param {Function} func The function to associate metadata with. - * @param {*} data The metadata. - * @returns {Function} Returns `func`. - */ - var setData = shortOut(baseSetData); - - /** - * A simple wrapper around the global [`setTimeout`](https://mdn.io/setTimeout). - * - * @private - * @param {Function} func The function to delay. - * @param {number} wait The number of milliseconds to delay invocation. - * @returns {number|Object} Returns the timer id or timeout object. - */ - var setTimeout = ctxSetTimeout || function(func, wait) { - return root.setTimeout(func, wait); - }; - - /** - * Sets the `toString` method of `func` to return `string`. - * - * @private - * @param {Function} func The function to modify. - * @param {Function} string The `toString` result. - * @returns {Function} Returns `func`. - */ - var setToString = shortOut(baseSetToString); - - /** - * Sets the `toString` method of `wrapper` to mimic the source of `reference` - * with wrapper details in a comment at the top of the source body. - * - * @private - * @param {Function} wrapper The function to modify. - * @param {Function} reference The reference function. - * @param {number} bitmask The bitmask flags. See `createWrap` for more details. - * @returns {Function} Returns `wrapper`. - */ - function setWrapToString(wrapper, reference, bitmask) { - var source = (reference + ''); - return setToString(wrapper, insertWrapDetails(source, updateWrapDetails(getWrapDetails(source), bitmask))); - } - - /** - * Creates a function that'll short out and invoke `identity` instead - * of `func` when it's called `HOT_COUNT` or more times in `HOT_SPAN` - * milliseconds. - * - * @private - * @param {Function} func The function to restrict. - * @returns {Function} Returns the new shortable function. - */ - function shortOut(func) { - var count = 0, - lastCalled = 0; - - return function() { - var stamp = nativeNow(), - remaining = HOT_SPAN - (stamp - lastCalled); - - lastCalled = stamp; - if (remaining > 0) { - if (++count >= HOT_COUNT) { - return arguments[0]; - } - } else { - count = 0; - } - return func.apply(undefined, arguments); - }; - } - - /** - * A specialized version of `_.shuffle` which mutates and sets the size of `array`. - * - * @private - * @param {Array} array The array to shuffle. - * @param {number} [size=array.length] The size of `array`. - * @returns {Array} Returns `array`. - */ - function shuffleSelf(array, size) { - var index = -1, - length = array.length, - lastIndex = length - 1; - - size = size === undefined ? length : size; - while (++index < size) { - var rand = baseRandom(index, lastIndex), - value = array[rand]; - - array[rand] = array[index]; - array[index] = value; - } - array.length = size; - return array; - } - - /** - * Converts `string` to a property path array. - * - * @private - * @param {string} string The string to convert. - * @returns {Array} Returns the property path array. - */ - var stringToPath = memoizeCapped(function(string) { - var result = []; - if (reLeadingDot.test(string)) { - result.push(''); - } - string.replace(rePropName, function(match, number, quote, string) { - result.push(quote ? string.replace(reEscapeChar, '$1') : (number || match)); - }); - return result; - }); - - /** - * Converts `value` to a string key if it's not a string or symbol. - * - * @private - * @param {*} value The value to inspect. - * @returns {string|symbol} Returns the key. - */ - function toKey(value) { - if (typeof value == 'string' || isSymbol(value)) { - return value; - } - var result = (value + ''); - return (result == '0' && (1 / value) == -INFINITY) ? '-0' : result; - } - - /** - * Converts `func` to its source code. - * - * @private - * @param {Function} func The function to convert. - * @returns {string} Returns the source code. - */ - function toSource(func) { - if (func != null) { - try { - return funcToString.call(func); - } catch (e) {} - try { - return (func + ''); - } catch (e) {} - } - return ''; - } - - /** - * Updates wrapper `details` based on `bitmask` flags. - * - * @private - * @returns {Array} details The details to modify. - * @param {number} bitmask The bitmask flags. See `createWrap` for more details. - * @returns {Array} Returns `details`. - */ - function updateWrapDetails(details, bitmask) { - arrayEach(wrapFlags, function(pair) { - var value = '_.' + pair[0]; - if ((bitmask & pair[1]) && !arrayIncludes(details, value)) { - details.push(value); - } - }); - return details.sort(); - } - - /** - * Creates a clone of `wrapper`. - * - * @private - * @param {Object} wrapper The wrapper to clone. - * @returns {Object} Returns the cloned wrapper. - */ - function wrapperClone(wrapper) { - if (wrapper instanceof LazyWrapper) { - return wrapper.clone(); - } - var result = new LodashWrapper(wrapper.__wrapped__, wrapper.__chain__); - result.__actions__ = copyArray(wrapper.__actions__); - result.__index__ = wrapper.__index__; - result.__values__ = wrapper.__values__; - return result; - } - - /*------------------------------------------------------------------------*/ - - /** - * Creates an array of elements split into groups the length of `size`. - * If `array` can't be split evenly, the final chunk will be the remaining - * elements. - * - * @static - * @memberOf _ - * @since 3.0.0 - * @category Array - * @param {Array} array The array to process. - * @param {number} [size=1] The length of each chunk - * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`. - * @returns {Array} Returns the new array of chunks. - * @example - * - * _.chunk(['a', 'b', 'c', 'd'], 2); - * // => [['a', 'b'], ['c', 'd']] - * - * _.chunk(['a', 'b', 'c', 'd'], 3); - * // => [['a', 'b', 'c'], ['d']] - */ - function chunk(array, size, guard) { - if ((guard ? isIterateeCall(array, size, guard) : size === undefined)) { - size = 1; - } else { - size = nativeMax(toInteger(size), 0); - } - var length = array == null ? 0 : array.length; - if (!length || size < 1) { - return []; - } - var index = 0, - resIndex = 0, - result = Array(nativeCeil(length / size)); - - while (index < length) { - result[resIndex++] = baseSlice(array, index, (index += size)); - } - return result; - } - - /** - * Creates an array with all falsey values removed. The values `false`, `null`, - * `0`, `""`, `undefined`, and `NaN` are falsey. - * - * @static - * @memberOf _ - * @since 0.1.0 - * @category Array - * @param {Array} array The array to compact. - * @returns {Array} Returns the new array of filtered values. - * @example - * - * _.compact([0, 1, false, 2, '', 3]); - * // => [1, 2, 3] - */ - function compact(array) { - var index = -1, - length = array == null ? 0 : array.length, - resIndex = 0, - result = []; - - while (++index < length) { - var value = array[index]; - if (value) { - result[resIndex++] = value; - } - } - return result; - } - - /** - * Creates a new array concatenating `array` with any additional arrays - * and/or values. - * - * @static - * @memberOf _ - * @since 4.0.0 - * @category Array - * @param {Array} array The array to concatenate. - * @param {...*} [values] The values to concatenate. - * @returns {Array} Returns the new concatenated array. - * @example - * - * var array = [1]; - * var other = _.concat(array, 2, [3], [[4]]); - * - * console.log(other); - * // => [1, 2, 3, [4]] - * - * console.log(array); - * // => [1] - */ - function concat() { - var length = arguments.length; - if (!length) { - return []; - } - var args = Array(length - 1), - array = arguments[0], - index = length; - - while (index--) { - args[index - 1] = arguments[index]; - } - return arrayPush(isArray(array) ? copyArray(array) : [array], baseFlatten(args, 1)); - } - - /** - * Creates an array of `array` values not included in the other given arrays - * using [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero) - * for equality comparisons. The order and references of result values are - * determined by the first array. - * - * **Note:** Unlike `_.pullAll`, this method returns a new array. - * - * @static - * @memberOf _ - * @since 0.1.0 - * @category Array - * @param {Array} array The array to inspect. - * @param {...Array} [values] The values to exclude. - * @returns {Array} Returns the new array of filtered values. - * @see _.without, _.xor - * @example - * - * _.difference([2, 1], [2, 3]); - * // => [1] - */ - var difference = baseRest(function(array, values) { - return isArrayLikeObject(array) - ? baseDifference(array, baseFlatten(values, 1, isArrayLikeObject, true)) - : []; - }); - - /** - * This method is like `_.difference` except that it accepts `iteratee` which - * is invoked for each element of `array` and `values` to generate the criterion - * by which they're compared. The order and references of result values are - * determined by the first array. The iteratee is invoked with one argument: - * (value). - * - * **Note:** Unlike `_.pullAllBy`, this method returns a new array. - * - * @static - * @memberOf _ - * @since 4.0.0 - * @category Array - * @param {Array} array The array to inspect. - * @param {...Array} [values] The values to exclude. - * @param {Function} [iteratee=_.identity] The iteratee invoked per element. - * @returns {Array} Returns the new array of filtered values. - * @example - * - * _.differenceBy([2.1, 1.2], [2.3, 3.4], Math.floor); - * // => [1.2] - * - * // The `_.property` iteratee shorthand. - * _.differenceBy([{ 'x': 2 }, { 'x': 1 }], [{ 'x': 1 }], 'x'); - * // => [{ 'x': 2 }] - */ - var differenceBy = baseRest(function(array, values) { - var iteratee = last(values); - if (isArrayLikeObject(iteratee)) { - iteratee = undefined; - } - return isArrayLikeObject(array) - ? baseDifference(array, baseFlatten(values, 1, isArrayLikeObject, true), getIteratee(iteratee, 2)) - : []; - }); - - /** - * This method is like `_.difference` except that it accepts `comparator` - * which is invoked to compare elements of `array` to `values`. The order and - * references of result values are determined by the first array. The comparator - * is invoked with two arguments: (arrVal, othVal). - * - * **Note:** Unlike `_.pullAllWith`, this method returns a new array. - * - * @static - * @memberOf _ - * @since 4.0.0 - * @category Array - * @param {Array} array The array to inspect. - * @param {...Array} [values] The values to exclude. - * @param {Function} [comparator] The comparator invoked per element. - * @returns {Array} Returns the new array of filtered values. - * @example - * - * var objects = [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }]; - * - * _.differenceWith(objects, [{ 'x': 1, 'y': 2 }], _.isEqual); - * // => [{ 'x': 2, 'y': 1 }] - */ - var differenceWith = baseRest(function(array, values) { - var comparator = last(values); - if (isArrayLikeObject(comparator)) { - comparator = undefined; - } - return isArrayLikeObject(array) - ? baseDifference(array, baseFlatten(values, 1, isArrayLikeObject, true), undefined, comparator) - : []; - }); - - /** - * Creates a slice of `array` with `n` elements dropped from the beginning. - * - * @static - * @memberOf _ - * @since 0.5.0 - * @category Array - * @param {Array} array The array to query. - * @param {number} [n=1] The number of elements to drop. - * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`. - * @returns {Array} Returns the slice of `array`. - * @example - * - * _.drop([1, 2, 3]); - * // => [2, 3] - * - * _.drop([1, 2, 3], 2); - * // => [3] - * - * _.drop([1, 2, 3], 5); - * // => [] - * - * _.drop([1, 2, 3], 0); - * // => [1, 2, 3] - */ - function drop(array, n, guard) { - var length = array == null ? 0 : array.length; - if (!length) { - return []; - } - n = (guard || n === undefined) ? 1 : toInteger(n); - return baseSlice(array, n < 0 ? 0 : n, length); - } - - /** - * Creates a slice of `array` with `n` elements dropped from the end. - * - * @static - * @memberOf _ - * @since 3.0.0 - * @category Array - * @param {Array} array The array to query. - * @param {number} [n=1] The number of elements to drop. - * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`. - * @returns {Array} Returns the slice of `array`. - * @example - * - * _.dropRight([1, 2, 3]); - * // => [1, 2] - * - * _.dropRight([1, 2, 3], 2); - * // => [1] - * - * _.dropRight([1, 2, 3], 5); - * // => [] - * - * _.dropRight([1, 2, 3], 0); - * // => [1, 2, 3] - */ - function dropRight(array, n, guard) { - var length = array == null ? 0 : array.length; - if (!length) { - return []; - } - n = (guard || n === undefined) ? 1 : toInteger(n); - n = length - n; - return baseSlice(array, 0, n < 0 ? 0 : n); - } - - /** - * Creates a slice of `array` excluding elements dropped from the end. - * Elements are dropped until `predicate` returns falsey. The predicate is - * invoked with three arguments: (value, index, array). - * - * @static - * @memberOf _ - * @since 3.0.0 - * @category Array - * @param {Array} array The array to query. - * @param {Function} [predicate=_.identity] The function invoked per iteration. - * @returns {Array} Returns the slice of `array`. - * @example - * - * var users = [ - * { 'user': 'barney', 'active': true }, - * { 'user': 'fred', 'active': false }, - * { 'user': 'pebbles', 'active': false } - * ]; - * - * _.dropRightWhile(users, function(o) { return !o.active; }); - * // => objects for ['barney'] - * - * // The `_.matches` iteratee shorthand. - * _.dropRightWhile(users, { 'user': 'pebbles', 'active': false }); - * // => objects for ['barney', 'fred'] - * - * // The `_.matchesProperty` iteratee shorthand. - * _.dropRightWhile(users, ['active', false]); - * // => objects for ['barney'] - * - * // The `_.property` iteratee shorthand. - * _.dropRightWhile(users, 'active'); - * // => objects for ['barney', 'fred', 'pebbles'] - */ - function dropRightWhile(array, predicate) { - return (array && array.length) - ? baseWhile(array, getIteratee(predicate, 3), true, true) - : []; - } - - /** - * Creates a slice of `array` excluding elements dropped from the beginning. - * Elements are dropped until `predicate` returns falsey. The predicate is - * invoked with three arguments: (value, index, array). - * - * @static - * @memberOf _ - * @since 3.0.0 - * @category Array - * @param {Array} array The array to query. - * @param {Function} [predicate=_.identity] The function invoked per iteration. - * @returns {Array} Returns the slice of `array`. - * @example - * - * var users = [ - * { 'user': 'barney', 'active': false }, - * { 'user': 'fred', 'active': false }, - * { 'user': 'pebbles', 'active': true } - * ]; - * - * _.dropWhile(users, function(o) { return !o.active; }); - * // => objects for ['pebbles'] - * - * // The `_.matches` iteratee shorthand. - * _.dropWhile(users, { 'user': 'barney', 'active': false }); - * // => objects for ['fred', 'pebbles'] - * - * // The `_.matchesProperty` iteratee shorthand. - * _.dropWhile(users, ['active', false]); - * // => objects for ['pebbles'] - * - * // The `_.property` iteratee shorthand. - * _.dropWhile(users, 'active'); - * // => objects for ['barney', 'fred', 'pebbles'] - */ - function dropWhile(array, predicate) { - return (array && array.length) - ? baseWhile(array, getIteratee(predicate, 3), true) - : []; - } - - /** - * Fills elements of `array` with `value` from `start` up to, but not - * including, `end`. - * - * **Note:** This method mutates `array`. - * - * @static - * @memberOf _ - * @since 3.2.0 - * @category Array - * @param {Array} array The array to fill. - * @param {*} value The value to fill `array` with. - * @param {number} [start=0] The start position. - * @param {number} [end=array.length] The end position. - * @returns {Array} Returns `array`. - * @example - * - * var array = [1, 2, 3]; - * - * _.fill(array, 'a'); - * console.log(array); - * // => ['a', 'a', 'a'] - * - * _.fill(Array(3), 2); - * // => [2, 2, 2] - * - * _.fill([4, 6, 8, 10], '*', 1, 3); - * // => [4, '*', '*', 10] - */ - function fill(array, value, start, end) { - var length = array == null ? 0 : array.length; - if (!length) { - return []; - } - if (start && typeof start != 'number' && isIterateeCall(array, value, start)) { - start = 0; - end = length; - } - return baseFill(array, value, start, end); - } - - /** - * This method is like `_.find` except that it returns the index of the first - * element `predicate` returns truthy for instead of the element itself. - * - * @static - * @memberOf _ - * @since 1.1.0 - * @category Array - * @param {Array} array The array to inspect. - * @param {Function} [predicate=_.identity] The function invoked per iteration. - * @param {number} [fromIndex=0] The index to search from. - * @returns {number} Returns the index of the found element, else `-1`. - * @example - * - * var users = [ - * { 'user': 'barney', 'active': false }, - * { 'user': 'fred', 'active': false }, - * { 'user': 'pebbles', 'active': true } - * ]; - * - * _.findIndex(users, function(o) { return o.user == 'barney'; }); - * // => 0 - * - * // The `_.matches` iteratee shorthand. - * _.findIndex(users, { 'user': 'fred', 'active': false }); - * // => 1 - * - * // The `_.matchesProperty` iteratee shorthand. - * _.findIndex(users, ['active', false]); - * // => 0 - * - * // The `_.property` iteratee shorthand. - * _.findIndex(users, 'active'); - * // => 2 - */ - function findIndex(array, predicate, fromIndex) { - var length = array == null ? 0 : array.length; - if (!length) { - return -1; - } - var index = fromIndex == null ? 0 : toInteger(fromIndex); - if (index < 0) { - index = nativeMax(length + index, 0); - } - return baseFindIndex(array, getIteratee(predicate, 3), index); - } - - /** - * This method is like `_.findIndex` except that it iterates over elements - * of `collection` from right to left. - * - * @static - * @memberOf _ - * @since 2.0.0 - * @category Array - * @param {Array} array The array to inspect. - * @param {Function} [predicate=_.identity] The function invoked per iteration. - * @param {number} [fromIndex=array.length-1] The index to search from. - * @returns {number} Returns the index of the found element, else `-1`. - * @example - * - * var users = [ - * { 'user': 'barney', 'active': true }, - * { 'user': 'fred', 'active': false }, - * { 'user': 'pebbles', 'active': false } - * ]; - * - * _.findLastIndex(users, function(o) { return o.user == 'pebbles'; }); - * // => 2 - * - * // The `_.matches` iteratee shorthand. - * _.findLastIndex(users, { 'user': 'barney', 'active': true }); - * // => 0 - * - * // The `_.matchesProperty` iteratee shorthand. - * _.findLastIndex(users, ['active', false]); - * // => 2 - * - * // The `_.property` iteratee shorthand. - * _.findLastIndex(users, 'active'); - * // => 0 - */ - function findLastIndex(array, predicate, fromIndex) { - var length = array == null ? 0 : array.length; - if (!length) { - return -1; - } - var index = length - 1; - if (fromIndex !== undefined) { - index = toInteger(fromIndex); - index = fromIndex < 0 - ? nativeMax(length + index, 0) - : nativeMin(index, length - 1); - } - return baseFindIndex(array, getIteratee(predicate, 3), index, true); - } - - /** - * Flattens `array` a single level deep. - * - * @static - * @memberOf _ - * @since 0.1.0 - * @category Array - * @param {Array} array The array to flatten. - * @returns {Array} Returns the new flattened array. - * @example - * - * _.flatten([1, [2, [3, [4]], 5]]); - * // => [1, 2, [3, [4]], 5] - */ - function flatten(array) { - var length = array == null ? 0 : array.length; - return length ? baseFlatten(array, 1) : []; - } - - /** - * Recursively flattens `array`. - * - * @static - * @memberOf _ - * @since 3.0.0 - * @category Array - * @param {Array} array The array to flatten. - * @returns {Array} Returns the new flattened array. - * @example - * - * _.flattenDeep([1, [2, [3, [4]], 5]]); - * // => [1, 2, 3, 4, 5] - */ - function flattenDeep(array) { - var length = array == null ? 0 : array.length; - return length ? baseFlatten(array, INFINITY) : []; - } - - /** - * Recursively flatten `array` up to `depth` times. - * - * @static - * @memberOf _ - * @since 4.4.0 - * @category Array - * @param {Array} array The array to flatten. - * @param {number} [depth=1] The maximum recursion depth. - * @returns {Array} Returns the new flattened array. - * @example - * - * var array = [1, [2, [3, [4]], 5]]; - * - * _.flattenDepth(array, 1); - * // => [1, 2, [3, [4]], 5] - * - * _.flattenDepth(array, 2); - * // => [1, 2, 3, [4], 5] - */ - function flattenDepth(array, depth) { - var length = array == null ? 0 : array.length; - if (!length) { - return []; - } - depth = depth === undefined ? 1 : toInteger(depth); - return baseFlatten(array, depth); - } - - /** - * The inverse of `_.toPairs`; this method returns an object composed - * from key-value `pairs`. - * - * @static - * @memberOf _ - * @since 4.0.0 - * @category Array - * @param {Array} pairs The key-value pairs. - * @returns {Object} Returns the new object. - * @example - * - * _.fromPairs([['a', 1], ['b', 2]]); - * // => { 'a': 1, 'b': 2 } - */ - function fromPairs(pairs) { - var index = -1, - length = pairs == null ? 0 : pairs.length, - result = {}; - - while (++index < length) { - var pair = pairs[index]; - result[pair[0]] = pair[1]; - } - return result; - } - - /** - * Gets the first element of `array`. - * - * @static - * @memberOf _ - * @since 0.1.0 - * @alias first - * @category Array - * @param {Array} array The array to query. - * @returns {*} Returns the first element of `array`. - * @example - * - * _.head([1, 2, 3]); - * // => 1 - * - * _.head([]); - * // => undefined - */ - function head(array) { - return (array && array.length) ? array[0] : undefined; - } - - /** - * Gets the index at which the first occurrence of `value` is found in `array` - * using [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero) - * for equality comparisons. If `fromIndex` is negative, it's used as the - * offset from the end of `array`. - * - * @static - * @memberOf _ - * @since 0.1.0 - * @category Array - * @param {Array} array The array to inspect. - * @param {*} value The value to search for. - * @param {number} [fromIndex=0] The index to search from. - * @returns {number} Returns the index of the matched value, else `-1`. - * @example - * - * _.indexOf([1, 2, 1, 2], 2); - * // => 1 - * - * // Search from the `fromIndex`. - * _.indexOf([1, 2, 1, 2], 2, 2); - * // => 3 - */ - function indexOf(array, value, fromIndex) { - var length = array == null ? 0 : array.length; - if (!length) { - return -1; - } - var index = fromIndex == null ? 0 : toInteger(fromIndex); - if (index < 0) { - index = nativeMax(length + index, 0); - } - return baseIndexOf(array, value, index); - } - - /** - * Gets all but the last element of `array`. - * - * @static - * @memberOf _ - * @since 0.1.0 - * @category Array - * @param {Array} array The array to query. - * @returns {Array} Returns the slice of `array`. - * @example - * - * _.initial([1, 2, 3]); - * // => [1, 2] - */ - function initial(array) { - var length = array == null ? 0 : array.length; - return length ? baseSlice(array, 0, -1) : []; - } - - /** - * Creates an array of unique values that are included in all given arrays - * using [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero) - * for equality comparisons. The order and references of result values are - * determined by the first array. - * - * @static - * @memberOf _ - * @since 0.1.0 - * @category Array - * @param {...Array} [arrays] The arrays to inspect. - * @returns {Array} Returns the new array of intersecting values. - * @example - * - * _.intersection([2, 1], [2, 3]); - * // => [2] - */ - var intersection = baseRest(function(arrays) { - var mapped = arrayMap(arrays, castArrayLikeObject); - return (mapped.length && mapped[0] === arrays[0]) - ? baseIntersection(mapped) - : []; - }); - - /** - * This method is like `_.intersection` except that it accepts `iteratee` - * which is invoked for each element of each `arrays` to generate the criterion - * by which they're compared. The order and references of result values are - * determined by the first array. The iteratee is invoked with one argument: - * (value). - * - * @static - * @memberOf _ - * @since 4.0.0 - * @category Array - * @param {...Array} [arrays] The arrays to inspect. - * @param {Function} [iteratee=_.identity] The iteratee invoked per element. - * @returns {Array} Returns the new array of intersecting values. - * @example - * - * _.intersectionBy([2.1, 1.2], [2.3, 3.4], Math.floor); - * // => [2.1] - * - * // The `_.property` iteratee shorthand. - * _.intersectionBy([{ 'x': 1 }], [{ 'x': 2 }, { 'x': 1 }], 'x'); - * // => [{ 'x': 1 }] - */ - var intersectionBy = baseRest(function(arrays) { - var iteratee = last(arrays), - mapped = arrayMap(arrays, castArrayLikeObject); - - if (iteratee === last(mapped)) { - iteratee = undefined; - } else { - mapped.pop(); - } - return (mapped.length && mapped[0] === arrays[0]) - ? baseIntersection(mapped, getIteratee(iteratee, 2)) - : []; - }); - - /** - * This method is like `_.intersection` except that it accepts `comparator` - * which is invoked to compare elements of `arrays`. The order and references - * of result values are determined by the first array. The comparator is - * invoked with two arguments: (arrVal, othVal). - * - * @static - * @memberOf _ - * @since 4.0.0 - * @category Array - * @param {...Array} [arrays] The arrays to inspect. - * @param {Function} [comparator] The comparator invoked per element. - * @returns {Array} Returns the new array of intersecting values. - * @example - * - * var objects = [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }]; - * var others = [{ 'x': 1, 'y': 1 }, { 'x': 1, 'y': 2 }]; - * - * _.intersectionWith(objects, others, _.isEqual); - * // => [{ 'x': 1, 'y': 2 }] - */ - var intersectionWith = baseRest(function(arrays) { - var comparator = last(arrays), - mapped = arrayMap(arrays, castArrayLikeObject); - - comparator = typeof comparator == 'function' ? comparator : undefined; - if (comparator) { - mapped.pop(); - } - return (mapped.length && mapped[0] === arrays[0]) - ? baseIntersection(mapped, undefined, comparator) - : []; - }); - - /** - * Converts all elements in `array` into a string separated by `separator`. - * - * @static - * @memberOf _ - * @since 4.0.0 - * @category Array - * @param {Array} array The array to convert. - * @param {string} [separator=','] The element separator. - * @returns {string} Returns the joined string. - * @example - * - * _.join(['a', 'b', 'c'], '~'); - * // => 'a~b~c' - */ - function join(array, separator) { - return array == null ? '' : nativeJoin.call(array, separator); - } - - /** - * Gets the last element of `array`. - * - * @static - * @memberOf _ - * @since 0.1.0 - * @category Array - * @param {Array} array The array to query. - * @returns {*} Returns the last element of `array`. - * @example - * - * _.last([1, 2, 3]); - * // => 3 - */ - function last(array) { - var length = array == null ? 0 : array.length; - return length ? array[length - 1] : undefined; - } - - /** - * This method is like `_.indexOf` except that it iterates over elements of - * `array` from right to left. - * - * @static - * @memberOf _ - * @since 0.1.0 - * @category Array - * @param {Array} array The array to inspect. - * @param {*} value The value to search for. - * @param {number} [fromIndex=array.length-1] The index to search from. - * @returns {number} Returns the index of the matched value, else `-1`. - * @example - * - * _.lastIndexOf([1, 2, 1, 2], 2); - * // => 3 - * - * // Search from the `fromIndex`. - * _.lastIndexOf([1, 2, 1, 2], 2, 2); - * // => 1 - */ - function lastIndexOf(array, value, fromIndex) { - var length = array == null ? 0 : array.length; - if (!length) { - return -1; - } - var index = length; - if (fromIndex !== undefined) { - index = toInteger(fromIndex); - index = index < 0 ? nativeMax(length + index, 0) : nativeMin(index, length - 1); - } - return value === value - ? strictLastIndexOf(array, value, index) - : baseFindIndex(array, baseIsNaN, index, true); - } - - /** - * Gets the element at index `n` of `array`. If `n` is negative, the nth - * element from the end is returned. - * - * @static - * @memberOf _ - * @since 4.11.0 - * @category Array - * @param {Array} array The array to query. - * @param {number} [n=0] The index of the element to return. - * @returns {*} Returns the nth element of `array`. - * @example - * - * var array = ['a', 'b', 'c', 'd']; - * - * _.nth(array, 1); - * // => 'b' - * - * _.nth(array, -2); - * // => 'c'; - */ - function nth(array, n) { - return (array && array.length) ? baseNth(array, toInteger(n)) : undefined; - } - - /** - * Removes all given values from `array` using - * [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero) - * for equality comparisons. - * - * **Note:** Unlike `_.without`, this method mutates `array`. Use `_.remove` - * to remove elements from an array by predicate. - * - * @static - * @memberOf _ - * @since 2.0.0 - * @category Array - * @param {Array} array The array to modify. - * @param {...*} [values] The values to remove. - * @returns {Array} Returns `array`. - * @example - * - * var array = ['a', 'b', 'c', 'a', 'b', 'c']; - * - * _.pull(array, 'a', 'c'); - * console.log(array); - * // => ['b', 'b'] - */ - var pull = baseRest(pullAll); - - /** - * This method is like `_.pull` except that it accepts an array of values to remove. - * - * **Note:** Unlike `_.difference`, this method mutates `array`. - * - * @static - * @memberOf _ - * @since 4.0.0 - * @category Array - * @param {Array} array The array to modify. - * @param {Array} values The values to remove. - * @returns {Array} Returns `array`. - * @example - * - * var array = ['a', 'b', 'c', 'a', 'b', 'c']; - * - * _.pullAll(array, ['a', 'c']); - * console.log(array); - * // => ['b', 'b'] - */ - function pullAll(array, values) { - return (array && array.length && values && values.length) - ? basePullAll(array, values) - : array; - } - - /** - * This method is like `_.pullAll` except that it accepts `iteratee` which is - * invoked for each element of `array` and `values` to generate the criterion - * by which they're compared. The iteratee is invoked with one argument: (value). - * - * **Note:** Unlike `_.differenceBy`, this method mutates `array`. - * - * @static - * @memberOf _ - * @since 4.0.0 - * @category Array - * @param {Array} array The array to modify. - * @param {Array} values The values to remove. - * @param {Function} [iteratee=_.identity] The iteratee invoked per element. - * @returns {Array} Returns `array`. - * @example - * - * var array = [{ 'x': 1 }, { 'x': 2 }, { 'x': 3 }, { 'x': 1 }]; - * - * _.pullAllBy(array, [{ 'x': 1 }, { 'x': 3 }], 'x'); - * console.log(array); - * // => [{ 'x': 2 }] - */ - function pullAllBy(array, values, iteratee) { - return (array && array.length && values && values.length) - ? basePullAll(array, values, getIteratee(iteratee, 2)) - : array; - } - - /** - * This method is like `_.pullAll` except that it accepts `comparator` which - * is invoked to compare elements of `array` to `values`. The comparator is - * invoked with two arguments: (arrVal, othVal). - * - * **Note:** Unlike `_.differenceWith`, this method mutates `array`. - * - * @static - * @memberOf _ - * @since 4.6.0 - * @category Array - * @param {Array} array The array to modify. - * @param {Array} values The values to remove. - * @param {Function} [comparator] The comparator invoked per element. - * @returns {Array} Returns `array`. - * @example - * - * var array = [{ 'x': 1, 'y': 2 }, { 'x': 3, 'y': 4 }, { 'x': 5, 'y': 6 }]; - * - * _.pullAllWith(array, [{ 'x': 3, 'y': 4 }], _.isEqual); - * console.log(array); - * // => [{ 'x': 1, 'y': 2 }, { 'x': 5, 'y': 6 }] - */ - function pullAllWith(array, values, comparator) { - return (array && array.length && values && values.length) - ? basePullAll(array, values, undefined, comparator) - : array; - } - - /** - * Removes elements from `array` corresponding to `indexes` and returns an - * array of removed elements. - * - * **Note:** Unlike `_.at`, this method mutates `array`. - * - * @static - * @memberOf _ - * @since 3.0.0 - * @category Array - * @param {Array} array The array to modify. - * @param {...(number|number[])} [indexes] The indexes of elements to remove. - * @returns {Array} Returns the new array of removed elements. - * @example - * - * var array = ['a', 'b', 'c', 'd']; - * var pulled = _.pullAt(array, [1, 3]); - * - * console.log(array); - * // => ['a', 'c'] - * - * console.log(pulled); - * // => ['b', 'd'] - */ - var pullAt = flatRest(function(array, indexes) { - var length = array == null ? 0 : array.length, - result = baseAt(array, indexes); - - basePullAt(array, arrayMap(indexes, function(index) { - return isIndex(index, length) ? +index : index; - }).sort(compareAscending)); - - return result; - }); - - /** - * Removes all elements from `array` that `predicate` returns truthy for - * and returns an array of the removed elements. The predicate is invoked - * with three arguments: (value, index, array). - * - * **Note:** Unlike `_.filter`, this method mutates `array`. Use `_.pull` - * to pull elements from an array by value. - * - * @static - * @memberOf _ - * @since 2.0.0 - * @category Array - * @param {Array} array The array to modify. - * @param {Function} [predicate=_.identity] The function invoked per iteration. - * @returns {Array} Returns the new array of removed elements. - * @example - * - * var array = [1, 2, 3, 4]; - * var evens = _.remove(array, function(n) { - * return n % 2 == 0; - * }); - * - * console.log(array); - * // => [1, 3] - * - * console.log(evens); - * // => [2, 4] - */ - function remove(array, predicate) { - var result = []; - if (!(array && array.length)) { - return result; - } - var index = -1, - indexes = [], - length = array.length; - - predicate = getIteratee(predicate, 3); - while (++index < length) { - var value = array[index]; - if (predicate(value, index, array)) { - result.push(value); - indexes.push(index); - } - } - basePullAt(array, indexes); - return result; - } - - /** - * Reverses `array` so that the first element becomes the last, the second - * element becomes the second to last, and so on. - * - * **Note:** This method mutates `array` and is based on - * [`Array#reverse`](https://mdn.io/Array/reverse). - * - * @static - * @memberOf _ - * @since 4.0.0 - * @category Array - * @param {Array} array The array to modify. - * @returns {Array} Returns `array`. - * @example - * - * var array = [1, 2, 3]; - * - * _.reverse(array); - * // => [3, 2, 1] - * - * console.log(array); - * // => [3, 2, 1] - */ - function reverse(array) { - return array == null ? array : nativeReverse.call(array); - } - - /** - * Creates a slice of `array` from `start` up to, but not including, `end`. - * - * **Note:** This method is used instead of - * [`Array#slice`](https://mdn.io/Array/slice) to ensure dense arrays are - * returned. - * - * @static - * @memberOf _ - * @since 3.0.0 - * @category Array - * @param {Array} array The array to slice. - * @param {number} [start=0] The start position. - * @param {number} [end=array.length] The end position. - * @returns {Array} Returns the slice of `array`. - */ - function slice(array, start, end) { - var length = array == null ? 0 : array.length; - if (!length) { - return []; - } - if (end && typeof end != 'number' && isIterateeCall(array, start, end)) { - start = 0; - end = length; - } - else { - start = start == null ? 0 : toInteger(start); - end = end === undefined ? length : toInteger(end); - } - return baseSlice(array, start, end); - } - - /** - * Uses a binary search to determine the lowest index at which `value` - * should be inserted into `array` in order to maintain its sort order. - * - * @static - * @memberOf _ - * @since 0.1.0 - * @category Array - * @param {Array} array The sorted array to inspect. - * @param {*} value The value to evaluate. - * @returns {number} Returns the index at which `value` should be inserted - * into `array`. - * @example - * - * _.sortedIndex([30, 50], 40); - * // => 1 - */ - function sortedIndex(array, value) { - return baseSortedIndex(array, value); - } - - /** - * This method is like `_.sortedIndex` except that it accepts `iteratee` - * which is invoked for `value` and each element of `array` to compute their - * sort ranking. The iteratee is invoked with one argument: (value). - * - * @static - * @memberOf _ - * @since 4.0.0 - * @category Array - * @param {Array} array The sorted array to inspect. - * @param {*} value The value to evaluate. - * @param {Function} [iteratee=_.identity] The iteratee invoked per element. - * @returns {number} Returns the index at which `value` should be inserted - * into `array`. - * @example - * - * var objects = [{ 'x': 4 }, { 'x': 5 }]; - * - * _.sortedIndexBy(objects, { 'x': 4 }, function(o) { return o.x; }); - * // => 0 - * - * // The `_.property` iteratee shorthand. - * _.sortedIndexBy(objects, { 'x': 4 }, 'x'); - * // => 0 - */ - function sortedIndexBy(array, value, iteratee) { - return baseSortedIndexBy(array, value, getIteratee(iteratee, 2)); - } - - /** - * This method is like `_.indexOf` except that it performs a binary - * search on a sorted `array`. - * - * @static - * @memberOf _ - * @since 4.0.0 - * @category Array - * @param {Array} array The array to inspect. - * @param {*} value The value to search for. - * @returns {number} Returns the index of the matched value, else `-1`. - * @example - * - * _.sortedIndexOf([4, 5, 5, 5, 6], 5); - * // => 1 - */ - function sortedIndexOf(array, value) { - var length = array == null ? 0 : array.length; - if (length) { - var index = baseSortedIndex(array, value); - if (index < length && eq(array[index], value)) { - return index; - } - } - return -1; - } - - /** - * This method is like `_.sortedIndex` except that it returns the highest - * index at which `value` should be inserted into `array` in order to - * maintain its sort order. - * - * @static - * @memberOf _ - * @since 3.0.0 - * @category Array - * @param {Array} array The sorted array to inspect. - * @param {*} value The value to evaluate. - * @returns {number} Returns the index at which `value` should be inserted - * into `array`. - * @example - * - * _.sortedLastIndex([4, 5, 5, 5, 6], 5); - * // => 4 - */ - function sortedLastIndex(array, value) { - return baseSortedIndex(array, value, true); - } - - /** - * This method is like `_.sortedLastIndex` except that it accepts `iteratee` - * which is invoked for `value` and each element of `array` to compute their - * sort ranking. The iteratee is invoked with one argument: (value). - * - * @static - * @memberOf _ - * @since 4.0.0 - * @category Array - * @param {Array} array The sorted array to inspect. - * @param {*} value The value to evaluate. - * @param {Function} [iteratee=_.identity] The iteratee invoked per element. - * @returns {number} Returns the index at which `value` should be inserted - * into `array`. - * @example - * - * var objects = [{ 'x': 4 }, { 'x': 5 }]; - * - * _.sortedLastIndexBy(objects, { 'x': 4 }, function(o) { return o.x; }); - * // => 1 - * - * // The `_.property` iteratee shorthand. - * _.sortedLastIndexBy(objects, { 'x': 4 }, 'x'); - * // => 1 - */ - function sortedLastIndexBy(array, value, iteratee) { - return baseSortedIndexBy(array, value, getIteratee(iteratee, 2), true); - } - - /** - * This method is like `_.lastIndexOf` except that it performs a binary - * search on a sorted `array`. - * - * @static - * @memberOf _ - * @since 4.0.0 - * @category Array - * @param {Array} array The array to inspect. - * @param {*} value The value to search for. - * @returns {number} Returns the index of the matched value, else `-1`. - * @example - * - * _.sortedLastIndexOf([4, 5, 5, 5, 6], 5); - * // => 3 - */ - function sortedLastIndexOf(array, value) { - var length = array == null ? 0 : array.length; - if (length) { - var index = baseSortedIndex(array, value, true) - 1; - if (eq(array[index], value)) { - return index; - } - } - return -1; - } - - /** - * This method is like `_.uniq` except that it's designed and optimized - * for sorted arrays. - * - * @static - * @memberOf _ - * @since 4.0.0 - * @category Array - * @param {Array} array The array to inspect. - * @returns {Array} Returns the new duplicate free array. - * @example - * - * _.sortedUniq([1, 1, 2]); - * // => [1, 2] - */ - function sortedUniq(array) { - return (array && array.length) - ? baseSortedUniq(array) - : []; - } - - /** - * This method is like `_.uniqBy` except that it's designed and optimized - * for sorted arrays. - * - * @static - * @memberOf _ - * @since 4.0.0 - * @category Array - * @param {Array} array The array to inspect. - * @param {Function} [iteratee] The iteratee invoked per element. - * @returns {Array} Returns the new duplicate free array. - * @example - * - * _.sortedUniqBy([1.1, 1.2, 2.3, 2.4], Math.floor); - * // => [1.1, 2.3] - */ - function sortedUniqBy(array, iteratee) { - return (array && array.length) - ? baseSortedUniq(array, getIteratee(iteratee, 2)) - : []; - } - - /** - * Gets all but the first element of `array`. - * - * @static - * @memberOf _ - * @since 4.0.0 - * @category Array - * @param {Array} array The array to query. - * @returns {Array} Returns the slice of `array`. - * @example - * - * _.tail([1, 2, 3]); - * // => [2, 3] - */ - function tail(array) { - var length = array == null ? 0 : array.length; - return length ? baseSlice(array, 1, length) : []; - } - - /** - * Creates a slice of `array` with `n` elements taken from the beginning. - * - * @static - * @memberOf _ - * @since 0.1.0 - * @category Array - * @param {Array} array The array to query. - * @param {number} [n=1] The number of elements to take. - * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`. - * @returns {Array} Returns the slice of `array`. - * @example - * - * _.take([1, 2, 3]); - * // => [1] - * - * _.take([1, 2, 3], 2); - * // => [1, 2] - * - * _.take([1, 2, 3], 5); - * // => [1, 2, 3] - * - * _.take([1, 2, 3], 0); - * // => [] - */ - function take(array, n, guard) { - if (!(array && array.length)) { - return []; - } - n = (guard || n === undefined) ? 1 : toInteger(n); - return baseSlice(array, 0, n < 0 ? 0 : n); - } - - /** - * Creates a slice of `array` with `n` elements taken from the end. - * - * @static - * @memberOf _ - * @since 3.0.0 - * @category Array - * @param {Array} array The array to query. - * @param {number} [n=1] The number of elements to take. - * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`. - * @returns {Array} Returns the slice of `array`. - * @example - * - * _.takeRight([1, 2, 3]); - * // => [3] - * - * _.takeRight([1, 2, 3], 2); - * // => [2, 3] - * - * _.takeRight([1, 2, 3], 5); - * // => [1, 2, 3] - * - * _.takeRight([1, 2, 3], 0); - * // => [] - */ - function takeRight(array, n, guard) { - var length = array == null ? 0 : array.length; - if (!length) { - return []; - } - n = (guard || n === undefined) ? 1 : toInteger(n); - n = length - n; - return baseSlice(array, n < 0 ? 0 : n, length); - } - - /** - * Creates a slice of `array` with elements taken from the end. Elements are - * taken until `predicate` returns falsey. The predicate is invoked with - * three arguments: (value, index, array). - * - * @static - * @memberOf _ - * @since 3.0.0 - * @category Array - * @param {Array} array The array to query. - * @param {Function} [predicate=_.identity] The function invoked per iteration. - * @returns {Array} Returns the slice of `array`. - * @example - * - * var users = [ - * { 'user': 'barney', 'active': true }, - * { 'user': 'fred', 'active': false }, - * { 'user': 'pebbles', 'active': false } - * ]; - * - * _.takeRightWhile(users, function(o) { return !o.active; }); - * // => objects for ['fred', 'pebbles'] - * - * // The `_.matches` iteratee shorthand. - * _.takeRightWhile(users, { 'user': 'pebbles', 'active': false }); - * // => objects for ['pebbles'] - * - * // The `_.matchesProperty` iteratee shorthand. - * _.takeRightWhile(users, ['active', false]); - * // => objects for ['fred', 'pebbles'] - * - * // The `_.property` iteratee shorthand. - * _.takeRightWhile(users, 'active'); - * // => [] - */ - function takeRightWhile(array, predicate) { - return (array && array.length) - ? baseWhile(array, getIteratee(predicate, 3), false, true) - : []; - } - - /** - * Creates a slice of `array` with elements taken from the beginning. Elements - * are taken until `predicate` returns falsey. The predicate is invoked with - * three arguments: (value, index, array). - * - * @static - * @memberOf _ - * @since 3.0.0 - * @category Array - * @param {Array} array The array to query. - * @param {Function} [predicate=_.identity] The function invoked per iteration. - * @returns {Array} Returns the slice of `array`. - * @example - * - * var users = [ - * { 'user': 'barney', 'active': false }, - * { 'user': 'fred', 'active': false }, - * { 'user': 'pebbles', 'active': true } - * ]; - * - * _.takeWhile(users, function(o) { return !o.active; }); - * // => objects for ['barney', 'fred'] - * - * // The `_.matches` iteratee shorthand. - * _.takeWhile(users, { 'user': 'barney', 'active': false }); - * // => objects for ['barney'] - * - * // The `_.matchesProperty` iteratee shorthand. - * _.takeWhile(users, ['active', false]); - * // => objects for ['barney', 'fred'] - * - * // The `_.property` iteratee shorthand. - * _.takeWhile(users, 'active'); - * // => [] - */ - function takeWhile(array, predicate) { - return (array && array.length) - ? baseWhile(array, getIteratee(predicate, 3)) - : []; - } - - /** - * Creates an array of unique values, in order, from all given arrays using - * [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero) - * for equality comparisons. - * - * @static - * @memberOf _ - * @since 0.1.0 - * @category Array - * @param {...Array} [arrays] The arrays to inspect. - * @returns {Array} Returns the new array of combined values. - * @example - * - * _.union([2], [1, 2]); - * // => [2, 1] - */ - var union = baseRest(function(arrays) { - return baseUniq(baseFlatten(arrays, 1, isArrayLikeObject, true)); - }); - - /** - * This method is like `_.union` except that it accepts `iteratee` which is - * invoked for each element of each `arrays` to generate the criterion by - * which uniqueness is computed. Result values are chosen from the first - * array in which the value occurs. The iteratee is invoked with one argument: - * (value). - * - * @static - * @memberOf _ - * @since 4.0.0 - * @category Array - * @param {...Array} [arrays] The arrays to inspect. - * @param {Function} [iteratee=_.identity] The iteratee invoked per element. - * @returns {Array} Returns the new array of combined values. - * @example - * - * _.unionBy([2.1], [1.2, 2.3], Math.floor); - * // => [2.1, 1.2] - * - * // The `_.property` iteratee shorthand. - * _.unionBy([{ 'x': 1 }], [{ 'x': 2 }, { 'x': 1 }], 'x'); - * // => [{ 'x': 1 }, { 'x': 2 }] - */ - var unionBy = baseRest(function(arrays) { - var iteratee = last(arrays); - if (isArrayLikeObject(iteratee)) { - iteratee = undefined; - } - return baseUniq(baseFlatten(arrays, 1, isArrayLikeObject, true), getIteratee(iteratee, 2)); - }); - - /** - * This method is like `_.union` except that it accepts `comparator` which - * is invoked to compare elements of `arrays`. Result values are chosen from - * the first array in which the value occurs. The comparator is invoked - * with two arguments: (arrVal, othVal). - * - * @static - * @memberOf _ - * @since 4.0.0 - * @category Array - * @param {...Array} [arrays] The arrays to inspect. - * @param {Function} [comparator] The comparator invoked per element. - * @returns {Array} Returns the new array of combined values. - * @example - * - * var objects = [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }]; - * var others = [{ 'x': 1, 'y': 1 }, { 'x': 1, 'y': 2 }]; - * - * _.unionWith(objects, others, _.isEqual); - * // => [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }, { 'x': 1, 'y': 1 }] - */ - var unionWith = baseRest(function(arrays) { - var comparator = last(arrays); - comparator = typeof comparator == 'function' ? comparator : undefined; - return baseUniq(baseFlatten(arrays, 1, isArrayLikeObject, true), undefined, comparator); - }); - - /** - * Creates a duplicate-free version of an array, using - * [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero) - * for equality comparisons, in which only the first occurrence of each element - * is kept. The order of result values is determined by the order they occur - * in the array. - * - * @static - * @memberOf _ - * @since 0.1.0 - * @category Array - * @param {Array} array The array to inspect. - * @returns {Array} Returns the new duplicate free array. - * @example - * - * _.uniq([2, 1, 2]); - * // => [2, 1] - */ - function uniq(array) { - return (array && array.length) ? baseUniq(array) : []; - } - - /** - * This method is like `_.uniq` except that it accepts `iteratee` which is - * invoked for each element in `array` to generate the criterion by which - * uniqueness is computed. The order of result values is determined by the - * order they occur in the array. The iteratee is invoked with one argument: - * (value). - * - * @static - * @memberOf _ - * @since 4.0.0 - * @category Array - * @param {Array} array The array to inspect. - * @param {Function} [iteratee=_.identity] The iteratee invoked per element. - * @returns {Array} Returns the new duplicate free array. - * @example - * - * _.uniqBy([2.1, 1.2, 2.3], Math.floor); - * // => [2.1, 1.2] - * - * // The `_.property` iteratee shorthand. - * _.uniqBy([{ 'x': 1 }, { 'x': 2 }, { 'x': 1 }], 'x'); - * // => [{ 'x': 1 }, { 'x': 2 }] - */ - function uniqBy(array, iteratee) { - return (array && array.length) ? baseUniq(array, getIteratee(iteratee, 2)) : []; - } - - /** - * This method is like `_.uniq` except that it accepts `comparator` which - * is invoked to compare elements of `array`. The order of result values is - * determined by the order they occur in the array.The comparator is invoked - * with two arguments: (arrVal, othVal). - * - * @static - * @memberOf _ - * @since 4.0.0 - * @category Array - * @param {Array} array The array to inspect. - * @param {Function} [comparator] The comparator invoked per element. - * @returns {Array} Returns the new duplicate free array. - * @example - * - * var objects = [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }, { 'x': 1, 'y': 2 }]; - * - * _.uniqWith(objects, _.isEqual); - * // => [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }] - */ - function uniqWith(array, comparator) { - comparator = typeof comparator == 'function' ? comparator : undefined; - return (array && array.length) ? baseUniq(array, undefined, comparator) : []; - } - - /** - * This method is like `_.zip` except that it accepts an array of grouped - * elements and creates an array regrouping the elements to their pre-zip - * configuration. - * - * @static - * @memberOf _ - * @since 1.2.0 - * @category Array - * @param {Array} array The array of grouped elements to process. - * @returns {Array} Returns the new array of regrouped elements. - * @example - * - * var zipped = _.zip(['a', 'b'], [1, 2], [true, false]); - * // => [['a', 1, true], ['b', 2, false]] - * - * _.unzip(zipped); - * // => [['a', 'b'], [1, 2], [true, false]] - */ - function unzip(array) { - if (!(array && array.length)) { - return []; - } - var length = 0; - array = arrayFilter(array, function(group) { - if (isArrayLikeObject(group)) { - length = nativeMax(group.length, length); - return true; - } - }); - return baseTimes(length, function(index) { - return arrayMap(array, baseProperty(index)); - }); - } - - /** - * This method is like `_.unzip` except that it accepts `iteratee` to specify - * how regrouped values should be combined. The iteratee is invoked with the - * elements of each group: (...group). - * - * @static - * @memberOf _ - * @since 3.8.0 - * @category Array - * @param {Array} array The array of grouped elements to process. - * @param {Function} [iteratee=_.identity] The function to combine - * regrouped values. - * @returns {Array} Returns the new array of regrouped elements. - * @example - * - * var zipped = _.zip([1, 2], [10, 20], [100, 200]); - * // => [[1, 10, 100], [2, 20, 200]] - * - * _.unzipWith(zipped, _.add); - * // => [3, 30, 300] - */ - function unzipWith(array, iteratee) { - if (!(array && array.length)) { - return []; - } - var result = unzip(array); - if (iteratee == null) { - return result; - } - return arrayMap(result, function(group) { - return apply(iteratee, undefined, group); - }); - } - - /** - * Creates an array excluding all given values using - * [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero) - * for equality comparisons. - * - * **Note:** Unlike `_.pull`, this method returns a new array. - * - * @static - * @memberOf _ - * @since 0.1.0 - * @category Array - * @param {Array} array The array to inspect. - * @param {...*} [values] The values to exclude. - * @returns {Array} Returns the new array of filtered values. - * @see _.difference, _.xor - * @example - * - * _.without([2, 1, 2, 3], 1, 2); - * // => [3] - */ - var without = baseRest(function(array, values) { - return isArrayLikeObject(array) - ? baseDifference(array, values) - : []; - }); - - /** - * Creates an array of unique values that is the - * [symmetric difference](https://en.wikipedia.org/wiki/Symmetric_difference) - * of the given arrays. The order of result values is determined by the order - * they occur in the arrays. - * - * @static - * @memberOf _ - * @since 2.4.0 - * @category Array - * @param {...Array} [arrays] The arrays to inspect. - * @returns {Array} Returns the new array of filtered values. - * @see _.difference, _.without - * @example - * - * _.xor([2, 1], [2, 3]); - * // => [1, 3] - */ - var xor = baseRest(function(arrays) { - return baseXor(arrayFilter(arrays, isArrayLikeObject)); - }); - - /** - * This method is like `_.xor` except that it accepts `iteratee` which is - * invoked for each element of each `arrays` to generate the criterion by - * which by which they're compared. The order of result values is determined - * by the order they occur in the arrays. The iteratee is invoked with one - * argument: (value). - * - * @static - * @memberOf _ - * @since 4.0.0 - * @category Array - * @param {...Array} [arrays] The arrays to inspect. - * @param {Function} [iteratee=_.identity] The iteratee invoked per element. - * @returns {Array} Returns the new array of filtered values. - * @example - * - * _.xorBy([2.1, 1.2], [2.3, 3.4], Math.floor); - * // => [1.2, 3.4] - * - * // The `_.property` iteratee shorthand. - * _.xorBy([{ 'x': 1 }], [{ 'x': 2 }, { 'x': 1 }], 'x'); - * // => [{ 'x': 2 }] - */ - var xorBy = baseRest(function(arrays) { - var iteratee = last(arrays); - if (isArrayLikeObject(iteratee)) { - iteratee = undefined; - } - return baseXor(arrayFilter(arrays, isArrayLikeObject), getIteratee(iteratee, 2)); - }); - - /** - * This method is like `_.xor` except that it accepts `comparator` which is - * invoked to compare elements of `arrays`. The order of result values is - * determined by the order they occur in the arrays. The comparator is invoked - * with two arguments: (arrVal, othVal). - * - * @static - * @memberOf _ - * @since 4.0.0 - * @category Array - * @param {...Array} [arrays] The arrays to inspect. - * @param {Function} [comparator] The comparator invoked per element. - * @returns {Array} Returns the new array of filtered values. - * @example - * - * var objects = [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }]; - * var others = [{ 'x': 1, 'y': 1 }, { 'x': 1, 'y': 2 }]; - * - * _.xorWith(objects, others, _.isEqual); - * // => [{ 'x': 2, 'y': 1 }, { 'x': 1, 'y': 1 }] - */ - var xorWith = baseRest(function(arrays) { - var comparator = last(arrays); - comparator = typeof comparator == 'function' ? comparator : undefined; - return baseXor(arrayFilter(arrays, isArrayLikeObject), undefined, comparator); - }); - - /** - * Creates an array of grouped elements, the first of which contains the - * first elements of the given arrays, the second of which contains the - * second elements of the given arrays, and so on. - * - * @static - * @memberOf _ - * @since 0.1.0 - * @category Array - * @param {...Array} [arrays] The arrays to process. - * @returns {Array} Returns the new array of grouped elements. - * @example - * - * _.zip(['a', 'b'], [1, 2], [true, false]); - * // => [['a', 1, true], ['b', 2, false]] - */ - var zip = baseRest(unzip); - - /** - * This method is like `_.fromPairs` except that it accepts two arrays, - * one of property identifiers and one of corresponding values. - * - * @static - * @memberOf _ - * @since 0.4.0 - * @category Array - * @param {Array} [props=[]] The property identifiers. - * @param {Array} [values=[]] The property values. - * @returns {Object} Returns the new object. - * @example - * - * _.zipObject(['a', 'b'], [1, 2]); - * // => { 'a': 1, 'b': 2 } - */ - function zipObject(props, values) { - return baseZipObject(props || [], values || [], assignValue); - } - - /** - * This method is like `_.zipObject` except that it supports property paths. - * - * @static - * @memberOf _ - * @since 4.1.0 - * @category Array - * @param {Array} [props=[]] The property identifiers. - * @param {Array} [values=[]] The property values. - * @returns {Object} Returns the new object. - * @example - * - * _.zipObjectDeep(['a.b[0].c', 'a.b[1].d'], [1, 2]); - * // => { 'a': { 'b': [{ 'c': 1 }, { 'd': 2 }] } } - */ - function zipObjectDeep(props, values) { - return baseZipObject(props || [], values || [], baseSet); - } - - /** - * This method is like `_.zip` except that it accepts `iteratee` to specify - * how grouped values should be combined. The iteratee is invoked with the - * elements of each group: (...group). - * - * @static - * @memberOf _ - * @since 3.8.0 - * @category Array - * @param {...Array} [arrays] The arrays to process. - * @param {Function} [iteratee=_.identity] The function to combine - * grouped values. - * @returns {Array} Returns the new array of grouped elements. - * @example - * - * _.zipWith([1, 2], [10, 20], [100, 200], function(a, b, c) { - * return a + b + c; - * }); - * // => [111, 222] - */ - var zipWith = baseRest(function(arrays) { - var length = arrays.length, - iteratee = length > 1 ? arrays[length - 1] : undefined; - - iteratee = typeof iteratee == 'function' ? (arrays.pop(), iteratee) : undefined; - return unzipWith(arrays, iteratee); - }); - - /*------------------------------------------------------------------------*/ - - /** - * Creates a `lodash` wrapper instance that wraps `value` with explicit method - * chain sequences enabled. The result of such sequences must be unwrapped - * with `_#value`. - * - * @static - * @memberOf _ - * @since 1.3.0 - * @category Seq - * @param {*} value The value to wrap. - * @returns {Object} Returns the new `lodash` wrapper instance. - * @example - * - * var users = [ - * { 'user': 'barney', 'age': 36 }, - * { 'user': 'fred', 'age': 40 }, - * { 'user': 'pebbles', 'age': 1 } - * ]; - * - * var youngest = _ - * .chain(users) - * .sortBy('age') - * .map(function(o) { - * return o.user + ' is ' + o.age; - * }) - * .head() - * .value(); - * // => 'pebbles is 1' - */ - function chain(value) { - var result = lodash(value); - result.__chain__ = true; - return result; - } - - /** - * This method invokes `interceptor` and returns `value`. The interceptor - * is invoked with one argument; (value). The purpose of this method is to - * "tap into" a method chain sequence in order to modify intermediate results. - * - * @static - * @memberOf _ - * @since 0.1.0 - * @category Seq - * @param {*} value The value to provide to `interceptor`. - * @param {Function} interceptor The function to invoke. - * @returns {*} Returns `value`. - * @example - * - * _([1, 2, 3]) - * .tap(function(array) { - * // Mutate input array. - * array.pop(); - * }) - * .reverse() - * .value(); - * // => [2, 1] - */ - function tap(value, interceptor) { - interceptor(value); - return value; - } - - /** - * This method is like `_.tap` except that it returns the result of `interceptor`. - * The purpose of this method is to "pass thru" values replacing intermediate - * results in a method chain sequence. - * - * @static - * @memberOf _ - * @since 3.0.0 - * @category Seq - * @param {*} value The value to provide to `interceptor`. - * @param {Function} interceptor The function to invoke. - * @returns {*} Returns the result of `interceptor`. - * @example - * - * _(' abc ') - * .chain() - * .trim() - * .thru(function(value) { - * return [value]; - * }) - * .value(); - * // => ['abc'] - */ - function thru(value, interceptor) { - return interceptor(value); - } - - /** - * This method is the wrapper version of `_.at`. - * - * @name at - * @memberOf _ - * @since 1.0.0 - * @category Seq - * @param {...(string|string[])} [paths] The property paths to pick. - * @returns {Object} Returns the new `lodash` wrapper instance. - * @example - * - * var object = { 'a': [{ 'b': { 'c': 3 } }, 4] }; - * - * _(object).at(['a[0].b.c', 'a[1]']).value(); - * // => [3, 4] - */ - var wrapperAt = flatRest(function(paths) { - var length = paths.length, - start = length ? paths[0] : 0, - value = this.__wrapped__, - interceptor = function(object) { return baseAt(object, paths); }; - - if (length > 1 || this.__actions__.length || - !(value instanceof LazyWrapper) || !isIndex(start)) { - return this.thru(interceptor); - } - value = value.slice(start, +start + (length ? 1 : 0)); - value.__actions__.push({ - 'func': thru, - 'args': [interceptor], - 'thisArg': undefined - }); - return new LodashWrapper(value, this.__chain__).thru(function(array) { - if (length && !array.length) { - array.push(undefined); - } - return array; - }); - }); - - /** - * Creates a `lodash` wrapper instance with explicit method chain sequences enabled. - * - * @name chain - * @memberOf _ - * @since 0.1.0 - * @category Seq - * @returns {Object} Returns the new `lodash` wrapper instance. - * @example - * - * var users = [ - * { 'user': 'barney', 'age': 36 }, - * { 'user': 'fred', 'age': 40 } - * ]; - * - * // A sequence without explicit chaining. - * _(users).head(); - * // => { 'user': 'barney', 'age': 36 } - * - * // A sequence with explicit chaining. - * _(users) - * .chain() - * .head() - * .pick('user') - * .value(); - * // => { 'user': 'barney' } - */ - function wrapperChain() { - return chain(this); - } - - /** - * Executes the chain sequence and returns the wrapped result. - * - * @name commit - * @memberOf _ - * @since 3.2.0 - * @category Seq - * @returns {Object} Returns the new `lodash` wrapper instance. - * @example - * - * var array = [1, 2]; - * var wrapped = _(array).push(3); - * - * console.log(array); - * // => [1, 2] - * - * wrapped = wrapped.commit(); - * console.log(array); - * // => [1, 2, 3] - * - * wrapped.last(); - * // => 3 - * - * console.log(array); - * // => [1, 2, 3] - */ - function wrapperCommit() { - return new LodashWrapper(this.value(), this.__chain__); - } - - /** - * Gets the next value on a wrapped object following the - * [iterator protocol](https://mdn.io/iteration_protocols#iterator). - * - * @name next - * @memberOf _ - * @since 4.0.0 - * @category Seq - * @returns {Object} Returns the next iterator value. - * @example - * - * var wrapped = _([1, 2]); - * - * wrapped.next(); - * // => { 'done': false, 'value': 1 } - * - * wrapped.next(); - * // => { 'done': false, 'value': 2 } - * - * wrapped.next(); - * // => { 'done': true, 'value': undefined } - */ - function wrapperNext() { - if (this.__values__ === undefined) { - this.__values__ = toArray(this.value()); - } - var done = this.__index__ >= this.__values__.length, - value = done ? undefined : this.__values__[this.__index__++]; - - return { 'done': done, 'value': value }; - } - - /** - * Enables the wrapper to be iterable. - * - * @name Symbol.iterator - * @memberOf _ - * @since 4.0.0 - * @category Seq - * @returns {Object} Returns the wrapper object. - * @example - * - * var wrapped = _([1, 2]); - * - * wrapped[Symbol.iterator]() === wrapped; - * // => true - * - * Array.from(wrapped); - * // => [1, 2] - */ - function wrapperToIterator() { - return this; - } - - /** - * Creates a clone of the chain sequence planting `value` as the wrapped value. - * - * @name plant - * @memberOf _ - * @since 3.2.0 - * @category Seq - * @param {*} value The value to plant. - * @returns {Object} Returns the new `lodash` wrapper instance. - * @example - * - * function square(n) { - * return n * n; - * } - * - * var wrapped = _([1, 2]).map(square); - * var other = wrapped.plant([3, 4]); - * - * other.value(); - * // => [9, 16] - * - * wrapped.value(); - * // => [1, 4] - */ - function wrapperPlant(value) { - var result, - parent = this; - - while (parent instanceof baseLodash) { - var clone = wrapperClone(parent); - clone.__index__ = 0; - clone.__values__ = undefined; - if (result) { - previous.__wrapped__ = clone; - } else { - result = clone; - } - var previous = clone; - parent = parent.__wrapped__; - } - previous.__wrapped__ = value; - return result; - } - - /** - * This method is the wrapper version of `_.reverse`. - * - * **Note:** This method mutates the wrapped array. - * - * @name reverse - * @memberOf _ - * @since 0.1.0 - * @category Seq - * @returns {Object} Returns the new `lodash` wrapper instance. - * @example - * - * var array = [1, 2, 3]; - * - * _(array).reverse().value() - * // => [3, 2, 1] - * - * console.log(array); - * // => [3, 2, 1] - */ - function wrapperReverse() { - var value = this.__wrapped__; - if (value instanceof LazyWrapper) { - var wrapped = value; - if (this.__actions__.length) { - wrapped = new LazyWrapper(this); - } - wrapped = wrapped.reverse(); - wrapped.__actions__.push({ - 'func': thru, - 'args': [reverse], - 'thisArg': undefined - }); - return new LodashWrapper(wrapped, this.__chain__); - } - return this.thru(reverse); - } - - /** - * Executes the chain sequence to resolve the unwrapped value. - * - * @name value - * @memberOf _ - * @since 0.1.0 - * @alias toJSON, valueOf - * @category Seq - * @returns {*} Returns the resolved unwrapped value. - * @example - * - * _([1, 2, 3]).value(); - * // => [1, 2, 3] - */ - function wrapperValue() { - return baseWrapperValue(this.__wrapped__, this.__actions__); - } - - /*------------------------------------------------------------------------*/ - - /** - * Creates an object composed of keys generated from the results of running - * each element of `collection` thru `iteratee`. The corresponding value of - * each key is the number of times the key was returned by `iteratee`. The - * iteratee is invoked with one argument: (value). - * - * @static - * @memberOf _ - * @since 0.5.0 - * @category Collection - * @param {Array|Object} collection The collection to iterate over. - * @param {Function} [iteratee=_.identity] The iteratee to transform keys. - * @returns {Object} Returns the composed aggregate object. - * @example - * - * _.countBy([6.1, 4.2, 6.3], Math.floor); - * // => { '4': 1, '6': 2 } - * - * // The `_.property` iteratee shorthand. - * _.countBy(['one', 'two', 'three'], 'length'); - * // => { '3': 2, '5': 1 } - */ - var countBy = createAggregator(function(result, value, key) { - if (hasOwnProperty.call(result, key)) { - ++result[key]; - } else { - baseAssignValue(result, key, 1); - } - }); - - /** - * Checks if `predicate` returns truthy for **all** elements of `collection`. - * Iteration is stopped once `predicate` returns falsey. The predicate is - * invoked with three arguments: (value, index|key, collection). - * - * **Note:** This method returns `true` for - * [empty collections](https://en.wikipedia.org/wiki/Empty_set) because - * [everything is true](https://en.wikipedia.org/wiki/Vacuous_truth) of - * elements of empty collections. - * - * @static - * @memberOf _ - * @since 0.1.0 - * @category Collection - * @param {Array|Object} collection The collection to iterate over. - * @param {Function} [predicate=_.identity] The function invoked per iteration. - * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`. - * @returns {boolean} Returns `true` if all elements pass the predicate check, - * else `false`. - * @example - * - * _.every([true, 1, null, 'yes'], Boolean); - * // => false - * - * var users = [ - * { 'user': 'barney', 'age': 36, 'active': false }, - * { 'user': 'fred', 'age': 40, 'active': false } - * ]; - * - * // The `_.matches` iteratee shorthand. - * _.every(users, { 'user': 'barney', 'active': false }); - * // => false - * - * // The `_.matchesProperty` iteratee shorthand. - * _.every(users, ['active', false]); - * // => true - * - * // The `_.property` iteratee shorthand. - * _.every(users, 'active'); - * // => false - */ - function every(collection, predicate, guard) { - var func = isArray(collection) ? arrayEvery : baseEvery; - if (guard && isIterateeCall(collection, predicate, guard)) { - predicate = undefined; - } - return func(collection, getIteratee(predicate, 3)); - } - - /** - * Iterates over elements of `collection`, returning an array of all elements - * `predicate` returns truthy for. The predicate is invoked with three - * arguments: (value, index|key, collection). - * - * **Note:** Unlike `_.remove`, this method returns a new array. - * - * @static - * @memberOf _ - * @since 0.1.0 - * @category Collection - * @param {Array|Object} collection The collection to iterate over. - * @param {Function} [predicate=_.identity] The function invoked per iteration. - * @returns {Array} Returns the new filtered array. - * @see _.reject - * @example - * - * var users = [ - * { 'user': 'barney', 'age': 36, 'active': true }, - * { 'user': 'fred', 'age': 40, 'active': false } - * ]; - * - * _.filter(users, function(o) { return !o.active; }); - * // => objects for ['fred'] - * - * // The `_.matches` iteratee shorthand. - * _.filter(users, { 'age': 36, 'active': true }); - * // => objects for ['barney'] - * - * // The `_.matchesProperty` iteratee shorthand. - * _.filter(users, ['active', false]); - * // => objects for ['fred'] - * - * // The `_.property` iteratee shorthand. - * _.filter(users, 'active'); - * // => objects for ['barney'] - */ - function filter(collection, predicate) { - var func = isArray(collection) ? arrayFilter : baseFilter; - return func(collection, getIteratee(predicate, 3)); - } - - /** - * Iterates over elements of `collection`, returning the first element - * `predicate` returns truthy for. The predicate is invoked with three - * arguments: (value, index|key, collection). - * - * @static - * @memberOf _ - * @since 0.1.0 - * @category Collection - * @param {Array|Object} collection The collection to inspect. - * @param {Function} [predicate=_.identity] The function invoked per iteration. - * @param {number} [fromIndex=0] The index to search from. - * @returns {*} Returns the matched element, else `undefined`. - * @example - * - * var users = [ - * { 'user': 'barney', 'age': 36, 'active': true }, - * { 'user': 'fred', 'age': 40, 'active': false }, - * { 'user': 'pebbles', 'age': 1, 'active': true } - * ]; - * - * _.find(users, function(o) { return o.age < 40; }); - * // => object for 'barney' - * - * // The `_.matches` iteratee shorthand. - * _.find(users, { 'age': 1, 'active': true }); - * // => object for 'pebbles' - * - * // The `_.matchesProperty` iteratee shorthand. - * _.find(users, ['active', false]); - * // => object for 'fred' - * - * // The `_.property` iteratee shorthand. - * _.find(users, 'active'); - * // => object for 'barney' - */ - var find = createFind(findIndex); - - /** - * This method is like `_.find` except that it iterates over elements of - * `collection` from right to left. - * - * @static - * @memberOf _ - * @since 2.0.0 - * @category Collection - * @param {Array|Object} collection The collection to inspect. - * @param {Function} [predicate=_.identity] The function invoked per iteration. - * @param {number} [fromIndex=collection.length-1] The index to search from. - * @returns {*} Returns the matched element, else `undefined`. - * @example - * - * _.findLast([1, 2, 3, 4], function(n) { - * return n % 2 == 1; - * }); - * // => 3 - */ - var findLast = createFind(findLastIndex); - - /** - * Creates a flattened array of values by running each element in `collection` - * thru `iteratee` and flattening the mapped results. The iteratee is invoked - * with three arguments: (value, index|key, collection). - * - * @static - * @memberOf _ - * @since 4.0.0 - * @category Collection - * @param {Array|Object} collection The collection to iterate over. - * @param {Function} [iteratee=_.identity] The function invoked per iteration. - * @returns {Array} Returns the new flattened array. - * @example - * - * function duplicate(n) { - * return [n, n]; - * } - * - * _.flatMap([1, 2], duplicate); - * // => [1, 1, 2, 2] - */ - function flatMap(collection, iteratee) { - return baseFlatten(map(collection, iteratee), 1); - } - - /** - * This method is like `_.flatMap` except that it recursively flattens the - * mapped results. - * - * @static - * @memberOf _ - * @since 4.7.0 - * @category Collection - * @param {Array|Object} collection The collection to iterate over. - * @param {Function} [iteratee=_.identity] The function invoked per iteration. - * @returns {Array} Returns the new flattened array. - * @example - * - * function duplicate(n) { - * return [[[n, n]]]; - * } - * - * _.flatMapDeep([1, 2], duplicate); - * // => [1, 1, 2, 2] - */ - function flatMapDeep(collection, iteratee) { - return baseFlatten(map(collection, iteratee), INFINITY); - } - - /** - * This method is like `_.flatMap` except that it recursively flattens the - * mapped results up to `depth` times. - * - * @static - * @memberOf _ - * @since 4.7.0 - * @category Collection - * @param {Array|Object} collection The collection to iterate over. - * @param {Function} [iteratee=_.identity] The function invoked per iteration. - * @param {number} [depth=1] The maximum recursion depth. - * @returns {Array} Returns the new flattened array. - * @example - * - * function duplicate(n) { - * return [[[n, n]]]; - * } - * - * _.flatMapDepth([1, 2], duplicate, 2); - * // => [[1, 1], [2, 2]] - */ - function flatMapDepth(collection, iteratee, depth) { - depth = depth === undefined ? 1 : toInteger(depth); - return baseFlatten(map(collection, iteratee), depth); - } - - /** - * Iterates over elements of `collection` and invokes `iteratee` for each element. - * The iteratee is invoked with three arguments: (value, index|key, collection). - * Iteratee functions may exit iteration early by explicitly returning `false`. - * - * **Note:** As with other "Collections" methods, objects with a "length" - * property are iterated like arrays. To avoid this behavior use `_.forIn` - * or `_.forOwn` for object iteration. - * - * @static - * @memberOf _ - * @since 0.1.0 - * @alias each - * @category Collection - * @param {Array|Object} collection The collection to iterate over. - * @param {Function} [iteratee=_.identity] The function invoked per iteration. - * @returns {Array|Object} Returns `collection`. - * @see _.forEachRight - * @example - * - * _.forEach([1, 2], function(value) { - * console.log(value); - * }); - * // => Logs `1` then `2`. - * - * _.forEach({ 'a': 1, 'b': 2 }, function(value, key) { - * console.log(key); - * }); - * // => Logs 'a' then 'b' (iteration order is not guaranteed). - */ - function forEach(collection, iteratee) { - var func = isArray(collection) ? arrayEach : baseEach; - return func(collection, getIteratee(iteratee, 3)); - } - - /** - * This method is like `_.forEach` except that it iterates over elements of - * `collection` from right to left. - * - * @static - * @memberOf _ - * @since 2.0.0 - * @alias eachRight - * @category Collection - * @param {Array|Object} collection The collection to iterate over. - * @param {Function} [iteratee=_.identity] The function invoked per iteration. - * @returns {Array|Object} Returns `collection`. - * @see _.forEach - * @example - * - * _.forEachRight([1, 2], function(value) { - * console.log(value); - * }); - * // => Logs `2` then `1`. - */ - function forEachRight(collection, iteratee) { - var func = isArray(collection) ? arrayEachRight : baseEachRight; - return func(collection, getIteratee(iteratee, 3)); - } - - /** - * Creates an object composed of keys generated from the results of running - * each element of `collection` thru `iteratee`. The order of grouped values - * is determined by the order they occur in `collection`. The corresponding - * value of each key is an array of elements responsible for generating the - * key. The iteratee is invoked with one argument: (value). - * - * @static - * @memberOf _ - * @since 0.1.0 - * @category Collection - * @param {Array|Object} collection The collection to iterate over. - * @param {Function} [iteratee=_.identity] The iteratee to transform keys. - * @returns {Object} Returns the composed aggregate object. - * @example - * - * _.groupBy([6.1, 4.2, 6.3], Math.floor); - * // => { '4': [4.2], '6': [6.1, 6.3] } - * - * // The `_.property` iteratee shorthand. - * _.groupBy(['one', 'two', 'three'], 'length'); - * // => { '3': ['one', 'two'], '5': ['three'] } - */ - var groupBy = createAggregator(function(result, value, key) { - if (hasOwnProperty.call(result, key)) { - result[key].push(value); - } else { - baseAssignValue(result, key, [value]); - } - }); - - /** - * Checks if `value` is in `collection`. If `collection` is a string, it's - * checked for a substring of `value`, otherwise - * [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero) - * is used for equality comparisons. If `fromIndex` is negative, it's used as - * the offset from the end of `collection`. - * - * @static - * @memberOf _ - * @since 0.1.0 - * @category Collection - * @param {Array|Object|string} collection The collection to inspect. - * @param {*} value The value to search for. - * @param {number} [fromIndex=0] The index to search from. - * @param- {Object} [guard] Enables use as an iteratee for methods like `_.reduce`. - * @returns {boolean} Returns `true` if `value` is found, else `false`. - * @example - * - * _.includes([1, 2, 3], 1); - * // => true - * - * _.includes([1, 2, 3], 1, 2); - * // => false - * - * _.includes({ 'a': 1, 'b': 2 }, 1); - * // => true - * - * _.includes('abcd', 'bc'); - * // => true - */ - function includes(collection, value, fromIndex, guard) { - collection = isArrayLike(collection) ? collection : values(collection); - fromIndex = (fromIndex && !guard) ? toInteger(fromIndex) : 0; - - var length = collection.length; - if (fromIndex < 0) { - fromIndex = nativeMax(length + fromIndex, 0); - } - return isString(collection) - ? (fromIndex <= length && collection.indexOf(value, fromIndex) > -1) - : (!!length && baseIndexOf(collection, value, fromIndex) > -1); - } - - /** - * Invokes the method at `path` of each element in `collection`, returning - * an array of the results of each invoked method. Any additional arguments - * are provided to each invoked method. If `path` is a function, it's invoked - * for, and `this` bound to, each element in `collection`. - * - * @static - * @memberOf _ - * @since 4.0.0 - * @category Collection - * @param {Array|Object} collection The collection to iterate over. - * @param {Array|Function|string} path The path of the method to invoke or - * the function invoked per iteration. - * @param {...*} [args] The arguments to invoke each method with. - * @returns {Array} Returns the array of results. - * @example - * - * _.invokeMap([[5, 1, 7], [3, 2, 1]], 'sort'); - * // => [[1, 5, 7], [1, 2, 3]] - * - * _.invokeMap([123, 456], String.prototype.split, ''); - * // => [['1', '2', '3'], ['4', '5', '6']] - */ - var invokeMap = baseRest(function(collection, path, args) { - var index = -1, - isFunc = typeof path == 'function', - result = isArrayLike(collection) ? Array(collection.length) : []; - - baseEach(collection, function(value) { - result[++index] = isFunc ? apply(path, value, args) : baseInvoke(value, path, args); - }); - return result; - }); - - /** - * Creates an object composed of keys generated from the results of running - * each element of `collection` thru `iteratee`. The corresponding value of - * each key is the last element responsible for generating the key. The - * iteratee is invoked with one argument: (value). - * - * @static - * @memberOf _ - * @since 4.0.0 - * @category Collection - * @param {Array|Object} collection The collection to iterate over. - * @param {Function} [iteratee=_.identity] The iteratee to transform keys. - * @returns {Object} Returns the composed aggregate object. - * @example - * - * var array = [ - * { 'dir': 'left', 'code': 97 }, - * { 'dir': 'right', 'code': 100 } - * ]; - * - * _.keyBy(array, function(o) { - * return String.fromCharCode(o.code); - * }); - * // => { 'a': { 'dir': 'left', 'code': 97 }, 'd': { 'dir': 'right', 'code': 100 } } - * - * _.keyBy(array, 'dir'); - * // => { 'left': { 'dir': 'left', 'code': 97 }, 'right': { 'dir': 'right', 'code': 100 } } - */ - var keyBy = createAggregator(function(result, value, key) { - baseAssignValue(result, key, value); - }); - - /** - * Creates an array of values by running each element in `collection` thru - * `iteratee`. The iteratee is invoked with three arguments: - * (value, index|key, collection). - * - * Many lodash methods are guarded to work as iteratees for methods like - * `_.every`, `_.filter`, `_.map`, `_.mapValues`, `_.reject`, and `_.some`. - * - * The guarded methods are: - * `ary`, `chunk`, `curry`, `curryRight`, `drop`, `dropRight`, `every`, - * `fill`, `invert`, `parseInt`, `random`, `range`, `rangeRight`, `repeat`, - * `sampleSize`, `slice`, `some`, `sortBy`, `split`, `take`, `takeRight`, - * `template`, `trim`, `trimEnd`, `trimStart`, and `words` - * - * @static - * @memberOf _ - * @since 0.1.0 - * @category Collection - * @param {Array|Object} collection The collection to iterate over. - * @param {Function} [iteratee=_.identity] The function invoked per iteration. - * @returns {Array} Returns the new mapped array. - * @example - * - * function square(n) { - * return n * n; - * } - * - * _.map([4, 8], square); - * // => [16, 64] - * - * _.map({ 'a': 4, 'b': 8 }, square); - * // => [16, 64] (iteration order is not guaranteed) - * - * var users = [ - * { 'user': 'barney' }, - * { 'user': 'fred' } - * ]; - * - * // The `_.property` iteratee shorthand. - * _.map(users, 'user'); - * // => ['barney', 'fred'] - */ - function map(collection, iteratee) { - var func = isArray(collection) ? arrayMap : baseMap; - return func(collection, getIteratee(iteratee, 3)); - } - - /** - * This method is like `_.sortBy` except that it allows specifying the sort - * orders of the iteratees to sort by. If `orders` is unspecified, all values - * are sorted in ascending order. Otherwise, specify an order of "desc" for - * descending or "asc" for ascending sort order of corresponding values. - * - * @static - * @memberOf _ - * @since 4.0.0 - * @category Collection - * @param {Array|Object} collection The collection to iterate over. - * @param {Array[]|Function[]|Object[]|string[]} [iteratees=[_.identity]] - * The iteratees to sort by. - * @param {string[]} [orders] The sort orders of `iteratees`. - * @param- {Object} [guard] Enables use as an iteratee for methods like `_.reduce`. - * @returns {Array} Returns the new sorted array. - * @example - * - * var users = [ - * { 'user': 'fred', 'age': 48 }, - * { 'user': 'barney', 'age': 34 }, - * { 'user': 'fred', 'age': 40 }, - * { 'user': 'barney', 'age': 36 } - * ]; - * - * // Sort by `user` in ascending order and by `age` in descending order. - * _.orderBy(users, ['user', 'age'], ['asc', 'desc']); - * // => objects for [['barney', 36], ['barney', 34], ['fred', 48], ['fred', 40]] - */ - function orderBy(collection, iteratees, orders, guard) { - if (collection == null) { - return []; - } - if (!isArray(iteratees)) { - iteratees = iteratees == null ? [] : [iteratees]; - } - orders = guard ? undefined : orders; - if (!isArray(orders)) { - orders = orders == null ? [] : [orders]; - } - return baseOrderBy(collection, iteratees, orders); - } - - /** - * Creates an array of elements split into two groups, the first of which - * contains elements `predicate` returns truthy for, the second of which - * contains elements `predicate` returns falsey for. The predicate is - * invoked with one argument: (value). - * - * @static - * @memberOf _ - * @since 3.0.0 - * @category Collection - * @param {Array|Object} collection The collection to iterate over. - * @param {Function} [predicate=_.identity] The function invoked per iteration. - * @returns {Array} Returns the array of grouped elements. - * @example - * - * var users = [ - * { 'user': 'barney', 'age': 36, 'active': false }, - * { 'user': 'fred', 'age': 40, 'active': true }, - * { 'user': 'pebbles', 'age': 1, 'active': false } - * ]; - * - * _.partition(users, function(o) { return o.active; }); - * // => objects for [['fred'], ['barney', 'pebbles']] - * - * // The `_.matches` iteratee shorthand. - * _.partition(users, { 'age': 1, 'active': false }); - * // => objects for [['pebbles'], ['barney', 'fred']] - * - * // The `_.matchesProperty` iteratee shorthand. - * _.partition(users, ['active', false]); - * // => objects for [['barney', 'pebbles'], ['fred']] - * - * // The `_.property` iteratee shorthand. - * _.partition(users, 'active'); - * // => objects for [['fred'], ['barney', 'pebbles']] - */ - var partition = createAggregator(function(result, value, key) { - result[key ? 0 : 1].push(value); - }, function() { return [[], []]; }); - - /** - * Reduces `collection` to a value which is the accumulated result of running - * each element in `collection` thru `iteratee`, where each successive - * invocation is supplied the return value of the previous. If `accumulator` - * is not given, the first element of `collection` is used as the initial - * value. The iteratee is invoked with four arguments: - * (accumulator, value, index|key, collection). - * - * Many lodash methods are guarded to work as iteratees for methods like - * `_.reduce`, `_.reduceRight`, and `_.transform`. - * - * The guarded methods are: - * `assign`, `defaults`, `defaultsDeep`, `includes`, `merge`, `orderBy`, - * and `sortBy` - * - * @static - * @memberOf _ - * @since 0.1.0 - * @category Collection - * @param {Array|Object} collection The collection to iterate over. - * @param {Function} [iteratee=_.identity] The function invoked per iteration. - * @param {*} [accumulator] The initial value. - * @returns {*} Returns the accumulated value. - * @see _.reduceRight - * @example - * - * _.reduce([1, 2], function(sum, n) { - * return sum + n; - * }, 0); - * // => 3 - * - * _.reduce({ 'a': 1, 'b': 2, 'c': 1 }, function(result, value, key) { - * (result[value] || (result[value] = [])).push(key); - * return result; - * }, {}); - * // => { '1': ['a', 'c'], '2': ['b'] } (iteration order is not guaranteed) - */ - function reduce(collection, iteratee, accumulator) { - var func = isArray(collection) ? arrayReduce : baseReduce, - initAccum = arguments.length < 3; - - return func(collection, getIteratee(iteratee, 4), accumulator, initAccum, baseEach); - } - - /** - * This method is like `_.reduce` except that it iterates over elements of - * `collection` from right to left. - * - * @static - * @memberOf _ - * @since 0.1.0 - * @category Collection - * @param {Array|Object} collection The collection to iterate over. - * @param {Function} [iteratee=_.identity] The function invoked per iteration. - * @param {*} [accumulator] The initial value. - * @returns {*} Returns the accumulated value. - * @see _.reduce - * @example - * - * var array = [[0, 1], [2, 3], [4, 5]]; - * - * _.reduceRight(array, function(flattened, other) { - * return flattened.concat(other); - * }, []); - * // => [4, 5, 2, 3, 0, 1] - */ - function reduceRight(collection, iteratee, accumulator) { - var func = isArray(collection) ? arrayReduceRight : baseReduce, - initAccum = arguments.length < 3; - - return func(collection, getIteratee(iteratee, 4), accumulator, initAccum, baseEachRight); - } - - /** - * The opposite of `_.filter`; this method returns the elements of `collection` - * that `predicate` does **not** return truthy for. - * - * @static - * @memberOf _ - * @since 0.1.0 - * @category Collection - * @param {Array|Object} collection The collection to iterate over. - * @param {Function} [predicate=_.identity] The function invoked per iteration. - * @returns {Array} Returns the new filtered array. - * @see _.filter - * @example - * - * var users = [ - * { 'user': 'barney', 'age': 36, 'active': false }, - * { 'user': 'fred', 'age': 40, 'active': true } - * ]; - * - * _.reject(users, function(o) { return !o.active; }); - * // => objects for ['fred'] - * - * // The `_.matches` iteratee shorthand. - * _.reject(users, { 'age': 40, 'active': true }); - * // => objects for ['barney'] - * - * // The `_.matchesProperty` iteratee shorthand. - * _.reject(users, ['active', false]); - * // => objects for ['fred'] - * - * // The `_.property` iteratee shorthand. - * _.reject(users, 'active'); - * // => objects for ['barney'] - */ - function reject(collection, predicate) { - var func = isArray(collection) ? arrayFilter : baseFilter; - return func(collection, negate(getIteratee(predicate, 3))); - } - - /** - * Gets a random element from `collection`. - * - * @static - * @memberOf _ - * @since 2.0.0 - * @category Collection - * @param {Array|Object} collection The collection to sample. - * @returns {*} Returns the random element. - * @example - * - * _.sample([1, 2, 3, 4]); - * // => 2 - */ - function sample(collection) { - var func = isArray(collection) ? arraySample : baseSample; - return func(collection); - } - - /** - * Gets `n` random elements at unique keys from `collection` up to the - * size of `collection`. - * - * @static - * @memberOf _ - * @since 4.0.0 - * @category Collection - * @param {Array|Object} collection The collection to sample. - * @param {number} [n=1] The number of elements to sample. - * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`. - * @returns {Array} Returns the random elements. - * @example - * - * _.sampleSize([1, 2, 3], 2); - * // => [3, 1] - * - * _.sampleSize([1, 2, 3], 4); - * // => [2, 3, 1] - */ - function sampleSize(collection, n, guard) { - if ((guard ? isIterateeCall(collection, n, guard) : n === undefined)) { - n = 1; - } else { - n = toInteger(n); - } - var func = isArray(collection) ? arraySampleSize : baseSampleSize; - return func(collection, n); - } - - /** - * Creates an array of shuffled values, using a version of the - * [Fisher-Yates shuffle](https://en.wikipedia.org/wiki/Fisher-Yates_shuffle). - * - * @static - * @memberOf _ - * @since 0.1.0 - * @category Collection - * @param {Array|Object} collection The collection to shuffle. - * @returns {Array} Returns the new shuffled array. - * @example - * - * _.shuffle([1, 2, 3, 4]); - * // => [4, 1, 3, 2] - */ - function shuffle(collection) { - var func = isArray(collection) ? arrayShuffle : baseShuffle; - return func(collection); - } - - /** - * Gets the size of `collection` by returning its length for array-like - * values or the number of own enumerable string keyed properties for objects. - * - * @static - * @memberOf _ - * @since 0.1.0 - * @category Collection - * @param {Array|Object|string} collection The collection to inspect. - * @returns {number} Returns the collection size. - * @example - * - * _.size([1, 2, 3]); - * // => 3 - * - * _.size({ 'a': 1, 'b': 2 }); - * // => 2 - * - * _.size('pebbles'); - * // => 7 - */ - function size(collection) { - if (collection == null) { - return 0; - } - if (isArrayLike(collection)) { - return isString(collection) ? stringSize(collection) : collection.length; - } - var tag = getTag(collection); - if (tag == mapTag || tag == setTag) { - return collection.size; - } - return baseKeys(collection).length; - } - - /** - * Checks if `predicate` returns truthy for **any** element of `collection`. - * Iteration is stopped once `predicate` returns truthy. The predicate is - * invoked with three arguments: (value, index|key, collection). - * - * @static - * @memberOf _ - * @since 0.1.0 - * @category Collection - * @param {Array|Object} collection The collection to iterate over. - * @param {Function} [predicate=_.identity] The function invoked per iteration. - * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`. - * @returns {boolean} Returns `true` if any element passes the predicate check, - * else `false`. - * @example - * - * _.some([null, 0, 'yes', false], Boolean); - * // => true - * - * var users = [ - * { 'user': 'barney', 'active': true }, - * { 'user': 'fred', 'active': false } - * ]; - * - * // The `_.matches` iteratee shorthand. - * _.some(users, { 'user': 'barney', 'active': false }); - * // => false - * - * // The `_.matchesProperty` iteratee shorthand. - * _.some(users, ['active', false]); - * // => true - * - * // The `_.property` iteratee shorthand. - * _.some(users, 'active'); - * // => true - */ - function some(collection, predicate, guard) { - var func = isArray(collection) ? arraySome : baseSome; - if (guard && isIterateeCall(collection, predicate, guard)) { - predicate = undefined; - } - return func(collection, getIteratee(predicate, 3)); - } - - /** - * Creates an array of elements, sorted in ascending order by the results of - * running each element in a collection thru each iteratee. This method - * performs a stable sort, that is, it preserves the original sort order of - * equal elements. The iteratees are invoked with one argument: (value). - * - * @static - * @memberOf _ - * @since 0.1.0 - * @category Collection - * @param {Array|Object} collection The collection to iterate over. - * @param {...(Function|Function[])} [iteratees=[_.identity]] - * The iteratees to sort by. - * @returns {Array} Returns the new sorted array. - * @example - * - * var users = [ - * { 'user': 'fred', 'age': 48 }, - * { 'user': 'barney', 'age': 36 }, - * { 'user': 'fred', 'age': 40 }, - * { 'user': 'barney', 'age': 34 } - * ]; - * - * _.sortBy(users, [function(o) { return o.user; }]); - * // => objects for [['barney', 36], ['barney', 34], ['fred', 48], ['fred', 40]] - * - * _.sortBy(users, ['user', 'age']); - * // => objects for [['barney', 34], ['barney', 36], ['fred', 40], ['fred', 48]] - */ - var sortBy = baseRest(function(collection, iteratees) { - if (collection == null) { - return []; - } - var length = iteratees.length; - if (length > 1 && isIterateeCall(collection, iteratees[0], iteratees[1])) { - iteratees = []; - } else if (length > 2 && isIterateeCall(iteratees[0], iteratees[1], iteratees[2])) { - iteratees = [iteratees[0]]; - } - return baseOrderBy(collection, baseFlatten(iteratees, 1), []); - }); - - /*------------------------------------------------------------------------*/ - - /** - * Gets the timestamp of the number of milliseconds that have elapsed since - * the Unix epoch (1 January 1970 00:00:00 UTC). - * - * @static - * @memberOf _ - * @since 2.4.0 - * @category Date - * @returns {number} Returns the timestamp. - * @example - * - * _.defer(function(stamp) { - * console.log(_.now() - stamp); - * }, _.now()); - * // => Logs the number of milliseconds it took for the deferred invocation. - */ - var now = ctxNow || function() { - return root.Date.now(); - }; - - /*------------------------------------------------------------------------*/ - - /** - * The opposite of `_.before`; this method creates a function that invokes - * `func` once it's called `n` or more times. - * - * @static - * @memberOf _ - * @since 0.1.0 - * @category Function - * @param {number} n The number of calls before `func` is invoked. - * @param {Function} func The function to restrict. - * @returns {Function} Returns the new restricted function. - * @example - * - * var saves = ['profile', 'settings']; - * - * var done = _.after(saves.length, function() { - * console.log('done saving!'); - * }); - * - * _.forEach(saves, function(type) { - * asyncSave({ 'type': type, 'complete': done }); - * }); - * // => Logs 'done saving!' after the two async saves have completed. - */ - function after(n, func) { - if (typeof func != 'function') { - throw new TypeError(FUNC_ERROR_TEXT); - } - n = toInteger(n); - return function() { - if (--n < 1) { - return func.apply(this, arguments); - } - }; - } - - /** - * Creates a function that invokes `func`, with up to `n` arguments, - * ignoring any additional arguments. - * - * @static - * @memberOf _ - * @since 3.0.0 - * @category Function - * @param {Function} func The function to cap arguments for. - * @param {number} [n=func.length] The arity cap. - * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`. - * @returns {Function} Returns the new capped function. - * @example - * - * _.map(['6', '8', '10'], _.ary(parseInt, 1)); - * // => [6, 8, 10] - */ - function ary(func, n, guard) { - n = guard ? undefined : n; - n = (func && n == null) ? func.length : n; - return createWrap(func, WRAP_ARY_FLAG, undefined, undefined, undefined, undefined, n); - } - - /** - * Creates a function that invokes `func`, with the `this` binding and arguments - * of the created function, while it's called less than `n` times. Subsequent - * calls to the created function return the result of the last `func` invocation. - * - * @static - * @memberOf _ - * @since 3.0.0 - * @category Function - * @param {number} n The number of calls at which `func` is no longer invoked. - * @param {Function} func The function to restrict. - * @returns {Function} Returns the new restricted function. - * @example - * - * jQuery(element).on('click', _.before(5, addContactToList)); - * // => Allows adding up to 4 contacts to the list. - */ - function before(n, func) { - var result; - if (typeof func != 'function') { - throw new TypeError(FUNC_ERROR_TEXT); - } - n = toInteger(n); - return function() { - if (--n > 0) { - result = func.apply(this, arguments); - } - if (n <= 1) { - func = undefined; - } - return result; - }; - } - - /** - * Creates a function that invokes `func` with the `this` binding of `thisArg` - * and `partials` prepended to the arguments it receives. - * - * The `_.bind.placeholder` value, which defaults to `_` in monolithic builds, - * may be used as a placeholder for partially applied arguments. - * - * **Note:** Unlike native `Function#bind`, this method doesn't set the "length" - * property of bound functions. - * - * @static - * @memberOf _ - * @since 0.1.0 - * @category Function - * @param {Function} func The function to bind. - * @param {*} thisArg The `this` binding of `func`. - * @param {...*} [partials] The arguments to be partially applied. - * @returns {Function} Returns the new bound function. - * @example - * - * function greet(greeting, punctuation) { - * return greeting + ' ' + this.user + punctuation; - * } - * - * var object = { 'user': 'fred' }; - * - * var bound = _.bind(greet, object, 'hi'); - * bound('!'); - * // => 'hi fred!' - * - * // Bound with placeholders. - * var bound = _.bind(greet, object, _, '!'); - * bound('hi'); - * // => 'hi fred!' - */ - var bind = baseRest(function(func, thisArg, partials) { - var bitmask = WRAP_BIND_FLAG; - if (partials.length) { - var holders = replaceHolders(partials, getHolder(bind)); - bitmask |= WRAP_PARTIAL_FLAG; - } - return createWrap(func, bitmask, thisArg, partials, holders); - }); - - /** - * Creates a function that invokes the method at `object[key]` with `partials` - * prepended to the arguments it receives. - * - * This method differs from `_.bind` by allowing bound functions to reference - * methods that may be redefined or don't yet exist. See - * [Peter Michaux's article](http://peter.michaux.ca/articles/lazy-function-definition-pattern) - * for more details. - * - * The `_.bindKey.placeholder` value, which defaults to `_` in monolithic - * builds, may be used as a placeholder for partially applied arguments. - * - * @static - * @memberOf _ - * @since 0.10.0 - * @category Function - * @param {Object} object The object to invoke the method on. - * @param {string} key The key of the method. - * @param {...*} [partials] The arguments to be partially applied. - * @returns {Function} Returns the new bound function. - * @example - * - * var object = { - * 'user': 'fred', - * 'greet': function(greeting, punctuation) { - * return greeting + ' ' + this.user + punctuation; - * } - * }; - * - * var bound = _.bindKey(object, 'greet', 'hi'); - * bound('!'); - * // => 'hi fred!' - * - * object.greet = function(greeting, punctuation) { - * return greeting + 'ya ' + this.user + punctuation; - * }; - * - * bound('!'); - * // => 'hiya fred!' - * - * // Bound with placeholders. - * var bound = _.bindKey(object, 'greet', _, '!'); - * bound('hi'); - * // => 'hiya fred!' - */ - var bindKey = baseRest(function(object, key, partials) { - var bitmask = WRAP_BIND_FLAG | WRAP_BIND_KEY_FLAG; - if (partials.length) { - var holders = replaceHolders(partials, getHolder(bindKey)); - bitmask |= WRAP_PARTIAL_FLAG; - } - return createWrap(key, bitmask, object, partials, holders); - }); - - /** - * Creates a function that accepts arguments of `func` and either invokes - * `func` returning its result, if at least `arity` number of arguments have - * been provided, or returns a function that accepts the remaining `func` - * arguments, and so on. The arity of `func` may be specified if `func.length` - * is not sufficient. - * - * The `_.curry.placeholder` value, which defaults to `_` in monolithic builds, - * may be used as a placeholder for provided arguments. - * - * **Note:** This method doesn't set the "length" property of curried functions. - * - * @static - * @memberOf _ - * @since 2.0.0 - * @category Function - * @param {Function} func The function to curry. - * @param {number} [arity=func.length] The arity of `func`. - * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`. - * @returns {Function} Returns the new curried function. - * @example - * - * var abc = function(a, b, c) { - * return [a, b, c]; - * }; - * - * var curried = _.curry(abc); - * - * curried(1)(2)(3); - * // => [1, 2, 3] - * - * curried(1, 2)(3); - * // => [1, 2, 3] - * - * curried(1, 2, 3); - * // => [1, 2, 3] - * - * // Curried with placeholders. - * curried(1)(_, 3)(2); - * // => [1, 2, 3] - */ - function curry(func, arity, guard) { - arity = guard ? undefined : arity; - var result = createWrap(func, WRAP_CURRY_FLAG, undefined, undefined, undefined, undefined, undefined, arity); - result.placeholder = curry.placeholder; - return result; - } - - /** - * This method is like `_.curry` except that arguments are applied to `func` - * in the manner of `_.partialRight` instead of `_.partial`. - * - * The `_.curryRight.placeholder` value, which defaults to `_` in monolithic - * builds, may be used as a placeholder for provided arguments. - * - * **Note:** This method doesn't set the "length" property of curried functions. - * - * @static - * @memberOf _ - * @since 3.0.0 - * @category Function - * @param {Function} func The function to curry. - * @param {number} [arity=func.length] The arity of `func`. - * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`. - * @returns {Function} Returns the new curried function. - * @example - * - * var abc = function(a, b, c) { - * return [a, b, c]; - * }; - * - * var curried = _.curryRight(abc); - * - * curried(3)(2)(1); - * // => [1, 2, 3] - * - * curried(2, 3)(1); - * // => [1, 2, 3] - * - * curried(1, 2, 3); - * // => [1, 2, 3] - * - * // Curried with placeholders. - * curried(3)(1, _)(2); - * // => [1, 2, 3] - */ - function curryRight(func, arity, guard) { - arity = guard ? undefined : arity; - var result = createWrap(func, WRAP_CURRY_RIGHT_FLAG, undefined, undefined, undefined, undefined, undefined, arity); - result.placeholder = curryRight.placeholder; - return result; - } - - /** - * Creates a debounced function that delays invoking `func` until after `wait` - * milliseconds have elapsed since the last time the debounced function was - * invoked. The debounced function comes with a `cancel` method to cancel - * delayed `func` invocations and a `flush` method to immediately invoke them. - * Provide `options` to indicate whether `func` should be invoked on the - * leading and/or trailing edge of the `wait` timeout. The `func` is invoked - * with the last arguments provided to the debounced function. Subsequent - * calls to the debounced function return the result of the last `func` - * invocation. - * - * **Note:** If `leading` and `trailing` options are `true`, `func` is - * invoked on the trailing edge of the timeout only if the debounced function - * is invoked more than once during the `wait` timeout. - * - * If `wait` is `0` and `leading` is `false`, `func` invocation is deferred - * until to the next tick, similar to `setTimeout` with a timeout of `0`. - * - * See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/) - * for details over the differences between `_.debounce` and `_.throttle`. - * - * @static - * @memberOf _ - * @since 0.1.0 - * @category Function - * @param {Function} func The function to debounce. - * @param {number} [wait=0] The number of milliseconds to delay. - * @param {Object} [options={}] The options object. - * @param {boolean} [options.leading=false] - * Specify invoking on the leading edge of the timeout. - * @param {number} [options.maxWait] - * The maximum time `func` is allowed to be delayed before it's invoked. - * @param {boolean} [options.trailing=true] - * Specify invoking on the trailing edge of the timeout. - * @returns {Function} Returns the new debounced function. - * @example - * - * // Avoid costly calculations while the window size is in flux. - * jQuery(window).on('resize', _.debounce(calculateLayout, 150)); - * - * // Invoke `sendMail` when clicked, debouncing subsequent calls. - * jQuery(element).on('click', _.debounce(sendMail, 300, { - * 'leading': true, - * 'trailing': false - * })); - * - * // Ensure `batchLog` is invoked once after 1 second of debounced calls. - * var debounced = _.debounce(batchLog, 250, { 'maxWait': 1000 }); - * var source = new EventSource('/stream'); - * jQuery(source).on('message', debounced); - * - * // Cancel the trailing debounced invocation. - * jQuery(window).on('popstate', debounced.cancel); - */ - function debounce(func, wait, options) { - var lastArgs, - lastThis, - maxWait, - result, - timerId, - lastCallTime, - lastInvokeTime = 0, - leading = false, - maxing = false, - trailing = true; - - if (typeof func != 'function') { - throw new TypeError(FUNC_ERROR_TEXT); - } - wait = toNumber(wait) || 0; - if (isObject(options)) { - leading = !!options.leading; - maxing = 'maxWait' in options; - maxWait = maxing ? nativeMax(toNumber(options.maxWait) || 0, wait) : maxWait; - trailing = 'trailing' in options ? !!options.trailing : trailing; - } - - function invokeFunc(time) { - var args = lastArgs, - thisArg = lastThis; - - lastArgs = lastThis = undefined; - lastInvokeTime = time; - result = func.apply(thisArg, args); - return result; - } - - function leadingEdge(time) { - // Reset any `maxWait` timer. - lastInvokeTime = time; - // Start the timer for the trailing edge. - timerId = setTimeout(timerExpired, wait); - // Invoke the leading edge. - return leading ? invokeFunc(time) : result; - } - - function remainingWait(time) { - var timeSinceLastCall = time - lastCallTime, - timeSinceLastInvoke = time - lastInvokeTime, - result = wait - timeSinceLastCall; - - return maxing ? nativeMin(result, maxWait - timeSinceLastInvoke) : result; - } - - function shouldInvoke(time) { - var timeSinceLastCall = time - lastCallTime, - timeSinceLastInvoke = time - lastInvokeTime; - - // Either this is the first call, activity has stopped and we're at the - // trailing edge, the system time has gone backwards and we're treating - // it as the trailing edge, or we've hit the `maxWait` limit. - return (lastCallTime === undefined || (timeSinceLastCall >= wait) || - (timeSinceLastCall < 0) || (maxing && timeSinceLastInvoke >= maxWait)); - } - - function timerExpired() { - var time = now(); - if (shouldInvoke(time)) { - return trailingEdge(time); - } - // Restart the timer. - timerId = setTimeout(timerExpired, remainingWait(time)); - } - - function trailingEdge(time) { - timerId = undefined; - - // Only invoke if we have `lastArgs` which means `func` has been - // debounced at least once. - if (trailing && lastArgs) { - return invokeFunc(time); - } - lastArgs = lastThis = undefined; - return result; - } - - function cancel() { - if (timerId !== undefined) { - clearTimeout(timerId); - } - lastInvokeTime = 0; - lastArgs = lastCallTime = lastThis = timerId = undefined; - } - - function flush() { - return timerId === undefined ? result : trailingEdge(now()); - } - - function debounced() { - var time = now(), - isInvoking = shouldInvoke(time); - - lastArgs = arguments; - lastThis = this; - lastCallTime = time; - - if (isInvoking) { - if (timerId === undefined) { - return leadingEdge(lastCallTime); - } - if (maxing) { - // Handle invocations in a tight loop. - timerId = setTimeout(timerExpired, wait); - return invokeFunc(lastCallTime); - } - } - if (timerId === undefined) { - timerId = setTimeout(timerExpired, wait); - } - return result; - } - debounced.cancel = cancel; - debounced.flush = flush; - return debounced; - } - - /** - * Defers invoking the `func` until the current call stack has cleared. Any - * additional arguments are provided to `func` when it's invoked. - * - * @static - * @memberOf _ - * @since 0.1.0 - * @category Function - * @param {Function} func The function to defer. - * @param {...*} [args] The arguments to invoke `func` with. - * @returns {number} Returns the timer id. - * @example - * - * _.defer(function(text) { - * console.log(text); - * }, 'deferred'); - * // => Logs 'deferred' after one millisecond. - */ - var defer = baseRest(function(func, args) { - return baseDelay(func, 1, args); - }); - - /** - * Invokes `func` after `wait` milliseconds. Any additional arguments are - * provided to `func` when it's invoked. - * - * @static - * @memberOf _ - * @since 0.1.0 - * @category Function - * @param {Function} func The function to delay. - * @param {number} wait The number of milliseconds to delay invocation. - * @param {...*} [args] The arguments to invoke `func` with. - * @returns {number} Returns the timer id. - * @example - * - * _.delay(function(text) { - * console.log(text); - * }, 1000, 'later'); - * // => Logs 'later' after one second. - */ - var delay = baseRest(function(func, wait, args) { - return baseDelay(func, toNumber(wait) || 0, args); - }); - - /** - * Creates a function that invokes `func` with arguments reversed. - * - * @static - * @memberOf _ - * @since 4.0.0 - * @category Function - * @param {Function} func The function to flip arguments for. - * @returns {Function} Returns the new flipped function. - * @example - * - * var flipped = _.flip(function() { - * return _.toArray(arguments); - * }); - * - * flipped('a', 'b', 'c', 'd'); - * // => ['d', 'c', 'b', 'a'] - */ - function flip(func) { - return createWrap(func, WRAP_FLIP_FLAG); - } - - /** - * Creates a function that memoizes the result of `func`. If `resolver` is - * provided, it determines the cache key for storing the result based on the - * arguments provided to the memoized function. By default, the first argument - * provided to the memoized function is used as the map cache key. The `func` - * is invoked with the `this` binding of the memoized function. - * - * **Note:** The cache is exposed as the `cache` property on the memoized - * function. Its creation may be customized by replacing the `_.memoize.Cache` - * constructor with one whose instances implement the - * [`Map`](http://ecma-international.org/ecma-262/7.0/#sec-properties-of-the-map-prototype-object) - * method interface of `clear`, `delete`, `get`, `has`, and `set`. - * - * @static - * @memberOf _ - * @since 0.1.0 - * @category Function - * @param {Function} func The function to have its output memoized. - * @param {Function} [resolver] The function to resolve the cache key. - * @returns {Function} Returns the new memoized function. - * @example - * - * var object = { 'a': 1, 'b': 2 }; - * var other = { 'c': 3, 'd': 4 }; - * - * var values = _.memoize(_.values); - * values(object); - * // => [1, 2] - * - * values(other); - * // => [3, 4] - * - * object.a = 2; - * values(object); - * // => [1, 2] - * - * // Modify the result cache. - * values.cache.set(object, ['a', 'b']); - * values(object); - * // => ['a', 'b'] - * - * // Replace `_.memoize.Cache`. - * _.memoize.Cache = WeakMap; - */ - function memoize(func, resolver) { - if (typeof func != 'function' || (resolver != null && typeof resolver != 'function')) { - throw new TypeError(FUNC_ERROR_TEXT); - } - var memoized = function() { - var args = arguments, - key = resolver ? resolver.apply(this, args) : args[0], - cache = memoized.cache; - - if (cache.has(key)) { - return cache.get(key); - } - var result = func.apply(this, args); - memoized.cache = cache.set(key, result) || cache; - return result; - }; - memoized.cache = new (memoize.Cache || MapCache); - return memoized; - } - - // Expose `MapCache`. - memoize.Cache = MapCache; - - /** - * Creates a function that negates the result of the predicate `func`. The - * `func` predicate is invoked with the `this` binding and arguments of the - * created function. - * - * @static - * @memberOf _ - * @since 3.0.0 - * @category Function - * @param {Function} predicate The predicate to negate. - * @returns {Function} Returns the new negated function. - * @example - * - * function isEven(n) { - * return n % 2 == 0; - * } - * - * _.filter([1, 2, 3, 4, 5, 6], _.negate(isEven)); - * // => [1, 3, 5] - */ - function negate(predicate) { - if (typeof predicate != 'function') { - throw new TypeError(FUNC_ERROR_TEXT); - } - return function() { - var args = arguments; - switch (args.length) { - case 0: return !predicate.call(this); - case 1: return !predicate.call(this, args[0]); - case 2: return !predicate.call(this, args[0], args[1]); - case 3: return !predicate.call(this, args[0], args[1], args[2]); - } - return !predicate.apply(this, args); - }; - } - - /** - * Creates a function that is restricted to invoking `func` once. Repeat calls - * to the function return the value of the first invocation. The `func` is - * invoked with the `this` binding and arguments of the created function. - * - * @static - * @memberOf _ - * @since 0.1.0 - * @category Function - * @param {Function} func The function to restrict. - * @returns {Function} Returns the new restricted function. - * @example - * - * var initialize = _.once(createApplication); - * initialize(); - * initialize(); - * // => `createApplication` is invoked once - */ - function once(func) { - return before(2, func); - } - - /** - * Creates a function that invokes `func` with its arguments transformed. - * - * @static - * @since 4.0.0 - * @memberOf _ - * @category Function - * @param {Function} func The function to wrap. - * @param {...(Function|Function[])} [transforms=[_.identity]] - * The argument transforms. - * @returns {Function} Returns the new function. - * @example - * - * function doubled(n) { - * return n * 2; - * } - * - * function square(n) { - * return n * n; - * } - * - * var func = _.overArgs(function(x, y) { - * return [x, y]; - * }, [square, doubled]); - * - * func(9, 3); - * // => [81, 6] - * - * func(10, 5); - * // => [100, 10] - */ - var overArgs = castRest(function(func, transforms) { - transforms = (transforms.length == 1 && isArray(transforms[0])) - ? arrayMap(transforms[0], baseUnary(getIteratee())) - : arrayMap(baseFlatten(transforms, 1), baseUnary(getIteratee())); - - var funcsLength = transforms.length; - return baseRest(function(args) { - var index = -1, - length = nativeMin(args.length, funcsLength); - - while (++index < length) { - args[index] = transforms[index].call(this, args[index]); - } - return apply(func, this, args); - }); - }); - - /** - * Creates a function that invokes `func` with `partials` prepended to the - * arguments it receives. This method is like `_.bind` except it does **not** - * alter the `this` binding. - * - * The `_.partial.placeholder` value, which defaults to `_` in monolithic - * builds, may be used as a placeholder for partially applied arguments. - * - * **Note:** This method doesn't set the "length" property of partially - * applied functions. - * - * @static - * @memberOf _ - * @since 0.2.0 - * @category Function - * @param {Function} func The function to partially apply arguments to. - * @param {...*} [partials] The arguments to be partially applied. - * @returns {Function} Returns the new partially applied function. - * @example - * - * function greet(greeting, name) { - * return greeting + ' ' + name; - * } - * - * var sayHelloTo = _.partial(greet, 'hello'); - * sayHelloTo('fred'); - * // => 'hello fred' - * - * // Partially applied with placeholders. - * var greetFred = _.partial(greet, _, 'fred'); - * greetFred('hi'); - * // => 'hi fred' - */ - var partial = baseRest(function(func, partials) { - var holders = replaceHolders(partials, getHolder(partial)); - return createWrap(func, WRAP_PARTIAL_FLAG, undefined, partials, holders); - }); - - /** - * This method is like `_.partial` except that partially applied arguments - * are appended to the arguments it receives. - * - * The `_.partialRight.placeholder` value, which defaults to `_` in monolithic - * builds, may be used as a placeholder for partially applied arguments. - * - * **Note:** This method doesn't set the "length" property of partially - * applied functions. - * - * @static - * @memberOf _ - * @since 1.0.0 - * @category Function - * @param {Function} func The function to partially apply arguments to. - * @param {...*} [partials] The arguments to be partially applied. - * @returns {Function} Returns the new partially applied function. - * @example - * - * function greet(greeting, name) { - * return greeting + ' ' + name; - * } - * - * var greetFred = _.partialRight(greet, 'fred'); - * greetFred('hi'); - * // => 'hi fred' - * - * // Partially applied with placeholders. - * var sayHelloTo = _.partialRight(greet, 'hello', _); - * sayHelloTo('fred'); - * // => 'hello fred' - */ - var partialRight = baseRest(function(func, partials) { - var holders = replaceHolders(partials, getHolder(partialRight)); - return createWrap(func, WRAP_PARTIAL_RIGHT_FLAG, undefined, partials, holders); - }); - - /** - * Creates a function that invokes `func` with arguments arranged according - * to the specified `indexes` where the argument value at the first index is - * provided as the first argument, the argument value at the second index is - * provided as the second argument, and so on. - * - * @static - * @memberOf _ - * @since 3.0.0 - * @category Function - * @param {Function} func The function to rearrange arguments for. - * @param {...(number|number[])} indexes The arranged argument indexes. - * @returns {Function} Returns the new function. - * @example - * - * var rearged = _.rearg(function(a, b, c) { - * return [a, b, c]; - * }, [2, 0, 1]); - * - * rearged('b', 'c', 'a') - * // => ['a', 'b', 'c'] - */ - var rearg = flatRest(function(func, indexes) { - return createWrap(func, WRAP_REARG_FLAG, undefined, undefined, undefined, indexes); - }); - - /** - * Creates a function that invokes `func` with the `this` binding of the - * created function and arguments from `start` and beyond provided as - * an array. - * - * **Note:** This method is based on the - * [rest parameter](https://mdn.io/rest_parameters). - * - * @static - * @memberOf _ - * @since 4.0.0 - * @category Function - * @param {Function} func The function to apply a rest parameter to. - * @param {number} [start=func.length-1] The start position of the rest parameter. - * @returns {Function} Returns the new function. - * @example - * - * var say = _.rest(function(what, names) { - * return what + ' ' + _.initial(names).join(', ') + - * (_.size(names) > 1 ? ', & ' : '') + _.last(names); - * }); - * - * say('hello', 'fred', 'barney', 'pebbles'); - * // => 'hello fred, barney, & pebbles' - */ - function rest(func, start) { - if (typeof func != 'function') { - throw new TypeError(FUNC_ERROR_TEXT); - } - start = start === undefined ? start : toInteger(start); - return baseRest(func, start); - } - - /** - * Creates a function that invokes `func` with the `this` binding of the - * create function and an array of arguments much like - * [`Function#apply`](http://www.ecma-international.org/ecma-262/7.0/#sec-function.prototype.apply). - * - * **Note:** This method is based on the - * [spread operator](https://mdn.io/spread_operator). - * - * @static - * @memberOf _ - * @since 3.2.0 - * @category Function - * @param {Function} func The function to spread arguments over. - * @param {number} [start=0] The start position of the spread. - * @returns {Function} Returns the new function. - * @example - * - * var say = _.spread(function(who, what) { - * return who + ' says ' + what; - * }); - * - * say(['fred', 'hello']); - * // => 'fred says hello' - * - * var numbers = Promise.all([ - * Promise.resolve(40), - * Promise.resolve(36) - * ]); - * - * numbers.then(_.spread(function(x, y) { - * return x + y; - * })); - * // => a Promise of 76 - */ - function spread(func, start) { - if (typeof func != 'function') { - throw new TypeError(FUNC_ERROR_TEXT); - } - start = start == null ? 0 : nativeMax(toInteger(start), 0); - return baseRest(function(args) { - var array = args[start], - otherArgs = castSlice(args, 0, start); - - if (array) { - arrayPush(otherArgs, array); - } - return apply(func, this, otherArgs); - }); - } - - /** - * Creates a throttled function that only invokes `func` at most once per - * every `wait` milliseconds. The throttled function comes with a `cancel` - * method to cancel delayed `func` invocations and a `flush` method to - * immediately invoke them. Provide `options` to indicate whether `func` - * should be invoked on the leading and/or trailing edge of the `wait` - * timeout. The `func` is invoked with the last arguments provided to the - * throttled function. Subsequent calls to the throttled function return the - * result of the last `func` invocation. - * - * **Note:** If `leading` and `trailing` options are `true`, `func` is - * invoked on the trailing edge of the timeout only if the throttled function - * is invoked more than once during the `wait` timeout. - * - * If `wait` is `0` and `leading` is `false`, `func` invocation is deferred - * until to the next tick, similar to `setTimeout` with a timeout of `0`. - * - * See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/) - * for details over the differences between `_.throttle` and `_.debounce`. - * - * @static - * @memberOf _ - * @since 0.1.0 - * @category Function - * @param {Function} func The function to throttle. - * @param {number} [wait=0] The number of milliseconds to throttle invocations to. - * @param {Object} [options={}] The options object. - * @param {boolean} [options.leading=true] - * Specify invoking on the leading edge of the timeout. - * @param {boolean} [options.trailing=true] - * Specify invoking on the trailing edge of the timeout. - * @returns {Function} Returns the new throttled function. - * @example - * - * // Avoid excessively updating the position while scrolling. - * jQuery(window).on('scroll', _.throttle(updatePosition, 100)); - * - * // Invoke `renewToken` when the click event is fired, but not more than once every 5 minutes. - * var throttled = _.throttle(renewToken, 300000, { 'trailing': false }); - * jQuery(element).on('click', throttled); - * - * // Cancel the trailing throttled invocation. - * jQuery(window).on('popstate', throttled.cancel); - */ - function throttle(func, wait, options) { - var leading = true, - trailing = true; - - if (typeof func != 'function') { - throw new TypeError(FUNC_ERROR_TEXT); - } - if (isObject(options)) { - leading = 'leading' in options ? !!options.leading : leading; - trailing = 'trailing' in options ? !!options.trailing : trailing; - } - return debounce(func, wait, { - 'leading': leading, - 'maxWait': wait, - 'trailing': trailing - }); - } - - /** - * Creates a function that accepts up to one argument, ignoring any - * additional arguments. - * - * @static - * @memberOf _ - * @since 4.0.0 - * @category Function - * @param {Function} func The function to cap arguments for. - * @returns {Function} Returns the new capped function. - * @example - * - * _.map(['6', '8', '10'], _.unary(parseInt)); - * // => [6, 8, 10] - */ - function unary(func) { - return ary(func, 1); - } - - /** - * Creates a function that provides `value` to `wrapper` as its first - * argument. Any additional arguments provided to the function are appended - * to those provided to the `wrapper`. The wrapper is invoked with the `this` - * binding of the created function. - * - * @static - * @memberOf _ - * @since 0.1.0 - * @category Function - * @param {*} value The value to wrap. - * @param {Function} [wrapper=identity] The wrapper function. - * @returns {Function} Returns the new function. - * @example - * - * var p = _.wrap(_.escape, function(func, text) { - * return '

' + func(text) + '

'; - * }); - * - * p('fred, barney, & pebbles'); - * // => '

fred, barney, & pebbles

' - */ - function wrap(value, wrapper) { - return partial(castFunction(wrapper), value); - } - - /*------------------------------------------------------------------------*/ - - /** - * Casts `value` as an array if it's not one. - * - * @static - * @memberOf _ - * @since 4.4.0 - * @category Lang - * @param {*} value The value to inspect. - * @returns {Array} Returns the cast array. - * @example - * - * _.castArray(1); - * // => [1] - * - * _.castArray({ 'a': 1 }); - * // => [{ 'a': 1 }] - * - * _.castArray('abc'); - * // => ['abc'] - * - * _.castArray(null); - * // => [null] - * - * _.castArray(undefined); - * // => [undefined] - * - * _.castArray(); - * // => [] - * - * var array = [1, 2, 3]; - * console.log(_.castArray(array) === array); - * // => true - */ - function castArray() { - if (!arguments.length) { - return []; - } - var value = arguments[0]; - return isArray(value) ? value : [value]; - } - - /** - * Creates a shallow clone of `value`. - * - * **Note:** This method is loosely based on the - * [structured clone algorithm](https://mdn.io/Structured_clone_algorithm) - * and supports cloning arrays, array buffers, booleans, date objects, maps, - * numbers, `Object` objects, regexes, sets, strings, symbols, and typed - * arrays. The own enumerable properties of `arguments` objects are cloned - * as plain objects. An empty object is returned for uncloneable values such - * as error objects, functions, DOM nodes, and WeakMaps. - * - * @static - * @memberOf _ - * @since 0.1.0 - * @category Lang - * @param {*} value The value to clone. - * @returns {*} Returns the cloned value. - * @see _.cloneDeep - * @example - * - * var objects = [{ 'a': 1 }, { 'b': 2 }]; - * - * var shallow = _.clone(objects); - * console.log(shallow[0] === objects[0]); - * // => true - */ - function clone(value) { - return baseClone(value, CLONE_SYMBOLS_FLAG); - } - - /** - * This method is like `_.clone` except that it accepts `customizer` which - * is invoked to produce the cloned value. If `customizer` returns `undefined`, - * cloning is handled by the method instead. The `customizer` is invoked with - * up to four arguments; (value [, index|key, object, stack]). - * - * @static - * @memberOf _ - * @since 4.0.0 - * @category Lang - * @param {*} value The value to clone. - * @param {Function} [customizer] The function to customize cloning. - * @returns {*} Returns the cloned value. - * @see _.cloneDeepWith - * @example - * - * function customizer(value) { - * if (_.isElement(value)) { - * return value.cloneNode(false); - * } - * } - * - * var el = _.cloneWith(document.body, customizer); - * - * console.log(el === document.body); - * // => false - * console.log(el.nodeName); - * // => 'BODY' - * console.log(el.childNodes.length); - * // => 0 - */ - function cloneWith(value, customizer) { - customizer = typeof customizer == 'function' ? customizer : undefined; - return baseClone(value, CLONE_SYMBOLS_FLAG, customizer); - } - - /** - * This method is like `_.clone` except that it recursively clones `value`. - * - * @static - * @memberOf _ - * @since 1.0.0 - * @category Lang - * @param {*} value The value to recursively clone. - * @returns {*} Returns the deep cloned value. - * @see _.clone - * @example - * - * var objects = [{ 'a': 1 }, { 'b': 2 }]; - * - * var deep = _.cloneDeep(objects); - * console.log(deep[0] === objects[0]); - * // => false - */ - function cloneDeep(value) { - return baseClone(value, CLONE_DEEP_FLAG | CLONE_SYMBOLS_FLAG); - } - - /** - * This method is like `_.cloneWith` except that it recursively clones `value`. - * - * @static - * @memberOf _ - * @since 4.0.0 - * @category Lang - * @param {*} value The value to recursively clone. - * @param {Function} [customizer] The function to customize cloning. - * @returns {*} Returns the deep cloned value. - * @see _.cloneWith - * @example - * - * function customizer(value) { - * if (_.isElement(value)) { - * return value.cloneNode(true); - * } - * } - * - * var el = _.cloneDeepWith(document.body, customizer); - * - * console.log(el === document.body); - * // => false - * console.log(el.nodeName); - * // => 'BODY' - * console.log(el.childNodes.length); - * // => 20 - */ - function cloneDeepWith(value, customizer) { - customizer = typeof customizer == 'function' ? customizer : undefined; - return baseClone(value, CLONE_DEEP_FLAG | CLONE_SYMBOLS_FLAG, customizer); - } - - /** - * Checks if `object` conforms to `source` by invoking the predicate - * properties of `source` with the corresponding property values of `object`. - * - * **Note:** This method is equivalent to `_.conforms` when `source` is - * partially applied. - * - * @static - * @memberOf _ - * @since 4.14.0 - * @category Lang - * @param {Object} object The object to inspect. - * @param {Object} source The object of property predicates to conform to. - * @returns {boolean} Returns `true` if `object` conforms, else `false`. - * @example - * - * var object = { 'a': 1, 'b': 2 }; - * - * _.conformsTo(object, { 'b': function(n) { return n > 1; } }); - * // => true - * - * _.conformsTo(object, { 'b': function(n) { return n > 2; } }); - * // => false - */ - function conformsTo(object, source) { - return source == null || baseConformsTo(object, source, keys(source)); - } - - /** - * Performs a - * [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero) - * comparison between two values to determine if they are equivalent. - * - * @static - * @memberOf _ - * @since 4.0.0 - * @category Lang - * @param {*} value The value to compare. - * @param {*} other The other value to compare. - * @returns {boolean} Returns `true` if the values are equivalent, else `false`. - * @example - * - * var object = { 'a': 1 }; - * var other = { 'a': 1 }; - * - * _.eq(object, object); - * // => true - * - * _.eq(object, other); - * // => false - * - * _.eq('a', 'a'); - * // => true - * - * _.eq('a', Object('a')); - * // => false - * - * _.eq(NaN, NaN); - * // => true - */ - function eq(value, other) { - return value === other || (value !== value && other !== other); - } - - /** - * Checks if `value` is greater than `other`. - * - * @static - * @memberOf _ - * @since 3.9.0 - * @category Lang - * @param {*} value The value to compare. - * @param {*} other The other value to compare. - * @returns {boolean} Returns `true` if `value` is greater than `other`, - * else `false`. - * @see _.lt - * @example - * - * _.gt(3, 1); - * // => true - * - * _.gt(3, 3); - * // => false - * - * _.gt(1, 3); - * // => false - */ - var gt = createRelationalOperation(baseGt); - - /** - * Checks if `value` is greater than or equal to `other`. - * - * @static - * @memberOf _ - * @since 3.9.0 - * @category Lang - * @param {*} value The value to compare. - * @param {*} other The other value to compare. - * @returns {boolean} Returns `true` if `value` is greater than or equal to - * `other`, else `false`. - * @see _.lte - * @example - * - * _.gte(3, 1); - * // => true - * - * _.gte(3, 3); - * // => true - * - * _.gte(1, 3); - * // => false - */ - var gte = createRelationalOperation(function(value, other) { - return value >= other; - }); - - /** - * Checks if `value` is likely an `arguments` object. - * - * @static - * @memberOf _ - * @since 0.1.0 - * @category Lang - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if `value` is an `arguments` object, - * else `false`. - * @example - * - * _.isArguments(function() { return arguments; }()); - * // => true - * - * _.isArguments([1, 2, 3]); - * // => false - */ - var isArguments = baseIsArguments(function() { return arguments; }()) ? baseIsArguments : function(value) { - return isObjectLike(value) && hasOwnProperty.call(value, 'callee') && - !propertyIsEnumerable.call(value, 'callee'); - }; - - /** - * Checks if `value` is classified as an `Array` object. - * - * @static - * @memberOf _ - * @since 0.1.0 - * @category Lang - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if `value` is an array, else `false`. - * @example - * - * _.isArray([1, 2, 3]); - * // => true - * - * _.isArray(document.body.children); - * // => false - * - * _.isArray('abc'); - * // => false - * - * _.isArray(_.noop); - * // => false - */ - var isArray = Array.isArray; - - /** - * Checks if `value` is classified as an `ArrayBuffer` object. - * - * @static - * @memberOf _ - * @since 4.3.0 - * @category Lang - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if `value` is an array buffer, else `false`. - * @example - * - * _.isArrayBuffer(new ArrayBuffer(2)); - * // => true - * - * _.isArrayBuffer(new Array(2)); - * // => false - */ - var isArrayBuffer = nodeIsArrayBuffer ? baseUnary(nodeIsArrayBuffer) : baseIsArrayBuffer; - - /** - * Checks if `value` is array-like. A value is considered array-like if it's - * not a function and has a `value.length` that's an integer greater than or - * equal to `0` and less than or equal to `Number.MAX_SAFE_INTEGER`. - * - * @static - * @memberOf _ - * @since 4.0.0 - * @category Lang - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if `value` is array-like, else `false`. - * @example - * - * _.isArrayLike([1, 2, 3]); - * // => true - * - * _.isArrayLike(document.body.children); - * // => true - * - * _.isArrayLike('abc'); - * // => true - * - * _.isArrayLike(_.noop); - * // => false - */ - function isArrayLike(value) { - return value != null && isLength(value.length) && !isFunction(value); - } - - /** - * This method is like `_.isArrayLike` except that it also checks if `value` - * is an object. - * - * @static - * @memberOf _ - * @since 4.0.0 - * @category Lang - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if `value` is an array-like object, - * else `false`. - * @example - * - * _.isArrayLikeObject([1, 2, 3]); - * // => true - * - * _.isArrayLikeObject(document.body.children); - * // => true - * - * _.isArrayLikeObject('abc'); - * // => false - * - * _.isArrayLikeObject(_.noop); - * // => false - */ - function isArrayLikeObject(value) { - return isObjectLike(value) && isArrayLike(value); - } - - /** - * Checks if `value` is classified as a boolean primitive or object. - * - * @static - * @memberOf _ - * @since 0.1.0 - * @category Lang - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if `value` is a boolean, else `false`. - * @example - * - * _.isBoolean(false); - * // => true - * - * _.isBoolean(null); - * // => false - */ - function isBoolean(value) { - return value === true || value === false || - (isObjectLike(value) && baseGetTag(value) == boolTag); - } - - /** - * Checks if `value` is a buffer. - * - * @static - * @memberOf _ - * @since 4.3.0 - * @category Lang - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if `value` is a buffer, else `false`. - * @example - * - * _.isBuffer(new Buffer(2)); - * // => true - * - * _.isBuffer(new Uint8Array(2)); - * // => false - */ - var isBuffer = nativeIsBuffer || stubFalse; - - /** - * Checks if `value` is classified as a `Date` object. - * - * @static - * @memberOf _ - * @since 0.1.0 - * @category Lang - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if `value` is a date object, else `false`. - * @example - * - * _.isDate(new Date); - * // => true - * - * _.isDate('Mon April 23 2012'); - * // => false - */ - var isDate = nodeIsDate ? baseUnary(nodeIsDate) : baseIsDate; - - /** - * Checks if `value` is likely a DOM element. - * - * @static - * @memberOf _ - * @since 0.1.0 - * @category Lang - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if `value` is a DOM element, else `false`. - * @example - * - * _.isElement(document.body); - * // => true - * - * _.isElement(''); - * // => false - */ - function isElement(value) { - return isObjectLike(value) && value.nodeType === 1 && !isPlainObject(value); - } - - /** - * Checks if `value` is an empty object, collection, map, or set. - * - * Objects are considered empty if they have no own enumerable string keyed - * properties. - * - * Array-like values such as `arguments` objects, arrays, buffers, strings, or - * jQuery-like collections are considered empty if they have a `length` of `0`. - * Similarly, maps and sets are considered empty if they have a `size` of `0`. - * - * @static - * @memberOf _ - * @since 0.1.0 - * @category Lang - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if `value` is empty, else `false`. - * @example - * - * _.isEmpty(null); - * // => true - * - * _.isEmpty(true); - * // => true - * - * _.isEmpty(1); - * // => true - * - * _.isEmpty([1, 2, 3]); - * // => false - * - * _.isEmpty({ 'a': 1 }); - * // => false - */ - function isEmpty(value) { - if (value == null) { - return true; - } - if (isArrayLike(value) && - (isArray(value) || typeof value == 'string' || typeof value.splice == 'function' || - isBuffer(value) || isTypedArray(value) || isArguments(value))) { - return !value.length; - } - var tag = getTag(value); - if (tag == mapTag || tag == setTag) { - return !value.size; - } - if (isPrototype(value)) { - return !baseKeys(value).length; - } - for (var key in value) { - if (hasOwnProperty.call(value, key)) { - return false; - } - } - return true; - } - - /** - * Performs a deep comparison between two values to determine if they are - * equivalent. - * - * **Note:** This method supports comparing arrays, array buffers, booleans, - * date objects, error objects, maps, numbers, `Object` objects, regexes, - * sets, strings, symbols, and typed arrays. `Object` objects are compared - * by their own, not inherited, enumerable properties. Functions and DOM - * nodes are compared by strict equality, i.e. `===`. - * - * @static - * @memberOf _ - * @since 0.1.0 - * @category Lang - * @param {*} value The value to compare. - * @param {*} other The other value to compare. - * @returns {boolean} Returns `true` if the values are equivalent, else `false`. - * @example - * - * var object = { 'a': 1 }; - * var other = { 'a': 1 }; - * - * _.isEqual(object, other); - * // => true - * - * object === other; - * // => false - */ - function isEqual(value, other) { - return baseIsEqual(value, other); - } - - /** - * This method is like `_.isEqual` except that it accepts `customizer` which - * is invoked to compare values. If `customizer` returns `undefined`, comparisons - * are handled by the method instead. The `customizer` is invoked with up to - * six arguments: (objValue, othValue [, index|key, object, other, stack]). - * - * @static - * @memberOf _ - * @since 4.0.0 - * @category Lang - * @param {*} value The value to compare. - * @param {*} other The other value to compare. - * @param {Function} [customizer] The function to customize comparisons. - * @returns {boolean} Returns `true` if the values are equivalent, else `false`. - * @example - * - * function isGreeting(value) { - * return /^h(?:i|ello)$/.test(value); - * } - * - * function customizer(objValue, othValue) { - * if (isGreeting(objValue) && isGreeting(othValue)) { - * return true; - * } - * } - * - * var array = ['hello', 'goodbye']; - * var other = ['hi', 'goodbye']; - * - * _.isEqualWith(array, other, customizer); - * // => true - */ - function isEqualWith(value, other, customizer) { - customizer = typeof customizer == 'function' ? customizer : undefined; - var result = customizer ? customizer(value, other) : undefined; - return result === undefined ? baseIsEqual(value, other, undefined, customizer) : !!result; - } - - /** - * Checks if `value` is an `Error`, `EvalError`, `RangeError`, `ReferenceError`, - * `SyntaxError`, `TypeError`, or `URIError` object. - * - * @static - * @memberOf _ - * @since 3.0.0 - * @category Lang - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if `value` is an error object, else `false`. - * @example - * - * _.isError(new Error); - * // => true - * - * _.isError(Error); - * // => false - */ - function isError(value) { - if (!isObjectLike(value)) { - return false; - } - var tag = baseGetTag(value); - return tag == errorTag || tag == domExcTag || - (typeof value.message == 'string' && typeof value.name == 'string' && !isPlainObject(value)); - } - - /** - * Checks if `value` is a finite primitive number. - * - * **Note:** This method is based on - * [`Number.isFinite`](https://mdn.io/Number/isFinite). - * - * @static - * @memberOf _ - * @since 0.1.0 - * @category Lang - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if `value` is a finite number, else `false`. - * @example - * - * _.isFinite(3); - * // => true - * - * _.isFinite(Number.MIN_VALUE); - * // => true - * - * _.isFinite(Infinity); - * // => false - * - * _.isFinite('3'); - * // => false - */ - function isFinite(value) { - return typeof value == 'number' && nativeIsFinite(value); - } - - /** - * Checks if `value` is classified as a `Function` object. - * - * @static - * @memberOf _ - * @since 0.1.0 - * @category Lang - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if `value` is a function, else `false`. - * @example - * - * _.isFunction(_); - * // => true - * - * _.isFunction(/abc/); - * // => false - */ - function isFunction(value) { - if (!isObject(value)) { - return false; - } - // The use of `Object#toString` avoids issues with the `typeof` operator - // in Safari 9 which returns 'object' for typed arrays and other constructors. - var tag = baseGetTag(value); - return tag == funcTag || tag == genTag || tag == asyncTag || tag == proxyTag; - } - - /** - * Checks if `value` is an integer. - * - * **Note:** This method is based on - * [`Number.isInteger`](https://mdn.io/Number/isInteger). - * - * @static - * @memberOf _ - * @since 4.0.0 - * @category Lang - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if `value` is an integer, else `false`. - * @example - * - * _.isInteger(3); - * // => true - * - * _.isInteger(Number.MIN_VALUE); - * // => false - * - * _.isInteger(Infinity); - * // => false - * - * _.isInteger('3'); - * // => false - */ - function isInteger(value) { - return typeof value == 'number' && value == toInteger(value); - } - - /** - * Checks if `value` is a valid array-like length. - * - * **Note:** This method is loosely based on - * [`ToLength`](http://ecma-international.org/ecma-262/7.0/#sec-tolength). - * - * @static - * @memberOf _ - * @since 4.0.0 - * @category Lang - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if `value` is a valid length, else `false`. - * @example - * - * _.isLength(3); - * // => true - * - * _.isLength(Number.MIN_VALUE); - * // => false - * - * _.isLength(Infinity); - * // => false - * - * _.isLength('3'); - * // => false - */ - function isLength(value) { - return typeof value == 'number' && - value > -1 && value % 1 == 0 && value <= MAX_SAFE_INTEGER; - } - - /** - * Checks if `value` is the - * [language type](http://www.ecma-international.org/ecma-262/7.0/#sec-ecmascript-language-types) - * of `Object`. (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`) - * - * @static - * @memberOf _ - * @since 0.1.0 - * @category Lang - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if `value` is an object, else `false`. - * @example - * - * _.isObject({}); - * // => true - * - * _.isObject([1, 2, 3]); - * // => true - * - * _.isObject(_.noop); - * // => true - * - * _.isObject(null); - * // => false - */ - function isObject(value) { - var type = typeof value; - return value != null && (type == 'object' || type == 'function'); - } - - /** - * Checks if `value` is object-like. A value is object-like if it's not `null` - * and has a `typeof` result of "object". - * - * @static - * @memberOf _ - * @since 4.0.0 - * @category Lang - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if `value` is object-like, else `false`. - * @example - * - * _.isObjectLike({}); - * // => true - * - * _.isObjectLike([1, 2, 3]); - * // => true - * - * _.isObjectLike(_.noop); - * // => false - * - * _.isObjectLike(null); - * // => false - */ - function isObjectLike(value) { - return value != null && typeof value == 'object'; - } - - /** - * Checks if `value` is classified as a `Map` object. - * - * @static - * @memberOf _ - * @since 4.3.0 - * @category Lang - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if `value` is a map, else `false`. - * @example - * - * _.isMap(new Map); - * // => true - * - * _.isMap(new WeakMap); - * // => false - */ - var isMap = nodeIsMap ? baseUnary(nodeIsMap) : baseIsMap; - - /** - * Performs a partial deep comparison between `object` and `source` to - * determine if `object` contains equivalent property values. - * - * **Note:** This method is equivalent to `_.matches` when `source` is - * partially applied. - * - * Partial comparisons will match empty array and empty object `source` - * values against any array or object value, respectively. See `_.isEqual` - * for a list of supported value comparisons. - * - * @static - * @memberOf _ - * @since 3.0.0 - * @category Lang - * @param {Object} object The object to inspect. - * @param {Object} source The object of property values to match. - * @returns {boolean} Returns `true` if `object` is a match, else `false`. - * @example - * - * var object = { 'a': 1, 'b': 2 }; - * - * _.isMatch(object, { 'b': 2 }); - * // => true - * - * _.isMatch(object, { 'b': 1 }); - * // => false - */ - function isMatch(object, source) { - return object === source || baseIsMatch(object, source, getMatchData(source)); - } - - /** - * This method is like `_.isMatch` except that it accepts `customizer` which - * is invoked to compare values. If `customizer` returns `undefined`, comparisons - * are handled by the method instead. The `customizer` is invoked with five - * arguments: (objValue, srcValue, index|key, object, source). - * - * @static - * @memberOf _ - * @since 4.0.0 - * @category Lang - * @param {Object} object The object to inspect. - * @param {Object} source The object of property values to match. - * @param {Function} [customizer] The function to customize comparisons. - * @returns {boolean} Returns `true` if `object` is a match, else `false`. - * @example - * - * function isGreeting(value) { - * return /^h(?:i|ello)$/.test(value); - * } - * - * function customizer(objValue, srcValue) { - * if (isGreeting(objValue) && isGreeting(srcValue)) { - * return true; - * } - * } - * - * var object = { 'greeting': 'hello' }; - * var source = { 'greeting': 'hi' }; - * - * _.isMatchWith(object, source, customizer); - * // => true - */ - function isMatchWith(object, source, customizer) { - customizer = typeof customizer == 'function' ? customizer : undefined; - return baseIsMatch(object, source, getMatchData(source), customizer); - } - - /** - * Checks if `value` is `NaN`. - * - * **Note:** This method is based on - * [`Number.isNaN`](https://mdn.io/Number/isNaN) and is not the same as - * global [`isNaN`](https://mdn.io/isNaN) which returns `true` for - * `undefined` and other non-number values. - * - * @static - * @memberOf _ - * @since 0.1.0 - * @category Lang - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if `value` is `NaN`, else `false`. - * @example - * - * _.isNaN(NaN); - * // => true - * - * _.isNaN(new Number(NaN)); - * // => true - * - * isNaN(undefined); - * // => true - * - * _.isNaN(undefined); - * // => false - */ - function isNaN(value) { - // An `NaN` primitive is the only value that is not equal to itself. - // Perform the `toStringTag` check first to avoid errors with some - // ActiveX objects in IE. - return isNumber(value) && value != +value; - } - - /** - * Checks if `value` is a pristine native function. - * - * **Note:** This method can't reliably detect native functions in the presence - * of the core-js package because core-js circumvents this kind of detection. - * Despite multiple requests, the core-js maintainer has made it clear: any - * attempt to fix the detection will be obstructed. As a result, we're left - * with little choice but to throw an error. Unfortunately, this also affects - * packages, like [babel-polyfill](https://www.npmjs.com/package/babel-polyfill), - * which rely on core-js. - * - * @static - * @memberOf _ - * @since 3.0.0 - * @category Lang - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if `value` is a native function, - * else `false`. - * @example - * - * _.isNative(Array.prototype.push); - * // => true - * - * _.isNative(_); - * // => false - */ - function isNative(value) { - if (isMaskable(value)) { - throw new Error(CORE_ERROR_TEXT); - } - return baseIsNative(value); - } - - /** - * Checks if `value` is `null`. - * - * @static - * @memberOf _ - * @since 0.1.0 - * @category Lang - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if `value` is `null`, else `false`. - * @example - * - * _.isNull(null); - * // => true - * - * _.isNull(void 0); - * // => false - */ - function isNull(value) { - return value === null; - } - - /** - * Checks if `value` is `null` or `undefined`. - * - * @static - * @memberOf _ - * @since 4.0.0 - * @category Lang - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if `value` is nullish, else `false`. - * @example - * - * _.isNil(null); - * // => true - * - * _.isNil(void 0); - * // => true - * - * _.isNil(NaN); - * // => false - */ - function isNil(value) { - return value == null; - } - - /** - * Checks if `value` is classified as a `Number` primitive or object. - * - * **Note:** To exclude `Infinity`, `-Infinity`, and `NaN`, which are - * classified as numbers, use the `_.isFinite` method. - * - * @static - * @memberOf _ - * @since 0.1.0 - * @category Lang - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if `value` is a number, else `false`. - * @example - * - * _.isNumber(3); - * // => true - * - * _.isNumber(Number.MIN_VALUE); - * // => true - * - * _.isNumber(Infinity); - * // => true - * - * _.isNumber('3'); - * // => false - */ - function isNumber(value) { - return typeof value == 'number' || - (isObjectLike(value) && baseGetTag(value) == numberTag); - } - - /** - * Checks if `value` is a plain object, that is, an object created by the - * `Object` constructor or one with a `[[Prototype]]` of `null`. - * - * @static - * @memberOf _ - * @since 0.8.0 - * @category Lang - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if `value` is a plain object, else `false`. - * @example - * - * function Foo() { - * this.a = 1; - * } - * - * _.isPlainObject(new Foo); - * // => false - * - * _.isPlainObject([1, 2, 3]); - * // => false - * - * _.isPlainObject({ 'x': 0, 'y': 0 }); - * // => true - * - * _.isPlainObject(Object.create(null)); - * // => true - */ - function isPlainObject(value) { - if (!isObjectLike(value) || baseGetTag(value) != objectTag) { - return false; - } - var proto = getPrototype(value); - if (proto === null) { - return true; - } - var Ctor = hasOwnProperty.call(proto, 'constructor') && proto.constructor; - return typeof Ctor == 'function' && Ctor instanceof Ctor && - funcToString.call(Ctor) == objectCtorString; - } - - /** - * Checks if `value` is classified as a `RegExp` object. - * - * @static - * @memberOf _ - * @since 0.1.0 - * @category Lang - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if `value` is a regexp, else `false`. - * @example - * - * _.isRegExp(/abc/); - * // => true - * - * _.isRegExp('/abc/'); - * // => false - */ - var isRegExp = nodeIsRegExp ? baseUnary(nodeIsRegExp) : baseIsRegExp; - - /** - * Checks if `value` is a safe integer. An integer is safe if it's an IEEE-754 - * double precision number which isn't the result of a rounded unsafe integer. - * - * **Note:** This method is based on - * [`Number.isSafeInteger`](https://mdn.io/Number/isSafeInteger). - * - * @static - * @memberOf _ - * @since 4.0.0 - * @category Lang - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if `value` is a safe integer, else `false`. - * @example - * - * _.isSafeInteger(3); - * // => true - * - * _.isSafeInteger(Number.MIN_VALUE); - * // => false - * - * _.isSafeInteger(Infinity); - * // => false - * - * _.isSafeInteger('3'); - * // => false - */ - function isSafeInteger(value) { - return isInteger(value) && value >= -MAX_SAFE_INTEGER && value <= MAX_SAFE_INTEGER; - } - - /** - * Checks if `value` is classified as a `Set` object. - * - * @static - * @memberOf _ - * @since 4.3.0 - * @category Lang - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if `value` is a set, else `false`. - * @example - * - * _.isSet(new Set); - * // => true - * - * _.isSet(new WeakSet); - * // => false - */ - var isSet = nodeIsSet ? baseUnary(nodeIsSet) : baseIsSet; - - /** - * Checks if `value` is classified as a `String` primitive or object. - * - * @static - * @since 0.1.0 - * @memberOf _ - * @category Lang - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if `value` is a string, else `false`. - * @example - * - * _.isString('abc'); - * // => true - * - * _.isString(1); - * // => false - */ - function isString(value) { - return typeof value == 'string' || - (!isArray(value) && isObjectLike(value) && baseGetTag(value) == stringTag); - } - - /** - * Checks if `value` is classified as a `Symbol` primitive or object. - * - * @static - * @memberOf _ - * @since 4.0.0 - * @category Lang - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if `value` is a symbol, else `false`. - * @example - * - * _.isSymbol(Symbol.iterator); - * // => true - * - * _.isSymbol('abc'); - * // => false - */ - function isSymbol(value) { - return typeof value == 'symbol' || - (isObjectLike(value) && baseGetTag(value) == symbolTag); - } - - /** - * Checks if `value` is classified as a typed array. - * - * @static - * @memberOf _ - * @since 3.0.0 - * @category Lang - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if `value` is a typed array, else `false`. - * @example - * - * _.isTypedArray(new Uint8Array); - * // => true - * - * _.isTypedArray([]); - * // => false - */ - var isTypedArray = nodeIsTypedArray ? baseUnary(nodeIsTypedArray) : baseIsTypedArray; - - /** - * Checks if `value` is `undefined`. - * - * @static - * @since 0.1.0 - * @memberOf _ - * @category Lang - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if `value` is `undefined`, else `false`. - * @example - * - * _.isUndefined(void 0); - * // => true - * - * _.isUndefined(null); - * // => false - */ - function isUndefined(value) { - return value === undefined; - } - - /** - * Checks if `value` is classified as a `WeakMap` object. - * - * @static - * @memberOf _ - * @since 4.3.0 - * @category Lang - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if `value` is a weak map, else `false`. - * @example - * - * _.isWeakMap(new WeakMap); - * // => true - * - * _.isWeakMap(new Map); - * // => false - */ - function isWeakMap(value) { - return isObjectLike(value) && getTag(value) == weakMapTag; - } - - /** - * Checks if `value` is classified as a `WeakSet` object. - * - * @static - * @memberOf _ - * @since 4.3.0 - * @category Lang - * @param {*} value The value to check. - * @returns {boolean} Returns `true` if `value` is a weak set, else `false`. - * @example - * - * _.isWeakSet(new WeakSet); - * // => true - * - * _.isWeakSet(new Set); - * // => false - */ - function isWeakSet(value) { - return isObjectLike(value) && baseGetTag(value) == weakSetTag; - } - - /** - * Checks if `value` is less than `other`. - * - * @static - * @memberOf _ - * @since 3.9.0 - * @category Lang - * @param {*} value The value to compare. - * @param {*} other The other value to compare. - * @returns {boolean} Returns `true` if `value` is less than `other`, - * else `false`. - * @see _.gt - * @example - * - * _.lt(1, 3); - * // => true - * - * _.lt(3, 3); - * // => false - * - * _.lt(3, 1); - * // => false - */ - var lt = createRelationalOperation(baseLt); - - /** - * Checks if `value` is less than or equal to `other`. - * - * @static - * @memberOf _ - * @since 3.9.0 - * @category Lang - * @param {*} value The value to compare. - * @param {*} other The other value to compare. - * @returns {boolean} Returns `true` if `value` is less than or equal to - * `other`, else `false`. - * @see _.gte - * @example - * - * _.lte(1, 3); - * // => true - * - * _.lte(3, 3); - * // => true - * - * _.lte(3, 1); - * // => false - */ - var lte = createRelationalOperation(function(value, other) { - return value <= other; - }); - - /** - * Converts `value` to an array. - * - * @static - * @since 0.1.0 - * @memberOf _ - * @category Lang - * @param {*} value The value to convert. - * @returns {Array} Returns the converted array. - * @example - * - * _.toArray({ 'a': 1, 'b': 2 }); - * // => [1, 2] - * - * _.toArray('abc'); - * // => ['a', 'b', 'c'] - * - * _.toArray(1); - * // => [] - * - * _.toArray(null); - * // => [] - */ - function toArray(value) { - if (!value) { - return []; - } - if (isArrayLike(value)) { - return isString(value) ? stringToArray(value) : copyArray(value); - } - if (symIterator && value[symIterator]) { - return iteratorToArray(value[symIterator]()); - } - var tag = getTag(value), - func = tag == mapTag ? mapToArray : (tag == setTag ? setToArray : values); - - return func(value); - } - - /** - * Converts `value` to a finite number. - * - * @static - * @memberOf _ - * @since 4.12.0 - * @category Lang - * @param {*} value The value to convert. - * @returns {number} Returns the converted number. - * @example - * - * _.toFinite(3.2); - * // => 3.2 - * - * _.toFinite(Number.MIN_VALUE); - * // => 5e-324 - * - * _.toFinite(Infinity); - * // => 1.7976931348623157e+308 - * - * _.toFinite('3.2'); - * // => 3.2 - */ - function toFinite(value) { - if (!value) { - return value === 0 ? value : 0; - } - value = toNumber(value); - if (value === INFINITY || value === -INFINITY) { - var sign = (value < 0 ? -1 : 1); - return sign * MAX_INTEGER; - } - return value === value ? value : 0; - } - - /** - * Converts `value` to an integer. - * - * **Note:** This method is loosely based on - * [`ToInteger`](http://www.ecma-international.org/ecma-262/7.0/#sec-tointeger). - * - * @static - * @memberOf _ - * @since 4.0.0 - * @category Lang - * @param {*} value The value to convert. - * @returns {number} Returns the converted integer. - * @example - * - * _.toInteger(3.2); - * // => 3 - * - * _.toInteger(Number.MIN_VALUE); - * // => 0 - * - * _.toInteger(Infinity); - * // => 1.7976931348623157e+308 - * - * _.toInteger('3.2'); - * // => 3 - */ - function toInteger(value) { - var result = toFinite(value), - remainder = result % 1; - - return result === result ? (remainder ? result - remainder : result) : 0; - } - - /** - * Converts `value` to an integer suitable for use as the length of an - * array-like object. - * - * **Note:** This method is based on - * [`ToLength`](http://ecma-international.org/ecma-262/7.0/#sec-tolength). - * - * @static - * @memberOf _ - * @since 4.0.0 - * @category Lang - * @param {*} value The value to convert. - * @returns {number} Returns the converted integer. - * @example - * - * _.toLength(3.2); - * // => 3 - * - * _.toLength(Number.MIN_VALUE); - * // => 0 - * - * _.toLength(Infinity); - * // => 4294967295 - * - * _.toLength('3.2'); - * // => 3 - */ - function toLength(value) { - return value ? baseClamp(toInteger(value), 0, MAX_ARRAY_LENGTH) : 0; - } - - /** - * Converts `value` to a number. - * - * @static - * @memberOf _ - * @since 4.0.0 - * @category Lang - * @param {*} value The value to process. - * @returns {number} Returns the number. - * @example - * - * _.toNumber(3.2); - * // => 3.2 - * - * _.toNumber(Number.MIN_VALUE); - * // => 5e-324 - * - * _.toNumber(Infinity); - * // => Infinity - * - * _.toNumber('3.2'); - * // => 3.2 - */ - function toNumber(value) { - if (typeof value == 'number') { - return value; - } - if (isSymbol(value)) { - return NAN; - } - if (isObject(value)) { - var other = typeof value.valueOf == 'function' ? value.valueOf() : value; - value = isObject(other) ? (other + '') : other; - } - if (typeof value != 'string') { - return value === 0 ? value : +value; - } - value = value.replace(reTrim, ''); - var isBinary = reIsBinary.test(value); - return (isBinary || reIsOctal.test(value)) - ? freeParseInt(value.slice(2), isBinary ? 2 : 8) - : (reIsBadHex.test(value) ? NAN : +value); - } - - /** - * Converts `value` to a plain object flattening inherited enumerable string - * keyed properties of `value` to own properties of the plain object. - * - * @static - * @memberOf _ - * @since 3.0.0 - * @category Lang - * @param {*} value The value to convert. - * @returns {Object} Returns the converted plain object. - * @example - * - * function Foo() { - * this.b = 2; - * } - * - * Foo.prototype.c = 3; - * - * _.assign({ 'a': 1 }, new Foo); - * // => { 'a': 1, 'b': 2 } - * - * _.assign({ 'a': 1 }, _.toPlainObject(new Foo)); - * // => { 'a': 1, 'b': 2, 'c': 3 } - */ - function toPlainObject(value) { - return copyObject(value, keysIn(value)); - } - - /** - * Converts `value` to a safe integer. A safe integer can be compared and - * represented correctly. - * - * @static - * @memberOf _ - * @since 4.0.0 - * @category Lang - * @param {*} value The value to convert. - * @returns {number} Returns the converted integer. - * @example - * - * _.toSafeInteger(3.2); - * // => 3 - * - * _.toSafeInteger(Number.MIN_VALUE); - * // => 0 - * - * _.toSafeInteger(Infinity); - * // => 9007199254740991 - * - * _.toSafeInteger('3.2'); - * // => 3 - */ - function toSafeInteger(value) { - return value - ? baseClamp(toInteger(value), -MAX_SAFE_INTEGER, MAX_SAFE_INTEGER) - : (value === 0 ? value : 0); - } - - /** - * Converts `value` to a string. An empty string is returned for `null` - * and `undefined` values. The sign of `-0` is preserved. - * - * @static - * @memberOf _ - * @since 4.0.0 - * @category Lang - * @param {*} value The value to convert. - * @returns {string} Returns the converted string. - * @example - * - * _.toString(null); - * // => '' - * - * _.toString(-0); - * // => '-0' - * - * _.toString([1, 2, 3]); - * // => '1,2,3' - */ - function toString(value) { - return value == null ? '' : baseToString(value); - } - - /*------------------------------------------------------------------------*/ - - /** - * Assigns own enumerable string keyed properties of source objects to the - * destination object. Source objects are applied from left to right. - * Subsequent sources overwrite property assignments of previous sources. - * - * **Note:** This method mutates `object` and is loosely based on - * [`Object.assign`](https://mdn.io/Object/assign). - * - * @static - * @memberOf _ - * @since 0.10.0 - * @category Object - * @param {Object} object The destination object. - * @param {...Object} [sources] The source objects. - * @returns {Object} Returns `object`. - * @see _.assignIn - * @example - * - * function Foo() { - * this.a = 1; - * } - * - * function Bar() { - * this.c = 3; - * } - * - * Foo.prototype.b = 2; - * Bar.prototype.d = 4; - * - * _.assign({ 'a': 0 }, new Foo, new Bar); - * // => { 'a': 1, 'c': 3 } - */ - var assign = createAssigner(function(object, source) { - if (isPrototype(source) || isArrayLike(source)) { - copyObject(source, keys(source), object); - return; - } - for (var key in source) { - if (hasOwnProperty.call(source, key)) { - assignValue(object, key, source[key]); - } - } - }); - - /** - * This method is like `_.assign` except that it iterates over own and - * inherited source properties. - * - * **Note:** This method mutates `object`. - * - * @static - * @memberOf _ - * @since 4.0.0 - * @alias extend - * @category Object - * @param {Object} object The destination object. - * @param {...Object} [sources] The source objects. - * @returns {Object} Returns `object`. - * @see _.assign - * @example - * - * function Foo() { - * this.a = 1; - * } - * - * function Bar() { - * this.c = 3; - * } - * - * Foo.prototype.b = 2; - * Bar.prototype.d = 4; - * - * _.assignIn({ 'a': 0 }, new Foo, new Bar); - * // => { 'a': 1, 'b': 2, 'c': 3, 'd': 4 } - */ - var assignIn = createAssigner(function(object, source) { - copyObject(source, keysIn(source), object); - }); - - /** - * This method is like `_.assignIn` except that it accepts `customizer` - * which is invoked to produce the assigned values. If `customizer` returns - * `undefined`, assignment is handled by the method instead. The `customizer` - * is invoked with five arguments: (objValue, srcValue, key, object, source). - * - * **Note:** This method mutates `object`. - * - * @static - * @memberOf _ - * @since 4.0.0 - * @alias extendWith - * @category Object - * @param {Object} object The destination object. - * @param {...Object} sources The source objects. - * @param {Function} [customizer] The function to customize assigned values. - * @returns {Object} Returns `object`. - * @see _.assignWith - * @example - * - * function customizer(objValue, srcValue) { - * return _.isUndefined(objValue) ? srcValue : objValue; - * } - * - * var defaults = _.partialRight(_.assignInWith, customizer); - * - * defaults({ 'a': 1 }, { 'b': 2 }, { 'a': 3 }); - * // => { 'a': 1, 'b': 2 } - */ - var assignInWith = createAssigner(function(object, source, srcIndex, customizer) { - copyObject(source, keysIn(source), object, customizer); - }); - - /** - * This method is like `_.assign` except that it accepts `customizer` - * which is invoked to produce the assigned values. If `customizer` returns - * `undefined`, assignment is handled by the method instead. The `customizer` - * is invoked with five arguments: (objValue, srcValue, key, object, source). - * - * **Note:** This method mutates `object`. - * - * @static - * @memberOf _ - * @since 4.0.0 - * @category Object - * @param {Object} object The destination object. - * @param {...Object} sources The source objects. - * @param {Function} [customizer] The function to customize assigned values. - * @returns {Object} Returns `object`. - * @see _.assignInWith - * @example - * - * function customizer(objValue, srcValue) { - * return _.isUndefined(objValue) ? srcValue : objValue; - * } - * - * var defaults = _.partialRight(_.assignWith, customizer); - * - * defaults({ 'a': 1 }, { 'b': 2 }, { 'a': 3 }); - * // => { 'a': 1, 'b': 2 } - */ - var assignWith = createAssigner(function(object, source, srcIndex, customizer) { - copyObject(source, keys(source), object, customizer); - }); - - /** - * Creates an array of values corresponding to `paths` of `object`. - * - * @static - * @memberOf _ - * @since 1.0.0 - * @category Object - * @param {Object} object The object to iterate over. - * @param {...(string|string[])} [paths] The property paths to pick. - * @returns {Array} Returns the picked values. - * @example - * - * var object = { 'a': [{ 'b': { 'c': 3 } }, 4] }; - * - * _.at(object, ['a[0].b.c', 'a[1]']); - * // => [3, 4] - */ - var at = flatRest(baseAt); - - /** - * Creates an object that inherits from the `prototype` object. If a - * `properties` object is given, its own enumerable string keyed properties - * are assigned to the created object. - * - * @static - * @memberOf _ - * @since 2.3.0 - * @category Object - * @param {Object} prototype The object to inherit from. - * @param {Object} [properties] The properties to assign to the object. - * @returns {Object} Returns the new object. - * @example - * - * function Shape() { - * this.x = 0; - * this.y = 0; - * } - * - * function Circle() { - * Shape.call(this); - * } - * - * Circle.prototype = _.create(Shape.prototype, { - * 'constructor': Circle - * }); - * - * var circle = new Circle; - * circle instanceof Circle; - * // => true - * - * circle instanceof Shape; - * // => true - */ - function create(prototype, properties) { - var result = baseCreate(prototype); - return properties == null ? result : baseAssign(result, properties); - } - - /** - * Assigns own and inherited enumerable string keyed properties of source - * objects to the destination object for all destination properties that - * resolve to `undefined`. Source objects are applied from left to right. - * Once a property is set, additional values of the same property are ignored. - * - * **Note:** This method mutates `object`. - * - * @static - * @since 0.1.0 - * @memberOf _ - * @category Object - * @param {Object} object The destination object. - * @param {...Object} [sources] The source objects. - * @returns {Object} Returns `object`. - * @see _.defaultsDeep - * @example - * - * _.defaults({ 'a': 1 }, { 'b': 2 }, { 'a': 3 }); - * // => { 'a': 1, 'b': 2 } - */ - var defaults = baseRest(function(args) { - args.push(undefined, customDefaultsAssignIn); - return apply(assignInWith, undefined, args); - }); - - /** - * This method is like `_.defaults` except that it recursively assigns - * default properties. - * - * **Note:** This method mutates `object`. - * - * @static - * @memberOf _ - * @since 3.10.0 - * @category Object - * @param {Object} object The destination object. - * @param {...Object} [sources] The source objects. - * @returns {Object} Returns `object`. - * @see _.defaults - * @example - * - * _.defaultsDeep({ 'a': { 'b': 2 } }, { 'a': { 'b': 1, 'c': 3 } }); - * // => { 'a': { 'b': 2, 'c': 3 } } - */ - var defaultsDeep = baseRest(function(args) { - args.push(undefined, customDefaultsMerge); - return apply(mergeWith, undefined, args); - }); - - /** - * This method is like `_.find` except that it returns the key of the first - * element `predicate` returns truthy for instead of the element itself. - * - * @static - * @memberOf _ - * @since 1.1.0 - * @category Object - * @param {Object} object The object to inspect. - * @param {Function} [predicate=_.identity] The function invoked per iteration. - * @returns {string|undefined} Returns the key of the matched element, - * else `undefined`. - * @example - * - * var users = { - * 'barney': { 'age': 36, 'active': true }, - * 'fred': { 'age': 40, 'active': false }, - * 'pebbles': { 'age': 1, 'active': true } - * }; - * - * _.findKey(users, function(o) { return o.age < 40; }); - * // => 'barney' (iteration order is not guaranteed) - * - * // The `_.matches` iteratee shorthand. - * _.findKey(users, { 'age': 1, 'active': true }); - * // => 'pebbles' - * - * // The `_.matchesProperty` iteratee shorthand. - * _.findKey(users, ['active', false]); - * // => 'fred' - * - * // The `_.property` iteratee shorthand. - * _.findKey(users, 'active'); - * // => 'barney' - */ - function findKey(object, predicate) { - return baseFindKey(object, getIteratee(predicate, 3), baseForOwn); - } - - /** - * This method is like `_.findKey` except that it iterates over elements of - * a collection in the opposite order. - * - * @static - * @memberOf _ - * @since 2.0.0 - * @category Object - * @param {Object} object The object to inspect. - * @param {Function} [predicate=_.identity] The function invoked per iteration. - * @returns {string|undefined} Returns the key of the matched element, - * else `undefined`. - * @example - * - * var users = { - * 'barney': { 'age': 36, 'active': true }, - * 'fred': { 'age': 40, 'active': false }, - * 'pebbles': { 'age': 1, 'active': true } - * }; - * - * _.findLastKey(users, function(o) { return o.age < 40; }); - * // => returns 'pebbles' assuming `_.findKey` returns 'barney' - * - * // The `_.matches` iteratee shorthand. - * _.findLastKey(users, { 'age': 36, 'active': true }); - * // => 'barney' - * - * // The `_.matchesProperty` iteratee shorthand. - * _.findLastKey(users, ['active', false]); - * // => 'fred' - * - * // The `_.property` iteratee shorthand. - * _.findLastKey(users, 'active'); - * // => 'pebbles' - */ - function findLastKey(object, predicate) { - return baseFindKey(object, getIteratee(predicate, 3), baseForOwnRight); - } - - /** - * Iterates over own and inherited enumerable string keyed properties of an - * object and invokes `iteratee` for each property. The iteratee is invoked - * with three arguments: (value, key, object). Iteratee functions may exit - * iteration early by explicitly returning `false`. - * - * @static - * @memberOf _ - * @since 0.3.0 - * @category Object - * @param {Object} object The object to iterate over. - * @param {Function} [iteratee=_.identity] The function invoked per iteration. - * @returns {Object} Returns `object`. - * @see _.forInRight - * @example - * - * function Foo() { - * this.a = 1; - * this.b = 2; - * } - * - * Foo.prototype.c = 3; - * - * _.forIn(new Foo, function(value, key) { - * console.log(key); - * }); - * // => Logs 'a', 'b', then 'c' (iteration order is not guaranteed). - */ - function forIn(object, iteratee) { - return object == null - ? object - : baseFor(object, getIteratee(iteratee, 3), keysIn); - } - - /** - * This method is like `_.forIn` except that it iterates over properties of - * `object` in the opposite order. - * - * @static - * @memberOf _ - * @since 2.0.0 - * @category Object - * @param {Object} object The object to iterate over. - * @param {Function} [iteratee=_.identity] The function invoked per iteration. - * @returns {Object} Returns `object`. - * @see _.forIn - * @example - * - * function Foo() { - * this.a = 1; - * this.b = 2; - * } - * - * Foo.prototype.c = 3; - * - * _.forInRight(new Foo, function(value, key) { - * console.log(key); - * }); - * // => Logs 'c', 'b', then 'a' assuming `_.forIn` logs 'a', 'b', then 'c'. - */ - function forInRight(object, iteratee) { - return object == null - ? object - : baseForRight(object, getIteratee(iteratee, 3), keysIn); - } - - /** - * Iterates over own enumerable string keyed properties of an object and - * invokes `iteratee` for each property. The iteratee is invoked with three - * arguments: (value, key, object). Iteratee functions may exit iteration - * early by explicitly returning `false`. - * - * @static - * @memberOf _ - * @since 0.3.0 - * @category Object - * @param {Object} object The object to iterate over. - * @param {Function} [iteratee=_.identity] The function invoked per iteration. - * @returns {Object} Returns `object`. - * @see _.forOwnRight - * @example - * - * function Foo() { - * this.a = 1; - * this.b = 2; - * } - * - * Foo.prototype.c = 3; - * - * _.forOwn(new Foo, function(value, key) { - * console.log(key); - * }); - * // => Logs 'a' then 'b' (iteration order is not guaranteed). - */ - function forOwn(object, iteratee) { - return object && baseForOwn(object, getIteratee(iteratee, 3)); - } - - /** - * This method is like `_.forOwn` except that it iterates over properties of - * `object` in the opposite order. - * - * @static - * @memberOf _ - * @since 2.0.0 - * @category Object - * @param {Object} object The object to iterate over. - * @param {Function} [iteratee=_.identity] The function invoked per iteration. - * @returns {Object} Returns `object`. - * @see _.forOwn - * @example - * - * function Foo() { - * this.a = 1; - * this.b = 2; - * } - * - * Foo.prototype.c = 3; - * - * _.forOwnRight(new Foo, function(value, key) { - * console.log(key); - * }); - * // => Logs 'b' then 'a' assuming `_.forOwn` logs 'a' then 'b'. - */ - function forOwnRight(object, iteratee) { - return object && baseForOwnRight(object, getIteratee(iteratee, 3)); - } - - /** - * Creates an array of function property names from own enumerable properties - * of `object`. - * - * @static - * @since 0.1.0 - * @memberOf _ - * @category Object - * @param {Object} object The object to inspect. - * @returns {Array} Returns the function names. - * @see _.functionsIn - * @example - * - * function Foo() { - * this.a = _.constant('a'); - * this.b = _.constant('b'); - * } - * - * Foo.prototype.c = _.constant('c'); - * - * _.functions(new Foo); - * // => ['a', 'b'] - */ - function functions(object) { - return object == null ? [] : baseFunctions(object, keys(object)); - } - - /** - * Creates an array of function property names from own and inherited - * enumerable properties of `object`. - * - * @static - * @memberOf _ - * @since 4.0.0 - * @category Object - * @param {Object} object The object to inspect. - * @returns {Array} Returns the function names. - * @see _.functions - * @example - * - * function Foo() { - * this.a = _.constant('a'); - * this.b = _.constant('b'); - * } - * - * Foo.prototype.c = _.constant('c'); - * - * _.functionsIn(new Foo); - * // => ['a', 'b', 'c'] - */ - function functionsIn(object) { - return object == null ? [] : baseFunctions(object, keysIn(object)); - } - - /** - * Gets the value at `path` of `object`. If the resolved value is - * `undefined`, the `defaultValue` is returned in its place. - * - * @static - * @memberOf _ - * @since 3.7.0 - * @category Object - * @param {Object} object The object to query. - * @param {Array|string} path The path of the property to get. - * @param {*} [defaultValue] The value returned for `undefined` resolved values. - * @returns {*} Returns the resolved value. - * @example - * - * var object = { 'a': [{ 'b': { 'c': 3 } }] }; - * - * _.get(object, 'a[0].b.c'); - * // => 3 - * - * _.get(object, ['a', '0', 'b', 'c']); - * // => 3 - * - * _.get(object, 'a.b.c', 'default'); - * // => 'default' - */ - function get(object, path, defaultValue) { - var result = object == null ? undefined : baseGet(object, path); - return result === undefined ? defaultValue : result; - } - - /** - * Checks if `path` is a direct property of `object`. - * - * @static - * @since 0.1.0 - * @memberOf _ - * @category Object - * @param {Object} object The object to query. - * @param {Array|string} path The path to check. - * @returns {boolean} Returns `true` if `path` exists, else `false`. - * @example - * - * var object = { 'a': { 'b': 2 } }; - * var other = _.create({ 'a': _.create({ 'b': 2 }) }); - * - * _.has(object, 'a'); - * // => true - * - * _.has(object, 'a.b'); - * // => true - * - * _.has(object, ['a', 'b']); - * // => true - * - * _.has(other, 'a'); - * // => false - */ - function has(object, path) { - return object != null && hasPath(object, path, baseHas); - } - - /** - * Checks if `path` is a direct or inherited property of `object`. - * - * @static - * @memberOf _ - * @since 4.0.0 - * @category Object - * @param {Object} object The object to query. - * @param {Array|string} path The path to check. - * @returns {boolean} Returns `true` if `path` exists, else `false`. - * @example - * - * var object = _.create({ 'a': _.create({ 'b': 2 }) }); - * - * _.hasIn(object, 'a'); - * // => true - * - * _.hasIn(object, 'a.b'); - * // => true - * - * _.hasIn(object, ['a', 'b']); - * // => true - * - * _.hasIn(object, 'b'); - * // => false - */ - function hasIn(object, path) { - return object != null && hasPath(object, path, baseHasIn); - } - - /** - * Creates an object composed of the inverted keys and values of `object`. - * If `object` contains duplicate values, subsequent values overwrite - * property assignments of previous values. - * - * @static - * @memberOf _ - * @since 0.7.0 - * @category Object - * @param {Object} object The object to invert. - * @returns {Object} Returns the new inverted object. - * @example - * - * var object = { 'a': 1, 'b': 2, 'c': 1 }; - * - * _.invert(object); - * // => { '1': 'c', '2': 'b' } - */ - var invert = createInverter(function(result, value, key) { - result[value] = key; - }, constant(identity)); - - /** - * This method is like `_.invert` except that the inverted object is generated - * from the results of running each element of `object` thru `iteratee`. The - * corresponding inverted value of each inverted key is an array of keys - * responsible for generating the inverted value. The iteratee is invoked - * with one argument: (value). - * - * @static - * @memberOf _ - * @since 4.1.0 - * @category Object - * @param {Object} object The object to invert. - * @param {Function} [iteratee=_.identity] The iteratee invoked per element. - * @returns {Object} Returns the new inverted object. - * @example - * - * var object = { 'a': 1, 'b': 2, 'c': 1 }; - * - * _.invertBy(object); - * // => { '1': ['a', 'c'], '2': ['b'] } - * - * _.invertBy(object, function(value) { - * return 'group' + value; - * }); - * // => { 'group1': ['a', 'c'], 'group2': ['b'] } - */ - var invertBy = createInverter(function(result, value, key) { - if (hasOwnProperty.call(result, value)) { - result[value].push(key); - } else { - result[value] = [key]; - } - }, getIteratee); - - /** - * Invokes the method at `path` of `object`. - * - * @static - * @memberOf _ - * @since 4.0.0 - * @category Object - * @param {Object} object The object to query. - * @param {Array|string} path The path of the method to invoke. - * @param {...*} [args] The arguments to invoke the method with. - * @returns {*} Returns the result of the invoked method. - * @example - * - * var object = { 'a': [{ 'b': { 'c': [1, 2, 3, 4] } }] }; - * - * _.invoke(object, 'a[0].b.c.slice', 1, 3); - * // => [2, 3] - */ - var invoke = baseRest(baseInvoke); - - /** - * Creates an array of the own enumerable property names of `object`. - * - * **Note:** Non-object values are coerced to objects. See the - * [ES spec](http://ecma-international.org/ecma-262/7.0/#sec-object.keys) - * for more details. - * - * @static - * @since 0.1.0 - * @memberOf _ - * @category Object - * @param {Object} object The object to query. - * @returns {Array} Returns the array of property names. - * @example - * - * function Foo() { - * this.a = 1; - * this.b = 2; - * } - * - * Foo.prototype.c = 3; - * - * _.keys(new Foo); - * // => ['a', 'b'] (iteration order is not guaranteed) - * - * _.keys('hi'); - * // => ['0', '1'] - */ - function keys(object) { - return isArrayLike(object) ? arrayLikeKeys(object) : baseKeys(object); - } - - /** - * Creates an array of the own and inherited enumerable property names of `object`. - * - * **Note:** Non-object values are coerced to objects. - * - * @static - * @memberOf _ - * @since 3.0.0 - * @category Object - * @param {Object} object The object to query. - * @returns {Array} Returns the array of property names. - * @example - * - * function Foo() { - * this.a = 1; - * this.b = 2; - * } - * - * Foo.prototype.c = 3; - * - * _.keysIn(new Foo); - * // => ['a', 'b', 'c'] (iteration order is not guaranteed) - */ - function keysIn(object) { - return isArrayLike(object) ? arrayLikeKeys(object, true) : baseKeysIn(object); - } - - /** - * The opposite of `_.mapValues`; this method creates an object with the - * same values as `object` and keys generated by running each own enumerable - * string keyed property of `object` thru `iteratee`. The iteratee is invoked - * with three arguments: (value, key, object). - * - * @static - * @memberOf _ - * @since 3.8.0 - * @category Object - * @param {Object} object The object to iterate over. - * @param {Function} [iteratee=_.identity] The function invoked per iteration. - * @returns {Object} Returns the new mapped object. - * @see _.mapValues - * @example - * - * _.mapKeys({ 'a': 1, 'b': 2 }, function(value, key) { - * return key + value; - * }); - * // => { 'a1': 1, 'b2': 2 } - */ - function mapKeys(object, iteratee) { - var result = {}; - iteratee = getIteratee(iteratee, 3); - - baseForOwn(object, function(value, key, object) { - baseAssignValue(result, iteratee(value, key, object), value); - }); - return result; - } - - /** - * Creates an object with the same keys as `object` and values generated - * by running each own enumerable string keyed property of `object` thru - * `iteratee`. The iteratee is invoked with three arguments: - * (value, key, object). - * - * @static - * @memberOf _ - * @since 2.4.0 - * @category Object - * @param {Object} object The object to iterate over. - * @param {Function} [iteratee=_.identity] The function invoked per iteration. - * @returns {Object} Returns the new mapped object. - * @see _.mapKeys - * @example - * - * var users = { - * 'fred': { 'user': 'fred', 'age': 40 }, - * 'pebbles': { 'user': 'pebbles', 'age': 1 } - * }; - * - * _.mapValues(users, function(o) { return o.age; }); - * // => { 'fred': 40, 'pebbles': 1 } (iteration order is not guaranteed) - * - * // The `_.property` iteratee shorthand. - * _.mapValues(users, 'age'); - * // => { 'fred': 40, 'pebbles': 1 } (iteration order is not guaranteed) - */ - function mapValues(object, iteratee) { - var result = {}; - iteratee = getIteratee(iteratee, 3); - - baseForOwn(object, function(value, key, object) { - baseAssignValue(result, key, iteratee(value, key, object)); - }); - return result; - } - - /** - * This method is like `_.assign` except that it recursively merges own and - * inherited enumerable string keyed properties of source objects into the - * destination object. Source properties that resolve to `undefined` are - * skipped if a destination value exists. Array and plain object properties - * are merged recursively. Other objects and value types are overridden by - * assignment. Source objects are applied from left to right. Subsequent - * sources overwrite property assignments of previous sources. - * - * **Note:** This method mutates `object`. - * - * @static - * @memberOf _ - * @since 0.5.0 - * @category Object - * @param {Object} object The destination object. - * @param {...Object} [sources] The source objects. - * @returns {Object} Returns `object`. - * @example - * - * var object = { - * 'a': [{ 'b': 2 }, { 'd': 4 }] - * }; - * - * var other = { - * 'a': [{ 'c': 3 }, { 'e': 5 }] - * }; - * - * _.merge(object, other); - * // => { 'a': [{ 'b': 2, 'c': 3 }, { 'd': 4, 'e': 5 }] } - */ - var merge = createAssigner(function(object, source, srcIndex) { - baseMerge(object, source, srcIndex); - }); - - /** - * This method is like `_.merge` except that it accepts `customizer` which - * is invoked to produce the merged values of the destination and source - * properties. If `customizer` returns `undefined`, merging is handled by the - * method instead. The `customizer` is invoked with six arguments: - * (objValue, srcValue, key, object, source, stack). - * - * **Note:** This method mutates `object`. - * - * @static - * @memberOf _ - * @since 4.0.0 - * @category Object - * @param {Object} object The destination object. - * @param {...Object} sources The source objects. - * @param {Function} customizer The function to customize assigned values. - * @returns {Object} Returns `object`. - * @example - * - * function customizer(objValue, srcValue) { - * if (_.isArray(objValue)) { - * return objValue.concat(srcValue); - * } - * } - * - * var object = { 'a': [1], 'b': [2] }; - * var other = { 'a': [3], 'b': [4] }; - * - * _.mergeWith(object, other, customizer); - * // => { 'a': [1, 3], 'b': [2, 4] } - */ - var mergeWith = createAssigner(function(object, source, srcIndex, customizer) { - baseMerge(object, source, srcIndex, customizer); - }); - - /** - * The opposite of `_.pick`; this method creates an object composed of the - * own and inherited enumerable property paths of `object` that are not omitted. - * - * **Note:** This method is considerably slower than `_.pick`. - * - * @static - * @since 0.1.0 - * @memberOf _ - * @category Object - * @param {Object} object The source object. - * @param {...(string|string[])} [paths] The property paths to omit. - * @returns {Object} Returns the new object. - * @example - * - * var object = { 'a': 1, 'b': '2', 'c': 3 }; - * - * _.omit(object, ['a', 'c']); - * // => { 'b': '2' } - */ - var omit = flatRest(function(object, paths) { - var result = {}; - if (object == null) { - return result; - } - var isDeep = false; - paths = arrayMap(paths, function(path) { - path = castPath(path, object); - isDeep || (isDeep = path.length > 1); - return path; - }); - copyObject(object, getAllKeysIn(object), result); - if (isDeep) { - result = baseClone(result, CLONE_DEEP_FLAG | CLONE_FLAT_FLAG | CLONE_SYMBOLS_FLAG, customOmitClone); - } - var length = paths.length; - while (length--) { - baseUnset(result, paths[length]); - } - return result; - }); - - /** - * The opposite of `_.pickBy`; this method creates an object composed of - * the own and inherited enumerable string keyed properties of `object` that - * `predicate` doesn't return truthy for. The predicate is invoked with two - * arguments: (value, key). - * - * @static - * @memberOf _ - * @since 4.0.0 - * @category Object - * @param {Object} object The source object. - * @param {Function} [predicate=_.identity] The function invoked per property. - * @returns {Object} Returns the new object. - * @example - * - * var object = { 'a': 1, 'b': '2', 'c': 3 }; - * - * _.omitBy(object, _.isNumber); - * // => { 'b': '2' } - */ - function omitBy(object, predicate) { - return pickBy(object, negate(getIteratee(predicate))); - } - - /** - * Creates an object composed of the picked `object` properties. - * - * @static - * @since 0.1.0 - * @memberOf _ - * @category Object - * @param {Object} object The source object. - * @param {...(string|string[])} [paths] The property paths to pick. - * @returns {Object} Returns the new object. - * @example - * - * var object = { 'a': 1, 'b': '2', 'c': 3 }; - * - * _.pick(object, ['a', 'c']); - * // => { 'a': 1, 'c': 3 } - */ - var pick = flatRest(function(object, paths) { - return object == null ? {} : basePick(object, paths); - }); - - /** - * Creates an object composed of the `object` properties `predicate` returns - * truthy for. The predicate is invoked with two arguments: (value, key). - * - * @static - * @memberOf _ - * @since 4.0.0 - * @category Object - * @param {Object} object The source object. - * @param {Function} [predicate=_.identity] The function invoked per property. - * @returns {Object} Returns the new object. - * @example - * - * var object = { 'a': 1, 'b': '2', 'c': 3 }; - * - * _.pickBy(object, _.isNumber); - * // => { 'a': 1, 'c': 3 } - */ - function pickBy(object, predicate) { - if (object == null) { - return {}; - } - var props = arrayMap(getAllKeysIn(object), function(prop) { - return [prop]; - }); - predicate = getIteratee(predicate); - return basePickBy(object, props, function(value, path) { - return predicate(value, path[0]); - }); - } - - /** - * This method is like `_.get` except that if the resolved value is a - * function it's invoked with the `this` binding of its parent object and - * its result is returned. - * - * @static - * @since 0.1.0 - * @memberOf _ - * @category Object - * @param {Object} object The object to query. - * @param {Array|string} path The path of the property to resolve. - * @param {*} [defaultValue] The value returned for `undefined` resolved values. - * @returns {*} Returns the resolved value. - * @example - * - * var object = { 'a': [{ 'b': { 'c1': 3, 'c2': _.constant(4) } }] }; - * - * _.result(object, 'a[0].b.c1'); - * // => 3 - * - * _.result(object, 'a[0].b.c2'); - * // => 4 - * - * _.result(object, 'a[0].b.c3', 'default'); - * // => 'default' - * - * _.result(object, 'a[0].b.c3', _.constant('default')); - * // => 'default' - */ - function result(object, path, defaultValue) { - path = castPath(path, object); - - var index = -1, - length = path.length; - - // Ensure the loop is entered when path is empty. - if (!length) { - length = 1; - object = undefined; - } - while (++index < length) { - var value = object == null ? undefined : object[toKey(path[index])]; - if (value === undefined) { - index = length; - value = defaultValue; - } - object = isFunction(value) ? value.call(object) : value; - } - return object; - } - - /** - * Sets the value at `path` of `object`. If a portion of `path` doesn't exist, - * it's created. Arrays are created for missing index properties while objects - * are created for all other missing properties. Use `_.setWith` to customize - * `path` creation. - * - * **Note:** This method mutates `object`. - * - * @static - * @memberOf _ - * @since 3.7.0 - * @category Object - * @param {Object} object The object to modify. - * @param {Array|string} path The path of the property to set. - * @param {*} value The value to set. - * @returns {Object} Returns `object`. - * @example - * - * var object = { 'a': [{ 'b': { 'c': 3 } }] }; - * - * _.set(object, 'a[0].b.c', 4); - * console.log(object.a[0].b.c); - * // => 4 - * - * _.set(object, ['x', '0', 'y', 'z'], 5); - * console.log(object.x[0].y.z); - * // => 5 - */ - function set(object, path, value) { - return object == null ? object : baseSet(object, path, value); - } - - /** - * This method is like `_.set` except that it accepts `customizer` which is - * invoked to produce the objects of `path`. If `customizer` returns `undefined` - * path creation is handled by the method instead. The `customizer` is invoked - * with three arguments: (nsValue, key, nsObject). - * - * **Note:** This method mutates `object`. - * - * @static - * @memberOf _ - * @since 4.0.0 - * @category Object - * @param {Object} object The object to modify. - * @param {Array|string} path The path of the property to set. - * @param {*} value The value to set. - * @param {Function} [customizer] The function to customize assigned values. - * @returns {Object} Returns `object`. - * @example - * - * var object = {}; - * - * _.setWith(object, '[0][1]', 'a', Object); - * // => { '0': { '1': 'a' } } - */ - function setWith(object, path, value, customizer) { - customizer = typeof customizer == 'function' ? customizer : undefined; - return object == null ? object : baseSet(object, path, value, customizer); - } - - /** - * Creates an array of own enumerable string keyed-value pairs for `object` - * which can be consumed by `_.fromPairs`. If `object` is a map or set, its - * entries are returned. - * - * @static - * @memberOf _ - * @since 4.0.0 - * @alias entries - * @category Object - * @param {Object} object The object to query. - * @returns {Array} Returns the key-value pairs. - * @example - * - * function Foo() { - * this.a = 1; - * this.b = 2; - * } - * - * Foo.prototype.c = 3; - * - * _.toPairs(new Foo); - * // => [['a', 1], ['b', 2]] (iteration order is not guaranteed) - */ - var toPairs = createToPairs(keys); - - /** - * Creates an array of own and inherited enumerable string keyed-value pairs - * for `object` which can be consumed by `_.fromPairs`. If `object` is a map - * or set, its entries are returned. - * - * @static - * @memberOf _ - * @since 4.0.0 - * @alias entriesIn - * @category Object - * @param {Object} object The object to query. - * @returns {Array} Returns the key-value pairs. - * @example - * - * function Foo() { - * this.a = 1; - * this.b = 2; - * } - * - * Foo.prototype.c = 3; - * - * _.toPairsIn(new Foo); - * // => [['a', 1], ['b', 2], ['c', 3]] (iteration order is not guaranteed) - */ - var toPairsIn = createToPairs(keysIn); - - /** - * An alternative to `_.reduce`; this method transforms `object` to a new - * `accumulator` object which is the result of running each of its own - * enumerable string keyed properties thru `iteratee`, with each invocation - * potentially mutating the `accumulator` object. If `accumulator` is not - * provided, a new object with the same `[[Prototype]]` will be used. The - * iteratee is invoked with four arguments: (accumulator, value, key, object). - * Iteratee functions may exit iteration early by explicitly returning `false`. - * - * @static - * @memberOf _ - * @since 1.3.0 - * @category Object - * @param {Object} object The object to iterate over. - * @param {Function} [iteratee=_.identity] The function invoked per iteration. - * @param {*} [accumulator] The custom accumulator value. - * @returns {*} Returns the accumulated value. - * @example - * - * _.transform([2, 3, 4], function(result, n) { - * result.push(n *= n); - * return n % 2 == 0; - * }, []); - * // => [4, 9] - * - * _.transform({ 'a': 1, 'b': 2, 'c': 1 }, function(result, value, key) { - * (result[value] || (result[value] = [])).push(key); - * }, {}); - * // => { '1': ['a', 'c'], '2': ['b'] } - */ - function transform(object, iteratee, accumulator) { - var isArr = isArray(object), - isArrLike = isArr || isBuffer(object) || isTypedArray(object); - - iteratee = getIteratee(iteratee, 4); - if (accumulator == null) { - var Ctor = object && object.constructor; - if (isArrLike) { - accumulator = isArr ? new Ctor : []; - } - else if (isObject(object)) { - accumulator = isFunction(Ctor) ? baseCreate(getPrototype(object)) : {}; - } - else { - accumulator = {}; - } - } - (isArrLike ? arrayEach : baseForOwn)(object, function(value, index, object) { - return iteratee(accumulator, value, index, object); - }); - return accumulator; - } - - /** - * Removes the property at `path` of `object`. - * - * **Note:** This method mutates `object`. - * - * @static - * @memberOf _ - * @since 4.0.0 - * @category Object - * @param {Object} object The object to modify. - * @param {Array|string} path The path of the property to unset. - * @returns {boolean} Returns `true` if the property is deleted, else `false`. - * @example - * - * var object = { 'a': [{ 'b': { 'c': 7 } }] }; - * _.unset(object, 'a[0].b.c'); - * // => true - * - * console.log(object); - * // => { 'a': [{ 'b': {} }] }; - * - * _.unset(object, ['a', '0', 'b', 'c']); - * // => true - * - * console.log(object); - * // => { 'a': [{ 'b': {} }] }; - */ - function unset(object, path) { - return object == null ? true : baseUnset(object, path); - } - - /** - * This method is like `_.set` except that accepts `updater` to produce the - * value to set. Use `_.updateWith` to customize `path` creation. The `updater` - * is invoked with one argument: (value). - * - * **Note:** This method mutates `object`. - * - * @static - * @memberOf _ - * @since 4.6.0 - * @category Object - * @param {Object} object The object to modify. - * @param {Array|string} path The path of the property to set. - * @param {Function} updater The function to produce the updated value. - * @returns {Object} Returns `object`. - * @example - * - * var object = { 'a': [{ 'b': { 'c': 3 } }] }; - * - * _.update(object, 'a[0].b.c', function(n) { return n * n; }); - * console.log(object.a[0].b.c); - * // => 9 - * - * _.update(object, 'x[0].y.z', function(n) { return n ? n + 1 : 0; }); - * console.log(object.x[0].y.z); - * // => 0 - */ - function update(object, path, updater) { - return object == null ? object : baseUpdate(object, path, castFunction(updater)); - } - - /** - * This method is like `_.update` except that it accepts `customizer` which is - * invoked to produce the objects of `path`. If `customizer` returns `undefined` - * path creation is handled by the method instead. The `customizer` is invoked - * with three arguments: (nsValue, key, nsObject). - * - * **Note:** This method mutates `object`. - * - * @static - * @memberOf _ - * @since 4.6.0 - * @category Object - * @param {Object} object The object to modify. - * @param {Array|string} path The path of the property to set. - * @param {Function} updater The function to produce the updated value. - * @param {Function} [customizer] The function to customize assigned values. - * @returns {Object} Returns `object`. - * @example - * - * var object = {}; - * - * _.updateWith(object, '[0][1]', _.constant('a'), Object); - * // => { '0': { '1': 'a' } } - */ - function updateWith(object, path, updater, customizer) { - customizer = typeof customizer == 'function' ? customizer : undefined; - return object == null ? object : baseUpdate(object, path, castFunction(updater), customizer); - } - - /** - * Creates an array of the own enumerable string keyed property values of `object`. - * - * **Note:** Non-object values are coerced to objects. - * - * @static - * @since 0.1.0 - * @memberOf _ - * @category Object - * @param {Object} object The object to query. - * @returns {Array} Returns the array of property values. - * @example - * - * function Foo() { - * this.a = 1; - * this.b = 2; - * } - * - * Foo.prototype.c = 3; - * - * _.values(new Foo); - * // => [1, 2] (iteration order is not guaranteed) - * - * _.values('hi'); - * // => ['h', 'i'] - */ - function values(object) { - return object == null ? [] : baseValues(object, keys(object)); - } - - /** - * Creates an array of the own and inherited enumerable string keyed property - * values of `object`. - * - * **Note:** Non-object values are coerced to objects. - * - * @static - * @memberOf _ - * @since 3.0.0 - * @category Object - * @param {Object} object The object to query. - * @returns {Array} Returns the array of property values. - * @example - * - * function Foo() { - * this.a = 1; - * this.b = 2; - * } - * - * Foo.prototype.c = 3; - * - * _.valuesIn(new Foo); - * // => [1, 2, 3] (iteration order is not guaranteed) - */ - function valuesIn(object) { - return object == null ? [] : baseValues(object, keysIn(object)); - } - - /*------------------------------------------------------------------------*/ - - /** - * Clamps `number` within the inclusive `lower` and `upper` bounds. - * - * @static - * @memberOf _ - * @since 4.0.0 - * @category Number - * @param {number} number The number to clamp. - * @param {number} [lower] The lower bound. - * @param {number} upper The upper bound. - * @returns {number} Returns the clamped number. - * @example - * - * _.clamp(-10, -5, 5); - * // => -5 - * - * _.clamp(10, -5, 5); - * // => 5 - */ - function clamp(number, lower, upper) { - if (upper === undefined) { - upper = lower; - lower = undefined; - } - if (upper !== undefined) { - upper = toNumber(upper); - upper = upper === upper ? upper : 0; - } - if (lower !== undefined) { - lower = toNumber(lower); - lower = lower === lower ? lower : 0; - } - return baseClamp(toNumber(number), lower, upper); - } - - /** - * Checks if `n` is between `start` and up to, but not including, `end`. If - * `end` is not specified, it's set to `start` with `start` then set to `0`. - * If `start` is greater than `end` the params are swapped to support - * negative ranges. - * - * @static - * @memberOf _ - * @since 3.3.0 - * @category Number - * @param {number} number The number to check. - * @param {number} [start=0] The start of the range. - * @param {number} end The end of the range. - * @returns {boolean} Returns `true` if `number` is in the range, else `false`. - * @see _.range, _.rangeRight - * @example - * - * _.inRange(3, 2, 4); - * // => true - * - * _.inRange(4, 8); - * // => true - * - * _.inRange(4, 2); - * // => false - * - * _.inRange(2, 2); - * // => false - * - * _.inRange(1.2, 2); - * // => true - * - * _.inRange(5.2, 4); - * // => false - * - * _.inRange(-3, -2, -6); - * // => true - */ - function inRange(number, start, end) { - start = toFinite(start); - if (end === undefined) { - end = start; - start = 0; - } else { - end = toFinite(end); - } - number = toNumber(number); - return baseInRange(number, start, end); - } - - /** - * Produces a random number between the inclusive `lower` and `upper` bounds. - * If only one argument is provided a number between `0` and the given number - * is returned. If `floating` is `true`, or either `lower` or `upper` are - * floats, a floating-point number is returned instead of an integer. - * - * **Note:** JavaScript follows the IEEE-754 standard for resolving - * floating-point values which can produce unexpected results. - * - * @static - * @memberOf _ - * @since 0.7.0 - * @category Number - * @param {number} [lower=0] The lower bound. - * @param {number} [upper=1] The upper bound. - * @param {boolean} [floating] Specify returning a floating-point number. - * @returns {number} Returns the random number. - * @example - * - * _.random(0, 5); - * // => an integer between 0 and 5 - * - * _.random(5); - * // => also an integer between 0 and 5 - * - * _.random(5, true); - * // => a floating-point number between 0 and 5 - * - * _.random(1.2, 5.2); - * // => a floating-point number between 1.2 and 5.2 - */ - function random(lower, upper, floating) { - if (floating && typeof floating != 'boolean' && isIterateeCall(lower, upper, floating)) { - upper = floating = undefined; - } - if (floating === undefined) { - if (typeof upper == 'boolean') { - floating = upper; - upper = undefined; - } - else if (typeof lower == 'boolean') { - floating = lower; - lower = undefined; - } - } - if (lower === undefined && upper === undefined) { - lower = 0; - upper = 1; - } - else { - lower = toFinite(lower); - if (upper === undefined) { - upper = lower; - lower = 0; - } else { - upper = toFinite(upper); - } - } - if (lower > upper) { - var temp = lower; - lower = upper; - upper = temp; - } - if (floating || lower % 1 || upper % 1) { - var rand = nativeRandom(); - return nativeMin(lower + (rand * (upper - lower + freeParseFloat('1e-' + ((rand + '').length - 1)))), upper); - } - return baseRandom(lower, upper); - } - - /*------------------------------------------------------------------------*/ - - /** - * Converts `string` to [camel case](https://en.wikipedia.org/wiki/CamelCase). - * - * @static - * @memberOf _ - * @since 3.0.0 - * @category String - * @param {string} [string=''] The string to convert. - * @returns {string} Returns the camel cased string. - * @example - * - * _.camelCase('Foo Bar'); - * // => 'fooBar' - * - * _.camelCase('--foo-bar--'); - * // => 'fooBar' - * - * _.camelCase('__FOO_BAR__'); - * // => 'fooBar' - */ - var camelCase = createCompounder(function(result, word, index) { - word = word.toLowerCase(); - return result + (index ? capitalize(word) : word); - }); - - /** - * Converts the first character of `string` to upper case and the remaining - * to lower case. - * - * @static - * @memberOf _ - * @since 3.0.0 - * @category String - * @param {string} [string=''] The string to capitalize. - * @returns {string} Returns the capitalized string. - * @example - * - * _.capitalize('FRED'); - * // => 'Fred' - */ - function capitalize(string) { - return upperFirst(toString(string).toLowerCase()); - } - - /** - * Deburrs `string` by converting - * [Latin-1 Supplement](https://en.wikipedia.org/wiki/Latin-1_Supplement_(Unicode_block)#Character_table) - * and [Latin Extended-A](https://en.wikipedia.org/wiki/Latin_Extended-A) - * letters to basic Latin letters and removing - * [combining diacritical marks](https://en.wikipedia.org/wiki/Combining_Diacritical_Marks). - * - * @static - * @memberOf _ - * @since 3.0.0 - * @category String - * @param {string} [string=''] The string to deburr. - * @returns {string} Returns the deburred string. - * @example - * - * _.deburr('déjà vu'); - * // => 'deja vu' - */ - function deburr(string) { - string = toString(string); - return string && string.replace(reLatin, deburrLetter).replace(reComboMark, ''); - } - - /** - * Checks if `string` ends with the given target string. - * - * @static - * @memberOf _ - * @since 3.0.0 - * @category String - * @param {string} [string=''] The string to inspect. - * @param {string} [target] The string to search for. - * @param {number} [position=string.length] The position to search up to. - * @returns {boolean} Returns `true` if `string` ends with `target`, - * else `false`. - * @example - * - * _.endsWith('abc', 'c'); - * // => true - * - * _.endsWith('abc', 'b'); - * // => false - * - * _.endsWith('abc', 'b', 2); - * // => true - */ - function endsWith(string, target, position) { - string = toString(string); - target = baseToString(target); - - var length = string.length; - position = position === undefined - ? length - : baseClamp(toInteger(position), 0, length); - - var end = position; - position -= target.length; - return position >= 0 && string.slice(position, end) == target; - } - - /** - * Converts the characters "&", "<", ">", '"', and "'" in `string` to their - * corresponding HTML entities. - * - * **Note:** No other characters are escaped. To escape additional - * characters use a third-party library like [_he_](https://mths.be/he). - * - * Though the ">" character is escaped for symmetry, characters like - * ">" and "/" don't need escaping in HTML and have no special meaning - * unless they're part of a tag or unquoted attribute value. See - * [Mathias Bynens's article](https://mathiasbynens.be/notes/ambiguous-ampersands) - * (under "semi-related fun fact") for more details. - * - * When working with HTML you should always - * [quote attribute values](http://wonko.com/post/html-escaping) to reduce - * XSS vectors. - * - * @static - * @since 0.1.0 - * @memberOf _ - * @category String - * @param {string} [string=''] The string to escape. - * @returns {string} Returns the escaped string. - * @example - * - * _.escape('fred, barney, & pebbles'); - * // => 'fred, barney, & pebbles' - */ - function escape(string) { - string = toString(string); - return (string && reHasUnescapedHtml.test(string)) - ? string.replace(reUnescapedHtml, escapeHtmlChar) - : string; - } - - /** - * Escapes the `RegExp` special characters "^", "$", "\", ".", "*", "+", - * "?", "(", ")", "[", "]", "{", "}", and "|" in `string`. - * - * @static - * @memberOf _ - * @since 3.0.0 - * @category String - * @param {string} [string=''] The string to escape. - * @returns {string} Returns the escaped string. - * @example - * - * _.escapeRegExp('[lodash](https://lodash.com/)'); - * // => '\[lodash\]\(https://lodash\.com/\)' - */ - function escapeRegExp(string) { - string = toString(string); - return (string && reHasRegExpChar.test(string)) - ? string.replace(reRegExpChar, '\\$&') - : string; - } - - /** - * Converts `string` to - * [kebab case](https://en.wikipedia.org/wiki/Letter_case#Special_case_styles). - * - * @static - * @memberOf _ - * @since 3.0.0 - * @category String - * @param {string} [string=''] The string to convert. - * @returns {string} Returns the kebab cased string. - * @example - * - * _.kebabCase('Foo Bar'); - * // => 'foo-bar' - * - * _.kebabCase('fooBar'); - * // => 'foo-bar' - * - * _.kebabCase('__FOO_BAR__'); - * // => 'foo-bar' - */ - var kebabCase = createCompounder(function(result, word, index) { - return result + (index ? '-' : '') + word.toLowerCase(); - }); - - /** - * Converts `string`, as space separated words, to lower case. - * - * @static - * @memberOf _ - * @since 4.0.0 - * @category String - * @param {string} [string=''] The string to convert. - * @returns {string} Returns the lower cased string. - * @example - * - * _.lowerCase('--Foo-Bar--'); - * // => 'foo bar' - * - * _.lowerCase('fooBar'); - * // => 'foo bar' - * - * _.lowerCase('__FOO_BAR__'); - * // => 'foo bar' - */ - var lowerCase = createCompounder(function(result, word, index) { - return result + (index ? ' ' : '') + word.toLowerCase(); - }); - - /** - * Converts the first character of `string` to lower case. - * - * @static - * @memberOf _ - * @since 4.0.0 - * @category String - * @param {string} [string=''] The string to convert. - * @returns {string} Returns the converted string. - * @example - * - * _.lowerFirst('Fred'); - * // => 'fred' - * - * _.lowerFirst('FRED'); - * // => 'fRED' - */ - var lowerFirst = createCaseFirst('toLowerCase'); - - /** - * Pads `string` on the left and right sides if it's shorter than `length`. - * Padding characters are truncated if they can't be evenly divided by `length`. - * - * @static - * @memberOf _ - * @since 3.0.0 - * @category String - * @param {string} [string=''] The string to pad. - * @param {number} [length=0] The padding length. - * @param {string} [chars=' '] The string used as padding. - * @returns {string} Returns the padded string. - * @example - * - * _.pad('abc', 8); - * // => ' abc ' - * - * _.pad('abc', 8, '_-'); - * // => '_-abc_-_' - * - * _.pad('abc', 3); - * // => 'abc' - */ - function pad(string, length, chars) { - string = toString(string); - length = toInteger(length); - - var strLength = length ? stringSize(string) : 0; - if (!length || strLength >= length) { - return string; - } - var mid = (length - strLength) / 2; - return ( - createPadding(nativeFloor(mid), chars) + - string + - createPadding(nativeCeil(mid), chars) - ); - } - - /** - * Pads `string` on the right side if it's shorter than `length`. Padding - * characters are truncated if they exceed `length`. - * - * @static - * @memberOf _ - * @since 4.0.0 - * @category String - * @param {string} [string=''] The string to pad. - * @param {number} [length=0] The padding length. - * @param {string} [chars=' '] The string used as padding. - * @returns {string} Returns the padded string. - * @example - * - * _.padEnd('abc', 6); - * // => 'abc ' - * - * _.padEnd('abc', 6, '_-'); - * // => 'abc_-_' - * - * _.padEnd('abc', 3); - * // => 'abc' - */ - function padEnd(string, length, chars) { - string = toString(string); - length = toInteger(length); - - var strLength = length ? stringSize(string) : 0; - return (length && strLength < length) - ? (string + createPadding(length - strLength, chars)) - : string; - } - - /** - * Pads `string` on the left side if it's shorter than `length`. Padding - * characters are truncated if they exceed `length`. - * - * @static - * @memberOf _ - * @since 4.0.0 - * @category String - * @param {string} [string=''] The string to pad. - * @param {number} [length=0] The padding length. - * @param {string} [chars=' '] The string used as padding. - * @returns {string} Returns the padded string. - * @example - * - * _.padStart('abc', 6); - * // => ' abc' - * - * _.padStart('abc', 6, '_-'); - * // => '_-_abc' - * - * _.padStart('abc', 3); - * // => 'abc' - */ - function padStart(string, length, chars) { - string = toString(string); - length = toInteger(length); - - var strLength = length ? stringSize(string) : 0; - return (length && strLength < length) - ? (createPadding(length - strLength, chars) + string) - : string; - } - - /** - * Converts `string` to an integer of the specified radix. If `radix` is - * `undefined` or `0`, a `radix` of `10` is used unless `value` is a - * hexadecimal, in which case a `radix` of `16` is used. - * - * **Note:** This method aligns with the - * [ES5 implementation](https://es5.github.io/#x15.1.2.2) of `parseInt`. - * - * @static - * @memberOf _ - * @since 1.1.0 - * @category String - * @param {string} string The string to convert. - * @param {number} [radix=10] The radix to interpret `value` by. - * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`. - * @returns {number} Returns the converted integer. - * @example - * - * _.parseInt('08'); - * // => 8 - * - * _.map(['6', '08', '10'], _.parseInt); - * // => [6, 8, 10] - */ - function parseInt(string, radix, guard) { - if (guard || radix == null) { - radix = 0; - } else if (radix) { - radix = +radix; - } - return nativeParseInt(toString(string).replace(reTrimStart, ''), radix || 0); - } - - /** - * Repeats the given string `n` times. - * - * @static - * @memberOf _ - * @since 3.0.0 - * @category String - * @param {string} [string=''] The string to repeat. - * @param {number} [n=1] The number of times to repeat the string. - * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`. - * @returns {string} Returns the repeated string. - * @example - * - * _.repeat('*', 3); - * // => '***' - * - * _.repeat('abc', 2); - * // => 'abcabc' - * - * _.repeat('abc', 0); - * // => '' - */ - function repeat(string, n, guard) { - if ((guard ? isIterateeCall(string, n, guard) : n === undefined)) { - n = 1; - } else { - n = toInteger(n); - } - return baseRepeat(toString(string), n); - } - - /** - * Replaces matches for `pattern` in `string` with `replacement`. - * - * **Note:** This method is based on - * [`String#replace`](https://mdn.io/String/replace). - * - * @static - * @memberOf _ - * @since 4.0.0 - * @category String - * @param {string} [string=''] The string to modify. - * @param {RegExp|string} pattern The pattern to replace. - * @param {Function|string} replacement The match replacement. - * @returns {string} Returns the modified string. - * @example - * - * _.replace('Hi Fred', 'Fred', 'Barney'); - * // => 'Hi Barney' - */ - function replace() { - var args = arguments, - string = toString(args[0]); - - return args.length < 3 ? string : string.replace(args[1], args[2]); - } - - /** - * Converts `string` to - * [snake case](https://en.wikipedia.org/wiki/Snake_case). - * - * @static - * @memberOf _ - * @since 3.0.0 - * @category String - * @param {string} [string=''] The string to convert. - * @returns {string} Returns the snake cased string. - * @example - * - * _.snakeCase('Foo Bar'); - * // => 'foo_bar' - * - * _.snakeCase('fooBar'); - * // => 'foo_bar' - * - * _.snakeCase('--FOO-BAR--'); - * // => 'foo_bar' - */ - var snakeCase = createCompounder(function(result, word, index) { - return result + (index ? '_' : '') + word.toLowerCase(); - }); - - /** - * Splits `string` by `separator`. - * - * **Note:** This method is based on - * [`String#split`](https://mdn.io/String/split). - * - * @static - * @memberOf _ - * @since 4.0.0 - * @category String - * @param {string} [string=''] The string to split. - * @param {RegExp|string} separator The separator pattern to split by. - * @param {number} [limit] The length to truncate results to. - * @returns {Array} Returns the string segments. - * @example - * - * _.split('a-b-c', '-', 2); - * // => ['a', 'b'] - */ - function split(string, separator, limit) { - if (limit && typeof limit != 'number' && isIterateeCall(string, separator, limit)) { - separator = limit = undefined; - } - limit = limit === undefined ? MAX_ARRAY_LENGTH : limit >>> 0; - if (!limit) { - return []; - } - string = toString(string); - if (string && ( - typeof separator == 'string' || - (separator != null && !isRegExp(separator)) - )) { - separator = baseToString(separator); - if (!separator && hasUnicode(string)) { - return castSlice(stringToArray(string), 0, limit); - } - } - return string.split(separator, limit); - } - - /** - * Converts `string` to - * [start case](https://en.wikipedia.org/wiki/Letter_case#Stylistic_or_specialised_usage). - * - * @static - * @memberOf _ - * @since 3.1.0 - * @category String - * @param {string} [string=''] The string to convert. - * @returns {string} Returns the start cased string. - * @example - * - * _.startCase('--foo-bar--'); - * // => 'Foo Bar' - * - * _.startCase('fooBar'); - * // => 'Foo Bar' - * - * _.startCase('__FOO_BAR__'); - * // => 'FOO BAR' - */ - var startCase = createCompounder(function(result, word, index) { - return result + (index ? ' ' : '') + upperFirst(word); - }); - - /** - * Checks if `string` starts with the given target string. - * - * @static - * @memberOf _ - * @since 3.0.0 - * @category String - * @param {string} [string=''] The string to inspect. - * @param {string} [target] The string to search for. - * @param {number} [position=0] The position to search from. - * @returns {boolean} Returns `true` if `string` starts with `target`, - * else `false`. - * @example - * - * _.startsWith('abc', 'a'); - * // => true - * - * _.startsWith('abc', 'b'); - * // => false - * - * _.startsWith('abc', 'b', 1); - * // => true - */ - function startsWith(string, target, position) { - string = toString(string); - position = position == null - ? 0 - : baseClamp(toInteger(position), 0, string.length); - - target = baseToString(target); - return string.slice(position, position + target.length) == target; - } - - /** - * Creates a compiled template function that can interpolate data properties - * in "interpolate" delimiters, HTML-escape interpolated data properties in - * "escape" delimiters, and execute JavaScript in "evaluate" delimiters. Data - * properties may be accessed as free variables in the template. If a setting - * object is given, it takes precedence over `_.templateSettings` values. - * - * **Note:** In the development build `_.template` utilizes - * [sourceURLs](http://www.html5rocks.com/en/tutorials/developertools/sourcemaps/#toc-sourceurl) - * for easier debugging. - * - * For more information on precompiling templates see - * [lodash's custom builds documentation](https://lodash.com/custom-builds). - * - * For more information on Chrome extension sandboxes see - * [Chrome's extensions documentation](https://developer.chrome.com/extensions/sandboxingEval). - * - * @static - * @since 0.1.0 - * @memberOf _ - * @category String - * @param {string} [string=''] The template string. - * @param {Object} [options={}] The options object. - * @param {RegExp} [options.escape=_.templateSettings.escape] - * The HTML "escape" delimiter. - * @param {RegExp} [options.evaluate=_.templateSettings.evaluate] - * The "evaluate" delimiter. - * @param {Object} [options.imports=_.templateSettings.imports] - * An object to import into the template as free variables. - * @param {RegExp} [options.interpolate=_.templateSettings.interpolate] - * The "interpolate" delimiter. - * @param {string} [options.sourceURL='lodash.templateSources[n]'] - * The sourceURL of the compiled template. - * @param {string} [options.variable='obj'] - * The data object variable name. - * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`. - * @returns {Function} Returns the compiled template function. - * @example - * - * // Use the "interpolate" delimiter to create a compiled template. - * var compiled = _.template('hello <%= user %>!'); - * compiled({ 'user': 'fred' }); - * // => 'hello fred!' - * - * // Use the HTML "escape" delimiter to escape data property values. - * var compiled = _.template('<%- value %>'); - * compiled({ 'value': '