coinbase-pro 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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