cryptomarket-sdk 1.0.0 → 3.0.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.
@@ -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