straight 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +1 -0
  3. data/Gemfile.lock +5 -1
  4. data/README.md +20 -10
  5. data/VERSION +1 -1
  6. data/lib/straight.rb +10 -1
  7. data/lib/straight/blockchain_adapter.rb +2 -13
  8. data/lib/straight/blockchain_adapters/biteasy_adapter.rb +74 -0
  9. data/lib/straight/blockchain_adapters/blockchain_info_adapter.rb +37 -17
  10. data/lib/straight/blockchain_adapters/mycelium_adapter.rb +144 -0
  11. data/lib/straight/exchange_rate_adapter.rb +20 -1
  12. data/lib/straight/exchange_rate_adapters/average_rate_adapter.rb +54 -0
  13. data/lib/straight/exchange_rate_adapters/bitpay_adapter.rb +5 -2
  14. data/lib/straight/exchange_rate_adapters/bitstamp_adapter.rb +2 -1
  15. data/lib/straight/exchange_rate_adapters/btce_adapter.rb +18 -0
  16. data/lib/straight/exchange_rate_adapters/coinbase_adapter.rb +2 -4
  17. data/lib/straight/exchange_rate_adapters/kraken_adapter.rb +18 -0
  18. data/lib/straight/exchange_rate_adapters/localbitcoins_adapter.rb +17 -0
  19. data/lib/straight/exchange_rate_adapters/okcoin_adapter.rb +18 -0
  20. data/lib/straight/gateway.rb +20 -9
  21. data/lib/straight/order.rb +46 -13
  22. data/spec/lib/blockchain_adapters/{helloblock_io_spec.rb → biteasy_adapter_spec.rb} +23 -18
  23. data/spec/lib/blockchain_adapters/{blockchain_info_spec.rb → blockchain_info_adapter_spec.rb} +8 -3
  24. data/spec/lib/blockchain_adapters/mycelium_adapter_spec.rb +54 -0
  25. data/spec/lib/exchange_rate_adapter_spec.rb +6 -1
  26. data/spec/lib/exchange_rate_adapters/average_rate_adapter_spec.rb +43 -0
  27. data/spec/lib/exchange_rate_adapters/bitpay_adapter_spec.rb +14 -1
  28. data/spec/lib/exchange_rate_adapters/bitstamp_adapter_spec.rb +14 -1
  29. data/spec/lib/exchange_rate_adapters/btce_adapter_spec.rb +27 -0
  30. data/spec/lib/exchange_rate_adapters/coinbase_adapter_spec.rb +14 -1
  31. data/spec/lib/exchange_rate_adapters/kraken_adapter_spec.rb +27 -0
  32. data/spec/lib/exchange_rate_adapters/localbitcoins_adapter_spec.rb +27 -0
  33. data/spec/lib/exchange_rate_adapters/okcoin_adapter_spec.rb +27 -0
  34. data/spec/lib/gateway_spec.rb +23 -5
  35. data/spec/lib/order_spec.rb +18 -2
  36. data/straight.gemspec +95 -0
  37. metadata +33 -6
  38. data/lib/straight/blockchain_adapters/helloblock_io_adapter.rb +0 -53
@@ -3,7 +3,8 @@ module Straight
3
3
 
4
4
  class Adapter
5
5
 
6
- class CurrencyNotFound < Exception; end
6
+ include Singleton
7
+
7
8
  class FetchingFailed < Exception; end
8
9
  class CurrencyNotSupported < Exception; end
9
10
 
@@ -39,6 +40,24 @@ module Straight
39
40
  nil # this should be changed in descendant classes
40
41
  end
41
42
 
