straight 0.1.0 → 0.2.0

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 (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