honeymaker 0.12.4-x86_64-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.
- checksums.yaml +7 -0
- data/.github/workflows/native-gems.yml +100 -0
- data/.github/workflows/release.yml +36 -0
- data/.github/workflows/test.yml +33 -0
- data/LICENSE +21 -0
- data/README.md +219 -0
- data/Rakefile +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/lib/honeymaker/3.4/honeymaker_native.so +0 -0
- data/lib/honeymaker/4.0/honeymaker_native.so +0 -0
- data/lib/honeymaker/client.rb +139 -0
- data/lib/honeymaker/clients/binance.rb +687 -0
- data/lib/honeymaker/clients/binance_us.rb +42 -0
- data/lib/honeymaker/clients/bingx.rb +242 -0
- data/lib/honeymaker/clients/bitget.rb +318 -0
- data/lib/honeymaker/clients/bitrue.rb +245 -0
- data/lib/honeymaker/clients/bitvavo.rb +244 -0
- data/lib/honeymaker/clients/bybit.rb +308 -0
- data/lib/honeymaker/clients/coinbase.rb +326 -0
- data/lib/honeymaker/clients/gemini.rb +189 -0
- data/lib/honeymaker/clients/hyperliquid.rb +232 -0
- data/lib/honeymaker/clients/kraken.rb +379 -0
- data/lib/honeymaker/clients/kraken_futures.rb +78 -0
- data/lib/honeymaker/clients/kucoin.rb +310 -0
- data/lib/honeymaker/clients/mexc.rb +249 -0
- data/lib/honeymaker/exchange.rb +129 -0
- data/lib/honeymaker/exchanges/binance.rb +55 -0
- data/lib/honeymaker/exchanges/binance_us.rb +9 -0
- data/lib/honeymaker/exchanges/bingx.rb +56 -0
- data/lib/honeymaker/exchanges/bitget.rb +52 -0
- data/lib/honeymaker/exchanges/bitrue.rb +53 -0
- data/lib/honeymaker/exchanges/bitvavo.rb +65 -0
- data/lib/honeymaker/exchanges/bybit.rb +57 -0
- data/lib/honeymaker/exchanges/coinbase.rb +69 -0
- data/lib/honeymaker/exchanges/gemini.rb +134 -0
- data/lib/honeymaker/exchanges/hyperliquid.rb +67 -0
- data/lib/honeymaker/exchanges/kraken.rb +122 -0
- data/lib/honeymaker/exchanges/kucoin.rb +52 -0
- data/lib/honeymaker/exchanges/mexc.rb +56 -0
- data/lib/honeymaker/native/kraken.rb +263 -0
- data/lib/honeymaker/native.rb +42 -0
- data/lib/honeymaker/result.rb +46 -0
- data/lib/honeymaker/utils.rb +39 -0
- data/lib/honeymaker/version.rb +5 -0
- data/lib/honeymaker.rb +97 -0
- 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/sig/honeymaker.rbs +4 -0
- data/test/fixtures/binance_book_ticker.json +7 -0
- data/test/fixtures/binance_exchange_info.json +30 -0
- data/test/fixtures/bingx_symbols.json +26 -0
- data/test/fixtures/bingx_ticker.json +12 -0
- data/test/fixtures/bitget_symbols.json +28 -0
- data/test/fixtures/bitget_tickers.json +14 -0
- data/test/fixtures/bitmart_symbols.json +26 -0
- data/test/fixtures/bitmart_ticker.json +12 -0
- data/test/fixtures/bitrue_book_ticker.json +7 -0
- data/test/fixtures/bitrue_exchange_info.json +34 -0
- data/test/fixtures/bitvavo_book_ticker.json +7 -0
- data/test/fixtures/bitvavo_markets.json +30 -0
- data/test/fixtures/bybit_instruments.json +23 -0
- data/test/fixtures/bybit_tickers.json +19 -0
- data/test/fixtures/coinbase_product.json +7 -0
- data/test/fixtures/coinbase_products.json +24 -0
- data/test/fixtures/gemini_pubticker.json +10 -0
- data/test/fixtures/gemini_symbol_detail.json +10 -0
- data/test/fixtures/gemini_symbols.json +1 -0
- data/test/fixtures/hyperliquid_all_mids.json +5 -0
- data/test/fixtures/hyperliquid_spot_meta.json +12 -0
- data/test/fixtures/kraken_asset_pairs.json +43 -0
- data/test/fixtures/kraken_ticker.json +16 -0
- data/test/fixtures/kucoin_orderbook_level1.json +10 -0
- data/test/fixtures/kucoin_symbols.json +17 -0
- data/test/fixtures/mexc_book_ticker.json +7 -0
- data/test/fixtures/mexc_exchange_info.json +64 -0
- data/test/honeymaker/client_test.rb +90 -0
- data/test/honeymaker/clients/binance_client_test.rb +151 -0
- data/test/honeymaker/clients/binance_us_client_test.rb +25 -0
- data/test/honeymaker/clients/bingx_client_test.rb +106 -0
- data/test/honeymaker/clients/bitget_client_test.rb +146 -0
- data/test/honeymaker/clients/bitrue_client_test.rb +69 -0
- data/test/honeymaker/clients/bitvavo_client_test.rb +156 -0
- data/test/honeymaker/clients/bybit_client_test.rb +137 -0
- data/test/honeymaker/clients/coinbase_client_test.rb +153 -0
- data/test/honeymaker/clients/gemini_client_test.rb +100 -0
- data/test/honeymaker/clients/honeymaker_client_registry_test.rb +39 -0
- data/test/honeymaker/clients/hyperliquid_client_test.rb +292 -0
- data/test/honeymaker/clients/kraken_client_test.rb +255 -0
- data/test/honeymaker/clients/kraken_futures_client_test.rb +54 -0
- data/test/honeymaker/clients/kucoin_client_test.rb +149 -0
- data/test/honeymaker/clients/mexc_client_test.rb +98 -0
- data/test/honeymaker/clients/validation_test.rb +167 -0
- data/test/honeymaker/error_chain_test.rb +133 -0
- data/test/honeymaker/exchange_test.rb +138 -0
- data/test/honeymaker/exchanges/binance_test.rb +83 -0
- data/test/honeymaker/exchanges/binance_us_test.rb +53 -0
- data/test/honeymaker/exchanges/bingx_test.rb +65 -0
- data/test/honeymaker/exchanges/bitget_test.rb +65 -0
- data/test/honeymaker/exchanges/bitrue_test.rb +65 -0
- data/test/honeymaker/exchanges/bitvavo_test.rb +65 -0
- data/test/honeymaker/exchanges/bybit_test.rb +55 -0
- data/test/honeymaker/exchanges/coinbase_test.rb +106 -0
- data/test/honeymaker/exchanges/gemini_catalogue_test.rb +118 -0
- data/test/honeymaker/exchanges/gemini_test.rb +184 -0
- data/test/honeymaker/exchanges/hyperliquid_test.rb +64 -0
- data/test/honeymaker/exchanges/kraken_test.rb +191 -0
- data/test/honeymaker/exchanges/kucoin_test.rb +55 -0
- data/test/honeymaker/exchanges/mexc_test.rb +97 -0
- data/test/honeymaker/honeymaker_test.rb +27 -0
- data/test/honeymaker/result_test.rb +42 -0
- data/test/honeymaker/utils_test.rb +64 -0
- 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/fixture_helper.rb +13 -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
- data/test/test_helper.rb +9 -0
- metadata +254 -0
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
require "support/parity_helper"
|
|
5
|
+
|
|
6
|
+
class KrakenClientParityTest < Minitest::Test
|
|
7
|
+
include ParityHelper
|
|
8
|
+
|
|
9
|
+
NONCE = 1_727_000_000_000_001
|
|
10
|
+
SECRET = Base64.strict_encode64("test_secret_key_1234567890123456")
|
|
11
|
+
JSON_CT = { "content-type" => "application/json" }.freeze
|
|
12
|
+
SIGNED = %w[api-key api-sign content-type accept user-agent accept-encoding].freeze
|
|
13
|
+
|
|
14
|
+
def teardown
|
|
15
|
+
Honeymaker::Native::Ext::Kraken.fixed_nonce = nil if defined?(Honeymaker::Native::Ext::Kraken)
|
|
16
|
+
super
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def clients(key, secret, url)
|
|
20
|
+
[Honeymaker::Clients::Kraken, Honeymaker::Native::KrakenClient].map do |k|
|
|
21
|
+
k.new(api_key: key, api_secret: secret).tap do |c|
|
|
22
|
+
c.instance_variable_set(:@connection, c.send(:build_client_connection, url))
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Legacy then native against ONE double (same URL); returns [lr, nr, lreqs, nreqs].
|
|
28
|
+
def run_both(replies, key: "test_key", secret: SECRET)
|
|
29
|
+
Honeymaker::Clients::Kraken.any_instance.stubs(:nonce).returns(NONCE)
|
|
30
|
+
Honeymaker::Native::Ext::Kraken.fixed_nonce = NONCE
|
|
31
|
+
d = VenueDouble.new(replies)
|
|
32
|
+
legacy, native = clients(key, secret, d.url)
|
|
33
|
+
lr = begin; yield(legacy); rescue StandardError => e; e; end
|
|
34
|
+
lq = d.requests
|
|
35
|
+
d.reset!(replies)
|
|
36
|
+
nr = begin; yield(native); rescue StandardError => e; e; end
|
|
37
|
+
[lr, nr, lq, d.requests]
|
|
38
|
+
ensure
|
|
39
|
+
d&.close
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def assert_same_requests(legacy, native)
|
|
43
|
+
assert_equal legacy.size, native.size, "request count"
|
|
44
|
+
legacy.zip(native).each do |l, n|
|
|
45
|
+
assert_equal [l.method, l.target, l.body], [n.method, n.target, n.body]
|
|
46
|
+
assert_equal l.headers.slice(*SIGNED), n.headers.slice(*SIGNED)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def assert_parity(replies, **kw, &call)
|
|
51
|
+
lr, nr, lq, nq = run_both(replies, **kw, &call)
|
|
52
|
+
assert_same_ruby(lr, nr)
|
|
53
|
+
assert_same_requests(lq, nq)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def ok(result) = [200, JSON_CT, { "error" => [], "result" => result }.to_json]
|
|
57
|
+
def venue(*errors) = [200, JSON_CT, { "error" => errors, "result" => {} }.to_json]
|
|
58
|
+
|
|
59
|
+
COMMON = [
|
|
60
|
+
[500, JSON_CT, '{"error":["EService:Unavailable"]}'],
|
|
61
|
+
[500, { "content-type" => "text/plain" }, ""],
|
|
62
|
+
[200, { "content-type" => "text/html" }, "<html>maintenance</html>"],
|
|
63
|
+
[200, JSON_CT, ""],
|
|
64
|
+
[200, JSON_CT, "null"],
|
|
65
|
+
[200, JSON_CT, "{not json"],
|
|
66
|
+
[200, JSON_CT, '{"error":[],"result":{"dup":1,"dup":2}}'],
|
|
67
|
+
[200, JSON_CT, '{"error":["EGeneral:Invalid arguments"],"result":{}}'],
|
|
68
|
+
[200, JSON_CT, '{"error":[null,false],"result":{}}']
|
|
69
|
+
].freeze
|
|
70
|
+
|
|
71
|
+
def each_reply(*specific, &blk) = (specific + COMMON).each { |r| blk.call(r) }
|
|
72
|
+
|
|
73
|
+
def test_get_extended_balance_and_get_balances
|
|
74
|
+
bal = { "XXBT" => { "balance" => "0.5000000000", "hold_trade" => "0.1" }, "ZEUR" => { "balance" => "10.5" },
|
|
75
|
+
"USDT.F" => { "balance" => "0", "hold_trade" => "0" }, "XXDG" => { "balance" => 12_345_678_901_234_567_890 },
|
|
76
|
+
"BONK" => { "balance" => 0.1 }, "ETH2.S" => { "balance" => "1e-9" }, "NEG" => { "balance" => "-0.0000", "hold_trade" => "0.1" },
|
|
77
|
+
"" => { "balance" => "1" }, "..." => { "balance" => "2" } }
|
|
78
|
+
each_reply(ok(bal), venue("EAPI:Invalid key"), ok({ "X" => { "balance" => nil } }), ok({ "X" => { "balance" => "NaN" } }), ok([]), ok(["x"])) do |reply|
|
|
79
|
+
assert_parity([reply], &:get_extended_balance)
|
|
80
|
+
assert_parity([reply], &:get_balances)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def test_query_orders_info
|
|
85
|
+
orders = {
|
|
86
|
+
"O1" => { "status" => "closed", "descr" => { "type" => "BUY", "ordertype" => "market", "price" => "0" },
|
|
87
|
+
"vol" => "10", "vol_exec" => "10", "cost" => "500.5", "price" => "50.05", "oflags" => "fciq,viqc" },
|
|
88
|
+
"O2" => { "status" => "open", "descr" => { "type" => "sell", "ordertype" => "limit", "price" => "60000" },
|
|
89
|
+
"vol" => "0.01", "vol_exec" => "0", "cost" => "0", "price" => "0", "oflags" => "" },
|
|
90
|
+
"O3" => { "status" => "expired", "descr" => { "ordertype" => "stop-loss" }, "vol" => "1", "vol_exec" => "0",
|
|
91
|
+
"cost" => "0", "price" => "0" },
|
|
92
|
+
"O4" => { "status" => "pending", "descr" => false, "vol" => 1, "vol_exec" => 0.5, "cost" => "0", "price" => "0" }
|
|
93
|
+
}
|
|
94
|
+
each_reply(ok(orders), ok({}), ok([]), ok(["x"])) do |reply|
|
|
95
|
+
assert_parity([reply]) { |c| c.query_orders_info(txid: "O1,O2,O3,O4") }
|
|
96
|
+
assert_parity([reply]) { |c| c.query_orders_info(txid: "O1", trades: true, userref: 7, consolidate_taker: false) }
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def test_add_order_and_cancel
|
|
101
|
+
each_reply(ok({ "txid" => ["OX-1"], "descr" => { "order" => "buy 1 XBTUSD" } }), ok({ "descr" => {} }), ok({ "txid" => [] })) do |reply|
|
|
102
|
+
assert_parity([reply]) { |c| c.add_order(ordertype: "market", type: "buy", volume: "0.001", pair: "XBTUSDT", oflags: ["viqc"]) }
|
|
103
|
+
assert_parity([reply]) do |c|
|
|
104
|
+
c.add_order(ordertype: "limit", type: "sell", volume: BigDecimal("0.001"), pair: "XBTUSDT", price: "60000.1",
|
|
105
|
+
close: "limit", close_price: "61000", validate: true, userref: 42, timeinforce: "GTC", oflags: [])
|
|
106
|
+
end
|
|
107
|
+
assert_parity([reply]) { |c| c.cancel_order(txid: "OX-1") }
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def test_public_endpoints
|
|
112
|
+
each_reply(ok({ "XXBTZUSD" => { "a" => ["50000.1", "1", "1.000"], "b" => ["50000.0", "2", "2.000"] } })) do |reply|
|
|
113
|
+
assert_parity([reply]) { |c| c.get_ticker_information(pair: "XBTUSD") }
|
|
114
|
+
assert_parity([reply], &:get_ticker_information)
|
|
115
|
+
assert_parity([reply]) { |c| c.get_tradable_asset_pairs(pairs: %w[XBTUSD ETHUSD], aclass_base: "all") }
|
|
116
|
+
assert_parity([reply]) { |c| c.get_tradable_asset_pairs(pairs: []) }
|
|
117
|
+
assert_parity([reply]) { |c| c.get_asset_info(assets: %w[XBT], aclass: "currency") }
|
|
118
|
+
assert_parity([reply]) { |c| c.get_ohlc_data(pair: "XBTUSD", interval: 60, since: 1_700_000_000) }
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def test_other_private_endpoints
|
|
123
|
+
each_reply(ok({ "count" => 1 })) do |reply|
|
|
124
|
+
assert_parity([reply], &:get_api_key_info)
|
|
125
|
+
assert_parity([reply]) { |c| c.get_trades_history(type: "all", start: 1, end_time: 2, ofs: 50) }
|
|
126
|
+
assert_parity([reply]) { |c| c.get_ledgers(asset: "XBT", start: 1, end_time: 2, ofs: 0) }
|
|
127
|
+
assert_parity([reply]) { |c| c.get_withdraw_addresses(asset: "XBT", method: "Bitcoin") }
|
|
128
|
+
assert_parity([reply], &:get_withdraw_methods)
|
|
129
|
+
assert_parity([reply]) { |c| c.withdraw(asset: "XBT", key: "my key", amount: "0.01", address: "bc1q\u2026") }
|
|
130
|
+
assert_parity([reply]) { |c| c.get_earn_allocations(ascending: true, converted_asset: "USD", hide_zero_allocations: false) }
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def page(trades, count)
|
|
135
|
+
ok({ "trades" => trades.to_h { |t| [t[:id], { "ordertxid" => t[:o], "type" => "buy", "ordertype" => "market",
|
|
136
|
+
"vol" => t[:vol], "cost" => t[:cost], "fee" => "0.01",
|
|
137
|
+
"pair" => "XXBTZUSD", "time" => t[:time] }] }, "count" => count })
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def test_closed_orders_from_trades
|
|
141
|
+
p1 = page([{ id: "T1", o: "O1", vol: "0.1", cost: "5000", time: 1_700_000_000.1234 },
|
|
142
|
+
{ id: "T2", o: "OX", vol: "1", cost: "1", time: 1_700_000_001 }], 3)
|
|
143
|
+
p2 = page([{ id: "T3", o: "O1", vol: "0.2", cost: "10001", time: "1700000002.5" }], "3")
|
|
144
|
+
assert_parity([p1, p2]) { |c| c.closed_orders_from_trades(order_ids: %w[O1 O2], start: 1_699_000_000) }
|
|
145
|
+
assert_parity([p1, p2]) { |c| c.closed_orders_from_trades(order_ids: "O1", max_pages: 1) }
|
|
146
|
+
assert_parity([page([{ id: "T9", o: "O1", vol: "-0", cost: "0", time: 1 }], 1)]) { |c| c.closed_orders_from_trades(order_ids: %w[O1]) }
|
|
147
|
+
COMMON.each { |bad| assert_parity([p1, bad]) { |c| c.closed_orders_from_trades(order_ids: %w[O1]) } }
|
|
148
|
+
# Malformed pages must raise like legacy, never return partial fills (Codex r2 #1).
|
|
149
|
+
['{"error":[],"result":false}', '{"error":[],"result":"oops"}', '{"error":[],"result":{"trades":{"T5":null},"count":9}}',
|
|
150
|
+
'{"error":[],"result":{"trades":[1],"count":9}}',
|
|
151
|
+
'{"error":[],"result":{"trades":{"T6":{"ordertxid":"O1","vol":"1","cost":"1","fee":"0","time":1}},"count":true}}',
|
|
152
|
+
'{"error":[],"result":{"trades":[],"count":9}}', '{"error":[],"result":null}'].each do |bad|
|
|
153
|
+
assert_parity([p1, [200, JSON_CT, bad]]) { |c| c.closed_orders_from_trades(order_ids: %w[O1]) }
|
|
154
|
+
assert_parity([[200, JSON_CT, bad]]) { |c| c.closed_orders_from_trades(order_ids: %w[O1]) }
|
|
155
|
+
end
|
|
156
|
+
assert_parity([ok({ "trades" => {}, "count" => 0 })]) { |c| c.closed_orders_from_trades(order_ids: %w[O1]) }
|
|
157
|
+
assert_parity([p1]) { |c| c.closed_orders_from_trades(order_ids: []) }
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def test_closed_orders_from_trades_counts_a_trade_seen_on_two_pages_once
|
|
161
|
+
t1 = { id: "T1", o: "O1", vol: "0.1", cost: "5000", time: 1_700_000_000 }
|
|
162
|
+
t2 = { id: "T2", o: "O1", vol: "0.2", cost: "10001", time: 1_700_000_001 }
|
|
163
|
+
p1 = page([t1, t2], 3)
|
|
164
|
+
p2 = page([t2], 3)
|
|
165
|
+
assert_parity([p1, p2]) { |c| c.closed_orders_from_trades(order_ids: %w[O1], start: 1_699_000_000) }
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def test_aggregate_edges
|
|
169
|
+
[{ "type" => false }, { "type" => 1 }, { "type" => nil }, { "ordertype" => 5 }].each do |over|
|
|
170
|
+
bad_page = page([{ id: "T1", o: "O1", vol: "1", cost: "2", time: 1 }], 1)
|
|
171
|
+
body = JSON.parse(bad_page[2])
|
|
172
|
+
body["result"]["trades"]["T1"].merge!(over)
|
|
173
|
+
assert_parity([[200, JSON_CT, body.to_json]]) { |c| c.closed_orders_from_trades(order_ids: %w[O1]) }
|
|
174
|
+
end
|
|
175
|
+
assert_parity([page([{ id: "T1", o: "O1", vol: "1", cost: "2", time: 1 }], "1_000"),
|
|
176
|
+
page([{ id: "T2", o: "O1", vol: "1", cost: "2", time: 2 }], "99999999999999999999999")]) do |c|
|
|
177
|
+
c.closed_orders_from_trades(order_ids: %w[O1], max_pages: 2)
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def test_validate
|
|
182
|
+
[ok({ "XXBT" => { "balance" => "1" } }), venue("EAPI:Invalid key"), [403, JSON_CT, "{}"],
|
|
183
|
+
[200, { "content-type" => "text/html" }, "x"], [200, JSON_CT, '{"error":[null]}']].each do |reply|
|
|
184
|
+
assert_parity([reply]) { |c| c.validate(:trading) }
|
|
185
|
+
assert_parity([reply]) { |c| c.validate(:read) }
|
|
186
|
+
end
|
|
187
|
+
assert_parity([ok({})], key: "", &:validate)
|
|
188
|
+
assert_parity([ok({})]) { |c| c.validate(:bogus) }
|
|
189
|
+
["", "null", "[1]", "5", "true", '"abc"', '"error"'].each do |body|
|
|
190
|
+
assert_parity([[200, JSON_CT, body]]) { |c| c.validate(:trading) }
|
|
191
|
+
assert_parity([[200, JSON_CT, body]]) { |c| c.validate(:read) }
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def test_string_balance_entries
|
|
196
|
+
["abc", "balance", "hold_trade balance"].each do |balance|
|
|
197
|
+
assert_parity([ok({ "X" => balance })], &:get_balances)
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def test_array_balance_entries
|
|
202
|
+
[[["XXBT", { "balance" => "1" }]], ["x"], [["X", "abc"]], [["X", "balance"]],
|
|
203
|
+
[[nil, { "balance" => "1" }]], [[]],
|
|
204
|
+
[["XXBT", { "balance" => "1" }, "ignored"], ["XBT", { "balance" => "2" }]]].each do |balances|
|
|
205
|
+
assert_parity([ok(balances)], &:get_balances)
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def test_array_order_entries
|
|
210
|
+
raw = { "vol" => "1", "vol_exec" => "1", "cost" => "2", "price" => "0" }
|
|
211
|
+
[[["O1", raw]], [[5, raw]], [[nil, raw]], ["x"], [["O1", "abc"]], [[]],
|
|
212
|
+
[["O1", raw, "ignored"], ["O1", raw.merge("vol" => "2")]]].each do |orders|
|
|
213
|
+
assert_parity([ok(orders)]) { |c| c.query_orders_info(txid: "O1") }
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def test_string_order_descriptions
|
|
218
|
+
["abc", "type", "ordertype price", "type ordertype price"].each do |descr|
|
|
219
|
+
raw = { "descr" => descr, "vol" => "1", "vol_exec" => "1", "cost" => "2", "price" => "0" }
|
|
220
|
+
assert_parity([ok({ "O1" => raw })]) { |c| c.query_orders_info(txid: "O1") }
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def test_hash_and_string_order_txids
|
|
225
|
+
[{ "Z" => "first", "A" => "second" }, {}, "OX-1"].each do |txid|
|
|
226
|
+
assert_parity([ok({ "txid" => txid })]) do |c|
|
|
227
|
+
c.add_order(ordertype: "market", type: "buy", volume: "1", pair: "X")
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def test_side_downcase_is_context_independent
|
|
233
|
+
raw = { "descr" => { "type" => "ΑΣ" }, "vol" => "1", "vol_exec" => "1", "cost" => "2", "price" => "0" }
|
|
234
|
+
assert_parity([ok({ "O1" => raw })]) { |c| c.query_orders_info(txid: "O1") }
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def test_nil_parity_has_no_deprecation_warnings
|
|
238
|
+
_, warnings = capture_io do
|
|
239
|
+
assert_parity([ok({})]) { |c| c.add_order(ordertype: "market", type: "buy", volume: "1", pair: "X") }
|
|
240
|
+
end
|
|
241
|
+
refute_match(/DEPRECATED/, warnings)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def test_binary_maintenance_page_is_unreadable_like_legacy
|
|
245
|
+
page = [200, { "content-type" => "text/html" }, "<html>\xFF\xFE maintenance</html>".b]
|
|
246
|
+
assert_parity([page], &:get_balances)
|
|
247
|
+
assert_parity([page]) { |c| c.query_orders_info(txid: "O1") }
|
|
248
|
+
assert_parity([page]) { |c| c.add_order(ordertype: "market", type: "buy", volume: "1", pair: "X") }
|
|
249
|
+
assert_parity([page]) { |c| c.validate(:trading) }
|
|
250
|
+
assert_parity([ok({ "trades" => { "T1" => { "ordertxid" => "O1", "vol" => "1", "cost" => "1", "fee" => "0", "time" => 1 } }, "count" => 9 }), page]) do |c|
|
|
251
|
+
c.closed_orders_from_trades(order_ids: %w[O1])
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def test_invalid_utf8_in_add_order_json
|
|
256
|
+
['{"error":[],"result":{"txid":["O1"],"descr":{"order":"buy X"}}}',
|
|
257
|
+
'{"error":[],"result":{"txid":["OX"]}}'].each do |body|
|
|
258
|
+
assert_parity([[200, JSON_CT, body.sub("X", "\xff")]]) do |c|
|
|
259
|
+
c.add_order(ordertype: "market", type: "buy", volume: "1", pair: "X")
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def direct_results(data)
|
|
265
|
+
[Honeymaker::Clients::Kraken, Honeymaker::Native::KrakenClient].map do |klass|
|
|
266
|
+
client = klass.new
|
|
267
|
+
response = Honeymaker::Result::Success.new(data)
|
|
268
|
+
client.stubs(:get_extended_balance).returns(response)
|
|
269
|
+
client.stubs(:post_private).returns(response)
|
|
270
|
+
client.stubs(:get_trades_history).returns(response)
|
|
271
|
+
begin; yield(client); rescue StandardError => e; e; end
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def test_binary_error_text_through_client
|
|
276
|
+
data = { "error" => ["EAPI:Invalid nonce \xff\xfe".b] }
|
|
277
|
+
assert_same_ruby(*direct_results(data, &:get_balances))
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def test_binary_and_invalid_normalizer_values
|
|
281
|
+
["\xff", "\xff".b].each do |suffix|
|
|
282
|
+
data = { "error" => [], "result" => { "txid" => ["O" + suffix] } }
|
|
283
|
+
assert_same_ruby(*direct_results(data) { |c| c.add_order(ordertype: "market", type: "buy", volume: "1", pair: "X") })
|
|
284
|
+
["balance" + suffix, { "balance" => "1" + suffix }].each do |balance|
|
|
285
|
+
assert_same_ruby(*direct_results({ "result" => { "X" => balance } }, &:get_balances))
|
|
286
|
+
end
|
|
287
|
+
raw = { "vol" => "1", "vol_exec" => "1", "cost" => "2", "price" => "0" }
|
|
288
|
+
[{ "descr" => "type" + suffix }, { "descr" => { "type" => "BUY" + suffix } },
|
|
289
|
+
{ "oflags" => "viqc," + suffix }].each do |over|
|
|
290
|
+
assert_same_ruby(*direct_results({ "result" => { "O1" => raw.merge(over) } }) { |c| c.query_orders_info(txid: "O1") })
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def test_invalid_string_keys_and_trade_side
|
|
296
|
+
["\xff", "\xff".b].each do |suffix|
|
|
297
|
+
assert_same_ruby(*direct_results({ "result" => { "X" + suffix => { "balance" => "1" } } }, &:get_balances))
|
|
298
|
+
raw = { "vol" => "1", "vol_exec" => "1", "cost" => "2", "price" => "0" }
|
|
299
|
+
assert_same_ruby(*direct_results({ "result" => { "O" + suffix => raw } }) { |c| c.query_orders_info(txid: "O1") })
|
|
300
|
+
trade = { "ordertxid" => "O1", "type" => "BUY" + suffix, "vol" => "1", "cost" => "2", "fee" => "0", "time" => 1 }
|
|
301
|
+
assert_same_ruby(*direct_results({ "result" => { "trades" => { "T1" => trade }, "count" => 1 } }) do |c|
|
|
302
|
+
c.closed_orders_from_trades(order_ids: ["O1"])
|
|
303
|
+
end)
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def test_decimal_normalizers_under_gc_stress
|
|
308
|
+
cases = [
|
|
309
|
+
[{ "result" => { "XXBT" => { "balance" => "2", "hold_trade" => "0.5" } } }, ->(c) { c.get_balances }],
|
|
310
|
+
[{ "result" => { "O1" => { "vol" => "1", "vol_exec" => "1", "cost" => "2", "price" => "2" } } },
|
|
311
|
+
->(c) { c.query_orders_info(txid: "O1") }],
|
|
312
|
+
[{ "result" => { "trades" => { "T1" => { "ordertxid" => "O1", "type" => "buy", "ordertype" => "market",
|
|
313
|
+
"vol" => "1", "cost" => "2", "fee" => "0.1", "time" => 1 } }, "count" => 1 } },
|
|
314
|
+
->(c) { c.closed_orders_from_trades(order_ids: ["O1"]) }]
|
|
315
|
+
]
|
|
316
|
+
cases.each do |data, call|
|
|
317
|
+
results = direct_results(data) do |client|
|
|
318
|
+
GC.stress = true
|
|
319
|
+
begin; call.call(client); ensure; GC.stress = false; end
|
|
320
|
+
end
|
|
321
|
+
assert_same_ruby(*results)
|
|
322
|
+
end
|
|
323
|
+
ensure
|
|
324
|
+
GC.stress = false
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def test_unexpected_native_verdict_raises_shape_error
|
|
328
|
+
client = Honeymaker::Native::KrakenClient.new
|
|
329
|
+
client.stubs(:native).returns(stub(finish: ["bogus"]))
|
|
330
|
+
error = assert_raises(Honeymaker::Native::ShapeError) do
|
|
331
|
+
client.send(:finish, "balances", Honeymaker::Result::Success.new({})) { |x| x }
|
|
332
|
+
end
|
|
333
|
+
assert_equal 'unexpected verdict "bogus"', error.message
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def test_unauthenticated_private_call
|
|
337
|
+
assert_parity([venue("EAPI:Invalid key")], key: nil, secret: nil, &:get_extended_balance)
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def test_constants_and_surface
|
|
341
|
+
assert_equal Honeymaker::Clients::Kraken.rate_limits, Honeymaker::Native::KrakenClient.rate_limits
|
|
342
|
+
assert_equal Honeymaker::Clients::Kraken::URL, Honeymaker::Native::KrakenClient::URL
|
|
343
|
+
legacy, native = Honeymaker::Clients::Kraken, Honeymaker::Native::KrakenClient
|
|
344
|
+
assert_equal legacy.public_instance_methods(false).sort, native.public_instance_methods(false).sort
|
|
345
|
+
legacy.public_instance_methods(false).each do |m|
|
|
346
|
+
assert_equal legacy.instance_method(m).parameters, native.instance_method(m).parameters, "#{m} signature"
|
|
347
|
+
end
|
|
348
|
+
assert_respond_to native, :reset_nonce_state!
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
def test_rejected_arguments_raise_before_any_request
|
|
352
|
+
[->(c) { c.add_order(ordertype: "market", type: "buy", volume: "1", pair: "X", oflags: nil) },
|
|
353
|
+
->(c) { c.add_order(ordertype: "market", type: "buy", volume: "1", pair: "X", oflags: "viqc") },
|
|
354
|
+
->(c) { c.get_tradable_asset_pairs(pairs: "XBTUSD") },
|
|
355
|
+
->(c) { c.get_asset_info(assets: 5) }].each do |call|
|
|
356
|
+
lr, nr, lq, nq = run_both([ok({})], &call)
|
|
357
|
+
assert_same_ruby(lr, nr)
|
|
358
|
+
assert_equal [0, 0], [lq.size, nq.size], "no request may leave on a rejected argument"
|
|
359
|
+
end
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
def test_argument_value_shapes_reach_the_wire_like_legacy
|
|
363
|
+
assert_parity([ok({})]) { |c| c.query_orders_info(txid: %w[O1 O2]) } # repeated txid=
|
|
364
|
+
assert_parity([ok({})]) { |c| c.add_order(ordertype: "market", type: "buy", volume: 1, pair: "X", oflags: [false]) }
|
|
365
|
+
assert_parity([ok({})]) { |c| c.add_order(ordertype: "market", type: "buy", volume: "1", pair: "X", validate: false, userref: 0) }
|
|
366
|
+
assert_parity([ok({})]) { |c| c.get_ohlc_data(pair: :XBTUSD, interval: 60.0, since: nil) }
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
def test_concurrent_private_calls_get_unique_increasing_nonces
|
|
370
|
+
Honeymaker::Native::Ext::Kraken.fixed_nonce = nil
|
|
371
|
+
d = VenueDouble.new([ok({})])
|
|
372
|
+
c = Honeymaker::Native::KrakenClient.new(api_key: "k", api_secret: SECRET)
|
|
373
|
+
c.instance_variable_set(:@connection, c.send(:build_client_connection, d.url))
|
|
374
|
+
8.times.map { Thread.new { 10.times { assert c.get_extended_balance.success? } } }.each(&:join)
|
|
375
|
+
nonces = d.requests.map { |r| r.body[/nonce=(\d+)/, 1].to_i }
|
|
376
|
+
assert_equal 80, nonces.size
|
|
377
|
+
assert_equal nonces.uniq.size, nonces.size, "unique per key"
|
|
378
|
+
ensure
|
|
379
|
+
d&.close
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
def test_api_key_with_trailing_newline
|
|
383
|
+
assert_parity([ok({})], key: "test_key\n", &:get_extended_balance)
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
def test_native_uses_the_proxied_connection
|
|
387
|
+
d = VenueDouble.new([ok({})])
|
|
388
|
+
p = ProxyDouble.new(d.port)
|
|
389
|
+
c = Honeymaker::Native::KrakenClient.new(api_key: "k", api_secret: SECRET, proxy: p.url)
|
|
390
|
+
c.instance_variable_set(:@connection, c.send(:build_client_connection, d.url))
|
|
391
|
+
assert c.get_extended_balance.success?
|
|
392
|
+
assert_equal 1, p.lines.size, "request went through the proxy"
|
|
393
|
+
assert_match %r{\APOST http://127\.0\.0\.1:#{d.port}/0/private/BalanceEx }, p.lines.first
|
|
394
|
+
ensure
|
|
395
|
+
p&.close
|
|
396
|
+
d&.close
|
|
397
|
+
end
|
|
398
|
+
end
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
require "support/parity_helper"
|
|
5
|
+
|
|
6
|
+
class KrakenExchangeParityTest < Minitest::Test
|
|
7
|
+
include ParityHelper
|
|
8
|
+
include FixtureHelper
|
|
9
|
+
|
|
10
|
+
JSON_CT = { "content-type" => "application/json" }.freeze
|
|
11
|
+
|
|
12
|
+
def assert_parity(reply)
|
|
13
|
+
d = VenueDouble.new([reply])
|
|
14
|
+
results = [Honeymaker::Exchanges::Kraken, Honeymaker::Native::KrakenExchange].map do |k|
|
|
15
|
+
d.reset!([reply])
|
|
16
|
+
e = k.new
|
|
17
|
+
e.instance_variable_set(:@connection, e.send(:build_connection, d.url))
|
|
18
|
+
r = begin; yield(e); rescue StandardError => x; x; end
|
|
19
|
+
[r, d.requests.map { |q| [q.method, q.target] }]
|
|
20
|
+
end
|
|
21
|
+
assert_same_ruby(results[0][0], results[1][0])
|
|
22
|
+
assert_equal results[0][1], results[1][1]
|
|
23
|
+
ensure
|
|
24
|
+
d&.close
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def test_tickers_info_from_the_real_fixture
|
|
28
|
+
assert_parity([200, JSON_CT, File.read(fixture_path("kraken_asset_pairs.json"))], &:get_tickers_info)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def test_tickers_info_edges
|
|
32
|
+
pairs = {
|
|
33
|
+
"XXBTZUSD" => { "altname" => "XBTUSD", "wsname" => "XBT/USD", "ordermin" => "0.0001", "costmin" => "0.5",
|
|
34
|
+
"lot_decimals" => 8, "cost_decimals" => 5, "pair_decimals" => 1, "status" => "online" },
|
|
35
|
+
"NVDASPVUSD" => { "altname" => "NVDAxUSD", "wsname" => "NVDAx/USD", "aclass_base" => "tokenized_asset", "ordermin" => "0.1" },
|
|
36
|
+
"NVDAxUSD" => { "altname" => "NVDAxUSD", "wsname" => "NVDAx/USD", "aclass_base" => "tokenized_asset" },
|
|
37
|
+
"NOWS" => { "altname" => "NOWS", "wsname" => "" },
|
|
38
|
+
"WEIRD" => { "altname" => "W", "wsname" => "ABC", "costmin" => 7.5, "status" => "cancel_only" },
|
|
39
|
+
"XF" => { "altname" => "XF", "wsname" => "ETH/XBT", "costmin" => "1" }
|
|
40
|
+
}
|
|
41
|
+
[[200, JSON_CT, { "error" => [], "result" => pairs }.to_json],
|
|
42
|
+
[200, JSON_CT, '{"error":["EGeneral:Temporary lockout"]}'],
|
|
43
|
+
[500, JSON_CT, "{}"]].each { |reply| assert_parity(reply, &:get_tickers_info) }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def test_one_malformed_row_fails_the_whole_catalogue_like_legacy
|
|
47
|
+
replies = [false, 1, { "x" => 1 }].map do |bad|
|
|
48
|
+
pairs = { "XXBTZUSD" => { "altname" => "XBTUSD", "wsname" => "XBT/USD", "status" => "online" },
|
|
49
|
+
"BAD" => { "altname" => "BAD", "wsname" => bad } }
|
|
50
|
+
[200, JSON_CT, { "error" => [], "result" => pairs }.to_json]
|
|
51
|
+
end
|
|
52
|
+
# Non-object bodies too: Failure on both, messages differ (spec §5.1).
|
|
53
|
+
replies += [[200, { "content-type" => "text/html" }, "x"], [200, JSON_CT, "null"],
|
|
54
|
+
[200, { "content-type" => "text/html" }, "\xFF\xFE".b]]
|
|
55
|
+
replies.each do |reply|
|
|
56
|
+
bad = reply[2][0, 40]
|
|
57
|
+
d = VenueDouble.new([reply])
|
|
58
|
+
rs = [Honeymaker::Exchanges::Kraken, Honeymaker::Native::KrakenExchange].map do |k|
|
|
59
|
+
d.reset!([reply])
|
|
60
|
+
e = k.new
|
|
61
|
+
e.instance_variable_set(:@connection, e.send(:build_connection, d.url))
|
|
62
|
+
e.get_tickers_info
|
|
63
|
+
end
|
|
64
|
+
assert rs.all?(&:failure?), "wsname=#{bad.inspect} must fail the catalogue on both"
|
|
65
|
+
assert_nullable_equal rs[0].data, rs[1].data
|
|
66
|
+
ensure
|
|
67
|
+
d&.close
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def test_bid_ask_and_price
|
|
72
|
+
t = { "error" => [], "result" => { "XXBTZUSD" => { "a" => ["50000.10", "1", "1.000"], "b" => ["49999.90", "2", "2.000"] } } }.to_json
|
|
73
|
+
assert_parity([200, JSON_CT, t]) { |e| e.get_bid_ask("XBTUSD") }
|
|
74
|
+
assert_parity([200, JSON_CT, t]) { |e| e.get_price("XBTUSD") }
|
|
75
|
+
assert_parity([200, JSON_CT, '{"error":["EQuery:Unknown asset pair"]}']) { |e| e.get_bid_ask("NOPE") }
|
|
76
|
+
assert_parity([200, JSON_CT, '{"error":[],"result":{"X":{"a":["NaN"],"b":["-0"]}}}']) { |e| e.get_bid_ask("X") }
|
|
77
|
+
assert_parity([200, JSON_CT, '{"error":[],"result":{"X":{"a":[50001],"b":[50000]}}}']) { |e| e.get_bid_ask("X") }
|
|
78
|
+
assert_parity([200, JSON_CT, '{"error":[],"result":{"X":{"a":[1.5],"b":[1.25]}}}']) { |e| e.get_bid_ask("X") }
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def test_invalid_utf8_ticker_fields_and_price_strings
|
|
82
|
+
['{"error":[],"result":{"X":{"altname":"X","wsname":"X/USD"}}}',
|
|
83
|
+
'{"error":[],"result":{"X":{"altname":"A","wsname":"X/USD"}}}'].each do |body|
|
|
84
|
+
assert_parity([200, JSON_CT, body.sub('"altname":"X"', '"altname":"X' + "\xff" + '"').sub('"wsname":"X/USD"', '"wsname":"X' + "\xff" + '/USD"')], &:get_tickers_info)
|
|
85
|
+
end
|
|
86
|
+
["1\xff", "\xff1"].each do |value|
|
|
87
|
+
["[\"#{value}\"]", "\"#{value}\""].each do |price|
|
|
88
|
+
body = '{"error":[],"result":{"X":{"a":["2"],"b":' + price + '}}}'
|
|
89
|
+
assert_parity([200, JSON_CT, body]) { |e| e.get_bid_ask("X") }
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def test_classify_invalid_utf8_keeps_legacy_exception
|
|
95
|
+
["EAPI:Invalid nonce \xff", "EAPI:Invalid nonce".encode("UTF-16LE")].each do |message|
|
|
96
|
+
results = [Honeymaker::Exchanges::Kraken, Honeymaker::Native::KrakenExchange].map do |klass|
|
|
97
|
+
begin; klass.new.classify_error(message); rescue StandardError => e; e; end
|
|
98
|
+
end
|
|
99
|
+
assert_same_ruby(*results)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def test_classify_binary_error_bytes_and_capture_encoding
|
|
104
|
+
["EAPI:Invalid nonce \xff\xfe".b,
|
|
105
|
+
"EAccount:Invalid permissions:X\xff trading restricted for US.".b,
|
|
106
|
+
"prefix \xff EService:Busy".b, "unknown \xff".b].each do |message|
|
|
107
|
+
assert_same_ruby(Honeymaker::Exchanges::Kraken.new.classify_error(message),
|
|
108
|
+
Honeymaker::Native::KrakenExchange.new.classify_error(message))
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def test_classify_error_corpus
|
|
113
|
+
["EAccount:Invalid permissions:NVDAx trading restricted for DE.",
|
|
114
|
+
"EAccount:Invalid permissions:XBT trading restricted for US",
|
|
115
|
+
"EAccount:Invalid permissions:XBT trading restricted for \u00dc.",
|
|
116
|
+
"prefix EAPI:Invalid nonce suffix", "EGeneral:Internal error", "EService:Unavailable",
|
|
117
|
+
"EService:Busy", "EService:Deadline elapsed", "EOrder:Insufficient funds", "", nil].each do |msg|
|
|
118
|
+
assert_nullable_equal Honeymaker::Exchanges::Kraken.new.classify_error(msg),
|
|
119
|
+
Honeymaker::Native::KrakenExchange.new.classify_error(msg), msg.inspect
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def test_non_object_catalogue_body_is_a_failure_on_both
|
|
124
|
+
[[200, { "content-type" => "text/html" }, "x"], [200, JSON_CT, "null"]].each do |reply|
|
|
125
|
+
d = VenueDouble.new([reply])
|
|
126
|
+
rs = [Honeymaker::Exchanges::Kraken, Honeymaker::Native::KrakenExchange].map do |k|
|
|
127
|
+
d.reset!([reply])
|
|
128
|
+
e = k.new
|
|
129
|
+
e.instance_variable_set(:@connection, e.send(:build_connection, d.url))
|
|
130
|
+
e.get_tickers_info
|
|
131
|
+
end
|
|
132
|
+
assert rs.all?(&:failure?)
|
|
133
|
+
assert_nullable_equal rs[0].data, rs[1].data
|
|
134
|
+
ensure
|
|
135
|
+
d&.close
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def assert_nullable_equal(expected, actual, msg = nil)
|
|
140
|
+
expected.nil? ? assert_nil(actual, msg) : assert_equal(expected, actual, msg)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def test_catalogue_ruby_shapes
|
|
144
|
+
[[], {}, [["X", { "wsname" => "X/USD", "altname" => "XUSD" }]],
|
|
145
|
+
{ "X" => "no fields" },
|
|
146
|
+
{ "X" => { "wsname" => {} } },
|
|
147
|
+
{ "X" => { "wsname" => [] } }].each do |pairs|
|
|
148
|
+
assert_parity([200, JSON_CT, { "error" => [], "result" => pairs }.to_json], &:get_tickers_info)
|
|
149
|
+
end
|
|
150
|
+
pairs = ["/", "//", "X/", "X//", "/USD", "X//USD", "X/USD/"].to_h do |ws|
|
|
151
|
+
[ws, { "wsname" => ws, "altname" => ws, "costmin" => false, "status" => nil }]
|
|
152
|
+
end
|
|
153
|
+
assert_parity([200, JSON_CT, { "error" => [false, nil], "result" => pairs }.to_json], &:get_tickers_info)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def test_bid_ask_ruby_indexing_and_decimal_errors
|
|
157
|
+
[{ "a" => "23", "b" => "12" }, { "a" => 3, "b" => 2 },
|
|
158
|
+
{ "a" => ["invalid"], "b" => ["1"] }, "ab"].each do |row|
|
|
159
|
+
[{ "X" => row }, [["X", row]]].each do |result|
|
|
160
|
+
assert_parity([200, JSON_CT, { "error" => [], "result" => result }.to_json]) { |e| e.get_bid_ask("X") }
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def test_catalogue_under_gc_stress
|
|
166
|
+
body = File.read(fixture_path("kraken_asset_pairs.json"))
|
|
167
|
+
GC.stress = true
|
|
168
|
+
assert_parity([200, JSON_CT, body], &:get_tickers_info)
|
|
169
|
+
ensure
|
|
170
|
+
GC.stress = false
|
|
171
|
+
end
|
|
172
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
require "yaml"
|
|
5
|
+
|
|
6
|
+
class BranchHardeningTest < Minitest::Test
|
|
7
|
+
ROOT = File.expand_path("../..", __dir__)
|
|
8
|
+
|
|
9
|
+
def test_workflows_parse_and_use_read_only_default_permissions
|
|
10
|
+
%w[test native-gems release].each do |name|
|
|
11
|
+
path = File.join(ROOT, ".github/workflows/#{name}.yml")
|
|
12
|
+
workflow = YAML.load_file(path)
|
|
13
|
+
assert_equal({ "contents" => "read" }, workflow["permissions"], name)
|
|
14
|
+
File.foreach(path).grep(/uses: /).each do |line|
|
|
15
|
+
next if line.include?("uses: ./")
|
|
16
|
+
|
|
17
|
+
assert_match(%r{uses: [\w/-]+@[0-9a-f]{40} # v\d+\.\d+\.\d+\s*$}, line, path)
|
|
18
|
+
end
|
|
19
|
+
if name == "release"
|
|
20
|
+
assert_equal({ "id-token" => "write", "contents" => "read" }, workflow.dig("jobs", "publish", "permissions"))
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def test_source_gem_does_not_pin_the_rust_toolchain
|
|
26
|
+
spec = Gem::Specification.load(File.join(ROOT, "honeymaker.gemspec"))
|
|
27
|
+
refute_includes spec.files, "rust-toolchain.toml"
|
|
28
|
+
assert_includes spec.files, "Cargo.toml"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def test_unused_integer_coercion_is_removed
|
|
32
|
+
refute_match(/pub fn to_i\b/, File.read(File.join(ROOT, "crates/honeymaker/src/semantics.rs")))
|
|
33
|
+
end
|
|
34
|
+
end
|