43
+ # This method will get value we are interested in from hash and
44
+ # prevent failing with 'undefined method [] for Nil' if at some point hash doesn't have such key value pair
45
+ def get_rate_value_from_hash(rates_hash, *keys)
46
+ keys.inject(rates_hash) do |intermediate, key|
47
+ if intermediate.respond_to?(:[])
48
+ intermediate[key]
49
+ else
50
+ raise CurrencyNotSupported
51
+ end
52
+ end
53
+ end
54
+
55
+ # We dont want to have false positive rate, because nil.to_f is 0.0
56
+ # This method checks that rate value is not nil
57
+ def rate_to_f(rate)
58
+ rate ? rate.to_f : raise(CurrencyNotSupported)
59
+ end
60
+
42
61
  end
43
62
 
44
63
  end
@@ -0,0 +1,54 @@
1
+ module Straight
2
+ module ExchangeRate
3
+
4
+ class AverageRateAdapter < Adapter
5
+
6
+ # Takes exchange rate adapters instances or classes as arguments
7
+ def self.instance(*adapters)
8
+ instance = super()
9
+ instance.instance_variable_set(:@adapters, adapters.map { |adapter| adapter.respond_to?(:instance) ? adapter.instance : adapter })
10
+ instance
11
+ end
12
+
13
+ def fetch_rates!
14
+ failed_fetches = 0
15
+ @adapters.each do |adapter|
16
+ begin
17
+ adapter.fetch_rates!
18
+ rescue Exception => e
19
+ failed_fetches += 1
20
+ raise e if failed_fetches == @adapters.size
21
+ end
22
+ end
23
+ end
24
+
25
+ def rate_for(currency_code)
26
+ rates = []
27
+ @adapters.each do |adapter|
28
+ begin
29
+ rates << adapter.rate_for(currency_code)
30
+ rescue CurrencyNotSupported
31
+ rates << nil
32
+ end
33
+ end
34
+
35
+ unless rates.select(&:nil?).size == @adapters.size
36
+ rates.compact!
37
+ rates.inject {|sum, rate| sum + rate} / rates.size
38
+ else
39
+ raise CurrencyNotSupported
40
+ end
41
+ end
42
+
43
+ def get_rate_value_from_hash(rates_hash, *keys)
44
+ raise "This method is not supposed to be used in #{self.class}."
45
+ end
46
+
47
+ def rate_to_f(rate)
48
+ raise "This method is not supposed to be used in #{self.class}."
49
+ end
50
+
51
+ end
52
+
53
+ end
54
+ end
@@ -7,8 +7,11 @@ module Straight
7
7
 
8
8
  def rate_for(currency_code)
9
9
  super
10
- @rates.each do |r|
11
- return r['rate'].to_f if r['code'] == currency_code
10
+ @rates.each do |rt|
11
+ if rt['code'] == currency_code
12
+ rate = get_rate_value_from_hash(rt, 'rate')
13
+ return rate_to_f(rate)
14
+ end
12
15
  end
13
16
  raise CurrencyNotSupported
14
17
  end
@@ -8,7 +8,8 @@ module Straight
8
8
  def rate_for(currency_code)
9
9
  super
10
10
  raise CurrencyNotSupported if currency_code != 'USD'
11
- @rates['last'].to_f
11
+ rate = get_rate_value_from_hash(@rates, "last")
12
+ rate_to_f(rate)
12
13
  end
13
14
 
14
15
  end
@@ -0,0 +1,18 @@
1
+ module Straight
2
+ module ExchangeRate
3
+
4
+ class BtceAdapter < Adapter
5
+
6
+ FETCH_URL = 'https://btc-e.com/api/2/btc_usd/ticker'
7
+
8
+ def rate_for(currency_code)
9
+ super
10
+ raise CurrencyNotSupported if !FETCH_URL.include?("btc_#{currency_code.downcase}")
11
+ rate = get_rate_value_from_hash(@rates, 'ticker', 'last')
12
+ rate_to_f(rate)
13
+ end
14
+
15
+ end
16
+
17
+ end
18
+ end
@@ -7,10 +7,8 @@ module Straight
7
7
 
8
8
  def rate_for(currency_code)
9
9
  super
