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,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