coinbase-pro 0.4.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.
@@ -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
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "coinbase/pro"
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
@@ -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/pro/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "coinbase-pro"
8
+ spec.version = Coinbase::Pro::VERSION
9
+ spec.authors = ["Vertbase"]
10
+ spec.email = ["dev@vertbase.com"]
11
+
12
+ spec.summary = "Client library for Coinbase Pro"
13
+ spec.homepage = "https://github.com/vertbase/coinbase-pro"
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,21 @@
1
+ require "bigdecimal"
2
+ require "json"
3
+ require "uri"
4
+ require "net/http"
5
+ require "em-http-request"
6
+ require "faye/websocket"
7
+
8
+ require "coinbase/pro/errors"
9
+ require "coinbase/pro/api_object"
10
+ require "coinbase/pro/api_response"
11
+ require "coinbase/pro/api_client.rb"
12
+ require "coinbase/pro/adapters/net_http.rb"
13
+ require "coinbase/pro/adapters/em_http.rb"
14
+ require "coinbase/pro/client"
15
+ require "coinbase/pro/websocket"
16
+
17
+ module Coinbase
18
+ # Coinbase Pro module
19
+ module Pro
20
+ end
21
+ end
@@ -0,0 +1,83 @@
1
+ module Coinbase
2
+ module Pro
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 Pro
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,413 @@
1
+ module Coinbase
2
+ module Pro
3
+ # Net-http client for Coinbase Pro API
4
+ class APIClient
5
+ def initialize(api_key = '', api_secret = '', api_pass = '', options = {})
6
+ @api_uri = URI.parse(options[:api_url] || "https://api.pro.coinbase.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
+ def payment_method_withdrawal(amount, currency, payment_method_id, params = {})
246
+ params[:amount] = amount
247
+ params[:currency] = currency
248
+ params[:payment_method_id] = payment_method_id
249
+ out = nil
250
+ post("/withdrawals/payment-method", params) do |resp|
251
+ out = response_object(resp)
252
+ yield(out, resp) if block_given?
253
+ end
254
+ out
255
+ end
256
+
257
+ def coinbase_withdrawal(amount, currency, coinbase_account_id, params = {})
258
+ params[:amount] = amount
259
+ params[:currency] = currency
260
+ params[:coinbase_account_id] = coinbase_account_id
261
+ out = nil
262
+ post("/withdrawals/coinbase-account", params) do |resp|
263
+ out = response_object(resp)
264
+ yield(out, resp) if block_given?
265
+ end
266
+ out
267
+ end
268
+
269
+ def crypto_withdrawal(amount, currency, crypto_address, params = {})
270
+ params[:amount] = amount
271
+ params[:currency] = currency
272
+ params[:crypto_address] = crypto_address
273
+ out = nil
274
+ post("/withdrawals/crypto", params) do |resp|
275
+ out = response_object(resp)
276
+ yield(out, resp) if block_given?
277
+ end
278
+ out
279
+ end
280
+
281
+ def payment_methods(params = {})
282
+ out = nil
283
+ get("/payment-methods", params, paginate: true) do |resp|
284
+ out = response_collection(resp)
285
+ yield(out, resp) if block_given?
286
+ end
287
+ out
288
+ end
289
+
290
+ def coinbase_accounts(params = {})
291
+ out = nil
292
+ get("/coinbase-accounts", params, paginate: true) do |resp|
293
+ out = response_collection(resp)
294
+ yield(out, resp) if block_given?
295
+ end
296
+ out
297
+ end
298
+
299
+ private
300
+
301
+ def response_collection(resp)
302
+ out = resp.map { |item| APIObject.new(item) }
303
+ out.instance_eval { @response = resp }
304
+ add_metadata(out)
305
+ out
306
+ end
307
+
308
+ def response_object(resp)
309
+ out = APIObject.new(resp)
310
+ out.instance_eval { @response = resp.response }
311
+ add_metadata(out)
312
+ out
313
+ end
314
+
315
+ def add_metadata(resp)
316
+ resp.instance_eval do
317
+ def response
318
+ @response
319
+ end
320
+
321
+ def raw
322
+ @response.raw
323
+ end
324
+
325
+ def response_headers
326
+ @response.headers
327
+ end
328
+
329
+ def response_status
330
+ @response.status
331
+ end
332
+ end
333
+ resp
334
+ end
335
+
336
+ def get(path, params = {}, options = {})
337
+ params[:limit] ||= 100 if options[:paginate] == true
338
+
339
+ http_verb('GET', "#{path}?#{URI.encode_www_form(params)}") do |resp|
340
+ begin
341
+ out = JSON.parse(resp.body)
342
+ rescue JSON::ParserError
343
+ out = resp.body
344
+ end
345
+ out.instance_eval { @response = resp }
346
+ add_metadata(out)
347
+
348
+ if options[:paginate] && out.count == params[:limit]
349
+ params[:after] = resp.headers['CB-AFTER']
350
+ get(path, params, options) do |pages|
351
+ out += pages
352
+ add_metadata(out)
353
+ yield(out)
354
+ end
355
+ else
356
+ yield(out)
357
+ end
358
+ end
359
+ end
360
+
361
+ def post(path, params = {})
362
+ http_verb('POST', path, params.to_json) do |resp|
363
+ begin
364
+ out = JSON.parse(resp.body)
365
+ rescue JSON::ParserError
366
+ out = resp.body
367
+ end
368
+ out.instance_eval { @response = resp }
369
+ add_metadata(out)
370
+ yield(out)
371
+ end
372
+ end
373
+
374
+ def delete(path)
375
+ http_verb('DELETE', path) do |resp|
376
+ begin
377
+ out = JSON.parse(resp.body)
378
+ rescue
379
+ out = resp.body
380
+ end
381
+ out.instance_eval { @response = resp }
382
+ add_metadata(out)
383
+ yield(out)
384
+ end
385
+ end
386
+
387
+ def http_verb(_method, _path, _body)
388
+ fail NotImplementedError
389
+ end
390
+
391
+ def self.whitelisted_certificates
392
+ path = File.expand_path(File.join(File.dirname(__FILE__), 'ca-coinbase.crt'))
393
+
394
+ certs = [ [] ]
395
+ File.readlines(path).each do |line|
396
+ next if ["\n", "#"].include?(line[0])
397
+ certs.last << line
398
+ certs << [] if line == "-----END CERTIFICATE-----\n"
399
+ end
400
+
401
+ result = OpenSSL::X509::Store.new
402
+
403
+ certs.each do |lines|
404
+ next if lines.empty?
405
+ cert = OpenSSL::X509::Certificate.new(lines.join)
406
+ result.add_cert(cert)
407
+ end
408
+
409
+ result
410
+ end
411
+ end
412
+ end
413
+ end