10
- if rate = @rates["btc_to_#{currency_code.downcase}"]
11
- return rate.to_f
12
- end
13
- raise CurrencyNotSupported
10
+ rate = get_rate_value_from_hash(@rates, "btc_to_#{currency_code.downcase}")
11
+ rate_to_f(rate)
14
12
  end
15
13
 
16
14
  end
@@ -0,0 +1,18 @@
1
+ module Straight
2
+ module ExchangeRate
3
+
4
+ class KrakenAdapter < Adapter
5
+
6
+ FETCH_URL = 'https://api.kraken.com/0/public/Ticker?pair=xbtusd'
7
+
8
+ def rate_for(currency_code)
9
+ super
10
+ rate = get_rate_value_from_hash(@rates, 'result', 'XXBTZ' + currency_code.upcase, 'c')
11
+ rate = rate.kind_of?(Array) ? rate.first : raise(CurrencyNotSupported)
12
+ rate_to_f(rate)
13
+ end
14
+
15
+ end
16
+
17
+ end
18
+ end
@@ -0,0 +1,17 @@
1
+ module Straight
2
+ module ExchangeRate
3
+
4
+ class LocalbitcoinsAdapter < Adapter
5
+
6
+ FETCH_URL = 'https://localbitcoins.com/bitcoinaverage/ticker-all-currencies/'
7
+
8
+ def rate_for(currency_code)
9
+ super
10
+ rate = get_rate_value_from_hash(@rates, currency_code.upcase, 'rates', 'last')
11
+ rate_to_f(rate)
12
+ end
13
+
14
+ end
15
+
16
+ end
17
+ end
@@ -0,0 +1,18 @@
1
+ module Straight
2
+ module ExchangeRate
3
+
4
+ class OkcoinAdapter < Adapter
5
+
6
+ FETCH_URL = 'https://www.okcoin.com/api/ticker.do?ok=1'
7
+
8
+ def rate_for(currency_code)
9
+ super
10
+ raise CurrencyNotSupported if currency_code != 'USD'
11
+ rate = get_rate_value_from_hash(@rates, 'ticker', 'last')
12
+ rate_to_f(rate)
13
+ end
14
+
15
+ end
16
+
17
+ end
18
+ end
@@ -43,9 +43,8 @@ module Straight
43
43
 
44
44
  # Determines the algorithm for consequitive checks of the order status.
45
45
  DEFAULT_STATUS_CHECK_SCHEDULE = -> (period, iteration_index) do
46
- return false if period > 640
47
46
  iteration_index += 1
48
- if iteration_index > 5
47
+ if iteration_index >= 20
49
48
  period *= 2
50
49
  iteration_index = 0
51
50
  end
@@ -56,7 +55,7 @@ module Straight
56
55
  # call super() somehwere inside those methods.
57
56
  #
58
57
  # In short, the idea is to let the class we're being prepended to do its magic
59
- # after out methods are finished.
58
+ # after our methods are finished.
60
59
  module Prependable
61
60
  end
62
61
 
@@ -85,7 +84,8 @@ module Straight
85
84
  # the one a user of this class is going to properly increment) that is used to generate a
86
85
  # an BIP32 bitcoin address deterministically.
87
86
  def address_for_keychain_id(id)
88
- keychain.node_for_path(id.to_s).to_address
87
+ # The 'm/0/n' notation is used by both Electrum and Mycelium
88
+ keychain.node_for_path("m/0/#{id.to_s}").to_address
89
89
  end
90
90
 
91
91
  def fetch_transaction(tid, address: nil)
@@ -101,7 +101,7 @@ module Straight
101
101
  end
102
102
 
103
103
  def keychain
104
- @keychain ||= MoneyTree::Node.from_serialized_address(@pubkey)
104
+ @keychain ||= MoneyTree::Node.from_serialized_address(pubkey)
105
105
  end
106
106
 
107
107
  # This is a callback method called from each order
@@ -130,6 +130,13 @@ module Straight
130
130
  end
