currency-rate 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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