honeymaker 0.12.3 → 0.12.4
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 +4 -4
- data/.github/workflows/native-gems.yml +100 -0
- data/.github/workflows/release.yml +36 -0
- data/.github/workflows/test.yml +19 -4
- data/Cargo.lock +1078 -0
- data/Cargo.toml +14 -0
- data/README.md +39 -0
- data/Rakefile +28 -16
- data/crates/honeymaker/Cargo.toml +18 -0
- data/crates/honeymaker/src/encode.rs +106 -0
- data/crates/honeymaker/src/kraken/lookup.rs +140 -0
- data/crates/honeymaker/src/kraken/mod.rs +5 -0
- data/crates/honeymaker/src/kraken/normalize.rs +536 -0
- data/crates/honeymaker/src/kraken/requests.rs +286 -0
- data/crates/honeymaker/src/kraken/sign.rs +166 -0
- data/crates/honeymaker/src/kraken/trades.rs +201 -0
- data/crates/honeymaker/src/lib.rs +10 -0
- data/crates/honeymaker/src/num.rs +115 -0
- data/crates/honeymaker/src/semantics.rs +100 -0
- data/crates/honeymaker/src/types.rs +85 -0
- data/crates/honeymaker/tests/kraken_stage2.rs +592 -0
- data/crates/honeymaker/tests/kraken_trades_pager.rs +307 -0
- data/crates/honeymaker/tests/kraken_vectors.rs +78 -0
- data/crates/honeymaker/tests/vectors/decimal.json +146 -0
- data/crates/honeymaker/tests/vectors/kraken_add_order_wire.json +74 -0
- data/crates/honeymaker/tests/vectors/kraken_signing.json +3314 -0
- data/crates/honeymaker/tests/vectors/kraken_trades_pager.json +1583 -0
- data/crates/honeymaker-client/Cargo.toml +30 -0
- data/crates/honeymaker-client/examples/probe.rs +147 -0
- data/crates/honeymaker-client/src/kraken.rs +514 -0
- data/crates/honeymaker-client/src/lib.rs +4 -0
- data/crates/honeymaker-client/src/transport/connect.rs +583 -0
- data/crates/honeymaker-client/src/transport/io_timeout.rs +352 -0
- data/crates/honeymaker-client/src/transport/response.rs +169 -0
- data/crates/honeymaker-client/src/transport.rs +467 -0
- data/crates/honeymaker-client/tests/classification.rs +203 -0
- data/crates/honeymaker-client/tests/kraken_client.rs +949 -0
- data/crates/honeymaker-client/tests/response.rs +522 -0
- data/crates/honeymaker-client/tests/support/mod.rs +550 -0
- data/crates/honeymaker-client/tests/transport.rs +828 -0
- data/ext/honeymaker_native/Cargo.toml +16 -0
- data/ext/honeymaker_native/extconf.rb +6 -0
- data/ext/honeymaker_native/src/convert.rs +150 -0
- data/ext/honeymaker_native/src/kraken.rs +362 -0
- data/ext/honeymaker_native/src/lib.rs +90 -0
- data/ext/honeymaker_native/src/ruby_decimal.rs +90 -0
- data/lib/honeymaker/clients/bybit.rb +19 -2
- data/lib/honeymaker/clients/coinbase.rb +3 -2
- data/lib/honeymaker/clients/kraken.rb +6 -1
- data/lib/honeymaker/exchanges/coinbase.rb +5 -4
- data/lib/honeymaker/native/kraken.rb +263 -0
- data/lib/honeymaker/native.rb +42 -0
- data/lib/honeymaker/version.rb +1 -1
- data/lib/honeymaker.rb +12 -2
- data/script/parity/kraken_client_live.rb +191 -0
- data/script/parity/kraken_live.rb +70 -0
- data/script/parity/kraken_transport.rb +604 -0
- data/script/parity/resolve.c +49 -0
- data/script/release/push_gems.sh +28 -0
- data/script/vectors/kraken.rb +50 -0
- data/script/vectors/kraken_stage2.rb +146 -0
- data/test/fixtures/coinbase_products.json +1 -1
- data/test/honeymaker/clients/bybit_client_test.rb +25 -0
- data/test/honeymaker/clients/coinbase_client_test.rb +14 -0
- data/test/honeymaker/clients/kraken_client_test.rb +15 -0
- data/test/honeymaker/exchanges/coinbase_test.rb +22 -2
- data/test/native/backend_switch_test.rb +53 -0
- data/test/native/convert_test.rb +137 -0
- data/test/native/gem_isolation_test.rb +17 -0
- data/test/native/loader_test.rb +29 -0
- data/test/native/lockstep_test.rb +23 -0
- data/test/native/signing_smoke_test.rb +28 -0
- data/test/parity/kraken_client_parity_test.rb +398 -0
- data/test/parity/kraken_exchange_parity_test.rb +172 -0
- data/test/release/branch_hardening_test.rb +34 -0
- data/test/release/push_gems_test.rb +79 -0
- data/test/smoke.rb +24 -0
- data/test/support/kraken_signing_smoke.rb +21 -0
- data/test/support/parity_helper.rb +62 -0
- data/test/support/venue_double.rb +100 -0
- metadata +84 -4
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
use honeymaker_core::num::Num;
|
|
2
|
+
use magnus::{Ruby, Value, prelude::*, value::BoxValue};
|
|
3
|
+
|
|
4
|
+
/// Ruby's BigDecimal behind the core's Num trait. BoxValue registers the object with the GC,
|
|
5
|
+
/// so it is safe inside the Vecs and structs the normalizers return. Calls retain the GVL.
|
|
6
|
+
pub struct RubyDecimal(BoxValue<Value>);
|
|
7
|
+
|
|
8
|
+
impl RubyDecimal {
|
|
9
|
+
pub fn value(&self) -> Value {
|
|
10
|
+
*self.0
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
fn wrap(v: Value) -> Self {
|
|
14
|
+
RubyDecimal(BoxValue::new(v))
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
fn ruby() -> Ruby {
|
|
18
|
+
Ruby::get().expect("called on a Ruby thread")
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
impl Num for RubyDecimal {
|
|
23
|
+
type Error = magnus::Error;
|
|
24
|
+
|
|
25
|
+
fn parse(text: &str) -> Result<Self, magnus::Error> {
|
|
26
|
+
Ok(Self::wrap(
|
|
27
|
+
Self::ruby()
|
|
28
|
+
.module_kernel()
|
|
29
|
+
.funcall("BigDecimal", (text,))?,
|
|
30
|
+
))
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
fn parse_to_s(v: &serde_json::Value) -> Result<Self, magnus::Error> {
|
|
34
|
+
let ruby = Self::ruby();
|
|
35
|
+
let obj = BoxValue::new(crate::convert::from_json(&ruby, v)?);
|
|
36
|
+
let text = BoxValue::new(obj.funcall::<_, _, Value>("to_s", ())?);
|
|
37
|
+
Ok(Self::wrap(
|
|
38
|
+
ruby.module_kernel().funcall("BigDecimal", (*text,))?,
|
|
39
|
+
))
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
fn parse_json(v: &serde_json::Value) -> Result<Self, magnus::Error> {
|
|
43
|
+
let ruby = Self::ruby();
|
|
44
|
+
let obj = BoxValue::new(crate::convert::from_json(&ruby, v)?);
|
|
45
|
+
Ok(Self::wrap(
|
|
46
|
+
ruby.module_kernel().funcall("BigDecimal", (*obj,))?,
|
|
47
|
+
))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
fn string_op(
|
|
51
|
+
v: &serde_json::Value,
|
|
52
|
+
op: honeymaker_core::semantics::StringOp<'_>,
|
|
53
|
+
) -> Result<serde_json::Value, magnus::Error> {
|
|
54
|
+
use honeymaker_core::semantics::StringOp;
|
|
55
|
+
let ruby = Self::ruby();
|
|
56
|
+
let obj = BoxValue::new(crate::convert::from_json(&ruby, v)?);
|
|
57
|
+
let result: Value = match op {
|
|
58
|
+
StringOp::Index(key) => obj.funcall("[]", (key,))?,
|
|
59
|
+
StringOp::Split(sep) => obj.funcall("split", (sep,))?,
|
|
60
|
+
StringOp::First => obj.funcall("[]", (0,))?,
|
|
61
|
+
StringOp::DowncaseSymbol | StringOp::Symbol => {
|
|
62
|
+
let text = BoxValue::new(if matches!(op, StringOp::DowncaseSymbol) {
|
|
63
|
+
obj.funcall("downcase", ())?
|
|
64
|
+
} else {
|
|
65
|
+
*obj
|
|
66
|
+
});
|
|
67
|
+
// Validate now, in legacy's evaluation order, before parsing decimals.
|
|
68
|
+
text.funcall::<_, _, Value>("to_sym", ())?;
|
|
69
|
+
*text
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
crate::convert::to_json(&ruby, result)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
fn add(&self, o: &Self) -> Result<Self, magnus::Error> {
|
|
76
|
+
Ok(Self::wrap(self.value().funcall("+", (o.value(),))?))
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
fn sub(&self, o: &Self) -> Result<Self, magnus::Error> {
|
|
80
|
+
Ok(Self::wrap(self.value().funcall("-", (o.value(),))?))
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
fn div(&self, o: &Self) -> Result<Self, magnus::Error> {
|
|
84
|
+
Ok(Self::wrap(self.value().funcall("/", (o.value(),))?))
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
fn is_zero(&self) -> Result<bool, magnus::Error> {
|
|
88
|
+
self.value().funcall("zero?", ())
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -96,6 +96,15 @@ module Honeymaker
|
|
|
96
96
|
})
|
|
97
97
|
end
|
|
98
98
|
|
|
99
|
+
# Closed orders that /v5/order/realtime no longer returns.
|
|
100
|
+
def order_history(category:, symbol: nil, order_id: nil, order_link_id: nil, order_status: nil,
|
|
101
|
+
start_time: nil, end_time: nil, limit: nil, cursor: nil)
|
|
102
|
+
get_authenticated("/v5/order/history", {
|
|
103
|
+
category: category, symbol: symbol, orderId: order_id, orderLinkId: order_link_id,
|
|
104
|
+
orderStatus: order_status, startTime: start_time, endTime: end_time, limit: limit, cursor: cursor
|
|
105
|
+
})
|
|
106
|
+
end
|
|
107
|
+
|
|
99
108
|
def transaction_log(account_type: nil, category: nil, currency: nil, type: nil, start_time: nil, end_time: nil, limit: nil, cursor: nil)
|
|
100
109
|
get_authenticated("/v5/account/transaction-log", {
|
|
101
110
|
accountType: account_type, category: category, currency: currency, type: type,
|
|
@@ -116,6 +125,13 @@ module Honeymaker
|
|
|
116
125
|
})
|
|
117
126
|
end
|
|
118
127
|
|
|
128
|
+
# Off-chain deposits from another Bybit user; query-record does not include them.
|
|
129
|
+
def internal_deposit_records(tx_id: nil, coin: nil, start_time: nil, end_time: nil, limit: nil, cursor: nil)
|
|
130
|
+
get_authenticated("/v5/asset/deposit/query-internal-record", {
|
|
131
|
+
txID: tx_id, coin: coin, startTime: start_time, endTime: end_time, limit: limit, cursor: cursor
|
|
132
|
+
})
|
|
133
|
+
end
|
|
134
|
+
|
|
119
135
|
def withdraw_records(coin: nil, start_time: nil, end_time: nil, limit: nil, cursor: nil, withdraw_type: nil)
|
|
120
136
|
get_authenticated("/v5/asset/withdraw/query-record", {
|
|
121
137
|
coin: coin, startTime: start_time, endTime: end_time,
|
|
@@ -207,9 +223,10 @@ module Honeymaker
|
|
|
207
223
|
def parse_order_status(status)
|
|
208
224
|
case status
|
|
209
225
|
when "Created", "Untriggered" then :unknown
|
|
210
|
-
when "New", "PartiallyFilled"
|
|
226
|
+
when "New", "PartiallyFilled" then :open
|
|
211
227
|
when "Filled" then :closed
|
|
212
|
-
|
|
228
|
+
# PartiallyFilledCanceled is final: a spot order cancelled after a partial fill.
|
|
229
|
+
when "Cancelled", "PartiallyFilledCanceled", "Expired", "Deactivated" then :cancelled
|
|
213
230
|
when "Rejected" then :failed
|
|
214
231
|
else :unknown
|
|
215
232
|
end
|
|
@@ -105,8 +105,9 @@ module Honeymaker
|
|
|
105
105
|
get("/api/v3/brokerage/key_permissions")
|
|
106
106
|
end
|
|
107
107
|
|
|
108
|
-
|
|
109
|
-
|
|
108
|
+
# One page of accounts; pass the response's `cursor` back while `has_next` is true.
|
|
109
|
+
def list_accounts(limit: nil, cursor: nil)
|
|
110
|
+
get("/api/v3/brokerage/accounts", { limit: limit, cursor: cursor })
|
|
110
111
|
end
|
|
111
112
|
|
|
112
113
|
def list_portfolios
|
|
@@ -148,6 +148,7 @@ module Honeymaker
|
|
|
148
148
|
return Result::Success.new({}) if wanted.empty?
|
|
149
149
|
|
|
150
150
|
by_order = Hash.new { |h, k| h[k] = [] }
|
|
151
|
+
seen = {}
|
|
151
152
|
offset = 0
|
|
152
153
|
pages = 0
|
|
153
154
|
loop do
|
|
@@ -161,7 +162,11 @@ module Honeymaker
|
|
|
161
162
|
trades = result.data.dig("result", "trades") || {}
|
|
162
163
|
break if trades.empty?
|
|
163
164
|
|
|
164
|
-
trades.
|
|
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
|
|
165
170
|
otxid = t["ordertxid"]
|
|
166
171
|
by_order[otxid] << t if wanted.include?(otxid)
|
|
167
172
|
end
|
|
@@ -6,7 +6,7 @@ module Honeymaker
|
|
|
6
6
|
BASE_URL = "https://api.coinbase.com"
|
|
7
7
|
|
|
8
8
|
ASSET_BLACKLIST = [
|
|
9
|
-
"
|
|
9
|
+
"RNDR", # delisted; has the same external_id as RENDER
|
|
10
10
|
"ZETACHAIN", # has the same external_id as ZETA
|
|
11
11
|
"WAXL" # has the same external_id as AXL
|
|
12
12
|
].freeze
|
|
@@ -51,10 +51,11 @@ module Honeymaker
|
|
|
51
51
|
|
|
52
52
|
private
|
|
53
53
|
|
|
54
|
-
# Coinbase only signals problems explicitly: a `trading_disabled`
|
|
55
|
-
# `status`. Absent
|
|
54
|
+
# Coinbase only signals problems explicitly: a `trading_disabled`, `is_disabled`, `limit_only`
|
|
55
|
+
# or `cancel_only` flag, or a non-"online" `status`. Absent any signal, treat the pair as
|
|
56
|
+
# enabled. limit_only is reported as not tradable, like Kraken's limit_only status.
|
|
56
57
|
def trading_enabled?(product)
|
|
57
|
-
return false if
|
|
58
|
+
return false if %w[trading_disabled is_disabled limit_only cancel_only].any? { |flag| product[flag] == true }
|
|
58
59
|
return false if product.key?("status") && product["status"] != "online"
|
|
59
60
|
|
|
60
61
|
true
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
5
|
+
module Honeymaker
|
|
6
|
+
module Native
|
|
7
|
+
# Kraken on the Rust core. Signatures and argument expressions are copied from Clients::Kraken
|
|
8
|
+
# (a test compares signatures). Transport, body encoding and Results are the inherited,
|
|
9
|
+
# unchanged Ruby: connection + URI.encode_www_form + with_rescue. Rust lays out and signs the
|
|
10
|
+
# requests and normalizes the answers.
|
|
11
|
+
class KrakenClient < Honeymaker::Client
|
|
12
|
+
URL = "https://api.kraken.com"
|
|
13
|
+
RATE_LIMITS = { default: 1000, orders: 1000 }.freeze
|
|
14
|
+
UNREADABLE = "Kraken: unreadable response"
|
|
15
|
+
|
|
16
|
+
def self.reset_nonce_state!
|
|
17
|
+
Native.load!
|
|
18
|
+
Ext::Kraken.reset_nonce_state!
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def query_orders_info(txid:, trades: nil, userref: nil, consolidate_taker: true)
|
|
22
|
+
result = post_private("query_orders_info", trades: trades, userref: userref, txid: txid, consolidate_taker: consolidate_taker)
|
|
23
|
+
finish("query_orders_info", result) do |orders|
|
|
24
|
+
(result.data["result"] || {}).each { |id, raw| orders[id][:raw] = raw }
|
|
25
|
+
orders
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def add_order(ordertype:, type:, volume:, pair:, userref: nil, cl_ord_id: nil,
|
|
30
|
+
displayvol: nil, price: nil, price2: nil, trigger: nil, leverage: nil,
|
|
31
|
+
reduce_only: nil, stptype: nil, oflags: [], timeinforce: nil,
|
|
32
|
+
starttm: nil, expiretm: nil, close: nil, close_price: nil,
|
|
33
|
+
close_price2: nil, deadline: nil, validate: nil)
|
|
34
|
+
result = post_private("add_order",
|
|
35
|
+
ordertype: ordertype, type: type, volume: volume, pair: pair, userref: userref,
|
|
36
|
+
cl_ord_id: cl_ord_id, displayvol: displayvol, price: price, price2: price2,
|
|
37
|
+
trigger: trigger, leverage: leverage, reduce_only: reduce_only, stptype: stptype,
|
|
38
|
+
oflags: oflags.any? ? oflags.join(",") : nil,
|
|
39
|
+
timeinforce: timeinforce, starttm: starttm, expiretm: expiretm, close: close,
|
|
40
|
+
close_price: close_price, close_price2: close_price2, deadline: deadline, validate: validate)
|
|
41
|
+
finish("add_order", result) { |order_id| { order_id: order_id, raw: result.data } }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def cancel_order(txid: nil, cl_ord_id: nil)
|
|
45
|
+
post_private("cancel_order", txid: txid, cl_ord_id: cl_ord_id)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def get_tradable_asset_pairs(pairs: nil, info: nil, country_code: nil, aclass_base: nil)
|
|
49
|
+
get_public("get_tradable_asset_pairs", pairs: pairs ? pairs.join(",") : nil, info: info,
|
|
50
|
+
country_code: country_code, aclass_base: aclass_base)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def get_asset_info(assets: nil, aclass: nil)
|
|
54
|
+
get_public("get_asset_info", assets: assets ? assets.join(",") : nil, aclass: aclass)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def get_ticker_information(pair: nil)
|
|
58
|
+
get_public("get_ticker_information", pair: pair)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def get_extended_balance
|
|
62
|
+
post_private("get_extended_balance")
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def get_api_key_info
|
|
66
|
+
post_private("get_api_key_info")
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def get_balances
|
|
70
|
+
finish("balances", get_extended_balance) { |balances| balances }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def get_ohlc_data(pair:, interval: nil, since: nil)
|
|
74
|
+
get_public("get_ohlc_data", pair: pair, interval: interval, since: since)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def get_trades_history(type: nil, trades: nil, start: nil, end_time: nil, ofs: nil)
|
|
78
|
+
post_private("get_trades_history", type: type, trades: trades, start: start, end_time: end_time, ofs: ofs)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def get_ledgers(asset: nil, type: nil, start: nil, end_time: nil, ofs: nil)
|
|
82
|
+
post_private("get_ledgers", asset: asset, type: type, start: start, end_time: end_time, ofs: ofs)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# The paging loop stays Ruby (verbatim legacy) so dig/to_i/include? keep Ruby's
|
|
86
|
+
# coercions; Rust aggregates. A Rust pager comes with stage 2, when Rust callers need one.
|
|
87
|
+
def closed_orders_from_trades(order_ids:, start: nil, max_pages: 20)
|
|
88
|
+
wanted = Array(order_ids)
|
|
89
|
+
return Result::Success.new({}) if wanted.empty?
|
|
90
|
+
|
|
91
|
+
by_order = Hash.new { |h, k| h[k] = [] }
|
|
92
|
+
seen = {}
|
|
93
|
+
offset = 0
|
|
94
|
+
pages = 0
|
|
95
|
+
loop do
|
|
96
|
+
result = get_trades_history(start: start, ofs: offset)
|
|
97
|
+
return result if result.failure?
|
|
98
|
+
return unreadable unless result.data.is_a?(Hash)
|
|
99
|
+
|
|
100
|
+
errors = result.data["error"]
|
|
101
|
+
return Result::Failure.new(*errors) if errors.is_a?(Array) && errors.any?
|
|
102
|
+
|
|
103
|
+
trades = result.data.dig("result", "trades") || {}
|
|
104
|
+
break if trades.empty?
|
|
105
|
+
|
|
106
|
+
trades.each_pair do |trade_id, t|
|
|
107
|
+
# Offset paging over a list that grows at the top can serve a trade twice: count it once.
|
|
108
|
+
next if seen.key?(trade_id)
|
|
109
|
+
|
|
110
|
+
seen[trade_id] = true
|
|
111
|
+
otxid = t["ordertxid"]
|
|
112
|
+
by_order[otxid] << t if wanted.include?(otxid)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Do NOT early-exit when each id has been *seen* — a partial fill can have trades on
|
|
116
|
+
# later pages. Page the whole [start, now] window (bounded by `start` + max_pages).
|
|
117
|
+
pages += 1
|
|
118
|
+
offset += trades.size
|
|
119
|
+
count = result.data.dig("result", "count").to_i
|
|
120
|
+
break if offset >= count || pages >= max_pages
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
Native.load!
|
|
124
|
+
Result::Success.new(Ext::Kraken.aggregate_trades(by_order))
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def get_withdraw_addresses(asset: nil, method: nil)
|
|
128
|
+
post_private("get_withdraw_addresses", asset: asset, method: method)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def get_withdraw_methods(asset: nil)
|
|
132
|
+
post_private("get_withdraw_methods", asset: asset)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def withdraw(asset:, key:, amount:, address: nil)
|
|
136
|
+
post_private("withdraw", asset: asset, key: key, amount: amount, address: address)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def get_earn_allocations(ascending: nil, converted_asset: nil, hide_zero_allocations: nil)
|
|
140
|
+
post_private("get_earn_allocations", ascending: ascending, converted_asset: converted_asset,
|
|
141
|
+
hide_zero_allocations: hide_zero_allocations)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
private
|
|
145
|
+
|
|
146
|
+
def validate_trading_credentials
|
|
147
|
+
result = get_extended_balance
|
|
148
|
+
return Result::Failure.new("Invalid trading credentials") if result.failure?
|
|
149
|
+
|
|
150
|
+
errors = result.data["error"]
|
|
151
|
+
if errors.is_a?(Array) && errors.none?
|
|
152
|
+
Result::Success.new(true)
|
|
153
|
+
else
|
|
154
|
+
Result::Failure.new("Invalid trading credentials")
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def validate_read_credentials
|
|
159
|
+
validate_trading_credentials
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def native
|
|
163
|
+
@native ||= begin
|
|
164
|
+
Native.load!
|
|
165
|
+
Ext::Kraken.new(@api_key, @api_secret)
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# The legacy post_private: Rust lays out the pairs (nonce first) and signs; Ruby encodes.
|
|
170
|
+
def post_private(op, params = {})
|
|
171
|
+
with_rescue do
|
|
172
|
+
path, pairs = native.build_post(op, params.transform_keys(&:to_s))
|
|
173
|
+
response = connection.post do |req|
|
|
174
|
+
req.url path
|
|
175
|
+
req.body = URI.encode_www_form(pairs)
|
|
176
|
+
req.headers = native.sign(req.path, req.body)
|
|
177
|
+
end
|
|
178
|
+
response.body
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# The legacy get_public: Rust lays out the params; Faraday encodes them.
|
|
183
|
+
def get_public(op, params = {})
|
|
184
|
+
with_rescue do
|
|
185
|
+
path, query, headers = native.build_get(op, params.transform_keys(&:to_s))
|
|
186
|
+
response = connection.get do |req|
|
|
187
|
+
req.url path
|
|
188
|
+
req.headers = headers
|
|
189
|
+
req.params = query
|
|
190
|
+
end
|
|
191
|
+
response.body
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def finish(op, result)
|
|
196
|
+
return result if result.failure?
|
|
197
|
+
# Guard in Ruby, before any conversion: a binary maintenance page must answer like the
|
|
198
|
+
# hardened legacy, not fail inside the converter.
|
|
199
|
+
return unreadable unless result.data.is_a?(Hash)
|
|
200
|
+
|
|
201
|
+
verdict = native.finish(op, result.data)
|
|
202
|
+
case verdict[0]
|
|
203
|
+
when "ok" then Result::Success.new(yield(verdict[1]))
|
|
204
|
+
when "venue" then Result::Failure.new(*result.data["error"])
|
|
205
|
+
when "unreadable" then unreadable
|
|
206
|
+
else raise ShapeError, "unexpected verdict #{verdict[0].inspect}"
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def unreadable
|
|
211
|
+
Result::Failure.new(UNREADABLE, data: { unreadable: true })
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
class KrakenExchange < Honeymaker::Exchange
|
|
216
|
+
BASE_URL = "https://api.kraken.com"
|
|
217
|
+
|
|
218
|
+
def classify_error(message)
|
|
219
|
+
return nil if message.nil?
|
|
220
|
+
|
|
221
|
+
Native.load!
|
|
222
|
+
Ext::Kraken.classify_error(message)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def get_tickers_info
|
|
226
|
+
with_rescue do
|
|
227
|
+
response = connection.get("/0/public/AssetPairs", { aclass_base: "all" })
|
|
228
|
+
raise ShapeError, "Kraken: catalogue body is not an object" unless response.body.is_a?(Hash)
|
|
229
|
+
|
|
230
|
+
verdict = native.finish("tickers_info", response.body)
|
|
231
|
+
return Result::Failure.new(*response.body["error"]) if verdict[0] == "venue"
|
|
232
|
+
|
|
233
|
+
verdict[1].each { |t| t[:minimum_quote_size] = t[:minimum_quote_size].to_s }
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def get_bid_ask(symbol)
|
|
238
|
+
with_rescue do
|
|
239
|
+
response = connection.get("/0/public/Ticker") { |req| req.params = { pair: symbol } }
|
|
240
|
+
raise ShapeError, "Kraken: ticker body is not an object" unless response.body.is_a?(Hash)
|
|
241
|
+
|
|
242
|
+
verdict = native.finish("bid_ask", response.body)
|
|
243
|
+
raise StandardError, response.body["error"].first if verdict[0] == "venue"
|
|
244
|
+
|
|
245
|
+
verdict[1]
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
private
|
|
250
|
+
|
|
251
|
+
def native
|
|
252
|
+
@native ||= begin
|
|
253
|
+
Native.load!
|
|
254
|
+
Ext::Kraken.new(nil, nil)
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def connection
|
|
259
|
+
@connection ||= build_connection(BASE_URL)
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Honeymaker
|
|
4
|
+
# Rust-backed venue logic. A process picks its backend per exchange once, when honeymaker is
|
|
5
|
+
# required: HONEYMAKER_NATIVE=kraken. With the flag off the extension is never loaded.
|
|
6
|
+
module Native
|
|
7
|
+
# A venue payload whose shape legacy would have tripped over with an incidental NoMethodError
|
|
8
|
+
# or TypeError. Still a StandardError, which is all consumers rescue.
|
|
9
|
+
class ShapeError < StandardError; end
|
|
10
|
+
|
|
11
|
+
SUPPORTED = %w[kraken].freeze
|
|
12
|
+
|
|
13
|
+
class << self
|
|
14
|
+
def enabled
|
|
15
|
+
@enabled ||= begin
|
|
16
|
+
names = ENV.fetch("HONEYMAKER_NATIVE", "").split(",").map { |name| name.strip.downcase }.reject(&:empty?).uniq
|
|
17
|
+
unsupported = names - SUPPORTED
|
|
18
|
+
unless unsupported.empty?
|
|
19
|
+
raise ArgumentError, "Unsupported HONEYMAKER_NATIVE names: #{unsupported.join(', ')}; supported names: #{SUPPORTED.join(', ')}"
|
|
20
|
+
end
|
|
21
|
+
names.freeze
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def enabled?(name)
|
|
26
|
+
name = name.to_s.strip.downcase
|
|
27
|
+
names = enabled
|
|
28
|
+
SUPPORTED.include?(name) && names.include?(name)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def load!
|
|
32
|
+
return if defined?(Ext)
|
|
33
|
+
|
|
34
|
+
begin
|
|
35
|
+
require "honeymaker/#{RUBY_VERSION[/\A\d+\.\d+/]}/honeymaker_native" # fat platform gem
|
|
36
|
+
rescue LoadError
|
|
37
|
+
require "honeymaker/honeymaker_native" # compiled from source / rake compile
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
data/lib/honeymaker/version.rb
CHANGED
data/lib/honeymaker.rb
CHANGED
|
@@ -9,6 +9,7 @@ require_relative "honeymaker/result"
|
|
|
9
9
|
require_relative "honeymaker/utils"
|
|
10
10
|
require_relative "honeymaker/exchange"
|
|
11
11
|
require_relative "honeymaker/client"
|
|
12
|
+
require_relative "honeymaker/native"
|
|
12
13
|
require_relative "honeymaker/clients/binance"
|
|
13
14
|
require_relative "honeymaker/clients/binance_us"
|
|
14
15
|
require_relative "honeymaker/clients/kraken"
|
|
@@ -37,13 +38,18 @@ require_relative "honeymaker/exchanges/hyperliquid"
|
|
|
37
38
|
require_relative "honeymaker/exchanges/bingx"
|
|
38
39
|
require_relative "honeymaker/exchanges/bitrue"
|
|
39
40
|
|
|
41
|
+
if Honeymaker::Native.enabled?("kraken")
|
|
42
|
+
Honeymaker::Native.load! # fail at boot, not at the first order
|
|
43
|
+
require_relative "honeymaker/native/kraken"
|
|
44
|
+
end
|
|
45
|
+
|
|
40
46
|
module Honeymaker
|
|
41
47
|
class Error < StandardError; end
|
|
42
48
|
|
|
43
49
|
EXCHANGES = {
|
|
44
50
|
"binance" => Exchanges::Binance,
|
|
45
51
|
"binance_us" => Exchanges::BinanceUs,
|
|
46
|
-
"kraken" => Exchanges::Kraken,
|
|
52
|
+
"kraken" => Native.enabled?("kraken") ? Native::KrakenExchange : Exchanges::Kraken,
|
|
47
53
|
"coinbase" => Exchanges::Coinbase,
|
|
48
54
|
"mexc" => Exchanges::Mexc,
|
|
49
55
|
"gemini" => Exchanges::Gemini,
|
|
@@ -59,7 +65,7 @@ module Honeymaker
|
|
|
59
65
|
CLIENTS = {
|
|
60
66
|
"binance" => Clients::Binance,
|
|
61
67
|
"binance_us" => Clients::BinanceUs,
|
|
62
|
-
"kraken" => Clients::Kraken,
|
|
68
|
+
"kraken" => Native.enabled?("kraken") ? Native::KrakenClient : Clients::Kraken,
|
|
63
69
|
"coinbase" => Clients::Coinbase,
|
|
64
70
|
"bybit" => Clients::Bybit,
|
|
65
71
|
"mexc" => Clients::Mexc,
|
|
@@ -73,6 +79,10 @@ module Honeymaker
|
|
|
73
79
|
"kraken_futures" => Clients::KrakenFutures
|
|
74
80
|
}.freeze
|
|
75
81
|
|
|
82
|
+
def self.backend(name)
|
|
83
|
+
Native.enabled?(name) ? :native : :ruby
|
|
84
|
+
end
|
|
85
|
+
|
|
76
86
|
def self.exchange(name)
|
|
77
87
|
klass = EXCHANGES[name.to_s]
|
|
78
88
|
raise Error, "Unknown exchange: #{name}" unless klass
|