cointools 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 0e581465b03a85e017f097f09f991294a9d87676
4
- data.tar.gz: f9e6d0cfeab51850d8d858a50990838305b5a13c
3
+ metadata.gz: 652a3663b1f2a32c461ddf19bb73f95cdadf155c
4
+ data.tar.gz: 9cd0d5c8891c59fcfebd24dfd038da884a8495f4
5
5
  SHA512:
6
- metadata.gz: 0f3237529fa8ad91074c3564ca1268bdbf52d2904e016e2744b4d714f829d3e37feb48614eb5fad7aa2833727f9168c5aa1f731ede1a71330b810f336215612d
7
- data.tar.gz: 81833105049ee6fcc85a3ecd323fce79087fd75d25300486922e60031e102f997df4153db3696bbdbdbbe5fb9c64c8d23ac17a5efb0a3b613376a5116e13f5fa
6
+ metadata.gz: 36720bdfcb1f20d8727684fadd4f61cb28e5cb574eb59cb60db1f2ac8d9017f0b3010db97f3e601d4fd027de0a512f4cca4819125b1dfa3f2066ad717f35c56f
7
+ data.tar.gz: af00dbb857cbc28ff07bfc84895e044d0f2cfcebef8b2c9e083ef1d58014567a1c47aa29eb7eef70c2c5d7b1f7f4e5ce5c252b4e4381eab9c4794d29b0d0f420
data/CHANGELOG.md CHANGED
@@ -1,3 +1,13 @@
1
+ #### Version 0.5.0 (12.07.2018)
2
+
3
+ * switched to CoinMarketCap API v2, CMC's `get_price` and `get_price_by_symbol` calls now return data like coin name/symbol, rank and market cap
4
+ * new `get_all_prices` method in CMC that returns all 1600+ coins sorted by rank
5
+ * renamed `--list-exchanges` to `--exchanges`, `--list-markets` to `--markets`, `--list-fiat-currencies` to `--fiat-currencies`, `--fiat-currency` to `--convert-to`
6
+ * all commands print verbose output by default, use `--quiet/-q` to print only the price
7
+ * time can be passed as a string to `get_price` methods
8
+ * improved error handling
9
+ * added a huge amount of tests
10
+
1
11
  #### Version 0.4.0 (23.05.2018)
2
12
 
3
13
  * added CoinCap.io class and `coincap` command
data/README.md CHANGED
@@ -37,11 +37,13 @@ To check the current price, skip the timestamp:
37
37
  cryptowatch bitfinex btcusd
38
38
  ```
39
39
 
40
+ By default the price is printed in a verbose format that includes the requested market name and the time of the returned price. Add a `-q/--quiet` option to only print the price as a single number (e.g. to pass it further to another script).
41
+
40
42
  You can fetch a list of available exchanges and markets using these commands:
41
43
 
42
44
  ```
43
- cryptowatch --list-exchanges
44
- cryptowatch --list-markets bithumb
45
+ cryptowatch --exchanges
46
+ cryptowatch --markets bithumb
45
47
  ```
46
48
 
47
49
  In code:
@@ -85,7 +87,9 @@ Alternatively, you can pass the cryptocurrency's symbol using the `-s` or `--sym
85
87
  cmcap -s powr
86
88
  ```
87
89
 
88
- However, this operation needs to download a complete ticker for all currencies and scan through the list, so it's recommended to use the name as in the example above.
90
+ However, be aware that coin symbols are not unique on CoinMarketCap - there are at least a few examples of duplicates. In such case, the first coin that matches (probably the older one) will be returned.
91
+
92
+ By default the price is printed in a verbose format that includes the requested coin symbol and the time of the returned price. Add a `-q/--quiet` option to only print the price as a single number (e.g. to pass it further to another script).
89
93
 
90
94
  You can also use the `-b` or `--btc-price` flag to request a price in BTC instead of USD:
91
95
 
@@ -93,13 +97,13 @@ You can also use the `-b` or `--btc-price` flag to request a price in BTC instea
93
97
  cmcap power-ledger -b
94
98
  ```
95
99
 
96
- Or you can request the price in one of the ~30 other supported fiat currencies with `-f` or `--fiat-currency`:
100
+ Or you can request the price in one of the ~30 other supported fiat currencies with `-c` or `--convert-to`:
97
101
 
98
102
  ```
99
- cmcap request-network -fEUR
103
+ cmcap request-network -cEUR
100
104
  ```
101
105
 
102
- You can print a list of supported fiat currencies with `cmcap --list-fiat-currencies`.
106
+ You can print a list of supported fiat currencies with `cmcap --fiat-currencies`.
103
107
 
104
108
  Same things in code:
105
109
 
@@ -109,17 +113,40 @@ cmc = CoinTools::CoinMarketCap.new
109
113
 
110
114
  p CoinTools::CoinMarketCap::FIAT_CURRENCIES
111
115
 
112
- ltc = cryptowatch.get_price('litecoin')
116
+ ltc = cmc.get_price('litecoin')
113
117
  puts "LTC: #{ltc.usd_price} USD / #{ltc.btc_price} BTC"
114
118
 
115
- xmr = cryptowatch.get_price_by_symbol('xmr')
119
+ xmr = cmc.get_price_by_symbol('xmr')
116
120
  puts "XMR: #{xmr.usd_price} USD / #{xmr.btc_price} BTC"
117
121
 
118
- eth = cryptowatch.get_price('ethereum', convert_to: 'EUR')
122
+ eth = cmc.get_price('ethereum', convert_to: 'EUR')
119
123
  puts "ETH: #{eth.converted_price} EUR"
120
124
  ```
121
125
 
122
- The soft rate limit for the API is 10 requests per minute (for the currently used v1 API).
126
+ The soft rate limit for the API is 30 requests per minute (for API v2).
127
+
128
+ Note: since in the v2 API specific coin tickers can only be looked up using CoinMarketCap's internal numeric ids (e.g. Ethereum = 1027), both lookup methods available here - by the coin name ("slug") and symbol - have to first download a `/listings` JSON with a mapping of all coins on the site. The result of that call is cached in a `CoinTools::CoinMarketCap` object, so if you do many lookups in code, in one go or over some period of time, it's recommended to reuse the object instead of recreating it each time.
129
+
130
+ The API only allows one additional currency apart from USD, so if you pass a `convert_to` parameter, `btc_price` will not be returned, only `converted_price`.
131
+
132
+
133
+ ### Getting all coin prices
134
+
135
+ You can also download the whole table of 1600+ coins listed on CoinMarketCap using the `get_all_prices` method. The method also takes a `convert_to` parameter, which works as described above. This method has to use paging to download data in batches and currently makes around 16 calls (which takes about half a minute), so do not call it repeatedly or you might go over the rate limit.
136
+
137
+ You can pass a block to this method to receive each batch of coins as they arrive. Take into account however that coins are not sorted by rank, but by numeric id (or roughly by creation date), so if you want e.g. top 100 coins, you still need to download the whole list. This is mostly intended to let you track progress in some way, e.g.:
138
+
139
+ ```ruby
140
+ require 'cointools/coinmarketcap'
141
+ cmc = CoinTools::CoinMarketCap.new
142
+
143
+ coins = cmc.get_all_prices { |l| print '.' }
144
+ puts
145
+
146
+ coins.first(50).each do |c|
147
+ puts c.symbol.ljust(5) + c.market_cap.to_i.to_s.rjust(15)
148
+ end
149
+ ```
123
150
 
124
151
 
125
152
  ## CoinCap
@@ -138,6 +165,8 @@ To check the current price, skip the timestamp:
138
165
  coincap xmr
139
166
  ```
