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.
- 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,149 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
|
|
5
|
+
class Honeymaker::Clients::KucoinTest < Minitest::Test
|
|
6
|
+
def setup
|
|
7
|
+
@client = Honeymaker::Clients::Kucoin.new(
|
|
8
|
+
api_key: "test_key", api_secret: "test_secret", passphrase: "test_pass"
|
|
9
|
+
)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def test_url
|
|
13
|
+
assert_equal "https://api.kucoin.com", Honeymaker::Clients::Kucoin::URL
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def test_accepts_passphrase
|
|
17
|
+
assert_equal "test_pass", @client.passphrase
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def test_get_symbols
|
|
21
|
+
stub_connection(:get, { "data" => [{ "symbol" => "BTC-USDT" }] })
|
|
22
|
+
result = @client.get_symbols
|
|
23
|
+
assert result.success?
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def test_get_all_tickers
|
|
27
|
+
stub_connection(:get, { "data" => { "ticker" => [] } })
|
|
28
|
+
result = @client.get_all_tickers
|
|
29
|
+
assert result.success?
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def test_get_accounts
|
|
33
|
+
stub_connection(:get, { "data" => [{ "currency" => "BTC" }] })
|
|
34
|
+
result = @client.get_accounts
|
|
35
|
+
assert result.success?
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def test_place_order
|
|
39
|
+
stub_connection(:post, { "code" => "200000", "data" => { "orderId" => "123" } })
|
|
40
|
+
result = @client.place_order(client_oid: "c1", side: "buy", symbol: "BTC-USDT", type: "market", funds: "100")
|
|
41
|
+
assert result.success?
|
|
42
|
+
assert_equal "123", result.data[:order_id]
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def test_get_order
|
|
46
|
+
stub_connection(:get, { "code" => "200000", "data" => {
|
|
47
|
+
"id" => "123", "symbol" => "BTC-USDT", "type" => "market", "side" => "buy",
|
|
48
|
+
"isActive" => false, "cancelExist" => false, "dealSize" => "0.001",
|
|
49
|
+
"dealFunds" => "50", "size" => "0.001", "price" => "50000"
|
|
50
|
+
} })
|
|
51
|
+
result = @client.get_order(order_id: "123")
|
|
52
|
+
assert result.success?
|
|
53
|
+
assert_equal :closed, result.data[:status]
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def test_cancel_order
|
|
57
|
+
stub_connection(:delete, { "data" => { "cancelledOrderIds" => ["123"] } })
|
|
58
|
+
result = @client.cancel_order(order_id: "123")
|
|
59
|
+
assert result.success?
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def test_withdraw
|
|
63
|
+
stub_connection(:post, { "data" => { "withdrawalId" => "w1" } })
|
|
64
|
+
result = @client.withdraw(currency: "BTC", address: "addr", amount: "0.1")
|
|
65
|
+
assert result.success?
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def test_get_historical_orders
|
|
69
|
+
stub_connection(:get, { "data" => { "items" => [{ "id" => "ho1", "symbol" => "BTC-USDT" }] } })
|
|
70
|
+
result = @client.get_historical_orders
|
|
71
|
+
assert result.success?
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def test_get_fills
|
|
75
|
+
stub_connection(:get, { "data" => { "items" => [{ "tradeId" => "t1" }] } })
|
|
76
|
+
result = @client.get_fills(symbol: "BTC-USDT")
|
|
77
|
+
assert result.success?
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def test_get_deposits
|
|
81
|
+
stub_connection(:get, { "data" => { "items" => [{ "currency" => "BTC" }] } })
|
|
82
|
+
result = @client.get_deposits
|
|
83
|
+
assert result.success?
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def test_get_withdrawals
|
|
87
|
+
stub_connection(:get, { "data" => { "items" => [{ "currency" => "ETH" }] } })
|
|
88
|
+
result = @client.get_withdrawals
|
|
89
|
+
assert result.success?
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def test_signed_headers_include_passphrase_and_version
|
|
93
|
+
ts = "1234567890"
|
|
94
|
+
headers = @client.send(:signed_headers, ts, "payload")
|
|
95
|
+
assert_equal "test_key", headers[:"KC-API-KEY"]
|
|
96
|
+
assert_equal "2", headers[:"KC-API-KEY-VERSION"]
|
|
97
|
+
assert headers.key?(:"KC-API-SIGN")
|
|
98
|
+
assert headers.key?(:"KC-API-PASSPHRASE")
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def test_passphrase_is_signed
|
|
102
|
+
ts = "1234567890"
|
|
103
|
+
headers = @client.send(:signed_headers, ts, "payload")
|
|
104
|
+
# KuCoin v2 signs the passphrase with the secret
|
|
105
|
+
raw_passphrase = "test_pass"
|
|
106
|
+
expected = Base64.strict_encode64(OpenSSL::HMAC.digest("sha256", "test_secret", raw_passphrase))
|
|
107
|
+
assert_equal expected, headers[:"KC-API-PASSPHRASE"]
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# KuCoin signals business errors in an HTTP-200 envelope. Callers classify on this text
|
|
111
|
+
# (insufficient funds, bad key, stale timestamp), so the code and msg must survive.
|
|
112
|
+
def test_place_order_keeps_api_code_and_message
|
|
113
|
+
stub_connection(:post, { "code" => "200004", "msg" => "Balance insufficient!" })
|
|
114
|
+
result = @client.place_order(client_oid: "c1", side: "buy", symbol: "BTC-USDT", type: "market", funds: "100")
|
|
115
|
+
assert result.failure?
|
|
116
|
+
assert_includes result.errors.first, "Balance insufficient!"
|
|
117
|
+
assert_includes result.errors.first, "200004"
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def test_get_order_keeps_api_code_and_message
|
|
121
|
+
stub_connection(:get, { "code" => "400002", "msg" => "Invalid KC-API-TIMESTAMP." })
|
|
122
|
+
result = @client.get_order(order_id: "123")
|
|
123
|
+
assert result.failure?
|
|
124
|
+
assert_includes result.errors.first, "Invalid KC-API-TIMESTAMP."
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def test_get_balances_keeps_api_code_and_message
|
|
128
|
+
stub_connection(:get, { "code" => "400003", "msg" => "KC-API-KEY not exists" })
|
|
129
|
+
result = @client.get_balances
|
|
130
|
+
assert result.failure?
|
|
131
|
+
assert_includes result.errors.first, "KC-API-KEY not exists"
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def test_api_error_falls_back_when_body_carries_no_detail
|
|
135
|
+
stub_connection(:post, { "code" => nil })
|
|
136
|
+
result = @client.place_order(client_oid: "c1", side: "buy", symbol: "BTC-USDT", type: "market", funds: "100")
|
|
137
|
+
assert result.failure?
|
|
138
|
+
assert_equal "KuCoin API error", result.errors.first
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
private
|
|
142
|
+
|
|
143
|
+
def stub_connection(method, body)
|
|
144
|
+
response = stub(body: body)
|
|
145
|
+
connection = stub
|
|
146
|
+
connection.stubs(method).returns(response)
|
|
147
|
+
@client.instance_variable_set(:@connection, connection)
|
|
148
|
+
end
|
|
149
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
|
|
5
|
+
class Honeymaker::Clients::MexcTest < Minitest::Test
|
|
6
|
+
def setup
|
|
7
|
+
@client = Honeymaker::Clients::Mexc.new(api_key: "test_key", api_secret: "test_secret")
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def test_url
|
|
11
|
+
assert_equal "https://api.mexc.com", Honeymaker::Clients::Mexc::URL
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def test_exchange_information
|
|
15
|
+
stub_connection(:get, { "symbols" => [{ "symbol" => "BTCUSDT" }] })
|
|
16
|
+
result = @client.exchange_information
|
|
17
|
+
assert result.success?
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def test_symbol_price_ticker
|
|
21
|
+
stub_connection(:get, { "symbol" => "BTCUSDT", "price" => "50000" })
|
|
22
|
+
result = @client.symbol_price_ticker(symbol: "BTCUSDT")
|
|
23
|
+
assert result.success?
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def test_account_information
|
|
27
|
+
stub_connection(:get, { "balances" => [{ "asset" => "BTC" }] })
|
|
28
|
+
result = @client.account_information
|
|
29
|
+
assert result.success?
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def test_new_order
|
|
33
|
+
stub_connection(:post, { "orderId" => "123" })
|
|
34
|
+
result = @client.new_order(symbol: "BTCUSDT", side: "BUY", type: "MARKET", quantity: "0.001")
|
|
35
|
+
assert result.success?
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def test_query_order
|
|
39
|
+
stub_connection(:get, {
|
|
40
|
+
"orderId" => "123", "symbol" => "BTCUSDT", "status" => "FILLED", "type" => "MARKET",
|
|
41
|
+
"side" => "BUY", "origQty" => "0.001", "executedQty" => "0.001",
|
|
42
|
+
"cummulativeQuoteQty" => "50", "price" => "50000"
|
|
43
|
+
})
|
|
44
|
+
result = @client.query_order(symbol: "BTCUSDT", order_id: "123")
|
|
45
|
+
assert result.success?
|
|
46
|
+
assert_equal :closed, result.data[:status]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def test_cancel_order
|
|
50
|
+
stub_connection(:delete, { "orderId" => "123" })
|
|
51
|
+
result = @client.cancel_order(symbol: "BTCUSDT", order_id: "123")
|
|
52
|
+
assert result.success?
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def test_withdraw
|
|
56
|
+
stub_connection(:post, { "id" => "w1" })
|
|
57
|
+
result = @client.withdraw(coin: "BTC", address: "addr", amount: "0.1")
|
|
58
|
+
assert result.success?
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def test_account_trade_list
|
|
62
|
+
stub_connection(:get, [{ "symbol" => "BTCUSDT", "id" => 1 }])
|
|
63
|
+
result = @client.account_trade_list(symbol: "BTCUSDT")
|
|
64
|
+
assert result.success?
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def test_deposit_history
|
|
68
|
+
stub_connection(:get, [{ "coin" => "BTC" }])
|
|
69
|
+
result = @client.deposit_history
|
|
70
|
+
assert result.success?
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def test_withdraw_history
|
|
74
|
+
stub_connection(:get, [{ "coin" => "ETH" }])
|
|
75
|
+
result = @client.withdraw_history
|
|
76
|
+
assert result.success?
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def test_headers_include_api_key
|
|
80
|
+
headers = @client.send(:headers)
|
|
81
|
+
assert_equal "test_key", headers[:"X-MEXC-APIKEY"]
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def test_sign_params
|
|
85
|
+
sig = @client.send(:sign_params, { symbol: "BTCUSDT", timestamp: 1234567890 })
|
|
86
|
+
assert sig.is_a?(String)
|
|
87
|
+
assert_equal 64, sig.length
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
def stub_connection(method, body)
|
|
93
|
+
response = stub(body: body)
|
|
94
|
+
connection = stub
|
|
95
|
+
connection.stubs(method).returns(response)
|
|
96
|
+
@client.instance_variable_set(:@connection, connection)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
|
|
5
|
+
class ValidationTest < Minitest::Test
|
|
6
|
+
def test_validate_without_credentials
|
|
7
|
+
client = Honeymaker::Client.new
|
|
8
|
+
result = client.validate(:trading)
|
|
9
|
+
assert result.failure?
|
|
10
|
+
assert_includes result.errors, "No credentials provided"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def test_validate_unknown_type
|
|
14
|
+
client = Honeymaker::Client.new(api_key: "k", api_secret: "s")
|
|
15
|
+
assert_raises(Honeymaker::Error) { client.validate(:unknown) }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def test_validate_rescues_errors
|
|
19
|
+
client = Honeymaker::Client.new(api_key: "k", api_secret: "s")
|
|
20
|
+
client.define_singleton_method(:validate_trading_credentials) { raise "boom" }
|
|
21
|
+
result = client.validate(:trading)
|
|
22
|
+
assert result.failure?
|
|
23
|
+
assert_includes result.errors, "boom"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Binance
|
|
27
|
+
def test_binance_validate_trading_success
|
|
28
|
+
client = Honeymaker::Clients::Binance.new(api_key: "k", api_secret: "s")
|
|
29
|
+
# cancel_order returns error code -2011 = valid key
|
|
30
|
+
stub_connection(client, :delete, { "code" => -2011, "msg" => "Unknown order" })
|
|
31
|
+
result = client.validate(:trading)
|
|
32
|
+
assert result.success?
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def test_binance_validate_trading_failure
|
|
36
|
+
client = Honeymaker::Clients::Binance.new(api_key: "k", api_secret: "s")
|
|
37
|
+
stub_connection(client, :delete, { "code" => -2015, "msg" => "Invalid API-key" })
|
|
38
|
+
result = client.validate(:trading)
|
|
39
|
+
assert result.failure?
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def test_binance_validate_read
|
|
43
|
+
client = Honeymaker::Clients::Binance.new(api_key: "k", api_secret: "s")
|
|
44
|
+
stub_connection(client, :get, { "ipRestrict" => true })
|
|
45
|
+
result = client.validate(:read)
|
|
46
|
+
assert result.success?
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Kraken
|
|
50
|
+
def test_kraken_validate_trading_success
|
|
51
|
+
client = Honeymaker::Clients::Kraken.new(api_key: "k", api_secret: Base64.strict_encode64("s" * 32))
|
|
52
|
+
stub_connection(client, :post, { "error" => [], "result" => { "XXBT" => "0.5" } })
|
|
53
|
+
result = client.validate(:trading)
|
|
54
|
+
assert result.success?
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def test_kraken_validate_trading_failure
|
|
58
|
+
client = Honeymaker::Clients::Kraken.new(api_key: "k", api_secret: Base64.strict_encode64("s" * 32))
|
|
59
|
+
stub_connection(client, :post, { "error" => ["EAPI:Invalid key"], "result" => {} })
|
|
60
|
+
result = client.validate(:trading)
|
|
61
|
+
assert result.failure?
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Coinbase
|
|
65
|
+
def test_coinbase_validate_trading_success
|
|
66
|
+
key = OpenSSL::PKey::EC.generate("prime256v1")
|
|
67
|
+
client = Honeymaker::Clients::Coinbase.new(api_key: "k", api_secret: key.to_pem)
|
|
68
|
+
stub_connection(client, :get, { "accounts" => [] })
|
|
69
|
+
result = client.validate(:trading)
|
|
70
|
+
assert result.success?
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Bybit
|
|
74
|
+
def test_bybit_validate_trading_success
|
|
75
|
+
client = Honeymaker::Clients::Bybit.new(api_key: "k", api_secret: "s")
|
|
76
|
+
stub_connection(client, :get, { "retCode" => 0, "result" => {} })
|
|
77
|
+
result = client.validate(:trading)
|
|
78
|
+
assert result.success?
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def test_bybit_validate_trading_failure
|
|
82
|
+
client = Honeymaker::Clients::Bybit.new(api_key: "k", api_secret: "s")
|
|
83
|
+
stub_connection(client, :get, { "retCode" => 10003, "retMsg" => "Invalid apikey" })
|
|
84
|
+
result = client.validate(:trading)
|
|
85
|
+
assert result.failure?
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# MEXC
|
|
89
|
+
def test_mexc_validate_trading_success
|
|
90
|
+
client = Honeymaker::Clients::Mexc.new(api_key: "k", api_secret: "s")
|
|
91
|
+
stub_connection(client, :get, { "balances" => [] })
|
|
92
|
+
result = client.validate(:trading)
|
|
93
|
+
assert result.success?
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Bitget
|
|
97
|
+
def test_bitget_validate_trading_success
|
|
98
|
+
client = Honeymaker::Clients::Bitget.new(api_key: "k", api_secret: "s", passphrase: "p")
|
|
99
|
+
stub_connection(client, :get, { "code" => "00000", "data" => [] })
|
|
100
|
+
result = client.validate(:trading)
|
|
101
|
+
assert result.success?
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def test_bitget_validate_trading_failure
|
|
105
|
+
client = Honeymaker::Clients::Bitget.new(api_key: "k", api_secret: "s", passphrase: "p")
|
|
106
|
+
stub_connection(client, :get, { "code" => "40014", "msg" => "Invalid ApiKey" })
|
|
107
|
+
result = client.validate(:trading)
|
|
108
|
+
assert result.failure?
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# KuCoin
|
|
112
|
+
def test_kucoin_validate_trading_success
|
|
113
|
+
client = Honeymaker::Clients::Kucoin.new(api_key: "k", api_secret: "s", passphrase: "p")
|
|
114
|
+
stub_connection(client, :get, { "code" => "200000", "data" => [] })
|
|
115
|
+
result = client.validate(:trading)
|
|
116
|
+
assert result.success?
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Bitvavo
|
|
120
|
+
def test_bitvavo_validate_trading_success
|
|
121
|
+
client = Honeymaker::Clients::Bitvavo.new(api_key: "k", api_secret: "s")
|
|
122
|
+
stub_connection(client, :get, [{ "symbol" => "BTC", "available" => "0.5" }])
|
|
123
|
+
result = client.validate(:trading)
|
|
124
|
+
assert result.success?
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Gemini
|
|
128
|
+
def test_gemini_validate_trading_success
|
|
129
|
+
client = Honeymaker::Clients::Gemini.new(api_key: "k", api_secret: "s")
|
|
130
|
+
stub_connection(client, :post, [{ "currency" => "BTC", "amount" => "0.5" }])
|
|
131
|
+
result = client.validate(:trading)
|
|
132
|
+
assert result.success?
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# BingX
|
|
136
|
+
def test_bingx_validate_trading_success
|
|
137
|
+
client = Honeymaker::Clients::BingX.new(api_key: "k", api_secret: "s")
|
|
138
|
+
stub_connection(client, :get, { "code" => 0, "data" => {} })
|
|
139
|
+
result = client.validate(:trading)
|
|
140
|
+
assert result.success?
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Bitrue
|
|
144
|
+
def test_bitrue_validate_trading_success
|
|
145
|
+
client = Honeymaker::Clients::Bitrue.new(api_key: "k", api_secret: "s")
|
|
146
|
+
stub_connection(client, :get, { "balances" => [] })
|
|
147
|
+
result = client.validate(:trading)
|
|
148
|
+
assert result.success?
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Hyperliquid
|
|
152
|
+
def test_hyperliquid_validate_trading_success
|
|
153
|
+
client = Honeymaker::Clients::Hyperliquid.new(api_key: "0xabc", api_secret: "s")
|
|
154
|
+
stub_connection(client, :post, [])
|
|
155
|
+
result = client.validate(:trading)
|
|
156
|
+
assert result.success?
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
private
|
|
160
|
+
|
|
161
|
+
def stub_connection(client, method, body)
|
|
162
|
+
response = stub(body: body)
|
|
163
|
+
connection = stub
|
|
164
|
+
connection.stubs(method).returns(response)
|
|
165
|
+
client.instance_variable_set(:@connection, connection)
|
|
166
|
+
end
|
|
167
|
+
end
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
require "socket"
|
|
5
|
+
|
|
6
|
+
# A transport failure — the request got no HTTP answer at all — carries the chain of exception
|
|
7
|
+
# classes behind it, outermost first. The text of such a failure rarely says what happened
|
|
8
|
+
# ("end of file reached"); the chain does, and it is what lets a caller tell a request that never
|
|
9
|
+
# left (DNS, a refused connection, a proxy refusing CONNECT) from one that may have landed (EOF,
|
|
10
|
+
# reset, read timeout). Honeymaker only reports it; deciding what it means is the caller's job.
|
|
11
|
+
class Honeymaker::ErrorChainTest < Minitest::Test
|
|
12
|
+
def test_walks_wrapped_exceptions_and_causes_outermost_first
|
|
13
|
+
error = Faraday::ConnectionFailed.new(EOFError.new("end of file reached"))
|
|
14
|
+
|
|
15
|
+
assert_equal %w[Faraday::ConnectionFailed EOFError], Honeymaker::Utils.error_chain(error)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def test_follows_ruby_causes_and_names_each_class_once
|
|
19
|
+
error = begin
|
|
20
|
+
begin
|
|
21
|
+
raise Errno::ECONNREFUSED
|
|
22
|
+
rescue Errno::ECONNREFUSED
|
|
23
|
+
raise Faraday::ConnectionFailed, "connection refused"
|
|
24
|
+
end
|
|
25
|
+
rescue Faraday::ConnectionFailed => e
|
|
26
|
+
e
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
assert_equal %w[Faraday::ConnectionFailed Errno::ECONNREFUSED], Honeymaker::Utils.error_chain(error)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Adapters re-wrap: a class seen once must not hide what sits beneath its second instance.
|
|
33
|
+
def test_a_repeated_class_does_not_cut_the_chain_short
|
|
34
|
+
inner = Faraday::ConnectionFailed.new(Errno::ECONNREFUSED.new)
|
|
35
|
+
error = Faraday::ConnectionFailed.new(inner)
|
|
36
|
+
|
|
37
|
+
assert_equal %w[Faraday::ConnectionFailed Errno::ECONNREFUSED], Honeymaker::Utils.error_chain(error)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def test_client_failure_carries_the_chain_and_keeps_its_text
|
|
41
|
+
result = Honeymaker::Client.new.send(:with_rescue) do
|
|
42
|
+
raise Faraday::ConnectionFailed, EOFError.new("end of file reached")
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
assert_equal ["end of file reached"], result.errors
|
|
46
|
+
assert_nil result.data[:status]
|
|
47
|
+
assert_equal %w[Faraday::ConnectionFailed EOFError], result.data[:error_chain]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def test_exchange_failure_carries_the_chain_and_keeps_its_text
|
|
51
|
+
result = Honeymaker::Exchange.new.send(:with_rescue) do
|
|
52
|
+
raise Faraday::ConnectionFailed, EOFError.new("end of file reached")
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
assert_equal ["end of file reached"], result.errors
|
|
56
|
+
assert_equal %w[Faraday::ConnectionFailed EOFError], result.data[:error_chain]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# An HTTP answer is not a transport failure: its status says what happened.
|
|
60
|
+
def test_a_failure_with_an_http_answer_has_no_chain
|
|
61
|
+
error = Faraday::ServerError.new("boom", { status: 503, body: "unavailable" })
|
|
62
|
+
[Honeymaker::Client.new, Honeymaker::Exchange.new].each do |receiver|
|
|
63
|
+
result = receiver.send(:with_rescue) { raise error }
|
|
64
|
+
|
|
65
|
+
assert_nil result.data[:error_chain], receiver.class.name
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# --- The chains the real adapter produces, against local sockets -------------------------------
|
|
70
|
+
|
|
71
|
+
def setup
|
|
72
|
+
@servers = []
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Closing a server makes its blocked #accept raise, which ends the serving thread; joining it
|
|
76
|
+
# keeps that thread from outliving the test.
|
|
77
|
+
def teardown
|
|
78
|
+
@servers.each do |server, thread|
|
|
79
|
+
server.close
|
|
80
|
+
thread.join(1)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def test_real_chain_for_a_connection_closed_before_the_answer
|
|
85
|
+
port = local_server
|
|
86
|
+
|
|
87
|
+
assert_equal %w[Faraday::ConnectionFailed EOFError], chain_of("http://127.0.0.1:#{port}")
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def test_real_chain_for_a_refused_connection
|
|
91
|
+
port = TCPServer.new("127.0.0.1", 0).then { |s| s.addr[1].tap { s.close } }
|
|
92
|
+
|
|
93
|
+
assert_includes chain_of("http://127.0.0.1:#{port}"), "Errno::ECONNREFUSED"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def test_real_chain_for_a_proxy_refusing_connect
|
|
97
|
+
port = local_server("HTTP/1.1 407 Proxy Authentication Required\r\nContent-Length: 0\r\n\r\n")
|
|
98
|
+
client = Honeymaker::Client.new(proxy: "http://127.0.0.1:#{port}")
|
|
99
|
+
result = client.send(:with_rescue) { client.send(:build_client_connection, "https://example.com").get("/") }
|
|
100
|
+
|
|
101
|
+
assert_equal %w[Faraday::ConnectionFailed Net::HTTPClientException], result.data[:error_chain]
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
private
|
|
105
|
+
|
|
106
|
+
# A local server that reads each request and answers with `reply`, or hangs up without an answer
|
|
107
|
+
# when there is none. Returns its port; teardown closes it.
|
|
108
|
+
def local_server(reply = nil)
|
|
109
|
+
server = TCPServer.new("127.0.0.1", 0)
|
|
110
|
+
thread = Thread.new do
|
|
111
|
+
loop do
|
|
112
|
+
client = server.accept
|
|
113
|
+
begin
|
|
114
|
+
client.readpartial(4096)
|
|
115
|
+
client.write(reply) if reply
|
|
116
|
+
rescue EOFError, SystemCallError
|
|
117
|
+
nil
|
|
118
|
+
ensure
|
|
119
|
+
client.close
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
rescue IOError, SystemCallError
|
|
123
|
+
nil # the server was closed in teardown
|
|
124
|
+
end
|
|
125
|
+
@servers << [server, thread]
|
|
126
|
+
server.addr[1]
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def chain_of(url)
|
|
130
|
+
client = Honeymaker::Client.new
|
|
131
|
+
client.send(:with_rescue) { client.send(:build_client_connection, url).get("/") }.data[:error_chain]
|
|
132
|
+
end
|
|
133
|
+
end
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
|
|
5
|
+
class Honeymaker::ExchangeTest < Minitest::Test
|
|
6
|
+
def test_get_tickers_info_raises_not_implemented
|
|
7
|
+
exchange = Honeymaker::Exchange.new
|
|
8
|
+
assert_raises(NotImplementedError) { exchange.get_tickers_info }
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def test_with_rescue_wraps_faraday_errors
|
|
12
|
+
exchange = Honeymaker::Exchange.new
|
|
13
|
+
result = exchange.send(:with_rescue) { raise Faraday::TimeoutError, "timeout" }
|
|
14
|
+
assert result.failure?
|
|
15
|
+
assert_match(/timeout/, result.errors.first)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def test_with_rescue_wraps_standard_errors
|
|
19
|
+
exchange = Honeymaker::Exchange.new
|
|
20
|
+
result = exchange.send(:with_rescue) { raise StandardError, "boom" }
|
|
21
|
+
assert result.failure?
|
|
22
|
+
assert_equal ["boom"], result.errors
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def test_with_rescue_returns_success_on_no_error
|
|
26
|
+
exchange = Honeymaker::Exchange.new
|
|
27
|
+
result = exchange.send(:with_rescue) { [1, 2, 3] }
|
|
28
|
+
assert result.success?
|
|
29
|
+
assert_equal [1, 2, 3], result.data
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def test_get_bid_ask_raises_not_implemented
|
|
33
|
+
exchange = Honeymaker::Exchange.new
|
|
34
|
+
assert_raises(NotImplementedError) { exchange.get_bid_ask("BTCUSDT") }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def test_get_price_returns_midpoint
|
|
38
|
+
exchange = Honeymaker::Exchange.new
|
|
39
|
+
bid_ask = { bid: BigDecimal("100"), ask: BigDecimal("102") }
|
|
40
|
+
exchange.define_singleton_method(:get_bid_ask) { |_| Honeymaker::Result::Success.new(bid_ask) }
|
|
41
|
+
|
|
42
|
+
result = exchange.get_price("BTCUSDT")
|
|
43
|
+
assert result.success?
|
|
44
|
+
assert_equal BigDecimal("101"), result.data
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def test_get_price_propagates_failure
|
|
48
|
+
exchange = Honeymaker::Exchange.new
|
|
49
|
+
exchange.define_singleton_method(:get_bid_ask) { |_| Honeymaker::Result::Failure.new("error") }
|
|
50
|
+
|
|
51
|
+
result = exchange.get_price("BTCUSDT")
|
|
52
|
+
assert result.failure?
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def test_tickers_info_caches_result
|
|
56
|
+
exchange = Honeymaker::Exchange.new
|
|
57
|
+
tickers = [{ ticker: "BTCUSDT", base: "BTC", quote: "USDT" }]
|
|
58
|
+
call_count = 0
|
|
59
|
+
exchange.define_singleton_method(:get_tickers_info) do
|
|
60
|
+
call_count += 1
|
|
61
|
+
Honeymaker::Result::Success.new(tickers)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
result1 = exchange.tickers_info
|
|
65
|
+
result2 = exchange.tickers_info
|
|
66
|
+
assert result1.success?
|
|
67
|
+
assert result2.success?
|
|
68
|
+
assert_equal 1, call_count
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def test_tickers_info_does_not_cache_failure
|
|
72
|
+
exchange = Honeymaker::Exchange.new
|
|
73
|
+
call_count = 0
|
|
74
|
+
exchange.define_singleton_method(:get_tickers_info) do
|
|
75
|
+
call_count += 1
|
|
76
|
+
Honeymaker::Result::Failure.new("error")
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
exchange.tickers_info
|
|
80
|
+
exchange.tickers_info
|
|
81
|
+
assert_equal 2, call_count
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def test_find_ticker_returns_matching_ticker
|
|
85
|
+
exchange = Honeymaker::Exchange.new
|
|
86
|
+
tickers = [
|
|
87
|
+
{ ticker: "BTCUSDT", base: "BTC", quote: "USDT" },
|
|
88
|
+
{ ticker: "ETHUSDT", base: "ETH", quote: "USDT" }
|
|
89
|
+
]
|
|
90
|
+
exchange.define_singleton_method(:get_tickers_info) do
|
|
91
|
+
Honeymaker::Result::Success.new(tickers)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
result = exchange.find_ticker("ETHUSDT")
|
|
95
|
+
assert result.success?
|
|
96
|
+
assert_equal "ETH", result.data[:base]
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def test_find_ticker_returns_failure_for_unknown_symbol
|
|
100
|
+
exchange = Honeymaker::Exchange.new
|
|
101
|
+
exchange.define_singleton_method(:get_tickers_info) do
|
|
102
|
+
Honeymaker::Result::Success.new([{ ticker: "BTCUSDT" }])
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
result = exchange.find_ticker("NOPE")
|
|
106
|
+
assert result.failure?
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def test_symbols_returns_base_quote_pairs
|
|
110
|
+
exchange = Honeymaker::Exchange.new
|
|
111
|
+
tickers = [
|
|
112
|
+
{ ticker: "BTCUSDT", base: "BTC", quote: "USDT" },
|
|
113
|
+
{ ticker: "ETHUSDT", base: "ETH", quote: "USDT" }
|
|
114
|
+
]
|
|
115
|
+
exchange.define_singleton_method(:get_tickers_info) do
|
|
116
|
+
Honeymaker::Result::Success.new(tickers)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
result = exchange.symbols
|
|
120
|
+
assert result.success?
|
|
121
|
+
assert_equal [{ base: "BTC", quote: "USDT" }, { base: "ETH", quote: "USDT" }], result.data
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def test_cache_ttl_defaults_to_3600
|
|
125
|
+
exchange = Honeymaker::Exchange.new
|
|
126
|
+
assert_equal 3600, exchange.cache_ttl
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def test_classify_error_returns_nil_when_no_patterns
|
|
130
|
+
exchange = Honeymaker::Exchange.new
|
|
131
|
+
assert_nil exchange.classify_error("anything")
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def test_classify_error_returns_nil_for_nil_message
|
|
135
|
+
exchange = Honeymaker::Exchange.new
|
|
136
|
+
assert_nil exchange.classify_error(nil)
|
|
137
|
+
end
|
|
138
|
+
end
|