├── .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 |   [](https://github.com/trsathya/Cryptex/releases)  ![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 | 
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 |
--------------------------------------------------------------------------------