140
167
 
168
+ By default the price is printed in a verbose format that includes the requested coin symbol and the time of the returned price. Add a `-q/--quiet` option to only print the price as a single number (e.g. to pass it further to another script).
169
+
141
170
  You can also use the `-b` or `--btc-price` flag to request a price in BTC instead of USD, or `-e` or `--eur-price` for EUR:
142
171
 
143
172
  ```
data/bin/cmcap CHANGED
@@ -1,37 +1,37 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  require 'bundler/setup'
4
- require 'cointools'
4
+ require 'cointools/coinmarketcap'
5
5
 
6
6
  require 'optparse'
7
7
  require 'time'
8
8
 
9
9
  def print_help
10
- puts "Usage: #{$PROGRAM_NAME} <coin_name> [-b/--btc-price] [-fXXX/--fiat-currency XXX] [-v/--verbose]"
10
+ puts "Usage: #{$PROGRAM_NAME} <coin_name> [-b/--btc-price] [-cXXX/--convert-to XXX] [-q/--quiet]"
11
11
  puts " e.g.: #{$PROGRAM_NAME} litecoin"
12
12
  puts
13
13
  puts "* To look up the currency by symbol instead of name (slower, downloads more data):"
14
- puts " #{$PROGRAM_NAME} -s/--symbol <symbol> [-b/--btc-price] [-fXXX/--fiat-currency XXX] [-v/--verbose]"
14
+ puts " #{$PROGRAM_NAME} -s/--symbol <symbol> [-b/--btc-price] [-cXXX/--convert-to XXX] [-q/--quiet]"
15
15
  puts " e.g.: #{$PROGRAM_NAME} -s LTC"
16
16
  puts
17
17
  puts "* To list supported fiat currencies:"
18
- puts " #{$PROGRAM_NAME} --list-fiat-currences"
18
+ puts " #{$PROGRAM_NAME} --fiat-currences"
19
19
  puts
20
20
  puts "* -b / --btc-price: returns the coin's price in BTC instead of USD"
21
- puts "* -fEUR / --fiat-currency EUR: returns the price in a given fiat currency instead of USD"
21
+ puts "* -cEUR / --convert-to EUR: returns the price in a given fiat currency instead of USD"
22
22
  end
23
23
 
24
- verbose = false
24
+ quiet = false
25
25
  btc_price = false
26
26
  fiat_currency = nil
27
27
  symbol = nil
28
28
 
29
29
  OptionParser.new do |opts|
30
- opts.on('-v', '--verbose') { verbose = true }
30
+ opts.on('-q', '--quiet') { quiet = true }
31
31
  opts.on('-b', '--btc-price') { btc_price = true }
32
- opts.on('-fXXX', '--fiat-currency XXX') { |f| fiat_currency = f }
32
+ opts.on('-cXXX', '--convert-to XXX') { |f| fiat_currency = f }
33
33
 
34
- opts.on('--list-fiat-currencies') do
34
+ opts.on('--fiat-currencies') do
35
35
  puts CoinTools::CoinMarketCap::FIAT_CURRENCIES
36
36
  exit 0
37
37
  end
@@ -56,7 +56,7 @@ unless symbol
56
56
  end
57
57
 
58
58
  if fiat_currency && btc_price
59
- puts "#{$PROGRAM_NAME}: --btc-price and --fiat-currency options cannot be used together"
59
+ puts "#{$PROGRAM_NAME}: --btc-price and --convert-to options cannot be used together"
60
60
  exit 1
61
61
  end
62
62
 
@@ -78,22 +78,26 @@ begin
78
78
  unit = 'USD'
79
79
  end
80
80
 
81
- if verbose
82
- puts "#{symbol&.upcase || coin_name} @ #{result.time || Time.now} ==> #{price} #{unit}"
83
- puts
84
- else
81
+ if quiet
85
82
  puts price
83
+ else
84
+ puts "#{symbol&.upcase || coin_name} @ #{result.last_updated || Time.now} ==> #{price} #{unit}"
85
+ puts
86
86
  end
87
- rescue CoinTools::CoinMarketCap::BadRequestException => e
88
- $stderr.puts "Error: Incorrect coin name: #{coin_name} (#{e})"
87
+ rescue CoinTools::InvalidSymbolError => e
88
+ $stderr.puts "Error: Invalid coin name: '#{symbol}'"
89
89
  exit 1
90
- rescue CoinTools::CoinMarketCap::InvalidResponseException => e
91
- $stderr.puts "Error: Something went wrong: #{e}"
90
+ rescue CoinTools::InvalidFiatCurrencyError => e
91
+ $stderr.puts "Error: Unsupported fiat currency: '#{fiat_currency}'"
92
92
  exit 1
93
- rescue CoinTools::CoinMarketCap::NoDataException => e
94
- $stderr.puts "Error: #{e}"
93
+ rescue CoinTools::UnknownCoinError => e
94
+ if symbol
95
+ $stderr.puts "Error: No such coin: #{symbol.upcase}"
96
+ else
97
+ $stderr.puts "Error: No such coin: #{coin_name} (remember to use CoinMarketCap's IDs, or the -s option)"
98
+ end
95
99
  exit 1
96
- rescue CoinTools::CoinMarketCap::InvalidFiatCurrencyException => e
97
- $stderr.puts "Error: Unsupported fiat currency: #{fiat_currency}"
100
+ rescue CoinTools::ResponseError => e
101
+ $stderr.puts "Error: Something went wrong: #{e.nice_message}"
98
102
  exit 1
99
103
  end
data/bin/coincap CHANGED
@@ -1,25 +1,24 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  require 'bundler/setup'
4
- require 'cointools'
4
+ require 'cointools/coincap'
5
5
 
6
6
  require 'optparse'
7
- require 'time'
8
7
 
9
8
  def print_help
10
- puts "Usage: #{$PROGRAM_NAME} <symbol> [<date>] [-b/--btc-price | -e/--eur-price] [-v/--verbose]"
9
+ puts "Usage: #{$PROGRAM_NAME} <symbol> [<date>] [-b/--btc-price | -e/--eur-price] [-q/--quiet]"
11
10
  puts " e.g.: #{$PROGRAM_NAME} NANO \"2017-12-20 15:00\""
12
11
  puts
13
12
  puts "* -b / --btc-price: returns the coin's price in BTC instead of USD (current prices only)"
14
13
  puts "* -e / --eur-price: returns the coin's price in EUR (current prices only)"
15
14
  end
16
15
 
17
- verbose = false
16
+ quiet = false
18
17
  btc_price = false
19
18
  eur_price = false
20
19
 
21
20
  OptionParser.new do |opts|
22
- opts.on('-v', '--verbose') { verbose = true }
21
+ opts.on('-q', '--quiet') { quiet = true }
23
22
  opts.on('-b', '--btc-price') { btc_price = true }
24
23
  opts.on('-e', '--eur-price') { eur_price = true }
25
24
 
@@ -37,19 +36,19 @@ if ARGV.length < 1 || ARGV.length > 2
37
36
  end
38
37
 
39
38
  symbol = ARGV[0]
40
- date = Time.parse(ARGV[1]) if ARGV[1]
39
+ date = ARGV[1]
41
40
 
42
- if eur_price && btc_price
43
- puts "#{$PROGRAM_NAME}: --btc-price and --eur-price options cannot be used together"
44
- exit 1
45
- end
41
+ begin
42
+ if eur_price && btc_price
43
+ puts "#{$PROGRAM_NAME}: --btc-price and --eur-price options cannot be used together"
44
+ exit 1
45
+ end
46
46
 
47
- if date && (eur_price || btc_price)
48
- puts "#{$PROGRAM_NAME}: --btc-price and --eur-price options cannot be used for historical prices"
49
- exit 1
50
- end
47
+ if date && (eur_price || btc_price)
48
+ puts "#{$PROGRAM_NAME}: --btc-price and --eur-price options cannot be used for historical prices"
49
+ exit 1
50
+ end
51
51
 
52
- begin
53
52
  result = CoinTools::CoinCap.new.get_price(symbol, date)
54
53
 
55
54
  if btc_price
@@ -63,19 +62,22 @@ begin
63
62
  unit = 'USD'
64
63
  end
65
64
 
66
- if verbose
65
+ if quiet
66
+ puts price
67
+ else
67
68
  puts "#{symbol.upcase} @ #{result.time || Time.now} ==> #{price} #{unit}"
68
69
  puts
69
- else
70
- puts price
71
70
  end
72
- rescue CoinTools::CoinCap::BadRequestException => e
73
- $stderr.puts "Error: Incorrect coin name: #{coin_name} (#{e})"
71
+ rescue CoinTools::InvalidSymbolError => e
72
+ $stderr.puts "Error: Invalid coin name: '#{symbol}'"
73
+ exit 1
74
+ rescue CoinTools::InvalidDateError => e
75
+ $stderr.puts "Error: Invalid date format: '#{date}' (#{e})"
74
76
  exit 1
75
- rescue CoinTools::CoinCap::InvalidResponseException => e
76
- $stderr.puts "Error: Something went wrong: #{e}"
77
+ rescue CoinTools::UnknownCoinError => e
78
+ $stderr.puts "Error: No such coin: #{symbol.upcase}"
77
79
  exit 1
78
- rescue CoinTools::CoinCap::NoDataException => e
79
- $stderr.puts "Error: #{e}"
80
+ rescue CoinTools::ResponseError => e
81
+ $stderr.puts "Error: Something went wrong: #{e.nice_message}"
80
82
  exit 1
81
83
  end
data/bin/cryptowatch CHANGED
@@ -1,27 +1,26 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  require 'bundler/setup'
4
- require 'cointools'
4
+ require 'cointools/cryptowatch'
5
5
 
6
6
  require 'optparse'
7
- require 'time'
8
7
 
9
8
  def print_help
10
- puts "Usage: #{$PROGRAM_NAME} <exchange> <market> [<date>] [-v/--verbose] [-f/--fast]"
9
+ puts "Usage: #{$PROGRAM_NAME} <exchange> <market> [<date>] [-q/--quiet] [-f/--fast]"
11
10
  puts " e.g.: #{$PROGRAM_NAME} gdax btcusd \"2017-06-30 15:27\""
12
11
  puts
13
12
  puts "* --fast: tries to use less API time allowance at the cost of worse reliability"
14
13
  puts "* To print a list of available exchanges:"
15
- puts " #{$PROGRAM_NAME} --list-exchanges"
14
+ puts " #{$PROGRAM_NAME} --exchanges"
16
15
  puts "* To print a list of available markets on an exchange:"
17
- puts " #{$PROGRAM_NAME} --list-markets kraken"
16
+ puts " #{$PROGRAM_NAME} --markets kraken"
18
17
  end
19
18
 
20
- verbose = false
19
+ quiet = false
21
20
  fast = false
22
21
 
23
22
  OptionParser.new do |opts|
24
- opts.on('-v', '--verbose') { verbose = true }
23
+ opts.on('-q', '--quiet') { quiet = true }
25
24
 
26
25
  opts.on('-h', '--help') do
27
26
  print_help
@@ -30,17 +29,23 @@ OptionParser.new do |opts|
30
29
 
31
30
  opts.on('-f', '--fast') { fast = true }
32
31
 
33
- opts.on('--list-exchanges') do
32
+ opts.on('--exchanges') do
34
33
  puts CoinTools::Cryptowatch.new.exchanges
35
34
  exit 0
36
35
  end
37
36
 
38
- opts.on('--list-markets EXCHANGE') do |exchange|
37
+ opts.on('--markets EXCHANGE') do |exchange|
39
38
  begin
40
39
  puts CoinTools::Cryptowatch.new.get_markets(exchange)
41
40
  exit 0
42
- rescue CoinTools::Cryptowatch::BadRequestException => e
43
- $stderr.puts "Error: Incorrect exchange name: #{e}"
41
+ rescue CoinTools::InvalidExchangeError => e
42
+ $stderr.puts "Error: Invalid exchange name: '#{exchange}'"
43
+ exit 1
44
+ rescue CoinTools::UnknownExchangeError => e
45
+ $stderr.puts "Error: Unknown exchange: #{exchange}"
46
+ exit 1
47
+ rescue CoinTools::ResponseError => e
48
+ $stderr.puts "Error: Something went wrong: #{e.nice_message}"
44
49
  exit 1
45
50
  end
46
51
  end
@@ -55,25 +60,32 @@ end
55
60
 
56
61
  exchange = ARGV[0].downcase
57
62
  market = ARGV[1].downcase
58
- date = Time.parse(ARGV[2]) if ARGV[2]
63
+ date = ARGV[2]
64
+ currency = market[-3..-1].upcase
59
65
 
60
66
  begin
61
67
  method = fast ? :get_price_fast : :get_price
62
68
  result = CoinTools::Cryptowatch.new.send(method, exchange, market, date)
63
69
 
64
- if verbose
65
- puts "#{exchange}:#{market} @ #{result.time || Time.now} ==> #{result.price}"
66
- puts
67
- else
70
+ if quiet
68
71
  puts result.price
72
+ else
73
+ puts "#{exchange}:#{market} @ #{result.time || Time.now} ==> #{result.price} #{currency}"
74
+ puts
69
75
  end
70
- rescue CoinTools::Cryptowatch::InvalidDateException => e
71
- $stderr.puts "Error: #{e}"
76
+ rescue CoinTools::InvalidExchangeError => e
77
+ $stderr.puts "Error: Invalid exchange name: '#{exchange}'"
78
+ exit 1
79
+ rescue CoinTools::InvalidSymbolError => e
80
+ $stderr.puts "Error: Invalid market name: '#{market}'"
81
+ exit 1
82
+ rescue CoinTools::InvalidDateError => e
83
+ $stderr.puts "Error: Invalid date format: '#{date}' (#{e})"
72
84
  exit 1
73
- rescue CoinTools::Cryptowatch::BadRequestException => e
74
- $stderr.puts "Error: Incorrect exchange or market name (#{e})"
85
+ rescue CoinTools::UnknownCoinError => e
86
+ $stderr.puts "Error: Unknown exchange/market pair: #{exchange}/#{market}"
75
87
  exit 1
76
- rescue CoinTools::Cryptowatch::NoDataException => e
77
- $stderr.puts "Error: #{e}: data not ready yet"
88
+ rescue CoinTools::ResponseError => e
89
+ $stderr.puts "Error: Something went wrong: #{e.nice_message}"
78
90
  exit 1
79
91
  end
@@ -0,0 +1,21 @@
1
+ module CoinTools
2
+ class BaseStruct
3
+ def self.make(*fields)
4
+ struct = Class.new(self)
5
+
6
+ fields.each do |f|
7
+ raise ArgumentError.new("Invalid field name: #{f}") unless f.to_s =~ /[a-z][a-z_]*/
8
+ end
9
+
10
+ struct.class_eval <<-CODE
11
+ def initialize(#{fields.map { |f| "#{f}:" }.join(', ')})
12
+ #{fields.map { |f| "@#{f} = #{f}" }.join("\n")}
13
+ end
14
+
15
+ attr_reader #{fields.map { |f| ":#{f}" }.join(', ')}
16
+ CODE
17
+
18
+ struct
19
+ end
20
+ end
21
+ end
@@ -1,38 +1,30 @@
1
- require_relative 'version'
1
+ require_relative 'base_struct'
2
+ require_relative 'errors'
3
+ require_relative 'request'
4
+ require_relative 'utils'
2
5
 
3
- require 'json'
4
6
  require 'net/http'
5
7
  require 'uri'
6
8
 
7
9
  module CoinTools
8
10
  class CoinCap
9
11
  BASE_URL = "https://coincap.io"
10
- USER_AGENT = "cointools/#{CoinTools::VERSION}"
11
-
12
- DataPoint = Struct.new(:time, :usd_price, :eur_price, :btc_price)
13
-
14
12
  PERIODS = [1, 7, 30, 90, 180, 365]
15
13
 
16
- class InvalidResponseException < StandardError
17
- attr_reader :response
14
+ DataPoint = BaseStruct.make(:time, :usd_price, :eur_price, :btc_price)
18
15
 
19
- def initialize(response)
20
- super("#{response.code} #{response.message}")
21
- @response = response
22
- end
23
- end
24
-
25
- class BadRequestException < InvalidResponseException
26
- end
27
-
28
- class InvalidDateException < StandardError
29
- end
30
16
 
31
17
  def get_price(symbol, time = nil)
32
- return get_current_price(symbol) if time.nil?
18
+ raise InvalidSymbolError if symbol.to_s.empty?
19
+
20
+ if time.nil?
21
+ return get_current_price(symbol)
22
+ elsif time.is_a?(String)
23
+ time = Utils.parse_time(time)
24
+ end
33
25
 
34
- (time <= Time.now) or raise InvalidDateException.new('Future date was passed')
35
- (time.year >= 2009) or raise InvalidDateException.new('Too early date was passed')
26
+ (time <= Time.now) or raise InvalidDateError.new('Future date was passed')
27
+ (time.year >= 2009) or raise InvalidDateError.new('Too early date was passed')
36
28
 
37
29
  period = period_for_time(time)
38
30
 
@@ -42,59 +34,63 @@ module CoinTools
42
34
  url = URI("#{BASE_URL}/history/#{symbol.upcase}")
43
35
  end
44
36
 
45
- unixtime = time.to_i
46
-
47
- response = make_request(url)
37
+ response = Request.get(url)
48
38
 
49
39
  case response
50
40
  when Net::HTTPSuccess
51
- json = JSON.load(response.body)
41
+ json = Utils.parse_json(response.body)
42
+ raise UnknownCoinError.new(response) if json.nil? || json.empty?
43
+ raise JSONError.new(response) unless json.is_a?(Hash)
44
+
52
45
  data = json['price']
46
+ raise JSONError.new(response) unless data.is_a?(Array)
53
47
 
48
+ unixtime = time.to_i
54
49
  timestamp, price = best_matching_record(data, unixtime)
50
+ raise NoDataError.new(response) if timestamp.nil? || price.nil?
51
+
55
52
  actual_time = Time.at(timestamp / 1000)
56
53
 
57
- return DataPoint.new(actual_time, price, nil, nil)
58
- when Net::HTTPBadRequest
59
- raise BadRequestException.new(response)
54
+ return DataPoint.new(time: actual_time, usd_price: price, eur_price: nil, btc_price: nil)
55
+ when Net::HTTPClientError
56
+ raise BadRequestError.new(response)
60
57
  else
61
- raise InvalidResponseException.new(response)
58
+ raise ServiceUnavailableError.new(response)
62
59
  end
63
60
  end
64
61
 
65
62
  def get_current_price(symbol)
63
+ raise InvalidSymbolError if symbol.to_s.empty?
64
+
66
65
  url = URI("#{BASE_URL}/page/#{symbol.upcase}")
67
66
 
68
- response = make_request(url)
67
+ response = Request.get(url)
69
68
 
70
69
  case response
71
70
  when Net::HTTPSuccess
72
- json = JSON.load(response.body)
71
+ json = Utils.parse_json(response.body)
72
+ raise UnknownCoinError.new(response) if json.nil? || json.empty?
73
+ raise JSONError.new(response) unless json.is_a?(Hash)
73
74
 
74
75
  usd_price = json['price_usd']
75
76
  eur_price = json['price_eur']
76
77
  btc_price = json['price_btc']
77
78
 
78
- return DataPoint.new(nil, usd_price, eur_price, btc_price)
79
- when Net::HTTPBadRequest
80
- raise BadRequestException.new(response)
79
+ if usd_price || eur_price || btc_price
80
+ return DataPoint.new(time: nil, usd_price: usd_price, eur_price: eur_price, btc_price: btc_price)
81
+ else
82
+ raise NoDataError.new(response)
83
+ end
84
+ when Net::HTTPClientError
85
+ raise BadRequestError.new(response)
81
86
  else
82
- raise InvalidResponseException.new(response)
87
+ raise ServiceUnavailableError.new(response)
83
88
  end
84
89
  end
85
90
 
86
91
 
87
92
  private
88
93
 
89
- def make_request(url)
90
- Net::HTTP.start(url.host, url.port, use_ssl: true) do |http|
91
- request = Net::HTTP::Get.new(url)
92
- request['User-Agent'] = USER_AGENT
93
-
94
- http.request(request)
95
- end
96
- end
97
-
98
94
  def best_matching_record(data, unixtime)
99
95
  millitime = unixtime * 1000
100
96
  data.sort_by { |record| (record[0] - millitime).abs }.first
@@ -1,13 +1,14 @@
1
- require_relative 'version'
1
+ require_relative 'base_struct'
2
+ require_relative 'errors'
3
+ require_relative 'request'
4
+ require_relative 'utils'
2
5
 
3
- require 'json'
4
6
  require 'net/http'
5
7
  require 'uri'
6
8
 
7
9
  module CoinTools
8
10
  class CoinMarketCap
9
11
  BASE_URL = "https://api.coinmarketcap.com"
10
- USER_AGENT = "cointools/#{CoinTools::VERSION}"
11
12
 
12
13
  FIAT_CURRENCIES = [
13
14
  'AUD', 'BRL', 'CAD', 'CHF', 'CLP', 'CNY', 'CZK', 'DKK', 'EUR', 'GBP',
@@ -16,120 +17,224 @@ module CoinTools
16
17
  'ZAR'
17
18
  ]
18
19
 
19
- class DataPoint
20
- attr_reader :time, :usd_price, :btc_price, :converted_price
20
+ class Listing
21
+ attr_reader :numeric_id, :name, :symbol, :text_id
21
22
 
22
- def initialize(time, usd_price, btc_price, converted_price = nil)
23
- @time = time
24
- @usd_price = usd_price
25
- @btc_price = btc_price
26
- @converted_price = converted_price
23
+ def initialize(json)
24
+ [:id, :name, :symbol, :website_slug].each do |key|
25
+ raise ArgumentError.new("Missing #{key} field") unless json[key.to_s]
26
+ end
27
+
28
+ @numeric_id = json['id']
29
+ @name = json['name']
30
+ @symbol = json['symbol']
31
+ @text_id = json['website_slug']
27
32
  end
28
33
  end
29
34
 
30
- class InvalidResponseException < StandardError
31
- attr_reader :response
35
+ class CoinData < Listing
36
+ attr_reader :last_updated, :usd_price, :btc_price, :converted_price, :rank, :market_cap
37
+
38
+ def initialize(json, convert_to = nil)
39
+ super(json)
40
+
41
+ raise ArgumentError.new('Missing rank field') unless json['rank']
42
+ raise ArgumentError.new('Invalid rank field') unless json['rank'].is_a?(Integer) && json['rank'] > 0
43
+ @rank = json['rank']
32
44
 
33
- def initialize(response)
34
- super("#{response.code} #{response.message}")
35
- @response = response
45
+ quotes = json['quotes']
46
+ raise ArgumentError.new('Missing quotes field') unless quotes
47
+ raise ArgumentError.new('Invalid quotes field') unless quotes.is_a?(Hash)
48
+
49
+ usd_quote = quotes['USD']
50
+ raise ArgumentError.new('Missing USD quote info') unless usd_quote
51
+ raise ArgumentError.new('Invalid USD quote info') unless usd_quote.is_a?(Hash)
52
+
53
+ @usd_price = usd_quote['price']&.to_f
54
+ @market_cap = usd_quote['market_cap']&.to_f
55
+
56
+ if convert_to
57
+ converted_quote = quotes[convert_to.upcase]
58
+
59
+ if converted_quote
60
+ raise ArgumentError.new("Invalid #{convert_to.upcase} quote info") unless converted_quote.is_a?(Hash)
61
+ @converted_price = converted_quote['price']&.to_f
62
+ end
63
+ else
64
+ btc_quote = quotes['BTC']
65
+
66
+ if btc_quote
67
+ raise ArgumentError.new("Invalid BTC quote info") unless btc_quote.is_a?(Hash)
68
+ @btc_price = btc_quote['price']&.to_f
69
+ end
70
+ end
71
+
72
+ timestamp = json['last_updated']
73
+ @last_updated = Time.at(timestamp) if timestamp
36
74
  end
37
75
  end
38
76
 
39
- class BadRequestException < InvalidResponseException
77
+ def symbol_map
78
+ load_listings if @symbol_map.nil?
79
+ @symbol_map
40
80
  end
41
81
 
42
- class NoDataException < StandardError
82
+ def id_map
83
+ load_listings if @id_map.nil?
84
+ @id_map
43
85
  end
44
86
 
45
- class InvalidFiatCurrencyException < StandardError
46
- end
87
+ def load_listings
88
+ url = URI("#{BASE_URL}/v2/listings/")
89
+
90
+ response = Request.get(url)
91
+
92
+ case response
93
+ when Net::HTTPSuccess
94
+ data = parse_response(response, Array)
47
95
 
48
- class InvalidDateException < StandardError
96
+ @id_map = {}
97
+ @symbol_map = {}
98
+
99
+ begin
100
+ data.each do |record|
101
+ listing = Listing.new(record)
102
+
103
+ @id_map[listing.text_id] = listing
104
+ @symbol_map[listing.symbol] = listing
105
+ end
106
+ rescue ArgumentError => e
107
+ # TODO: JSONError vs. NoDataError? + error class docs
108
+ raise JSONError.new(response, e.message)
109
+ end
110
+
111
+ data.length
112
+ when Net::HTTPClientError
113
+ raise BadRequestError.new(response)
114
+ else
115
+ raise ServiceUnavailableError.new(response)
116
+ end
49
117
  end
50
118
 
51
119
  def get_price(coin_name, convert_to: nil)
52
- url = URI("#{BASE_URL}/v1/ticker/#{coin_name}/")
120
+ raise InvalidSymbolError if coin_name.to_s.empty?
121
+
122
+ validate_fiat_currency(convert_to) if convert_to
123
+
124
+ listing = id_map[coin_name.to_s]
125
+ raise InvalidSymbolError if listing.nil?
126
+
127
+ get_price_for_listing(listing, convert_to: convert_to)
128
+ end
129
+
130
+ def get_price_by_symbol(coin_symbol, convert_to: nil)
131
+ raise InvalidSymbolError if coin_symbol.to_s.empty?
132
+
133
+ validate_fiat_currency(convert_to) if convert_to
134
+
135
+ listing = symbol_map[coin_symbol.to_s]
136
+ raise InvalidSymbolError if listing.nil?
137
+
138
+ get_price_for_listing(listing, convert_to: convert_to)
139
+ end
140
+
141
+ def get_price_for_listing(listing, convert_to: nil)
142
+ url = URI("#{BASE_URL}/v2/ticker/#{listing.numeric_id}/")
53
143
 
54
144
  if convert_to
55
- validate_fiat_currency(convert_to)
56
- url += "?convert=#{convert_to}"
145
+ url.query = "convert=#{convert_to.upcase}"
146
+ else
147
+ url.query = "convert=BTC"
57
148
  end
58
149
 
59
- response = make_request(url)
150
+ response = Request.get(url)
60
151
 
61
152
  case response
62
153
  when Net::HTTPSuccess
63
- json = JSON.load(response.body)
64
- record = json[0]
65
-
66
- usd_price = record['price_usd']&.to_f
67
- btc_price = record['price_btc']&.to_f
68
- timestamp = Time.at(record['last_updated'].to_i)
154
+ record = parse_response(response, Hash)
69
155
 
70
- if convert_to
71
- converted_price = record["price_#{convert_to.downcase}"]&.to_f
72
- raise NoDataException.new('Conversion to chosen fiat currency failed') if converted_price.nil?
156
+ begin
157
+ return CoinData.new(record, convert_to)
158
+ rescue ArgumentError => e
159
+ raise JSONError.new(response, e.message)
73
160
  end
74
-
75
- return DataPoint.new(timestamp, usd_price, btc_price, converted_price)
76
161
  when Net::HTTPNotFound
77
- raise BadRequestException.new(response)
162
+ raise UnknownCoinError.new(response)
163
+ when Net::HTTPClientError
164
+ raise BadRequestError.new(response)
78
165
  else
79
- raise InvalidResponseException.new(response)
166
+ raise ServiceUnavailableError.new(response)
80
167
  end
81
168
  end
82
169
 
83
- def get_price_by_symbol(coin_symbol, convert_to: nil)
84
- url = URI("#{BASE_URL}/v1/ticker/?limit=0")
85
- symbol = coin_symbol.downcase
170
+ def get_all_prices(convert_to: nil, &block)
171
+ url = URI("#{BASE_URL}/v2/ticker/?structure=array&sort=id&limit=100")
86
172
 
87
173
  if convert_to
88
174
  validate_fiat_currency(convert_to)
89
- url.query += "&convert=#{convert_to}"
175
+ url.query += "&convert=#{convert_to.upcase}"
176
+ else
177
+ url.query += "&convert=BTC"
90
178
  end
91
179
 
92
- response = make_request(url)
180
+ start = 0
181
+ coins = []
182
+
183
+ loop do
184
+ new_batch = fetch_full_ticker_page(url, convert_to, coins.length, &block)
185
+
186
+ if new_batch.empty?
187
+ break
188
+ else
189
+ coins.concat(new_batch)
190
+ end
191
+ end
192
+
193
+ coins.sort_by(&:rank)
194
+ end
195
+
196
+
197
+ private
198
+
199
+ def fetch_full_ticker_page(base_url, convert_to, start)
200
+ url = base_url.clone
201
+ url.query += "&start=#{start}"
202
+ response = Request.get(url)
93
203
 
94
204
  case response
95
205
  when Net::HTTPSuccess
96
- json = JSON.load(response.body)
97
- record = json.detect { |r| r['symbol'].downcase == symbol }
98
- raise NoDataException.new('No coin found with given symbol') if record.nil?
99
-
100
- usd_price = record['price_usd']&.to_f
101
- btc_price = record['price_btc']&.to_f
102
- timestamp = Time.at(record['last_updated'].to_i)
206
+ data = parse_response(response, Array)
103
207
 
104
- if convert_to
105
- converted_price = record["price_#{convert_to.downcase}"]&.to_f
106
- raise NoDataException.new('Conversion to chosen fiat currency failed') if converted_price.nil?
208
+ begin
209
+ new_batch = data.map { |record| CoinData.new(record, convert_to) }
210
+ rescue ArgumentError => e
211
+ raise JSONError.new(response, e.message)
107
212
  end
108
213
 
109
- return DataPoint.new(timestamp, usd_price, btc_price, converted_price)
214
+ yield new_batch if block_given? && !new_batch.empty?
215
+
216
+ new_batch
110
217
  when Net::HTTPNotFound
111
- raise BadRequestException.new(response)
218
+ []
219
+ when Net::HTTPClientError
220
+ raise BadRequestError.new(response)
112
221
  else
113
- raise InvalidResponseException.new(response)
222
+ raise ServiceUnavailableError.new(response)
114
223
  end
115
224
  end
116
225
 
117
-
118
- private
119
-
120
226
  def validate_fiat_currency(fiat_currency)
121
- unless FIAT_CURRENCIES.include?(fiat_currency.upcase)
122
- raise InvalidFiatCurrencyException
123
- end
227
+ raise InvalidFiatCurrencyError unless FIAT_CURRENCIES.include?(fiat_currency.upcase)
124
228
  end
125
229
 
126
- def make_request(url)
127
- Net::HTTP.start(url.host, url.port, use_ssl: true) do |http|
128
- request = Net::HTTP::Get.new(url)
129
- request['User-Agent'] = USER_AGENT
230
+ def parse_response(response, expected_data_type)
231
+ json = Utils.parse_json(response.body)
130
232
 
131
- http.request(request)
132
- end
233
+ raise JSONError.new(response) unless json.is_a?(Hash) && json['metadata']
234
+ raise BadRequestError.new(response, json['metadata']['error']) if json['metadata']['error']
235
+ raise JSONError.new(response) unless json['data'].is_a?(expected_data_type)
236
+
237
+ json['data']
133
238
  end
134
239
  end
135
240
  end
@@ -1,15 +1,14 @@
1
- require_relative 'version'
1
+ require_relative 'base_struct'
2
+ require_relative 'errors'
3
+ require_relative 'request'
4
+ require_relative 'utils'
2
5
 
3
- require 'json'
4
6
  require 'net/http'
5
7
  require 'uri'
6
8
 
7
9
  module CoinTools
8
10
  class Cryptowatch
9
11
  BASE_URL = "https://api.cryptowat.ch"
10
- USER_AGENT = "cointools/#{CoinTools::VERSION}"
11
-
12
- DataPoint = Struct.new(:price, :time, :api_time_spent, :api_time_remaining)
13
12
 
14
13
  # we expect this many days worth of data for a given period precision (in seconds); NOT guaranteed by the API
15
14
  DAYS_FOR_PERIODS = {
@@ -17,96 +16,130 @@ module CoinTools
17
16
  7200 => 365, 14400 => 1.5 * 365, 21600 => 2 * 365, 43200 => 3 * 365, 86400 => 4 * 365
18
17
  }
19
18
 
20
- class InvalidResponseException < StandardError
21
- attr_reader :response
22
-
23
- def initialize(response)
24
- super("#{response.code} #{response.message}")
25
- @response = response
26
- end
27
- end
28
-
29
- class BadRequestException < InvalidResponseException
30
- end
19
+ DataPoint = BaseStruct.make(:price, :time, :api_time_spent, :api_time_remaining)
31
20
 
32
- class NoDataException < StandardError
33
- end
34
-
35
- class InvalidDateException < StandardError
36
- end
37
21
 
38
22
  def exchanges
39
23
  @exchanges ||= get_exchanges
40
24
  end
41
25
 
42
26
  def get_markets(exchange)
27
+ raise InvalidExchangeError if exchange.to_s.empty?
28
+
43
29
  url = URI("#{BASE_URL}/markets/#{exchange}")
44
30
 
45
- response = make_request(url)
31
+ response = Request.get(url)
46
32
 
47
33
  case response
48
34
  when Net::HTTPSuccess
49
- json = JSON.load(response.body)
35
+ json = Utils.parse_json(response.body)
36
+ raise JSONError.new(response) unless json.is_a?(Hash) && json['result'].is_a?(Array)
37
+
50
38
  return json['result'].select { |m| m['active'] == true }.map { |m| m['pair'] }.sort
51
- when Net::HTTPBadRequest
52
- raise BadRequestException.new(response)
39
+ when Net::HTTPNotFound
40
+ raise UnknownExchangeError.new(response)
41
+ when Net::HTTPClientError
42
+ raise BadRequestError.new(response)
53
43
  else
54
- raise InvalidResponseException.new(response)
44
+ raise ServiceUnavailableError.new(response)
55
45
  end
56
46
  end
57
47
 
58
48
  def get_price(exchange, market, time = nil)
59
- return get_current_price(exchange, market) if time.nil?
49
+ raise InvalidExchangeError if exchange.to_s.empty?
50
+ raise InvalidSymbolError if market.to_s.empty?
60
51
 
61
- (time <= Time.now) or raise InvalidDateException.new('Future date was passed')
62
- (time.year >= 2009) or raise InvalidDateException.new('Too early date was passed')
52
+ if time.nil?
53
+ return get_current_price(exchange, market)
54
+ elsif time.is_a?(String)
55
+ time = Utils.parse_time(time)
56
+ end
57
+
58
+ (time <= Time.now) or raise InvalidDateError.new('Future date was passed')
59
+ (time.year >= 2009) or raise InvalidDateError.new('Too early date was passed')
63
60
 
64
61
  unixtime = time.to_i
65
62
  current_time = Time.now.to_i
66
63
  url = URI("#{BASE_URL}/markets/#{exchange}/#{market}/ohlc?after=#{unixtime}")
67
64
 
68
- response = make_request(url)
65
+ response = Request.get(url)
69
66
 
70
67
  case response
71
68
  when Net::HTTPSuccess
72
- json = JSON.load(response.body)
69
+ json = Utils.parse_json(response.body)
70
+ raise JSONError.new(response) unless json.is_a?(Hash)
71
+
73
72
  data = json['result']
74
73
  allowance = json['allowance']
74
+ raise JSONError.new(response) unless data.is_a?(Hash) && allowance.is_a?(Hash)
75
75
 
76
76
  timestamp, o, h, l, c, volume = best_matching_record(data, unixtime, current_time)
77
- raise NoDataException.new('No data found for a given time') if timestamp.nil?
77
+ raise NoDataError.new(response, 'No price data returned') unless timestamp && o
78
78
 
79
79
  actual_time = Time.at(timestamp)
80
- return DataPoint.new(o, actual_time, allowance['cost'], allowance['remaining'])
81
- when Net::HTTPBadRequest
82
- raise BadRequestException.new(response)
80
+
81
+ return DataPoint.new(
82
+ price: o,
83
+ time: actual_time,
84
+ api_time_spent: allowance['cost'],
85
+ api_time_remaining: allowance['remaining']
86
+ )
87
+ when Net::HTTPNotFound
88
+ raise UnknownCoinError.new(response)
89
+ when Net::HTTPClientError
90
+ raise BadRequestError.new(response)
83
91
  else
84
- raise InvalidResponseException.new(response)
92
+ raise ServiceUnavailableError.new(response)
85
93
  end
86
94
  end
87
95
 
88
96
  def get_current_price(exchange, market)
97
+ raise InvalidExchangeError if exchange.to_s.empty?
98
+ raise InvalidSymbolError if market.to_s.empty?
99
+
89
100
  url = URI("#{BASE_URL}/markets/#{exchange}/#{market}/price")
90
101
 
91
- response = make_request(url)
102
+ response = Request.get(url)
92
103
 
93
104
  case response
94
105
  when Net::HTTPSuccess
95
- json = JSON.load(response.body)
96
- price = json['result']['price']
97
- allowance = json['allowance']
106
+ json = Utils.parse_json(response.body)
107
+ raise JSONError.new(response) unless json.is_a?(Hash)
98
108
 
99
- return DataPoint.new(price, nil, allowance['cost'], allowance['remaining'])
100
- when Net::HTTPBadRequest
101
- raise BadRequestException.new(response)
109
+ data = json['result']
110
+ allowance = json['allowance']
111
+ raise JSONError.new(response) unless data.is_a?(Hash) && allowance.is_a?(Hash)
112
+
113
+ price = data['price']
114
+ raise NoDataError.new(response) unless price
115
+
116
+ return DataPoint.new(
117
+ price: price,
118
+ time: nil,
119
+ api_time_spent: allowance['cost'],
120
+ api_time_remaining: allowance['remaining']
121
+ )
122
+ when Net::HTTPNotFound
123
+ raise UnknownCoinError.new(response)
124
+ when Net::HTTPClientError
125
+ raise BadRequestError.new(response)
102
126
  else
103
- raise InvalidResponseException.new(response)
127
+ raise ServiceUnavailableError.new(response)
104
128
  end
105
129
  end
106
130
 
107
- def get_price_fast(exchange, market, time)
108
- (time <= Time.now) or raise InvalidDateException.new('Future date was passed')
109
- (time.year >= 2009) or raise InvalidDateException.new('Too early date was passed')
131
+ def get_price_fast(exchange, market, time = nil)
132
+ raise InvalidExchangeError if exchange.to_s.empty?
133
+ raise InvalidSymbolError if market.to_s.empty?
134
+
135
+ if time.nil?
136
+ return get_current_price(exchange, market)
137
+ elsif time.is_a?(String)
138
+ time = Utils.parse_time(time)
139
+ end
140
+
141
+ (time <= Time.now) or raise InvalidDateError.new('Future date was passed')
142
+ (time.year >= 2009) or raise InvalidDateError.new('Too early date was passed')
110
143
 
111
144
  period = precision_for_time(time)
112
145
 
@@ -118,43 +151,45 @@ module CoinTools
118
151
  current_time = Time.now.to_i
119
152
  url = URI("#{BASE_URL}/markets/#{exchange}/#{market}/ohlc?after=#{unixtime}&periods=#{period}")
120
153
 
121
- response = make_request(url)
154
+ response = Request.get(url)
122
155
 
123
156
  case response
124
157
  when Net::HTTPSuccess
125
- json = JSON.load(response.body)
158
+ json = Utils.parse_json(response.body)
159
+ raise JSONError.new(response) unless json.is_a?(Hash)
160
+
126
161
  data = json['result']
127
162
  allowance = json['allowance']
163
+ raise JSONError.new(response) unless data.is_a?(Hash) && allowance.is_a?(Hash)
128
164
 
129
165
  timestamp, o, h, l, c, volume = best_matching_record(data, unixtime, current_time)
130
- raise NoDataException.new('No data found for a given time') if timestamp.nil?
166
+ raise NoDataError.new(response, 'No price data returned') unless timestamp && o
131
167
 
132
168
  actual_time = Time.at(timestamp)
133
- return DataPoint.new(o, actual_time, allowance['cost'], allowance['remaining'])
134
- when Net::HTTPBadRequest
135
- raise BadRequestException.new(response)
169
+
170
+ return DataPoint.new(
171
+ price: o,
172
+ time: actual_time,
173
+ api_time_spent: allowance['cost'],
174
+ api_time_remaining: allowance['remaining']
175
+ )
176
+ when Net::HTTPNotFound
177
+ raise UnknownCoinError.new(response)
178
+ when Net::HTTPClientError
179
+ raise BadRequestError.new(response)
136
180
  else
137
- raise InvalidResponseException.new(response)
181
+ raise ServiceUnavailableError.new(response)
138
182
  end
139
183
  end
140
184
 
141
185
 
142
186
  private
143
187
 
144
- def make_request(url)
145
- Net::HTTP.start(url.host, url.port, use_ssl: true) do |http|
146
- request = Net::HTTP::Get.new(url)
147
- request['User-Agent'] = USER_AGENT
148
-
149
- http.request(request)
150
- end
151
- end
152
-
153
188
  def best_matching_record(data, unixtime, request_time)
154
189
  candidates = []
155
190
 
156
191
  data.keys.sort_by { |k| k.to_i }.each do |k|
157
- records = data[k]
192
+ records = data[k] || []
158
193
  previous = nil
159
194
 
160
195
  records.each do |record|
@@ -192,14 +227,18 @@ module CoinTools
192
227
  def get_exchanges
193
228
  url = URI("#{BASE_URL}/exchanges")
194
229
 
195
- response = make_request(url)
230
+ response = Request.get(url)
196
231
 
197
232
  case response
198
233
  when Net::HTTPSuccess
199
- json = JSON.load(response.body)
234
+ json = Utils.parse_json(response.body)
235
+ raise JSONError.new(response) unless json.is_a?(Hash) && json['result'].is_a?(Array)
236
+
200
237
  return json['result'].select { |e| e['active'] == true }.map { |e| e['symbol'] }.sort
238
+ when Net::HTTPClientError
239
+ raise BadRequestError.new(response)
201
240
  else
202
- raise InvalidResponseException.new(response)
241
+ raise ServiceUnavailableError.new(response)
203
242
  end
204
243
  end
205
244
  end
@@ -0,0 +1,57 @@
1
+ module CoinTools
2
+ class Error < StandardError
3
+ def nice_message
4
+ "#{self.class}: #{message}"
5
+ end
6
+ end
7
+
8
+
9
+ class UserError < Error
10
+ end
11
+
12
+ class InvalidDateError < UserError
13
+ end
14
+
15
+ class InvalidExchangeError < UserError
16
+ end
17
+
18
+ class InvalidFiatCurrencyError < UserError
19
+ end
20
+
21
+ class InvalidSymbolError < UserError
22
+ end
23
+
24
+
25
+ class ResponseError < Error
26
+ attr_reader :response
27
+
28
+ def initialize(response, message = nil)
29
+ super(message || "#{response.code} #{response.message}")
30
+ @response = response
31
+ end
32
+ end
33
+
34
+ class JSONError < ResponseError
35
+ def initialize(response, message = nil)
36
+ super(response, message || "Incorrect JSON structure")
37
+ end
38
+ end
39
+
40
+ class NoDataError < ResponseError
41
+ def initialize(response, message = nil)
42
+ super(response, message || "Missing data in the response")
43
+ end
44
+ end
45
+
46
+ class BadRequestError < ResponseError
47
+ end
48
+
49
+ class UnknownCoinError < BadRequestError
50
+ end
51
+
52
+ class UnknownExchangeError < BadRequestError
53
+ end
54
+
55
+ class ServiceUnavailableError < ResponseError
56
+ end
57
+ end
@@ -0,0 +1,21 @@
1
+ require 'net/http'
2
+ require 'uri'
3
+
4
+ require_relative 'version'
5
+
6
+ module CoinTools
7
+ module Request
8
+ USER_AGENT = "cointools/#{CoinTools::VERSION}"
9
+
10
+ def self.get(url)
11
+ url = URI(url) if url.is_a?(String)
12
+
13
+ Net::HTTP.start(url.host, url.port, use_ssl: true) do |http|
14
+ request = Net::HTTP::Get.new(url)
15
+ request['User-Agent'] = USER_AGENT
16
+
17
+ http.request(request)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,20 @@
1
+ require_relative 'errors'
2
+
3
+ require 'json'
4
+ require 'time'
5
+
6
+ module CoinTools
7
+ module Utils
8
+ class << self
9
+ def parse_json(text)
10
+ JSON.parse(text, quirks_mode: true)
11
+ end
12
+
13
+ def parse_time(text)
14
+ Time.parse(text)
15
+ rescue ArgumentError => e
16
+ raise InvalidDateError.new(e.message)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -1,3 +1,3 @@
1
1
  module CoinTools
2
- VERSION = "0.4.0"
2
+ VERSION = "0.5.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cointools
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kuba Suder
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-05-23 00:00:00.000000000 Z
11
+ date: 2018-07-12 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description:
14
14
  email:
@@ -27,9 +27,13 @@ files:
27
27
  - bin/coincap
28
28
  - bin/cryptowatch
29
29
  - lib/cointools.rb
30
+ - lib/cointools/base_struct.rb
30
31
  - lib/cointools/coincap.rb
31
32
  - lib/cointools/coinmarketcap.rb
32
33
  - lib/cointools/cryptowatch.rb
34
+ - lib/cointools/errors.rb
35
+ - lib/cointools/request.rb
36
+ - lib/cointools/utils.rb
33
37
  - lib/cointools/version.rb
34
38
  homepage: https://github.com/mackuba/cointools
35
39
  licenses:
@@ -51,7 +55,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
51
55
  version: '0'
52
56
  requirements: []
53
57
  rubyforge_project:
54
- rubygems_version: 2.5.2
58
+ rubygems_version: 2.5.2.3
55
59
  signing_key:
56
60
  specification_version: 4
57
61
  summary: A collection of scripts for checking cryptocurrency prices.