├── .gitignore ├── .swift-version ├── .swiftpm └── xcode │ ├── package.xcworkspace │ └── contents.xcworkspacedata │ └── xcshareddata │ └── xcschemes │ └── Cryptex.xcscheme ├── Cartfile ├── Cartfile.resolved ├── Cryptex.podspec ├── Cryptex.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ └── Cryptex.xcscheme ├── Cryptex.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── Cryptex ├── Cryptex.h └── Info.plist ├── CryptexTests ├── CryptexTests.swift └── Info.plist ├── LICENSE ├── Package.swift ├── README.md ├── SampleAppUI.png ├── Sources ├── Binance.swift ├── BitGrail.swift ├── Bitfinex.swift ├── CoinExchange.swift ├── CoinMarketCap.swift ├── Common │ ├── APIType.swift │ ├── Balance.swift │ ├── Currency.swift │ ├── CurrencyPair.swift │ ├── Enums.swift │ ├── ExchangeDataStore.swift │ ├── Extensions.swift │ ├── MockURLSession.swift │ ├── Network.swift │ ├── Protocols.swift │ ├── Ticker.swift │ └── UserPreference.swift ├── Cryptopia.swift ├── GDAX.swift ├── Gemini.swift ├── Koinex.swift ├── Kraken.swift └── Poloniex.swift ├── Tests ├── CryptexTests │ └── CryptexTests.swift └── LinuxMain.swift ├── UI ├── CryptEx │ ├── API │ │ ├── API.swift │ │ └── Services.swift │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ ├── Contents.json │ │ ├── Pixel.imageset │ │ │ ├── Contents.json │ │ │ └── Pixel.png │ │ └── TransparentPixel.imageset │ │ │ ├── Contents.json │ │ │ └── TransparentPixel.png │ ├── BackgroundServices │ │ └── BackgroundService.swift │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ ├── CryptExUI-Bridging-Header.h │ ├── Info.plist │ ├── TickerCell.swift │ ├── TickerCell.xib │ └── ViewController │ │ ├── All │ │ └── AllBalancesVC.swift │ │ ├── BalancesVC.swift │ │ ├── ExchangeVC.swift │ │ ├── Gemini │ │ └── GeminiPastTradesVC.swift │ │ ├── Poloniex │ │ ├── PoloniexDepositsWithdrawalsVC.swift │ │ └── PoloniexTradeHistoryVC.swift │ │ ├── RefreshableTableVC.swift │ │ ├── Settings │ │ └── NotificationSettingsVC.swift │ │ └── TickersVC.swift ├── CryptExUI.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ └── contents.xcworkspacedata ├── CryptExUI.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── Podfile └── docs └── index.html /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .DS_Store 3 | .build 4 | 5 | Package.resolved 6 | 7 | UI/Pods 8 | UI/Podfile.lock 9 | **/xcuserdata/**/* 10 | 11 | # Carthage 12 | Carthage 13 | 14 | -------------------------------------------------------------------------------- /.swift-version: -------------------------------------------------------------------------------- 1 | 5.0 2 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/Cryptex.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 47 | 53 | 54 | 55 | 56 | 57 | 67 | 68 | 74 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /Cartfile: -------------------------------------------------------------------------------- 1 | github "krzyzanowskim/CryptoSwift" 2 | -------------------------------------------------------------------------------- /Cartfile.resolved: -------------------------------------------------------------------------------- 1 | github "krzyzanowskim/CryptoSwift" 2 | -------------------------------------------------------------------------------- /Cryptex.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "Cryptex" 3 | s.version = "0.0.6" 4 | s.summary = "Cryptocurrency Exchange API Clients in Swift." 5 | s.description = <<-DESC 6 | Multiple crypto currency exchange api clients in swift. 7 | DESC 8 | s.homepage = "https://github.com/trsathya/cryptex" 9 | s.license = "MIT" 10 | s.authors = 'Sathyakumar Rajaraman', 'Mathias Klenk', 'Rob Saunders' 11 | s.source = { :git => "https://github.com/trsathya/cryptex.git", :tag => "#{s.version}" } 12 | s.ios.deployment_target = "8.0" 13 | s.osx.deployment_target = "10.12" 14 | s.watchos.deployment_target = "2.0" 15 | s.tvos.deployment_target = "9.0" 16 | s.requires_arc = true 17 | s.dependency "CryptoSwift" 18 | s.default_subspec = "All" 19 | s.subspec "All" do |ss| 20 | ss.dependency 'Cryptex/CoinMarketCap' 21 | ss.dependency 'Cryptex/Gemini' 22 | ss.dependency 'Cryptex/GDAX' 23 | ss.dependency 'Cryptex/Poloniex' 24 | ss.dependency 'Cryptex/Binance' 25 | ss.dependency 'Cryptex/Koinex' 26 | ss.dependency 'Cryptex/Cryptopia' 27 | ss.dependency 'Cryptex/BitGrail' 28 | ss.dependency 'Cryptex/CoinExchange' 29 | ss.dependency 'Cryptex/Bitfinex' 30 | ss.dependency 'Cryptex/Kraken' 31 | end 32 | s.subspec "Common" do |ss| 33 | ss.source_files = "Sources/Common/**/*.swift" 34 | end 35 | s.subspec "CoinMarketCap" do |ss| 36 | ss.source_files = "Sources/CoinMarketCap.swift" 37 | ss.dependency 'Cryptex/Common' 38 | end 39 | s.subspec "Gemini" do |ss| 40 | ss.source_files = "Sources/Gemini.swift" 41 | ss.dependency 'Cryptex/Common' 42 | end 43 | s.subspec "GDAX" do |ss| 44 | ss.source_files = "Sources/GDAX.swift" 45 | ss.dependency 'Cryptex/Common' 46 | end 47 | s.subspec "Poloniex" do |ss| 48 | ss.source_files = "Sources/Poloniex.swift" 49 | ss.dependency 'Cryptex/Common' 50 | end 51 | s.subspec "Binance" do |ss| 52 | ss.source_files = "Sources/Binance.swift" 53 | ss.dependency 'Cryptex/Common' 54 | end 55 | s.subspec "Koinex" do |ss| 56 | ss.source_files = "Sources/Koinex.swift" 57 | ss.dependency 'Cryptex/Common' 58 | end 59 | s.subspec "Cryptopia" do |ss| 60 | ss.source_files = "Sources/Cryptopia.swift" 61 | ss.dependency 'Cryptex/Common' 62 | end 63 | s.subspec "BitGrail" do |ss| 64 | ss.source_files = "Sources/BitGrail.swift" 65 | ss.dependency 'Cryptex/Common' 66 | end 67 | s.subspec "CoinExchange" do |ss| 68 | ss.source_files = "Sources/CoinExchange.swift" 69 | ss.dependency 'Cryptex/Common' 70 | end 71 | s.subspec "Bitfinex" do |ss| 72 | ss.source_files = "Sources/Bitfinex.swift" 73 | ss.dependency 'Cryptex/Common' 74 | end 75 | s.subspec "Kraken" do |ss| 76 | ss.source_files = "Sources/Kraken.swift" 77 | ss.dependency 'Cryptex/Common' 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /Cryptex.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Cryptex.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Cryptex.xcodeproj/xcshareddata/xcschemes/Cryptex.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 49 | 50 | 51 | 52 | 53 | 54 | 64 | 65 | 71 | 72 | 73 | 74 | 75 | 76 | 82 | 83 | 89 | 90 | 91 | 92 | 94 | 95 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /Cryptex.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Cryptex.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Cryptex/Cryptex.h: -------------------------------------------------------------------------------- 1 | // 2 | // Cryptex.h 3 | // Cryptex 4 | // 5 | // Created by Sathya on 4/22/18. 6 | // Copyright © 2018 Sathya. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for Cryptex. 12 | FOUNDATION_EXPORT double CryptexVersionNumber; 13 | 14 | //! Project version string for Cryptex. 15 | FOUNDATION_EXPORT const unsigned char CryptexVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /Cryptex/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSPrincipalClass 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /CryptexTests/CryptexTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CryptexTests.swift 3 | // CryptexTests 4 | // 5 | // Created by Sathya on 4/22/18. 6 | // Copyright © 2018 Sathya. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Cryptex 11 | 12 | class CryptexTests: XCTestCase { 13 | 14 | override func setUp() { 15 | super.setUp() 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | } 18 | 19 | override func tearDown() { 20 | // Put teardown code here. This method is called after the invocation of each test method in the class. 21 | super.tearDown() 22 | } 23 | 24 | func testExample() { 25 | // This is an example of a functional test case. 26 | // Use XCTAssert and related functions to verify your tests produce the correct results. 27 | } 28 | 29 | func testPerformanceExample() { 30 | // This is an example of a performance test case. 31 | self.measure { 32 | // Put the code you want to measure the time of here. 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /CryptexTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Sathyakumar Rajaraman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.0 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "Cryptex", 8 | platforms: [ 9 | .iOS(.v10), 10 | .macOS(.v10_12), 11 | .watchOS(.v4) 12 | ], 13 | products: [ 14 | // Products define the executables and libraries produced by a package, and make them visible to other packages. 15 | .library( 16 | name: "Cryptex", 17 | targets: ["Cryptex"]), 18 | ], 19 | dependencies: [ 20 | // Dependencies declare other packages that this package depends on. 21 | .package(url: "https://github.com/krzyzanowskim/CryptoSwift.git", from: "1.4.0") 22 | ], 23 | targets: [ 24 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 25 | // Targets can depend on other targets in this package, and on products in packages which this package depends on. 26 | .target( 27 | name: "Cryptex", 28 | dependencies: ["CryptoSwift"], path: ".", exclude: ["Tests", "CryptexTests"]), 29 | .testTarget( 30 | name: "CryptexTests", 31 | dependencies: ["Cryptex"]), 32 | ], 33 | swiftLanguageVersions: [SwiftVersion.v5] 34 | ) 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cryptex - iOS SDK for crypto currencies in Swift 4 2 | 3 | ![Swift 4.2](https://img.shields.io/badge/Swift-4.2-brightgreen.svg) ![CocoaPods](https://img.shields.io/cocoapods/v/Cryptex.svg) [![GitHub release](https://img.shields.io/github/release/trsathya/Cryptex.svg)](https://github.com/trsathya/Cryptex/releases) ![Github Commits Since last release](https://img.shields.io/github/commits-since/trsathya/Cryptex/latest.svg) ![badge-mit] 4 | ![badge-platforms] ![badge-pms] 5 | 6 | Cryptex, a single Swift 4 library and an iOS app to watch prices and check realtime account balances across multiple cryptocurrency exchanges. Trading features are coming soon. 7 | 8 | ![Sample App UI](SampleAppUI.png) 9 | 10 | ## Requirements 11 | 12 | - iOS 9.0+ | macOS 10.10+ | tvOS 9.0+ | watchOS 2.0+ 13 | - Xcode 8.3+ 14 | 15 | ## Integration 16 | 17 | #### CocoaPods (iOS 9+, OS X 10.9+) 18 | 19 | To install all exchanges 20 | ```ruby 21 | pod 'Cryptex', '~> 0.0.6' 22 | ``` 23 | 24 | To install only one exchange 25 | ```ruby 26 | pod 'Cryptex/Gemini', '~> 0.0.6' 27 | ``` 28 | 29 | To install two or more exchanges 30 | ```ruby 31 | pod 'Cryptex', '~> 0.0.6', :subspecs => ['Gemini', 'GDAX', "Poloniex"] 32 | ``` 33 | 34 | #### Carthage (iOS 8+, OS X 10.9+) 35 | 36 | ``` 37 | github "trsathya/Cryptex" ~> 0.0.6 38 | ``` 39 | 40 | #### Swift Package Manager 41 | 42 | ```swift 43 | dependencies: [ 44 | .Package(url: "https://github.com/trsathya/Cryptex", from: "0.0.6"), 45 | ] 46 | ``` 47 | 48 | ## Usage 49 | 50 | #### Initialization 51 | 52 | ```swift 53 | import Cryptex 54 | ``` 55 | 56 | ##### Fetch coinmarketcap.com global data 57 | ```swift 58 | let coinMarketCapService = CoinMarketCap.Service(key: nil, secret: nil, session: URLSession.shared, userPreference: .USD_BTC, currencyOverrides: nil) 59 | coinMarketCapService.getGlobal { (_) in 60 | if let data = coinMarketCapService.store.globalMarketDataResponse.globalData { 61 | print(data) 62 | } 63 | } 64 | ``` 65 | 66 | ##### Console logs 67 | ``` 68 | GET https://api.coinmarketcap.com/v1/global 69 | 200 https://api.coinmarketcap.com/v1/global/ 70 | Response Data: { 71 | "total_market_cap_usd": 585234214361.0, 72 | "total_24h_volume_usd": 22202189284.0, 73 | "bitcoin_percentage_of_market_cap": 34.15, 74 | "active_currencies": 896, 75 | "active_assets": 567, 76 | "active_markets": 8187, 77 | "last_updated": 1517118863 78 | } 79 | Optional(Cryptex.CoinMarketCap.GlobalMarketData(marketCap: 585234214361, volume24Hrs: 22202189284, bitcoinDominance: 34.15, activeCurrencies: 896, activeAssets: 567, activeMarkets: 8187, lastUpdated: 1517118863)) 80 | 81 | ``` 82 | Or 83 | 84 | ##### Fetch Gemini public ticker data 85 | ```swift 86 | let geminiService = Gemini.Service(key: nil, secret: nil, session: URLSession.shared, userPreference: .USD_BTC, currencyOverrides: nil) 87 | geminiService.getTickers { (_) in 88 | print(geminiService.store.tickerByName) 89 | } 90 | ``` 91 | ##### Console logs 92 | ``` 93 | GET https://api.gemini.com/v1/symbols 94 | 200 https://api.gemini.com/v1/symbols 95 | GET https://api.gemini.com/v1/pubticker/BTCUSD 96 | GET https://api.gemini.com/v1/pubticker/ETHBTC 97 | GET https://api.gemini.com/v1/pubticker/ETHUSD 98 | 200 https://api.gemini.com/v1/pubticker/ETHBTC 99 | 200 https://api.gemini.com/v1/pubticker/ETHUSD 100 | 200 https://api.gemini.com/v1/pubticker/BTCUSD 101 | [ 102 | BTCUSD : 11721 USD, 103 | ETHBTC : 0.0977 BTC, 104 | ETHUSD : 1148.99 USD] 105 | ``` 106 | Or 107 | ##### Fetch Gemini private account balance data 108 | ```swift 109 | let geminiService = Gemini.Service(key: , secret: , session: URLSession.shared, userPreference: .USD_BTC, currencyOverrides: nil) 110 | geminiService.getBalances { (_) in 111 | for balance in self.gemini.store.balances { 112 | print("\(balance) \(self.gemini.store.balanceInPreferredCurrency(balance: balance).usdFormatted ?? "")") 113 | } 114 | } 115 | ``` 116 | ##### Console logs 117 | ``` 118 | GET https://api.gemini.com/v1/symbols 119 | 200 https://api.gemini.com/v1/symbols 120 | GET https://api.gemini.com/v1/pubticker/BTCUSD 121 | GET https://api.gemini.com/v1/pubticker/ETHBTC 122 | GET https://api.gemini.com/v1/pubticker/ETHUSD 123 | 200 https://api.gemini.com/v1/pubticker/BTCUSD 124 | 200 https://api.gemini.com/v1/pubticker/ETHUSD 125 | 200 https://api.gemini.com/v1/pubticker/ETHBTC 126 | POST https://api.gemini.com/v1/balances 127 | 200 https://api.gemini.com/v1/balances 128 | 129 | BTC: 0.29182653 $3,420.49 130 | USD: 26.96 $26.96 131 | ETH: 0.00000017 $0.00 132 | ``` 133 | 134 | **Note:** While creating Binance service, pass a currency override array to resolve a currency code difference. This is because Binance chose to use the code BCC for BitcoinCash instead of BCH. 135 | ```swift 136 | let currencyOverrides = ["BCC": Currency(name: "Bitcoin Cash", code: "BCC")] 137 | let binanceService = Binance.Service(key: key, secret: secret, session: session, userPreference: .USDT_BTC, currencyOverrides: currencyOverrides) 138 | ``` 139 | 140 | [badge-pms]: https://img.shields.io/badge/supports-CocoaPods%20%7C%20Carthage%20%7C%20SwiftPM-green.svg 141 | [badge-platforms]: https://img.shields.io/badge/platforms-macOS%20%7C%20iOS%20%7C%20watchOS%20%7C%20tvOS%20%7C%20Linux-lightgrey.svg 142 | [badge-mit]: https://img.shields.io/badge/license-MIT-blue.svg 143 | -------------------------------------------------------------------------------- /SampleAppUI.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trsathya/Cryptex/541d9274b689384120bfcd2aa7469648f3f70e3b/SampleAppUI.png -------------------------------------------------------------------------------- /Sources/Binance.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Binance.swift 3 | // Cryptex 4 | // 5 | // Created by Sathyakumar Rajaraman on 12/31/17. 6 | // Copyright © 2017 Sathyakumar. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import CryptoSwift 11 | 12 | public struct Binance { 13 | 14 | public struct Account { 15 | public let makerCommission: NSDecimalNumber 16 | public let takerCommission: NSDecimalNumber 17 | public let buyerCommission: NSDecimalNumber 18 | public let sellerCommission: NSDecimalNumber 19 | public let canTrade: Bool 20 | public let canWithdraw: Bool 21 | public let canDeposit: Bool 22 | public let balances: [Balance] 23 | 24 | public init?(json: [String: Any], currencyStore: CurrencyStoreType) { 25 | makerCommission = NSDecimalNumber(json["makerCommission"]) 26 | takerCommission = NSDecimalNumber(json["takerCommission"]) 27 | buyerCommission = NSDecimalNumber(json["buyerCommission"]) 28 | sellerCommission = NSDecimalNumber(json["sellerCommission"]) 29 | canTrade = json["canTrade"] as? Bool ?? false 30 | canWithdraw = json["canWithdraw"] as? Bool ?? false 31 | canDeposit = json["canDeposit"] as? Bool ?? false 32 | if let balancesJSON = json["balances"] as? [[String: String]] { 33 | balances = balancesJSON.compactMap { Balance(json: $0, currencyStore: currencyStore) } 34 | } else { 35 | balances = [] 36 | } 37 | } 38 | 39 | public class Balance: Cryptex.Balance { 40 | public var locked: NSDecimalNumber 41 | 42 | public init?(json: [String: String], currencyStore: CurrencyStoreType) { 43 | guard 44 | let freeString = json["free"] 45 | , let lockedString = json["locked"] 46 | , freeString != "0.00000000" || lockedString != "0.00000000" 47 | else { return nil } 48 | locked = NSDecimalNumber(string: lockedString) 49 | super.init(currency: currencyStore.forCode(json["asset"] ?? ""), quantity: NSDecimalNumber(string: freeString)) 50 | } 51 | } 52 | } 53 | 54 | public class Store: ExchangeDataStore { 55 | 56 | override fileprivate init() { 57 | super.init() 58 | name = "Binance" 59 | accountingCurrency = .USDT 60 | } 61 | 62 | public var tickersResponse: HTTPURLResponse? = nil 63 | public var accountResponse: (response: HTTPURLResponse?, account: Binance.Account?) = (nil, nil) 64 | } 65 | 66 | public enum API { 67 | case getAllPrices 68 | case account 69 | } 70 | 71 | public class Service: Network, TickerServiceType { 72 | 73 | public let store = Store() 74 | 75 | public func getTickers(completion: @escaping (ResponseType) -> Void) { 76 | let apiType = Binance.API.getAllPrices 77 | if apiType.checkInterval(response: store.tickersResponse) { 78 | completion(.cached) 79 | } else { 80 | binanceDataTaskFor(api: apiType, completion: { (response) in 81 | guard 82 | let tickerArray = response.json as? [[String: String]] 83 | else { 84 | print("Error: Cast Failed in \(#function)") 85 | return 86 | } 87 | 88 | var tickers: [Ticker] = [] 89 | for ticker in tickerArray { 90 | let currencyPair = CurrencyPair(symbol: ticker["symbol"] ?? "", currencyStore: self) 91 | let price = NSDecimalNumber(string: ticker["price"]) 92 | let ticker = Ticker(symbol: currencyPair, price: price) 93 | tickers.append(ticker) 94 | } 95 | self.store.setTickersInDictionary(tickers: tickers) 96 | self.store.tickersResponse = response.httpResponse 97 | completion(.fetched) 98 | 99 | }).resume() 100 | } 101 | } 102 | 103 | public func getAccount(completion: @escaping (ResponseType) -> Void) { 104 | let apiType = Binance.API.account 105 | if apiType.checkInterval(response: store.accountResponse.response) { 106 | completion(.cached) 107 | } else { 108 | binanceDataTaskFor(api: apiType) { (response) in 109 | guard let json = response.json as? [String: Any] else { 110 | print("Error: Cast Failed in \(#function)") 111 | return 112 | } 113 | let account = Binance.Account(json: json, currencyStore: self) 114 | if let balances = account?.balances { 115 | self.store.balances = balances 116 | } 117 | self.store.accountResponse = (response.httpResponse, account) 118 | completion(.fetched) 119 | }.resume() 120 | } 121 | } 122 | 123 | func binanceDataTaskFor(api: APIType, completion: ((Response) -> Void)?) -> URLSessionDataTask { 124 | return dataTaskFor(api: api) { (response) in 125 | // Handle error here 126 | completion?(response) 127 | } 128 | } 129 | 130 | public override func requestFor(api: APIType) -> NSMutableURLRequest { 131 | let mutableURLRequest = api.mutableRequest 132 | 133 | if let key = key, let secret = secret, api.authenticated { 134 | 135 | var postData = api.postData 136 | postData["recvWindow"] = "5000" 137 | postData["timestamp"] = "\(Int(Date().timeIntervalSince1970 * 1000))" 138 | 139 | if let hmac_sha = try? HMAC(key: secret, variant: .sha256).authenticate(Array(postData.queryString.utf8)) { 140 | let signature = Data(bytes: hmac_sha).toHexString() 141 | postData["signature"] = signature 142 | } 143 | 144 | var postDataString = "" 145 | if let data = postData.data, let string = data.string, postData.count > 0 { 146 | 147 | postDataString = string 148 | 149 | // POST payload 150 | if case .POST = api.httpMethod { 151 | mutableURLRequest.httpBody = data 152 | } else if case .GET = api.httpMethod { 153 | var urlString = mutableURLRequest.url?.absoluteString 154 | urlString?.append("?") 155 | urlString?.append(postData.queryString) 156 | let url = URL(string: urlString!) 157 | mutableURLRequest.url = url 158 | } 159 | 160 | api.print("Request Data: \(postDataString)", content: .response) 161 | } 162 | mutableURLRequest.setValue(key, forHTTPHeaderField: "X-MBX-APIKEY") 163 | } 164 | 165 | return mutableURLRequest 166 | } 167 | } 168 | } 169 | 170 | extension Binance.API: APIType { 171 | public var host: String { 172 | return "https://api.binance.com/api" 173 | } 174 | 175 | public var path: String { 176 | switch self { 177 | case .getAllPrices: return "/v1/ticker/allPrices" 178 | case .account: return "/v3/account" 179 | } 180 | } 181 | 182 | public var httpMethod: HttpMethod { 183 | return .GET 184 | } 185 | 186 | public var authenticated: Bool { 187 | switch self { 188 | case .getAllPrices: return false 189 | case .account: return true 190 | } 191 | } 192 | 193 | public var loggingEnabled: LogLevel { 194 | switch self { 195 | case .getAllPrices: return .url 196 | case .account: return .url 197 | } 198 | } 199 | 200 | public var postData: [String: String] { 201 | return [:] 202 | } 203 | 204 | public var refetchInterval: TimeInterval { 205 | switch self { 206 | case .getAllPrices: return .aMinute 207 | case .account: return .aMinute 208 | } 209 | } 210 | } 211 | 212 | extension Binance.Service: BalanceServiceType { 213 | 214 | public func getBalances(completion: @escaping ( ResponseType) -> Void) { 215 | getTickers(completion: { (_) in 216 | self.getAccount(completion: { (response) in 217 | completion(response) 218 | }) 219 | }) 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /Sources/BitGrail.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BitGrail.swift 3 | // Cryptex 4 | // 5 | // Created by Sathyakumar Rajaraman on 1/2/18. 6 | // 7 | 8 | import Foundation 9 | import CryptoSwift 10 | 11 | extension CurrencyPair { 12 | 13 | var bitGrailLabel: String { 14 | return quantity.code + "/" + price.code 15 | } 16 | 17 | convenience init(bitGrailLabel: String, currencyStore: CurrencyStoreType) { 18 | let currencySymbols = bitGrailLabel.components(separatedBy: "/") 19 | let quantity = currencyStore.forCode(currencySymbols[0]) 20 | let price = currencyStore.forCode(currencySymbols[1]) 21 | self.init(quantity: quantity, price: price) 22 | } 23 | } 24 | 25 | public struct BitGrail { 26 | 27 | public class Market: Ticker { 28 | public var market: String = "" 29 | public var last = NSDecimalNumber.zero 30 | public var high = NSDecimalNumber.zero 31 | public var low = NSDecimalNumber.zero 32 | public var volume = NSDecimalNumber.zero 33 | public var coinVolume = NSDecimalNumber.zero 34 | public var bid = NSDecimalNumber.zero 35 | public var ask = NSDecimalNumber.zero 36 | 37 | public init(json: [String: Any], currencyPair: CurrencyPair) { 38 | 39 | market = json["market"] as? String ?? "" 40 | last = NSDecimalNumber(json["last"]) 41 | high = NSDecimalNumber(json["High"]) 42 | low = NSDecimalNumber(json["Low"]) 43 | volume = NSDecimalNumber(json["Volume"]) 44 | coinVolume = NSDecimalNumber(json["coinVolume"]) 45 | ask = NSDecimalNumber(json["ask"]) 46 | bid = NSDecimalNumber(json["bid"]) 47 | super.init(symbol: currencyPair, price: last) 48 | } 49 | } 50 | 51 | public struct MarketHistory { 52 | public let tradePairId: Int 53 | public let label: String 54 | public let type: String 55 | public let price: NSDecimalNumber 56 | public let amount: NSDecimalNumber 57 | public let total: NSDecimalNumber 58 | public let timestamp: TimeInterval 59 | } 60 | 61 | public class Balance: Cryptex.Balance { 62 | public let reserved: NSDecimalNumber 63 | 64 | public init(json: [String: String], currency: Currency) { 65 | reserved = NSDecimalNumber(json["reserved"]) 66 | super.init(currency: currency, quantity: NSDecimalNumber(json["balance"])) 67 | } 68 | } 69 | 70 | public enum API { 71 | case getMarkets 72 | case getBalance 73 | } 74 | 75 | public class Store: ExchangeDataStore { 76 | 77 | override fileprivate init() { 78 | super.init() 79 | name = "BitGrail" 80 | accountingCurrency = .Bitcoin 81 | } 82 | 83 | public var tickersResponse: HTTPURLResponse? = nil 84 | public var balanceResponse: HTTPURLResponse? = nil 85 | } 86 | 87 | public class Service: Network, TickerServiceType, BalanceServiceType { 88 | public let store = Store() 89 | 90 | public func getTickers(completion: @escaping (ResponseType) -> Void) { 91 | let apiType = BitGrail.API.getMarkets 92 | if apiType.checkInterval(response: store.tickersResponse) { 93 | completion(.cached) 94 | } else { 95 | bitGrailDataTaskFor(api: apiType) { (response) in 96 | guard let markets = response.json as? [String: Any] else { return } 97 | 98 | var tickers: [Market] = [] 99 | markets.forEach({ (keyValue) in 100 | if let tickersDictionary = keyValue.value as? [String: Any], let markets = tickersDictionary["markets"] as? [String: [String: String]] { 101 | for market in markets { 102 | let currencyPair = CurrencyPair(bitGrailLabel: market.key, currencyStore: self) 103 | tickers.append(Market(json: market.value, currencyPair: currencyPair)) 104 | } 105 | } 106 | }) 107 | self.store.setTickersInDictionary(tickers: tickers) 108 | self.store.tickersResponse = response.httpResponse 109 | completion(.fetched) 110 | }.resume() 111 | } 112 | } 113 | 114 | public func getBalances(completion: @escaping (ResponseType) -> Void) { 115 | let apiType = BitGrail.API.getBalance 116 | 117 | if apiType.checkInterval(response: store.balanceResponse) { 118 | 119 | completion(.cached) 120 | 121 | } else { 122 | 123 | getTickers(completion: { (_) in 124 | self.bitGrailDataTaskFor(api: apiType) { (response) in 125 | guard let balancesJSON = response.json as? [String: Any] else { return } 126 | 127 | var balances: [Balance] = [] 128 | balancesJSON.forEach({ (arg) in 129 | guard let value = arg.value as? [String: String] else { return } 130 | let currency = self.forCode(arg.key) 131 | let balance = Balance(json: value, currency: currency) 132 | if balance.quantity != .zero { 133 | balances.append(balance) 134 | } 135 | }) 136 | 137 | self.store.balances = balances 138 | self.store.balanceResponse = response.httpResponse 139 | completion(.fetched) 140 | 141 | }.resume() 142 | }) 143 | } 144 | } 145 | 146 | func bitGrailDataTaskFor(api: APIType, completion: ((Response) -> Void)?) -> URLSessionDataTask { 147 | return dataTaskFor(api: api) { (response) in 148 | guard let json = response.json as? [String: Any] else { return } 149 | if let success = json["success"] as? Int, let jsonData = json["response"], success == 1 { 150 | var tempResponse = response 151 | tempResponse.json = jsonData 152 | completion?(tempResponse) 153 | } else { 154 | api.print(response.string, content: .response) 155 | } 156 | } 157 | } 158 | 159 | public override func requestFor(api: APIType) -> NSMutableURLRequest { 160 | let mutableURLRequest = api.mutableRequest 161 | if let key = key, let secret = secret, api.authenticated { 162 | var postData = api.postData 163 | postData["nonce"] = "\(Int(Date().timeIntervalSince1970 * 1000))" 164 | let requestString = postData.queryString 165 | api.print("Request Data: \(requestString)", content: .response) 166 | // POST payload 167 | let requestData = Array(requestString.utf8) 168 | if case .POST = api.httpMethod { 169 | mutableURLRequest.httpBody = requestString.utf8Data() 170 | } 171 | 172 | if let hmac_sha512 = try? HMAC(key: Array(secret.utf8), variant: .sha512).authenticate(requestData) { 173 | mutableURLRequest.setValue(hmac_sha512.toHexString(), forHTTPHeaderField: "SIGNATURE") 174 | } 175 | mutableURLRequest.setValue(key, forHTTPHeaderField: "KEY") 176 | } 177 | return mutableURLRequest 178 | } 179 | } 180 | } 181 | 182 | extension BitGrail.API: APIType { 183 | public var host: String { 184 | return "https://api.bitgrail.com/" 185 | } 186 | 187 | public var path: String { 188 | switch self { 189 | case .getMarkets: return "v1/markets" 190 | case .getBalance: return "v1/balances" 191 | } 192 | } 193 | 194 | public var httpMethod: HttpMethod { 195 | switch self { 196 | case .getMarkets: return .GET 197 | case .getBalance: return .POST 198 | } 199 | } 200 | 201 | public var authenticated: Bool { 202 | switch self { 203 | case .getMarkets: return false 204 | case .getBalance: return true 205 | } 206 | } 207 | 208 | public var loggingEnabled: LogLevel { 209 | switch self { 210 | case .getMarkets: return .response 211 | case .getBalance: return .response 212 | } 213 | } 214 | 215 | public var postData: [String : String] { 216 | switch self { 217 | case .getMarkets: return [:] 218 | case .getBalance: return [:] 219 | } 220 | } 221 | 222 | public var refetchInterval: TimeInterval { 223 | switch self { 224 | case .getMarkets: return .aMinute 225 | case .getBalance: return .aMinute 226 | } 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /Sources/Bitfinex.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Bitfinex.swift 3 | // Cryptex 4 | // 5 | // Created by Sathyakumar Rajaraman on 12/31/17. 6 | // Copyright © 2017 Sathyakumar. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import CryptoSwift 11 | 12 | public struct Bitfinex { 13 | 14 | public class Ticker: Cryptex.Ticker { 15 | public let mid: NSDecimalNumber 16 | public let bid: NSDecimalNumber 17 | public let ask: NSDecimalNumber 18 | public let lastPrice: NSDecimalNumber 19 | public let low: NSDecimalNumber 20 | public let high: NSDecimalNumber 21 | public let volume: NSDecimalNumber 22 | public let timestamp: NSDecimalNumber 23 | 24 | public init(json: [String: String], for symbol: CurrencyPair) { 25 | mid = NSDecimalNumber(json["mid"]) 26 | bid = NSDecimalNumber(json["bid"]) 27 | ask = NSDecimalNumber(json["ask"]) 28 | lastPrice = NSDecimalNumber(json["last_price"]) 29 | low = NSDecimalNumber(json["low"]) 30 | high = NSDecimalNumber(json["high"]) 31 | volume = NSDecimalNumber(json["volume"]) 32 | timestamp = NSDecimalNumber(json["timestamp"]) 33 | super.init(symbol: symbol, price: lastPrice) 34 | } 35 | } 36 | 37 | public class Balance: Cryptex.Balance { 38 | 39 | public let type: String 40 | public let amount: NSDecimalNumber 41 | public let available: NSDecimalNumber 42 | 43 | public init(json: [String: String], currencyStore: CurrencyStoreType) { 44 | type = json["type"] ?? "" 45 | amount = NSDecimalNumber(json["amount"]) 46 | available = NSDecimalNumber(json["available"]) 47 | super.init(currency: currencyStore.forCode(json["currency"] ?? ""), quantity: available) 48 | } 49 | } 50 | 51 | public class Store: ExchangeDataStore { 52 | 53 | override fileprivate init() { 54 | super.init() 55 | name = "Bitfinex" 56 | accountingCurrency = .USD 57 | } 58 | 59 | public var symbolsResponse: (response: HTTPURLResponse?, symbols: [CurrencyPair]) = (nil, []) 60 | public var tickerResponse: [String: HTTPURLResponse] = [:] 61 | public var balanceResponse: HTTPURLResponse? = nil 62 | public var accountFeesResponse: HTTPURLResponse? = nil 63 | } 64 | 65 | public enum API { 66 | case symbols 67 | case ticker(String) 68 | case balances 69 | } 70 | 71 | public class Service: Network { 72 | 73 | public let store = Store() 74 | 75 | public func getSymbols(completion: @escaping (ResponseType) -> Void) { 76 | let apiType = Bitfinex.API.symbols 77 | if apiType.checkInterval(response: store.symbolsResponse.response) { 78 | completion(.cached) 79 | } else { 80 | bitfinexDataTaskFor(api: apiType, completion: { (response) in 81 | guard let stringArray = response.json as? [String] else { 82 | completion(.unexpected(response)) 83 | return 84 | } 85 | let geminiSymbols = stringArray.compactMap { CurrencyPair(symbol: $0, currencyStore: self) } 86 | self.store.symbolsResponse = (response.httpResponse, geminiSymbols) 87 | completion(.fetched) 88 | }).resume() 89 | } 90 | } 91 | 92 | public func getTicker(symbol: CurrencyPair, completion: @escaping (CurrencyPair, ResponseType) -> Void) { 93 | let apiType = Bitfinex.API.ticker(symbol.displaySymbol) 94 | if apiType.checkInterval(response: store.tickerResponse[symbol.displaySymbol]) { 95 | completion(symbol, .cached) 96 | } else { 97 | bitfinexDataTaskFor(api: apiType, completion: { (response) in 98 | guard let json = response.json as? [String: String] else { 99 | completion(symbol, .unexpected(response)) 100 | return 101 | } 102 | self.store.setTicker(ticker: Ticker(json: json, for: symbol), symbol: symbol.displaySymbol) 103 | self.store.tickerResponse[symbol.displaySymbol] = response.httpResponse 104 | completion(symbol, .fetched) 105 | }).resume() 106 | } 107 | } 108 | 109 | public func getAccountBalances(completion: @escaping (ResponseType) -> Void) { 110 | let apiType = Bitfinex.API.balances 111 | if apiType.checkInterval(response: store.balanceResponse) { 112 | completion(.cached) 113 | } else { 114 | bitfinexDataTaskFor(api: apiType) { (response) in 115 | guard let json = response.json as? [[String: String]] else { 116 | print("Error: Cast Failed in \(#function)") 117 | return 118 | } 119 | var balances: [Balance] = [] 120 | json.forEach({ (dictionary) in 121 | balances.append(Balance(json: dictionary, currencyStore: self)) 122 | }) 123 | self.store.balances = balances 124 | self.store.balanceResponse = response.httpResponse 125 | completion(.fetched) 126 | }.resume() 127 | } 128 | } 129 | 130 | private func bitfinexDataTaskFor(api: APIType, completion: ((Response) -> Void)?) -> URLSessionDataTask { 131 | return dataTaskFor(api: api) { (response) in 132 | // Handle error here 133 | completion?(response) 134 | } 135 | } 136 | 137 | public override func requestFor(api: APIType) -> NSMutableURLRequest { 138 | let mutableURLRequest = api.mutableRequest 139 | 140 | if let key = key, let secret = secret, api.authenticated { 141 | 142 | var postDataDictionary = api.postData 143 | postDataDictionary["request"] = api.path 144 | postDataDictionary["nonce"] = "\(getTimestampInSeconds())" // String nonce 145 | 146 | var postDataString = "" 147 | if let data = postDataDictionary.data { 148 | postDataString = data.base64EncodedString() 149 | } 150 | 151 | mutableURLRequest.setValue(postDataString, forHTTPHeaderField: "X-BFX-PAYLOAD") 152 | 153 | do { 154 | let hmac_sha = try HMAC(key: secret, variant: .sha384).authenticate(Array(postDataString.utf8)) 155 | mutableURLRequest.setValue(hmac_sha.toHexString(), forHTTPHeaderField: "X-BFX-SIGNATURE") 156 | } catch { 157 | print(error) 158 | } 159 | mutableURLRequest.setValue(key, forHTTPHeaderField: "X-BFX-APIKEY") 160 | } 161 | 162 | return mutableURLRequest 163 | } 164 | } 165 | } 166 | 167 | extension Bitfinex.API: APIType { 168 | public var host: String { 169 | return "https://api.bitfinex.com" 170 | } 171 | 172 | public var path: String { 173 | switch self { 174 | case .symbols: return "/v1/symbols" 175 | case .ticker(let symbol): return "/v1/pubticker/\(symbol)" 176 | case .balances: return "/v1/balances" 177 | } 178 | } 179 | 180 | public var httpMethod: HttpMethod { 181 | return .GET 182 | } 183 | 184 | public var authenticated: Bool { 185 | switch self { 186 | case .symbols: return false 187 | case .ticker: return false 188 | case .balances: return true 189 | } 190 | } 191 | 192 | public var loggingEnabled: LogLevel { 193 | switch self { 194 | case .symbols: return .url 195 | case .ticker: return .url 196 | case .balances: return .url 197 | } 198 | } 199 | 200 | public var postData: [String: String] { 201 | return [:] 202 | } 203 | 204 | public var refetchInterval: TimeInterval { 205 | switch self { 206 | case .symbols: return .aMonth 207 | case .ticker: return .aMinute 208 | case .balances: return .aMinute 209 | } 210 | } 211 | } 212 | 213 | extension Bitfinex.Service: TickerServiceType, BalanceServiceType { 214 | 215 | public func getTickers(completion: @escaping ( ResponseType) -> Void) { 216 | getSymbols(completion: { _ in 217 | 218 | var tasks: [String: Bool] = [:] 219 | 220 | self.store.symbolsResponse.symbols.forEach { symbol in 221 | tasks[symbol.displaySymbol] = false 222 | } 223 | 224 | self.store.symbolsResponse.symbols.forEach { symbol in 225 | self.getTicker(symbol: symbol, completion: { (currencyPair, responseType) in 226 | tasks[currencyPair.displaySymbol] = true 227 | 228 | let flag = tasks.values.reduce(true, { (result, value) -> Bool in 229 | return result && value 230 | }) 231 | if flag { 232 | completion(responseType) 233 | } 234 | }) 235 | } 236 | }) 237 | } 238 | 239 | public func getBalances(completion: @escaping ( ResponseType) -> Void) { 240 | getTickers(completion: { (_) in 241 | self.getAccountBalances(completion: { (responseType) in 242 | completion(responseType) 243 | }) 244 | }) 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /Sources/CoinExchange.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CoinExchange.swift 3 | // Cryptex 4 | // 5 | // Created by Sathyakumar Rajaraman on 1/2/18. 6 | // 7 | 8 | import Foundation 9 | import CryptoSwift 10 | 11 | extension CurrencyPair { 12 | 13 | var coinExchangeLabel: String { 14 | return quantity.code + "/" + price.code 15 | } 16 | 17 | convenience init(coinExchangeLabel: String, currencyStore: CurrencyStoreType) { 18 | let currencySymbols = coinExchangeLabel.components(separatedBy: "/") 19 | let quantity = currencyStore.forCode(currencySymbols[0]) 20 | let price = currencyStore.forCode(currencySymbols[1]) 21 | self.init(quantity: quantity, price: price) 22 | } 23 | } 24 | 25 | public struct CoinExchange { 26 | 27 | public class Market: CurrencyPair { 28 | public var marketID: String = "" 29 | public var marketAssetName: String = "" 30 | public var marketAssetCode: String = "" 31 | public var marketAssetID: String = "" 32 | public var marketAssetType: String = "" 33 | public var baseCurrency: String = "" 34 | public var baseCurrencyCode: String = "" 35 | public var baseCurrencyID: String = "" 36 | public var active: Bool 37 | 38 | public init(json: [String: Any]) { 39 | marketID = json["MarketID"] as? String ?? "" 40 | marketAssetName = json["MarketAssetName"] as? String ?? "" 41 | marketAssetCode = json["MarketAssetCode"] as? String ?? "" 42 | marketAssetID = json["MarketAssetID"] as? String ?? "" 43 | marketAssetType = json["MarketAssetType"] as? String ?? "" 44 | baseCurrency = json["BaseCurrency"] as? String ?? "" 45 | baseCurrencyCode = json["BaseCurrencyCode"] as? String ?? "" 46 | baseCurrencyID = json["BaseCurrencyID"] as? String ?? "" 47 | active = json["Active"] as? Bool ?? false 48 | let quantityCurrency = Currency(name: marketAssetName, code: marketAssetCode) 49 | let priceCurrency = Currency(name: baseCurrency, code: baseCurrencyCode) 50 | super.init(quantity: quantityCurrency, price: priceCurrency) 51 | } 52 | } 53 | 54 | public class MarketSummary: Ticker { 55 | public var marketID: String = "" 56 | public var lastPrice = NSDecimalNumber.zero 57 | public var change = NSDecimalNumber.zero 58 | public var highPrice = NSDecimalNumber.zero 59 | public var lowPrice = NSDecimalNumber.zero 60 | public var volume = NSDecimalNumber.zero 61 | public var btcVolume = NSDecimalNumber.zero 62 | public var tradeCount = NSDecimalNumber.zero 63 | public var bidPrice = NSDecimalNumber.zero 64 | public var askPrice = NSDecimalNumber.zero 65 | public var buyOrderCount = NSDecimalNumber.zero 66 | public var sellOrderCount = NSDecimalNumber.zero 67 | 68 | public init?(json: [String: Any], markets: [String: Market]) { 69 | marketID = json["MarketID"] as? String ?? "" 70 | guard let market = markets[marketID] else { return nil } 71 | lastPrice = NSDecimalNumber(json["LastPrice"]) 72 | change = NSDecimalNumber(json["Change"]) 73 | highPrice = NSDecimalNumber(json["HighPrice"]) 74 | lowPrice = NSDecimalNumber(json["LowPrice"]) 75 | volume = NSDecimalNumber(json["Volume"]) 76 | btcVolume = NSDecimalNumber(json["BTCVolume"]) 77 | tradeCount = NSDecimalNumber(json["TradeCount"]) 78 | bidPrice = NSDecimalNumber(json["BidPrice"]) 79 | askPrice = NSDecimalNumber(json["AskPrice"]) 80 | buyOrderCount = NSDecimalNumber(json["BuyOrderCount"]) 81 | sellOrderCount = NSDecimalNumber(json["SellOrderCount"]) 82 | super.init(symbol: CurrencyPair(quantity: market.quantity, price: market.price), price: lastPrice) 83 | } 84 | } 85 | 86 | public struct MarketHistory { 87 | public let tradePairId: Int 88 | public let label: String 89 | public let type: String 90 | public let price: NSDecimalNumber 91 | public let amount: NSDecimalNumber 92 | public let total: NSDecimalNumber 93 | public let timestamp: TimeInterval 94 | } 95 | 96 | public class Balance: Cryptex.Balance { 97 | public let reserved: NSDecimalNumber 98 | 99 | public init(json: [String: String], currency: Currency) { 100 | reserved = NSDecimalNumber(json["reserved"]) 101 | super.init(currency: currency, quantity: NSDecimalNumber(json["balance"])) 102 | } 103 | } 104 | 105 | public enum API { 106 | case getmarkets 107 | case getmarketsummaries 108 | case getBalance 109 | } 110 | 111 | public class Store: ExchangeDataStore { 112 | 113 | override fileprivate init() { 114 | super.init() 115 | name = "CoinExchange" 116 | accountingCurrency = .USDT 117 | } 118 | 119 | public var currencyPairsResponse: (response: HTTPURLResponse?, currencyPairs: [String: Market]) = (nil, [:]) 120 | public var tickersResponse: HTTPURLResponse? = nil 121 | public var balanceResponse: HTTPURLResponse? = nil 122 | } 123 | 124 | public class Service: Network, TickerServiceType, BalanceServiceType { 125 | public let store = Store() 126 | 127 | public func getCurrencyPairs(completion: @escaping (ResponseType) -> Void) { 128 | let apiType = CoinExchange.API.getmarkets 129 | if apiType.checkInterval(response: store.currencyPairsResponse.response) { 130 | completion(.cached) 131 | } else { 132 | coinExchangeDataTaskFor(api: apiType) { (response) in 133 | guard let marketsJSON = response.json as? [[String: Any]] else { return } 134 | var markets: [String: Market] = [:] 135 | marketsJSON.forEach({ (marketJSON) in 136 | let market = Market(json: marketJSON) 137 | markets[market.marketID] = market 138 | }) 139 | self.store.currencyPairsResponse = (response.httpResponse, markets) 140 | completion(.fetched) 141 | }.resume() 142 | } 143 | } 144 | 145 | public func getTickers(completion: @escaping (ResponseType) -> Void) { 146 | let apiType = CoinExchange.API.getmarketsummaries 147 | if apiType.checkInterval(response: store.tickersResponse) { 148 | completion(.cached) 149 | } else { 150 | coinExchangeDataTaskFor(api: apiType) { (response) in 151 | guard let marketSummaries = response.json as? [[String: String]] else { return } 152 | 153 | let tickers = marketSummaries.compactMap { MarketSummary(json: $0, markets: self.store.currencyPairsResponse.currencyPairs) } 154 | self.store.setTickersInDictionary(tickers: tickers) 155 | 156 | self.store.tickersResponse = response.httpResponse 157 | completion(.fetched) 158 | }.resume() 159 | } 160 | } 161 | 162 | public func getBalances(completion: @escaping (ResponseType) -> Void) { 163 | let apiType = CoinExchange.API.getBalance 164 | 165 | if apiType.checkInterval(response: store.balanceResponse) { 166 | 167 | completion(.cached) 168 | 169 | } else { 170 | 171 | coinExchangeDataTaskFor(api: apiType) { (response) in 172 | guard let balancesJSON = response.json as? [String: Any] else { return } 173 | 174 | var balances: [Balance] = [] 175 | balancesJSON.forEach({ (arg) in 176 | guard let value = arg.value as? [String: String] else { return } 177 | let currency = self.forCode(arg.key) 178 | balances.append(Balance(json: value, currency: currency)) 179 | }) 180 | self.store.balances = balances 181 | self.store.balanceResponse = response.httpResponse 182 | completion(.fetched) 183 | 184 | }.resume() 185 | } 186 | } 187 | 188 | func coinExchangeDataTaskFor(api: APIType, completion: ((Response) -> Void)?) -> URLSessionDataTask { 189 | return dataTaskFor(api: api) { (response) in 190 | guard let json = response.json as? [String: Any] else { return } 191 | if let success = json["success"] as? String, let jsonData = json["result"], Int(success) == 1 { 192 | var tempResponse = response 193 | tempResponse.json = jsonData 194 | completion?(tempResponse) 195 | } else { 196 | api.print(json["message"] ?? "", content: .response) 197 | } 198 | } 199 | } 200 | 201 | public override func requestFor(api: APIType) -> NSMutableURLRequest { 202 | let mutableURLRequest = api.mutableRequest 203 | if let key = key, let secret = secret, api.authenticated { 204 | var postData = api.postData 205 | postData["nonce"] = "\(Int(Date().timeIntervalSince1970 * 1000))" 206 | let requestString = postData.queryString 207 | api.print("Request Data: \(requestString)", content: .response) 208 | // POST payload 209 | let requestData = Array(requestString.utf8) 210 | if case .POST = api.httpMethod { 211 | mutableURLRequest.httpBody = requestString.utf8Data() 212 | } 213 | 214 | if let hmac_sha512 = try? HMAC(key: Array(secret.utf8), variant: .sha512).authenticate(requestData) { 215 | mutableURLRequest.setValue(hmac_sha512.toHexString(), forHTTPHeaderField: "SIGNATURE") 216 | } 217 | mutableURLRequest.setValue(key, forHTTPHeaderField: "KEY") 218 | } 219 | return mutableURLRequest 220 | } 221 | } 222 | } 223 | 224 | extension CoinExchange.API: APIType { 225 | public var host: String { 226 | return "https://www.coinexchange.io/api/" 227 | } 228 | 229 | public var path: String { 230 | switch self { 231 | case .getmarkets: return "v1/getmarkets" 232 | case .getmarketsummaries: return "v1/getmarketsummaries" 233 | case .getBalance: return "v1/balances" 234 | } 235 | } 236 | 237 | public var httpMethod: HttpMethod { 238 | switch self { 239 | case .getmarkets: return .GET 240 | case .getmarketsummaries: return .GET 241 | case .getBalance: return .POST 242 | } 243 | } 244 | 245 | public var authenticated: Bool { 246 | switch self { 247 | case .getmarkets: return false 248 | case .getmarketsummaries: return false 249 | case .getBalance: return true 250 | } 251 | } 252 | 253 | public var loggingEnabled: LogLevel { 254 | switch self { 255 | case .getmarkets: return .url 256 | case .getmarketsummaries: return .url 257 | case .getBalance: return .url 258 | } 259 | } 260 | 261 | public var postData: [String : String] { 262 | switch self { 263 | case .getmarkets: return [:] 264 | case .getmarketsummaries: return [:] 265 | case .getBalance: return [:] 266 | } 267 | } 268 | 269 | public var refetchInterval: TimeInterval { 270 | switch self { 271 | case .getmarkets: return .aWeek 272 | case .getmarketsummaries: return .aMinute 273 | case .getBalance: return .aMinute 274 | } 275 | } 276 | } 277 | -------------------------------------------------------------------------------- /Sources/CoinMarketCap.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CoinMarketCap.swift 3 | // Cryptex 4 | // 5 | // Created by Sathyakumar Rajaraman on 1/2/18. 6 | // 7 | 8 | import Foundation 9 | import CryptoSwift 10 | 11 | 12 | public struct CoinMarketCap { 13 | 14 | public class CMCTicker: Ticker { 15 | public var id: String = "" 16 | public var rank: String = "" 17 | public var volume24Hrs = NSDecimalNumber.zero 18 | public var marketCap = NSDecimalNumber.zero 19 | public var availableSupply = NSDecimalNumber.zero 20 | public var totalSupply = NSDecimalNumber.zero 21 | public var changeIn1Hr = NSDecimalNumber.zero 22 | public var changeIn24Hrs = NSDecimalNumber.zero 23 | public var changeIn7Days = NSDecimalNumber.zero 24 | public var lastUpdated = "" 25 | 26 | public init(json: [String: Any]) { 27 | let currency = Currency(name: json["name"] as? String ?? "", code: json["symbol"] as? String ?? "") 28 | let currencyPair = CurrencyPair(quantity: currency, price: .USD) 29 | let priceUSD = NSDecimalNumber(json["price_usd"]) 30 | super.init(symbol: currencyPair, price: priceUSD) 31 | id = json["id"] as? String ?? "" 32 | rank = json["rank"] as? String ?? "" 33 | priceInOtherCurencies?[Currency.USD] = priceUSD 34 | priceInOtherCurencies?[Currency.Bitcoin] = NSDecimalNumber(json["price_btc"]) 35 | volume24Hrs = NSDecimalNumber(json["24h_volume_usd"]) 36 | marketCap = NSDecimalNumber(json["market_cap_usd"]) 37 | availableSupply = NSDecimalNumber(json["available_supply"]) 38 | totalSupply = NSDecimalNumber(json["total_supply"]) 39 | changeIn1Hr = NSDecimalNumber(json["percent_change_1h"]) 40 | changeIn24Hrs = NSDecimalNumber(json["percent_change_24h"]) 41 | changeIn7Days = NSDecimalNumber(json["percent_change_7d"]) 42 | lastUpdated = json["last_updated"] as? String ?? "" 43 | } 44 | } 45 | 46 | public struct GlobalMarketData { 47 | public var marketCap: NSDecimalNumber 48 | public var volume24Hrs: NSDecimalNumber 49 | public var bitcoinDominance: NSDecimalNumber 50 | public var activeCurrencies: NSDecimalNumber 51 | public var activeAssets: NSDecimalNumber 52 | public var activeMarkets: NSDecimalNumber 53 | public var lastUpdated: NSDecimalNumber 54 | 55 | public init(json: [String: Any]) { 56 | marketCap = NSDecimalNumber(json["total_market_cap_usd"]) 57 | volume24Hrs = NSDecimalNumber(json["total_24h_volume_usd"]) 58 | bitcoinDominance = NSDecimalNumber(json["bitcoin_percentage_of_market_cap"]) 59 | activeCurrencies = NSDecimalNumber(json["active_currencies"]) 60 | activeAssets = NSDecimalNumber(json["active_assets"]) 61 | activeMarkets = NSDecimalNumber(json["active_markets"]) 62 | lastUpdated = NSDecimalNumber(json["last_updated"]) 63 | } 64 | 65 | public var description: String { 66 | var string = "" 67 | string += "MrktCp " + marketCap.shortFormatted + " | " 68 | string += "24hVol " + volume24Hrs.shortFormatted + " | " 69 | string += bitcoinDominance.stringValue + "% btc" 70 | return string 71 | } 72 | } 73 | 74 | public class Balance: Cryptex.Balance { 75 | public let reserved: NSDecimalNumber 76 | 77 | public init(json: [String: String], currency: Currency) { 78 | reserved = NSDecimalNumber(json["reserved"]) 79 | super.init(currency: currency, quantity: NSDecimalNumber(json["balance"])) 80 | } 81 | } 82 | 83 | public enum API { 84 | case getTicker 85 | case getGlobal 86 | } 87 | 88 | public class Store: ExchangeDataStore { 89 | 90 | override fileprivate init() { 91 | super.init() 92 | name = "CoinMarketCap" 93 | accountingCurrency = .USD 94 | } 95 | 96 | public var tickerResponse: (response: HTTPURLResponse?, tickers: [CMCTicker]) = (nil, []) 97 | public var globalMarketDataResponse: (response: HTTPURLResponse?, globalData: GlobalMarketData?) = (nil, nil) 98 | } 99 | 100 | public class Service: Network, TickerServiceType { 101 | public let store = Store() 102 | 103 | public func getTickers(completion: @escaping (ResponseType) -> Void) { 104 | let apiType = CoinMarketCap.API.getTicker 105 | if apiType.checkInterval(response: store.tickerResponse.response) { 106 | completion(.cached) 107 | } else { 108 | coinExchangeDataTaskFor(api: apiType) { (response) in 109 | guard let marketSummaries = response.json as? [[String: String]] else { return } 110 | 111 | let tickers: [CMCTicker] = marketSummaries.compactMap { CMCTicker(json: $0) } 112 | self.store.setTickersInDictionary(tickers: tickers) 113 | 114 | self.store.tickerResponse = (response.httpResponse, tickers) 115 | completion(.fetched) 116 | }.resume() 117 | } 118 | } 119 | 120 | public func getGlobal(completion: @escaping (ResponseType) -> Void) { 121 | let apiType = CoinMarketCap.API.getGlobal 122 | if apiType.checkInterval(response: store.globalMarketDataResponse.response) { 123 | completion(.cached) 124 | } else { 125 | coinExchangeDataTaskFor(api: apiType) { (response) in 126 | guard let global = response.json as? [String: Any] else { return } 127 | self.store.globalMarketDataResponse = (response.httpResponse, GlobalMarketData(json: global)) 128 | completion(.fetched) 129 | }.resume() 130 | } 131 | } 132 | 133 | func coinExchangeDataTaskFor(api: APIType, completion: ((Response) -> Void)?) -> URLSessionDataTask { 134 | return dataTaskFor(api: api) { (response) in 135 | completion?(response) 136 | } 137 | } 138 | 139 | public override func requestFor(api: APIType) -> NSMutableURLRequest { 140 | let mutableURLRequest = api.mutableRequest 141 | if let key = key, let secret = secret, api.authenticated { 142 | var postData = api.postData 143 | postData["nonce"] = "\(Int(Date().timeIntervalSince1970 * 1000))" 144 | let requestString = postData.queryString 145 | api.print("Request Data: \(requestString)", content: .response) 146 | // POST payload 147 | let requestData = Array(requestString.utf8) 148 | if case .POST = api.httpMethod { 149 | mutableURLRequest.httpBody = requestString.utf8Data() 150 | } 151 | 152 | if let hmac_sha512 = try? HMAC(key: Array(secret.utf8), variant: .sha512).authenticate(requestData) { 153 | mutableURLRequest.setValue(hmac_sha512.toHexString(), forHTTPHeaderField: "SIGNATURE") 154 | } 155 | mutableURLRequest.setValue(key, forHTTPHeaderField: "KEY") 156 | } 157 | return mutableURLRequest 158 | } 159 | } 160 | } 161 | 162 | extension CoinMarketCap.API: APIType { 163 | public var host: String { 164 | return "https://api.coinmarketcap.com/" 165 | } 166 | 167 | public var path: String { 168 | switch self { 169 | case .getTicker: return "v1/ticker?limit=0" 170 | case .getGlobal: return "v1/global" 171 | } 172 | } 173 | 174 | public var httpMethod: HttpMethod { 175 | switch self { 176 | case .getTicker: return .GET 177 | case .getGlobal: return .GET 178 | } 179 | } 180 | 181 | public var authenticated: Bool { 182 | switch self { 183 | case .getTicker: return false 184 | case .getGlobal: return false 185 | } 186 | } 187 | 188 | public var loggingEnabled: LogLevel { 189 | switch self { 190 | case .getTicker: return .response 191 | case .getGlobal: return .response 192 | } 193 | } 194 | 195 | public var postData: [String : String] { 196 | switch self { 197 | case .getTicker: return [:] 198 | case .getGlobal: return [:] 199 | } 200 | } 201 | 202 | public var refetchInterval: TimeInterval { 203 | switch self { 204 | case .getTicker: return .aMinute 205 | case .getGlobal: return .aMinute 206 | } 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /Sources/Common/APIType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // API.swift 3 | // Cryptex 4 | // 5 | // Created by Sathyakumar Rajaraman on 1/1/18. 6 | // 7 | 8 | import Foundation 9 | 10 | public protocol APIType { 11 | var host: String { get } 12 | var path: String { get } 13 | var httpMethod: HttpMethod { get } 14 | var authenticated: Bool { get } 15 | var loggingEnabled: LogLevel { get } 16 | var postData: [String: String] { get } 17 | var refetchInterval: TimeInterval { get } 18 | } 19 | 20 | public extension APIType { 21 | 22 | var mutableRequest: NSMutableURLRequest { 23 | let url = URL(string: host + path)! 24 | let mutableURLRequest = NSMutableURLRequest(url: url) 25 | mutableURLRequest.httpMethod = httpMethod.rawValue 26 | return mutableURLRequest 27 | } 28 | 29 | func checkInterval(response: HTTPURLResponse?) -> Bool { 30 | guard let response = response, let date = response.date, Date().timeIntervalSince(date) < refetchInterval else { return false } 31 | return true 32 | } 33 | 34 | func print(_ any: Any?, content: LogLevel) { 35 | guard let any = any, content.rawValue <= loggingEnabled.rawValue else { return } 36 | Swift.print(any) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/Common/Balance.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Balance.swift 3 | // Cryptex 4 | // 5 | // Created by Sathyakumar Rajaraman on 1/1/18. 6 | // 7 | 8 | import Foundation 9 | 10 | public protocol BalanceType { 11 | var currency: Currency { get } 12 | var quantity: NSDecimalNumber { get } 13 | } 14 | 15 | public class Balance: BalanceType, CustomStringConvertible { 16 | public let currency: Currency 17 | public let quantity: NSDecimalNumber 18 | 19 | public init(currency: Currency, quantity: NSDecimalNumber) { 20 | self.currency = currency 21 | self.quantity = quantity 22 | } 23 | 24 | public var description: String { 25 | return currency.code + ": " + quantity.stringValue 26 | } 27 | } 28 | 29 | public protocol DisplayableBalanceType { 30 | var name: String { get } 31 | var balanceQuantity: String { get } 32 | var priceInUSD: String { get } 33 | } 34 | 35 | public struct DisplayableBalance: DisplayableBalanceType { 36 | public let name: String 37 | public let balanceQuantity: String 38 | public let priceInUSD: String 39 | } 40 | -------------------------------------------------------------------------------- /Sources/Common/Currency.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Currency.swift 3 | // Cryptex 4 | // 5 | // Created by Sathyakumar Rajaraman on 12/30/17. 6 | // Copyright © 2017 Sathyakumar. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public class Currency: Hashable, Comparable { 12 | public let name: String 13 | public let code: String 14 | 15 | public init(name: String, code: String) { 16 | self.name = name 17 | self.code = code.uppercased() 18 | } 19 | 20 | public var hashValue: Int { 21 | return code.hashValue 22 | } 23 | 24 | public static func <(lhs: Currency, rhs: Currency) -> Bool { 25 | if lhs.name == rhs.name { 26 | return lhs.code < rhs.code 27 | } else { 28 | return lhs.name < rhs.name 29 | } 30 | } 31 | 32 | public static func ==(lhs: Currency, rhs: Currency) -> Bool { 33 | return lhs.code.lowercased() == rhs.code.lowercased() 34 | } 35 | 36 | func formatted(number: NSDecimalNumber) -> String { 37 | if let string = NumberFormatter.usd.string(from: number), self == .USD || self == .USDT { 38 | return string 39 | } else { 40 | return "\(number) \(code)" 41 | } 42 | } 43 | } 44 | 45 | public extension Currency { 46 | 47 | public convenience init(code: String) { 48 | self.init(name: code, code: code) 49 | } 50 | 51 | static let USD = Currency(name: "United States dollar", code: "USD") 52 | static let EUR = Currency(name: "Euro", code: "EUR") 53 | static let JPY = Currency(name: "Japanese yen", code: "JPY") 54 | static let GBP = Currency(name: "Pound sterling", code: "GBP") 55 | static let AUD = Currency(name: "Australian dollar", code: "AUD") 56 | static let CAD = Currency(name: "Canadian dollar", code: "CAD") 57 | static let CHF = Currency(name: "Swiss franc", code: "CHF") 58 | static let CNY = Currency(name: "Renminbi", code: "CNY") 59 | static let SEK = Currency(name: "Swedish krona", code: "SEK") 60 | static let NZD = Currency(name: "New Zealand dollar", code: "NZD") 61 | static let MXN = Currency(name: "Mexican peso", code: "MXN") 62 | static let SGD = Currency(name: "Singapore dollar", code: "SGD") 63 | static let HKD = Currency(name: "Hong Kong dollar", code: "HKD") 64 | static let NOK = Currency(name: "Norwegian krone", code: "NOK") 65 | static let KRW = Currency(name: "South Korean won", code: "KRW") 66 | static let TRY = Currency(name: "Turkish lira", code: "TRY") 67 | static let RUB = Currency(name: "Russian ruble", code: "RUB") 68 | static let INR = Currency(name: "Indian rupee", code: "INR") 69 | static let BRL = Currency(name: "Brazilian real", code: "BRL") 70 | static let ZAR = Currency(name: "South African rand", code: "ZAR") 71 | 72 | static let Bitcoin = Currency(name: "Bitcoin", code: "BTC") 73 | static let Ethereum = Currency(name: "Ethereum", code: "ETH") 74 | static let Litecoin = Currency(name: "Litecoin", code: "LTC") 75 | static let Ripple = Currency(name: "Ripple", code: "XRP") 76 | static let Cardano = Currency(name: "Cardano", code: "ADA") 77 | static let NEM = Currency(name: "NEM", code: "XEM") 78 | static let USDT = Currency(name: "Tether USD", code: "USDT") 79 | 80 | static let bitcoinCash = Currency(name: "Bitcoin Cash", code: "BCH") 81 | static let bitcoinGold = Currency(name: "Bitcoin Gold", code: "BTG") 82 | static let zcash = Currency(name: "ZCash", code: "ZEC") 83 | static let ethereumClassic = Currency(name: "Ethereum Classic", code: "ETC") 84 | static let stellar = Currency(name: "Stellar", code: "STR") 85 | static let dash = Currency(name: "Dash", code: "DASH") 86 | static let nxt = Currency(name: "NXT", code: "NXT") 87 | static let monero = Currency(name: "Monero", code: "XMR") 88 | static let augur = Currency(name: "Augur", code: "REP") 89 | 90 | static let bytecoin = Currency(name: "Bytecoin", code: "BCN") 91 | static let bitcoinDark = Currency(name: "BitcoinDark", code: "BTCD") 92 | static let mainSafeCoin = Currency(name: "MainSafeCoin", code: "MAID") 93 | static let blackCoin = Currency(name: "BlackCoin", code: "BLK") 94 | 95 | static let golem = Currency(name: "Golem", code: "GNT") 96 | static let lisk = Currency(name: "Lisk", code: "LSK") 97 | static let gnosis = Currency(name: "Gnosis", code: "GNO") 98 | static let steem = Currency(name: "STEEM", code: "STEEM") 99 | 100 | static let siacoin = Currency(name: "Siacoin", code: "SC") 101 | static let digiByte = Currency(name: "DigiByte", code: "DGB") 102 | static let bitShares = Currency(name: "BitShares", code: "BTS") 103 | static let stratis = Currency(name: "Stratis", code: "STRAT") 104 | static let factom = Currency(name: "Factom", code: "FCT") 105 | static let syscoin = Currency(name: "Syscoin", code: "SYS") 106 | static let dogecoin = Currency(name: "Dogecoin", code: "DOGE") 107 | static let gameCredits = Currency(name: "GameCredits", code: "GAME") 108 | static let lbryCredits = Currency(name: "LBRY Credits", code: "LBC") 109 | static let decred = Currency(name: "Decred", code: "DCR") 110 | static let neoscoin = Currency(name: "Neoscoin", code: "NEOS") 111 | static let viacoin = Currency(name: "Viacoin", code: "VIA") 112 | static let omni = Currency(name: "Omni", code: "OMNI") 113 | static let synereoAMP = Currency(name: "Synereo AMP", code: "AMP") 114 | static let vertcoin = Currency(name: "Vertcoin", code: "VTC") 115 | static let counterparty = Currency(name: "Counterparty", code: "XCP") 116 | static let clams = Currency(name: "CLAMS", code: "CLAM") 117 | static let pascalCoin = Currency(name: "PascalCoin", code: "PASC") 118 | static let gridcoinResearch = Currency(name: "GridCoin Research", code: "GRC") 119 | static let storjcoinX = Currency(name: "Storjcoin X", code: "SJCX") 120 | static let potCoin = Currency(name: "PotCoin", code: "POT") 121 | static let burst = Currency(name: "Burst", code: "BURST") 122 | static let huntercoin = Currency(name: "Huntercoin", code: "HUC") 123 | static let bitmark = Currency(name: "Bitmark", code: "BTM") 124 | static let bitCrystals = Currency(name: "BitCrystals", code: "BCY") 125 | static let primecoin = Currency(name: "Primecoin", code: "XPM") 126 | static let belacoin = Currency(name: "Belacoin", code: "BELA") 127 | static let peercoin = Currency(name: "Peercoin", code: "PPC") 128 | static let einsteinium = Currency(name: "Einsteinium", code: "EMC2") 129 | static let expanse = Currency(name: "Expanse", code: "EXP") 130 | 131 | static let dnotes = Currency(name: "DNotes", code: "NOTE") 132 | static let radium = Currency(name: "Radium", code: "RADS") 133 | static let veriCoin = Currency(name: "VeriCoin", code: "VRC") 134 | static let navCoin = Currency(name: "NAVCoin", code: "NAV") 135 | static let florincoin = Currency(name: "Florincoin", code: "FLO") 136 | static let pinkcoin = Currency(name: "Pinkcoin", code: "PINK") 137 | static let namecoin = Currency(name: "Namecoin", code: "NMC") 138 | static let nautiluscoin = Currency(name: "Nautiluscoin", code: "NAUT") 139 | static let foldingCoin = Currency(name: "FoldingCoin", code: "FLDC") 140 | static let nexium = Currency(name: "Nexium", code: "NXC") 141 | static let vcash = Currency(name: "Vcash", code: "XVC") 142 | static let riecoin = Currency(name: "Riecoin", code: "RIC") 143 | static let bitcoinPlus = Currency(name: "BitcoinPlus", code: "XBC") 144 | static let steemDollars = Currency(name: "Steem Dollars", code: "SBD") 145 | 146 | static let digixDAO = Currency(name: "DigixDAO", code: "DGD") 147 | static let neo = Currency(name: "Neo", code: "NEO") 148 | static let zCoin = Currency(name: "ZCoin", code: "XZC") 149 | static let qtum = Currency(name: "Qtum", code: "QTUM") 150 | static let gas = Currency(name: "Gas", code: "GAS") 151 | static let populous = Currency(name: "Populous", code: "PPT") 152 | static let binanceCoin = Currency(name: "Binance Coin", code: "BNB") 153 | static let bitcoinDiamond = Currency(name: "Bitcoin Diamond", code: "BCD") 154 | 155 | private static let currencies: [Currency] = [ 156 | USD, EUR, JPY, GBP, AUD, CAD, CHF, CNY, SEK, NZD, MXN, SGD, HKD, NOK, KRW, TRY, RUB, INR, BRL, ZAR, 157 | Bitcoin, 158 | Ethereum, 159 | Ripple, 160 | Litecoin, 161 | Cardano, 162 | NEM, 163 | USDT, 164 | bitcoinCash, 165 | bitcoinGold, 166 | zcash, 167 | ethereumClassic, 168 | stellar, 169 | dash, 170 | nxt, 171 | monero, 172 | augur, 173 | bytecoin, 174 | bitcoinDark, 175 | mainSafeCoin, 176 | blackCoin, 177 | golem, 178 | lisk, 179 | gnosis, 180 | steem, 181 | siacoin, 182 | digiByte, 183 | bitShares, 184 | stratis, 185 | factom, 186 | syscoin, 187 | dogecoin, 188 | gameCredits, 189 | lbryCredits, 190 | decred, 191 | neoscoin, 192 | viacoin, 193 | omni, 194 | synereoAMP, 195 | vertcoin, 196 | counterparty, 197 | clams, 198 | pascalCoin, 199 | gridcoinResearch, 200 | storjcoinX, 201 | potCoin, 202 | burst, 203 | huntercoin, 204 | bitmark, 205 | bitCrystals, 206 | primecoin, 207 | belacoin, 208 | peercoin, 209 | einsteinium, 210 | expanse, 211 | dnotes, 212 | radium, 213 | veriCoin, 214 | navCoin, 215 | florincoin, 216 | pinkcoin, 217 | namecoin, 218 | nautiluscoin, 219 | foldingCoin, 220 | nexium, 221 | vcash, 222 | riecoin, 223 | bitcoinPlus, 224 | steemDollars, 225 | digixDAO, 226 | neo, 227 | zCoin, 228 | qtum, 229 | gas, 230 | populous, 231 | binanceCoin, 232 | bitcoinDiamond 233 | ] 234 | 235 | static var currencyLookupDictionary: [String: Currency] = { 236 | return dictionary(array: Currency.currencies) 237 | }() 238 | 239 | static func dictionary(array: [Currency]) -> [String: Currency] { 240 | var dictionary: [String: Currency] = [:] 241 | array.forEach({ (currency) in 242 | dictionary[currency.code] = currency 243 | }) 244 | return dictionary 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /Sources/Common/CurrencyPair.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CurrencyPair.swift 3 | // Cryptex 4 | // 5 | // Created by Sathyakumar Rajaraman on 12/31/17. 6 | // 7 | 8 | import Foundation 9 | 10 | public class CurrencyPair: Equatable { 11 | 12 | public let quantity: Currency 13 | public let price: Currency 14 | 15 | public init(quantity: Currency, price: Currency) { 16 | self.quantity = quantity 17 | self.price = price 18 | } 19 | 20 | public convenience init(symbol: String, currencyStore: CurrencyStoreType) { 21 | let delimitterString = symbol.trimmingCharacters(in: .letters) 22 | if delimitterString.count == 1 { 23 | let currencySymbols = symbol.components(separatedBy: delimitterString) 24 | let quantity = currencyStore.forCode(currencySymbols[0]) 25 | let price = currencyStore.forCode(currencySymbols[1]) 26 | self.init(quantity: quantity, price: price) 27 | } else { 28 | var index = symbol.index(symbol.endIndex, offsetBy: -4) 29 | var priceCurrencyCode = String(symbol[index...]) 30 | if currencyStore.isKnown(code: priceCurrencyCode) == false { 31 | index = symbol.index(index, offsetBy: 1) 32 | priceCurrencyCode = String(symbol[index...]) 33 | } 34 | let price = currencyStore.forCode(priceCurrencyCode) 35 | let quantity = currencyStore.forCode(String(symbol[.. Bool { 50 | if lhs.quantity == rhs.quantity { 51 | return lhs.price < rhs.price 52 | } else { 53 | return lhs.quantity < rhs.quantity 54 | } 55 | } 56 | 57 | public static func ==(lhs: CurrencyPair, rhs: CurrencyPair) -> Bool { 58 | return lhs.quantity == rhs.quantity && lhs.price == rhs.price 59 | } 60 | } 61 | 62 | public extension CurrencyPair { 63 | static var btcusd = CurrencyPair(quantity: .Bitcoin, price: .USD) 64 | } 65 | 66 | -------------------------------------------------------------------------------- /Sources/Common/Enums.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Enums.swift 3 | // Cryptex 4 | // 5 | // Created by Sathyakumar Rajaraman on 1/1/18. 6 | // 7 | 8 | import Foundation 9 | 10 | public enum HttpMethod: String { 11 | case GET 12 | case POST 13 | case DELETE 14 | case PATCH 15 | case UPDATE 16 | } 17 | 18 | public enum LogLevel: UInt8 { 19 | case none = 0 20 | case url = 1 21 | case requestHeaders = 2 22 | case response = 3 23 | case responseHeaders = 4 24 | } 25 | 26 | public enum ResponseType { 27 | case fetched 28 | case cached 29 | case unexpected(Response) 30 | } 31 | 32 | public enum TransactionType: String { 33 | case none 34 | case buy 35 | case sell 36 | case withdraw 37 | case deposit 38 | } 39 | 40 | public enum TickerViewType: Int { 41 | case quantity = 0 42 | case price = 1 43 | case name = 2 44 | } 45 | -------------------------------------------------------------------------------- /Sources/Common/ExchangeDataStore.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExchangeDataStore.swift 3 | // Cryptex 4 | // 5 | // Created by Sathyakumar Rajaraman on 1/6/18. 6 | // 7 | 8 | import Foundation 9 | 10 | public class ExchangeDataStore { 11 | 12 | public var name = "ExchangeDataStore" 13 | public var accountingCurrency: Currency = .USD 14 | public var commonCurrency: Currency = .Bitcoin 15 | 16 | public var tickerByQuantityCCY: [[T]] = [] 17 | public var tickerByPriceCCY: [[T]] = [] 18 | public var tickerByName: [T] = [] 19 | 20 | public var balances: [U] = [] 21 | 22 | public var tickersDictionary: [String: T] = [:] { 23 | didSet { 24 | let tickers = tickersDictionary.values.compactMap{$0} 25 | var byQuantityCCY: [Currency: [T]] = [:] 26 | var byPriceCCY: [Currency: [T]] = [:] 27 | Set(tickers.map { $0.symbol.quantity }).forEach { byQuantityCCY[$0] = [] } 28 | Set(tickers.map { $0.symbol.price }).forEach { byPriceCCY[$0] = [] } 29 | tickers.forEach { ticker in 30 | byQuantityCCY[ticker.symbol.quantity]?.append(ticker) 31 | byPriceCCY[ticker.symbol.price]?.append(ticker) 32 | } 33 | tickerByQuantityCCY = byQuantityCCY.values.sorted(by: { (leftArray, rightArray) -> Bool in 34 | guard let left = leftArray.first, let right = rightArray.first else { return false } 35 | return left.price(in: accountingCurrency).compare(right.price(in: accountingCurrency)) == .orderedDescending 36 | }) 37 | tickerByPriceCCY = byPriceCCY.keys.compactMap { byPriceCCY[$0] } 38 | tickerByName = tickers.sorted(by: { (left, right) -> Bool in 39 | return left.symbol.displaySymbol < right.symbol.displaySymbol 40 | }) 41 | } 42 | } 43 | 44 | private func setPriceInUSD(tickers: [T]) -> [T] { 45 | return tickers.map({ (ticker) -> T in 46 | var t = ticker 47 | if ticker.symbol.price == accountingCurrency { 48 | if (t.priceInOtherCurencies?[accountingCurrency] = ticker.price) == nil { 49 | t.priceInOtherCurencies = [accountingCurrency: ticker.price] 50 | t.accountingCurrency = self.accountingCurrency 51 | } 52 | return t 53 | } else if let usdPrice = tickers.filter({ 54 | return $0.symbol == CurrencyPair(quantity: ticker.symbol.price, price: accountingCurrency) }).first?.price 55 | { 56 | let priceInOtherCurrency = usdPrice.multiplying(by: ticker.price) 57 | if (t.priceInOtherCurencies?[accountingCurrency] = priceInOtherCurrency) == nil { 58 | t.priceInOtherCurencies = [accountingCurrency: priceInOtherCurrency] 59 | t.accountingCurrency = self.accountingCurrency 60 | } 61 | return t 62 | } else { 63 | t.accountingCurrency = self.accountingCurrency 64 | return t 65 | } 66 | }) 67 | } 68 | 69 | public func setTicker(ticker: T, symbol: String) { 70 | var temp = tickersDictionary 71 | temp[symbol] = ticker 72 | setTickersInDictionary(tickers: temp.values.compactMap{$0}) 73 | } 74 | 75 | public func setTickersInDictionary(tickers: [T]) { 76 | tickersDictionary = [:] 77 | var dictionary: [String: T] = [:] 78 | setPriceInUSD(tickers: tickers).forEach { dictionary[$0.symbol.displaySymbol] = $0 } 79 | tickersDictionary = dictionary 80 | } 81 | 82 | public func balanceInAccountingCurrency(balance: BalanceType) -> NSDecimalNumber? { 83 | 84 | let fiatCurrencyPair = CurrencyPair(quantity: balance.currency, price: accountingCurrency) 85 | let cryptoCurrencyPair = CurrencyPair(quantity: balance.currency, price: commonCurrency) 86 | if let ticker = (tickerByName.filter {$0.symbol == fiatCurrencyPair}).first { 87 | return balance.quantity.multiplying(by: ticker.price(in: accountingCurrency)) 88 | } else if let ticker = (tickerByName.filter {$0.symbol == cryptoCurrencyPair}).first { 89 | return balance.quantity.multiplying(by: ticker.price(in: accountingCurrency)) 90 | } else { 91 | return nil 92 | } 93 | } 94 | 95 | public func displayablePrice(ticker: T) -> String { 96 | guard ticker.symbol.price != accountingCurrency else { return "" } 97 | return ticker.price.stringValue + " " + ticker.symbol.price.code 98 | } 99 | } 100 | 101 | extension ExchangeDataStore: TickerTableViewDataSource { 102 | 103 | private func ticker(section: Int, row: Int, viewType: TickerViewType) -> T? { 104 | switch viewType { 105 | case .quantity: return tickerByQuantityCCY[section][row] 106 | case .price: return tickerByPriceCCY[section][row] 107 | default: return nil 108 | } 109 | } 110 | 111 | public func sectionCount(viewType: TickerViewType) -> Int { 112 | switch viewType { 113 | case .quantity: return tickerByQuantityCCY.count 114 | case .price: return tickerByPriceCCY.count 115 | default: return 0 116 | } 117 | } 118 | public func tickerCount(section: Int, viewType: TickerViewType) -> Int { 119 | switch viewType { 120 | case .quantity: return tickerByQuantityCCY[section].count 121 | case .price: return tickerByPriceCCY[section].count 122 | default: return 0 123 | } 124 | } 125 | public func sectionHeaderTitle(section: Int, viewType: TickerViewType) -> String? { 126 | switch viewType { 127 | case .quantity: return tickerByQuantityCCY[section][0].symbol.quantity.name 128 | case .price: return tickerByPriceCCY[section][0].symbol.price.name 129 | default: return nil 130 | } 131 | } 132 | public func displayableTicker(section: Int, row: Int, viewType: TickerViewType) -> DisplayableTickerType? { 133 | guard let t = ticker(section: section, row: row, viewType: viewType) else { return nil } 134 | 135 | var formattedPriceInAccountingCurrency = "" 136 | if let priceInUSD = t.priceInOtherCurencies?[accountingCurrency] { 137 | formattedPriceInAccountingCurrency = accountingCurrency.formatted(number: priceInUSD) 138 | } 139 | return DisplayableTicker(name: t.name, price: displayablePrice(ticker: t), formattedPriceInAccountingCurrency: formattedPriceInAccountingCurrency) 140 | } 141 | } 142 | 143 | extension ExchangeDataStore: BalanceTableViewDataSource { 144 | 145 | public func getTotalBalance() -> NSDecimalNumber { 146 | var totalBalance = NSDecimalNumber.zero 147 | balances.forEach { (balance) in 148 | if let balanceInAccountingCurrency = balanceInAccountingCurrency(balance: balance) { 149 | totalBalance = totalBalance.adding(balanceInAccountingCurrency) 150 | } 151 | } 152 | return totalBalance 153 | } 154 | 155 | public func balanceCount() -> Int { 156 | return balances.count 157 | } 158 | 159 | public func displayableBalance(row: Int) -> DisplayableBalanceType { 160 | let balance = balances[row] 161 | let balanceInAccountingCurrency = self.balanceInAccountingCurrency(balance: balance) 162 | let price = balanceInAccountingCurrency == balance.quantity ? "" : balance.quantity.stringValue 163 | let priceInUSD = accountingCurrency.formatted(number: balanceInAccountingCurrency ?? .zero) 164 | return DisplayableBalance(name: balance.currency.name, balanceQuantity: price, priceInUSD: priceInUSD) 165 | } 166 | } 167 | 168 | -------------------------------------------------------------------------------- /Sources/Common/Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Extensions.swift 3 | // Cryptex 4 | // 5 | // Created by Sathyakumar Rajaraman on 12/31/17. 6 | // 7 | 8 | import Foundation 9 | 10 | public extension Int { 11 | public static let thousand = 1_000 12 | public static let million = 1_000_000 13 | public static let billion = 1_000_000 14 | public static let trillion = 1_000_000_000 15 | } 16 | 17 | 18 | public extension Locale { 19 | static let enUS = Locale(identifier: "en-US") 20 | static let enIN = Locale(identifier: "en-IN") 21 | } 22 | 23 | public extension TimeInterval { 24 | static let aMinute: TimeInterval = 60 25 | static let twoMinutes: TimeInterval = aMinute * 2 26 | static let fiveMinutes: TimeInterval = aMinute * 5 27 | static let tenMinutes: TimeInterval = aMinute * 10 28 | static let fifteenMinutes: TimeInterval = aMinute * 15 29 | static let thirtyMinutes: TimeInterval = aMinute * 30 30 | static let anHour: TimeInterval = aMinute * 60 31 | static let aDay: TimeInterval = anHour * 24 32 | static let aWeek: TimeInterval = aDay * 7 33 | static let aMonth: TimeInterval = aDay * 30 34 | static let aMonthAgo: TimeInterval = -1 * aMonth 35 | } 36 | 37 | public extension HTTPURLResponse { 38 | var date: Date? { 39 | guard let dateString = allHeaderFields["Date"] as? String else { return nil } 40 | return DateFormatter.httpHeader.date(from: dateString) 41 | } 42 | } 43 | 44 | public extension NumberFormatter { 45 | static var usd: NumberFormatter { 46 | let formatter = NumberFormatter() 47 | formatter.locale = .enUS 48 | formatter.numberStyle = .currency 49 | return formatter 50 | } 51 | 52 | static var inr: NumberFormatter { 53 | let formatter = NumberFormatter() 54 | formatter.locale = .enIN 55 | formatter.numberStyle = .currency 56 | return formatter 57 | } 58 | } 59 | 60 | public extension DateFormatter { 61 | static func doubleLineDateTime(date: Date) -> String { 62 | let df = DateFormatter() 63 | df.dateStyle = DateFormatter.Style.short 64 | df.timeStyle = DateFormatter.Style.none 65 | var string = df.string(from: date) 66 | df.dateStyle = DateFormatter.Style.none 67 | df.timeStyle = DateFormatter.Style.short 68 | string = string + "\n" + df.string(from: date) 69 | return string 70 | } 71 | 72 | static var httpHeader: DateFormatter { 73 | let df = DateFormatter() 74 | df.dateFormat = "EEE, dd MMM yyyy HH:mm:ss zzz" 75 | return df 76 | } 77 | } 78 | 79 | public extension NSDecimalNumber { 80 | convenience init(_ any: Any?) { 81 | if let string = any as? String { 82 | self.init(string: string) 83 | } else if let number = (any as? NSNumber)?.decimalValue { 84 | self.init(decimal: number) 85 | } else { 86 | self.init(value: 0) 87 | } 88 | } 89 | 90 | static var thousand: NSDecimalNumber { 91 | return NSDecimalNumber(value: Int.thousand) 92 | } 93 | 94 | static var million: NSDecimalNumber { 95 | return NSDecimalNumber(value: Int.million) 96 | } 97 | 98 | static var billion: NSDecimalNumber { 99 | return NSDecimalNumber(value: Int.billion) 100 | } 101 | 102 | static var trillion: NSDecimalNumber { 103 | return NSDecimalNumber(value: Int.trillion) 104 | } 105 | 106 | var timestampInSeconds: Int64 { 107 | let handler = NSDecimalNumberHandler(roundingMode: NSDecimalNumber.RoundingMode.down, scale: 0, raiseOnExactness: true, raiseOnOverflow: true, raiseOnUnderflow: true, raiseOnDivideByZero: true) 108 | return rounding(accordingToBehavior: handler).int64Value 109 | } 110 | 111 | var shortFormatted: String { 112 | var temp = self 113 | var suffix = "" 114 | if intValue >= Int.trillion { 115 | temp = temp.dividing(by: .trillion) 116 | suffix = "T" 117 | } else if intValue >= Int.billion { 118 | temp = temp.dividing(by: .billion) 119 | suffix = "B" 120 | } else if intValue >= Int.million { 121 | temp = temp.dividing(by: .million) 122 | suffix = "M" 123 | } else if intValue >= Int.thousand { 124 | temp = temp.dividing(by: .thousand) 125 | suffix = "K" 126 | } 127 | let num = NumberFormatter.usd.string(from: temp) ?? "" 128 | return num + suffix 129 | } 130 | 131 | var usdFormatted: String? { 132 | return NumberFormatter.usd.string(from: self) 133 | } 134 | } 135 | 136 | public extension NSDecimalNumberHandler { 137 | 138 | static var round: NSDecimalNumberHandler { 139 | return NSDecimalNumberHandler(scale: 0) 140 | } 141 | 142 | static var zeroDotEight: NSDecimalNumberHandler { 143 | return NSDecimalNumberHandler(scale: 8) 144 | } 145 | 146 | convenience init(scale: Int16) { 147 | self.init(roundingMode: .down, scale: scale, raiseOnExactness: true, raiseOnOverflow: true, raiseOnUnderflow: true, raiseOnDivideByZero: true) 148 | } 149 | } 150 | 151 | public extension String { 152 | func utf8Data() -> Data? { 153 | return data(using: .utf8) 154 | } 155 | } 156 | 157 | public extension NSMutableURLRequest { 158 | func printHeaders() { 159 | if let headers = allHTTPHeaderFields, headers.count > 0 { 160 | print("Headers:") 161 | headers.forEach { key, value in 162 | print(" \(key): \(value)") 163 | } 164 | } 165 | } 166 | } 167 | 168 | public extension Dictionary where Key: ExpressibleByStringLiteral, Value: ExpressibleByStringLiteral { 169 | var queryString: String { 170 | var postDataString = "" 171 | forEach { tuple in 172 | if postDataString.count != 0 { 173 | postDataString += "&" 174 | } 175 | postDataString += "\(tuple.key)=\(tuple.value)" 176 | } 177 | return postDataString 178 | } 179 | } 180 | 181 | extension Data { 182 | var string: String? { 183 | return String(data: self, encoding: .utf8) 184 | } 185 | } 186 | 187 | public extension Dictionary { 188 | var data: Data? { 189 | return try? JSONSerialization.data(withJSONObject: self, options: []) 190 | } 191 | } 192 | 193 | extension HTTPURLResponse { 194 | open override var description: String { 195 | return "\(statusCode) \(self.url?.absoluteString ?? "")" 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /Sources/Common/MockURLSession.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockURLSession.swift 3 | // Cryptex 4 | // 5 | // Created by Sathyakumar Rajaraman on 12/31/17. 6 | // 7 | 8 | // This class is a Swift 3 rewrite of https://github.com/announce/MockURLSession/blob/master/MockURLSession/MockURLSession.swift 9 | 10 | /** 11 | 12 | MIT License 13 | 14 | Copyright (c) 2016 15 | 16 | Permission is hereby granted, free of charge, to any person obtaining a copy 17 | of this software and associated documentation files (the "Software"), to deal 18 | in the Software without restriction, including without limitation the rights 19 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 20 | copies of the Software, and to permit persons to whom the Software is 21 | furnished to do so, subject to the following conditions: 22 | 23 | The above copyright notice and this permission notice shall be included in all 24 | copies or substantial portions of the Software. 25 | 26 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 27 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 28 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 29 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 30 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 31 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 32 | SOFTWARE. 33 | */ 34 | 35 | import Foundation 36 | 37 | public class MockURLSession: URLSession { 38 | private static let sharedInstance = MockURLSession() 39 | 40 | public typealias CompletionBlock = (Data?, URLResponse?, Error?) -> Void 41 | typealias Response = (data: Data?, response: URLResponse?, error: Error?) 42 | 43 | private var responses: [URL: Response] = [:] 44 | 45 | public override class var shared: URLSession { 46 | get { 47 | return MockURLSession.sharedInstance 48 | } 49 | } 50 | 51 | public override func dataTask(with url: URL, completionHandler: @escaping CompletionBlock) -> URLSessionDataTask { 52 | 53 | let response = responses[url] ?? (data: nil, response: nil, error: NSError(domain: "MockURLSession", code: 1, userInfo: [NSLocalizedDescriptionKey : "No response registered for (\(url.absoluteString))"])) 54 | 55 | return MockURLSessionDataTask(responseParameters: response, completionBlock: completionHandler) 56 | } 57 | 58 | public override func dataTask(with urlRequest: URLRequest, completionHandler: @escaping CompletionBlock) -> URLSessionDataTask { 59 | 60 | if let url = urlRequest.url { 61 | let response = responses[url] ?? (data: nil, response: nil, error: NSError(domain: "MockURLSession", code: 1, userInfo: [NSLocalizedDescriptionKey : "No response registered for (\(url.absoluteString))"])) 62 | 63 | return MockURLSessionDataTask(responseParameters: response, completionBlock: completionHandler) 64 | } else { 65 | let response: Response = (data: nil, response: nil, error: NSError(domain: "MockURLSession", code: 1, userInfo: [NSLocalizedDescriptionKey : "No response registered for \(urlRequest)"])) 66 | return MockURLSessionDataTask(responseParameters: response, completionBlock: completionHandler) 67 | } 68 | } 69 | 70 | public func registerMockResponse(url: URL, data: Data?, statusCode: Int = 200, headerFields: [String: String]? = nil, error: Error? = nil) { 71 | 72 | responses[url] = (data: data, response: HTTPURLResponse(url: url, statusCode: statusCode, httpVersion: nil, headerFields: headerFields), error: error) 73 | } 74 | 75 | class MockURLSessionDataTask: URLSessionDataTask { 76 | let responseParameters: Response 77 | let completionBlock: CompletionBlock 78 | 79 | init(responseParameters: Response, completionBlock: @escaping CompletionBlock) { 80 | self.responseParameters = responseParameters 81 | self.completionBlock = completionBlock 82 | } 83 | 84 | override func resume() { 85 | print("Mock \(responseParameters.response?.url?.absoluteString ?? "")") 86 | completionBlock(responseParameters.data, responseParameters.response, responseParameters.error) 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Sources/Common/Network.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Network.swift 3 | // Cryptex 4 | // 5 | // Created by Sathyakumar Rajaraman on 12/31/17. 6 | // 7 | 8 | import Foundation 9 | 10 | public protocol TickerServiceType { 11 | func getTickers(completion: @escaping (ResponseType) -> Void) 12 | } 13 | 14 | public protocol BalanceServiceType { 15 | func getBalances(completion: @escaping (ResponseType) -> Void) 16 | } 17 | 18 | public struct Response { 19 | let data: Data? 20 | let httpResponse: HTTPURLResponse? 21 | let error: Error? 22 | var json: Any? 23 | var string: String? 24 | 25 | init(data: Data?, httpResponse: HTTPURLResponse?, error: Error?) { 26 | self.data = data 27 | self.httpResponse = httpResponse 28 | self.error = error 29 | } 30 | } 31 | 32 | open class Network { 33 | 34 | let key: String? 35 | let secret: String? 36 | private let session: URLSession 37 | private var previousNonce: Int64 = 0 38 | private let nonceQueue = DispatchQueue(label: "com.sathyakumar.cryptex.network.nonce") 39 | public let userPreference: UserPreference 40 | var currencyOverrides: [String: Currency]? 41 | var apiCurrencyOverrides: [String: Currency]? 42 | 43 | public var isMock: Bool { 44 | return session is MockURLSession 45 | } 46 | 47 | public init(key: String?, secret: String?, session: URLSession, userPreference: UserPreference, currencyOverrides: [String: Currency]?) { 48 | self.key = key 49 | self.secret = secret 50 | self.session = session 51 | self.userPreference = userPreference 52 | self.currencyOverrides = currencyOverrides 53 | } 54 | 55 | public func dataTaskFor(api: APIType, completion: ((Response) -> Void)?) -> URLSessionDataTask { 56 | let urlRequest = requestFor(api: api) 57 | api.print("\(urlRequest.httpMethod) \(urlRequest.url?.absoluteString ?? "")", content: .url) 58 | if LogLevel.requestHeaders.rawValue <= api.loggingEnabled.rawValue { 59 | urlRequest.printHeaders() 60 | } 61 | return session.dataTask(with: urlRequest as URLRequest) { (data, urlResponse, error) in 62 | var response = Response(data: data, httpResponse: urlResponse as? HTTPURLResponse, error: error) 63 | response.string = data?.string 64 | if let data = data { 65 | response.json = try? JSONSerialization.jsonObject(with: data, options: []) 66 | } 67 | api.print("\(response.httpResponse?.description ?? "")", content: .url) 68 | api.print("Response Headers: \(String(describing: response.httpResponse))", content: .responseHeaders) 69 | api.print("Response Data: \(response.string ?? "")", content: .response) 70 | completion?(response) 71 | } 72 | } 73 | 74 | open func requestFor(api: APIType) -> NSMutableURLRequest { 75 | return api.mutableRequest 76 | } 77 | 78 | public func getTimestampInSeconds() -> Int64 { 79 | var ts: Int64 = 0 80 | nonceQueue.sync { 81 | let tsDecimal = NSDecimalNumber(value: Date().timeIntervalSince1970) 82 | ts = tsDecimal.timestampInSeconds 83 | if previousNonce == ts { 84 | let diff = 1.0 - tsDecimal.subtracting(NSDecimalNumber(value: ts)).doubleValue 85 | Thread.sleep(forTimeInterval: diff > 0 ? diff : 1) 86 | ts = NSDecimalNumber(value: Date().timeIntervalSince1970).timestampInSeconds 87 | } 88 | } 89 | previousNonce = ts 90 | return ts 91 | } 92 | } 93 | 94 | extension Network: CurrencyStoreType { 95 | public func isKnown(code: String) -> Bool { 96 | let uppercased = code.uppercased() 97 | if let overrides = apiCurrencyOverrides, let _ = overrides[uppercased] { 98 | return true 99 | } else if let overrides = currencyOverrides, let _ = overrides[uppercased] { 100 | return true 101 | } else if let _ = Currency.currencyLookupDictionary[uppercased] { 102 | return true 103 | } else { 104 | return false 105 | } 106 | } 107 | 108 | public func forCode(_ code: String) -> Currency { 109 | let uppercased = code.uppercased() 110 | if let overrides = apiCurrencyOverrides, let currency = overrides[uppercased] { 111 | return currency 112 | } else if let overrides = currencyOverrides, let currency = overrides[uppercased] { 113 | return currency 114 | } else if let currency = Currency.currencyLookupDictionary[uppercased] { 115 | return currency 116 | } else { 117 | return Currency(code: code) 118 | } 119 | } 120 | } 121 | 122 | -------------------------------------------------------------------------------- /Sources/Common/Protocols.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Protocols.swift 3 | // Cryptex 4 | // 5 | // Created by Sathyakumar Rajaraman on 12/31/17. 6 | // 7 | 8 | import Foundation 9 | 10 | public protocol CurrencyStoreType { 11 | func isKnown(code: String) -> Bool 12 | func forCode(_ code: String) -> Currency 13 | } 14 | 15 | 16 | -------------------------------------------------------------------------------- /Sources/Common/Ticker.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Ticker.swift 3 | // Cryptex 4 | // 5 | // Created by Sathyakumar Rajaraman on 1/6/18. 6 | // 7 | 8 | import Foundation 9 | 10 | public protocol TickerType: Comparable { 11 | var symbol: CurrencyPair { get } 12 | var price: NSDecimalNumber { get } 13 | var priceInOtherCurencies: [Currency: NSDecimalNumber]? { get set } 14 | var accountingCurrency: Currency { get set } 15 | } 16 | 17 | extension TickerType { 18 | 19 | var name: String { 20 | return symbol.quantity.name 21 | } 22 | 23 | func price(in currency: Currency) -> NSDecimalNumber { 24 | return priceInOtherCurencies?[currency] ?? .zero 25 | } 26 | 27 | public static func <(lhs: Self, rhs: Self) -> Bool { 28 | return lhs.price(in: lhs.accountingCurrency).compare(rhs.price(in: rhs.accountingCurrency)) == .orderedAscending 29 | } 30 | 31 | public static func >(lhs: Self, rhs: Self) -> Bool { 32 | return lhs.price(in: lhs.accountingCurrency).compare(rhs.price(in: rhs.accountingCurrency)) == .orderedDescending 33 | } 34 | 35 | public static func ==(lhs: Self, rhs: Self) -> Bool { 36 | return lhs.price(in: lhs.accountingCurrency).compare(rhs.price(in: rhs.accountingCurrency)) == .orderedSame 37 | } 38 | } 39 | 40 | public class Ticker: TickerType, CustomStringConvertible { 41 | public let symbol: CurrencyPair 42 | public let price: NSDecimalNumber 43 | public var priceInOtherCurencies: [Currency: NSDecimalNumber]? = [:] 44 | public var accountingCurrency: Currency = .USD 45 | 46 | public init(symbol: CurrencyPair, price: NSDecimalNumber) { 47 | self.symbol = symbol 48 | self.price = price 49 | } 50 | 51 | public var description: String { 52 | return "\n" + symbol.displaySymbol + " : " + price.stringValue + " " + symbol.price.code 53 | } 54 | } 55 | 56 | public protocol DisplayableTickerType { 57 | var name: String { get } 58 | var price: String { get } 59 | var formattedPriceInAccountingCurrency: String { get } 60 | } 61 | 62 | public struct DisplayableTicker: DisplayableTickerType { 63 | public var name: String 64 | public var price: String 65 | public var formattedPriceInAccountingCurrency: String 66 | } 67 | 68 | public protocol TickerTableViewDataSource { 69 | func sectionCount(viewType: TickerViewType) -> Int 70 | func tickerCount(section: Int, viewType: TickerViewType) -> Int 71 | func sectionHeaderTitle(section: Int, viewType: TickerViewType) -> String? 72 | func displayableTicker(section: Int, row: Int, viewType: TickerViewType) -> DisplayableTickerType? 73 | } 74 | 75 | public protocol BalanceTableViewDataSource { 76 | func getTotalBalance() -> NSDecimalNumber 77 | func balanceCount() -> Int 78 | func displayableBalance(row: Int) -> DisplayableBalanceType 79 | } 80 | -------------------------------------------------------------------------------- /Sources/Common/UserPreference.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserPreference.swift 3 | // Cryptex 4 | // 5 | // Created by Sathyakumar Rajaraman on 1/1/18. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct UserPreference { 11 | public var fiat: Currency 12 | public var crypto: Currency 13 | public var ignoredFiats: [Currency] 14 | //public var accounting: Accounting 15 | 16 | public init(fiat: Currency, crypto: Currency, ignoredFiats: [Currency]) { 17 | self.fiat = fiat 18 | self.crypto = crypto 19 | self.ignoredFiats = ignoredFiats 20 | } 21 | 22 | public static let USD_BTC = UserPreference(fiat: .USD, crypto: .Bitcoin, ignoredFiats: []) 23 | public static let USDT_BTC = UserPreference(fiat: .USDT, crypto: .Bitcoin, ignoredFiats: []) 24 | } 25 | 26 | 27 | -------------------------------------------------------------------------------- /Sources/Cryptopia.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Cryptopia.swift 3 | // Cryptex 4 | // 5 | // Created by Sathyakumar Rajaraman on 1/2/18. 6 | // 7 | 8 | import Foundation 9 | import CryptoSwift 10 | 11 | extension CurrencyPair { 12 | 13 | var cryptopiaLabel: String { 14 | return quantity.code + "/" + price.code 15 | } 16 | 17 | convenience init(cryptopiaLabel: String, currencyStore: CurrencyStoreType) { 18 | let currencySymbols = cryptopiaLabel.components(separatedBy: "/") 19 | let quantity = currencyStore.forCode(currencySymbols[0]) 20 | let price = currencyStore.forCode(currencySymbols[1]) 21 | self.init(quantity: quantity, price: price) 22 | } 23 | } 24 | 25 | public struct Cryptopia { 26 | 27 | public class Market: Ticker { 28 | public var tradePairId: Int = 0 29 | public var label: String = "" 30 | public var askPrice = NSDecimalNumber.zero 31 | public var bidPrice = NSDecimalNumber.zero 32 | public var low = NSDecimalNumber.zero 33 | public var high = NSDecimalNumber.zero 34 | public var volume = NSDecimalNumber.zero 35 | public var lastPrice = NSDecimalNumber.zero 36 | public var buyVolume = NSDecimalNumber.zero 37 | public var sellVolume = NSDecimalNumber.zero 38 | public var change = NSDecimalNumber.zero 39 | public var open = NSDecimalNumber.zero 40 | public var close = NSDecimalNumber.zero 41 | public var baseVolume = NSDecimalNumber.zero 42 | public var baseBuyVolume = NSDecimalNumber.zero 43 | public var baseSellVolume = NSDecimalNumber.zero 44 | 45 | public init(json: [String: Any], currencyStore: CurrencyStoreType) { 46 | tradePairId = json["TradePairId"] as? Int ?? 0 47 | label = json["Label"] as? String ?? "" 48 | askPrice = NSDecimalNumber(json["AskPrice"]) 49 | bidPrice = NSDecimalNumber(json["BidPrice"]) 50 | low = NSDecimalNumber(json["Low"]) 51 | high = NSDecimalNumber(json["High"]) 52 | volume = NSDecimalNumber(json["Volume"]) 53 | lastPrice = NSDecimalNumber(json["LastPrice"]) 54 | buyVolume = NSDecimalNumber(json["BuyVolume"]) 55 | sellVolume = NSDecimalNumber(json["SellVolume"]) 56 | change = NSDecimalNumber(json["Change"]) 57 | open = NSDecimalNumber(json["Open"]) 58 | close = NSDecimalNumber(json["Close"]) 59 | baseVolume = NSDecimalNumber(json["BaseVolume"]) 60 | baseBuyVolume = NSDecimalNumber(json["BaseBuyVolume"]) 61 | baseSellVolume = NSDecimalNumber(json["BaseSellVolume"]) 62 | super.init(symbol: CurrencyPair(cryptopiaLabel: label, currencyStore: currencyStore), price: lastPrice) 63 | } 64 | } 65 | 66 | public struct MarketHistory { 67 | public let tradePairId: Int 68 | public let label: String 69 | public let type: String 70 | public let price: NSDecimalNumber 71 | public let amount: NSDecimalNumber 72 | public let total: NSDecimalNumber 73 | public let timestamp: TimeInterval 74 | } 75 | 76 | public class Balance: Cryptex.Balance { 77 | public var currencyId: Int = 0 78 | public var symbol: String = "" 79 | public var total = NSDecimalNumber.zero 80 | public var available = NSDecimalNumber.zero 81 | public var unconfirmed = NSDecimalNumber.zero 82 | public var heldForTrades = NSDecimalNumber.zero 83 | public var pendingWithdraw = NSDecimalNumber.zero 84 | public var address: String = "" 85 | public var baseAddress: String = "" 86 | public var status: String = "" 87 | public var statusMessage: String = "" 88 | 89 | public init(json: [String: Any], currencyStore: CurrencyStoreType) { 90 | currencyId = json["CurrencyId"] as? Int ?? 0 91 | symbol = json["Symbol"] as? String ?? "" 92 | total = NSDecimalNumber(json["Total"]) 93 | available = NSDecimalNumber(json["Available"]) 94 | unconfirmed = NSDecimalNumber(json["Unconfirmed"]) 95 | heldForTrades = NSDecimalNumber(json["HeldForTrades"]) 96 | pendingWithdraw = NSDecimalNumber(json["PendingWithdraw"]) 97 | address = json["Address"] as? String ?? "" 98 | baseAddress = json["BaseAddress"] as? String ?? "" 99 | status = json["Status"] as? String ?? "" 100 | statusMessage = json["StatusMessage"] as? String ?? "" 101 | super.init(currency: currencyStore.forCode(symbol), quantity: total) 102 | } 103 | } 104 | 105 | public class CryptopiaCurrency: Currency { 106 | public var id: Int 107 | public var symbol: String 108 | public var algorithm: String 109 | public var withdrawFee: NSDecimalNumber 110 | public var minWithdraw: NSDecimalNumber 111 | public var minBaseTrade: NSDecimalNumber 112 | public var isTipEnabled: Bool 113 | public var minTip: NSDecimalNumber 114 | public var depositConfirmations: Int 115 | public var status: String 116 | public var statusMessage: String 117 | public var listingStatus: String 118 | 119 | public init(json: [String: Any]) { 120 | id = json["Id"] as? Int ?? 0 121 | symbol = json["Symbol"] as? String ?? "" 122 | algorithm = json["Algorithm"] as? String ?? "" 123 | withdrawFee = NSDecimalNumber(json["WithdrawFee"]) 124 | minWithdraw = NSDecimalNumber(json["MinWithdraw"]) 125 | minBaseTrade = NSDecimalNumber(json["MinBaseTrade"]) 126 | isTipEnabled = json["IsTipEnabled"] as? Bool ?? false 127 | minTip = NSDecimalNumber(json["MinTip"]) 128 | depositConfirmations = json["DepositConfirmations"] as? Int ?? 0 129 | status = json["Status"] as? String ?? "" 130 | statusMessage = json["StatusMessage"] as? String ?? "" 131 | listingStatus = json["ListingStatus"] as? String ?? "" 132 | super.init(name: json["Name"] as? String ?? "", code: symbol) 133 | } 134 | } 135 | 136 | public struct TradePair { 137 | public let id: Int 138 | public let label: String 139 | public let curency: String 140 | public let symbol: String 141 | public let baseCurrency: String 142 | public let baseSymbol: String 143 | public let status: String 144 | public let statusMessage: String 145 | public let tradeFee: NSDecimalNumber 146 | public let minimumTrade: NSDecimalNumber 147 | public let maximumTrade: NSDecimalNumber 148 | public let minimumBaseTrade: NSDecimalNumber 149 | public let maximumBaseTrade : NSDecimalNumber 150 | public let minimumPrice : NSDecimalNumber 151 | public let maximumPrice : NSDecimalNumber 152 | } 153 | 154 | public enum API { 155 | case getMarkets 156 | case getBalance 157 | // 158 | case getCurrencies 159 | case getTradePairs 160 | } 161 | 162 | public class Store: ExchangeDataStore { 163 | 164 | override fileprivate init() { 165 | super.init() 166 | name = "Cryptopia" 167 | accountingCurrency = .USDT 168 | } 169 | 170 | public var tickersResponse: HTTPURLResponse? = nil 171 | public var balanceResponse: HTTPURLResponse? = nil 172 | public var currenciesResponse: (response: HTTPURLResponse?, currencies: [CryptopiaCurrency]) = (nil, []) 173 | } 174 | 175 | public class Service: Network, TickerServiceType, BalanceServiceType { 176 | public let store = Store() 177 | 178 | public func getTickers(completion: @escaping (ResponseType) -> Void) { 179 | let apiType = Cryptopia.API.getMarkets 180 | if apiType.checkInterval(response: store.tickersResponse) { 181 | completion(.cached) 182 | } else { 183 | cryptopiaDataTaskFor(api: apiType) { (response) in 184 | guard let tickerArray = response.json as? [[String: Any]] else { return } 185 | var tickers: [Market] = [] 186 | for ticker in tickerArray { 187 | let ticker = Market(json: ticker, currencyStore: self) 188 | tickers.append(ticker) 189 | } 190 | self.store.setTickersInDictionary(tickers: tickers) 191 | 192 | self.store.tickersResponse = response.httpResponse 193 | completion(.fetched) 194 | 195 | }.resume() 196 | } 197 | } 198 | 199 | public func getBalances(completion: @escaping (ResponseType) -> Void) { 200 | let apiType = Cryptopia.API.getBalance 201 | 202 | if apiType.checkInterval(response: store.balanceResponse) { 203 | 204 | completion(.cached) 205 | 206 | } else { 207 | getCurrencies { (_) in 208 | self.getTickers { (_) in 209 | self.cryptopiaDataTaskFor(api: apiType) { (response) in 210 | 211 | guard let json = response.json as? [[String: Any]] else { return } 212 | let balances = json.map { Balance(json: $0, currencyStore: self) }.filter { $0.available != .zero } 213 | self.store.balances = balances 214 | self.store.balanceResponse = response.httpResponse 215 | completion(.fetched) 216 | 217 | }.resume() 218 | } 219 | } 220 | } 221 | } 222 | 223 | public func getCurrencies(completion: @escaping (ResponseType) -> Void) { 224 | let apiType = Cryptopia.API.getCurrencies 225 | 226 | if apiType.checkInterval(response: store.currenciesResponse.response) { 227 | completion(.cached) 228 | } else { 229 | cryptopiaDataTaskFor(api: apiType) { (response) in 230 | guard let json = response.json as? [[String: Any]] else { return } 231 | let currencies = json.map { CryptopiaCurrency(json: $0) } 232 | 233 | self.store.currenciesResponse = (response.httpResponse, currencies) 234 | self.apiCurrencyOverrides = Currency.dictionary(array: currencies) 235 | completion(.fetched) 236 | 237 | }.resume() 238 | } 239 | } 240 | 241 | func cryptopiaDataTaskFor(api: APIType, completion: ((Response) -> Void)?) -> URLSessionDataTask { 242 | return dataTaskFor(api: api) { (response) in 243 | guard let json = response.json as? [String: Any] else { return } 244 | if let success = json["Success"] as? Bool, let jsonData = json["Data"], success == true { 245 | var tempResponse = response 246 | tempResponse.json = jsonData 247 | completion?(tempResponse) 248 | } else { 249 | // Handle error here 250 | if let message = json["Message"] { 251 | api.print(message, content: .response) 252 | } 253 | if let apiError = json["Error"] { 254 | api.print(apiError, content: .response) 255 | } 256 | } 257 | } 258 | } 259 | 260 | public override func requestFor(api: APIType) -> NSMutableURLRequest { 261 | let mutableURLRequest = api.mutableRequest 262 | if let key = key, let secret = secret, api.authenticated { 263 | if let url = mutableURLRequest.url?.absoluteString.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlHostAllowed), 264 | let data = api.postData.data, let string = data.string { 265 | api.print("Request Data: \(string)", content: .response) 266 | // POST payload 267 | if case .POST = api.httpMethod { 268 | mutableURLRequest.httpBody = data 269 | } 270 | let nonce = "\(Date().timeIntervalSince1970)" 271 | let prehash = key + api.httpMethod.rawValue + url.lowercased() + nonce + data.md5().base64EncodedString() 272 | if let bytes = Data(base64Encoded: secret)?.bytes, let hmac_sha256 = try? HMAC(key: bytes, variant: .sha256).authenticate(Array(prehash.utf8)) { 273 | let authHeader = "amx " + key + ":" + Data(bytes: hmac_sha256).base64EncodedString() + ":" + nonce 274 | mutableURLRequest.setValue(authHeader, forHTTPHeaderField: "Authorization") 275 | } 276 | } 277 | } 278 | return mutableURLRequest 279 | } 280 | } 281 | } 282 | 283 | extension Cryptopia.API: APIType { 284 | public var host: String { 285 | return "https://www.cryptopia.co.nz/api" 286 | } 287 | 288 | public var path: String { 289 | switch self { 290 | case .getCurrencies: return "/GetCurrencies" 291 | case .getTradePairs: return "/GetTradePairs" 292 | case .getMarkets: return "/GetMarkets" 293 | case .getBalance: return "/GetBalance" 294 | } 295 | } 296 | 297 | public var httpMethod: HttpMethod { 298 | switch self { 299 | case .getCurrencies: return .GET 300 | case .getTradePairs: return .GET 301 | case .getMarkets: return .GET 302 | case .getBalance: return .POST 303 | } 304 | } 305 | 306 | public var authenticated: Bool { 307 | switch self { 308 | case .getCurrencies: return false 309 | case .getTradePairs: return false 310 | case .getMarkets: return false 311 | case .getBalance: return true 312 | } 313 | } 314 | 315 | public var loggingEnabled: LogLevel { 316 | switch self { 317 | case .getCurrencies: return .url 318 | case .getTradePairs: return .url 319 | case .getMarkets: return .url 320 | case .getBalance: return .url 321 | } 322 | } 323 | 324 | public var postData: [String : String] { 325 | switch self { 326 | case .getCurrencies: return [:] 327 | case .getTradePairs: return [:] 328 | case .getMarkets: return [:] 329 | case .getBalance: return [:] 330 | } 331 | } 332 | 333 | public var refetchInterval: TimeInterval { 334 | switch self { 335 | case .getCurrencies: return .aMonth 336 | case .getTradePairs: return .aMonth 337 | case .getMarkets: return .aMinute 338 | case .getBalance: return .aMinute 339 | } 340 | } 341 | } 342 | 343 | -------------------------------------------------------------------------------- /Sources/GDAX.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GDAX.swift 3 | // Cryptex 4 | // 5 | // Created by Sathyakumar Rajaraman on 12/31/17. 6 | // Copyright © 2017 Sathyakumar. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import CryptoSwift 11 | 12 | // GDAX has been renamed to Coinbase Pro. All varables wills stay the same and the API will have the same functionality but just point to the new endpoint https://api.pro.coinbase.com/ 13 | 14 | extension CurrencyPair { 15 | 16 | var gdaxProductId: String { 17 | return quantity.code + "-" + price.code 18 | } 19 | 20 | convenience init(gdaxProductId: String, currencyStore: CurrencyStoreType) { 21 | let currencySymbols = gdaxProductId.components(separatedBy: "-") 22 | let quantity = currencyStore.forCode(currencySymbols[0]) 23 | let price = currencyStore.forCode(currencySymbols[1]) 24 | self.init(quantity: quantity, price: price) 25 | } 26 | } 27 | 28 | public struct GDAX { 29 | public struct Product { 30 | public var id: CurrencyPair 31 | public var baseCurrency: Currency 32 | public var quoteCurrency: Currency 33 | public var baseMinSize: NSDecimalNumber 34 | public var baseMaxSize: NSDecimalNumber 35 | public var quoteIncrement: NSDecimalNumber 36 | public var displayName: String 37 | public var marginEnabled: Bool 38 | 39 | public init(json: [String: Any], currencyStore: CurrencyStoreType) { 40 | self.id = CurrencyPair(gdaxProductId: json["id"] as! String, currencyStore: currencyStore) 41 | self.baseCurrency = currencyStore.forCode(json["base_currency"] as! String) 42 | self.quoteCurrency = currencyStore.forCode(json["quote_currency"] as! String) 43 | self.baseMinSize = NSDecimalNumber(json["base_min_size"]) 44 | self.baseMaxSize = NSDecimalNumber(json["base_max_size"]) 45 | self.quoteIncrement = NSDecimalNumber(json["quote_increment"]) 46 | self.displayName = json["display_name"] as! String 47 | self.marginEnabled = json["margin_enabled"] as! Bool 48 | } 49 | } 50 | 51 | public class Ticker: Cryptex.Ticker { 52 | public var tradeId: Int 53 | public var size: NSDecimalNumber 54 | public var bid: NSDecimalNumber 55 | public var ask: NSDecimalNumber 56 | public var volume: NSDecimalNumber 57 | public var time: Date 58 | 59 | public init(json: [String: Any], symbol: CurrencyPair) { 60 | self.tradeId = json["trade_id"] as? Int ?? 0 61 | self.size = NSDecimalNumber(json["size"]) 62 | self.bid = NSDecimalNumber(json["bid"]) 63 | self.ask = NSDecimalNumber(json["ask"]) 64 | self.volume = NSDecimalNumber(json["volume"]) 65 | 66 | let dateFormatter = DateFormatter() 67 | dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'Z" //"yyyy-MM-dd'T'HH:mm:ss.SSSSSSZZZZZ" 68 | if let timeString = json["time"] as? String, let date = dateFormatter.date(from: timeString) { 69 | self.time = date 70 | } else { 71 | self.time = Date() 72 | } 73 | super.init(symbol: symbol, price: NSDecimalNumber(json["price"])) 74 | } 75 | } 76 | 77 | public class Account: Cryptex.Balance { 78 | public var id: String 79 | public var available: NSDecimalNumber 80 | public var hold: NSDecimalNumber 81 | public var profileId: String 82 | 83 | public init?(json: [String: Any], currencyStore: CurrencyStoreType) { 84 | id = json["id"] as? String ?? "" 85 | available = NSDecimalNumber(json["available"]) 86 | hold = NSDecimalNumber(json["hold"]) 87 | profileId = json["profile_id"] as? String ?? "" 88 | super.init(currency: currencyStore.forCode( json["currency"] as? String ?? "" ), quantity: NSDecimalNumber(json["balance"])) 89 | } 90 | } 91 | 92 | public class Store: ExchangeDataStore { 93 | 94 | override fileprivate init() { 95 | super.init() 96 | name = "GDAX" 97 | } 98 | 99 | public var productsResponse: (response: HTTPURLResponse?, products: [Product]) = (nil, []) 100 | public var tickersResponse: [String: (response: HTTPURLResponse?, ticker: GDAX.Ticker)] = [:] 101 | public var accountsResponse: (response: HTTPURLResponse?, accounts: [Account]) = (nil, []) 102 | } 103 | 104 | public enum API { 105 | case getProducts 106 | case getProductTicker(CurrencyPair) 107 | case listAccounts 108 | } 109 | 110 | public class Service: Network { 111 | 112 | private let passphrase: String 113 | public let store = Store() 114 | 115 | public required init(key: String?, secret: String?, passphrase: String, session: URLSession, userPreference: UserPreference, currencyOverrides: [String: Currency]?) { 116 | self.passphrase = passphrase 117 | super.init(key: key, secret: secret, session: session, userPreference: userPreference, currencyOverrides: nil) 118 | } 119 | 120 | public func getProducts(completion: @escaping (ResponseType) -> Void) { 121 | let apiType = GDAX.API.getProducts 122 | if apiType.checkInterval(response: store.productsResponse.response) { 123 | completion(.cached) 124 | } else { 125 | gdaxDataTaskFor(api: apiType) { (response) in 126 | guard let json = response.json as? [[String: Any]] else { 127 | print("Error: Cast Failed in \(#function)") 128 | return 129 | } 130 | 131 | self.store.productsResponse = (response.httpResponse, json.map({GDAX.Product(json: $0, currencyStore: self)}).filter { self.userPreference.ignoredFiats.contains($0.quoteCurrency) == false }) 132 | 133 | completion(.fetched) 134 | 135 | }.resume() 136 | } 137 | } 138 | 139 | public func getTicker(symbol: CurrencyPair, completion: @escaping (CurrencyPair, ResponseType) -> Void) { 140 | 141 | let apiType = GDAX.API.getProductTicker(symbol) 142 | 143 | if apiType.checkInterval(response: store.tickersResponse[symbol.displaySymbol]?.response) { 144 | 145 | completion(symbol, .cached) 146 | 147 | } else { 148 | 149 | gdaxDataTaskFor(api: apiType) { (response) in 150 | 151 | guard let json = response.json as? [String: Any] else { return } 152 | let ticker = GDAX.Ticker(json: json, symbol: symbol) 153 | 154 | self.store.setTicker(ticker: ticker, symbol: symbol.displaySymbol) 155 | self.store.tickersResponse[symbol.displaySymbol] = (response.httpResponse, ticker) 156 | completion(symbol, .fetched) 157 | 158 | }.resume() 159 | } 160 | } 161 | 162 | public func listAccounts(completion: @escaping (ResponseType) -> Void) { 163 | 164 | let apiType = GDAX.API.listAccounts 165 | 166 | if apiType.checkInterval(response: store.accountsResponse.response) { 167 | 168 | completion(.cached) 169 | 170 | } else { 171 | gdaxDataTaskFor(api: apiType) { (response) in 172 | guard let json = response.json as? [[String: Any]] else { 173 | print("Error: Cast Failed in \(#function)") 174 | return 175 | } 176 | let accounts = json.compactMap {GDAX.Account(json: $0, currencyStore: self)} 177 | self.store.balances = accounts 178 | self.store.accountsResponse = (response.httpResponse, accounts) 179 | completion(.fetched) 180 | }.resume() 181 | } 182 | } 183 | 184 | func gdaxDataTaskFor(api: APIType, completion: ((Response) -> Void)?) -> URLSessionDataTask { 185 | return dataTaskFor(api: api) { (response) in 186 | // Handle error here 187 | completion?(response) 188 | } 189 | } 190 | 191 | public override func requestFor(api: APIType) -> NSMutableURLRequest { 192 | let mutableURLRequest = api.mutableRequest 193 | 194 | if let key = key, let secret = secret, api.authenticated { 195 | 196 | var postDataString = "" 197 | if let data = api.postData.data, let string = data.string, api.postData.count > 0 { 198 | 199 | postDataString = string 200 | 201 | // POST payload 202 | if case .POST = api.httpMethod { 203 | mutableURLRequest.httpBody = data 204 | } 205 | 206 | api.print("Request Data: \(postDataString)", content: .response) 207 | } 208 | 209 | let ts = "\(Date().timeIntervalSince1970)" 210 | var prehash = ts + api.httpMethod.rawValue + api.path + postDataString 211 | 212 | if let bytes = Data(base64Encoded: secret)?.bytes, let hmac_sha = try? HMAC(key: bytes, variant: .sha256).authenticate(Array(prehash.utf8)), let signature = hmac_sha.toBase64() { 213 | mutableURLRequest.setValue(signature, forHTTPHeaderField: "CB-ACCESS-SIGN") 214 | } 215 | 216 | mutableURLRequest.setValue(ts, forHTTPHeaderField: "CB-ACCESS-TIMESTAMP") 217 | mutableURLRequest.setValue(passphrase, forHTTPHeaderField: "CB-ACCESS-PASSPHRASE") 218 | mutableURLRequest.setValue(key, forHTTPHeaderField: "CB-ACCESS-KEY") 219 | } 220 | 221 | return mutableURLRequest 222 | } 223 | } 224 | } 225 | 226 | extension GDAX.API: APIType { 227 | public var host: String { 228 | return "https://api.pro.coinbase.com" 229 | } 230 | 231 | public var path: String { 232 | switch self { 233 | case .getProducts: return "/products" 234 | case .getProductTicker(let currencyPair): return "/products/\(currencyPair.gdaxProductId)/ticker" 235 | case .listAccounts: return "/accounts" 236 | } 237 | } 238 | 239 | public var httpMethod: HttpMethod { 240 | return .GET 241 | } 242 | 243 | public var authenticated: Bool { 244 | switch self { 245 | case .listAccounts: return true 246 | default: return false 247 | } 248 | } 249 | 250 | public var loggingEnabled: LogLevel { 251 | switch self { 252 | case .getProducts: return .url 253 | case .getProductTicker(_): return .url 254 | case .listAccounts: return .url 255 | } 256 | } 257 | 258 | public var postData: [String: String] { 259 | return [:] 260 | } 261 | 262 | public var refetchInterval: TimeInterval { 263 | switch self { 264 | case .getProducts: return .aMonth 265 | case .getProductTicker(_): return .aMinute 266 | case .listAccounts: return .aMinute 267 | } 268 | } 269 | } 270 | 271 | extension GDAX.Service: TickerServiceType, BalanceServiceType { 272 | 273 | public func getBalances(completion: @escaping (ResponseType) -> Void) { 274 | getProducts(completion: { (_) in 275 | var tasks: [String: Bool] = [:] 276 | 277 | self.store.productsResponse.products.forEach { product in 278 | tasks[product.id.displaySymbol] = false 279 | } 280 | 281 | self.store.productsResponse.products.forEach { product in 282 | self.getTicker(symbol: product.id, completion: { _,_ in 283 | tasks[product.id.displaySymbol] = true 284 | 285 | let flag = tasks.values.reduce(true, { (result, value) -> Bool in 286 | return result && value 287 | }) 288 | 289 | if flag { 290 | self.listAccounts(completion: { (responseType) in 291 | completion(responseType) 292 | }) 293 | } 294 | }) 295 | } 296 | }) 297 | } 298 | 299 | public func getTickers(completion: @escaping (ResponseType) -> Void) { 300 | getProducts(completion: { (_) in 301 | self.store.productsResponse.products.forEach { product in 302 | self.getTicker(symbol: product.id, completion: { (_, responseType) in 303 | completion(responseType) 304 | }) 305 | } 306 | }) 307 | } 308 | } 309 | -------------------------------------------------------------------------------- /Sources/Koinex.swift: -------------------------------------------------------------------------------- 1 | // 2 | // koinex.swift 3 | // Cryptex 4 | // 5 | // Created by Sathyakumar Rajaraman on 01/01/18. 6 | // Copyright © 2018 Sathyakumar. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public extension CurrencyPair { 12 | var koinexSymbol: String { 13 | return quantity.code 14 | } 15 | } 16 | 17 | public struct Koinex { 18 | 19 | public class Store: ExchangeDataStore { 20 | 21 | override fileprivate init() { 22 | super.init() 23 | name = "Koinex" 24 | accountingCurrency = .INR 25 | } 26 | 27 | public var tickerResponse: HTTPURLResponse? = nil 28 | } 29 | 30 | public enum API { 31 | case ticker 32 | } 33 | 34 | public class Service: Network, TickerServiceType { 35 | 36 | public let store = Store() 37 | 38 | public func getTickers(completion: @escaping (ResponseType) -> Void) { 39 | let apiType = Koinex.API.ticker 40 | if apiType.checkInterval(response: store.tickerResponse) { 41 | completion(.cached) 42 | } else { 43 | dataTaskFor(api: apiType, completion: { (response) in 44 | guard let json = response.json as? [String: Any], let prices = json["prices"] as? [String: Any] else { 45 | print("Error: Cast Failed in \(#function)") 46 | return 47 | } 48 | var tickers: [Ticker] = [] 49 | for (key, value) in prices { 50 | let currency = self.forCode(key) 51 | let inr = self.forCode("inr") 52 | let symbol = CurrencyPair(quantity: currency, price: inr) 53 | let price = NSDecimalNumber(value) 54 | let ticker = Ticker(symbol: symbol, price: price) 55 | tickers.append(ticker) 56 | } 57 | self.store.setTickersInDictionary(tickers: tickers) 58 | self.store.tickerResponse = response.httpResponse 59 | completion(.fetched) 60 | }).resume() 61 | } 62 | } 63 | } 64 | } 65 | 66 | extension Koinex.API: APIType { 67 | 68 | public var host: String { 69 | return "https://koinex.in/api" 70 | } 71 | 72 | public var path: String { 73 | switch self { 74 | case .ticker: return "/ticker" 75 | } 76 | } 77 | 78 | public var httpMethod: HttpMethod { 79 | return .GET 80 | } 81 | 82 | public var authenticated: Bool { 83 | return false 84 | } 85 | 86 | public var loggingEnabled: LogLevel { 87 | switch self { 88 | case .ticker: return .url 89 | } 90 | } 91 | 92 | public var postData: [String: String] { 93 | return [:] 94 | } 95 | 96 | public var refetchInterval: TimeInterval { 97 | switch self { 98 | case .ticker: return .aMinute 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /Sources/Kraken.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Kraken.swift 3 | // Cryptex 4 | // 5 | // Created by Sathyakumar Rajaraman on 12/31/17. 6 | // Copyright © 2017 Sathyakumar. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import CryptoSwift 11 | 12 | public struct Kraken { 13 | 14 | public class Ticker: Cryptex.Ticker { 15 | public let mid: NSDecimalNumber 16 | public let bid: NSDecimalNumber 17 | public let ask: NSDecimalNumber 18 | public let lastPrice: NSDecimalNumber 19 | public let low: NSDecimalNumber 20 | public let high: NSDecimalNumber 21 | public let volume: NSDecimalNumber 22 | public let timestamp: NSDecimalNumber 23 | 24 | public init(json: [String: String], for symbol: CurrencyPair) { 25 | mid = NSDecimalNumber(json["mid"]) 26 | bid = NSDecimalNumber(json["bid"]) 27 | ask = NSDecimalNumber(json["ask"]) 28 | lastPrice = NSDecimalNumber(json["last_price"]) 29 | low = NSDecimalNumber(json["low"]) 30 | high = NSDecimalNumber(json["high"]) 31 | volume = NSDecimalNumber(json["volume"]) 32 | timestamp = NSDecimalNumber(json["timestamp"]) 33 | super.init(symbol: symbol, price: lastPrice) 34 | } 35 | } 36 | 37 | public class Balance: Cryptex.Balance { 38 | 39 | public let type: String 40 | public let amount: NSDecimalNumber 41 | public let available: NSDecimalNumber 42 | 43 | public init(json: [String: String], currencyStore: CurrencyStoreType) { 44 | type = json["type"] ?? "" 45 | amount = NSDecimalNumber(json["amount"]) 46 | available = NSDecimalNumber(json["available"]) 47 | super.init(currency: currencyStore.forCode(json["currency"] ?? ""), quantity: available) 48 | } 49 | } 50 | 51 | public class Store: ExchangeDataStore { 52 | 53 | override fileprivate init() { 54 | super.init() 55 | name = "Kraken" 56 | accountingCurrency = .USD 57 | } 58 | 59 | public var symbolsResponse: (response: HTTPURLResponse?, symbols: [CurrencyPair]) = (nil, []) 60 | public var tickerResponse: [String: HTTPURLResponse] = [:] 61 | public var balanceResponse: HTTPURLResponse? = nil 62 | public var accountFeesResponse: HTTPURLResponse? = nil 63 | } 64 | 65 | public enum API { 66 | case getServerTime 67 | case getAssetInfo 68 | case getTradableAssetPairs 69 | case getTickerInformation(String) 70 | case getOHLCData 71 | case getOrderBook 72 | case getRecentTrades 73 | case getRecentSpreadData 74 | // private 75 | case getAccountBalance 76 | case getTradeBalance 77 | case getOpenOrders 78 | case getClosedOrders 79 | case queryOrdersInfo 80 | case getTradesHistory 81 | case queryTradesInfo 82 | case getOpenPositions 83 | case getLedgersInfo 84 | case queryLedgers 85 | case getTradeVolume 86 | case addStandardOrder 87 | case cancelOpenOrder 88 | case getDepositMethods 89 | case getDepositAddresses 90 | case getStatusOfRecentDeposits 91 | case getWithdrawalInformation 92 | case withdrawFunds 93 | case getStatusOfRecentWithdrawals 94 | case requestWithdrawalCancelation 95 | } 96 | 97 | public class Service: Network { 98 | 99 | public let store = Store() 100 | 101 | public func getSymbols(completion: @escaping (ResponseType) -> Void) { 102 | let apiType = Kraken.API.getAssetInfo 103 | if apiType.checkInterval(response: store.symbolsResponse.response) { 104 | completion(.cached) 105 | } else { 106 | krakenDataTaskFor(api: apiType, completion: { (response) in 107 | guard let stringArray = response.json as? [String] else { 108 | completion(.unexpected(response)) 109 | return 110 | } 111 | let geminiSymbols = stringArray.compactMap { CurrencyPair(symbol: $0, currencyStore: self) } 112 | self.store.symbolsResponse = (response.httpResponse, geminiSymbols) 113 | completion(.fetched) 114 | }).resume() 115 | } 116 | } 117 | 118 | public func getTicker(symbol: CurrencyPair, completion: @escaping (CurrencyPair, ResponseType) -> Void) { 119 | let apiType = Kraken.API.getTickerInformation(symbol.displaySymbol) 120 | if apiType.checkInterval(response: store.tickerResponse[symbol.displaySymbol]) { 121 | completion(symbol, .cached) 122 | } else { 123 | krakenDataTaskFor(api: apiType, completion: { (response) in 124 | guard let json = response.json as? [String: String] else { 125 | completion(symbol, .unexpected(response)) 126 | return 127 | } 128 | self.store.setTicker(ticker: Ticker(json: json, for: symbol), symbol: symbol.displaySymbol) 129 | self.store.tickerResponse[symbol.displaySymbol] = response.httpResponse 130 | completion(symbol, .fetched) 131 | }).resume() 132 | } 133 | } 134 | 135 | public func getAccountBalances(completion: @escaping (ResponseType) -> Void) { 136 | let apiType = Kraken.API.getAccountBalance 137 | if apiType.checkInterval(response: store.balanceResponse) { 138 | completion(.cached) 139 | } else { 140 | krakenDataTaskFor(api: apiType) { (response) in 141 | 142 | guard let json = response.json as? Dictionary else { 143 | print("Error: Cast Failed in \(#function)") 144 | return 145 | } 146 | if let arrayOfCryptoBalances = json["result"] as? Dictionary { 147 | var balances: [Balance] = [] 148 | 149 | for cryptoBalance in arrayOfCryptoBalances { 150 | let newBalance = ["type": cryptoBalance.key, 151 | "amount": cryptoBalance.value, 152 | "available": cryptoBalance.value] 153 | 154 | balances.append(Balance(json: newBalance, currencyStore: self)) 155 | } 156 | 157 | self.store.balances = balances 158 | self.store.balanceResponse = response.httpResponse 159 | completion(.fetched) 160 | } 161 | }.resume() 162 | } 163 | } 164 | 165 | private func krakenDataTaskFor(api: APIType, completion: ((Response) -> Void)?) -> URLSessionDataTask { 166 | return dataTaskFor(api: api) { (response) in 167 | // Handle error here 168 | completion?(response) 169 | } 170 | } 171 | 172 | public override func requestFor(api: APIType) -> NSMutableURLRequest { 173 | let mutableURLRequest = api.mutableRequest 174 | 175 | if let key = key, let secret = secret, api.authenticated { 176 | 177 | var postDataDictionary = api.postData 178 | 179 | let timestamp = NSDate().timeIntervalSince1970 180 | let nonce = "\(Int64(timestamp*1000))" 181 | postDataDictionary["nonce"] = nonce 182 | //postDataDictionary?["otp"] = "123456" 183 | 184 | let postDataString = postDataDictionary.queryString 185 | 186 | // POST payload 187 | if case .POST = api.httpMethod { 188 | mutableURLRequest.httpBody = postDataString.utf8Data() 189 | } 190 | 191 | api.print("Request Data: \(postDataString)", content: .response) 192 | 193 | let prehashBytes = Array(api.path.utf8) + SHA2(variant: .sha256).calculate(for: Array((nonce + postDataString).utf8)) 194 | if let bytes = Data(base64Encoded: secret)?.bytes, 195 | let hmac_sha = try? HMAC(key: bytes, variant: .sha512).authenticate(prehashBytes), 196 | let signature = hmac_sha.toBase64() 197 | { 198 | mutableURLRequest.setValue(signature, forHTTPHeaderField: "API-SIGN") 199 | } 200 | 201 | mutableURLRequest.setValue(key, forHTTPHeaderField: "API-KEY") } 202 | 203 | return mutableURLRequest 204 | } 205 | } 206 | } 207 | 208 | extension Kraken.API: APIType { 209 | public var host: String { 210 | return "https://api.kraken.com" 211 | } 212 | 213 | public var path: String { 214 | switch self { 215 | case .getServerTime: return "/0/public/Time" 216 | case .getAssetInfo: return "/0/public/Assets" 217 | case .getTradableAssetPairs: return "/0/public/AssetPairs" 218 | case .getTickerInformation(_): return "/0/public/Ticker" 219 | case .getOHLCData: return "/0/public/OHLC" 220 | case .getOrderBook: return "/0/public/Depth" 221 | case .getRecentTrades: return "/0/public/Trades" 222 | case .getRecentSpreadData: return "/0/public/Spread" 223 | // private 224 | case .getAccountBalance: return "/0/private/Balance" 225 | case .getTradeBalance: return "/0/private/TradeBalance" 226 | case .getOpenOrders: return "/0/private/OpenOrders" 227 | case .getClosedOrders: return "/0/private/ClosedOrders" 228 | case .queryOrdersInfo: return "/0/private/QueryOrders" 229 | case .getTradesHistory: return "/0/private/TradesHistory" 230 | case .queryTradesInfo: return "/0/private/QueryTrades" 231 | case .getOpenPositions: return "/0/private/OpenPositions" 232 | case .getLedgersInfo: return "/0/private/Ledgers" 233 | case .queryLedgers: return "/0/private/QueryLedgers" 234 | case .getTradeVolume: return "/0/private/TradeVolume" 235 | case .addStandardOrder: return "/0/private/AddOrder" 236 | case .cancelOpenOrder: return "/0/private/CancelOrder" 237 | case .getDepositMethods: return "/0/private/DepositMethods" 238 | case .getDepositAddresses: return "/0/private/DepositAddresses" 239 | case .getStatusOfRecentDeposits: return "/0/private/DepositStatus" 240 | case .getWithdrawalInformation: return "/0/private/WithdrawInfo" 241 | case .withdrawFunds: return "/0/private/Withdraw" 242 | case .getStatusOfRecentWithdrawals: return "/0/private/WithdrawStatus" 243 | case .requestWithdrawalCancelation: return "/0/private/WithdrawCancel" 244 | } 245 | } 246 | 247 | public var httpMethod: HttpMethod { 248 | switch self { 249 | case .getServerTime: return .GET 250 | case .getAssetInfo: return .GET 251 | case .getTradableAssetPairs: return .GET 252 | case .getTickerInformation(_): return .GET 253 | case .getOHLCData: return .GET 254 | case .getOrderBook: return .GET 255 | case .getRecentTrades: return .GET 256 | case .getRecentSpreadData: return .GET 257 | case .getAccountBalance: return .POST 258 | case .getTradeBalance: return .POST 259 | case .getOpenOrders: return .POST 260 | case .getClosedOrders: return .POST 261 | case .queryOrdersInfo: return .POST 262 | case .getTradesHistory: return .POST 263 | case .queryTradesInfo: return .POST 264 | case .getOpenPositions: return .POST 265 | case .getLedgersInfo: return .POST 266 | case .queryLedgers: return .POST 267 | case .getTradeVolume: return .POST 268 | case .addStandardOrder: return .POST 269 | case .cancelOpenOrder: return .POST 270 | case .getDepositMethods: return .POST 271 | case .getDepositAddresses: return .POST 272 | case .getStatusOfRecentDeposits: return .POST 273 | case .getWithdrawalInformation: return .POST 274 | case .withdrawFunds: return .POST 275 | case .getStatusOfRecentWithdrawals: return .POST 276 | case .requestWithdrawalCancelation: return .POST 277 | } 278 | } 279 | 280 | public var authenticated: Bool { 281 | switch self { 282 | case .getServerTime: return false 283 | case .getAssetInfo: return false 284 | case .getTradableAssetPairs: return false 285 | case .getTickerInformation(_): return false 286 | case .getOHLCData: return false 287 | case .getOrderBook: return false 288 | case .getRecentTrades: return false 289 | case .getRecentSpreadData: return false 290 | case .getAccountBalance: return true 291 | case .getTradeBalance: return true 292 | case .getOpenOrders: return true 293 | case .getClosedOrders: return true 294 | case .queryOrdersInfo: return true 295 | case .getTradesHistory: return true 296 | case .queryTradesInfo: return true 297 | case .getOpenPositions: return true 298 | case .getLedgersInfo: return true 299 | case .queryLedgers: return true 300 | case .getTradeVolume: return true 301 | case .addStandardOrder: return true 302 | case .cancelOpenOrder: return true 303 | case .getDepositMethods: return true 304 | case .getDepositAddresses: return true 305 | case .getStatusOfRecentDeposits: return true 306 | case .getWithdrawalInformation: return true 307 | case .withdrawFunds: return true 308 | case .getStatusOfRecentWithdrawals: return true 309 | case .requestWithdrawalCancelation: return true 310 | } 311 | } 312 | 313 | public var loggingEnabled: LogLevel { 314 | return .response 315 | } 316 | 317 | public var postData: [String: String] { 318 | return [:] 319 | } 320 | 321 | public var refetchInterval: TimeInterval { 322 | return .aMinute 323 | } 324 | } 325 | 326 | extension Kraken.Service: TickerServiceType, BalanceServiceType { 327 | 328 | public func getTickers(completion: @escaping ( ResponseType) -> Void) { 329 | getSymbols(completion: { _ in 330 | 331 | var tasks: [String: Bool] = [:] 332 | 333 | self.store.symbolsResponse.symbols.forEach { symbol in 334 | tasks[symbol.displaySymbol] = false 335 | } 336 | 337 | self.store.symbolsResponse.symbols.forEach { symbol in 338 | self.getTicker(symbol: symbol, completion: { (currencyPair, responseType) in 339 | tasks[currencyPair.displaySymbol] = true 340 | 341 | let flag = tasks.values.reduce(true, { (result, value) -> Bool in 342 | return result && value 343 | }) 344 | if flag { 345 | completion(responseType) 346 | } 347 | }) 348 | } 349 | }) 350 | } 351 | 352 | public func getBalances(completion: @escaping ( ResponseType) -> Void) { 353 | getTickers(completion: { (_) in 354 | self.getAccountBalances(completion: { (responseType) in 355 | completion(responseType) 356 | }) 357 | }) 358 | } 359 | } 360 | -------------------------------------------------------------------------------- /Tests/CryptexTests/CryptexTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import Cryptex 3 | 4 | class CryptexTests: XCTestCase { 5 | func testExample() { 6 | // This is an example of a functional test case. 7 | // Use XCTAssert and related functions to verify your tests produce the correct 8 | // results. 9 | // XCTAssertEqual(Cryptex.text, "Hello, World!") 10 | } 11 | 12 | 13 | static var allTests = [ 14 | ("testExample", testExample), 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import CryptexTests 3 | 4 | //XCTMain([ 5 | // testCase(CryptexTests.allTests), 6 | //]) 7 | -------------------------------------------------------------------------------- /UI/CryptEx/API/API.swift: -------------------------------------------------------------------------------- 1 | // 2 | // API.swift 3 | // CryptExUI 4 | // 5 | // Created by Sathyakumar Rajaraman on 3/17/18. 6 | // Copyright © 2018 Sathyakumar. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct API { 12 | 13 | struct Gemini { 14 | static let key = "" 15 | static let secret = "" 16 | } 17 | 18 | struct Poloniex { 19 | static let key = "" 20 | static let secret = "" 21 | } 22 | 23 | struct GDAX { 24 | static let key = "" 25 | static let secret = "" 26 | static let passphrase = "" 27 | } 28 | 29 | struct Binance { 30 | static let key = "" 31 | static let secret = "" 32 | } 33 | 34 | struct Cryptopia { 35 | static let key = "" 36 | static let secret = "" 37 | } 38 | 39 | struct BitGrail { 40 | static let key = "" 41 | static let secret = "" 42 | } 43 | 44 | struct Bitfinex { 45 | static let key = "" 46 | static let secret = "" 47 | } 48 | 49 | struct Kraken { 50 | static let key = "" 51 | static let secret = "" 52 | } 53 | 54 | struct KuCoin { 55 | static let key = "" 56 | static let secret = "" 57 | } 58 | } 59 | 60 | -------------------------------------------------------------------------------- /UI/CryptEx/API/Services.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Services.swift 3 | // CryptEx 4 | // 5 | // Created by Sathyakumar Rajaraman on 3/17/18. 6 | // Copyright © 2018 Sathyakumar. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class Services { 12 | static let shared = Services() 13 | 14 | private init() { } 15 | 16 | lazy var coinMarketCap: CoinMarketCap.Service = { 17 | return CoinMarketCap.Service(key: nil, secret: nil, session: URLSession.shared, userPreference: .USD_BTC, currencyOverrides: nil) 18 | }() 19 | 20 | lazy var gemini: Gemini.Service = { 21 | return Gemini.Service(key: API.Gemini.key, secret: API.Gemini.secret, session: URLSession.shared, userPreference: .USD_BTC, currencyOverrides: nil) 22 | }() 23 | 24 | lazy var poloniex: Poloniex.Service = { 25 | return Poloniex.Service(key: API.Poloniex.key, secret: API.Poloniex.secret, session: URLSession.shared, userPreference: .USDT_BTC, currencyOverrides: nil) 26 | }() 27 | 28 | lazy var gdax: GDAX.Service = { 29 | let userPreference = UserPreference(fiat: .USD, crypto: .Bitcoin, ignoredFiats: [.EUR, .GBP]) 30 | return GDAX.Service(key: API.GDAX.key, secret: API.GDAX.secret, passphrase: API.GDAX.passphrase, session: URLSession.shared, userPreference: userPreference, currencyOverrides: nil) 31 | }() 32 | 33 | lazy var binance: Binance.Service = { 34 | return Binance.Service(key: API.Binance.key, secret: API.Binance.secret, session: URLSession.shared, userPreference: .USDT_BTC, currencyOverrides: ["BCC": Currency(name: "Bitcoin Cash", code: "BCC")]) 35 | }() 36 | 37 | lazy var cryptopia: Cryptopia.Service = { 38 | return Cryptopia.Service(key: API.Cryptopia.key, secret: API.Cryptopia.secret, session: URLSession.shared, userPreference: .USDT_BTC, currencyOverrides: nil) 39 | }() 40 | 41 | lazy var bitGrail: BitGrail.Service = { 42 | return BitGrail.Service(key: API.Binance.key, secret: API.Binance.secret, session: URLSession.shared, userPreference: .USD_BTC, currencyOverrides: nil) 43 | }() 44 | 45 | lazy var coinExchange: CoinExchange.Service = { 46 | let userPreference = UserPreference(fiat: .USDT, crypto: .Bitcoin, ignoredFiats: [.EUR]) 47 | return CoinExchange.Service(key: nil, secret: nil, session: URLSession.shared, userPreference: userPreference, currencyOverrides: nil) 48 | }() 49 | 50 | lazy var bitfinex: Bitfinex.Service = { 51 | return Bitfinex.Service(key: API.Bitfinex.key, secret: API.Bitfinex.secret, session: URLSession.shared, userPreference: .USD_BTC, currencyOverrides: nil) 52 | }() 53 | 54 | lazy var koinex: Koinex.Service = { 55 | let userPreference = UserPreference(fiat: .INR, crypto: .Bitcoin, ignoredFiats: []) 56 | return Koinex.Service(key: nil, secret: nil, session: URLSession.shared, userPreference: userPreference, currencyOverrides: nil) 57 | }() 58 | 59 | lazy var kraken: Kraken.Service = { 60 | return Kraken.Service(key: API.Kraken.key, secret: API.Kraken.secret, session: URLSession.shared, userPreference: .USD_BTC, currencyOverrides: nil) 61 | }() 62 | 63 | /* lazy var kuCoin: KuCoin.Service = { 64 | return KuCoin.Service(key: API.KuCoin.key, secret: API.KuCoin.secret, session: URLSession.shared, userPreference: .USDT_BTC, currencyOverrides: nil) 65 | }()*/ 66 | 67 | 68 | func balance() -> NSDecimalNumber { 69 | 70 | var totalBalance = NSDecimalNumber.zero 71 | 72 | totalBalance = totalBalance.adding(gemini.store.getTotalBalance()) 73 | totalBalance = totalBalance.adding(poloniex.store.getTotalBalance()) 74 | totalBalance = totalBalance.adding(gdax.store.getTotalBalance()) 75 | totalBalance = totalBalance.adding(binance.store.getTotalBalance()) 76 | totalBalance = totalBalance.adding(cryptopia.store.getTotalBalance()) 77 | if let btcPrice = gemini.store.tickersDictionary["BTCUSD"]?.price { 78 | let xrbValueInUSD = btcPrice.multiplying(by: bitGrail.store.getTotalBalance()) 79 | totalBalance = totalBalance.adding(xrbValueInUSD) 80 | } 81 | return totalBalance 82 | } 83 | 84 | func fetchAllBalances(completion: (() -> Void)?, failure: ((String?, String?) -> Void)?, captcha: ((String) -> Void)?) { 85 | coinMarketCap.getGlobal { (_) in 86 | completion?() 87 | } 88 | gemini.getBalances(completion: { _ in 89 | completion?() 90 | }) 91 | poloniex.getBalances(completion: { (_) in 92 | completion?() 93 | }) 94 | gdax.getBalances(completion: { (_) in 95 | completion?() 96 | }) 97 | binance.getBalances(completion: { (_) in 98 | completion?() 99 | }) 100 | cryptopia.getCurrencies { (_) in 101 | self.cryptopia.getTickers { (_) in 102 | self.cryptopia.getBalances(completion: { (_) in 103 | completion?() 104 | }) 105 | } 106 | } 107 | bitGrail.getBalances { (_) in 108 | completion?() 109 | } 110 | coinExchange.getCurrencyPairs { (_) in 111 | self.coinExchange.getTickers(completion: { (_) in 112 | completion?() 113 | }) 114 | } 115 | bitfinex.getBalances { (_) in 116 | completion?() 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /UI/CryptEx/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // CryptEx 4 | // 5 | // Created by Sathyakumar Rajaraman on 3/17/18. 6 | // Copyright © 2018 Sathyakumar. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import UserNotifications 11 | 12 | @UIApplicationMain 13 | class AppDelegate: UIResponder, UIApplicationDelegate { 14 | 15 | var window: UIWindow? 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 18 | // Override point for customization after application launch. 19 | 20 | application.setMinimumBackgroundFetchInterval(UIApplication.backgroundFetchIntervalMinimum) 21 | 22 | let center = UNUserNotificationCenter.current() 23 | center.requestAuthorization(options: [.sound, .sound, .badge]) { (granted, error) in 24 | if let error = error { 25 | print(error) 26 | } 27 | } 28 | return true 29 | } 30 | 31 | func applicationWillResignActive(_ application: UIApplication) { 32 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 33 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 34 | } 35 | 36 | func applicationDidEnterBackground(_ application: UIApplication) { 37 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 38 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 39 | } 40 | 41 | func applicationWillEnterForeground(_ application: UIApplication) { 42 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 43 | BackgroundService.shared.pause() 44 | } 45 | 46 | func applicationDidBecomeActive(_ application: UIApplication) { 47 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 48 | } 49 | 50 | func applicationWillTerminate(_ application: UIApplication) { 51 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 52 | } 53 | 54 | func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { 55 | BackgroundService.shared.resume(completionHandler: completionHandler) 56 | } 57 | } 58 | 59 | -------------------------------------------------------------------------------- /UI/CryptEx/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ios-marketing", 45 | "size" : "1024x1024", 46 | "scale" : "1x" 47 | } 48 | ], 49 | "info" : { 50 | "version" : 1, 51 | "author" : "xcode" 52 | } 53 | } -------------------------------------------------------------------------------- /UI/CryptEx/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /UI/CryptEx/Assets.xcassets/Pixel.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "Pixel.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /UI/CryptEx/Assets.xcassets/Pixel.imageset/Pixel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trsathya/Cryptex/541d9274b689384120bfcd2aa7469648f3f70e3b/UI/CryptEx/Assets.xcassets/Pixel.imageset/Pixel.png -------------------------------------------------------------------------------- /UI/CryptEx/Assets.xcassets/TransparentPixel.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "TransparentPixel.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /UI/CryptEx/Assets.xcassets/TransparentPixel.imageset/TransparentPixel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trsathya/Cryptex/541d9274b689384120bfcd2aa7469648f3f70e3b/UI/CryptEx/Assets.xcassets/TransparentPixel.imageset/TransparentPixel.png -------------------------------------------------------------------------------- /UI/CryptEx/BackgroundServices/BackgroundService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BackgroundService.swift 3 | // CryptEx 4 | // 5 | // Created by Sathyakumar Rajaraman on 3/17/18. 6 | // Copyright © 2018 Sathyakumar. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import UserNotifications 11 | 12 | class BackgroundService { 13 | static var shared = BackgroundService() 14 | 15 | private init() { } 16 | 17 | func resume(completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { 18 | let sharedServices = Services.shared 19 | sharedServices.coinMarketCap.getGlobal { (_) in 20 | if let string = sharedServices.coinMarketCap.store.globalMarketDataResponse.globalData?.description { 21 | LocalNotificationService.notify(identifier: "CoinMarketCap", title: "CoinMarketCap", message: string) 22 | } 23 | sharedServices.gemini.getBalances(completion: { _ in 24 | sharedServices.poloniex.getBalances(completion: { (_) in 25 | sharedServices.gdax.getBalances(completion: { (_) in 26 | sharedServices.binance.getBalances(completion: { (_) in 27 | sharedServices.cryptopia.getBalances(completion: { (_) in 28 | sharedServices.bitGrail.getBalances(completion: { (_) in 29 | sharedServices.bitfinex.getBalances { (_) in 30 | LocalNotificationService.notify(identifier: "Balance", title: nil, message: "Balance: \(NumberFormatter.usd.string(from: sharedServices.balance()) ?? "")") 31 | sharedServices.gemini.getTickers(completion: { (_) in 32 | let string = sharedServices.gemini.store.tickersDictionary.map({ (keyValue) -> String in 33 | return keyValue.key + " " + keyValue.value.price.stringValue 34 | }).joined(separator: "; ") 35 | LocalNotificationService.notify(identifier: "Gemini", title: nil, message: string) 36 | 37 | sharedServices.gdax.getTickers(completion: { (_) in 38 | let string = sharedServices.gdax.store.tickersResponse.map({ (keyValue) -> String in 39 | return keyValue.key + " " + keyValue.value.ticker.price.stringValue 40 | }).joined(separator: "; ") 41 | LocalNotificationService.notify(identifier: "GDAX", title: nil, message: string) 42 | completionHandler(.newData) 43 | }) 44 | }) 45 | } 46 | }) 47 | }) 48 | }) 49 | }) 50 | }) 51 | }) 52 | } 53 | } 54 | 55 | func pause() { 56 | 57 | } 58 | 59 | } 60 | 61 | class LocalNotificationService { 62 | static func notify(identifier: String, title: String?, message: String) { 63 | let content = UNMutableNotificationContent() 64 | if let title = title { 65 | content.title = NSString.localizedUserNotificationString(forKey: title, arguments: nil) 66 | } 67 | content.body = NSString.localizedUserNotificationString(forKey: message, arguments: nil) 68 | content.sound = UNNotificationSound.default 69 | // Deliver the notification in five seconds. 70 | let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false) 71 | let request = UNNotificationRequest(identifier: identifier, content: content, trigger: trigger) // Schedule the notification. 72 | let center = UNUserNotificationCenter.current() 73 | center.add(request) { (error : Error?) in 74 | if let _ = error { 75 | // Handle any errors 76 | } 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /UI/CryptEx/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /UI/CryptEx/CryptExUI-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Use this file to import your target's public headers that you would like to expose to Swift. 3 | // 4 | 5 | @import Cryptex; 6 | -------------------------------------------------------------------------------- /UI/CryptEx/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIBackgroundModes 24 | 25 | fetch 26 | 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIMainStoryboardFile 30 | Main 31 | UIRequiredDeviceCapabilities 32 | 33 | armv7 34 | 35 | UISupportedInterfaceOrientations 36 | 37 | UIInterfaceOrientationPortrait 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /UI/CryptEx/TickerCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TickerCell.swift 3 | // CryptExUI 4 | // 5 | // Created by Sathyakumar Rajaraman on 3/17/18. 6 | // Copyright © 2018 Sathyakumar. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | class TickerCell: UITableViewCell { 13 | @IBOutlet weak var nameLabel: UILabel! 14 | @IBOutlet weak var priceLabel: UILabel! 15 | @IBOutlet weak var USDPriceLabel: UILabel! 16 | } 17 | -------------------------------------------------------------------------------- /UI/CryptEx/TickerCell.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 27 | 28 | 29 | 30 | 36 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /UI/CryptEx/ViewController/All/AllBalancesVC.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AllBalancesVC.swift 3 | // CryptEx 4 | // 5 | // Created by Sathyakumar Rajaraman on 3/17/18. 6 | // Copyright © 2018 Sathyakumar. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class AllBalancesVC: RefreshableTableVC { 12 | 13 | var geminiService: Gemini.Service = Services.shared.gemini 14 | var poloniexService: Poloniex.Service = Services.shared.poloniex 15 | var gdaxService: GDAX.Service = Services.shared.gdax 16 | 17 | @IBOutlet private weak var totalLabel: UILabel! 18 | 19 | override func viewDidLoad() { 20 | super.viewDidLoad() 21 | tableView.register(UINib(nibName: "TickerCell", bundle: nil), forCellReuseIdentifier: "TickerCell") 22 | } 23 | 24 | override func loadData(forceFetch: Bool) { 25 | 26 | Services.shared.fetchAllBalances(completion: { 27 | DispatchQueue.main.async { 28 | self.tableView.reloadData() 29 | self.updateTotalBalance() 30 | } 31 | }, failure: { title, message in 32 | DispatchQueue.main.async { 33 | let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) 34 | alert.addAction(UIAlertAction(title: "Continue", style: .cancel, handler: nil)) 35 | self.present(alert, animated: true, completion: nil) 36 | } 37 | }, captcha: { captchaString in 38 | 39 | }) 40 | } 41 | 42 | func updateTotalBalance() { 43 | totalLabel.text = NumberFormatter.usd.string(from: Services.shared.balance()) 44 | } 45 | 46 | func exchangeNamesFor(indexPath: IndexPath) -> String { 47 | switch indexPath.row { 48 | case 0: 49 | return "Gemini" 50 | case 1: 51 | return "Poloniex" 52 | case 2: 53 | return "GDAX" 54 | default: 55 | return "" 56 | } 57 | } 58 | } 59 | 60 | extension AllBalancesVC: UITableViewDataSource { 61 | 62 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 63 | return 6 64 | } 65 | 66 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 67 | 68 | let cell = tableView.dequeueReusableCell(withIdentifier: "TickerCell", for: indexPath) as! TickerCell 69 | 70 | cell.nameLabel.text = exchangeNamesFor(indexPath: indexPath) 71 | cell.priceLabel.text = nil 72 | cell.accessoryType = .disclosureIndicator 73 | 74 | switch indexPath.row { 75 | case 0: 76 | cell.USDPriceLabel.text = NumberFormatter.usd.string(from: Services.shared.gemini.store.getTotalBalance()) 77 | case 1: 78 | cell.USDPriceLabel.text = NumberFormatter.usd.string(from: Services.shared.poloniex.store.getTotalBalance()) 79 | case 2: 80 | cell.USDPriceLabel.text = NumberFormatter.usd.string(from: Services.shared.gdax.store.getTotalBalance()) 81 | case 3: 82 | cell.USDPriceLabel.text = NumberFormatter.usd.string(from: Services.shared.binance.store.getTotalBalance()) 83 | case 4: 84 | cell.USDPriceLabel.text = NumberFormatter.usd.string(from: Services.shared.cryptopia.store.getTotalBalance()) 85 | case 5: 86 | if let btcusdPrice = Services.shared.gemini.store.tickersDictionary["BTCUSD"]?.price { 87 | let xrbusdPrice = btcusdPrice.multiplying(by: Services.shared.bitGrail.store.getTotalBalance()) 88 | cell.USDPriceLabel.text = NumberFormatter.usd.string(from: xrbusdPrice) 89 | } else { 90 | cell.USDPriceLabel.text = nil 91 | } 92 | default: 93 | break 94 | } 95 | return cell 96 | } 97 | } 98 | 99 | extension AllBalancesVC: UITableViewDelegate { 100 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 101 | tableView.deselectRow(at: indexPath, animated: true) 102 | performSegue(withIdentifier: "expand" + exchangeNamesFor(indexPath: indexPath) + "Balances", sender: self) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /UI/CryptEx/ViewController/BalancesVC.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BalancesVC.swift 3 | // CryptExUI 4 | // 5 | // Created by Sathyakumar Rajaraman on 3/17/18. 6 | // Copyright © 2018 Sathyakumar. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | class BalancesVC: RefreshableTableVC { 13 | var service: BalanceServiceType! 14 | var dataStore: BalanceTableViewDataSource! 15 | 16 | @IBOutlet private weak var totalLabel: UILabel! 17 | 18 | override func loadData(forceFetch: Bool) { 19 | tableView.register(UINib(nibName: "TickerCell", bundle: nil), forCellReuseIdentifier: "TickerCell") 20 | updateTotalBalance() 21 | 22 | service.getBalances(completion: { (_) in 23 | DispatchQueue.main.async { 24 | self.updateTotalBalance() 25 | self.tableView.reloadData() 26 | } 27 | }) 28 | } 29 | 30 | func updateTotalBalance() { 31 | self.totalLabel.text = NumberFormatter.usd.string(from: dataStore.getTotalBalance()) 32 | } 33 | } 34 | 35 | extension BalancesVC: UITableViewDataSource { 36 | 37 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 38 | return dataStore.balanceCount() 39 | } 40 | 41 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 42 | let cell = tableView.dequeueReusableCell(withIdentifier: "TickerCell", for: indexPath) as! TickerCell 43 | let balance = dataStore.displayableBalance(row: indexPath.row) 44 | cell.nameLabel.text = balance.name 45 | cell.priceLabel.text = balance.balanceQuantity 46 | cell.USDPriceLabel.text = balance.priceInUSD 47 | return cell 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /UI/CryptEx/ViewController/ExchangeVC.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // GeminiApp 4 | // 5 | // Created by Sathyakumar Rajaraman on 3/17/18. 6 | // Copyright © 2018 Sathyakumar. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ExchangeVC: RefreshableTableVC { 12 | 13 | let exchangeNames: [String] = ["All", "Gemini", "Poloniex", "GDAX", "Binance", "Cryptopia", "BitGrail", "CoinExchange", "Bitfinex", "Koinex", "Kraken"] 14 | let exchangeOptions: [[String]] = [ 15 | ["Balances"], 16 | ["Tickers", "Balances" 17 | ,"PastTrades" 18 | ], 19 | ["Tickers", "Balances" 20 | ,"TradeHistory" 21 | ,"DepositsWithdrawals" 22 | ], 23 | ["Tickers", "Balances"], 24 | ["Tickers", "Balances"], 25 | ["Tickers", "Balances"], 26 | ["Tickers", "Balances"], 27 | ["Tickers"], 28 | ["Tickers"], 29 | ["Tickers"], 30 | ["Tickers"] 31 | ] 32 | 33 | override func viewDidLoad() { 34 | super.viewDidLoad() 35 | tableView.register(UINib(nibName: "TickerCell", bundle: nil), forCellReuseIdentifier: "TickerCell") 36 | } 37 | 38 | override func loadData(forceFetch: Bool) { 39 | Services.shared.fetchAllBalances(completion: { 40 | DispatchQueue.main.async { 41 | self.tableView.reloadData() 42 | } 43 | }, failure: nil, captcha: { captchaString in 44 | print("Error while loading data") 45 | }) 46 | } 47 | } 48 | 49 | extension ExchangeVC: UITableViewDataSource { 50 | 51 | func numberOfSections(in tableView: UITableView) -> Int { 52 | return exchangeNames.count 53 | } 54 | 55 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 56 | return exchangeOptions[section].count 57 | } 58 | 59 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 60 | let cell = tableView.dequeueReusableCell(withIdentifier: "TickerCell", for: indexPath) as! TickerCell 61 | cell.nameLabel.text = exchangeNames[indexPath.section] + " " + exchangeOptions[indexPath.section][indexPath.row] 62 | switch (indexPath.section, indexPath.row) { 63 | case (0, 0): 64 | cell.USDPriceLabel.text = NumberFormatter.usd.string(from: Services.shared.balance()) 65 | case (1, 1): 66 | cell.USDPriceLabel.text = NumberFormatter.usd.string(from: Services.shared.gemini.store.getTotalBalance()) 67 | case (2, 1): 68 | cell.USDPriceLabel.text = NumberFormatter.usd.string(from: Services.shared.poloniex.store.getTotalBalance()) 69 | case (3, 1): 70 | cell.USDPriceLabel.text = NumberFormatter.usd.string(from: Services.shared.gdax.store.getTotalBalance()) 71 | case (4, 1): 72 | cell.USDPriceLabel.text = NumberFormatter.usd.string(from: Services.shared.binance.store.getTotalBalance()) 73 | case (5, 1): 74 | cell.USDPriceLabel.text = NumberFormatter.usd.string(from: Services.shared.cryptopia.store.getTotalBalance()) 75 | case (6, 1): 76 | if let btcusdPrice = Services.shared.gemini.store.tickersDictionary["BTCUSD"]?.price { 77 | let xrbusdPrice = btcusdPrice.multiplying(by: Services.shared.bitGrail.store.getTotalBalance()) 78 | cell.USDPriceLabel.text = NumberFormatter.usd.string(from: xrbusdPrice) 79 | } else { 80 | cell.USDPriceLabel.text = nil 81 | } 82 | default: 83 | cell.USDPriceLabel.text = nil 84 | } 85 | cell.priceLabel.text = nil 86 | return cell 87 | } 88 | } 89 | 90 | extension ExchangeVC: UITableViewDelegate { 91 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 92 | tableView.deselectRow(at: indexPath, animated: true) 93 | if indexPath.section == 0 { 94 | performSegue(withIdentifier: "show" + exchangeNames[indexPath.section] + exchangeOptions[indexPath.section][indexPath.row], sender: self) 95 | } else { 96 | if indexPath.row == 0 { 97 | if let vc = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "TickersVC") as? TickersVC { 98 | switch (indexPath.section) { 99 | case 1: 100 | vc.dataStore = Services.shared.gemini.store 101 | vc.service = Services.shared.gemini 102 | case 2: 103 | vc.dataStore = Services.shared.poloniex.store 104 | vc.service = Services.shared.poloniex 105 | case 3: 106 | vc.dataStore = Services.shared.gdax.store 107 | vc.service = Services.shared.gdax 108 | case 4: 109 | vc.dataStore = Services.shared.binance.store 110 | vc.service = Services.shared.binance 111 | case 5: 112 | vc.dataStore = Services.shared.cryptopia.store 113 | vc.service = Services.shared.cryptopia 114 | case 6: 115 | vc.dataStore = Services.shared.bitGrail.store 116 | vc.service = Services.shared.bitGrail 117 | case 7: 118 | vc.dataStore = Services.shared.coinExchange.store 119 | vc.service = Services.shared.coinExchange 120 | case 8: 121 | vc.dataStore = Services.shared.bitfinex.store 122 | vc.service = Services.shared.bitfinex 123 | case 9: 124 | vc.dataStore = Services.shared.koinex.store 125 | vc.service = Services.shared.koinex 126 | case 10: 127 | vc.dataStore = Services.shared.kraken.store 128 | vc.service = Services.shared.kraken 129 | default: break 130 | } 131 | vc.title = exchangeNames[indexPath.section] 132 | navigationController?.pushViewController(vc, animated: true) 133 | } 134 | } else if indexPath.row == 1 { 135 | if let vc = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "BalancesVC") as? BalancesVC { 136 | switch (indexPath.section) { 137 | case 1: 138 | vc.dataStore = Services.shared.gemini.store 139 | vc.service = Services.shared.gemini 140 | case 2: 141 | vc.dataStore = Services.shared.poloniex.store 142 | vc.service = Services.shared.poloniex 143 | case 3: 144 | vc.dataStore = Services.shared.gdax.store 145 | vc.service = Services.shared.gdax 146 | case 4: 147 | vc.dataStore = Services.shared.binance.store 148 | vc.service = Services.shared.binance 149 | case 5: 150 | vc.dataStore = Services.shared.cryptopia.store 151 | vc.service = Services.shared.cryptopia 152 | case 6: 153 | vc.dataStore = Services.shared.bitGrail.store 154 | vc.service = Services.shared.bitGrail 155 | case 7: 156 | vc.dataStore = Services.shared.coinExchange.store 157 | vc.service = Services.shared.coinExchange 158 | case 8: 159 | vc.dataStore = Services.shared.bitfinex.store 160 | vc.service = Services.shared.bitfinex 161 | default: break 162 | } 163 | navigationController?.pushViewController(vc, animated: true) 164 | } 165 | } 166 | } 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /UI/CryptEx/ViewController/Gemini/GeminiPastTradesVC.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GeminiPastTradesVC.swift 3 | // CryptEx 4 | // 5 | // Created by Sathyakumar Rajaraman on 3/17/18. 6 | // Copyright © 2018 Sathyakumar. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class GeminiPastTradesVC: RefreshableTableVC { 12 | let service = Services.shared.gemini 13 | 14 | override func loadData(forceFetch: Bool) { 15 | service.getPastTrades(completion: { (_, _) in 16 | DispatchQueue.main.async { 17 | self.tableView.reloadData() 18 | } 19 | }, failure: { (title, message) in 20 | DispatchQueue.main.async { 21 | let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) 22 | alert.addAction(UIAlertAction(title: "Continue", style: .cancel, handler: nil)) 23 | self.present(alert, animated: true, completion: nil) 24 | } 25 | }) 26 | } 27 | } 28 | 29 | extension GeminiPastTradesVC: UITableViewDataSource { 30 | 31 | func currencyPairFor(section: Int) -> String { 32 | return service.store.pastTradesResponse.keys.sorted()[section] 33 | } 34 | 35 | func pastTradesAt(section: Int) -> [Gemini.PastTrade]? { 36 | let currencyPair = currencyPairFor(section: section) 37 | return service.store.pastTradesResponse[currencyPair]?.pastTrades 38 | } 39 | 40 | func pastTradeAt(indexPath: IndexPath) -> Gemini.PastTrade? { 41 | return pastTradesAt(section: indexPath.section)?[indexPath.row] 42 | } 43 | 44 | func numberOfSections(in tableView: UITableView) -> Int { 45 | return service.store.pastTradesResponse.count 46 | } 47 | 48 | func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { 49 | guard let count = pastTradesAt(section: section)?.count, count > 0 else { return nil } 50 | return currencyPairFor(section: section) 51 | } 52 | 53 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 54 | return pastTradesAt(section: section)?.count ?? 0 55 | } 56 | 57 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 58 | let cell = tableView.dequeueReusableCell(withIdentifier: "PastTradesCell", for: indexPath) as! PastTradesCell 59 | 60 | if let trade = pastTradeAt(indexPath: indexPath) { 61 | let df = DateFormatter() 62 | df.dateStyle = DateFormatter.Style.short 63 | df.timeStyle = DateFormatter.Style.none 64 | var date = df.string(from: trade.timestamp) 65 | df.dateStyle = DateFormatter.Style.none 66 | df.timeStyle = DateFormatter.Style.short 67 | date = date + "\n" + df.string(from: trade.timestamp) 68 | cell.dateLabel.text = date 69 | cell.quantityLabel.text = trade.amount.stringValue 70 | cell.rateLabel.text = "@ " + trade.price.stringValue 71 | let multiplier: NSDecimalNumber = trade.type == .buy ? .one : NSDecimalNumber(value: -1) 72 | cell.priceInUSDLabel.text = NumberFormatter.usd.string(from: trade.amount.multiplying(by: trade.price).multiplying(by: multiplier)) 73 | } 74 | return cell 75 | } 76 | } 77 | 78 | class PastTradesCell: UITableViewCell { 79 | @IBOutlet weak var dateLabel: UILabel! 80 | @IBOutlet weak var quantityLabel: UILabel! 81 | @IBOutlet weak var rateLabel: UILabel! 82 | @IBOutlet weak var priceInUSDLabel: UILabel! 83 | } 84 | -------------------------------------------------------------------------------- /UI/CryptEx/ViewController/Poloniex/PoloniexDepositsWithdrawalsVC.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PoloniexDepositsWithdrawalsVC.swift 3 | // CryptEx 4 | // 5 | // Created by Sathyakumar Rajaraman on 3/17/18. 6 | // Copyright © 2018 Sathyakumar. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class PoloniexDepositsWithdrawalsVC: RefreshableTableVC { 12 | let service = Services.shared.poloniex 13 | 14 | override func loadData(forceFetch: Bool) { 15 | 16 | let now = Date() 17 | let start = Date(timeInterval: .aMonthAgo, since: now) 18 | 19 | service.returnDepositsWithdrawals(start: start, end: now, completion: { _ in 20 | DispatchQueue.main.async { 21 | self.tableView.reloadData() 22 | } 23 | }) 24 | } 25 | } 26 | 27 | extension PoloniexDepositsWithdrawalsVC: UITableViewDataSource { 28 | 29 | var deposits: [Poloniex.Deposit] { 30 | return service.store.depositsWithdrawalsResponse.deposits 31 | } 32 | 33 | var withdrawals: [Poloniex.Withdrawal] { 34 | return service.store.depositsWithdrawalsResponse.withdrawals 35 | } 36 | 37 | func numberOfSections(in tableView: UITableView) -> Int { 38 | return 2 39 | } 40 | 41 | func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { 42 | switch section { 43 | case 0: 44 | return deposits.count > 0 ? "Deposits" : nil 45 | case 1: 46 | return withdrawals.count > 0 ? "Withdrawals" : nil 47 | default: 48 | return nil 49 | } 50 | } 51 | 52 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 53 | switch section { 54 | case 0: 55 | return deposits.count 56 | case 1: 57 | return withdrawals.count 58 | default: 59 | return 0 60 | } 61 | } 62 | 63 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 64 | let cell = tableView.dequeueReusableCell(withIdentifier: "PastTradesCell", for: indexPath) as! PastTradesCell 65 | cell.quantityLabel.text = nil 66 | cell.rateLabel.text = nil 67 | switch indexPath.section { 68 | case 0: 69 | let deposit = deposits[indexPath.row] 70 | cell.dateLabel.text = DateFormatter.doubleLineDateTime(date: deposit.timestamp) 71 | cell.priceInUSDLabel.text = deposit.amount.stringValue + " " + deposit.currency.code 72 | case 1: 73 | let withdrawal = withdrawals[indexPath.row] 74 | cell.dateLabel.text = DateFormatter.doubleLineDateTime(date: withdrawal.timestamp) 75 | cell.priceInUSDLabel.text = withdrawal.amount.stringValue + " " + withdrawal.currency.code 76 | default: 77 | break 78 | } 79 | return cell 80 | } 81 | 82 | 83 | } 84 | -------------------------------------------------------------------------------- /UI/CryptEx/ViewController/Poloniex/PoloniexTradeHistoryVC.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PoloniexTradeHistoryVC.swift 3 | // CryptEx 4 | // 5 | // Created by Sathyakumar Rajaraman on 3/17/18. 6 | // Copyright © 2018 Sathyakumar. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class PoloniexTradeHistoryVC: RefreshableTableVC { 12 | let service = Services.shared.poloniex 13 | override func loadData(forceFetch: Bool) { 14 | 15 | let now = Date() 16 | let start = Date(timeInterval: .aMonthAgo, since: now) // expose a date picker for start date 17 | 18 | service.returnTradeHistory(start: start, end: now, completion: { (_) in 19 | DispatchQueue.main.async { 20 | self.tableView.reloadData() 21 | } 22 | }, captcha: { (captchaString) in 23 | 24 | }) 25 | } 26 | } 27 | 28 | extension PoloniexTradeHistoryVC: UITableViewDataSource { 29 | func currencyPairFor(section: Int) -> String { 30 | return service.store.pastTradesResponse.pastTrades.keys.sorted()[section] 31 | } 32 | 33 | func pastTradesAt(section: Int) -> [Poloniex.PastTrade]? { 34 | return service.store.pastTradesResponse.pastTrades[currencyPairFor(section: section)] 35 | } 36 | 37 | func pastTradeAt(indexPath: IndexPath) -> Poloniex.PastTrade? { 38 | return pastTradesAt(section: indexPath.section)?[indexPath.row] 39 | } 40 | 41 | func numberOfSections(in tableView: UITableView) -> Int { 42 | return service.store.pastTradesResponse.pastTrades.count 43 | } 44 | 45 | func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { 46 | guard let count = pastTradesAt(section: section)?.count, count > 0 else { return nil } 47 | return currencyPairFor(section: section) 48 | } 49 | 50 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 51 | return pastTradesAt(section: section)?.count ?? 0 52 | } 53 | 54 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 55 | let cell = tableView.dequeueReusableCell(withIdentifier: "PastTradesCell", for: indexPath) as! PastTradesCell 56 | 57 | if let trade = pastTradeAt(indexPath: indexPath) { 58 | let df = DateFormatter() 59 | df.dateStyle = DateFormatter.Style.short 60 | df.timeStyle = DateFormatter.Style.none 61 | var date = df.string(from: trade.date) 62 | df.dateStyle = DateFormatter.Style.none 63 | df.timeStyle = DateFormatter.Style.short 64 | date = date + "\n" + df.string(from: trade.date) 65 | cell.dateLabel.text = date 66 | cell.quantityLabel.text = trade.amount.stringValue 67 | cell.rateLabel.text = "@ " + trade.rate.stringValue 68 | let multiplier: NSDecimalNumber = trade.type == .buy ? .one : NSDecimalNumber(value: -1) 69 | cell.priceInUSDLabel.text = trade.amount.multiplying(by: trade.rate).multiplying(by: multiplier).rounding(accordingToBehavior: NSDecimalNumberHandler.zeroDotEight).stringValue 70 | } 71 | return cell 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /UI/CryptEx/ViewController/RefreshableTableVC.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RefreshableTableVC.swift 3 | // CryptEx 4 | // 5 | // Created by Sathyakumar Rajaraman on 3/17/18. 6 | // Copyright © 2018 Sathyakumar. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class RefreshableTableVC: UIViewController { 12 | @IBOutlet weak var tableView: UITableView! 13 | 14 | override func viewDidLoad() { 15 | super.viewDidLoad() 16 | let rc = UIRefreshControl() 17 | rc.addTarget(self, action: #selector(fetchData(refreshControl:)), for: .valueChanged) 18 | tableView.refreshControl = rc 19 | loadData(forceFetch: false) 20 | } 21 | 22 | @objc func fetchData(refreshControl: UIRefreshControl?) { 23 | refreshControl?.endRefreshing() 24 | loadData(forceFetch: true) 25 | } 26 | 27 | func loadData(forceFetch: Bool) { 28 | fatalError() 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /UI/CryptEx/ViewController/Settings/NotificationSettingsVC.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NotificationSettingsVC.swift 3 | // CryptEx 4 | // 5 | // Created by Sathyakumar Rajaraman on 3/17/18. 6 | // Copyright © 2018 Sathyakumar. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class NotificationSettingsVC: UITableViewController { 12 | 13 | @IBAction func dismiss() { 14 | dismiss(animated: true, completion: nil) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /UI/CryptEx/ViewController/TickersVC.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TickersVC.swift 3 | // CryptExUI 4 | // 5 | // Created by Sathyakumar Rajaraman on 3/17/18. 6 | // Copyright © 2018 Sathyakumar. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | 12 | class TickersVC: RefreshableTableVC, UITableViewDataSource { 13 | @IBOutlet weak var toolbar: UIToolbar! 14 | @IBOutlet weak var segmentedControl: UISegmentedControl! 15 | var navigationBarHeight: CGFloat = 0.0 16 | 17 | var dataStore: TickerTableViewDataSource! 18 | var service: TickerServiceType! 19 | 20 | override func viewDidLoad() { 21 | super.viewDidLoad() 22 | tableView.register(UINib(nibName: "TickerCell", bundle: nil), forCellReuseIdentifier: "TickerCell") 23 | let navigationBar = self.navigationController?.navigationBar 24 | navigationBar?.shadowImage = UIImage() 25 | } 26 | 27 | override func loadData(forceFetch: Bool) { 28 | service.getTickers(completion: { _ in 29 | DispatchQueue.main.async { 30 | self.tableView.reloadData() 31 | } 32 | }) 33 | } 34 | 35 | @IBAction func segmentedControlValueChanged(sender: UISegmentedControl) { 36 | tableView.reloadData() 37 | } 38 | 39 | func numberOfSections(in tableView: UITableView) -> Int { 40 | guard let type = TickerViewType(rawValue: segmentedControl.selectedSegmentIndex) else { return 0 } 41 | return dataStore.sectionCount(viewType: type) 42 | } 43 | 44 | func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { 45 | guard let type = TickerViewType(rawValue: segmentedControl.selectedSegmentIndex) else { return "" } 46 | return dataStore.sectionHeaderTitle(section: section, viewType: type) 47 | } 48 | 49 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 50 | guard let type = TickerViewType(rawValue: segmentedControl.selectedSegmentIndex) else { return 0 } 51 | return dataStore.tickerCount(section: section, viewType: type) 52 | } 53 | 54 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 55 | let cell = tableView.dequeueReusableCell(withIdentifier: "TickerCell", for: indexPath) as! TickerCell 56 | guard let type = TickerViewType(rawValue: segmentedControl.selectedSegmentIndex), let displayableTicker = dataStore.displayableTicker(section: indexPath.section, row: indexPath.row, viewType: type) else { return cell } 57 | cell.nameLabel.text = displayableTicker.name 58 | cell.priceLabel.text = displayableTicker.price 59 | cell.USDPriceLabel.text = displayableTicker.formattedPriceInAccountingCurrency 60 | return cell 61 | } 62 | } 63 | 64 | -------------------------------------------------------------------------------- /UI/CryptExUI.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /UI/CryptExUI.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /UI/CryptExUI.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /UI/Podfile: -------------------------------------------------------------------------------- 1 | platform :ios, '9.0' 2 | 3 | target 'CryptExUI' do 4 | use_frameworks! 5 | 6 | pod 'Cryptex', :path => '../' 7 | 8 | # To install all exchanges 9 | #pod 'Cryptex', '~> 0.0.4' 10 | 11 | # To install only one exchange 12 | #pod 'Cryptex/Gemini', '~> 0.0.4' 13 | 14 | # To install two or more exchanges 15 | #pod 'Cryptex', '~> 0.0.4', :subspecs => ['Gemini', 'GDAX', "Poloniex"] 16 | end 17 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Cryptex Docs 5 | 6 | 7 |

Cryptex Docs

8 | 9 | --------------------------------------------------------------------------------