bitsor 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/.circleci/config.yml +61 -0
  3. data/.gitignore +16 -0
  4. data/.rspec +2 -0
  5. data/.rubocop.yml +45 -0
  6. data/.ruby-version +1 -0
  7. data/.travis.yml +5 -0
  8. data/Gemfile +20 -0
  9. data/LICENSE.txt +21 -0
  10. data/README.md +74 -0
  11. data/Rakefile +9 -0
  12. data/bin/console +16 -0
  13. data/bin/setup +8 -0
  14. data/bitsor.gemspec +33 -0
  15. data/lib/bitsor/client/account_status.rb +14 -0
  16. data/lib/bitsor/client/available_books.rb +14 -0
  17. data/lib/bitsor/client/balance.rb +14 -0
  18. data/lib/bitsor/client/bitcoin_withdrawal.rb +12 -0
  19. data/lib/bitsor/client/debit_card_withdrawal.rb +12 -0
  20. data/lib/bitsor/client/ether_withdrawal.rb +12 -0
  21. data/lib/bitsor/client/fees.rb +14 -0
  22. data/lib/bitsor/client/funding.rb +14 -0
  23. data/lib/bitsor/client/funding_destination.rb +14 -0
  24. data/lib/bitsor/client/kyc_documents.rb +12 -0
  25. data/lib/bitsor/client/ledger.rb +38 -0
  26. data/lib/bitsor/client/mx_bank_codes.rb +12 -0
  27. data/lib/bitsor/client/open_orders.rb +14 -0
  28. data/lib/bitsor/client/order_book.rb +12 -0
  29. data/lib/bitsor/client/order_trades.rb +12 -0
  30. data/lib/bitsor/client/orders.rb +22 -0
  31. data/lib/bitsor/client/phone_number.rb +12 -0
  32. data/lib/bitsor/client/phone_number_withdrawal.rb +12 -0
  33. data/lib/bitsor/client/phone_verification.rb +12 -0
  34. data/lib/bitsor/client/spei_withdrawal.rb +12 -0
  35. data/lib/bitsor/client/ticker.rb +14 -0
  36. data/lib/bitsor/client/trades.rb +14 -0
  37. data/lib/bitsor/client/user_trades.rb +14 -0
  38. data/lib/bitsor/client/withdrawals.rb +12 -0
  39. data/lib/bitsor/client.rb +86 -0
  40. data/lib/bitsor/concerns/authorizable.rb +10 -0
  41. data/lib/bitsor/concerns/configurable.rb +41 -0
  42. data/lib/bitsor/concerns/connection.rb +86 -0
  43. data/lib/bitsor/concerns/rate_limit.rb +18 -0
  44. data/lib/bitsor/default.rb +32 -0
  45. data/lib/bitsor/error.rb +96 -0
  46. data/lib/bitsor/normalizer.rb +138 -0
  47. data/lib/bitsor/version.rb +20 -0
  48. data/lib/bitsor.rb +33 -0
  49. metadata +148 -0
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bitsor
4
+ class Client
5
+ module Trades
6
+ def trades(book:, marker: nil, sort: :desc, limit: 25)
7
+ normalize_response.with(:trade) do
8
+ get('/v3/trades/', book: book, marker: marker, sort: sort, limit: limit)
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
14
+
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bitsor
4
+ class Client
5
+ module UserTrades
6
+ def user_trades(book:, marker: nil, sort: :desc, limit: 25)
7
+ normalize_response.with(:user_trade) do
8
+ get('/v3/user_trades/', book: book, marker: marker, sort: sort, limit: limit)
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
14
+
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bitsor
4
+ class Client
5
+ module Withdrawals
6
+ def withdrawals(limit: 25)
7
+ get('/v3/withdrawals/', limit: limit)
8
+ end
9
+ end
10
+ end
11
+ end
12
+
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bitsor/error'
4
+ require 'bitsor/normalizer'
5
+
6
+ require 'bitsor/concerns/configurable'
7
+ require 'bitsor/concerns/rate_limit'
8
+ require 'bitsor/concerns/connection'
9
+ require 'bitsor/concerns/authorizable'
10
+
11
+ require 'bitsor/client/account_status'
12
+ require 'bitsor/client/available_books'
13
+ require 'bitsor/client/balance'
14
+ require 'bitsor/client/bitcoin_withdrawal'
15
+ require 'bitsor/client/debit_card_withdrawal'
16
+ require 'bitsor/client/ether_withdrawal'
17
+ require 'bitsor/client/fees'
18
+ require 'bitsor/client/funding'
19
+ require 'bitsor/client/funding_destination'
20
+ require 'bitsor/client/kyc_documents'
21
+ require 'bitsor/client/ledger'
22
+ require 'bitsor/client/mx_bank_codes'
23
+ require 'bitsor/client/open_orders'
24
+ require 'bitsor/client/order_book'
25
+ require 'bitsor/client/order_trades'
26
+ require 'bitsor/client/orders'
27
+ require 'bitsor/client/phone_number'
28
+ require 'bitsor/client/phone_number_withdrawal'
29
+ require 'bitsor/client/phone_verification'
30
+ require 'bitsor/client/spei_withdrawal'
31
+ require 'bitsor/client/ticker'
32
+ require 'bitsor/client/trades'
33
+ require 'bitsor/client/user_trades'
34
+ require 'bitsor/client/withdrawals'
35
+
36
+ module Bitsor
37
+ class Client
38
+ include Bitsor::Configurable
39
+ include Bitsor::Connection
40
+ include Bitsor::Authorizable
41
+
42
+ include Bitsor::Client::AccountStatus
43
+ include Bitsor::Client::AvailableBooks
44
+ include Bitsor::Client::Balance
45
+ include Bitsor::Client::BitcoinWithdrawal
46
+ include Bitsor::Client::DebitCardWithdrawal
47
+ include Bitsor::Client::EtherWithdrawal
48
+ include Bitsor::Client::Fees
49
+ include Bitsor::Client::Funding
50
+ include Bitsor::Client::FundingDestination
51
+ include Bitsor::Client::KycDocuments
52
+ include Bitsor::Client::Ledger
53
+ include Bitsor::Client::MxBankCodes
54
+ include Bitsor::Client::OpenOrders
55
+ include Bitsor::Client::OrderBook
56
+ include Bitsor::Client::OrderTrades
57
+ include Bitsor::Client::Orders
58
+ include Bitsor::Client::PhoneNumber
59
+ include Bitsor::Client::PhoneNumberWithdrawal
60
+ include Bitsor::Client::PhoneVerification
61
+ include Bitsor::Client::SpeiWithdrawal
62
+ include Bitsor::Client::Ticker
63
+ include Bitsor::Client::Trades
64
+ include Bitsor::Client::UserTrades
65
+ include Bitsor::Client::Withdrawals
66
+
67
+ attr_writer :client_id
68
+ attr_writer :api_key
69
+ attr_writer :api_secret
70
+
71
+ def initialize(options = {})
72
+ Bitsor::Configurable.keys.each do |key|
73
+ instance_variable_set(:"@#{key}", options[key] || Bitsor.instance_variable_get(:"@#{key}"))
74
+ end
75
+ end
76
+
77
+ def inspect
78
+ "Bitsor::Client(client_id: ****#{@client_id[4..-1]} api_key: ******#{@api_key[6..-1]}, object_id: #{format('0x00%x', (object_id << 1))})"
79
+ end
80
+
81
+ def normalize_response
82
+ @normalizer ||= Normalizer.new
83
+ end
84
+ end
85
+ end
86
+
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bitsor
4
+ module Authorizable
5
+ def authenticated?
6
+ !!(@client_id && @api_key && @api_secret)
7
+ end
8
+ end
9
+ end
10
+
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bitsor
4
+ module Configurable
5
+ attr_accessor :client_id, :api_key, :api_secret
6
+
7
+ class << self
8
+ def keys
9
+ @keys ||= [
10
+ :client_id,
11
+ :api_key,
12
+ :api_secret,
13
+ ]
14
+ end
15
+ end
16
+
17
+ def configure
18
+ yield self
19
+ end
20
+
21
+ def reset!
22
+ Bitsor::Configurable.keys.each do |key|
23
+ instance_variable_set(:"@#{key}", Bitsor::Default.options[key])
24
+ end
25
+ @last_response = nil
26
+ self
27
+ end
28
+ alias setup reset!
29
+
30
+ def api_endpoint
31
+ File.join(@api_endpoint, '')
32
+ end
33
+
34
+ private
35
+
36
+ def options
37
+ Hash[Bitsor::Configurable.keys.map { |key| [key, instance_variable_get(:"@#{key}")] }]
38
+ end
39
+ end
40
+ end
41
+
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'date'
4
+ require 'json'
5
+ require 'openssl'
6
+ require 'typhoeus'
7
+
8
+ require 'bitsor/error'
9
+
10
+ module Bitsor
11
+ module Connection
12
+ def get(url, options = {})
13
+ request :get, url, nil, parse_query(options)
14
+ end
15
+
16
+ def post(url, options = {})
17
+ request :post, url, parse_body(options)
18
+ end
19
+
20
+ def put(url, options = {})
21
+ request :put, url, parse_body(options)
22
+ end
23
+
24
+ def patch(url, options = {})
25
+ request :patch, url, parse_body(options)
26
+ end
27
+
28
+ def delete(url, options = {})
29
+ request :delete, url, parse_body(options)
30
+ end
31
+
32
+ def last_response
33
+ @last_response if defined? @last_response
34
+ end
35
+
36
+ protected
37
+
38
+ def endpoint
39
+ Bitsor::Default.api_endpoint
40
+ end
41
+
42
+ private
43
+
44
+ def request(method, path, body = nil, params = nil)
45
+ nonce = DateTime.now.strftime('%Q')
46
+ message = nonce + method.to_s.upcase + path + params.to_s + body.to_s
47
+ signature = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), api_secret, message)
48
+
49
+ url = "#{endpoint}#{path}#{params}"
50
+ request_options = {
51
+ method: method,
52
+ headers: {
53
+ Authorization: "Bitso #{api_key}:#{nonce}:#{signature}",
54
+ 'Content-Type': 'application/json',
55
+ },
56
+ }
57
+ request_options[:body] = body
58
+ response = Typhoeus::Request.new(url, request_options).run
59
+ @last_response = response
60
+
61
+ complete_request(response)
62
+ end
63
+
64
+ def complete_request(response)
65
+ if error = Bitsor::Error.from_response(response)
66
+ raise error
67
+ end
68
+
69
+ JSON.parse(response.body, symbolize_names: true)[:payload]
70
+ end
71
+
72
+ def parse_query(options)
73
+ return nil if options.empty? || !options
74
+
75
+ options = options.select { |_key, value| !value.nil? || (value && !value.empty?) }
76
+ "?#{URI.encode_www_form(options)}"
77
+ end
78
+
79
+ def parse_body(options)
80
+ return '' if options.nil? || options.empty?
81
+ options = (options || {}).delete_if { |_k, v| v.nil? }
82
+ options.to_json
83
+ end
84
+ end
85
+ end
86
+
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bitsor
4
+ class RateLimit < Struct.new(:limit, :remaining, :resets_at, :resets_in)
5
+ def self.from_response(response)
6
+ info = new
7
+ unless response&.headers.nil?
8
+ info.limit = (response.headers['X-RateLimit-Limit'] || 1).to_i
9
+ info.remaining = (response.headers['X-RateLimit-Remaining'] || 1).to_i
10
+ info.resets_at = Time.at((response.headers['X-RateLimit-Reset'] || Time.now).to_i)
11
+ info.resets_in = [(info.resets_at - Time.now).to_i, 0].max
12
+ end
13
+
14
+ info
15
+ end
16
+ end
17
+ end
18
+
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bitsor/version'
4
+
5
+ module Bitsor
6
+ module Default
7
+ API_ENDPOINT = 'https://api.bitso.com'
8
+
9
+ class << self
10
+ def options
11
+ Hash[Bitsor::Configurable.keys.map { |key| [key, send(key)] }]
12
+ end
13
+
14
+ def client_id
15
+ ENV['CLIENT_ID']
16
+ end
17
+
18
+ def api_key
19
+ ENV['API_KEY']
20
+ end
21
+
22
+ def api_secret
23
+ ENV['API_SECRET']
24
+ end
25
+
26
+ def api_endpoint
27
+ ENV['API_ENDPOINT'] || API_ENDPOINT
28
+ end
29
+ end
30
+ end
31
+ end
32
+
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bitsor
4
+ class Error < StandardError
5
+ def self.from_response(response)
6
+ status = response.response_code
7
+
8
+ if klass = case status
9
+ when 400 then Bitsor::BadRequest
10
+ when 401 then Bitsor::Forbidden
11
+ when 403 then Bitsor::Forbidden
12
+ when 404 then Bitsor::NotFound
13
+ when 405 then Bitsor::MethodNotAllowed
14
+ when 406 then Bitsor::NotAcceptable
15
+ when 422 then Bitsor::UnprocessableEntity
16
+ when 400..499 then Bitsor::ClientError
17
+ when 500 then Bitsor::InternalServerError
18
+ when 501 then Bitsor::NotImplemented
19
+ when 502 then Bitsor::BadGateway
20
+ when 503 then Bitsor::ServiceUnavailable
21
+ when 500..599 then Bitsor::ServerError
22
+ end
23
+ klass.new(response)
24
+ end
25
+ end
26
+
27
+ def initialize(response = nil)
28
+ @response = response
29
+ @request = response.request
30
+ @body = { error: {} }
31
+
32
+ begin
33
+ if response.body && !response.body.empty?
34
+ @body = JSON.parse(response.body, symbolize_names: true)
35
+ end
36
+ rescue JSON::ParserError => e
37
+ @body = { error: { code: response.response_code, message: 'Internal Server Error: An Error Was Encountered' } }
38
+ end
39
+
40
+ super(build_error_message)
41
+ end
42
+
43
+ def build_error_message
44
+ return nil if @response.nil?
45
+ message = ["#{@request.options[:method].to_s.upcase} "]
46
+ message << @response.options[:effective_url].to_s + "\n"
47
+ message << "Code #{@body[:error][:code]}: #{@body[:error][:message]} \n"
48
+ message.join
49
+ end
50
+
51
+ attr_accessor :response, :request, :body
52
+ end
53
+
54
+ # Raised on errors in the 400-499 range
55
+ class ClientError < Error; end
56
+
57
+ # Raised when 400 HTTP status code
58
+ class BadRequest < ClientError; end
59
+
60
+ # Raised when 401 HTTP status code
61
+ class Unauthorized < ClientError; end
62
+
63
+ # Raised when 403 HTTP status code
64
+ class Forbidden < ClientError; end
65
+
66
+ # Raised when 403 HTTP status code
67
+ class TooManyRequests < Forbidden; end
68
+
69
+ # Raised when 404 HTTP status code
70
+ class NotFound < ClientError; end
71
+
72
+ # Raised when 405 HTTP status code
73
+ class MethodNotAllowed < ClientError; end
74
+
75
+ # Raised when 406 HTTP status code
76
+ class NotAcceptable < ClientError; end
77
+
78
+ # Raised when 422 HTTP status code
79
+ class UnprocessableEntity < ClientError; end
80
+
81
+ # Raised on errors in the 500-599 range
82
+ class ServerError < Error; end
83
+
84
+ # Raised when 500 HTTP status code
85
+ class InternalServerError < ServerError; end
86
+
87
+ # Raised when 501 HTTP status code
88
+ class NotImplemented < ServerError; end
89
+
90
+ # Raised when 502 HTTP status code
91
+ class BadGateway < ServerError; end
92
+
93
+ # Raised when 503 HTTP status code
94
+ class ServiceUnavailable < ServerError; end
95
+ end
96
+
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'time'
4
+
5
+ module Bitsor
6
+ class Normalizer
7
+ SCHEMAS = {
8
+ account_balances: 'normalize_account_balances',
9
+ account_fees: 'normalize_account_fees',
10
+ account_status: 'normalize_account',
11
+ book: 'normalize_book',
12
+ funding: 'normalize_funding',
13
+ ledger: 'normalize_ledger',
14
+ order: 'normalize_order',
15
+ ticker: 'normalize_ticker',
16
+ trade: 'normalize_trade',
17
+ user_trade: 'normalize_user_trade',
18
+ }.freeze
19
+
20
+ def with(type)
21
+ method = SCHEMAS[type.to_sym] || :null_normalizer
22
+
23
+ response = yield
24
+
25
+ if response.class == Array
26
+ return response.map do |response_element|
27
+ send method, response_element
28
+ end
29
+ end
30
+
31
+ send method, response
32
+ end
33
+
34
+ private
35
+
36
+ def null_normalizer(response)
37
+ response
38
+ end
39
+
40
+ def normalize_account(response_object)
41
+ response_object[:client_id] = response_object[:client_id].to_i
42
+ response_object[:daily_limit] = response_object[:daily_limit].to_i
43
+ response_object[:monthly_limit] = response_object[:monthly_limit].to_i
44
+ response_object[:daily_remaining] = response_object[:daily_remaining].to_f
45
+ response_object[:monthly_remaining] = response_object[:monthly_remaining].to_f
46
+ response_object
47
+ end
48
+
49
+ def normalize_account_balances(response_object)
50
+ response_object[:balances] = response_object[:balances].map do |balance|
51
+ balance[:available] = balance[:available].to_f
52
+ balance[:locked] = balance[:locked].to_f
53
+ balance[:total] = balance[:total].to_f
54
+ balance[:pending_deposit] = balance[:pending_deposit].to_f
55
+ balance[:pending_withdrawal] = balance[:pending_withdrawal].to_f
56
+ balance
57
+ end
58
+ response_object
59
+ end
60
+
61
+ def normalize_book(response_object)
62
+ response_object[:minimum_price] = response_object[:minimum_price].to_f
63
+ response_object[:maximum_price] = response_object[:maximum_price].to_f
64
+ response_object[:minimum_amount] = response_object[:minimum_amount].to_f
65
+ response_object[:maximum_amount] = response_object[:maximum_amount].to_f
66
+ response_object[:minimum_value] = response_object[:minimum_value].to_f
67
+ response_object[:maximum_value] = response_object[:maximum_value].to_f
68
+ response_object
69
+ end
70
+
71
+ def normalize_account_fees(response_object)
72
+ response_object[:fees] = response_object[:fees].map do |fee|
73
+ fee[:fee_percent] = fee[:fee_percent].to_f
74
+ fee[:fee_decimal] = fee[:fee_decimal].to_f
75
+ fee
76
+ end
77
+ response_object[:withdrawal_fees][:btc] = response_object[:withdrawal_fees][:btc].to_f
78
+ response_object[:withdrawal_fees][:eth] = response_object[:withdrawal_fees][:eth].to_f
79
+ response_object
80
+ end
81
+
82
+ def normalize_funding(response_object)
83
+ response_object[:created_at] = DateTime.parse(response_object[:created_at])
84
+ response_object[:amount] = response_object[:amount].to_f
85
+ response_object
86
+ end
87
+
88
+ def normalize_order(response_object)
89
+ response_object[:original_amount] = response_object[:original_amount].to_f
90
+ response_object[:unfilled_amount] = response_object[:unfilled_amount].to_f
91
+ response_object[:original_value] = response_object[:original_value].to_f
92
+ response_object[:price] = response_object[:price].to_f
93
+ response_object[:created_at] = DateTime.parse(response_object[:created_at])
94
+ response_object[:updated_at] = DateTime.parse(response_object[:updated_at])
95
+ response_object
96
+ end
97
+
98
+ def normalize_ledger(response_object)
99
+ response_object[:created_at] = DateTime.parse(response_object[:created_at])
100
+ response_object[:balance_updates] = response_object[:balance_updates].map do |balance|
101
+ balance[:amount] = balance[:amount].to_f
102
+ balance
103
+ end
104
+ response_object
105
+ end
106
+
107
+ def normalize_ticker(response_object)
108
+ response_object[:volume] = response_object[:volume].to_f
109
+ response_object[:high] = response_object[:high].to_f
110
+ response_object[:last] = response_object[:last].to_f
111
+ response_object[:low] = response_object[:low].to_f
112
+ response_object[:vwap] = response_object[:vwap].to_f
113
+ response_object[:ask] = response_object[:ask].to_f
114
+ response_object[:bid] = response_object[:bid].to_f
115
+ response_object[:created_at] = DateTime.parse(response_object[:created_at])
116
+ response_object
117
+ end
118
+
119
+ def normalize_trade(response_object)
120
+ response_object[:amount] = response_object[:amount].to_f
121
+ response_object[:price] = response_object[:price].to_f
122
+ response_object[:created_at] = DateTime.parse(response_object[:created_at])
123
+ response_object
124
+ end
125
+
126
+ def normalize_user_trade(response_object)
127
+ response_object[:major] = response_object[:major].to_f
128
+ response_object[:minor] = response_object[:minor].to_f
129
+ response_object[:amount] = response_object[:amount].to_f
130
+ response_object[:fees_amount] = response_object[:fees_amount].to_f
131
+ response_object[:price] = response_object[:price].to_f
132
+ response_object[:tid] = response_object[:tid].to_i
133
+ response_object[:created_at] = DateTime.parse(response_object[:created_at])
134
+ response_object
135
+ end
136
+ end
137
+ end
138
+
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bitsor
4
+ # Current major release.
5
+ # @return [Integer]
6
+ MAJOR = 0
7
+
8
+ # Current minor release.
9
+ # @return [Integer]
10
+ MINOR = 1
11
+
12
+ # Current patch level.
13
+ # @return [Integer]
14
+ PATCH = 0
15
+
16
+ # Full release version.
17
+ # @return [String]
18
+ VERSION = [MAJOR, MINOR, PATCH].join('.').freeze
19
+ end
20
+
data/lib/bitsor.rb ADDED
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bitsor/client'
4
+ require 'bitsor/default'
5
+ require 'bitsor/error'
6
+
7
+ module Bitsor
8
+ class << self
9
+ include Bitsor::Configurable
10
+
11
+ def client
12
+ return @client if defined?(@client)
13
+ @client = Bitsor::Client.new(options)
14
+ end
15
+
16
+ private
17
+
18
+ def respond_to_missing?(method_name, include_private = false)
19
+ client.respond_to?(method_name, include_private)
20
+ end
21
+
22
+ def method_missing(method_name, *args, &block)
23
+ if client.respond_to?(method_name)
24
+ return client.send(method_name, *args, &block)
25
+ end
26
+
27
+ super
28
+ end
29
+ end
30
+ end
31
+
32
+ Bitsor.setup
33
+