orionx-sdk-ruby 1.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.
data/lib/orionx/api.rb ADDED
@@ -0,0 +1,168 @@
1
+ require "faraday"
2
+ require "json"
3
+ require "openssl"
4
+
5
+ module OrionX
6
+ class API
7
+ attr_reader :api_key, :api_secret, :api_endpoint, :config, :logger
8
+
9
+ def initialize(api_key: nil, api_secret: nil, api_endpoint: nil, config: nil)
10
+ @config = config || OrionX.configuration
11
+ @api_key = api_key || @config.api_key
12
+ @api_secret = api_secret || @config.api_secret
13
+ @api_endpoint = api_endpoint || @config.api_endpoint
14
+ @logger = @config.logger
15
+
16
+ validate_credentials!
17
+ setup_connection
18
+ end
19
+
20
+ def call(query, variables = {})
21
+ retries = 0
22
+ begin
23
+ @logger.debug("GraphQL Query: #{query}")
24
+ @logger.debug("Variables: #{variables.inspect}")
25
+
26
+ response = execute_request(query, variables)
27
+ handle_response(response)
28
+ rescue Faraday::TimeoutError => e
29
+ @logger.error("Request timeout: #{e.message}")
30
+ raise OrionX::NetworkError, "Request timeout: #{e.message}"
31
+ rescue Faraday::ConnectionFailed => e
32
+ @logger.error("Connection failed: #{e.message}")
33
+ raise OrionX::NetworkError, "Connection failed: #{e.message}"
34
+ rescue OrionX::APIError => e
35
+ if retries < @config.retries && retryable_error?(e)
36
+ retries += 1
37
+ @logger.warn("Retrying request (attempt #{retries}/#{@config.retries}): #{e.message}")
38
+ sleep(backoff_delay(retries))
39
+ retry
40
+ else
41
+ raise e
42
+ end
43
+ rescue => e
44
+ @logger.error("Unexpected error: #{e.message}")
45
+ @logger.debug("Backtrace: #{e.backtrace.join("\n")}")
46
+ raise OrionX::Error, "Unexpected error: #{e.message}"
47
+ end
48
+ end
49
+
50
+ private
51
+
52
+ def validate_credentials!
53
+ raise OrionX::AuthenticationError, "API key is required" if @api_key.nil? || @api_key.empty?
54
+ raise OrionX::AuthenticationError, "API secret is required" if @api_secret.nil? || @api_secret.empty?
55
+ raise OrionX::ValidationError, "API endpoint is required" if @api_endpoint.nil? || @api_endpoint.empty?
56
+ end
57
+
58
+ def setup_connection
59
+ @connection = Faraday.new(@api_endpoint) do |conn|
60
+ conn.request :json
61
+ conn.response :json, content_type: /\bjson$/
62
+ conn.options.timeout = @config.timeout
63
+ conn.adapter Faraday.default_adapter
64
+
65
+ if @config.debug?
66
+ conn.response :logger, @logger.logger, bodies: true, headers: true
67
+ end
68
+ end
69
+
70
+ @logger.info("API connection established to #{@api_endpoint}")
71
+ end
72
+
73
+ def execute_request(query, variables)
74
+ timestamp = Time.now.to_f
75
+ body = JSON.generate({ query: query, variables: variables })
76
+ signature = generate_signature(timestamp.to_s, body)
77
+
78
+ headers = {
79
+ "X-ORIONX-TIMESTAMP" => timestamp.to_s,
80
+ "X-ORIONX-APIKEY" => @api_key,
81
+ "X-ORIONX-SIGNATURE" => signature,
82
+ "Content-Type" => "application/json"
83
+ }
84
+
85
+ @logger.debug("Request headers: #{headers.inspect}")
86
+ @logger.debug("Request body: #{body}")
87
+
88
+ response = @connection.post do |req|
89
+ req.headers.merge!(headers)
90
+ req.body = body
91
+ end
92
+
93
+ @logger.debug("Response status: #{response.status}")
94
+ @logger.debug("Response body: #{response.body.inspect}")
95
+
96
+ response
97
+ end
98
+
99
+ def generate_signature(timestamp, body)
100
+ data = timestamp + body
101
+ signature = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new("sha512"), @api_secret, data)
102
+ @logger.debug("Generated signature for timestamp #{timestamp}")
103
+ signature
104
+ end
105
+
106
+ def handle_response(response)
107
+ case response.status
108
+ when 200
109
+ handle_success_response(response.body)
110
+ when 401
111
+ @logger.error("Authentication failed")
112
+ raise OrionX::AuthenticationError, "Authentication failed: Invalid API credentials"
113
+ when 429
114
+ @logger.error("Rate limit exceeded")
115
+ raise OrionX::RateLimitError, "Rate limit exceeded"
116
+ when 500
117
+ @logger.error("Internal server error")
118
+ raise OrionX::APIError, "Internal server error"
119
+ else
120
+ @logger.error("Unexpected response status: #{response.status}")
121
+ raise OrionX::APIError, "HTTP #{response.status}: #{response.body}"
122
+ end
123
+ end
124
+
125
+ def handle_success_response(body)
126
+ return {} if body.nil? || body.empty?
127
+
128
+ if body.is_a?(String)
129
+ begin
130
+ parsed_body = JSON.parse(body)
131
+ rescue JSON::ParserError => e
132
+ @logger.error("Failed to parse response JSON: #{e.message}")
133
+ raise OrionX::APIError, "Invalid JSON response: #{e.message}"
134
+ end
135
+ else
136
+ parsed_body = body
137
+ end
138
+
139
+ if parsed_body.key?("errors") && !parsed_body["errors"].empty?
140
+ error_message = parsed_body["errors"].first["message"]
141
+ @logger.error("GraphQL error: #{error_message}")
142
+
143
+ # Check if we have partial data
144
+ if parsed_body.key?("data") && !parsed_body["data"].nil?
145
+ errors_count = parsed_body["errors"].length
146
+ data_keys_count = parsed_body["data"].keys.length
147
+
148
+ # Return data if errors don't affect all fields
149
+ return parsed_body["data"] if errors_count < data_keys_count
150
+ end
151
+
152
+ raise OrionX::APIError, "GraphQL error: #{error_message}"
153
+ end
154
+
155
+ parsed_body["data"] || {}
156
+ end
157
+
158
+ def retryable_error?(error)
159
+ error.is_a?(OrionX::RateLimitError) ||
160
+ (error.is_a?(OrionX::APIError) && error.message.include?("500"))
161
+ end
162
+
163
+ def backoff_delay(attempt)
164
+ # Exponential backoff: 1s, 2s, 4s
165
+ 2 ** (attempt - 1)
166
+ end
167
+ end
168
+ end
@@ -0,0 +1,55 @@
1
+ module OrionX
2
+ class Client
3
+ attr_reader :user, :orders, :accounts, :markets, :transactions, :api, :logger
4
+
5
+ def initialize(api_key: nil, api_secret: nil, api_endpoint: nil, debug: false)
6
+ # Configure the SDK if parameters are provided
7
+ if api_key || api_secret || api_endpoint || debug
8
+ OrionX.configure do |config|
9
+ config.api_key = api_key if api_key
10
+ config.api_secret = api_secret if api_secret
11
+ config.api_endpoint = api_endpoint if api_endpoint
12
+ config.debug = debug
13
+ end
14
+ end
15
+
16
+ @api = OrionX::API.new
17
+ @logger = @api.logger
18
+
19
+ # Initialize endpoint modules
20
+ @user = OrionX::Endpoints::User.new(@api)
21
+ @orders = OrionX::Endpoints::Orders.new(@api)
22
+ @accounts = OrionX::Endpoints::Accounts.new(@api)
23
+ @markets = OrionX::Endpoints::Markets.new(@api)
24
+ @transactions = OrionX::Endpoints::Transactions.new(@api)
25
+
26
+ @logger.info("OrionX Client initialized successfully")
27
+ end
28
+
29
+ # Convenience method to get current user information
30
+ def me
31
+ @user.me
32
+ end
33
+
34
+ # Health check method
35
+ def ping
36
+ begin
37
+ me
38
+ { status: "ok", message: "Connection successful" }
39
+ rescue => e
40
+ { status: "error", message: e.message }
41
+ end
42
+ end
43
+
44
+ # Enable/disable debug mode
45
+ def debug=(enabled)
46
+ OrionX.configuration.debug = enabled
47
+ @logger.level = enabled ? ::Logger::DEBUG : ::Logger::INFO
48
+ @logger.info("Debug mode #{enabled ? 'enabled' : 'disabled'}")
49
+ end
50
+
51
+ def debug?
52
+ OrionX.configuration.debug?
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,43 @@
1
+ require "logger"
2
+
3
+ module OrionX
4
+ class Configuration
5
+ attr_accessor :api_key, :api_secret, :api_endpoint, :debug, :logger, :timeout, :retries
6
+
7
+ def initialize
8
+ @api_key = nil
9
+ @api_secret = nil
10
+ @api_endpoint = "https://api.orionx.com/graphql"
11
+ @debug = false
12
+ @logger = nil
13
+ @timeout = 30
14
+ @retries = 3
15
+ end
16
+
17
+ def valid?
18
+ !api_key.nil? && !api_secret.nil? && !api_endpoint.nil?
19
+ end
20
+
21
+ def logger
22
+ @logger ||= OrionX::Logger.new(level: debug? ? ::Logger::DEBUG : ::Logger::INFO)
23
+ end
24
+
25
+ def debug?
26
+ @debug
27
+ end
28
+ end
29
+
30
+ class << self
31
+ attr_writer :configuration
32
+
33
+ def configuration
34
+ @configuration ||= Configuration.new
35
+ end
36
+
37
+ def configure
38
+ yield(configuration)
39
+ configuration.logger.debug("OrionX SDK configured with endpoint: #{configuration.api_endpoint}")
40
+ configuration.logger.debug("Debug mode: #{configuration.debug?}")
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,103 @@
1
+ module OrionX
2
+ module Endpoints
3
+ class Accounts
4
+ def initialize(api_client)
5
+ @api = api_client
6
+ @logger = api_client.logger
7
+ end
8
+
9
+ # Get a specific account by currency code
10
+ def get_account(currency_code)
11
+ raise OrionX::ValidationError, "Currency code cannot be nil or empty" if currency_code.nil? || currency_code.empty?
12
+
13
+ query = <<~GRAPHQL
14
+ query sdk_getAccount($assetId: ID!) {
15
+ wallet(code: $assetId) {
16
+ _id
17
+ currency {
18
+ code
19
+ units
20
+ }
21
+ balance
22
+ availableBalance
23
+ availableNetworks {
24
+ code
25
+ }
26
+ balanceUSD
27
+ balanceCLP
28
+ }
29
+ }
30
+ GRAPHQL
31
+
32
+ @logger.debug("Fetching account for currency: #{currency_code}")
33
+ response = @api.call(query, { assetId: currency_code })
34
+
35
+ result = response.dig("wallet")
36
+ @logger.info("Account for #{currency_code} retrieved successfully") if result
37
+ result
38
+ end
39
+
40
+ # Get all accounts for the user
41
+ def get_accounts
42
+ query = <<~GRAPHQL
43
+ query sdk_getAccounts {
44
+ me {
45
+ wallets {
46
+ _id
47
+ currency {
48
+ code
49
+ units
50
+ }
51
+ balance
52
+ availableBalance
53
+ availableNetworks {
54
+ code
55
+ }
56
+ balanceUSD
57
+ balanceCLP
58
+ }
59
+ }
60
+ }
61
+ GRAPHQL
62
+
63
+ @logger.debug("Fetching all accounts")
64
+ response = @api.call(query)
65
+
66
+ result = response.dig("me", "wallets")
67
+ @logger.info("All accounts retrieved: #{result&.length || 0} accounts") if result
68
+ result
69
+ end
70
+
71
+ # Get account balance for a specific currency
72
+ def get_balance(currency_code)
73
+ account = get_account(currency_code)
74
+ return nil unless account
75
+
76
+ {
77
+ currency: account.dig("currency", "code"),
78
+ balance: account["balance"],
79
+ available_balance: account["availableBalance"],
80
+ balance_usd: account["balanceUSD"],
81
+ balance_clp: account["balanceCLP"]
82
+ }
83
+ end
84
+
85
+ # Get balances for all currencies
86
+ def get_balances
87
+ accounts = get_accounts
88
+ return [] unless accounts
89
+
90
+ accounts.map do |account|
91
+ {
92
+ currency: account.dig("currency", "code"),
93
+ balance: account["balance"],
94
+ available_balance: account["availableBalance"],
95
+ balance_usd: account["balanceUSD"],
96
+ balance_clp: account["balanceCLP"],
97
+ wallet_id: account["_id"]
98
+ }
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,128 @@
1
+ module OrionX
2
+ module Endpoints
3
+ class Markets
4
+ def initialize(api_client)
5
+ @api = api_client
6
+ @logger = api_client.logger
7
+ end
8
+
9
+ # Get information for a specific market
10
+ def get_market(market_code)
11
+ raise OrionX::ValidationError, "Market code cannot be nil or empty" if market_code.nil? || market_code.empty?
12
+
13
+ query = <<~GRAPHQL
14
+ query sdk_market($code: ID) {
15
+ market(code: $code) {
16
+ code
17
+ name
18
+ mainCurrency {
19
+ code
20
+ name
21
+ units
22
+ }
23
+ secondaryCurrency {
24
+ code
25
+ name
26
+ units
27
+ }
28
+ lastTrade {
29
+ price
30
+ }
31
+ }
32
+ }
33
+ GRAPHQL
34
+
35
+ @logger.debug("Fetching market data for: #{market_code}")
36
+ response = @api.call(query, { code: market_code })
37
+
38
+ result = response.dig("market")
39
+ @logger.info("Market #{market_code} data retrieved successfully") if result
40
+ result
41
+ end
42
+
43
+ # Get all available markets
44
+ def get_markets
45
+ query = <<~GRAPHQL
46
+ query sdk_markets {
47
+ markets {
48
+ code
49
+ name
50
+ mainCurrency {
51
+ code
52
+ name
53
+ units
54
+ }
55
+ secondaryCurrency {
56
+ code
57
+ name
58
+ units
59
+ }
60
+ lastTrade {
61
+ price
62
+ }
63
+ }
64
+ }
65
+ GRAPHQL
66
+
67
+ @logger.debug("Fetching all markets")
68
+ response = @api.call(query)
69
+
70
+ result = response.dig("markets")
71
+ @logger.info("All markets retrieved: #{result&.length || 0} markets") if result
72
+ result
73
+ end
74
+
75
+ # Get order book for a specific market
76
+ def get_orderbook(market_code, limit: 50)
77
+ raise OrionX::ValidationError, "Market code cannot be nil or empty" if market_code.nil? || market_code.empty?
78
+ raise OrionX::ValidationError, "Limit must be positive" if limit <= 0
79
+
80
+ query = <<~GRAPHQL
81
+ query sdk_marketOrderBook($marketCode: ID!, $limit: Int) {
82
+ marketOrderBook(marketCode: $marketCode, limit: $limit) {
83
+ sell {
84
+ amount
85
+ limitPrice
86
+ }
87
+ buy {
88
+ amount
89
+ limitPrice
90
+ }
91
+ spread
92
+ mid
93
+ }
94
+ }
95
+ GRAPHQL
96
+
97
+ @logger.debug("Fetching orderbook for #{market_code} (limit: #{limit})")
98
+ response = @api.call(query, { marketCode: market_code, limit: limit })
99
+
100
+ result = response.dig("marketOrderBook")
101
+ if result
102
+ @logger.info("Orderbook for #{market_code} retrieved - Buy orders: #{result.dig('buy')&.length || 0}, Sell orders: #{result.dig('sell')&.length || 0}")
103
+ end
104
+ result
105
+ end
106
+
107
+ # Get market statistics
108
+ def get_market_stats(market_code)
109
+ market = get_market(market_code)
110
+ orderbook = get_orderbook(market_code, limit: 1)
111
+
112
+ return nil unless market && orderbook
113
+
114
+ {
115
+ code: market["code"],
116
+ name: market["name"],
117
+ last_price: market.dig("lastTrade", "price"),
118
+ spread: orderbook["spread"],
119
+ mid_price: orderbook["mid"],
120
+ best_bid: orderbook.dig("buy", 0, "limitPrice"),
121
+ best_ask: orderbook.dig("sell", 0, "limitPrice"),
122
+ main_currency: market.dig("mainCurrency", "code"),
123
+ secondary_currency: market.dig("secondaryCurrency", "code")
124
+ }
125
+ end
126
+ end
127
+ end
128
+ end