131
131
  end
132
132
 
133
+ def current_exchange_rate(currency=self.default_currency)
134
+ currency = currency.to_s.upcase
135
+ try_adapters(@exchange_rate_adapters) do |a|
136
+ a.rate_for(currency)
137
+ end
138
+ end
139
+
133
140
  private
134
141
 
135
142
  # Calls the block with each adapter until one of them does not fail.
@@ -163,12 +170,16 @@ module Straight
163
170
  @default_currency = 'BTC'
164
171
  @blockchain_adapters = [
165
172
  Blockchain::BlockchainInfoAdapter.mainnet_adapter,
166
- Blockchain::HelloblockIoAdapter.mainnet_adapter
173
+ Blockchain::MyceliumAdapter.mainnet_adapter
167
174
  ]
168
175
  @exchange_rate_adapters = [
169
- ExchangeRate::BitpayAdapter.new,
170
- ExchangeRate::CoinbaseAdapter.new,
171
- ExchangeRate::BitstampAdapter.new
176
+ ExchangeRate::BitpayAdapter.instance,
177
+ ExchangeRate::CoinbaseAdapter.instance,
178
+ ExchangeRate::BitstampAdapter.instance,
179
+ ExchangeRate::BtceAdapter.instance,
180
+ ExchangeRate::KrakenAdapter.instance,
181
+ ExchangeRate::LocalbitcoinsAdapter.instance,
182
+ ExchangeRate::OkcoinAdapter.instance
172
183
  ]
173
184
  @status_check_schedule = DEFAULT_STATUS_CHECK_SCHEDULE
174
185
  end
@@ -44,6 +44,8 @@ module Straight
44
44
  expired: 5 # too much time passed since creating an order
45
45
  }
46
46
 
47
+ attr_reader :old_status
48
+
47
49
  class IncorrectAmount < Exception; end
48
50
 
49
51
  # If you are defining methods in this module, it means you most likely want to
@@ -61,7 +63,18 @@ module Straight
61
63
  # If as_sym is set to true, then each status is returned as Symbol, otherwise
62
64
  # an equivalent Integer from STATUSES is returned.
63
65
  def status(as_sym: false, reload: false)
64
- @status = super() if defined?(super)
66
+
67
+ if defined?(super)
68
+ begin
69
+ @status = super
70
+ # if no method with arguments found in the class
71
+ # we're prepending to, then let's use a standard getter
72
+ # with no argument.
73
+ rescue ArgumentError
74
+ @status = super()
75
+ end
76
+ end
77
+
65
78
  # Prohibit status update if the order was paid in some way.
66
79
  # This is just a caching workaround so we don't query
67
80
  # the blockchain needlessly. The actual safety switch is in the setter.
@@ -105,11 +118,16 @@ module Straight
105
118
  # The order in which these statements currently are prevents that error, because
106
119
  # by the time a callback checks the status it's already set.
107
120
  @status_changed = (@status != new_status)
121
+ @old_status = @status
108
122
  @status = new_status
109
- gateway.order_status_changed(self) if @status_changed
123
+ gateway.order_status_changed(self) if status_changed?
110
124
  super if defined?(super)
111
125
  end
112
126
 
127
+ def status_changed?
128
+ @status_changed
129
+ end
130
+
113
131
  end
114
132
 
115
133
  module Includable
@@ -144,22 +162,32 @@ module Straight
144
162
  # order.start_periodic_status_check
145
163
  # end
146
164
  #
147
- def start_periodic_status_check
148
- check_status_on_schedule
165
+ # `duration` argument (value is in seconds) allows you to
166
+ # control in what time an order expires. In other words, we
167
+ # keep checking for new transactions until the time passes.
168
+ # Then we stop and set Order's status to STATUS[:expired]. See
169
+ # #check_status_on_schedule for the implementation details.
170
+ def start_periodic_status_check(duration: 600)
171
+ check_status_on_schedule(duration: duration)
149
172
  end
