honeymaker 0.12.4-aarch64-linux

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 (136) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/native-gems.yml +100 -0
  3. data/.github/workflows/release.yml +36 -0
  4. data/.github/workflows/test.yml +33 -0
  5. data/LICENSE +21 -0
  6. data/README.md +219 -0
  7. data/Rakefile +78 -0
  8. data/crates/honeymaker/tests/vectors/decimal.json +146 -0
  9. data/crates/honeymaker/tests/vectors/kraken_add_order_wire.json +74 -0
  10. data/crates/honeymaker/tests/vectors/kraken_signing.json +3314 -0
  11. data/crates/honeymaker/tests/vectors/kraken_trades_pager.json +1583 -0
  12. data/lib/honeymaker/3.4/honeymaker_native.so +0 -0
  13. data/lib/honeymaker/4.0/honeymaker_native.so +0 -0
  14. data/lib/honeymaker/client.rb +139 -0
  15. data/lib/honeymaker/clients/binance.rb +687 -0
  16. data/lib/honeymaker/clients/binance_us.rb +42 -0
  17. data/lib/honeymaker/clients/bingx.rb +242 -0
  18. data/lib/honeymaker/clients/bitget.rb +318 -0
  19. data/lib/honeymaker/clients/bitrue.rb +245 -0
  20. data/lib/honeymaker/clients/bitvavo.rb +244 -0
  21. data/lib/honeymaker/clients/bybit.rb +308 -0
  22. data/lib/honeymaker/clients/coinbase.rb +326 -0
  23. data/lib/honeymaker/clients/gemini.rb +189 -0
  24. data/lib/honeymaker/clients/hyperliquid.rb +232 -0
  25. data/lib/honeymaker/clients/kraken.rb +379 -0
  26. data/lib/honeymaker/clients/kraken_futures.rb +78 -0
  27. data/lib/honeymaker/clients/kucoin.rb +310 -0
  28. data/lib/honeymaker/clients/mexc.rb +249 -0
  29. data/lib/honeymaker/exchange.rb +129 -0
  30. data/lib/honeymaker/exchanges/binance.rb +55 -0
  31. data/lib/honeymaker/exchanges/binance_us.rb +9 -0
  32. data/lib/honeymaker/exchanges/bingx.rb +56 -0
  33. data/lib/honeymaker/exchanges/bitget.rb +52 -0
  34. data/lib/honeymaker/exchanges/bitrue.rb +53 -0
  35. data/lib/honeymaker/exchanges/bitvavo.rb +65 -0
  36. data/lib/honeymaker/exchanges/bybit.rb +57 -0
  37. data/lib/honeymaker/exchanges/coinbase.rb +69 -0
  38. data/lib/honeymaker/exchanges/gemini.rb +134 -0
  39. data/lib/honeymaker/exchanges/hyperliquid.rb +67 -0
  40. data/lib/honeymaker/exchanges/kraken.rb +122 -0
  41. data/lib/honeymaker/exchanges/kucoin.rb +52 -0
  42. data/lib/honeymaker/exchanges/mexc.rb +56 -0
  43. data/lib/honeymaker/native/kraken.rb +263 -0
  44. data/lib/honeymaker/native.rb +42 -0
  45. data/lib/honeymaker/result.rb +46 -0
  46. data/lib/honeymaker/utils.rb +39 -0
  47. data/lib/honeymaker/version.rb +5 -0
  48. data/lib/honeymaker.rb +97 -0
  49. data/script/parity/kraken_client_live.rb +191 -0
  50. data/script/parity/kraken_live.rb +70 -0
  51. data/script/parity/kraken_transport.rb +604 -0
  52. data/script/parity/resolve.c +49 -0
  53. data/script/release/push_gems.sh +28 -0
  54. data/script/vectors/kraken.rb +50 -0
  55. data/script/vectors/kraken_stage2.rb +146 -0
  56. data/sig/honeymaker.rbs +4 -0
  57. data/test/fixtures/binance_book_ticker.json +7 -0
  58. data/test/fixtures/binance_exchange_info.json +30 -0
  59. data/test/fixtures/bingx_symbols.json +26 -0
  60. data/test/fixtures/bingx_ticker.json +12 -0
  61. data/test/fixtures/bitget_symbols.json +28 -0
  62. data/test/fixtures/bitget_tickers.json +14 -0
  63. data/test/fixtures/bitmart_symbols.json +26 -0
  64. data/test/fixtures/bitmart_ticker.json +12 -0
  65. data/test/fixtures/bitrue_book_ticker.json +7 -0
  66. data/test/fixtures/bitrue_exchange_info.json +34 -0
  67. data/test/fixtures/bitvavo_book_ticker.json +7 -0
  68. data/test/fixtures/bitvavo_markets.json +30 -0
  69. data/test/fixtures/bybit_instruments.json +23 -0
  70. data/test/fixtures/bybit_tickers.json +19 -0
  71. data/test/fixtures/coinbase_product.json +7 -0
  72. data/test/fixtures/coinbase_products.json +24 -0
  73. data/test/fixtures/gemini_pubticker.json +10 -0
  74. data/test/fixtures/gemini_symbol_detail.json +10 -0
  75. data/test/fixtures/gemini_symbols.json +1 -0
  76. data/test/fixtures/hyperliquid_all_mids.json +5 -0
  77. data/test/fixtures/hyperliquid_spot_meta.json +12 -0
  78. data/test/fixtures/kraken_asset_pairs.json +43 -0
  79. data/test/fixtures/kraken_ticker.json +16 -0
  80. data/test/fixtures/kucoin_orderbook_level1.json +10 -0
  81. data/test/fixtures/kucoin_symbols.json +17 -0
  82. data/test/fixtures/mexc_book_ticker.json +7 -0
  83. data/test/fixtures/mexc_exchange_info.json +64 -0
  84. data/test/honeymaker/client_test.rb +90 -0
  85. data/test/honeymaker/clients/binance_client_test.rb +151 -0
  86. data/test/honeymaker/clients/binance_us_client_test.rb +25 -0
  87. data/test/honeymaker/clients/bingx_client_test.rb +106 -0
  88. data/test/honeymaker/clients/bitget_client_test.rb +146 -0
  89. data/test/honeymaker/clients/bitrue_client_test.rb +69 -0
  90. data/test/honeymaker/clients/bitvavo_client_test.rb +156 -0
  91. data/test/honeymaker/clients/bybit_client_test.rb +137 -0
  92. data/test/honeymaker/clients/coinbase_client_test.rb +153 -0
  93. data/test/honeymaker/clients/gemini_client_test.rb +100 -0
  94. data/test/honeymaker/clients/honeymaker_client_registry_test.rb +39 -0
  95. data/test/honeymaker/clients/hyperliquid_client_test.rb +292 -0
  96. data/test/honeymaker/clients/kraken_client_test.rb +255 -0
  97. data/test/honeymaker/clients/kraken_futures_client_test.rb +54 -0
  98. data/test/honeymaker/clients/kucoin_client_test.rb +149 -0
  99. data/test/honeymaker/clients/mexc_client_test.rb +98 -0
  100. data/test/honeymaker/clients/validation_test.rb +167 -0
  101. data/test/honeymaker/error_chain_test.rb +133 -0
  102. data/test/honeymaker/exchange_test.rb +138 -0
  103. data/test/honeymaker/exchanges/binance_test.rb +83 -0
  104. data/test/honeymaker/exchanges/binance_us_test.rb +53 -0
  105. data/test/honeymaker/exchanges/bingx_test.rb +65 -0
  106. data/test/honeymaker/exchanges/bitget_test.rb +65 -0
  107. data/test/honeymaker/exchanges/bitrue_test.rb +65 -0
  108. data/test/honeymaker/exchanges/bitvavo_test.rb +65 -0
  109. data/test/honeymaker/exchanges/bybit_test.rb +55 -0
  110. data/test/honeymaker/exchanges/coinbase_test.rb +106 -0
  111. data/test/honeymaker/exchanges/gemini_catalogue_test.rb +118 -0
  112. data/test/honeymaker/exchanges/gemini_test.rb +184 -0
  113. data/test/honeymaker/exchanges/hyperliquid_test.rb +64 -0
  114. data/test/honeymaker/exchanges/kraken_test.rb +191 -0
  115. data/test/honeymaker/exchanges/kucoin_test.rb +55 -0
  116. data/test/honeymaker/exchanges/mexc_test.rb +97 -0
  117. data/test/honeymaker/honeymaker_test.rb +27 -0
  118. data/test/honeymaker/result_test.rb +42 -0
  119. data/test/honeymaker/utils_test.rb +64 -0
  120. data/test/native/backend_switch_test.rb +53 -0
  121. data/test/native/convert_test.rb +137 -0
  122. data/test/native/gem_isolation_test.rb +17 -0
  123. data/test/native/loader_test.rb +29 -0
  124. data/test/native/lockstep_test.rb +23 -0
  125. data/test/native/signing_smoke_test.rb +28 -0
  126. data/test/parity/kraken_client_parity_test.rb +398 -0
  127. data/test/parity/kraken_exchange_parity_test.rb +172 -0
  128. data/test/release/branch_hardening_test.rb +34 -0
  129. data/test/release/push_gems_test.rb +79 -0
  130. data/test/smoke.rb +24 -0
  131. data/test/support/fixture_helper.rb +13 -0
  132. data/test/support/kraken_signing_smoke.rb +21 -0
  133. data/test/support/parity_helper.rb +62 -0
  134. data/test/support/venue_double.rb +100 -0
  135. data/test/test_helper.rb +9 -0
  136. metadata +254 -0
