straight 0.2.3 → 1.0.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/.travis.yml +5 -0
  3. data/Gemfile +7 -5
  4. data/Gemfile.lock +17 -7
  5. data/README.md +3 -0
  6. data/Rakefile +9 -0
  7. data/VERSION +1 -1
  8. data/lib/straight.rb +9 -1
  9. data/lib/straight/address_providers/base.rb +28 -0
  10. data/lib/straight/address_providers/bip32.rb +22 -0
  11. data/lib/straight/blockchain_adapter.rb +17 -1
  12. data/lib/straight/blockchain_adapters/biteasy_adapter.rb +14 -13
  13. data/lib/straight/blockchain_adapters/blockchain_info_adapter.rb +15 -14
  14. data/lib/straight/blockchain_adapters/insight_adapter.rb +76 -0
  15. data/lib/straight/blockchain_adapters/mycelium_adapter.rb +74 -49
  16. data/lib/straight/exchange_rate_adapter.rb +2 -2
  17. data/lib/straight/exchange_rate_adapters/average_rate_adapter.rb +1 -1
  18. data/lib/straight/exchange_rate_adapters/okcoin_adapter.rb +1 -1
  19. data/lib/straight/faraday_monkeypatch.rb +22 -0
  20. data/lib/straight/gateway.rb +71 -34
  21. data/lib/straight/order.rb +43 -32
  22. data/straight.gemspec +20 -27
  23. metadata +30 -26
  24. data/spec/lib/blockchain_adapters/biteasy_adapter_spec.rb +0 -48
  25. data/spec/lib/blockchain_adapters/blockchain_info_adapter_spec.rb +0 -57
  26. data/spec/lib/blockchain_adapters/mycelium_adapter_spec.rb +0 -58
  27. data/spec/lib/exchange_rate_adapter_spec.rb +0 -55
  28. data/spec/lib/exchange_rate_adapters/average_rate_adapter_spec.rb +0 -43
  29. data/spec/lib/exchange_rate_adapters/bitpay_adapter_spec.rb +0 -27
  30. data/spec/lib/exchange_rate_adapters/bitstamp_adapter_spec.rb +0 -27
  31. data/spec/lib/exchange_rate_adapters/btce_adapter_spec.rb +0 -27
  32. data/spec/lib/exchange_rate_adapters/coinbase_adapter_spec.rb +0 -27
  33. data/spec/lib/exchange_rate_adapters/kraken_adapter_spec.rb +0 -27
  34. data/spec/lib/exchange_rate_adapters/localbitcoins_adapter_spec.rb +0 -27
  35. data/spec/lib/exchange_rate_adapters/okcoin_adapter_spec.rb +0 -27
  36. data/spec/lib/gateway_spec.rb +0 -98
  37. data/spec/lib/order_spec.rb +0 -128
  38. data/spec/spec_helper.rb +0 -1
@@ -5,8 +5,8 @@ module Straight
5
5
 
6
6
  include Singleton
7
7
 
8
- class FetchingFailed < Exception; end
9
- class CurrencyNotSupported < Exception; end
8
+ class FetchingFailed < StraightError; end
9
+ class CurrencyNotSupported < StraightError; end
10
10
 
11
11
  def initialize(rates_expire_in: 1800)
12
12
  @rates_expire_in = rates_expire_in # in seconds
@@ -15,7 +15,7 @@ module Straight
15
15
  @adapters.each do |adapter|
16
16
  begin
17
17
  adapter.fetch_rates!
18
- rescue Exception => e
18
+ rescue => e
19
19
  failed_fetches += 1
20
20
  raise e if failed_fetches == @adapters.size
21
21
  end
@@ -15,4 +15,4 @@ module Straight
15
15
  end
16
16
 
17
17
  end
18
- end
18
+ end
@@ -0,0 +1,22 @@
1
+ Faraday::SSLOptions = Faraday::Options.new(*(Faraday::SSLOptions.members | [:verify_callback])) do
2
+
3
+ def verify?
4
+ verify != false
5
+ end
6
+
7
+ def disable?
8
+ !verify?
9
+ end
10
+ end
11
+
12
+ Faraday::ConnectionOptions.options(ssl: Faraday::SSLOptions)
13
+
14
+ Faraday::Adapter::NetHttp.class_exec do
15
+
16
+ alias_method :orig_configure_ssl, :configure_ssl
17
+
18
+ def configure_ssl(http, ssl)
19
+ http.verify_callback = ssl[:verify_callback] if ssl[:verify_callback]
20
+ orig_configure_ssl(http, ssl)
21
+ end
22
+ end
@@ -18,7 +18,8 @@ module Straight
18
18
  module GatewayModule
