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.
Files changed (136) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/native-gems.yml +100 -0
  3. data/.github/workflows/release.yml +36 -0
  4. data/.github/workflows/test.yml +33 -0
  5. data/LICENSE +21 -0
  6. data/README.md +219 -0
  7. data/Rakefile +78 -0
  8. data/crates/honeymaker/tests/vectors/decimal.json +146 -0
  9. data/crates/honeymaker/tests/vectors/kraken_add_order_wire.json +74 -0
  10. data/crates/honeymaker/tests/vectors/kraken_signing.json +3314 -0
  11. data/crates/honeymaker/tests/vectors/kraken_trades_pager.json +1583 -0
  12. data/lib/honeymaker/3.4/honeymaker_native.so +0 -0
  13. data/lib/honeymaker/4.0/honeymaker_native.so +0 -0
  14. data/lib/honeymaker/client.rb +139 -0
  15. data/lib/honeymaker/clients/binance.rb +687 -0
  16. data/lib/honeymaker/clients/binance_us.rb +42 -0
  17. data/lib/honeymaker/clients/bingx.rb +242 -0
  18. data/lib/honeymaker/clients/bitget.rb +318 -0
  19. data/lib/honeymaker/clients/bitrue.rb +245 -0
  20. data/lib/honeymaker/clients/bitvavo.rb +244 -0
  21. data/lib/honeymaker/clients/bybit.rb +308 -0
  22. data/lib/honeymaker/clients/coinbase.rb +326 -0
  23. data/lib/honeymaker/clients/gemini.rb +189 -0
  24. data/lib/honeymaker/clients/hyperliquid.rb +232 -0
  25. data/lib/honeymaker/clients/kraken.rb +379 -0
  26. data/lib/honeymaker/clients/kraken_futures.rb +78 -0
  27. data/lib/honeymaker/clients/kucoin.rb +310 -0
  28. data/lib/honeymaker/clients/mexc.rb +249 -0
  29. data/lib/honeymaker/exchange.rb +129 -0
  30. data/lib/honeymaker/exchanges/binance.rb +55 -0
  31. data/lib/honeymaker/exchanges/binance_us.rb +9 -0
  32. data/lib/honeymaker/exchanges/bingx.rb +56 -0
  33. data/lib/honeymaker/exchanges/bitget.rb +52 -0
  34. data/lib/honeymaker/exchanges/bitrue.rb +53 -0
  35. data/lib/honeymaker/exchanges/bitvavo.rb +65 -0
  36. data/lib/honeymaker/exchanges/bybit.rb +57 -0
  37. data/lib/honeymaker/exchanges/coinbase.rb +69 -0
  38. data/lib/honeymaker/exchanges/gemini.rb +134 -0
  39. data/lib/honeymaker/exchanges/hyperliquid.rb +67 -0
  40. data/lib/honeymaker/exchanges/kraken.rb +122 -0
  41. data/lib/honeymaker/exchanges/kucoin.rb +52 -0
  42. data/lib/honeymaker/exchanges/mexc.rb +56 -0
  43. data/lib/honeymaker/native/kraken.rb +263 -0
  44. data/lib/honeymaker/native.rb +42 -0
  45. data/lib/honeymaker/result.rb +46 -0
  46. data/lib/honeymaker/utils.rb +39 -0
  47. data/lib/honeymaker/version.rb +5 -0
  48. data/lib/honeymaker.rb +97 -0
  49. data/script/parity/kraken_client_live.rb +191 -0
  50. data/script/parity/kraken_live.rb +70 -0
  51. data/script/parity/kraken_transport.rb +604 -0
  52. data/script/parity/resolve.c +49 -0
  53. data/script/release/push_gems.sh +28 -0
  54. data/script/vectors/kraken.rb +50 -0
  55. data/script/vectors/kraken_stage2.rb +146 -0
  56. data/sig/honeymaker.rbs +4 -0
  57. data/test/fixtures/binance_book_ticker.json +7 -0
  58. data/test/fixtures/binance_exchange_info.json +30 -0
  59. data/test/fixtures/bingx_symbols.json +26 -0
  60. data/test/fixtures/bingx_ticker.json +12 -0
  61. data/test/fixtures/bitget_symbols.json +28 -0
  62. data/test/fixtures/bitget_tickers.json +14 -0
  63. data/test/fixtures/bitmart_symbols.json +26 -0
  64. data/test/fixtures/bitmart_ticker.json +12 -0
  65. data/test/fixtures/bitrue_book_ticker.json +7 -0
  66. data/test/fixtures/bitrue_exchange_info.json +34 -0
  67. data/test/fixtures/bitvavo_book_ticker.json +7 -0
  68. data/test/fixtures/bitvavo_markets.json +30 -0
  69. data/test/fixtures/bybit_instruments.json +23 -0
  70. data/test/fixtures/bybit_tickers.json +19 -0
  71. data/test/fixtures/coinbase_product.json +7 -0
  72. data/test/fixtures/coinbase_products.json +24 -0
  73. data/test/fixtures/gemini_pubticker.json +10 -0
  74. data/test/fixtures/gemini_symbol_detail.json +10 -0
  75. data/test/fixtures/gemini_symbols.json +1 -0
  76. data/test/fixtures/hyperliquid_all_mids.json +5 -0
  77. data/test/fixtures/hyperliquid_spot_meta.json +12 -0
  78. data/test/fixtures/kraken_asset_pairs.json +43 -0
  79. data/test/fixtures/kraken_ticker.json +16 -0
  80. data/test/fixtures/kucoin_orderbook_level1.json +10 -0
  81. data/test/fixtures/kucoin_symbols.json +17 -0
  82. data/test/fixtures/mexc_book_ticker.json +7 -0
  83. data/test/fixtures/mexc_exchange_info.json +64 -0
  84. data/test/honeymaker/client_test.rb +90 -0
  85. data/test/honeymaker/clients/binance_client_test.rb +151 -0
  86. data/test/honeymaker/clients/binance_us_client_test.rb +25 -0
  87. data/test/honeymaker/clients/bingx_client_test.rb +106 -0
  88. data/test/honeymaker/clients/bitget_client_test.rb +146 -0
  89. data/test/honeymaker/clients/bitrue_client_test.rb +69 -0
  90. data/test/honeymaker/clients/bitvavo_client_test.rb +156 -0
  91. data/test/honeymaker/clients/bybit_client_test.rb +137 -0
  92. data/test/honeymaker/clients/coinbase_client_test.rb +153 -0
  93. data/test/honeymaker/clients/gemini_client_test.rb +100 -0
  94. data/test/honeymaker/clients/honeymaker_client_registry_test.rb +39 -0
  95. data/test/honeymaker/clients/hyperliquid_client_test.rb +292 -0
  96. data/test/honeymaker/clients/kraken_client_test.rb +255 -0
  97. data/test/honeymaker/clients/kraken_futures_client_test.rb +54 -0
  98. data/test/honeymaker/clients/kucoin_client_test.rb +149 -0
  99. data/test/honeymaker/clients/mexc_client_test.rb +98 -0
  100. data/test/honeymaker/clients/validation_test.rb +167 -0
  101. data/test/honeymaker/error_chain_test.rb +133 -0
  102. data/test/honeymaker/exchange_test.rb +138 -0
  103. data/test/honeymaker/exchanges/binance_test.rb +83 -0
  104. data/test/honeymaker/exchanges/binance_us_test.rb +53 -0
  105. data/test/honeymaker/exchanges/bingx_test.rb +65 -0
  106. data/test/honeymaker/exchanges/bitget_test.rb +65 -0
  107. data/test/honeymaker/exchanges/bitrue_test.rb +65 -0
  108. data/test/honeymaker/exchanges/bitvavo_test.rb +65 -0
  109. data/test/honeymaker/exchanges/bybit_test.rb +55 -0
  110. data/test/honeymaker/exchanges/coinbase_test.rb +106 -0
  111. data/test/honeymaker/exchanges/gemini_catalogue_test.rb +118 -0
  112. data/test/honeymaker/exchanges/gemini_test.rb +184 -0
  113. data/test/honeymaker/exchanges/hyperliquid_test.rb +64 -0
  114. data/test/honeymaker/exchanges/kraken_test.rb +191 -0
  115. data/test/honeymaker/exchanges/kucoin_test.rb +55 -0
  116. data/test/honeymaker/exchanges/mexc_test.rb +97 -0
  117. data/test/honeymaker/honeymaker_test.rb +27 -0
  118. data/test/honeymaker/result_test.rb +42 -0
  119. data/test/honeymaker/utils_test.rb +64 -0
  120. data/test/native/backend_switch_test.rb +53 -0
  121. data/test/native/convert_test.rb +137 -0
  122. data/test/native/gem_isolation_test.rb +17 -0
  123. data/test/native/loader_test.rb +29 -0
  124. data/test/native/lockstep_test.rb +23 -0
  125. data/test/native/signing_smoke_test.rb +28 -0
  126. data/test/parity/kraken_client_parity_test.rb +398 -0
  127. data/test/parity/kraken_exchange_parity_test.rb +172 -0
  128. data/test/release/branch_hardening_test.rb +34 -0
  129. data/test/release/push_gems_test.rb +79 -0
  130. data/test/smoke.rb +24 -0
  131. data/test/support/fixture_helper.rb +13 -0
  132. data/test/support/kraken_signing_smoke.rb +21 -0
  133. data/test/support/parity_helper.rb +62 -0
  134. data/test/support/venue_double.rb +100 -0
  135. data/test/test_helper.rb +9 -0
  136. metadata +254 -0
@@ -0,0 +1,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,10 @@
1
+ {
2
+ "code": "200000",
3
+ "data": {
4
+ "bestBid": "67123.45",
5
+ "bestBidSize": "1.234567",
6
+ "bestAsk": "67125.67",
7
+ "bestAskSize": "0.987654",
8
+ "time": 1700000000000
9
+ }
10
+ }
@@ -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,7 @@
1
+ {
2
+ "symbol": "BTCUSDT",
3
+ "bidPrice": "67123.45",
4
+ "bidQty": "1.234567",
5
+ "askPrice": "67125.67",
6
+ "askQty": "0.987654"
7
+ }
@@ -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