currency-rate 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/.document +5 -0
  3. data/.rspec +1 -0
  4. data/Gemfile +11 -0
  5. data/Gemfile.lock +91 -0
  6. data/LICENSE.txt +20 -0
  7. data/README.md +52 -0
  8. data/Rakefile +24 -0
  9. data/VERSION +1 -0
  10. data/currency-rate.gemspec +95 -0
  11. data/lib/adapter.rb +42 -0
  12. data/lib/btc_adapter.rb +17 -0
  13. data/lib/btc_adapters/average_rate_adapter.rb +50 -0
  14. data/lib/btc_adapters/bitpay_adapter.rb +18 -0
  15. data/lib/btc_adapters/bitstamp_adapter.rb +14 -0
  16. data/lib/btc_adapters/btce_adapter.rb +14 -0
  17. data/lib/btc_adapters/coinbase_adapter.rb +13 -0
  18. data/lib/btc_adapters/kraken_adapter.rb +14 -0
  19. data/lib/btc_adapters/localbitcoins_adapter.rb +13 -0
  20. data/lib/btc_adapters/okcoin_adapter.rb +14 -0
  21. data/lib/core_ext/classify.rb +7 -0
  22. data/lib/core_ext/deep_get.rb +11 -0
  23. data/lib/currency_rate.rb +49 -0
  24. data/lib/fiat_adapter.rb +30 -0
  25. data/lib/fiat_adapters/fixer_adapter.rb +11 -0
  26. data/lib/fiat_adapters/yahoo_adapter.rb +22 -0
  27. data/lib/storage.rb +17 -0
  28. data/spec/currency_rate_spec.rb +28 -0
  29. data/spec/lib/btc_adapter_spec.rb +55 -0
  30. data/spec/lib/btc_adapters/average_rate_adapter_spec.rb +51 -0
  31. data/spec/lib/btc_adapters/bitpay_adapter_spec.rb +35 -0
  32. data/spec/lib/btc_adapters/bitstamp_adapter_spec.rb +35 -0
  33. data/spec/lib/btc_adapters/btce_adapter_spec.rb +35 -0
  34. data/spec/lib/btc_adapters/coinbase_adapter_spec.rb +35 -0
  35. data/spec/lib/btc_adapters/kraken_adapter_spec.rb +35 -0
  36. data/spec/lib/btc_adapters/localbitcoins_adapter_spec.rb +35 -0
  37. data/spec/lib/btc_adapters/okcoin_adapter_spec.rb +35 -0
  38. data/spec/lib/fiat_adapters/fixer_adapter_spec.rb +22 -0
  39. data/spec/lib/fiat_adapters/yahoo_adapter_spec.rb +22 -0
  40. data/spec/lib/storage_spec.rb +25 -0
  41. data/spec/spec_helper.rb +13 -0
  42. metadata +169 -0