19
19
 
20
20
  # Raised when adapter's list (either Exchange or Blockchain adapters) is empty
21
- class NoAdaptersAvailable < Exception;end
21
+ class NoAdaptersAvailable < StraightError;end
22
+ class OrderAmountInvalid < StraightError;end
22
23
 
23
24
  # Only add getters and setters for those properties in the extended class
24
25
  # that don't already have them. This is very useful with ActiveRecord for example
@@ -27,6 +28,7 @@ module Straight
27
28
  base.class_eval do
28
29
  [
29
30
  :pubkey,
31
+ :test_pubkey,
30
32
  :confirmations_required,
31
33
  :status_check_schedule,
32
34
  :blockchain_adapters,
@@ -34,7 +36,11 @@ module Straight
34
36
  :order_callbacks,
35
37
  :order_class,
36
38
  :default_currency,
37
- :name
39
+ :name,
40
+ :address_provider,
41
+ :address_provider_type,
42
+ :address_derivation_scheme,
43
+ :test_mode
38
44
  ].each do |field|
39
45
  attr_reader field unless base.method_defined?(field)
40
46
  attr_writer field unless base.method_defined?("#{field}=")
@@ -64,52 +70,67 @@ module Straight
64
70
 
65
71
  module Includable
66
72
 
67
- # Creates a new order for the address derived from the pubkey and the keychain_id argument provided.
68
- # See explanation of this keychain_id argument is in the description for the #address_for_keychain_id method.
69
- def order_for_keychain_id(amount:, keychain_id:, currency: nil, btc_denomination: :satoshi)
73
+ def blockchain_adapters
74
+ return test_blockchain_adapters if test_mode
75
+ @blockchain_adapters
76
+ end
70
77
 
