bitsor 0.1.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.
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
+