cryptomarket-sdk 1.0.0 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cryptomarket
4
+ module Args
5
+ module Sort
6
+ ASC = 'ASC'
7
+ DESC = 'DESC'
8
+ end
9
+
10
+ module Period # rubocop:disable Style/Documentation
11
+ _1_MINS = 'M1' # rubocop:disable Naming/VariableName
12
+ _3_MINS = 'M3' # rubocop:disable Naming/VariableName
13
+ _5_MINS = 'M5' # rubocop:disable Naming/VariableName
14
+ _15_MINS = 'M15' # rubocop:disable Naming/VariableName
15
+ _30_MINS = 'M30' # rubocop:disable Naming/VariableName
16
+ _1_HOURS = 'H1' # rubocop:disable Naming/VariableName
17
+ _4_HOURS = 'H4' # rubocop:disable Naming/VariableName
18
+ _1_DAYS = 'D1' # rubocop:disable Naming/VariableName
19
+ _7_DAYS = 'D7' # rubocop:disable Naming/VariableName
20
+ _1_MONTHS = '1M' # rubocop:disable Naming/VariableName
21
+ end
22
+
23
+ module Side
24
+ BUY = 'buy'
25
+ SELL = 'sell'
26
+ end
27
+
28
+ module OrderType
29
+ LIMIT = 'limit'
30
+ MARKET = 'market'
31
+ STOP_LIMIT = 'stopLimit'
32
+ STOP_MARKET = 'stopMarket'
33
+ TAKE_PROFIT_LIMIT = 'takeProfitLimit'
34
+ TAKE_PROFIT_MARKET = 'takeProfitMarket'
35
+ end
36
+
37
+ module TimeInForce
38
+ GTC = 'GTC' # Good till canceled
39
+ IOC = 'IOC' # Immediate or cancell
40
+ FOK = 'FOK' # Fill or kill
41
+ DAY = 'Day' # Good for the day
42
+ GTD = 'GDT' # Good till date
43
+ end
44
+
45
+ module IdentifyBy
46
+ USERNAME = 'username'
47
+ EMAIL = 'email'
48
+ end
49
+
50
+ module Offchain
51
+ NEVER = 'never'
52
+ OPTIONALLY = 'optionally'
53
+ REQUIRED = 'required'
54
+ end
55
+
56
+ module Account
57
+ SPOT = 'spot'
58
+ WALLET = 'wallet'
59
+ end
60
+
61
+ module TransactionType
62
+ DEPOSIT = 'DEPOSIT'
63
+ WITHDRAW = 'WITHDRAW'
64
+ TRANSFER = 'TRANSFER'
65
+ SAWAP = 'SAWAP'
66
+ end
67
+
68
+ module TransactionSubtype
69
+ UNCLASSIFIED = 'UNCLASSIFIED'
70
+ BLOCKCHAIN = 'BLOCKCHAIN'
71
+ AIRDROP = 'AIRDROP'
72
+ AFFILIATE = 'AFFILIATE'
73
+ STAKING = 'STAKING'
74
+ BUY_CRYPTO = 'BUY_CRYPTO'
75
+ OFFCHAIN = 'OFFCHAIN'
76
+ FIAT = 'FIAT'
77
+ SUB_ACCOUNT = 'SUB_ACCOUNT'
78
+ WALLET_TO_SPOT = 'WALLET_TO_SPOT'
79
+ SPOT_TO_WALLET = 'SPOT_TO_WALLET'
80
+ WALLET_TO_DERIVATIVES = 'WALLET_TO_DERIVATIVES'
81
+ DERIVATIVES_TO_WALLET = 'DERIVATIVES_TO_WALLET'
82
+ CHAIN_SWITCH_FROM = 'CHAIN_SWITCH_FROM'
83
+ CHAIN_SWITCH_TO = 'CHAIN_SWITCH_TO'
84
+ INSTANT_EXCHANGE = 'INSTANT_EXCHANGE'
85
+ end
86
+
87
+ module TransactionStatus
88
+ CREATED = 'CREATED'
89
+ PENDING = 'PENDING'
90
+ FAILED = 'FAILED'
91
+ SUCCESS = 'SUCCESS'
92
+ ROLLED_BACK = 'ROLLED_BACK'
93
+ end
94
+
95
+ module SortBy
96
+ CREATED_AT = 'created_at'
97
+ ID = 'id'
98
+ end
99
+
100
+ module Contingency
101
+ ALL_OR_NONE = 'allOrNone'
102
+ AON = 'allOrNone'
103
+ ONE_CANCEL_OTHER = 'oneCancelOther'
104
+ OCO = 'oneCancelOther'
105
+ ONE_TRIGGER_OTHER = 'oneTriggerOther'
106
+ OTO = 'oneTriggerOther'
107
+ ONE_TRIGGER_ONE_CANCEL_OTHER = 'oneTriggerOneCancelOther'
108
+ OTOCO = 'oneTriggerOneCancelOther'
109
+ end
110
+
111
+ module NotificationType
112
+ SNAPSHOT = 'snapshot'
113
+ UPDATE = 'update'
114
+ DATA = 'data'
115
+ COMMAND = 'command'
116
+ end
117
+
118
+ module SubscriptionMode
119
+ UPDATES = 'updates'
120
+ BATCHES = 'batches'
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+ require 'uri'
5
+ require 'json'
6
+ require 'base64'
7
+ require 'rest-client'
8
+ require 'date'
9
+ require_relative 'exceptions'
10
+
11
+ module Cryptomarket
12
+ # Builds a credential used by the cryptomarket server
13
+ class CredentialsFactory
14
+ def initialize(api_version:, api_key:, api_secret:, window: nil)
15
+ @api_version = api_version
16
+ @api_key = api_key
17
+ @api_secret = api_secret
18
+ @window = window
19
+ end
20
+
21
+ def get_credential(http_method, method, params)
22
+ timestamp = DateTime.now.strftime('%Q')
23
+ msg = build_credential_message(http_method, method, timestamp, params)
24
+ digest = OpenSSL::Digest.new 'sha256'
25
+ signature = OpenSSL::HMAC.hexdigest digest, @api_secret, msg
26
+ signed = "#{@api_key}:#{signature}:#{timestamp}"
27
+ signed += ":#{@window}" unless @window.nil?
28
+ encoded = Base64.encode64(signed).delete "\n"
29
+ "HS256 #{encoded}"
30
+ end
31
+
32
+ def build_credential_message(http_method, method, timestamp, params)
33
+ msg = http_method + @api_version + method
34
+ msg += if http_method.upcase == 'POST'
35
+ params
36
+ else
37
+ not_post_params(http_method, params)
38
+ end
39
+ msg += timestamp
40
+ msg += @window unless @window.nil?
41
+ msg
42
+ end
43
+
44
+ def not_post_params(http_method, params)
45
+ msg = ''
46
+ if !params.nil? && params.keys.any?
47
+ msg += '?' if http_method.upcase == 'GET'
48
+ msg += URI.encode_www_form(params)
49
+ end
50
+ msg
51
+ end
52
+ end
53
+ end
@@ -1,24 +1,23 @@
1
- module Cryptomarket
2
- class SDKException < ::StandardError
3
- end
1
+ # frozen_string_literal: true
4
2
 