71
- amount = amount_from_exchange_rate(
72
- amount,
73
- currency: currency,
74
- btc_denomination: btc_denomination
78
+ def test_blockchain_adapters
79
+ @blockchain_adapters.map{ |el| el.class.testnet_adapter rescue next }.compact
80
+ end
81
+
82
+ # Creates a new order for the address derived from the pubkey and the keychain_id argument provided.
83
+ # See explanation of this keychain_id argument is in the description for the AddressProvider::Base#new_address method.
84
+ def new_order(args)
85
+
86
+ # Args: amount:, keychain_id: nil, currency: nil, btc_denomination: :satoshi
87
+ #
88
+ # The reason these arguments are supplied as a hash and not as named arguments
89
+ # is because we don't know in advance which arguments are required for a particular
90
+ # AddressAdapter. So we accpet all, check manually for required ones like :amount,
91
+ # set default values where needed and then hand them all to address_adapter.
92
+ if args[:amount].nil? || !args[:amount].kind_of?(Numeric) || args[:amount] <= 0
93
+ raise OrderAmountInvalid, "amount cannot be nil and should be more than 0"
94
+ end
95
+ # Setting default values
96
+ args[:currency] ||= default_currency
97
+ args[:btc_denomination] ||= :satoshi
98
+
99
+ amount = args[:amount_from_exchange_rate] = amount_from_exchange_rate(
100
+ args[:amount],
101
+ currency: args[:currency],
102
+ btc_denomination: args[:btc_denomination]
75
103
  )
76
104
 
105
+ if address_provider.takes_fees?
106
+ address, amount = address_provider.new_address_and_amount(**args)
107
+ else
108
+ address = address_provider.new_address(**args)
109
+ end
110
+
77
111
  order = Kernel.const_get(order_class).new
78
- order.amount = amount
79
112
  order.gateway = self
80
- order.address = address_for_keychain_id(keychain_id)
81
- order.keychain_id = keychain_id
113
+ order.keychain_id = args[:keychain_id]
114
+ order.address = address
115
+ order.amount = amount
82
116
  order
83
117
  end
84
118
 
85
- # Returns a Base58-encoded Bitcoin address to which the payment transaction
86
- # is expected to arrive. id is an an integer > 0 (hopefully not too large and hopefully
87
- # the one a user of this class is going to properly increment) that is used to generate a
88
- # an BIP32 bitcoin address deterministically.
89
- def address_for_keychain_id(id)
90
- # First check the depth. If the depth is 4 use '/i' notation (Mycelium iOS wallet)
91
- # TODO deal with other depths later. Currently only supports 0 and 4
92
- if keychain.depth > 0
93
- keychain.node_for_path(id.to_s).to_address
94
- else # Otherwise, use 'm/0/n' - both Electrum and Mycelium on Android
95
- keychain.node_for_path("m/0/#{id.to_s}").to_address
96
- end
97
- end
98
-
99
119
  def fetch_transaction(tid, address: nil)
100
- try_adapters(@blockchain_adapters, type: "blockchain") { |b| b.fetch_transaction(tid, address: address) }
120
+ try_adapters(blockchain_adapters, type: "blockchain") { |b| b.fetch_transaction(tid, address: address) }
101
121
  end
102
122
 
103
123
  def fetch_transactions_for(address)
104
- try_adapters(@blockchain_adapters, type: "blockchain") { |b| b.fetch_transactions_for(address) }
124
+ try_adapters(blockchain_adapters, type: "blockchain", raise_exceptions: [Blockchain::Adapter::BitcoinAddressInvalid]) { |b| b.fetch_transactions_for(address) }
105
125
  end
106
126
 
107
127
  def fetch_balance_for(address)
108
- try_adapters(@blockchain_adapters, type: "blockchain") { |b| b.fetch_balance_for(address) }
128
+ try_adapters(blockchain_adapters, type: "blockchain") { |b| b.fetch_balance_for(address) }
109
129
  end
110
130
 
111
131
  def keychain
112
- @keychain ||= MoneyTree::Node.from_bip32(pubkey)
132
+ key = self.test_mode ? self.test_pubkey : self.pubkey
133
+ @keychain ||= BTC::Keychain.new(xpub: key)
113
134
  end
114
135
 
115
136
  # This is a callback method called from each order
@@ -145,11 +166,23 @@ module Straight
145
166
  end
146
167
  end
147
168
 
169
+ def test_pubkey_missing?
170
+ address_provider_type == :Bip32 && test_mode && test_pubkey.to_s.empty?
171
+ end
172
+
173
+ def pubkey_missing?
174
+ address_provider_type == :Bip32 && !test_mode && pubkey.to_s.empty?
175
+ end
176
+
177
+ def address_provider_type
178
+ @address_provider ? @address_provider.class.name.split('::')[-1].to_sym : :Bip32
179
+ end
180
+
148
181
  private
149
-
182
+
150
183
  # Calls the block with each adapter until one of them does not fail.
151
184
  # Fails with the last exception.
152
- def try_adapters(adapters, type: nil, &block)
185
+ def try_adapters(adapters, type: nil, raise_exceptions: [], &block)
153
186
 
154
187
  # TODO: specify which adapters are unavailable (blockchain or exchange rate)
155
188
  raise NoAdaptersAvailable, "the list of #{type} adapters is empty or nil" if adapters.nil? || adapters.empty?
@@ -160,7 +193,8 @@ module Straight
160
193
  result = yield(adapter)
161
194
  last_exception = nil
162
195
  return result
163
- rescue Exception => e
196
+ rescue => e
197
+ raise e if raise_exceptions.include?(e)
164
198
  last_exception = e
165
199
  # If an Exception is raised, it passes on
166
200
  # to the next adapter and attempts to call a method on it.
@@ -182,7 +216,8 @@ module Straight
182
216
  @default_currency = 'BTC'
183
217
  @blockchain_adapters = [
184
218
  Blockchain::BlockchainInfoAdapter.mainnet_adapter,
185
- Blockchain::MyceliumAdapter.mainnet_adapter
219
+ Blockchain::MyceliumAdapter.mainnet_adapter,
220
+ Blockchain::InsightAdapter.mainnet_adapter(main_url: "https://insight.mycelium.com/api")
186
221
  ]
187
222
  @exchange_rate_adapters = [
188
223
  ExchangeRate::BitpayAdapter.instance,
@@ -194,6 +229,8 @@ module Straight
194
229
  ExchangeRate::OkcoinAdapter.instance
195
230
  ]
196
231
  @status_check_schedule = DEFAULT_STATUS_CHECK_SCHEDULE
232
+ @address_provider = AddressProvider::Bip32.new(self)
233
+ @test_mode = false
197
234
  end
198
235
 
199
236
  def order_class
@@ -22,7 +22,7 @@ module Straight
22
22
  # where we don't want to override AR getters and setters that set attribtues.
23
23
  def self.included(base)
24
24
  base.class_eval do
25
- [:amount, :address, :gateway, :keychain_id, :status, :tid].each do |field|
25
+ [:amount, :amount_paid, :address, :gateway, :keychain_id, :status, :tid, :title, :callback_url, :test_mode].each do |field|
26
26
  attr_reader field unless base.method_defined?(field)
27
27
  attr_writer field unless base.method_defined?("#{field}=")
28
28
  end
@@ -41,12 +41,13 @@ module Straight
41
41
  paid: 2, # transaction received with enough confirmations and the correct amount
42
42
  underpaid: 3, # amount that was received in a transaction was not enough
43
43
  overpaid: 4, # amount that was received in a transaction was too large
44
- expired: 5 # too much time passed since creating an order
44
+ expired: 5, # too much time passed since creating an order
45
+ canceled: 6, # user decides to economize
45
46
  }