150
173
 
151
174
  # Recursion here! Keeps calling itself according to the schedule until
152
175
  # either the status changes or the schedule tells it to stop.
153
- def check_status_on_schedule(period: 10, iteration_index: 0)
176
+ def check_status_on_schedule(period: 10, iteration_index: 0, duration: 600, time_passed: 0)
154
177
  self.status(reload: true)
155
- schedule = gateway.status_check_schedule.call(period, iteration_index)
156
- if schedule && self.status < 2 # Stop checking if status is >= 2
157
- sleep period
158
- check_status_on_schedule(
159
- period: schedule[:period],
160
- iteration_index: schedule[:iteration_index]
161
- )
162
- else
178
+ time_passed += period
179
+ if duration >= time_passed # Stop checking if status is >= 2
180
+ if self.status < 2
181
+ schedule = gateway.status_check_schedule.call(period, iteration_index)
182
+ sleep period
183
+ check_status_on_schedule(
184
+ period: schedule[:period],
185
+ iteration_index: schedule[:iteration_index],
186
+ duration: duration,
187
+ time_passed: time_passed
188
+ )
189
+ end
190
+ elsif self.status < 2
163
191
  self.status = STATUSES[:expired]
164
192
  end
165
193
  end
@@ -172,6 +200,11 @@ module Straight
172
200
  { status: status, amount: amount, address: address, tid: tid }
173
201
  end
174
202
 
203
+ def amount_in_btc(as: :number)
204
+ a = Satoshi.new(amount, from_unit: :satoshi, to_unit: :btc)
205
+ as == :string ? a.to_unit(as: :string) : a.to_unit
206
+ end
207
+
175
208
  end
176
209
 
177
210
  end
@@ -1,14 +1,8 @@
1
1
  require 'spec_helper'
2
2
 
3
- RSpec.describe Straight::Blockchain::HelloblockIoAdapter do
3
+ RSpec.describe Straight::Blockchain::BiteasyAdapter do
4
4
 
5
- subject(:adapter) { Straight::Blockchain::HelloblockIoAdapter.mainnet_adapter }
6
-
7
- it "fetches all transactions for the current address" do
8
- address = "3B1QZ8FpAaHBgkSB5gFt76ag5AW9VeP8xp"
9
- expect(adapter).to receive(:straighten_transaction).with(anything, address: address).at_least(:once)
10
- expect(adapter.fetch_transactions_for(address)).not_to be_empty
11
- end
5
+ subject(:adapter) { Straight::Blockchain::BiteasyAdapter.mainnet_adapter }
12
6
 
13
7
  it "fetches the balance for a given address" do
14
8
  address = "3B1QZ8FpAaHBgkSB5gFt76ag5AW9VeP8xp"
@@ -20,24 +14,35 @@ RSpec.describe Straight::Blockchain::HelloblockIoAdapter do
20
14
  expect(adapter.fetch_transaction(tid)[:total_amount]).to eq(832947)
21
15
  end
22
16
 
23
- it "gets a transaction id among other data" do
24
- tid = 'ae0d040f48d75fdc46d9035236a1782164857d6f0cca1f864640281115898560'
25
- expect(adapter.fetch_transaction(tid)[:tid]).to eq(tid)
17
+ it "calculates total_amount of a transaction for the given address only" do
18
+ t = { 'data' => {'outputs' => [{ 'value' => 1, 'to_address' => 'address1'}, { 'value' => 2, 'to_address' => 'address2'}] } }
19
+ expect(adapter.send(:straighten_transaction, t, address: 'address1')[:total_amount]).to eq(1)
20
+ end
21
+
22
+ it "fetches all transactions for the current address" do
23
+ address = "3B1QZ8FpAaHBgkSB5gFt76ag5AW9VeP8xp"
24
+ expect(adapter).to receive(:straighten_transaction).with(anything, address: address).at_least(:once)
25
+ expect(adapter.fetch_transactions_for(address)).not_to be_empty
26
26
  end
