mxvp-coinbase 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.
data/Rakefile ADDED
@@ -0,0 +1,7 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec) do
5
+ end
6
+
7
+ task default: :spec
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "coinbase/exchange"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
data/bin/setup ADDED
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+
7
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,29 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'coinbase/exchange/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "mxvp-coinbase"
8
+ spec.version = Coinbase::Exchange::VERSION
9
+ spec.authors = ["John Duhamel", "James Mock"]
10
+ spec.email = ["james.willard.mock@gmail.com"]
11
+
12
+ spec.summary = "Client library for Coinbase Exchange"
13
+ spec.homepage = "https://github.com/jwmock/coinbase-exchange-ruby"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
17
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
18
+ spec.require_paths = ["lib"]
19
+
20
+ spec.add_dependency "bigdecimal"
21
+ spec.add_dependency "faye-websocket"
22
+ spec.add_dependency "em-http-request"
23
+
24
+ spec.add_development_dependency "bundler", "~> 1.8"
25
+ spec.add_development_dependency "rake", "~> 10.0"
26
+ spec.add_development_dependency "rspec"
27
+ spec.add_development_dependency "webmock"
28
+ spec.add_development_dependency "pry"
29
+ end
@@ -0,0 +1,83 @@
1
+ module Coinbase
2
+ module Exchange
3
+ # EM-Http Adapter
4
+ class EMHTTPClient < APIClient
5
+ def initialize(api_key = '', api_secret = '', api_pass = '', options = {})
6
+ super(api_key, api_secret, api_pass, options)
7
+ end
8
+
9
+ private
10
+
11
+ def http_verb(method, path, body = nil)
12
+ if !EventMachine.reactor_running?
13
+ EM.run do
14
+ # FIXME: This doesn't work with paginated endpoints
15
+ http_verb(method, path, body) do |resp|
16
+ yield(resp)
17
+ EM.stop
18
+ end
19
+ end
20
+ else
21
+ req_ts = Time.now.utc.to_i.to_s
22
+ signature = Base64.encode64(
23
+ OpenSSL::HMAC.digest('sha256', Base64.decode64(@api_secret).strip,
24
+ "#{req_ts}#{method}#{path}#{body}")).strip
25
+ headers = {}
26
+ headers['Content-Type'] = 'application/json'
27
+ headers['CB-ACCESS-TIMESTAMP'] = req_ts
28
+ headers['CB-ACCESS-PASSPHRASE'] = @api_pass
29
+ headers['CB-ACCESS-KEY'] = @api_key
30
+ headers['CB-ACCESS-SIGN'] = signature
31
+
32
+ # NOTE: This is documented but not implemented in em-http-request
33
+ # https://github.com/igrigorik/em-http-request/issues/182
34
+ # https://github.com/igrigorik/em-http-request/pull/179
35
+ ssl_opts = { cert_chain_file: File.expand_path(File.join(File.dirname(__FILE__), 'ca-coinbase.crt')),
36
+ verify_peer: true }
37
+
38
+ case method
39
+ when 'GET'
40
+ req = EM::HttpRequest.new(@api_uri).get(path: path, head: headers, body: body, ssl: ssl_opts)
41
+ when 'POST'
42
+ req = EM::HttpRequest.new(@api_uri).post(path: path, head: headers, body: body, ssl: ssl_opts)
43
+ when 'DELETE'
44
+ req = EM::HttpRequest.new(@api_uri).delete(path: path, head: headers, ssl: ssl_opts)
45
+ else fail
46
+ end
47
+ req.callback do |resp|
48
+ case resp.response_header.status
49
+ when 200 then yield(EMHTTPResponse.new(resp))
50
+ when 400 then fail BadRequestError, resp.response
51
+ when 401 then fail NotAuthorizedError, resp.response
52
+ when 403 then fail ForbiddenError, resp.response
53
+ when 404 then fail NotFoundError, resp.response
54
+ when 429 then fail RateLimitError, resp.response
55
+ when 500 then fail InternalServerError, resp.response
56
+ end
57
+ end
58
+ req.errback do |resp|
59
+ fail APIError, "#{method} #{@api_uri}#{path}: #{resp.error}"
60
+ end
61
+ end
62
+ end
63
+ end
64
+
65
+ # EM-Http response object
66
+ class EMHTTPResponse < APIResponse
67
+ def body
68
+ @response.response
69
+ end
70
+
71
+ def headers
72
+ out = @response.response_header.map do |key, val|
73
+ [ key.upcase.gsub('_', '-'), val ]
74
+ end
75
+ out.to_h
76
+ end
77
+
78
+ def status
79
+ @response.response_header.status
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,67 @@
1
+ module Coinbase
2
+ module Exchange
3
+ # Net-HTTP adapter
4
+ class NetHTTPClient < APIClient
5
+ def initialize(api_key = '', api_secret = '', api_pass = '', options = {})
6
+ super(api_key, api_secret, api_pass, options)
7
+ @conn = Net::HTTP.new(@api_uri.host, @api_uri.port)
8
+ @conn.use_ssl = true if @api_uri.scheme == 'https'
9
+ @conn.cert_store = self.class.whitelisted_certificates
10
+ @conn.ssl_version = :TLSv1
11
+ end
12
+
13
+ private
14
+
15
+ def http_verb(method, path, body = nil)
16
+ case method
17
+ when 'GET' then req = Net::HTTP::Get.new(path)
18
+ when 'POST' then req = Net::HTTP::Post.new(path)
19
+ when 'DELETE' then req = Net::HTTP::Delete.new(path)
20
+ else fail
21
+ end
22
+
23
+ req.body = body
24
+
25
+ req_ts = Time.now.utc.to_i.to_s
26
+ signature = Base64.encode64(
27
+ OpenSSL::HMAC.digest('sha256', Base64.decode64(@api_secret).strip,
28
+ "#{req_ts}#{method}#{path}#{body}")).strip
29
+ req['Content-Type'] = 'application/json'
30
+ req['CB-ACCESS-TIMESTAMP'] = req_ts
31
+ req['CB-ACCESS-PASSPHRASE'] = @api_pass
32
+ req['CB-ACCESS-KEY'] = @api_key
33
+ req['CB-ACCESS-SIGN'] = signature
34
+
35
+ resp = @conn.request(req)
36
+ case resp.code
37
+ when "200" then yield(NetHTTPResponse.new(resp))
38
+ when "400" then fail BadRequestError, resp.body
39
+ when "401" then fail NotAuthorizedError, resp.body
40
+ when "403" then fail ForbiddenError, resp.body
41
+ when "404" then fail NotFoundError, resp.body
42
+ when "429" then fail RateLimitError, resp.body
43
+ when "500" then fail InternalServerError, resp.body
44
+ end
45
+ resp.body
46
+ end
47
+ end
48
+
49
+ # Net-Http response object
50
+ class NetHTTPResponse < APIResponse
51
+ def body
52
+ @response.body
53
+ end
54
+
55
+ def headers
56
+ out = @response.to_hash.map do |key, val|
57
+ [ key.upcase.gsub('_', '-'), val.count == 1 ? val.first : val ]
58
+ end
59
+ out.to_h
60
+ end
61
+
62
+ def status
63
+ @response.code.to_i
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,359 @@
1
+ module Coinbase
2
+ module Exchange
3
+ # Net-http client for Coinbase Exchange API
4
+ class APIClient
5
+ def initialize(api_key = '', api_secret = '', api_pass = '', options = {})
6
+ @api_uri = URI.parse(options[:api_url] || "https://api.gdax.com")
7
+ @api_pass = api_pass
8
+ @api_key = api_key
9
+ @api_secret = api_secret
10
+ @default_product = options[:product_id] || "BTC-USD"
11
+ end
12
+
13
+ def server_epoch(params = {})
14
+ get("/time", params) do |resp|
15
+ yield(resp) if block_given?
16
+ end
17
+ end
18
+
19
+ #
20
+ # Market Data
21
+ #
22
+ def currencies(params = {})
23
+ out = nil
24
+ get("/currencies", params) do |resp|
25
+ out = response_collection(resp)
26
+ yield(out, resp) if block_given?
27
+ end
28
+ out
29
+ end
30
+
31
+ def products(params = {})
32
+ out = nil
33
+ get("/products", params) do |resp|
34
+ out = response_collection(resp)
35
+ yield(out, resp) if block_given?
36
+ end
37
+ out
38
+ end
39
+
40
+ def orderbook(params = {})
41
+ product = params[:product_id] || @default_product
42
+
43
+ out = nil
44
+ get("/products/#{product}/book", params) do |resp|
45
+ out = response_object(resp)
46
+ yield(out, resp) if block_given?
47
+ end
48
+ out
49
+ end
50
+
51
+ def last_trade(params = {})
52
+ product = params[:product_id] || @default_product
53
+
54
+ out = nil
55
+ get("/products/#{product}/ticker", params) do |resp|
56
+ out = response_object(resp)
57
+ yield(out, resp) if block_given?
58
+ end
59
+ out
60
+ end
61
+
62
+ def trade_history(params = {})
63
+ product = params[:product_id] || @default_product
64
+
65
+ out = nil
66
+ get("/products/#{product}/trades", params, paginate: true) do |resp|
67
+ out = response_collection(resp)
68
+ yield(out, resp) if block_given?
69
+ end
70
+ out
71
+ end
72
+
73
+ def price_history(params = {})
74
+ product = params[:product_id] || @default_product
75
+
76
+ out = nil
77
+ get("/products/#{product}/candles", params) do |resp|
78
+ out = response_collection(
79
+ resp.map do |item|
80
+ { 'start' => Time.at(item[0]),
81
+ 'low' => item[1],
82
+ 'high' => item[2],
83
+ 'open' => item[3],
84
+ 'close' => item[4],
85
+ 'volume' => item[5]
86
+ }
87
+ end
88
+ )
89
+ yield(out, resp) if block_given?
90
+ end
91
+ out
92
+ end
93
+
94
+ def daily_stats(params = {})
95
+ product = params[:product_id] || @default_product
96
+
97
+ out = nil
98
+ get("/products/#{product}/stats", params) do |resp|
99
+ resp["start"] = (Time.now - 24 * 60 * 60).to_s
100
+ out = response_object(resp)
101
+ yield(out, resp) if block_given?
102
+ end
103
+ out
104
+ end
105
+
106
+ #
107
+ # Accounts
108
+ #
109
+ def accounts(params = {})
110
+ out = nil
111
+ get("/accounts", params) do |resp|
112
+ out = response_collection(resp)
113
+ yield(out, resp) if block_given?
114
+ end
115
+ out
116
+ end
117
+
118
+ def account(id, params = {})
119
+ out = nil
120
+ get("/accounts/#{id}", params) do |resp|
121
+ out = response_object(resp)
122
+ yield(out, resp) if block_given?
123
+ end
124
+ out
125
+ end
126
+
127
+ def account_history(id, params = {})
128
+ out = nil
129
+ get("/accounts/#{id}/ledger", params, paginate: true) do |resp|
130
+ out = response_collection(resp)
131
+ yield(out, resp) if block_given?
132
+ end
133
+ out
134
+ end
135
+
136
+ def account_holds(id, params = {})
137
+ out = nil
138
+ get("/accounts/#{id}/holds", params, paginate: true) do |resp|
139
+ out = response_collection(resp)
140
+ yield(out, resp) if block_given?
141
+ end
142
+ out
143
+ end
144
+
145
+ #
146
+ # Orders
147
+ #
148
+ def bid(amt, price, params = {})
149
+ params[:product_id] ||= @default_product
150
+ params[:size] = amt
151
+ params[:price] = price
152
+ params[:side] = "buy"
153
+
154
+ out = nil
155
+ post("/orders", params) do |resp|
156
+ out = response_object(resp)
157
+ yield(out, resp) if block_given?
158
+ end
159
+ out
160
+ end
161
+ alias_method :buy, :bid
162
+
163
+ def ask(amt, price, params = {})
164
+ params[:product_id] ||= @default_product
165
+ params[:size] = amt
166
+ params[:price] = price
167
+ params[:side] = "sell"
168
+
169
+ out = nil
170
+ post("/orders", params) do |resp|
171
+ out = response_object(resp)
172
+ yield(out, resp) if block_given?
173
+ end
174
+ out
175
+ end
176
+ alias_method :sell, :ask
177
+
178
+ def cancel(id)
179
+ out = nil
180
+ delete("/orders/#{id}") do |resp|
181
+ out = response_object(resp)
182
+ yield(out, resp) if block_given?
183
+ end
184
+ out
185
+ end
186
+
187
+ def orders(params = {})
188
+ params[:status] ||= "all"
189
+
190
+ out = nil
191
+ get("/orders", params, paginate: true) do |resp|
192
+ out = response_collection(resp)
193
+ yield(out, resp) if block_given?
194
+ end
195
+ out
196
+ end
197
+
198
+ def order(id, params = {})
199
+ out = nil
200
+ get("/orders/#{id}", params) do |resp|
201
+ out = response_object(resp)
202
+ yield(out, resp) if block_given?
203
+ end
204
+ out
205
+ end
206
+
207
+ def fills(params = {})
208
+ out = nil
209
+ get("/fills", params, paginate: true) do |resp|
210
+ out = response_collection(resp)
211
+ yield(out, resp) if block_given?
212
+ end
213
+ out
214
+ end
215
+
216
+ #
217
+ # Transfers
218
+ #
219
+ def deposit(account_id, amt, params = {})
220
+ params[:type] = "deposit"
221
+ params[:coinbase_account_id] = account_id
222
+ params[:amount] = amt
223
+
224
+ out = nil
225
+ post("/transfers", params) do |resp|
226
+ out = response_object(resp)
227
+ yield(out, resp) if block_given?
228
+ end
229
+ out
230
+ end
231
+
232
+ def withdraw(account_id, amt, params = {})
233
+ params[:type] = "withdraw"
234
+ params[:coinbase_account_id] = account_id
235
+ params[:amount] = amt
236
+
237
+ out = nil
238
+ post("/transfers", params) do |resp|
239
+ out = response_object(resp)
240
+ yield(out, resp) if block_given?
241
+ end
242
+ out
243
+ end
244
+
245
+ private
246
+
247
+ def response_collection(resp)
248
+ out = resp.map { |item| APIObject.new(item) }
249
+ out.instance_eval { @response = resp }
250
+ add_metadata(out)
251
+ out
252
+ end
253
+
254
+ def response_object(resp)
255
+ out = APIObject.new(resp)
256
+ out.instance_eval { @response = resp.response }
257
+ add_metadata(out)
258
+ out
259
+ end
260
+
261
+ def add_metadata(resp)
262
+ resp.instance_eval do
263
+ def response
264
+ @response
265
+ end
266
+
267
+ def raw
268
+ @response.raw
269
+ end
270
+
271
+ def response_headers
272
+ @response.headers
273
+ end
274
+
275
+ def response_status
276
+ @response.status
277
+ end
278
+ end
279
+ resp
280
+ end
281
+
282
+ def get(path, params = {}, options = {})
283
+ params[:limit] ||= 100 if options[:paginate] == true
284
+
285
+ http_verb('GET', "#{path}?#{URI.encode_www_form(params)}") do |resp|
286
+ begin
287
+ out = JSON.parse(resp.body)
288
+ rescue JSON::ParserError
289
+ out = resp.body
290
+ end
291
+ out.instance_eval { @response = resp }
292
+ add_metadata(out)
293
+
294
+ if options[:paginate] && out.count == params[:limit]
295
+ params[:after] = resp.headers['CB-AFTER']
296
+ get(path, params, options) do |pages|
297
+ out += pages
298
+ add_metadata(out)
299
+ yield(out)
300
+ end
301
+ else
302
+ yield(out)
303
+ end
304
+ end
305
+ end
306
+
307
+ def post(path, params = {})
308
+ http_verb('POST', path, params.to_json) do |resp|
309
+ begin
310
+ out = JSON.parse(resp.body)
311
+ rescue JSON::ParserError
312
+ out = resp.body
313
+ end
314
+ out.instance_eval { @response = resp }
315
+ add_metadata(out)
316
+ yield(out)
317
+ end
318
+ end
319
+
320
+ def delete(path)
321
+ http_verb('DELETE', path) do |resp|
322
+ begin
323
+ out = JSON.parse(resp.body)
324
+ rescue
325
+ out = resp.body
326
+ end
327
+ out.instance_eval { @response = resp }
328
+ add_metadata(out)
329
+ yield(out)
330
+ end
331
+ end
332
+
333
+ def http_verb(_method, _path, _body)
334
+ fail NotImplementedError
335
+ end
336
+
337
+ def self.whitelisted_certificates
338
+ path = File.expand_path(File.join(File.dirname(__FILE__), 'ca-coinbase.crt'))
339
+
340
+ certs = [ [] ]
341
+ File.readlines(path).each do |line|
342
+ next if ["\n", "#"].include?(line[0])
343
+ certs.last << line
344
+ certs << [] if line == "-----END CERTIFICATE-----\n"
345
+ end
346
+
347
+ result = OpenSSL::X509::Store.new
348
+
349
+ certs.each do |lines|
350
+ next if lines.empty?
351
+ cert = OpenSSL::X509::Certificate.new(lines.join)
352
+ result.add_cert(cert)
353
+ end
354
+
355
+ result
356
+ end
357
+ end
358
+ end
359
+ end