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,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"error": [],
|
|
3
|
+
"result": {
|
|
4
|
+
"XBTUSDT": {
|
|
5
|
+
"a": ["67125.67000", "1", "1.000"],
|
|
6
|
+
"b": ["67123.45000", "2", "2.000"],
|
|
7
|
+
"c": ["67124.50000", "0.00100000"],
|
|
8
|
+
"v": ["1234.56789012", "5678.90123456"],
|
|
9
|
+
"p": ["67100.00000", "67050.00000"],
|
|
10
|
+
"t": [12345, 67890],
|
|
11
|
+
"l": ["66800.00000", "66500.00000"],
|
|
12
|
+
"h": ["67500.00000", "67800.00000"],
|
|
13
|
+
"o": "67000.00000"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"data": [
|
|
3
|
+
{
|
|
4
|
+
"symbol": "BTC-USDT",
|
|
5
|
+
"baseCurrency": "BTC",
|
|
6
|
+
"quoteCurrency": "USDT",
|
|
7
|
+
"baseMinSize": "0.00001",
|
|
8
|
+
"quoteMinSize": "0.1",
|
|
9
|
+
"baseMaxSize": "10000000000",
|
|
10
|
+
"quoteMaxSize": "99999999",
|
|
11
|
+
"baseIncrement": "0.00000001",
|
|
12
|
+
"quoteIncrement": "0.000001",
|
|
13
|
+
"priceIncrement": "0.1",
|
|
14
|
+
"enableTrading": true
|
|
15
|
+
}
|
|
16
|
+
]
|
|
17
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_provenance": "Real capture: GET https://api.mexc.com/api/v3/exchangeInfo (2026-09-05), trimmed to two symbols and to the fields this parser reads. MEXC returns status \"1\" for every symbol and PERCENT_PRICE_BY_SIDE as its only filter \u2014 it emits neither \"TRADING\" nor LOT_SIZE/PRICE_FILTER/MIN_NOTIONAL. Re-capture and diff rather than hand-editing; mexc_test.rb asserts this file's SHA-256.",
|
|
3
|
+
"timezone": "CST",
|
|
4
|
+
"symbols": [
|
|
5
|
+
{
|
|
6
|
+
"symbol": "METALUSDT",
|
|
7
|
+
"status": "1",
|
|
8
|
+
"baseAsset": "METAL",
|
|
9
|
+
"baseAssetPrecision": 2,
|
|
10
|
+
"quoteAsset": "USDT",
|
|
11
|
+
"quotePrecision": 5,
|
|
12
|
+
"quoteAssetPrecision": 5,
|
|
13
|
+
"orderTypes": [
|
|
14
|
+
"LIMIT",
|
|
15
|
+
"MARKET",
|
|
16
|
+
"LIMIT_MAKER"
|
|
17
|
+
],
|
|
18
|
+
"isSpotTradingAllowed": true,
|
|
19
|
+
"isMarginTradingAllowed": false,
|
|
20
|
+
"quoteAmountPrecision": "1",
|
|
21
|
+
"baseSizePrecision": "0.1",
|
|
22
|
+
"permissions": [
|
|
23
|
+
"SPOT"
|
|
24
|
+
],
|
|
25
|
+
"filters": [
|
|
26
|
+
{
|
|
27
|
+
"filterType": "PERCENT_PRICE_BY_SIDE",
|
|
28
|
+
"bidMultiplierUp": "0.2",
|
|
29
|
+
"askMultiplierDown": "0.2"
|
|
30
|
+
}
|
|
31
|
+
],
|
|
32
|
+
"maxQuoteAmount": "2000000"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"symbol": "PALMAIUSDT",
|
|
36
|
+
"status": "1",
|
|
37
|
+
"baseAsset": "PALMAI",
|
|
38
|
+
"baseAssetPrecision": 2,
|
|
39
|
+
"quoteAsset": "USDT",
|
|
40
|
+
"quotePrecision": 4,
|
|
41
|
+
"quoteAssetPrecision": 4,
|
|
42
|
+
"orderTypes": [
|
|
43
|
+
"LIMIT",
|
|
44
|
+
"MARKET",
|
|
45
|
+
"LIMIT_MAKER"
|
|
46
|
+
],
|
|
47
|
+
"isSpotTradingAllowed": false,
|
|
48
|
+
"isMarginTradingAllowed": false,
|
|
49
|
+
"quoteAmountPrecision": "1",
|
|
50
|
+
"baseSizePrecision": "0",
|
|
51
|
+
"permissions": [
|
|
52
|
+
"SPOT"
|
|
53
|
+
],
|
|
54
|
+
"filters": [
|
|
55
|
+
{
|
|
56
|
+
"filterType": "PERCENT_PRICE_BY_SIDE",
|
|
57
|
+
"bidMultiplierUp": "0.2",
|
|
58
|
+
"askMultiplierDown": "0.2"
|
|
59
|
+
}
|
|
60
|
+
],
|
|
61
|
+
"maxQuoteAmount": "2000000"
|
|
62
|
+
}
|
|
63
|
+
]
|
|
64
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
|
|
5
|
+
class Honeymaker::ClientTest < Minitest::Test
|
|
6
|
+
def test_default_options
|
|
7
|
+
assert_equal 5, Honeymaker::Client::OPTIONS[:request][:open_timeout]
|
|
8
|
+
assert_equal 30, Honeymaker::Client::OPTIONS[:request][:read_timeout]
|
|
9
|
+
assert_equal 10, Honeymaker::Client::OPTIONS[:request][:write_timeout]
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def test_authenticated_with_credentials
|
|
13
|
+
client = Honeymaker::Client.new(api_key: "key", api_secret: "secret")
|
|
14
|
+
assert client.send(:authenticated?)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def test_not_authenticated_without_credentials
|
|
18
|
+
client = Honeymaker::Client.new
|
|
19
|
+
refute client.send(:authenticated?)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def test_not_authenticated_with_empty_credentials
|
|
23
|
+
client = Honeymaker::Client.new(api_key: "", api_secret: "")
|
|
24
|
+
refute client.send(:authenticated?)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def test_with_rescue_wraps_success
|
|
28
|
+
client = Honeymaker::Client.new
|
|
29
|
+
result = client.send(:with_rescue) { { "status" => "ok" } }
|
|
30
|
+
assert result.success?
|
|
31
|
+
assert_equal({ "status" => "ok" }, result.data)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def test_with_rescue_wraps_faraday_error
|
|
35
|
+
client = Honeymaker::Client.new
|
|
36
|
+
result = client.send(:with_rescue) { raise Faraday::TimeoutError, "timeout" }
|
|
37
|
+
assert result.failure?
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def test_with_rescue_wraps_standard_error
|
|
41
|
+
client = Honeymaker::Client.new
|
|
42
|
+
result = client.send(:with_rescue) { raise StandardError, "boom" }
|
|
43
|
+
assert result.failure?
|
|
44
|
+
assert_equal ["StandardError: boom"], result.errors
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# A bug in this gem, in a vendor gem, or in the caller is NOT an exchange error, but it lands in
|
|
48
|
+
# the same Result::Failure. Callers classify failure text to decide "out of funds" / "bad key" /
|
|
49
|
+
# "retry me", so an unlabelled TypeError reads as something the venue said and gets filed and
|
|
50
|
+
# reported as one. Naming the class makes the true source unmistakable, and the flag lets a
|
|
51
|
+
# caller refuse to classify it at all.
|
|
52
|
+
def test_with_rescue_names_the_exception_class_for_non_api_errors
|
|
53
|
+
client = Honeymaker::Client.new
|
|
54
|
+
result = client.send(:with_rescue) { raise TypeError, "String can't be coerced into Float" }
|
|
55
|
+
assert result.failure?
|
|
56
|
+
assert_equal ["TypeError: String can't be coerced into Float"], result.errors
|
|
57
|
+
assert result.data[:client_error], "a local exception must be flagged, not passed off as the venue's"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def test_with_rescue_labels_an_empty_message_with_its_class
|
|
61
|
+
client = Honeymaker::Client.new
|
|
62
|
+
result = client.send(:with_rescue) { raise NoMethodError, "" }
|
|
63
|
+
assert result.failure?
|
|
64
|
+
assert_equal ["NoMethodError"], result.errors
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Network failures reach callers through this branch too, and the transient-retry matchers key
|
|
68
|
+
# off substrings of them. Prefixing must not break that.
|
|
69
|
+
def test_with_rescue_keeps_network_substrings_matchable
|
|
70
|
+
client = Honeymaker::Client.new
|
|
71
|
+
result = client.send(:with_rescue) { raise Net::ReadTimeout, "Net::ReadTimeout with #<TCPSocket:(closed)>" }
|
|
72
|
+
assert_includes result.errors.first, "Net::ReadTimeout"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# An exchange error is not ours: it must stay unflagged and unprefixed so the venue's own text
|
|
76
|
+
# reaches the classifiers verbatim.
|
|
77
|
+
def test_with_rescue_does_not_flag_api_errors
|
|
78
|
+
client = Honeymaker::Client.new
|
|
79
|
+
result = client.send(:with_rescue) { raise Faraday::TimeoutError, "timeout" }
|
|
80
|
+
assert result.failure?
|
|
81
|
+
assert_nil result.data[:client_error]
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def test_hmac_sha256
|
|
85
|
+
client = Honeymaker::Client.new
|
|
86
|
+
sig = client.send(:hmac_sha256, "secret", "data")
|
|
87
|
+
expected = OpenSSL::HMAC.hexdigest("sha256", "secret", "data")
|
|
88
|
+
assert_equal expected, sig
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
|
|
5
|
+
class Honeymaker::Clients::BinanceTest < Minitest::Test
|
|
6
|
+
def setup
|
|
7
|
+
@client = Honeymaker::Clients::Binance.new(api_key: "test_key", api_secret: "test_secret")
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def test_url
|
|
11
|
+
assert_equal "https://api.binance.com", Honeymaker::Clients::Binance::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
|
+
assert_equal "BTCUSDT", result.data["symbols"].first["symbol"]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def test_new_order
|
|
22
|
+
stub_connection(:post, { "orderId" => 123, "status" => "FILLED" })
|
|
23
|
+
result = @client.new_order(symbol: "BTCUSDT", side: "BUY", type: "MARKET", quantity: "0.001")
|
|
24
|
+
assert result.success?
|
|
25
|
+
assert_equal "BTCUSDT-123", result.data[:order_id]
|
|
26
|
+
assert_equal 123, result.data[:raw]["orderId"]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def test_query_order
|
|
30
|
+
stub_connection(:get, {
|
|
31
|
+
"orderId" => 123, "symbol" => "BTCUSDT", "status" => "FILLED", "type" => "MARKET",
|
|
32
|
+
"side" => "BUY", "origQty" => "0.001", "origQuoteOrderQty" => "0",
|
|
33
|
+
"executedQty" => "0.001", "cummulativeQuoteQty" => "50", "price" => "50000"
|
|
34
|
+
})
|
|
35
|
+
result = @client.query_order(symbol: "BTCUSDT", order_id: 123)
|
|
36
|
+
assert result.success?
|
|
37
|
+
assert_equal :closed, result.data[:status]
|
|
38
|
+
assert_equal :buy, result.data[:side]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def test_account_information
|
|
42
|
+
stub_connection(:get, { "balances" => [{ "asset" => "BTC", "free" => "0.5" }] })
|
|
43
|
+
result = @client.account_information
|
|
44
|
+
assert result.success?
|
|
45
|
+
assert_equal "BTC", result.data["balances"].first["asset"]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def test_cancel_order
|
|
49
|
+
stub_connection(:delete, { "orderId" => 123, "status" => "CANCELED" })
|
|
50
|
+
result = @client.cancel_order(symbol: "BTCUSDT", order_id: 123)
|
|
51
|
+
assert result.success?
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def test_account_trade_list
|
|
55
|
+
stub_connection(:get, [{ "symbol" => "BTCUSDT", "id" => 123, "price" => "50000" }])
|
|
56
|
+
result = @client.account_trade_list(symbol: "BTCUSDT")
|
|
57
|
+
assert result.success?
|
|
58
|
+
assert_equal "BTCUSDT", result.data.first["symbol"]
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def test_deposit_history
|
|
62
|
+
stub_connection(:get, [{ "coin" => "BTC", "amount" => "0.5", "status" => 1 }])
|
|
63
|
+
result = @client.deposit_history
|
|
64
|
+
assert result.success?
|
|
65
|
+
assert_equal "BTC", result.data.first["coin"]
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def test_withdraw_history
|
|
69
|
+
stub_connection(:get, [{ "coin" => "ETH", "amount" => "1.0", "status" => 6 }])
|
|
70
|
+
result = @client.withdraw_history
|
|
71
|
+
assert result.success?
|
|
72
|
+
assert_equal "ETH", result.data.first["coin"]
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def test_convert_trade_flow
|
|
76
|
+
stub_connection(:get, { "list" => [{ "quoteId" => "q1", "fromAsset" => "BTC", "toAsset" => "USDT" }] })
|
|
77
|
+
result = @client.convert_trade_flow(start_time: 1710936000000, end_time: 1711022400000)
|
|
78
|
+
assert result.success?
|
|
79
|
+
assert_equal "q1", result.data["list"].first["quoteId"]
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def test_fiat_payments
|
|
83
|
+
stub_connection(:get, { "data" => [{ "orderNo" => "f1", "cryptoCurrency" => "BTC" }] })
|
|
84
|
+
result = @client.fiat_payments(transaction_type: 0)
|
|
85
|
+
assert result.success?
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def test_fiat_orders
|
|
89
|
+
stub_connection(:get, { "data" => [{ "orderNo" => "fo1", "fiatCurrency" => "USD" }] })
|
|
90
|
+
result = @client.fiat_orders(transaction_type: 0)
|
|
91
|
+
assert result.success?
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def test_dust_log
|
|
95
|
+
stub_connection(:get, { "total" => 1, "userAssetDribblets" => [{ "totalTransferedAmount" => "0.001" }] })
|
|
96
|
+
result = @client.dust_log
|
|
97
|
+
assert result.success?
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def test_asset_dividend
|
|
101
|
+
stub_connection(:get, { "rows" => [{ "asset" => "BNB", "amount" => "0.1" }], "total" => 1 })
|
|
102
|
+
result = @client.asset_dividend
|
|
103
|
+
assert result.success?
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def test_simple_earn_flexible_rewards
|
|
107
|
+
stub_connection(:get, { "rows" => [{ "asset" => "USDT", "rewards" => "0.5" }], "total" => 1 })
|
|
108
|
+
result = @client.simple_earn_flexible_rewards
|
|
109
|
+
assert result.success?
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def test_simple_earn_locked_rewards
|
|
113
|
+
stub_connection(:get, { "rows" => [{ "asset" => "ETH", "rewards" => "0.01" }], "total" => 1 })
|
|
114
|
+
result = @client.simple_earn_locked_rewards
|
|
115
|
+
assert result.success?
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def test_handles_api_error
|
|
119
|
+
connection = stub
|
|
120
|
+
connection.stubs(:get).raises(Faraday::ServerError.new("500", { status: 500, body: "Server Error" }))
|
|
121
|
+
@client.instance_variable_set(:@connection, connection)
|
|
122
|
+
result = @client.exchange_information
|
|
123
|
+
assert result.failure?
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def test_headers_include_api_key
|
|
127
|
+
headers = @client.send(:headers)
|
|
128
|
+
assert_equal "test_key", headers[:"X-MBX-APIKEY"]
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def test_headers_without_auth
|
|
132
|
+
client = Honeymaker::Clients::Binance.new
|
|
133
|
+
headers = client.send(:headers)
|
|
134
|
+
refute headers.key?(:"X-MBX-APIKEY")
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def test_sign_params
|
|
138
|
+
sig = @client.send(:sign_params, { symbol: "BTCUSDT", timestamp: 1234567890 })
|
|
139
|
+
assert sig.is_a?(String)
|
|
140
|
+
assert_equal 64, sig.length
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
private
|
|
144
|
+
|
|
145
|
+
def stub_connection(method, body)
|
|
146
|
+
response = stub(body: body)
|
|
147
|
+
connection = stub
|
|
148
|
+
connection.stubs(method).returns(response)
|
|
149
|
+
@client.instance_variable_set(:@connection, connection)
|
|
150
|
+
end
|
|
151
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
|
|
5
|
+
class Honeymaker::Clients::BinanceUsTest < Minitest::Test
|
|
6
|
+
def test_inherits_from_binance
|
|
7
|
+
client = Honeymaker::Clients::BinanceUs.new
|
|
8
|
+
assert_kind_of Honeymaker::Clients::Binance, client
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def test_url
|
|
12
|
+
assert_equal "https://api.binance.us", Honeymaker::Clients::BinanceUs::URL
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def test_exchange_information
|
|
16
|
+
client = Honeymaker::Clients::BinanceUs.new(api_key: "k", api_secret: "s")
|
|
17
|
+
response = stub(body: { "symbols" => [] })
|
|
18
|
+
connection = stub
|
|
19
|
+
connection.stubs(:get).returns(response)
|
|
20
|
+
client.instance_variable_set(:@connection, connection)
|
|
21
|
+
|
|
22
|
+
result = client.exchange_information
|
|
23
|
+
assert result.success?
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
|
|
5
|
+
class Honeymaker::Clients::BingXTest < Minitest::Test
|
|
6
|
+
def setup
|
|
7
|
+
@client = Honeymaker::Clients::BingX.new(api_key: "test_key", api_secret: "test_secret")
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def test_url
|
|
11
|
+
assert_equal "https://open-api.bingx.com", Honeymaker::Clients::BingX::URL
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def test_get_symbols
|
|
15
|
+
stub_connection(:get, { "data" => { "symbols" => [{ "symbol" => "BTC-USDT" }] } })
|
|
16
|
+
result = @client.get_symbols
|
|
17
|
+
assert result.success?
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def test_get_ticker
|
|
21
|
+
stub_connection(:get, { "data" => [{ "symbol" => "BTC-USDT" }] })
|
|
22
|
+
result = @client.get_ticker(symbol: "BTC-USDT")
|
|
23
|
+
assert result.success?
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def test_get_balances
|
|
27
|
+
stub_connection(:get, { "data" => { "balances" => [{ "asset" => "BTC" }] } })
|
|
28
|
+
result = @client.get_balances
|
|
29
|
+
assert result.success?
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def test_place_order
|
|
33
|
+
stub_connection(:post, { "data" => { "orderId" => "123" } })
|
|
34
|
+
result = @client.place_order(symbol: "BTC-USDT", side: "BUY", type: "MARKET", quantity: "0.001")
|
|
35
|
+
assert result.success?
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def test_get_order
|
|
39
|
+
stub_connection(:get, { "data" => { "orderId" => "123" } })
|
|
40
|
+
result = @client.get_order(symbol: "BTC-USDT", order_id: "123")
|
|
41
|
+
assert result.success?
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# BingX rejects with HTTP 200 and a non-zero `code`. Without a guard the rejection was read as
|
|
45
|
+
# a placed order and handed back as order_id "BTC-USDT-" (nil id), so a bot recorded a trade
|
|
46
|
+
# that does not exist and then polled a malformed id forever.
|
|
47
|
+
def test_place_order_fails_on_nonzero_code
|
|
48
|
+
stub_connection(:post, { "code" => 100004, "msg" => "insufficient balance" })
|
|
49
|
+
result = @client.place_order(symbol: "BTC-USDT", side: "BUY", type: "MARKET", quantity: "0.001")
|
|
50
|
+
assert result.failure?
|
|
51
|
+
assert_includes result.errors.first, "insufficient balance"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def test_get_order_fails_on_nonzero_code
|
|
55
|
+
stub_connection(:get, { "code" => 80016, "msg" => "Order does not exist" })
|
|
56
|
+
result = @client.get_order(symbol: "BTC-USDT", order_id: "123")
|
|
57
|
+
assert result.failure?
|
|
58
|
+
assert_includes result.errors.first, "Order does not exist"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def test_place_order_succeeds_on_zero_code
|
|
62
|
+
stub_connection(:post, { "code" => 0, "data" => { "orderId" => "123" } })
|
|
63
|
+
result = @client.place_order(symbol: "BTC-USDT", side: "BUY", type: "MARKET", quantity: "0.001")
|
|
64
|
+
assert result.success?
|
|
65
|
+
assert_equal "BTC-USDT-123", result.data[:order_id]
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def test_cancel_order
|
|
69
|
+
stub_connection(:post, { "data" => { "orderId" => "123" } })
|
|
70
|
+
result = @client.cancel_order(symbol: "BTC-USDT", order_id: "123")
|
|
71
|
+
assert result.success?
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def test_withdraw
|
|
75
|
+
stub_connection(:post, { "data" => { "id" => "w1" } })
|
|
76
|
+
result = @client.withdraw(coin: "BTC", address: "addr", amount: "0.1")
|
|
77
|
+
assert result.success?
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def test_get_trade_fills
|
|
81
|
+
stub_connection(:get, { "data" => [{ "tradeId" => "t1" }] })
|
|
82
|
+
result = @client.get_trade_fills(symbol: "BTC-USDT")
|
|
83
|
+
assert result.success?
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def test_deposit_history
|
|
87
|
+
stub_connection(:get, [{ "coin" => "BTC" }])
|
|
88
|
+
result = @client.deposit_history
|
|
89
|
+
assert result.success?
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def test_withdraw_history
|
|
93
|
+
stub_connection(:get, [{ "coin" => "ETH" }])
|
|
94
|
+
result = @client.withdraw_history
|
|
95
|
+
assert result.success?
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
private
|
|
99
|
+
|
|
100
|
+
def stub_connection(method, body)
|
|
101
|
+
response = stub(body: body)
|
|
102
|
+
connection = stub
|
|
103
|
+
connection.stubs(method).returns(response)
|
|
104
|
+
@client.instance_variable_set(:@connection, connection)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
|
|
5
|
+
class Honeymaker::Clients::BitgetTest < Minitest::Test
|
|
6
|
+
def setup
|
|
7
|
+
@client = Honeymaker::Clients::Bitget.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.bitget.com", Honeymaker::Clients::Bitget::URL
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def test_accepts_passphrase
|
|
17
|
+
assert_equal "test_pass", @client.passphrase
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def test_get_coins
|
|
21
|
+
stub_connection(:get, { "data" => [{ "coin" => "BTC" }] })
|
|
22
|
+
result = @client.get_coins
|
|
23
|
+
assert result.success?
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def test_get_symbols
|
|
27
|
+
stub_connection(:get, { "data" => [{ "symbol" => "BTCUSDT" }] })
|
|
28
|
+
result = @client.get_symbols
|
|
29
|
+
assert result.success?
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def test_get_account_assets
|
|
33
|
+
stub_connection(:get, { "data" => [{ "coin" => "BTC", "available" => "0.5" }] })
|
|
34
|
+
result = @client.get_account_assets
|
|
35
|
+
assert result.success?
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def test_place_order
|
|
39
|
+
stub_connection(:post, { "code" => "00000", "data" => { "orderId" => "123" } })
|
|
40
|
+
result = @client.place_order(symbol: "BTCUSDT", side: "buy", order_type: "market", size: "0.001")
|
|
41
|
+
assert result.success?
|
|
42
|
+
assert_equal "BTCUSDT-123", result.data[:order_id]
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def test_place_order_with_quote_size
|
|
46
|
+
stub_connection(:post, { "code" => "00000", "data" => { "orderId" => "456" } })
|
|
47
|
+
result = @client.place_order(symbol: "BTCUSDT", side: "buy", order_type: "market", quote_size: "100")
|
|
48
|
+
assert result.success?
|
|
49
|
+
assert_equal "BTCUSDT-456", result.data[:order_id]
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def test_get_order
|
|
53
|
+
stub_connection(:get, { "code" => "00000", "data" => [{
|
|
54
|
+
"orderId" => "123", "orderType" => "market", "side" => "buy", "status" => "filled",
|
|
55
|
+
"priceAvg" => "50000", "size" => "0.001", "baseVolume" => "0.001", "quoteVolume" => "50"
|
|
56
|
+
}] })
|
|
57
|
+
result = @client.get_order(order_id: "123")
|
|
58
|
+
assert result.success?
|
|
59
|
+
assert_equal :closed, result.data[:status]
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def test_cancel_order
|
|
63
|
+
stub_connection(:post, { "data" => { "orderId" => "123" } })
|
|
64
|
+
result = @client.cancel_order(symbol: "BTCUSDT", order_id: "123")
|
|
65
|
+
assert result.success?
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def test_get_candles
|
|
69
|
+
stub_connection(:get, { "data" => [] })
|
|
70
|
+
result = @client.get_candles(symbol: "BTCUSDT", granularity: "1min")
|
|
71
|
+
assert result.success?
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def test_withdraw
|
|
75
|
+
stub_connection(:post, { "data" => { "orderId" => "w1" } })
|
|
76
|
+
result = @client.withdraw(coin: "BTC", address: "addr", size: "0.1")
|
|
77
|
+
assert result.success?
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def test_get_fills
|
|
81
|
+
stub_connection(:get, { "data" => [{ "tradeId" => "t1" }] })
|
|
82
|
+
result = @client.get_fills(symbol: "BTCUSDT")
|
|
83
|
+
assert result.success?
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def test_deposit_list
|
|
87
|
+
stub_connection(:get, { "data" => [{ "coin" => "BTC" }] })
|
|
88
|
+
result = @client.deposit_list
|
|
89
|
+
assert result.success?
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def test_withdrawal_list
|
|
93
|
+
stub_connection(:get, { "data" => [{ "coin" => "ETH" }] })
|
|
94
|
+
result = @client.withdrawal_list
|
|
95
|
+
assert result.success?
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def test_signed_headers_include_passphrase
|
|
99
|
+
ts = "1234567890"
|
|
100
|
+
headers = @client.send(:signed_headers, ts, "payload")
|
|
101
|
+
assert_equal "test_key", headers[:"ACCESS-KEY"]
|
|
102
|
+
assert_equal "test_pass", headers[:"ACCESS-PASSPHRASE"]
|
|
103
|
+
assert_equal ts, headers[:"ACCESS-TIMESTAMP"]
|
|
104
|
+
assert headers.key?(:"ACCESS-SIGN")
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Bitget signals business errors in an HTTP-200 envelope. Callers classify on this text
|
|
108
|
+
# (insufficient funds, bad key, stale timestamp), so the code and msg must survive.
|
|
109
|
+
def test_place_order_keeps_api_code_and_message
|
|
110
|
+
stub_connection(:post, { "code" => "43012", "msg" => "Insufficient balance" })
|
|
111
|
+
result = @client.place_order(symbol: "BTCUSDT", side: "buy", order_type: "market", quote_size: "100")
|
|
112
|
+
assert result.failure?
|
|
113
|
+
assert_includes result.errors.first, "Insufficient balance"
|
|
114
|
+
assert_includes result.errors.first, "43012"
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def test_get_order_keeps_api_code_and_message
|
|
118
|
+
stub_connection(:get, { "code" => "40008", "msg" => "Request timestamp expired" })
|
|
119
|
+
result = @client.get_order(order_id: "BTCUSDT-123")
|
|
120
|
+
assert result.failure?
|
|
121
|
+
assert_includes result.errors.first, "Request timestamp expired"
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def test_get_balances_keeps_api_code_and_message
|
|
125
|
+
stub_connection(:get, { "code" => "40012", "msg" => "Apikey does not exist" })
|
|
126
|
+
result = @client.get_balances
|
|
127
|
+
assert result.failure?
|
|
128
|
+
assert_includes result.errors.first, "Apikey does not exist"
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def test_api_error_falls_back_when_body_carries_no_detail
|
|
132
|
+
stub_connection(:post, { "code" => nil })
|
|
133
|
+
result = @client.place_order(symbol: "BTCUSDT", side: "buy", order_type: "market", quote_size: "100")
|
|
134
|
+
assert result.failure?
|
|
135
|
+
assert_equal "Bitget API error", result.errors.first
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
private
|
|
139
|
+
|
|
140
|
+
def stub_connection(method, body)
|
|
141
|
+
response = stub(body: body)
|
|
142
|
+
connection = stub
|
|
143
|
+
connection.stubs(method).returns(response)
|
|
144
|
+
@client.instance_variable_set(:@connection, connection)
|
|
145
|
+
end
|
|
146
|
+
end
|