46
47
 
47
48
  attr_reader :old_status
48
49
 
49
- class IncorrectAmount < Exception; end
50
+ class IncorrectAmount < StraightError; end
50
51
 
51
52
  # If you are defining methods in this module, it means you most likely want to
52
53
  # call super() somehwere inside those methods. An example would be the #status=
@@ -65,7 +66,7 @@ module Straight
65
66
  def status(as_sym: false, reload: false)
66
67
 
67
68
  if defined?(super)
68
- begin
69
+ begin
69
70
  @status = super
70
71
  # if no method with arguments found in the class
71
72
  # we're prepending to, then let's use a standard getter
@@ -78,43 +79,25 @@ module Straight
78
79
  # Prohibit status update if the order was paid in some way.
79
80
  # This is just a caching workaround so we don't query
80
81
  # the blockchain needlessly. The actual safety switch is in the setter.
81
- # Therefore, even if you remove the following line, status won't actually
82
- # be allowed to change.
83
- return @status if @status && @status > 1
84
-
85
- if reload || !@status
86
- t = transaction(reload: reload)
87
- self.status = if t.nil?
88
- STATUSES[:new]
89
- else
90
- if t[:confirmations] >= gateway.confirmations_required
91
- if t[:total_amount] == amount
92
- STATUSES[:paid]
93
- elsif t[:total_amount] < amount
94
- STATUSES[:underpaid]
95
- else
96
- STATUSES[:overpaid]
97
- end
98
- else
99
- STATUSES[:unconfirmed]
100
- end
101
- end
82
+ if (reload || @status.nil?) && !status_locked?
83
+ self.status = get_transaction_status(reload: reload)
102
84
  end
103
- as_sym ? STATUSES.invert[@status] : @status
85
+
86
+ as_sym ? STATUSES.invert[@status] : @status
104
87
  end
105
88
 
106
89
  def status=(new_status)
107
90
  # Prohibit status update if the order was paid in some way,
108
91
  # so statuses above 1 are in fact immutable.
109
- return false if @status && @status > 1
92
+ return false if status_locked?
110
93
 
111
94
  self.tid = transaction[:tid] if transaction
112
-
95
+
113
96
  # Pay special attention to the order of these statements. If you place
114
97
  # the assignment @status = new_status below the callback call,
115
98
  # you may get a "Stack level too deep" error if the callback checks
116
99
  # for the status and it's nil (therefore, force reload and the cycle continues).
117
- #
100
+ #
118
101
  # The order in which these statements currently are prevents that error, because
119
102
  # by the time a callback checks the status it's already set.
120
103
  @status_changed = (@status != new_status)
@@ -124,6 +107,34 @@ module Straight
124
107
  super if defined?(super)
125
108
  end
126
109
 
