honeymaker 0.12.4-aarch64-linux

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class Honeymaker::Clients::BitrueTest < Minitest::Test
6
+ def setup
7
+ @client = Honeymaker::Clients::Bitrue.new(api_key: "test_key", api_secret: "test_secret")
8
+ end
9
+
10
+ def test_url
11
+ assert_equal "https://openapi.bitrue.com", Honeymaker::Clients::Bitrue::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, { "orderId" => "123", "status" => "FILLED" })
40
+ result = @client.query_order(symbol: "BTCUSDT", order_id: "123")
41
+ assert result.success?
42
+ end
43
+
44
+ def test_cancel_order
45
+ stub_connection(:delete, { "orderId" => "123" })
46
+ result = @client.cancel_order(symbol: "BTCUSDT", order_id: "123")
47
+ assert result.success?
48
+ end
49
+
50
+ def test_account_trade_list
51
+ stub_connection(:get, [{ "symbol" => "BTCUSDT", "id" => 1 }])
52
+ result = @client.account_trade_list(symbol: "BTCUSDT")
53
+ assert result.success?
54
+ end
55
+
56
+ def test_headers_include_api_key
57
+ headers = @client.send(:auth_headers)
58
+ assert_equal "test_key", headers[:"X-MBX-APIKEY"]
59
+ end
60
+
61
+ private
62
+
63
+ def stub_connection(method, body)
64
+ response = stub(body: body)
65
+ connection = stub
66
+ connection.stubs(method).returns(response)
67
+ @client.instance_variable_set(:@connection, connection)
68
+ end
69
+ end
@@ -0,0 +1,156 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class Honeymaker::Clients::BitvavoTest < Minitest::Test
6
+ def setup
7
+ @client = Honeymaker::Clients::Bitvavo.new(api_key: "test_key", api_secret: "test_secret")
8
+ end
9
+
10
+ def test_url
11
+ assert_equal "https://api.bitvavo.com", Honeymaker::Clients::Bitvavo::URL
12
+ end
13
+
14
+ def test_get_markets
15
+ stub_connection(:get, [{ "market" => "BTC-EUR" }])
16
+ result = @client.get_markets
17
+ assert result.success?
18
+ end
19
+
20
+ def test_get_ticker_price
21
+ stub_connection(:get, [{ "market" => "BTC-EUR", "price" => "50000" }])
22
+ result = @client.get_ticker_price
23
+ assert result.success?
24
+ end
25
+
26
+ def test_get_raw_balance
27
+ stub_connection(:get, [{ "symbol" => "BTC", "available" => "0.5" }])
28
+ result = @client.get_raw_balance
29
+ assert result.success?
30
+ end
31
+
32
+ def test_place_order
33
+ stub_connection(:post, { "orderId" => "123" })
34
+ result = @client.place_order(market: "BTC-EUR", side: "buy", order_type: "market", amount_quote: "100")
35
+ assert result.success?
36
+ end
37
+
38
+ def test_get_order
39
+ stub_connection(:get, { "orderId" => "123", "status" => "filled" })
40
+ result = @client.get_order(market: "BTC-EUR", order_id: "123")
41
+ assert result.success?
42
+ end
43
+
44
+ def test_cancel_order
45
+ stub_connection(:delete, { "orderId" => "123" })
46
+ result = @client.cancel_order(market: "BTC-EUR", order_id: "123")
47
+ assert result.success?
48
+ end
49
+
50
+ # Bitvavo requires a mandatory integer operatorId on every order create/cancel
51
+ # (errorCode 203). These tests yield the real Faraday request block so we assert
52
+ # what the client actually sends, not just the stubbed response.
53
+
54
+ def test_place_order_sends_default_operator_id
55
+ req = capture_request(:post, { "orderId" => "123" })
56
+ @client.place_order(market: "BTC-EUR", side: "buy", order_type: "market", amount_quote: "100")
57
+ assert_equal @client.class::DEFAULT_OPERATOR_ID, req.body[:operatorId]
58
+ assert_kind_of Integer, req.body[:operatorId]
59
+ end
60
+
61
+ def test_place_order_operator_id_override
62
+ req = capture_request(:post, { "orderId" => "123" })
63
+ @client.place_order(market: "BTC-EUR", side: "buy", order_type: "market", amount_quote: "100", operator_id: 42)
64
+ assert_equal 42, req.body[:operatorId]
65
+ end
66
+
67
+ def test_cancel_order_sends_default_operator_id
68
+ req = capture_request(:delete, { "orderId" => "123" })
69
+ @client.cancel_order(market: "BTC-EUR", order_id: "123")
70
+ assert_equal @client.class::DEFAULT_OPERATOR_ID, req.params[:operatorId]
71
+ assert_kind_of Integer, req.params[:operatorId]
72
+ end
73
+
74
+ def test_cancel_order_operator_id_override
75
+ req = capture_request(:delete, { "orderId" => "123" })
76
+ @client.cancel_order(market: "BTC-EUR", order_id: "123", operator_id: 42)
77
+ assert_equal 42, req.params[:operatorId]
78
+ end
79
+
80
+ def test_withdraw
81
+ stub_connection(:post, { "success" => true })
82
+ result = @client.withdraw(symbol: "BTC", amount: "0.1", address: "addr")
83
+ assert result.success?
84
+ end
85
+
86
+ def test_get_assets
87
+ stub_connection(:get, [{ "symbol" => "BTC" }])
88
+ result = @client.get_assets
89
+ assert result.success?
90
+ end
91
+
92
+ def test_get_trades
93
+ stub_connection(:get, [{ "id" => "t1", "side" => "buy" }])
94
+ result = @client.get_trades(market: "BTC-EUR")
95
+ assert result.success?
96
+ end
97
+
98
+ def test_get_deposit_history
99
+ stub_connection(:get, [{ "symbol" => "BTC", "amount" => "1.0" }])
100
+ result = @client.get_deposit_history
101
+ assert result.success?
102
+ end
103
+
104
+ def test_get_withdrawal_history
105
+ stub_connection(:get, [{ "symbol" => "ETH", "amount" => "2.0" }])
106
+ result = @client.get_withdrawal_history
107
+ assert result.success?
108
+ end
109
+
110
+ def test_signed_headers_include_access_window
111
+ ts = "1234567890"
112
+ headers = @client.send(:signed_headers, ts, "payload")
113
+ assert_equal "test_key", headers[:"BITVAVO-ACCESS-KEY"]
114
+ assert_equal "10000", headers[:"BITVAVO-ACCESS-WINDOW"]
115
+ assert headers.key?(:"BITVAVO-ACCESS-SIGNATURE")
116
+ end
117
+
118
+ def test_signing_payload_format
119
+ # Verify the signing payload matches Bitvavo's expected format
120
+ sig1 = @client.send(:hmac_sha256, "test_secret", "1234567890GET/v2/balance")
121
+ sig2 = @client.send(:hmac_sha256, "test_secret", "1234567890POST/v2/order{\"market\":\"BTC-EUR\"}")
122
+ assert sig1.is_a?(String)
123
+ assert sig2.is_a?(String)
124
+ refute_equal sig1, sig2
125
+ end
126
+
127
+ private
128
+
129
+ def stub_connection(method, body)
130
+ response = stub(body: body)
131
+ connection = stub
132
+ connection.stubs(method).returns(response)
133
+ @client.instance_variable_set(:@connection, connection)
134
+ end
135
+
136
+ # Records what the client sets on the Faraday request. Unlike stub_connection,
137
+ # this yields the request block (connection.post { |req| ... }) so the real
138
+ # body/params-building code in post_signed/delete_signed actually executes.
139
+ class RequestRecorder
140
+ attr_accessor :headers, :body, :params
141
+ attr_reader :requested_url
142
+
143
+ def url(path)
144
+ @requested_url = path
145
+ end
146
+ end
147
+
148
+ def capture_request(method, body)
149
+ response = stub(body: body)
150
+ recorder = RequestRecorder.new
151
+ connection = stub
152
+ connection.stubs(method).yields(recorder).returns(response)
153
+ @client.instance_variable_set(:@connection, connection)
154
+ recorder
155
+ end
156
+ end
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class Honeymaker::Clients::BybitTest < Minitest::Test
6
+ def setup
7
+ @client = Honeymaker::Clients::Bybit.new(api_key: "test_key", api_secret: "test_secret")
8
+ end
9
+
10
+ def test_url
11
+ assert_equal "https://api.bybit.com", Honeymaker::Clients::Bybit::URL
12
+ end
13
+
14
+ def test_instruments_info
15
+ stub_connection(:get, { "result" => { "list" => [{ "symbol" => "BTCUSDT" }] } })
16
+ result = @client.instruments_info(category: "spot")
17
+ assert result.success?
18
+ end
19
+
20
+ def test_tickers
21
+ stub_connection(:get, { "result" => { "list" => [{ "lastPrice" => "50000" }] } })
22
+ result = @client.tickers(category: "spot", symbol: "BTCUSDT")
23
+ assert result.success?
24
+ end
25
+
26
+ def test_wallet_balance
27
+ stub_connection(:get, { "result" => { "list" => [{ "coin" => [{ "coin" => "BTC" }] }] } })
28
+ result = @client.wallet_balance(account_type: "UNIFIED")
29
+ assert result.success?
30
+ end
31
+
32
+ def test_create_order
33
+ stub_connection(:post, { "retCode" => 0, "retMsg" => "OK", "result" => { "orderId" => "123" } })
34
+ result = @client.create_order(category: "spot", symbol: "BTCUSDT", side: "Buy", order_type: "Market", qty: "0.001")
35
+ assert result.success?
36
+ assert_equal "123", result.data[:order_id]
37
+ end
38
+
39
+ def test_get_order
40
+ stub_connection(:get, { "retCode" => 0, "retMsg" => "OK", "result" => { "list" => [{
41
+ "orderId" => "123", "orderType" => "Market", "side" => "Buy", "orderStatus" => "Filled",
42
+ "avgPrice" => "50000", "qty" => "0.001", "cumExecQty" => "0.001", "cumExecValue" => "50"
43
+ }] } })
44
+ result = @client.get_order(category: "spot", order_id: "123")
45
+ assert result.success?
46
+ assert_equal :closed, result.data[:status]
47
+ end
48
+
49
+ def test_get_order_partially_filled_canceled_is_cancelled
50
+ stub_connection(:get, { "retCode" => 0, "retMsg" => "OK", "result" => { "list" => [{
51
+ "orderId" => "123", "orderType" => "Limit", "side" => "Buy", "orderStatus" => "PartiallyFilledCanceled",
52
+ "avgPrice" => "50000", "qty" => "0.002", "cumExecQty" => "0.001", "cumExecValue" => "50"
53
+ }] } })
54
+ result = @client.get_order(category: "spot", order_id: "123")
55
+ assert_equal :cancelled, result.data[:status]
56
+ assert_equal BigDecimal("0.001"), result.data[:amount_exec]
57
+ end
58
+
59
+ def test_order_history
60
+ @client.expects(:get_authenticated).with("/v5/order/history", {
61
+ category: "spot", symbol: nil, orderId: "123", orderLinkId: nil, orderStatus: nil,
62
+ startTime: 1, endTime: 2, limit: 50, cursor: "c"
63
+ }).returns(Honeymaker::Result::Success.new({ "retCode" => 0 }))
64
+ assert @client.order_history(category: "spot", order_id: "123", start_time: 1, end_time: 2, limit: 50, cursor: "c").success?
65
+ end
66
+
67
+ def test_internal_deposit_records
68
+ @client.expects(:get_authenticated).with("/v5/asset/deposit/query-internal-record", {
69
+ txID: nil, coin: "USDT", startTime: 1, endTime: 2, limit: 50, cursor: "c"
70
+ }).returns(Honeymaker::Result::Success.new({ "retCode" => 0 }))
71
+ assert @client.internal_deposit_records(coin: "USDT", start_time: 1, end_time: 2, limit: 50, cursor: "c").success?
72
+ end
73
+
74
+ def test_cancel_order
75
+ stub_connection(:post, { "result" => { "orderId" => "123" } })
76
+ result = @client.cancel_order(category: "spot", symbol: "BTCUSDT", order_id: "123")
77
+ assert result.success?
78
+ end
79
+
80
+ def test_kline
81
+ stub_connection(:get, { "result" => { "list" => [] } })
82
+ result = @client.kline(category: "spot", symbol: "BTCUSDT", interval: "60")
83
+ assert result.success?
84
+ end
85
+
86
+ def test_withdraw
87
+ stub_connection(:post, { "result" => { "id" => "w1" } })
88
+ result = @client.withdraw(coin: "BTC", chain: "BTC", address: "addr", amount: "0.1")
89
+ assert result.success?
90
+ end
91
+
92
+ def test_transaction_log
93
+ stub_connection(:get, { "result" => { "list" => [{ "transactionTime" => "1710936000000" }] } })
94
+ result = @client.transaction_log
95
+ assert result.success?
96
+ end
97
+
98
+ def test_execution_list
99
+ stub_connection(:get, { "result" => { "list" => [{ "execId" => "e1" }] } })
100
+ result = @client.execution_list(category: "spot")
101
+ assert result.success?
102
+ end
103
+
104
+ def test_deposit_records
105
+ stub_connection(:get, { "result" => { "rows" => [{ "coin" => "BTC" }] } })
106
+ result = @client.deposit_records
107
+ assert result.success?
108
+ end
109
+
110
+ def test_withdraw_records
111
+ stub_connection(:get, { "result" => { "rows" => [{ "coin" => "ETH" }] } })
112
+ result = @client.withdraw_records
113
+ assert result.success?
114
+ end
115
+
116
+ def test_signed_headers_include_api_key
117
+ headers = @client.send(:signed_headers, "GET", { foo: "bar" })
118
+ assert_equal "test_key", headers[:"X-BAPI-API-KEY"]
119
+ assert headers.key?(:"X-BAPI-SIGN")
120
+ assert headers.key?(:"X-BAPI-TIMESTAMP")
121
+ end
122
+
123
+ def test_unauthenticated_client
124
+ client = Honeymaker::Clients::Bybit.new
125
+ headers = client.send(:signed_headers, "GET", {})
126
+ refute headers.key?(:"X-BAPI-SIGN")
127
+ end
128
+
129
+ private
130
+
131
+ def stub_connection(method, body)
132
+ response = stub(body: body)
133
+ connection = stub
134
+ connection.stubs(method).returns(response)
135
+ @client.instance_variable_set(:@connection, connection)
136
+ end
137
+ end
@@ -0,0 +1,153 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class Honeymaker::Clients::CoinbaseTest < Minitest::Test
6
+ def setup
7
+ # Use ECDSA key for testing
8
+ @key = OpenSSL::PKey::EC.generate("prime256v1")
9
+ @client = Honeymaker::Clients::Coinbase.new(
10
+ api_key: "test_key",
11
+ api_secret: @key.to_pem
12
+ )
13
+ end
14
+
15
+ def test_url
16
+ assert_equal "https://api.coinbase.com", Honeymaker::Clients::Coinbase::URL
17
+ end
18
+
19
+ def test_get_order
20
+ stub_connection(:get, { "order" => {
21
+ "order_id" => "abc", "status" => "FILLED", "order_type" => "MARKET", "side" => "BUY",
22
+ "average_filled_price" => "50000", "filled_size" => "0.001",
23
+ "total_value_after_fees" => "50", "outstanding_hold_amount" => "0",
24
+ "order_configuration" => { "market_market_ioc" => { "quote_size" => "50" } }
25
+ } })
26
+ result = @client.get_order(order_id: "abc")
27
+ assert result.success?
28
+ assert_equal "abc", result.data[:order_id]
29
+ assert_equal :closed, result.data[:status]
30
+ end
31
+
32
+ def test_create_order
33
+ stub_connection(:post, { "success" => true, "success_response" => { "order_id" => "xyz" } })
34
+ result = @client.create_order(
35
+ client_order_id: "c1", product_id: "BTC-USD",
36
+ side: "BUY", order_configuration: { market_market_ioc: { quote_size: "100" } }
37
+ )
38
+ assert result.success?
39
+ assert_equal "xyz", result.data[:order_id]
40
+ end
41
+
42
+ def test_cancel_orders
43
+ stub_connection(:post, { "results" => [{ "success" => true }] })
44
+ result = @client.cancel_orders(order_ids: ["abc"])
45
+ assert result.success?
46
+ end
47
+
48
+ def test_list_public_products
49
+ stub_connection(:get, { "products" => [{ "product_id" => "BTC-USD" }] })
50
+ result = @client.list_public_products
51
+ assert result.success?
52
+ end
53
+
54
+ def test_get_public_product
55
+ stub_connection(:get, { "product_id" => "BTC-USD" })
56
+ result = @client.get_public_product(product_id: "BTC-USD")
57
+ assert result.success?
58
+ end
59
+
60
+ def test_get_api_key_permissions
61
+ stub_connection(:get, { "can_trade" => true })
62
+ result = @client.get_api_key_permissions
63
+ assert result.success?
64
+ end
65
+
66
+ def test_list_accounts
67
+ stub_connection(:get, { "accounts" => [] })
68
+ result = @client.list_accounts
69
+ assert result.success?
70
+ end
71
+
72
+ def test_list_accounts_passes_cursor_and_limit
73
+ response = stub(body: { "accounts" => [], "has_next" => false, "cursor" => "" })
74
+ request = stub(url: nil)
75
+ request.stubs(:headers=)
76
+ request.stubs(:http_method).returns(:get)
77
+ request.stubs(:path).returns("/api/v3/brokerage/accounts")
78
+ request.expects(:params=).with({ limit: 250, cursor: "c2" })
79
+ connection = stub
80
+ connection.stubs(:get).yields(request).returns(response)
81
+ @client.instance_variable_set(:@connection, connection)
82
+
83
+ assert @client.list_accounts(limit: 250, cursor: "c2").success?
84
+ end
85
+
86
+ def test_send_money
87
+ stub_connection(:post, { "data" => { "id" => "tx1" } })
88
+ result = @client.send_money(account_id: "acc1", to: "addr", amount: "0.1", currency: "BTC")
89
+ assert result.success?
90
+ end
91
+
92
+ def test_list_fills
93
+ stub_connection(:get, { "fills" => [{ "trade_id" => "f1", "product_id" => "BTC-USD" }] })
94
+ result = @client.list_fills(product_id: "BTC-USD")
95
+ assert result.success?
96
+ assert_equal "f1", result.data["fills"].first["trade_id"]
97
+ end
98
+
99
+ def test_list_transactions
100
+ stub_connection(:get, { "data" => [{ "id" => "tx1", "type" => "send" }] })
101
+ result = @client.list_transactions(account_id: "acc1")
102
+ assert result.success?
103
+ assert_equal "tx1", result.data["data"].first["id"]
104
+ end
105
+
106
+ def test_handles_api_error
107
+ connection = stub
108
+ connection.stubs(:get).raises(Faraday::ClientError.new("401", { status: 401, body: "Unauthorized" }))
109
+ @client.instance_variable_set(:@connection, connection)
110
+ result = @client.list_accounts
111
+ assert result.failure?
112
+ end
113
+
114
+ def test_ecdsa_key_detected
115
+ assert @client.send(:ecdsa_key?)
116
+ end
117
+
118
+ def test_non_ecdsa_key_not_detected
119
+ client = Honeymaker::Clients::Coinbase.new(api_key: "k", api_secret: "not-a-pem-key")
120
+ refute client.send(:ecdsa_key?)
121
+ end
122
+
123
+ def test_ed25519_returns_nil_without_rbnacl
124
+ client = Honeymaker::Clients::Coinbase.new(api_key: "k", api_secret: Base64.encode64("short"))
125
+ # rbnacl may or may not be installed; either way should not raise
126
+ result = client.send(:ed25519_signing_key)
127
+ # Returns nil if rbnacl not installed or key is invalid
128
+ assert_nil result
129
+ end
130
+
131
+ def test_unauthenticated_headers
132
+ client = Honeymaker::Clients::Coinbase.new
133
+ stub_connection_on(client, :get, { "products" => [] })
134
+ result = client.list_public_products
135
+ assert result.success?
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
+
147
+ def stub_connection_on(client, method, body)
148
+ response = stub(body: body)
149
+ connection = stub
150
+ connection.stubs(method).returns(response)
151
+ client.instance_variable_set(:@connection, connection)
152
+ end
153
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class Honeymaker::Clients::GeminiTest < Minitest::Test
6
+ def setup
7
+ @client = Honeymaker::Clients::Gemini.new(api_key: "test_key", api_secret: "test_secret")
8
+ end
9
+
10
+ def test_url
11
+ assert_equal "https://api.gemini.com", Honeymaker::Clients::Gemini::URL
12
+ end
13
+
14
+ def test_get_symbols
15
+ stub_connection(:get, ["btcusd", "ethusd"])
16
+ result = @client.get_symbols
17
+ assert result.success?
18
+ assert_includes result.data, "btcusd"
19
+ end
20
+
21
+ def test_get_symbol_details
22
+ stub_connection(:get, { "symbol" => "BTCUSD", "base_currency" => "BTC" })
23
+ result = @client.get_symbol_details(symbol: "btcusd")
24
+ assert result.success?
25
+ end
26
+
27
+ def test_get_ticker
28
+ stub_connection(:get, { "last" => "50000" })
29
+ result = @client.get_ticker(symbol: "btcusd")
30
+ assert result.success?
31
+ end
32
+
33
+ def test_get_balances
34
+ stub_connection(:post, [{ "currency" => "BTC", "amount" => "0.5" }])
35
+ result = @client.get_balances
36
+ assert result.success?
37
+ end
38
+
39
+ def test_new_order
40
+ stub_connection(:post, { "order_id" => "123" })
41
+ result = @client.new_order(symbol: "btcusd", amount: "0.001", price: "50000", side: "buy", type: "exchange limit")
42
+ assert result.success?
43
+ end
44
+
45
+ def test_order_status
46
+ stub_connection(:post, { "order_id" => "123", "is_live" => false })
47
+ result = @client.order_status(order_id: "123")
48
+ assert result.success?
49
+ end
50
+
51
+ def test_cancel_order
52
+ stub_connection(:post, { "order_id" => "123", "is_cancelled" => true })
53
+ result = @client.cancel_order(order_id: "123")
54
+ assert result.success?
55
+ end
56
+
57
+ def test_withdraw
58
+ stub_connection(:post, { "destination" => "addr", "amount" => "0.1" })
59
+ result = @client.withdraw(currency: "btc", address: "addr", amount: "0.1")
60
+ assert result.success?
61
+ end
62
+
63
+ def test_get_my_trades
64
+ stub_connection(:post, [{ "tid" => 123, "price" => "50000" }])
65
+ result = @client.get_my_trades(symbol: "btcusd")
66
+ assert result.success?
67
+ end
68
+
69
+ def test_get_transfers
70
+ stub_connection(:post, [{ "type" => "Deposit", "currency" => "BTC" }])
71
+ result = @client.get_transfers
72
+ assert result.success?
73
+ end
74
+
75
+ # Gemini signs with HMAC-SHA384 of the base64 payload
76
+ # (https://developer.gemini.com/authentication/api-key); any other digest is rejected.
77
+ def test_private_requests_are_signed_with_hmac_sha384
78
+ request = Struct.new(:headers) { def url(_path) = nil }.new
79
+ connection = Object.new
80
+ connection.define_singleton_method(:post) do |&block|
81
+ block.call(request)
82
+ Struct.new(:body).new({})
83
+ end
84
+ @client.instance_variable_set(:@connection, connection)
85
+
86
+ @client.get_balances
87
+
88
+ payload = request.headers[:"X-GEMINI-PAYLOAD"]
89
+ assert_equal OpenSSL::HMAC.hexdigest("sha384", "test_secret", payload), request.headers[:"X-GEMINI-SIGNATURE"]
90
+ end
91
+
92
+ private
93
+
94
+ def stub_connection(method, body)
95
+ response = stub(body: body)
96
+ connection = stub
97
+ connection.stubs(method).returns(response)
98
+ @client.instance_variable_set(:@connection, connection)
99
+ end
100
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class HoneymakerClientRegistryTest < Minitest::Test
6
+ def test_client_returns_correct_class
7
+ client = Honeymaker.client("binance", api_key: "k", api_secret: "s")
8
+ assert_instance_of Honeymaker::Clients::Binance, client
9
+ end
10
+
11
+ def test_client_with_symbol
12
+ client = Honeymaker.client(:kraken, api_key: "k", api_secret: "s")
13
+ assert_instance_of Honeymaker::Clients::Kraken, client
14
+ end
15
+
16
+ def test_client_raises_for_unknown
17
+ assert_raises(Honeymaker::Error) { Honeymaker.client("unknown") }
18
+ end
19
+
20
+ def test_all_clients_registered
21
+ assert_equal 14, Honeymaker::CLIENTS.size
22
+ end
23
+
24
+ def test_client_passes_credentials
25
+ client = Honeymaker.client("binance", api_key: "my_key", api_secret: "my_secret")
26
+ assert_equal "my_key", client.api_key
27
+ assert_equal "my_secret", client.api_secret
28
+ end
29
+
30
+ def test_bitget_accepts_passphrase
31
+ client = Honeymaker.client("bitget", api_key: "k", api_secret: "s", passphrase: "p")
32
+ assert_equal "p", client.passphrase
33
+ end
34
+
35
+ def test_kucoin_accepts_passphrase
36
+ client = Honeymaker.client("kucoin", api_key: "k", api_secret: "s", passphrase: "p")
37
+ assert_equal "p", client.passphrase
38
+ end
39
+ end