@@ -0,0 +1,14 @@
1
+ module CurrencyRate
2
+ class BitstampAdapter < BtcAdapter
3
+
4
+ FETCH_URL = 'https://www.bitstamp.net/api/ticker/'
5
+
6
+ def rate_for(currency_code)
7
+ super
8
+ raise CurrencyNotSupported if currency_code != 'USD'
9
+ rate = get_rate_value_from_hash(@rates, "last")
10
+ rate_to_f(rate)
11
+ end
12
+
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ module CurrencyRate
2
+ class BtceAdapter < BtcAdapter
3
+
4
+ FETCH_URL = 'https://btc-e.com/api/2/btc_usd/ticker'
5
+
6
+ def rate_for(currency_code)
7
+ super
8
+ raise CurrencyNotSupported if !FETCH_URL.include?("btc_#{currency_code.downcase}")
9
+ rate = get_rate_value_from_hash(@rates, 'ticker', 'last')
10
+ rate_to_f(rate)
11
+ end
12
+
13
+ end
14
+ end
@@ -0,0 +1,13 @@
1
+ module CurrencyRate
2
+ class CoinbaseAdapter < BtcAdapter
3
+
4
+ FETCH_URL = 'https://coinbase.com/api/v1/currencies/exchange_rates'
5
+
6
+ def rate_for(currency_code)
7
+ super
8
+ rate = get_rate_value_from_hash(@rates, "btc_to_#{currency_code.downcase}")
9
+ rate_to_f(rate)
10
+ end
11
+
12
+ end
13
+ end
@@ -0,0 +1,14 @@
1
+ module CurrencyRate
2
+ class KrakenAdapter < BtcAdapter
3
+
4
+ FETCH_URL = 'https://api.kraken.com/0/public/Ticker?pair=xbtusd'
5
+
6
+ def rate_for(currency_code)
7
+ super
8
+ rate = get_rate_value_from_hash(@rates, 'result', 'XXBTZ' + currency_code.upcase, 'c')
9
+ rate = rate.kind_of?(Array) ? rate.first : raise(CurrencyNotSupported)
10
+ rate_to_f(rate)
11
+ end
12
+
13
+ end
14
+ end
@@ -0,0 +1,13 @@
1
+ module CurrencyRate
2
+ class LocalbitcoinsAdapter < BtcAdapter
3
+
4
+ FETCH_URL = 'https://localbitcoins.com/bitcoinaverage/ticker-all-currencies/'
5
+
6
+ def rate_for(currency_code)
7
+ super
8
+ rate = get_rate_value_from_hash(@rates, currency_code.upcase, 'rates', 'last')
9
+ rate_to_f(rate)
10
+ end
11
+
12
+ end
13
+ end
@@ -0,0 +1,14 @@
1
+ module CurrencyRate
2
+ class OkcoinAdapter < BtcAdapter
3
+
4
+ FETCH_URL = 'https://www.okcoin.com/api/ticker.do?ok=1'
5
+
6
+ def rate_for(currency_code)
7
+ super
8
+ raise CurrencyNotSupported if currency_code != 'USD'
9
+ rate = get_rate_value_from_hash(@rates, 'ticker', 'last')
10
+ rate_to_f(rate)
11
+ end
12
+
13
+ end
14
+ end
@@ -0,0 +1,7 @@
1
+ class String
2
+
3
+ def classify(prefix="")
4
+ Kernel.const_get(prefix.to_s + "::" + self.split('_').collect(&:capitalize).join)
5
+ end
6
+
7
+ end
@@ -0,0 +1,11 @@
1
+ class Hash
2
+ def deep_get(*args)
3
+ args.inject(self) do |intermediate, key|
4
+ if intermediate.respond_to?(:[])
5
+ intermediate[key]
6
+ else
7
+ return nil
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,49 @@
1
+ require 'singleton'
2
+ require 'bigdecimal'
3
+ require 'satoshi-unit'
4
+ require 'open-uri'
5
+ require 'json'
6
+
7
+ require "adapter"
8
+ require "btc_adapter"
9
+ require "fiat_adapter"
10
+
11
+ Dir["#{File.expand_path File.dirname(__FILE__)}/**/*.rb"].each { |f| require f }
12
+
13
+ module CurrencyRate
14
+
15
+ def self.get(adapter_name, currency)
16
+ adapter_class(adapter_name).rate_for(currency)
17
+ end
18
+
19
+ def self.convert(adapter_name, amount:, from:, to:, anchor_currency: nil)
20
+ a = adapter_class(adapter_name)
21
+
22
+ # Setting default values for anchor currency depending on
23
+ # which adapter type is un use.
24
+ anchor_currency = if a.kind_of?(BtcAdapter)
25
+ 'BTC' if anchor_currency.nil?
26
+ else
27
+ 'USD' if anchor_currency.nil?
28
+ end
29
+
30
+ # None of the currencies is anchor currency?
31
+ # No problem, convert the amount given into the anchor currency first.
32
+ unless [to, from].include?(anchor_currency)
33
+ amount = convert(adapter_name, amount: amount, from: from, to: anchor_currency)
34
+ from = anchor_currency
35
+ end
36
+
37
+ rate = get(a, (from == anchor_currency) ? to : from)
38
+ result = from == anchor_currency ? amount.to_f*rate : amount.to_f/rate
39
+ to == 'BTC' ? result : result.round(2)
40
+ end
41
+
42
+ private
43
+
44
+ def self.adapter_class(s)
45
+ return s unless s.kind_of?(String) # if we pass class, no need to convert
46
+ adapter = "#{s}_adapter".classify(CurrencyRate).instance
47
+ end
48
+
49
+ end
@@ -0,0 +1,30 @@
1
+ module CurrencyRate
2
+
3
+ class FiatAdapter < Adapter
4
+
5
+ CROSS_RATE_CURRENCY = 'USD'
6
+ SUPPORTED_CURRENCIES = %w(
7
+ AUD BGN BRL CAD CHF CNY CZK DKK GBP HKD HRK HUF IDR ILS INR JPY
8
+ KRW MXN MYR NOK NZD PHP PLN RON RUB SEK SGD THB TRY USD ZAR EUR
9
+ )
10
+ DECIMAL_PRECISION = 2
11
+
12
+ # Set half-even rounding mode
13
+ # http://apidock.com/ruby/BigDecimal/mode/class
14
+ BigDecimal.mode BigDecimal::ROUND_MODE, :banker
15
+
16
+ def rate_for(currency_code)
17
+ return 1 if currency_code == CROSS_RATE_CURRENCY
18
+ raise CurrencyNotSupported unless SUPPORTED_CURRENCIES.include?(currency_code)
19
+ super
20
+ # call 'super' in descendant classes and return real rate
21
+ end
22
+
23
+ def convert_from_currency(amount_in_currency, currency: CROSS_RATE_CURRENCY)
24
+ return amount_in_currency if currency == CROSS_RATE_CURRENCY
25
+ amount_in_currency.to_f / rate_for(currency)
26
+ end
27
+
28
+ end
29
+
30
+ end
@@ -0,0 +1,11 @@
1
+ module CurrencyRate
2
+ class FixerAdapter < FiatAdapter
3
+ FETCH_URL = "http://api.fixer.io/latest?base=#{CROSS_RATE_CURRENCY}"
4
+
5
+ def rate_for(currency_code)
6
+ super
7
+ rate = get_rate_value_from_hash(@rates, 'rates', currency_code)
8
+ rate_to_f(rate)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,22 @@
1
+ require 'uri'
2
+
3
+ module CurrencyRate
4
+ class YahooAdapter < FiatAdapter
5
+ # Example URL (follow to un-shorten): http://goo.gl/62Aedt
6
+ FETCH_URL = "http://query.yahooapis.com/v1/public/yql?" + URI.encode_www_form(
7
+ format: 'json',
8
+ env: "store://datatables.org/alltableswithkeys",
9
+ q: "SELECT * FROM yahoo.finance.xchange WHERE pair IN" +
10
+ # The following line is building array string in SQL: '("USDJPY", "USDRUB", ...)'
11
+ "(#{SUPPORTED_CURRENCIES.map{|x| '"' + CROSS_RATE_CURRENCY.upcase + x.upcase + '"'}.join(',')})"
12
+ )
13
+
14
+ def rate_for(currency_code)
15
+ super
16
+ rates = @rates.deep_get('query', 'results', 'rate')
17
+ rate = rates && rates.find{|x| x['id'] == CROSS_RATE_CURRENCY + currency_code.upcase}
18
+ rate = rate && rate['Rate']
19
+ rate_to_f(rate)
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,17 @@
1
+ module CurrencyRate
2
+ class Storage
3
+
4
+ def initialize(timeout: 1800)
5
+ @timeout = timeout
6
+ @mem_storage = {}
7
+ end
8
+
9
+ def fetch(key)
10
+ if @mem_storage[key].nil? || (@mem_storage[key][:timestamp] < (Time.now.to_i - @timeout))
11
+ @mem_storage[key] = { content: yield, timestamp: Time.now.to_i }
12
+ end
13
+ @mem_storage[key][:content]
14
+ end
15
+
16
+ end
17
+ end
@@ -0,0 +1,28 @@
1
+ require 'spec_helper'
2
+
3
+ describe CurrencyRate do
4
+
5
+ before :each do
6
+ allow(CurrencyRate::BitstampAdapter.instance).to receive('rate_for').with('USD').and_return(550)
7
+ allow(CurrencyRate::BitstampAdapter.instance).to receive('rate_for').with('RUB').and_return(33100)
8
+ allow(CurrencyRate::YahooAdapter.instance).to receive('rate_for').with('RUB').and_return(65)
9
+ end
10
+
11
+ it "fetches currency rate from a specified exchange" do
12
+ expect(CurrencyRate.get('Bitstamp', 'USD')).to eq(550)
13
+ end
14
+
15
+ it "converts one currency into another" do
16
+ expect(CurrencyRate.convert('Bitstamp', amount: 5, from: 'BTC', to: 'USD')).to eq(2750)
17
+ expect(CurrencyRate.convert('Bitstamp', amount: 2750, from: 'USD', to: 'BTC')).to eq(5)
18
+ expect(CurrencyRate.convert('Yahoo', amount: 300, from: 'USD', to: 'RUB')).to eq(19500)
19
+ expect(CurrencyRate.convert('Yahoo', amount: 19500, from: 'RUB', to: 'USD')).to eq(300)
20
+ end
21
+
22
+ it "converts non-anchor currencies" do
23
+ expect(CurrencyRate.convert('Bitstamp', amount: 1000, from: 'USD', to: 'RUB')).to eq(60181.82)
24
+ expect(CurrencyRate.convert('Bitstamp', amount: 60181.81, from: 'RUB', to: 'USD')).to eq(1000)
25
+ end
26
+
27
+
28
+ end
@@ -0,0 +1,55 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe CurrencyRate::BtcAdapter do
4
+
5
+ class CurrencyRate::Adapter
6
+ FETCH_URL = ''
7
+ end
8
+
9
+ before(:each) do
10
+ @exchange_adapter = CurrencyRate::BtcAdapter.instance
11
+ end
12
+
13
+ describe "converting currencies" do
14
+
15
+ before(:each) do
16
+ allow(@exchange_adapter).to receive(:fetch_rates!)
17
+ allow(@exchange_adapter).to receive(:rate_for).with('USD').and_return(450.5412)
18
+ end
19
+
20
+ it "converts amount from currency into BTC" do
21
+ expect(@exchange_adapter.convert_from_currency(2252.706, currency: 'USD')).to eq(500000000)
22
+ end
23
+
24
+ it "converts from btc into currency" do
25
+ expect(@exchange_adapter.convert_to_currency(500000000, currency: 'USD')).to eq(2252.706)
26
+ end
27
+
28
+ it "shows btc amounts in various denominations" do
29
+ expect(@exchange_adapter.convert_from_currency(2252.706, currency: 'USD', btc_denomination: :btc)).to eq(5)
30
+ expect(@exchange_adapter.convert_to_currency(5, currency: 'USD', btc_denomination: :btc)).to eq(2252.706)
31
+ end
32
+
33
+ it "accepts string as amount and converts it properly" do
34
+ expect(@exchange_adapter.convert_from_currency('2252.706', currency: 'USD', btc_denomination: :btc)).to eq(5)
35
+ expect(@exchange_adapter.convert_to_currency('5', currency: 'USD', btc_denomination: :btc)).to eq(2252.706)
36
+ end
37
+
38
+ end
39
+
40
+ it "when checking for rates, only calls fetch_rates! if they were checked long time ago or never" do
41
+ uri_mock = double('uri mock')
42
+ expect(URI).to receive(:parse).and_return(uri_mock).once
43
+ expect(uri_mock).to receive(:read).and_return('{ "USD": 534.4343 }').once
44
+ @exchange_adapter.rate_for('USD')
45
+ @exchange_adapter.rate_for('USD') # not calling fetch_rates! because we've just checked
46
+ @exchange_adapter.instance_variable_set(:@rates_updated_at, Time.now-1900)
47
+ @exchange_adapter.rate_for('USD')
48
+ end
49
+
50
+ it "raises exception if rate is nil" do
51
+ rate = nil
52
+ expect( -> { @exchange_adapter.rate_to_f(rate) }).to raise_error(CurrencyRate::Adapter::CurrencyNotSupported)
53
+ end
54
+
55
+ end
@@ -0,0 +1,51 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe CurrencyRate::AverageRateAdapter do
4
+
5
+ before :all do
6
+ VCR.insert_cassette 'exchange_rate_adapters/btc_adapters/average_rate_adapter'
7
+ end
8
+
9
+ after :all do
10
+ VCR.eject_cassette
11
+ end
12
+
13
+ before(:each) do
14
+ @average_rates_adapter = CurrencyRate::AverageRateAdapter.instance(
15
+ CurrencyRate::BitstampAdapter,
16
+ CurrencyRate::BitpayAdapter.instance,
17
+ )
18
+ end
19
+
20
+ it "calculates average rate" do
21
+ json_response_bistamp = '{"high": "232.89", "last": "100", "timestamp": "1423457015", "bid": "224.00", "vwap": "224.57", "volume": "14810.41127494", "low": "217.28", "ask": "224.13"}'
22
+ json_response_bitpay = '[{"code":"USD","name":"US Dollar","rate":200},{"code":"EUR","name":"Eurozone Euro","rate":197.179544}]'
23
+ uri_mock = double('uri mock')
24
+ allow(uri_mock).to receive(:read).with(read_timeout: 4).and_return(json_response_bistamp, json_response_bitpay)
25
+ allow(URI).to receive(:parse).and_return(uri_mock)
26
+ expect(@average_rates_adapter.rate_for('USD')).to eq 150
27
+ end
28
+
29
+ it "fetches rates for all adapters" do
30
+ expect(@average_rates_adapter.fetch_rates!).not_to be_empty
31
+ end
32
+
33
+ it 'raises error if all adapters failed to fetch rates' do
34
+ adapter_mocks = [double('adapter_1'), double('adapter_2')]
35
+ adapter_mocks.each do |adapter|
36
+ expect(adapter).to receive(:fetch_rates!).and_raise(CurrencyRate::Adapter::FetchingFailed)
37
+ end
38
+ average_rates_adapter = CurrencyRate::AverageRateAdapter.instance(*adapter_mocks)
39
+ expect( -> { average_rates_adapter.fetch_rates! }).to raise_error(CurrencyRate::Adapter::FetchingFailed)
40
+ end
41
+
42
+ it "raises exception if all adapters fail to get rates" do
43
+ expect( -> { @average_rates_adapter.rate_for('FEDcoin') }).to raise_error(CurrencyRate::Adapter::CurrencyNotSupported)
44
+ end
45
+
46
+ it "raises exception if unallowed method is called" do # fetch_rates! is not to be used in AverageRateAdapter itself
47
+ expect( -> { @average_rates_adapter.get_rate_value_from_hash(nil, 'nothing') }).to raise_error("This method is not supposed to be used in #{@average_rates_adapter.class}.")
48
+ expect( -> { @average_rates_adapter.rate_to_f(nil) }).to raise_error("This method is not supposed to be used in #{@average_rates_adapter.class}.")
49
+ end
50
+
51
+ end
@@ -0,0 +1,35 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe CurrencyRate::BitpayAdapter do
4
+
5
+ before :all do
6
+ VCR.insert_cassette 'exchange_rate_adapters/btc_adapters/bitpay_adapter.yml'
7
+ end
8
+
9
+ after :all do
10
+ VCR.eject_cassette
11
+ end
12
+
13
+ before(:each) do
14
+ @exchange_adapter = CurrencyRate::BitpayAdapter.instance
15
+ end
16
+
17
+ it "finds the rate for currency code" do
18
+ expect(@exchange_adapter.rate_for('USD')).to be_kind_of(Float)
19
+ expect( -> { @exchange_adapter.rate_for('FEDcoin') }).to raise_error(CurrencyRate::Adapter::CurrencyNotSupported)
20
+ end
21
+
22
+ it "raises exception if rate is nil" do
23
+ json_response_1 = '[{},{}]'
24
+ json_response_2 = '[{"code":"USD","name":"US Dollar","rat":223.59},{"code":"EUR","name":"Eurozone Euro","rate":197.179544}]'
25
+ json_response_3 = '[{"code":"USD","name":"US Dollar","rate":null},{"code":"EUR","name":"Eurozone Euro","rate":197.179544}]'
26
+ uri_mock = double('uri mock')
27
+ allow(uri_mock).to receive(:read).with(read_timeout: 4).and_return(json_response_1, json_response_2, json_response_3)
28
+ allow(URI).to receive(:parse).and_return(uri_mock)
29
+ 3.times do
30
+ @exchange_adapter.fetch_rates!
31
+ expect( -> { @exchange_adapter.rate_for('USD') }).to raise_error(CurrencyRate::Adapter::CurrencyNotSupported)
32
+ end
33
+ end
34
+
35
+ end
@@ -0,0 +1,35 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe CurrencyRate::BitstampAdapter do
4
+
5
+ before :all do
6
+ VCR.insert_cassette 'exchange_rate_adapters/btc_adapters/bitstamp_adapter'
7
+ end
8
+
9
+ after :all do
10
+ VCR.eject_cassette
11
+ end
12
+
13
+ before(:each) do
14
+ @exchange_adapter = CurrencyRate::BitstampAdapter.instance
15
+ end
16
+
17
+ it "finds the rate for currency code" do
18
+ expect(@exchange_adapter.rate_for('USD')).to be_kind_of(Float)
19
+ expect( -> { @exchange_adapter.rate_for('FEDcoin') }).to raise_error(CurrencyRate::Adapter::CurrencyNotSupported)
20
+ end
21
+
22
+ it "raises exception if rate is nil" do
23
+ json_response_1 = '{}'
24
+ json_response_2 = '{"high": "232.89", "list": "224.13", "timestamp": "1423457015", "bid": "224.00", "vwap": "224.57", "volume": "14810.41127494", "low": "217.28", "ask": "224.13"}'
25
+ json_response_3 = '{"high": "232.89", "last": null, "timestamp": "1423457015", "bid": "224.00", "vwap": "224.57", "volume": "14810.41127494", "low": "217.28", "ask": "224.13"}'
26
+ uri_mock = double('uri mock')
27
+ allow(uri_mock).to receive(:read).with(read_timeout: 4).and_return(json_response_1, json_response_2, json_response_3)
28
+ allow(URI).to receive(:parse).and_return(uri_mock)
29
+ 3.times do
30
+ @exchange_adapter.fetch_rates!
31
+ expect( -> { @exchange_adapter.rate_for('USD') }).to raise_error(CurrencyRate::Adapter::CurrencyNotSupported)
32
+ end
33
+ end
34
+
35
+ end