coinbase-exchange 0.1.1

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/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