coinbase-exchange 0.1.1

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/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
@@ -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 = "coinbase-exchange"
8
+ spec.version = Coinbase::Exchange::VERSION
9
+ spec.authors = ["John Duhamel"]
10
+ spec.email = ["john.duhamel@coinbase.com"]
11
+
12
+ spec.summary = "Client library for Coinbase Exchange"
13
+ spec.homepage = "https://github.com/coinbase/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 "em-http-request"
22
+ spec.add_dependency "faye-websocket"
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"
6
+ require "faye/websocket"
7
+
8
+ require "coinbase/exchange/errors"
9
+ require "coinbase/exchange/api_object"
10
+ require "coinbase/exchange/api_response"
11
+ require "coinbase/exchange/api_client.rb"
12
+ require "coinbase/exchange/adapters/net_http.rb"
13
+ require "coinbase/exchange/adapters/em_http.rb"
14
+ require "coinbase/exchange/client"
15
+ require "coinbase/exchange/websocket"
16
+
17
+ module Coinbase
18
+ # Coinbase Exchange module
19
+ module Exchange
20
+ end
21
+ end
@@ -0,0 +1,77 @@
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
+ case method
33
+ when 'GET'
34
+ req = EM::HttpRequest.new(@api_uri).get(path: path, head: headers, body: body)
35
+ when 'POST'
36
+ req = EM::HttpRequest.new(@api_uri).post(path: path, head: headers, body: body)
37
+ when 'DELETE'
38
+ req = EM::HttpRequest.new(@api_uri).delete(path: path, head: headers)
39
+ else fail
40
+ end
41
+ req.callback do |resp|
42
+ case resp.response_header.status
43
+ when 200 then yield(EMHTTPResponse.new(resp))
44
+ when 400 then fail BadRequestError, resp.response
45
+ when 401 then fail NotAuthorizedError, resp.response
46
+ when 403 then fail ForbiddenError, resp.response
47
+ when 404 then fail NotFoundError, resp.response
48
+ when 429 then fail RateLimitError, resp.response
49
+ when 500 then fail InternalServerError, resp.response
50
+ end
51
+ end
52
+ req.errback do |resp|
53
+ fail APIError, "#{method} #{@api_uri}#{path}: #{resp.error}"
54
+ end
55
+ end
56
+ end
57
+ end
58
+
59
+ # EM-Http response object
60
+ class EMHTTPResponse < APIResponse
61
+ def body
62
+ @response.response
63
+ end
64
+
65
+ def headers
66
+ out = @response.response_header.map do |key, val|
67
+ [ key.upcase.gsub('_', '-'), val ]
68
+ end
69
+ out.to_h
70
+ end
71
+
72
+ def status
73
+ @response.response_header.status
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,65 @@
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
9
+ end
10
+
11
+ private
12
+
13
+ def http_verb(method, path, body = nil)
14
+ case method
15
+ when 'GET' then req = Net::HTTP::Get.new(path)
16
+ when 'POST' then req = Net::HTTP::Post.new(path)
17
+ when 'DELETE' then req = Net::HTTP::Delete.new(path)
18
+ else fail
19
+ end
20
+
21
+ req.body = body
22
+
23
+ req_ts = Time.now.utc.to_i.to_s
24
+ signature = Base64.encode64(
25
+ OpenSSL::HMAC.digest('sha256', Base64.decode64(@api_secret).strip,
26
+ "#{req_ts}#{method}#{path}#{body}")).strip
27
+ req['Content-Type'] = 'application/json'
28
+ req['CB-ACCESS-TIMESTAMP'] = req_ts
29
+ req['CB-ACCESS-PASSPHRASE'] = @api_pass
30
+ req['CB-ACCESS-KEY'] = @api_key
31
+ req['CB-ACCESS-SIGN'] = signature
32
+
33
+ resp = @conn.request(req)
34
+ case resp.code
35
+ when "200" then yield(NetHTTPResponse.new(resp))
36
+ when "400" then fail BadRequestError, resp.body
37
+ when "401" then fail NotAuthorizedError, resp.body
38
+ when "403" then fail ForbiddenError, resp.body
39
+ when "404" then fail NotFoundError, resp.body
40
+ when "429" then fail RateLimitError, resp.body
41
+ when "500" then fail InternalServerError, resp.body
42
+ end
43
+ resp.body
44
+ end
45
+ end
46
+
47
+ # Net-Http response object
48
+ class NetHTTPResponse < APIResponse
49
+ def body
50
+ @response.body
51
+ end
52
+
53
+ def headers
54
+ out = @response.to_hash.map do |key, val|
55
+ [ key.upcase.gsub('_', '-'), val.count == 1 ? val.first : val ]
56
+ end
57
+ out.to_h
58
+ end
59
+
60
+ def status
61
+ @response.code.to_i
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,290 @@
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.exchange.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
+ get("/currencies", params) do |resp|
24
+ out = response_collection(resp)
25
+ yield(out) if block_given?
26
+ end
27
+ end
28
+
29
+ def products(params = {})
30
+ get("/products", params) do |resp|
31
+ out = response_collection(resp)
32
+ yield(out) if block_given?
33
+ end
34
+ end
35
+
36
+ def orderbook(params = {})
37
+ product = params[:product_id] || @default_product
38
+ get("/products/#{product}/book", params) do |resp|
39
+ out = response_object(resp)
40
+ yield(out) if block_given?
41
+ end
42
+ end
43
+
44
+ def last_trade(params = {})
45
+ product = params[:product_id] || @default_product
46
+ get("/products/#{product}/ticker", params) do |resp|
47
+ out = response_object(resp)
48
+ yield(out) if block_given?
49
+ end
50
+ end
51
+
52
+ def trade_history(params = {})
53
+ product = params[:product_id] || @default_product
54
+ get("/products/#{product}/trades", params, paginate: true) do |resp|
55
+ out = response_collection(resp)
56
+ yield(out) if block_given?
57
+ end
58
+ end
59
+
60
+ def price_history(params = {})
61
+ product = params[:product_id] || @default_product
62
+ get("/products/#{product}/candles", params) do |resp|
63
+ out = response_collection(
64
+ resp.map do |item|
65
+ { 'start' => Time.at(item[0]),
66
+ 'low' => item[1],
67
+ 'high' => item[2],
68
+ 'open' => item[3],
69
+ 'close' => item[4],
70
+ 'volume' => item[5]
71
+ }
72
+ end
73
+ )
74
+ yield(out) if block_given?
75
+ end
76
+ end
77
+
78
+ def daily_stats(params = {})
79
+ product = params[:product_id] || @default_product
80
+ get("/products/#{product}/stats", params) do |resp|
81
+ resp["start"] = (Time.now - 24 * 60 * 60).to_s
82
+ out = response_object(resp)
83
+ yield(out) if block_given?
84
+ end
85
+ end
86
+
87
+ #
88
+ # Accounts
89
+ #
90
+ def accounts(params = {})
91
+ get("/accounts", params) do |resp|
92
+ out = response_collection(resp)
93
+ yield(out) if block_given?
94
+ end
95
+ end
96
+
97
+ def account(id, params = {})
98
+ get("/accounts/#{id}", params) do |resp|
99
+ out = response_object(resp)
100
+ yield(out) if block_given?
101
+ end
102
+ end
103
+
104
+ def account_history(id, params = {})
105
+ get("/accounts/#{id}/ledger", params, paginate: true) do |resp|
106
+ out = response_collection(resp)
107
+ yield(out) if block_given?
108
+ end
109
+ end
110
+
111
+ def account_holds(id, params = {})
112
+ get("/accounts/#{id}/holds", params, paginate: true) do |resp|
113
+ out = response_collection(resp)
114
+ yield(out) if block_given?
115
+ end
116
+ end
117
+
118
+ #
119
+ # Orders
120
+ #
121
+ def bid(amt, price, params = {})
122
+ params[:product_id] ||= @default_product
123
+ params[:size] = amt
124
+ params[:price] = price
125
+ params[:side] = "buy"
126
+ post("/orders", params) do |resp|
127
+ out = response_object(resp)
128
+ yield(out) if block_given?
129
+ end
130
+ end
131
+ alias_method :buy, :bid
132
+
133
+ def ask(amt, price, params = {})
134
+ params[:product_id] ||= @default_product
135
+ params[:size] = amt
136
+ params[:price] = price
137
+ params[:side] = "sell"
138
+ post("/orders", params) do |resp|
139
+ out = response_object(resp)
140
+ yield(out) if block_given?
141
+ end
142
+ end
143
+ alias_method :sell, :ask
144
+
145
+ def cancel(id)
146
+ delete("/orders/#{id}") do |resp|
147
+ out = response_object(resp)
148
+ yield(out) if block_given?
149
+ end
150
+ end
151
+
152
+ def orders(params = {})
153
+ params[:status] ||= "all"
154
+ get("/orders", params, paginate: true) do |resp|
155
+ out = response_collection(resp)
156
+ yield(out) if block_given?
157
+ end
158
+ end
159
+
160
+ def order(id, params = {})
161
+ get("/orders/#{id}", params) do |resp|
162
+ out = response_object(resp)
163
+ yield(out) if block_given?
164
+ end
165
+ end
166
+
167
+ def fills(params = {})
168
+ get("/fills", params, paginate: true) do |resp|
169
+ out = response_collection(resp)
170
+ yield(out) if block_given?
171
+ end
172
+ end
173
+
174
+ #
175
+ # Transfers
176
+ #
177
+ def deposit(account_id, amt, params = {})
178
+ params[:type] = "deposit"
179
+ params[:coinbase_account_id] = account_id
180
+ params[:amount] = amt
181
+ post("/transfers", params) do |resp|
182
+ out = response_object(resp)
183
+ yield(out) if block_given?
184
+ end
185
+ end
186
+
187
+ def withdraw(account_id, amt, params = {})
188
+ params[:type] = "withdraw"
189
+ params[:coinbase_account_id] = account_id
190
+ params[:amount] = amt
191
+ post("/transfers", params) do |resp|
192
+ out = response_object(resp)
193
+ yield(out) if block_given?
194
+ end
195
+ end
196
+
197
+ private
198
+
199
+ def response_collection(resp)
200
+ out = resp.map { |item| APIObject.new(item) }
201
+ out.instance_eval { @response = resp.response }
202
+ add_metadata(out)
203
+ out
204
+ end
205
+
206
+ def response_object(resp)
207
+ out = APIObject.new(resp)
208
+ out.instance_eval { @response = resp.response }
209
+ add_metadata(out)
210
+ out
211
+ end
212
+
213
+ def add_metadata(resp)
214
+ resp.instance_eval do
215
+ def response
216
+ @response
217
+ end
218
+
219
+ def raw
220
+ @response.raw
221
+ end
222
+
223
+ def response_headers
224
+ @response.headers
225
+ end
226
+
227
+ def response_status
228
+ @response.status
229
+ end
230
+ end
231
+ resp
232
+ end
233
+
234
+ def get(path, params = {}, options = {})
235
+ params[:limit] ||= 100 if options[:paginate] == true
236
+
237
+ http_verb('GET', "#{path}?#{URI.encode_www_form(params)}") do |resp|
238
+ begin
239
+ out = JSON.parse(resp.body)
240
+ rescue JSON::ParserError
241
+ out = resp.body
242
+ end
243
+ out.instance_eval { @response = resp }
244
+ add_metadata(out)
245
+
246
+ if options[:paginate] && out.count == params[:limit]
247
+ params[:after] = resp.headers['CB-AFTER']
248
+ get(path, params, options) do |pages|
249
+ out += pages
250
+ add_metadata(out)
251
+ yield(out)
252
+ end
253
+ else
254
+ yield(out)
255
+ end
256
+ end
257
+ end
258
+
259
+ def post(path, params = {})
260
+ http_verb('POST', path, params.to_json) do |resp|
261
+ begin
262
+ out = JSON.parse(resp.body)
263
+ rescue JSON::ParserError
264
+ out = resp.body
265
+ end
266
+ out.instance_eval { @response = resp }
267
+ add_metadata(out)
268
+ yield(out)
269
+ end
270
+ end
271
+
272
+ def delete(path)
273
+ http_verb('DELETE', path) do |resp|
274
+ begin
275
+ out = JSON.parse(resp.body)
276
+ rescue
277
+ out = resp.body
278
+ end
279
+ out.instance_eval { @response = resp }
280
+ add_metadata(out)
281
+ yield(out)
282
+ end
283
+ end
284
+
285
+ def http_verb(_method, _path, _body)
286
+ fail NotImplementedError
287
+ end
288
+ end
289
+ end
290
+ end