110
+ def set_amount_paid(transaction)
111
+ self.amount_paid = transaction[:total_amount]
112
+ end
113
+
114
+ def get_transaction_status(reload: false)
115
+ t = transaction(reload: reload)
116
+
117
+ return STATUSES[:new] if t.nil?
118
+ return STATUSES[:unconfirmed] if status_unconfirmed?(t[:confirmations])
119
+
120
+ set_amount_paid(t)
121
+ if t[:total_amount] == amount
122
+ STATUSES[:paid]
123
+ elsif t[:total_amount] < amount
124
+ STATUSES[:underpaid]
125
+ else
126
+ STATUSES[:overpaid]
127
+ end
128
+ end
129
+
130
+ def status_unconfirmed?(confirmations)
131
+ confirmations < gateway.confirmations_required
132
+ end
133
+
134
+ def status_locked?
135
+ @status && @status > 1
136
+ end
137
+
127
138
  def status_changed?
128
139
  @status_changed
129
140
  end
@@ -170,7 +181,7 @@ module Straight
170
181
  def start_periodic_status_check(duration: 600)
171
182
  check_status_on_schedule(duration: duration)
172
183
  end
173
-
184
+
174
185
  # Recursion here! Keeps calling itself according to the schedule until
175
186
  # either the status changes or the schedule tells it to stop.
176
187
  def check_status_on_schedule(period: 10, iteration_index: 0, duration: 600, time_passed: 0)
@@ -200,8 +211,8 @@ module Straight
200
211
  { status: status, amount: amount, address: address, tid: tid }
201
212
  end
202
213
 
203
- def amount_in_btc(as: :number)
204
- a = Satoshi.new(amount, from_unit: :satoshi, to_unit: :btc)
214
+ def amount_in_btc(field: amount, as: :number)
215
+ a = Satoshi.new(field, from_unit: :satoshi, to_unit: :btc)
205
216
  as == :string ? a.to_unit(as: :string) : a.to_unit
206
217
  end
207
218
 
data/straight.gemspec CHANGED
@@ -2,16 +2,16 @@
2
2
  # DO NOT EDIT THIS FILE DIRECTLY
3
3
  # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
4
  # -*- encoding: utf-8 -*-
5
- # stub: straight 0.2.3 ruby lib
5
+ # stub: straight 1.0.0 ruby lib
6
6
 
7
7
  Gem::Specification.new do |s|
8
8
  s.name = "straight"
9
- s.version = "0.2.3"
9
+ s.version = "1.0.0"
10
10
 
11
11
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
12
12
  s.require_paths = ["lib"]
13
13
  s.authors = ["Roman Snitko"]
14
- s.date = "2015-05-30"
14
+ s.date = "2015-07-31"
15
15
  s.description = "An engine for the Straight payment gateway software. Requires no state to be saved (that is, no storage or DB). Its responsibilities only include processing data coming from an actual gateway."
16
16
  s.email = "roman.snitko@gmail.com"