27
27
 
28
- it "returns the number of confirmations for a transaction" do
28
+ it "calculates the number of confirmations for each transaction" do
29
29
  tid = 'ae0d040f48d75fdc46d9035236a1782164857d6f0cca1f864640281115898560'
30
30
  expect(adapter.fetch_transaction(tid)[:confirmations]).to be > 0
31
31
  end
32
32
 
33
- it "raises an exception when something goes wrong with fetching datd" do
34
- allow_any_instance_of(URI).to receive(:read).and_raise(OpenURI::HTTPError)
35
- expect( -> { adapter.http_request("http://blockchain.info/a-timed-out-request") }).to raise_error(Straight::Blockchain::Adapter::RequestError)
33
+ it "gets a transaction id among other data" do
34
+ tid = 'ae0d040f48d75fdc46d9035236a1782164857d6f0cca1f864640281115898560'
35
+ expect(adapter.fetch_transaction(tid)[:tid]).to eq(tid)
36
+ end
37
+
38
+ it "raises an exception when something goes wrong with fetching data" do
39
+ expect( -> { adapter.send(:api_request, "/a-404-request") }).to raise_error(Straight::Blockchain::Adapter::RequestError)
36
40
  end
37
41
 
38
- it "calculates total_amount of a transaction for the given address only" do
39
- t = { 'outputs' => [{ 'value' => 1, 'address' => 'address1'}, { 'value' => 1, 'address' => 'address2'}] }
40
- expect(adapter.send(:straighten_transaction, t, address: 'address1')[:total_amount]).to eq(1)
42
+ it "uses the same Singleton instance" do
43
+ a = Straight::Blockchain::BiteasyAdapter.mainnet_adapter
44
+ b = Straight::Blockchain::BiteasyAdapter.mainnet_adapter
45
+ expect(a).to eq(b)
41
46
  end
42
47
 
43
48
  end
@@ -31,7 +31,7 @@ RSpec.describe Straight::Blockchain::BlockchainInfoAdapter do
31
31
  end
32
32
 
33
33
  it "caches blockchain.info latestblock requests" do
34
- expect(adapter).to receive(:http_request).once.and_return('{ "height": 1 }')
34
+ expect(adapter).to receive(:api_request).once.and_return('{ "height": 1 }')
35
35
  adapter.send(:calculate_confirmations, { "block_height" => 1 }, force_latest_block_reload: true)
36
36
  adapter.send(:calculate_confirmations, { "block_height" => 1 })
37
37
  adapter.send(:calculate_confirmations, { "block_height" => 1 })
@@ -40,8 +40,7 @@ RSpec.describe Straight::Blockchain::BlockchainInfoAdapter do
40
40
  end
41
41
 
42
42
  it "raises an exception when something goes wrong with fetching datd" do
43
- allow_any_instance_of(URI).to receive(:read).and_raise(OpenURI::HTTPError)
44
- expect( -> { adapter.http_request("http://blockchain.info/a-timed-out-request") }).to raise_error(Straight::Blockchain::Adapter::RequestError)
43
+ expect( -> { adapter.send(:api_request, "/a-404-request") }).to raise_error(Straight::Blockchain::Adapter::RequestError)
45
44
  end
46
45
 
47
46
  it "calculates total_amount of a transaction for the given address only" do
@@ -49,4 +48,10 @@ RSpec.describe Straight::Blockchain::BlockchainInfoAdapter do
49
48
  expect(adapter.send(:straighten_transaction, t, address: 'address1')[:total_amount]).to eq(1)
50
49
  end
51
50
 
51
+ it "uses the same Singleton instance" do
52
+ a = Straight::Blockchain::BlockchainInfoAdapter.mainnet_adapter
53
+ b = Straight::Blockchain::BlockchainInfoAdapter.mainnet_adapter
54
+ expect(a).to eq(b)
55
+ end
56
+
52
57
  end