@@ -0,0 +1,379 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+ require "uri"
5
+
6
+ module Honeymaker
7
+ module Clients
8
+ class Kraken < Client
9
+ URL = "https://api.kraken.com"
10
+ UNREADABLE = "Kraken: unreadable response"
11
+
12
+ RATE_LIMITS = { default: 1000, orders: 1000 }.freeze
13
+
14
+ ASSET_MAP = {
15
+ "ZUSD" => "USD", "ZEUR" => "EUR", "ZGBP" => "GBP",
16
+ "ZJPY" => "JPY", "ZCHF" => "CHF", "ZCAD" => "CAD",
17
+ "ZAUD" => "AUD", "XXBT" => "XBT", "XETH" => "ETH",
18
+ "XXDG" => "XDG"
19
+ }.freeze
20
+
21
+ def query_orders_info(txid:, trades: nil, userref: nil, consolidate_taker: true)
22
+ result = post_private("/0/private/QueryOrders", {
23
+ nonce: nonce, trades: trades, userref: userref,
24
+ txid: txid, consolidate_taker: consolidate_taker
25
+ })
26
+ return result if result.failure?
27
+ return unreadable unless result.data.is_a?(Hash)
28
+
29
+ errors = result.data["error"]
30
+ return Result::Failure.new(*errors) if errors.is_a?(Array) && errors.any?
31
+
32
+ orders = {}
33
+ (result.data["result"] || {}).each do |order_id, raw|
34
+ orders[order_id] = normalize_order(order_id, raw)
35
+ end
36
+ Result::Success.new(orders)
37
+ end
38
+
39
+ def add_order(ordertype:, type:, volume:, pair:, userref: nil, cl_ord_id: nil,
40
+ displayvol: nil, price: nil, price2: nil, trigger: nil, leverage: nil,
41
+ reduce_only: nil, stptype: nil, oflags: [], timeinforce: nil,
42
+ starttm: nil, expiretm: nil, close: nil, close_price: nil,
43
+ close_price2: nil, deadline: nil, validate: nil)
44
+ result = post_private("/0/private/AddOrder", {
45
+ "nonce" => nonce, "ordertype" => ordertype, "type" => type,
46
+ "volume" => volume, "pair" => pair, "userref" => userref,
47
+ "cl_ord_id" => cl_ord_id, "displayvol" => displayvol,
48
+ "price" => price, "price2" => price2, "trigger" => trigger,
49
+ "leverage" => leverage, "reduce_only" => reduce_only,
50
+ "stptype" => stptype,
51
+ "oflags" => oflags.any? ? oflags.join(",") : nil,
52
+ "timeinforce" => timeinforce, "starttm" => starttm,
53
+ "expiretm" => expiretm, "close[ordertype]" => close,
54
+ "close[price]" => close_price, "close[price2]" => close_price2,
55
+ "deadline" => deadline, "validate" => validate
56
+ })
57
+ return result if result.failure?
58
+ return unreadable unless result.data.is_a?(Hash)
59
+
60
+ errors = result.data["error"]
61
+ return Result::Failure.new(*errors) if errors.is_a?(Array) && errors.any?
62
+
63
+ txid = (result.data.dig("result", "txid") || []).first
64
+ Result::Success.new({ order_id: txid, raw: result.data })
65
+ end
66
+
67
+ def cancel_order(txid: nil, cl_ord_id: nil)
68
+ post_private("/0/private/CancelOrder", { nonce: nonce, txid: txid, cl_ord_id: cl_ord_id })
69
+ end
70
+
71
+ # aclass_base: "all" also returns tokenized equities (xStocks); omitted, Kraken serves only the
72
+ # "currency" class. Note the asset-class parameter is spelled differently per endpoint —
73
+ # aclass_base here, asset_class on Ticker/OHLC/Depth/AddOrder.
74
+ def get_tradable_asset_pairs(pairs: nil, info: nil, country_code: nil, aclass_base: nil)
75
+ get_public("/0/public/AssetPairs", {
76
+ pair: pairs ? pairs.join(",") : nil, info: info, country_code: country_code,
77
+ aclass_base: aclass_base
78
+ })
79
+ end
80
+
81
+ def get_asset_info(assets: nil, aclass: nil)
82
+ get_public("/0/public/Assets", { asset: assets ? assets.join(",") : nil, aclass: aclass })
83
+ end
84
+
85
+ def get_ticker_information(pair: nil)
86
+ get_public("/0/public/Ticker", { pair: pair })
87
+ end
88
+
89
+ def get_extended_balance
90
+ post_private("/0/private/BalanceEx", { nonce: nonce })
91
+ end
92
+
93
+ # The key's own permission flags (query-funds, withdraw-funds, modify-trades, query-ledger, ...).
94
+ # Needs no permission itself, so it answers for any valid key.
95
+ def get_api_key_info
96
+ post_private("/0/private/GetApiKeyInfo", { nonce: nonce })
97
+ end
98
+
99
+ def get_balances
100
+ result = get_extended_balance
101
+ return result if result.failure?
102
+ return unreadable unless result.data.is_a?(Hash)
103
+
104
+ errors = result.data["error"]
105
+ return Result::Failure.new(*errors) if errors.is_a?(Array) && errors.any?
106
+
107
+ balances = {}
108
+ (result.data["result"] || {}).each do |symbol, balance|
109
+ mapped_symbol = ASSET_MAP[symbol.split(".").first] || symbol.split(".").first
110
+ total = BigDecimal(balance["balance"].to_s)
111
+ locked = BigDecimal((balance["hold_trade"] || "0").to_s)
112
+ free = total - locked
113
+ next if free.zero? && locked.zero?
114
+ balances[mapped_symbol] = { free: free, locked: locked }
115
+ end
116
+
117
+ Result::Success.new(balances)
118
+ end
119
+
120
+ def get_ohlc_data(pair:, interval: nil, since: nil)
121
+ get_public("/0/public/OHLC", { pair: pair, interval: interval, since: since })
122
+ end
123
+
124
+ def get_trades_history(type: nil, trades: nil, start: nil, end_time: nil, ofs: nil)
125
+ post_private("/0/private/TradesHistory", {
126
+ nonce: nonce, type: type, trades: trades,
127
+ start: start, end: end_time, ofs: ofs
128
+ })
129
+ end
130
+
131
+ def get_ledgers(asset: nil, type: nil, start: nil, end_time: nil, ofs: nil)
132
+ post_private("/0/private/Ledgers", {
133
+ nonce: nonce, asset: asset, type: type,
134
+ start: start, end: end_time, ofs: ofs
135
+ })
136
+ end
137
+
138
+ # Reconcile orders that QueryOrders has already dropped (Kraken removes terminal
139
+ # orders within hours) against TradesHistory, which is authoritative for executions
140
+ # and effectively unbounded in retention. Returns ONLY orders that actually executed,
141
+ # keyed by ordertxid, each as a per-order executed aggregate. Orders with no trades
142
+ # (never filled / truly gone) are simply absent from the result.
143
+ #
144
+ # `start` should be a unix timestamp bounding the lookback (e.g. the order's creation
145
+ # time) so we don't page the user's entire trade history.
146
+ def closed_orders_from_trades(order_ids:, start: nil, max_pages: 20)
147
+ wanted = Array(order_ids)
148
+ return Result::Success.new({}) if wanted.empty?
149
+
150
+ by_order = Hash.new { |h, k| h[k] = [] }
151
+ seen = {}
152
+ offset = 0
153
+ pages = 0
154
+ loop do
155
+ result = get_trades_history(start: start, ofs: offset)
156
+ return result if result.failure?
157
+ return unreadable unless result.data.is_a?(Hash)
158
+
159
+ errors = result.data["error"]
160
+ return Result::Failure.new(*errors) if errors.is_a?(Array) && errors.any?
161
+
162
+ trades = result.data.dig("result", "trades") || {}
163
+ break if trades.empty?
164
+
165
+ trades.each_pair do |trade_id, t|
166
+ # Offset paging over a list that grows at the top can serve a trade twice: count it once.
167
+ next if seen.key?(trade_id)
168
+
169
+ seen[trade_id] = true
170
+ otxid = t["ordertxid"]
171
+ by_order[otxid] << t if wanted.include?(otxid)
172
+ end
173
+
174
+ # Do NOT early-exit when each id has been *seen* — a partial fill can have trades on
175
+ # later pages. Page the whole [start, now] window (bounded by `start` + max_pages).
176
+ pages += 1
177
+ offset += trades.size
178
+ count = result.data.dig("result", "count").to_i
179
+ break if offset >= count || pages >= max_pages
180
+ end
181
+
182
+ Result::Success.new(by_order.transform_values { |trades| aggregate_trades(trades) })
183
+ end
184
+
185
+ def get_withdraw_addresses(asset: nil, method: nil)
186
+ post_private("/0/private/WithdrawAddresses", { nonce: nonce, asset: asset, method: method })
187
+ end
188
+
189
+ def get_withdraw_methods(asset: nil)
190
+ post_private("/0/private/WithdrawMethods", { nonce: nonce, asset: asset })
191
+ end
192
+
193
+ def withdraw(asset:, key:, amount:, address: nil)
194
+ post_private("/0/private/Withdraw", { nonce: nonce, asset: asset, key: key, amount: amount, address: address })
195
+ end
196
+
197
+ # --- Earn ---
198
+
199
+ def get_earn_allocations(ascending: nil, converted_asset: nil, hide_zero_allocations: nil)
200
+ post_private("/0/private/Earn/Allocations", {
201
+ nonce: nonce, ascending: ascending,
202
+ converted_asset: converted_asset,
203
+ hide_zero_allocations: hide_zero_allocations
204
+ })
205
+ end
206
+
207
+ private
208
+
209
+ # A 2xx body that isn't a JSON object (a maintenance page, "null"): the venue's answer is
210
+ # unknown, which is not the same as empty. Callers treat data[:unreadable] as ambiguous.
211
+ def unreadable
212
+ Result::Failure.new(UNREADABLE, data: { unreadable: true })
213
+ end
214
+
215
+ def aggregate_trades(trades)
216
+ first = trades.first
217
+ vol = trades.sum { |t| BigDecimal(t["vol"].to_s) }
218
+ cost = trades.sum { |t| BigDecimal(t["cost"].to_s) }
219
+ fee = trades.sum { |t| BigDecimal(t["fee"].to_s) }
220
+ {
221
+ order_id: first["ordertxid"],
222
+ status: :closed, # only executed orders reach here
223
+ side: first["type"]&.to_sym, # buy / sell
224
+ order_type: parse_order_type(first["ordertype"]),
225
+ price: vol.zero? ? nil : cost / vol, # VWAP across (partial) fills
226
+ amount: nil, # original order size unknown from trades
227
+ quote_amount: nil,
228
+ amount_exec: vol,
229
+ quote_amount_exec: cost,
230
+ fee: fee,
231
+ pair: first["pair"],
232
+ trade_count: trades.size,
233
+ last_trade_at: trades.map { |t| t["time"].to_f }.max,
234
+ raw: { "trades" => trades }
235
+ }
236
+ end
237
+
238
+ def normalize_order(order_id, raw)
239
+ descr = raw["descr"] || {}
240
+ order_type = parse_order_type(descr["ordertype"])
241
+ side = descr["type"]&.downcase&.to_sym
242
+ status = parse_order_status(raw["status"])
243
+
244
+ order_flags = (raw["oflags"] || "").split(",")
245
+ if order_flags.include?("viqc")
246
+ amount = nil
247
+ quote_amount = BigDecimal(raw["vol"].to_s)
248
+ else
249
+ amount = BigDecimal(raw["vol"].to_s)
250
+ quote_amount = nil
251
+ end
252
+
253
+ amount_exec = BigDecimal(raw["vol_exec"].to_s)
254
+ quote_amount_exec = BigDecimal(raw["cost"].to_s)
255
+
256
+ price = BigDecimal(raw["price"].to_s)
257
+ price = BigDecimal(descr["price"].to_s) if price.zero? && order_type == :limit
258
+ price = nil if price.zero?
259
+
260
+ {
261
+ order_id: order_id,
262
+ status: status,
263
+ side: side,
264
+ order_type: order_type,
265
+ price: price,
266
+ amount: amount,
267
+ quote_amount: quote_amount,
268
+ amount_exec: amount_exec,
269
+ quote_amount_exec: quote_amount_exec,
270
+ raw: raw
271
+ }
272
+ end
273
+
274
+ def parse_order_type(type)
275
+ case type
276
+ when "market" then :market
277
+ when "limit" then :limit
278
+ else :unknown
279
+ end
280
+ end
281
+
282
+ def parse_order_status(status)
283
+ case status
284
+ when "pending" then :unknown
285
+ when "open" then :open
286
+ when "closed" then :closed
287
+ when "canceled", "expired" then :cancelled
288
+ else :unknown
289
+ end
290
+ end
291
+
292
+ def validate_trading_credentials
293
+ result = get_extended_balance
294
+ return Result::Failure.new("Invalid trading credentials") if result.failure?
295
+
296
+ errors = result.data["error"]
297
+ if errors.is_a?(Array) && errors.none?
298
+ Result::Success.new(true)
299
+ else
300
+ Result::Failure.new("Invalid trading credentials")
301
+ end
302
+ end
303
+
304
+ def validate_read_credentials
305
+ validate_trading_credentials
306
+ end
307
+
308
+ def get_public(path, params = {})
309
+ with_rescue do
310
+ response = connection.get do |req|
311
+ req.url path
312
+ req.headers = public_headers
313
+ req.params = params.compact
314
+ end
315
+ response.body
316
+ end
317
+ end
318
+
319
+ def post_private(path, body = {})
320
+ with_rescue do
321
+ response = connection.post do |req|
322
+ req.url path
323
+ req.body = URI.encode_www_form(body.compact)
324
+ req.headers = private_headers(req.path, req.body)
325
+ end
326
+ response.body
327
+ end
328
+ end
329
+
330
+ # Kraken rejects any request whose nonce is <= the last nonce it saw for the
331
+ # same API key. We protect against in-process collisions (microsecond bursts,
332
+ # backward clock corrections) by tracking the last issued nonce per API key
333
+ # under a mutex. Cross-process reuse of the same API key (other workers,
334
+ # other containers, other tools) is not protected here — affected users
335
+ # should increase the Nonce Window on their Kraken API key.
336
+ @@nonce_mutex = Mutex.new
337
+ @@last_nonces = {}
338
+
339
+ def self.reset_nonce_state!
340
+ @@nonce_mutex.synchronize { @@last_nonces.clear }
341
+ end
342
+
343
+ def nonce
344
+ key = @api_key ? Digest::SHA256.hexdigest(@api_key) : :__no_api_key__
345
+ @@nonce_mutex.synchronize do
346
+ candidate = (Time.now.utc.to_f * 1_000_000).to_i
347
+ @@last_nonces[key] = [candidate, (@@last_nonces[key] || 0) + 1].max
348
+ end
349
+ end
350
+
351
+ def private_headers(path, body)
352
+ return public_headers unless authenticated?
353
+
354
+ request_nonce = URI.decode_www_form(body).to_h["nonce"]
355
+ data = "#{request_nonce}#{body}"
356
+ message = path + Digest::SHA256.digest(data)
357
+ decoded_key = Base64.decode64(@api_secret)
358
+ begin
359
+ hmac = OpenSSL::HMAC.digest("sha512", decoded_key, message)
360
+ rescue OpenSSL::HMACError
361
+ return public_headers
362
+ end
363
+ signature = Base64.strict_encode64(hmac)
364
+
365
+ {
366
+ "API-Key": @api_key,
367
+ "API-Sign": signature,
368
+ Accept: "application/json",
369
+ "Content-Type": "application/x-www-form-urlencoded",
370
+ "User-Agent": "Honeymaker Ruby"
371
+ }
372
+ end
373
+
374
+ def public_headers
375
+ { Accept: "application/json", "Content-Type": "application/json", "User-Agent": "Honeymaker Ruby" }
376
+ end
377
+ end
378
+ end
379
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+
5
+ module Honeymaker
6
+ module Clients
7
+ class KrakenFutures < Client
8
+ URL = "https://futures.kraken.com"
9
+
10
+ RATE_LIMITS = { default: 500, orders: 500 }.freeze
11
+
12
+ def get_accounts
13
+ get_private("/derivatives/api/v3/accounts")
14
+ end
15
+
16
+ def get_fills(last_fill_time: nil)
17
+ get_private("/derivatives/api/v3/fills", { lastFillTime: last_fill_time })
18
+ end
19
+
20
+ def get_open_positions
21
+ get_private("/derivatives/api/v3/openpositions")
22
+ end
23
+
24
+ def historical_funding_rates(symbol:)
25
+ get_public("/derivatives/api/v3/historicalfundingrates", { symbol: symbol })
26
+ end
27
+
28
+ private
29
+
30
+ def validate_trading_credentials
31
+ result = get_accounts
32
+ return Result::Failure.new("Invalid trading credentials") if result.failure?
33
+ Result::Success.new(true)
34
+ end
35
+
36
+ def validate_read_credentials
37
+ validate_trading_credentials
38
+ end
39
+
40
+ def get_public(path, params = {})
41
+ with_rescue do
42
+ response = connection.get do |req|
43
+ req.url path
44
+ req.params = params.compact
45
+ end
46
+ response.body
47
+ end
48
+ end
49
+
50
+ def get_private(path, params = {})
51
+ with_rescue do
52
+ params = params.compact
53
+ query_string = params.empty? ? "" : Faraday::Utils.build_query(params)
54
+ nonce = timestamp_ms.to_s
55
+
56
+ post_data = query_string
57
+ hash_input = "#{post_data}#{nonce}#{path}"
58
+ sha256_hash = Digest::SHA256.digest(hash_input)
59
+ decoded_secret = Base64.decode64(@api_secret)
60
+ hmac = OpenSSL::HMAC.digest("sha512", decoded_secret, sha256_hash)
61
+ authent = Base64.strict_encode64(hmac)
62
+
63
+ response = connection.get do |req|
64
+ req.url path
65
+ req.headers = {
66
+ "APIKey": @api_key,
67
+ "Authent": authent,
68
+ "Nonce": nonce,
69
+ Accept: "application/json"
70
+ }
71
+ req.params = params
72
+ end
73
+ response.body
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end