17
17
  s.extra_rdoc_files = [
@@ -21,6 +21,7 @@ Gem::Specification.new do |s|
21
21
  s.files = [
22
22
  ".document",
23
23
  ".rspec",
24
+ ".travis.yml",
24
25
  "Gemfile",
25
26
  "Gemfile.lock",
26
27
  "LICENSE.txt",
@@ -28,9 +29,12 @@ Gem::Specification.new do |s|
28
29
  "Rakefile",
29
30
  "VERSION",
30
31
  "lib/straight.rb",
32
+ "lib/straight/address_providers/base.rb",
33
+ "lib/straight/address_providers/bip32.rb",
31
34
  "lib/straight/blockchain_adapter.rb",
32
35
  "lib/straight/blockchain_adapters/biteasy_adapter.rb",
33
36
  "lib/straight/blockchain_adapters/blockchain_info_adapter.rb",
37
+ "lib/straight/blockchain_adapters/insight_adapter.rb",
34
38
  "lib/straight/blockchain_adapters/mycelium_adapter.rb",
35
39
  "lib/straight/exchange_rate_adapter.rb",
36
40
  "lib/straight/exchange_rate_adapters/average_rate_adapter.rb",
@@ -41,23 +45,9 @@ Gem::Specification.new do |s|
41
45
  "lib/straight/exchange_rate_adapters/kraken_adapter.rb",
42
46
  "lib/straight/exchange_rate_adapters/localbitcoins_adapter.rb",
43
47
  "lib/straight/exchange_rate_adapters/okcoin_adapter.rb",
48
+ "lib/straight/faraday_monkeypatch.rb",
44
49
  "lib/straight/gateway.rb",
45
50
  "lib/straight/order.rb",
46
- "spec/lib/blockchain_adapters/biteasy_adapter_spec.rb",
47
- "spec/lib/blockchain_adapters/blockchain_info_adapter_spec.rb",
48
- "spec/lib/blockchain_adapters/mycelium_adapter_spec.rb",
49
- "spec/lib/exchange_rate_adapter_spec.rb",
50
- "spec/lib/exchange_rate_adapters/average_rate_adapter_spec.rb",
51
- "spec/lib/exchange_rate_adapters/bitpay_adapter_spec.rb",
52
- "spec/lib/exchange_rate_adapters/bitstamp_adapter_spec.rb",
53
- "spec/lib/exchange_rate_adapters/btce_adapter_spec.rb",
54
- "spec/lib/exchange_rate_adapters/coinbase_adapter_spec.rb",
55
- "spec/lib/exchange_rate_adapters/kraken_adapter_spec.rb",
56
- "spec/lib/exchange_rate_adapters/localbitcoins_adapter_spec.rb",
57
- "spec/lib/exchange_rate_adapters/okcoin_adapter_spec.rb",
58
- "spec/lib/gateway_spec.rb",
59
- "spec/lib/order_spec.rb",
60
- "spec/spec_helper.rb",
61
51
  "straight.gemspec"
62
52
  ]
63
53
  s.homepage = "http://github.com/snitko/straight"
@@ -69,24 +59,27 @@ Gem::Specification.new do |s|
69
59
  s.specification_version = 4
70
60
 
71
61
  if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
72
- s.add_runtime_dependency(%q<money-tree>, ["= 0.9.0"])
73
- s.add_runtime_dependency(%q<satoshi-unit>, [">= 0"])
74
- s.add_runtime_dependency(%q<httparty>, [">= 0"])
62
+ s.add_runtime_dependency(%q<btcruby>, ["~> 1.0"])
63
+ s.add_runtime_dependency(%q<satoshi-unit>, ["~> 0.1"])
64
+ s.add_runtime_dependency(%q<httparty>, ["~> 0.13.5"])
65
+ s.add_runtime_dependency(%q<faraday>, [">= 0"])
75
66
  s.add_development_dependency(%q<bundler>, ["~> 1.0"])
76
67
  s.add_development_dependency(%q<jeweler>, ["~> 2.0.1"])
77
68
  s.add_development_dependency(%q<github_api>, ["= 0.11.3"])
78
69
  else
79
- s.add_dependency(%q<money-tree>, ["= 0.9.0"])
80
- s.add_dependency(%q<satoshi-unit>, [">= 0"])
81
- s.add_dependency(%q<httparty>, [">= 0"])
70
+ s.add_dependency(%q<btcruby>, ["~> 1.0"])
71
+ s.add_dependency(%q<satoshi-unit>, ["~> 0.1"])
72
+ s.add_dependency(%q<httparty>, ["~> 0.13.5"])
73
+ s.add_dependency(%q<faraday>, [">= 0"])
82
74
  s.add_dependency(%q<bundler>, ["~> 1.0"])
83
75
  s.add_dependency(%q<jeweler>, ["~> 2.0.1"])
84
76
  s.add_dependency(%q<github_api>, ["= 0.11.3"])
85
77
  end
86
78
  else
87
- s.add_dependency(%q<money-tree>, ["= 0.9.0"])
88
- s.add_dependency(%q<satoshi-unit>, [">= 0"])
89
- s.add_dependency(%q<httparty>, [">= 0"])
79
+ s.add_dependency(%q<btcruby>, ["~> 1.0"])
80
+ s.add_dependency(%q<satoshi-unit>, ["~> 0.1"])
81
+ s.add_dependency(%q<httparty>, ["~> 0.13.5"])
82
+ s.add_dependency(%q<faraday>, [">= 0"])
90
83
  s.add_dependency(%q<bundler>, ["~> 1.0"])
91
84
  s.add_dependency(%q<jeweler>, ["~> 2.0.1"])
92
85
  s.add_dependency(%q<github_api>, ["= 0.11.3"])