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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +64 -0
- data/CONTRIBUTING.md +152 -0
- data/Gemfile +15 -0
- data/README.md +408 -0
- data/Rakefile +10 -0
- data/lib/orionx/api.rb +168 -0
- data/lib/orionx/client.rb +55 -0
- data/lib/orionx/configuration.rb +43 -0
- data/lib/orionx/endpoints/accounts.rb +103 -0
- data/lib/orionx/endpoints/markets.rb +128 -0
- data/lib/orionx/endpoints/orders.rb +328 -0
- data/lib/orionx/endpoints/transactions.rb +269 -0
- data/lib/orionx/endpoints/user.rb +54 -0
- data/lib/orionx/logger.rb +63 -0
- data/lib/orionx/version.rb +3 -0
- data/lib/orionx.rb +20 -0
- data/spec/client_spec.rb +89 -0
- data/spec/configuration_spec.rb +71 -0
- data/spec/spec_helper.rb +59 -0
- metadata +218 -0
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
|