5
- class APIException < SDKException
6
- def initialize(hash)
7
- @code = hash['code']
8
- @message = hash['message']
9
- @description = hash['description']
10
- end
3
+ module Cryptomarket
4
+ class SDKException < ::StandardError
5
+ end
11
6
 
12
- def code
13
- return @code
14
- end
7
+ # Exception representing an error from the server
8
+ class APIException < SDKException
9
+ def initialize(hash)
10
+ @code = hash['code']
11
+ raw_message = hash['message']
12
+ @description = hash.key?('description') ? hash['description'] : ''
13
+ @message = "#{self.class.name} (code=#{@code}): #{raw_message}. #{@description}"
14
+ super
15
+ end
15
16
 
16
- def message
17
- return @message
18
- end
17
+ attr_reader :code, :message, :description
19
18
 
20
- def description
21
- return @description
22
- end
19
+ def to_s
20
+ @message
23
21
  end
24
- end
22
+ end
23
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+ require 'uri'
5
+ require 'json'
6
+ require 'base64'
7
+ require 'rest-client'
8
+ require 'date'
9
+ require_relative 'exceptions'
10
+ require_relative 'credentials_factory'
11
+
12
+ def post?(method)
13
+ method.upcase == 'POST'
14
+ end
15
+
16
+ def get?(method)
17
+ method.upcase == 'GET'
18
+ end
19
+
20
+ def put?(method)
21
+ method.upcase == 'PUT'
22
+ end
23
+
24
+ def patch?(method)
25
+ method.upcase == 'PATCH'
26
+ end
27
+
28
+ module Cryptomarket
29
+ # Manager of http requests to the cryptomarket server
30
+ class HttpManager
31
+ @@API_URL = 'https://api.exchange.cryptomkt.com' # rubocop:disable Naming/VariableName,Style/ClassVars
32
+ @@API_VERSION = '/api/3/' # rubocop:disable Naming/VariableName,Style/ClassVars
33
+
34
+ def initialize(api_key:, api_secret:, window: nil)
35
+ @credential_factory = Cryptomarket::CredentialsFactory.new(
36
+ api_version: @@API_VERSION, api_key: api_key, api_secret: api_secret, window: window
37
+ )
38
+ end
39
+
40
+ def make_request(method:, endpoint:, params: nil, public: false)
41
+ uri = URI(@@API_URL + @@API_VERSION + endpoint)
42
+ payload = build_payload(params)
43
+ headers = build_headers(method, endpoint, payload, public)
44
+ if ((method.upcase == 'GET') || (method.upcase == 'PUT')) && !payload.nil?
45
+ uri.query = URI.encode_www_form payload
46
+ payload = nil
47
+ end
48
+ do_request(method, uri, payload, headers)
49
+ end
50
+
51
+ def make_post_request(method:, endpoint:, params: nil)
52
+ uri = URI(@@API_URL + @@API_VERSION + endpoint)
53
+ payload = build_payload(params)
54
+ do_request(method, uri, payload, build_post_headers(endpoint, payload))
55
+ end
56
+
57
+ def build_headers(method, endpoint, params, public)
58
+ return {} if public
59
+
60
+ { 'Authorization' => @credential_factory.get_credential(method.upcase, endpoint, params) }
61
+ end
62
+
63
+ def build_payload(params)
64
+ return nil if params.nil?
65
+
66
+ payload = params.compact
67
+ payload = Hash[payload.sort_by { |key, _val| key.to_s }] if payload.is_a?(Hash)
68
+ payload
69
+ end
70
+
71
+ def do_request(method, uri, payload, headers)
72
+ args = { method: method.downcase.to_sym, url: uri.to_s, headers: headers }
73
+ if post?(method) || patch?(method)
74
+ args[:payload] = post?(method) ? payload.to_json : payload
75
+ end
76
+ response = RestClient::Request.execute(**args)
77
+ handle_response(response)
78
+ rescue RestClient::ExceptionWithResponse => e
79
+ handle_response(e.response)
80
+ end
81
+
82
+ def build_post_headers(endpoint, params)
83
+ { 'Content-Type' => 'application/json',
84
+ 'Authorization' => @credential_factory.get_credential('POST'.upcase, endpoint, params.to_json) }
85
+ end
86
+
87
+ def handle_response(response)
88
+ result = response.body
89
+ parsed_result = JSON.parse result
90
+ if (response.code != 200) && !parsed_result['error'].nil?
91
+ error = parsed_result['error']
92
+ exception = Cryptomarket::APIException.new error
93
+ raise exception
94
+ end
95
+ parsed_result
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+ require_relative 'client_base'
5
+
6
+ module Cryptomarket
7
+ module Websocket
8
+ # A websocket client that authenticates at the moment of connection
9
+ class AuthClient < ClientBase
10
+ # Creates a new client
11
+ def initialize(url:, api_key:, api_secret:, subscription_keys:, window: nil)
12
+ @api_key = api_key
13
+ @api_secret = api_secret
14
+ @window = window
15
+ super url: url, subscription_keys: subscription_keys
16
+ @authed = false
17
+ end
18
+
19
+ def connected?
20
+ (super.connected? and @authed)
21
+ end
22
+
23
+ # connects via websocket to the exchange and authenticates it.
24
+ def connect
25
+ super
26
+ authenticate(proc { |err, _result|
27
+ raise err unless err.nil?
28
+
29
+ @authed = true
30
+ })
31
+ wait_authed
32
+ end
33
+
34
+ def wait_authed
35
+ current_try = 0
36
+ max_tries = 60
37
+ while !@authed && (current_try < max_tries)
38
+ current_try += 1
39
+ sleep(1)
40
+ end
41
+ end
42
+
43
+ # Authenticates the websocket
44
+ #
45
+ # https://api.exchange.cryptomkt.com/#socket-session-authentication
46
+ #
47
+ # +Proc+ +callback+:: Optional. A +Proc+ to call with the result data. It takes two arguments, err and result.
48
+ # err is None for successful calls, result is None for calls with error: Proc.new {|err, result| ...}
49
+
50
+ def authenticate(callback = nil)
51
+ timestamp = Time.now.to_i * 1000
52
+ digest = OpenSSL::Digest.new 'sha256'
53
+ message = timestamp.to_s
54
+ message += @window.to_s unless @window.nil?
55
+ signature = OpenSSL::HMAC.hexdigest digest, @api_secret, message.to_s
56
+ params = build_auth_payload timestamp, signature
57
+ request('login', callback, params)
58
+ end
59
+
60
+ def build_auth_payload(timestamp, signature)
61
+ params = {
62
+ 'type' => 'HS256',
63
+ 'api_key' => @api_key,
64
+ 'timestamp' => timestamp,
65
+ 'signature' => signature
66
+ }
67
+ params['window'] = @window unless @window.nil?
68
+ params
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'reusable_callback'
4
+
5
+ module Cryptomarket
6
+ module Websocket
7
+ # A cache store for callbacks. uses reusable callbacks, so it cans use a callback for more than one time.
8
+ # Each callback stored
9
+ class CallbackCache
10
+ def initialize
11
+ @reusable_callbacks = {}
12
+ @subscription_callbacks = {}
13
+ @next_id = 1
14
+ end
15
+
16
+ def _get_next_id
17
+ next_id = @next_id
18
+ @next_id += 1
19
+ @next_id = 1 if @next_id.negative?
20
+ next_id
21
+ end
22
+
23
+ def store_callback(callback, call_count = 1)
24
+ id = _get_next_id
25
+ @reusable_callbacks[id] = ReusableCallback.new(callback, call_count)
26
+ id
27
+ end
28
+
29
+ def get_callback(id)
30
+ return nil unless @reusable_callbacks.key? id
31
+
32
+ callback, done_using = @reusable_callbacks[id].get_callback
33
+ @reusable_callbacks.delete(id) if done_using
34
+ callback
35
+ end
36
+
37
+ def store_subscription_callback(key, callback)
38
+ @subscription_callbacks[key] = callback
39
+ end
40
+
41
+ def get_subscription_callback(key)
42
+ return nil unless @subscription_callbacks.key? key
43
+
44
+ @subscription_callbacks[key]
45
+ end
46
+
47
+ def delete_subscription_callback(key)
48
+ return unless @subscription_callbacks.key? key
49
+
50
+ @subscription_callbacks.delete(key)
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'callback_cache'
4
+ require_relative 'ws_manager'
5
+ require_relative '../exceptions'
6
+
7
+ module Cryptomarket
8
+ module Websocket
9
+ # websockt client able to handle requests and subscriptions
10
+ class ClientBase
11
+ def initialize(url:, subscription_keys:, on_connect: -> {}, on_error: ->(error) {}, on_close: -> {})
12
+ @subscription_keys = subscription_keys
13
+ @callback_cache = CallbackCache.new
14
+ @ws_manager = WSManager.new self, url: url
15
+ @on_connect = on_connect
16
+ @on_error = on_error
17
+ @on_close = on_close
18
+ end
19
+
20
+ def connected?
21
+ @ws_manager.connected?
22
+ end
23
+
24
+ # connects via websocket to the exchange, it blocks until the connection is stablished
25
+ def connect
26
+ @ws_manager.connect
27
+ sleep(1) until @ws_manager.connected?
28
+ end
29
+
30
+ def on_open
31
+ @on_connect.call
32
+ end
33
+
34
+ def on_connect=(callback = nil, &block)
35
+ callback ||= block
36
+ @on_connect = callback
37
+ end
38
+
39
+ def on_connect
40
+ @on_connect.call
41
+ end
42
+
43
+ def on_error=(callback = nil, &block)
44
+ callback ||= block
45
+ @on_error = callback
46
+ end
47
+
48
+ def on_error(error)
49
+ @on_error.call(error)
50
+ end
51
+
52
+ def on_close=(callback = nil, &block)
53
+ callback ||= block
54
+ @on_close = callback
55
+ end
56
+
57
+ def on_close
58
+ @on_close.call
59
+ end
60
+
61
+ def close
62
+ @ws_manager.close
63
+ end
64
+
65
+ def send_subscription(method, callback, params, result_callback)
66
+ @callback_cache.store_subscription_callback(@subscription_keys[method][0], callback)
67
+ store_callback_and_send(method, params, result_callback)
68
+ end
69
+
70
+ def send_unsubscription(method, callback, params)
71
+ @callback_cache.delete_subscription_callback(@subscription_keys[method][0])
72
+ store_callback_and_send(method, params, callback)
73
+ end
74
+
75
+ def request(method, callback, params = {}, call_count = 1)
76
+ store_callback_and_send(method, params, callback, call_count)
77
+ end
78
+
79
+ def store_callback_and_send(method, params, callback_to_store = nil, call_count = 1)
80
+ params = params.compact unless params.nil?
81
+ payload = { 'method' => method, 'params' => params }
82
+ unless callback_to_store.nil?
83
+ id = @callback_cache.store_callback(callback_to_store, call_count)
84
+ payload['id'] = id
85
+ end
86
+ @ws_manager.send(payload)
87
+ end
88
+
89
+ def handle(message)
90
+ if message.key? 'id'
91
+ handle_response(message)
92
+ elsif message.key? 'method'
93
+ handle_notification(message)
94
+ end
95
+ end
96
+
97
+ def handle_notification(notification)
98
+ method = notification['method']
99
+ method_data = @subscription_keys[method]
100
+ notification_type = method_data[1]
101
+ callback = @callback_cache.get_subscription_callback(method_data[0])
102
+ return if callback.nil?
103
+
104
+ callback.call(notification['params'], notification_type)
105
+ end
106
+
107
+ def get_callback_for_response(response)
108
+ id = response['id']
109
+ return if id.nil?
110
+
111
+ @callback_cache.get_callback(id)
112
+ end
113
+
114
+ def handle_response(response)
115
+ callback = get_callback_for_response(response)
116
+ return if callback.nil?
117
+
118
+ if response.key? 'error'
119
+ callback.call(Cryptomarket::APIException.new(response['error']), nil)
120
+ nil
121
+ end
122
+ handle_good_response(response, callback)
123
+ end
124
+
125
+ def handle_good_response(response, callback)
126
+ result = response['result']
127
+ if result.is_a?(Hash) && result.key?('data')
128
+ callback.call(nil, result['data'])
129
+ else
130
+ callback.call(nil, result)
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end