honeymaker 0.11.0 → 0.11.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c8386a3ce395f193a10d210d48e62f48ffb5f2c2cf6ab2dbf77cd8a6466700c9
4
- data.tar.gz: 9a4bd23725e93deb9a4b8e6aad1f9e86f6b27f13ecf0507031a9fbd53b2f65ba
3
+ metadata.gz: a33b3f43a40a17e8f8655683b4349901db4222a6ec5db4d4a8f82585f0dacdd1
4
+ data.tar.gz: dde27e04419cb12327dfee7c3ecf69c61f36ddf28c1e9b7e2872c785a726106b
5
5
  SHA512:
6
- metadata.gz: 76f414d30b7ae364453bdb8b0522e2c1be01adc6db1126addfc2d0f3fc7111df3d3de41714dab6ec98b25dba8116605f9a9c064a58fe907aba2a4eb24f3f81f5
7
- data.tar.gz: b6d38bc14208cadd407faa789607deddba96a9bd0a5c63d0ed5efbbc6fc86bef6fadda21f30814051e0cae5c1ded2a1bba8a8f5e2a99f06074dffb9c545f9229
6
+ metadata.gz: f6b7a44553dbc9a413c6df817f4ffbff0f263c077f163e10bce5389d7aba60c108d956c68f9f713ac2b7fef69c699540e3759a1fb5470162146d746ace18a624
7
+ data.tar.gz: e53261fd199b590b1d23318d5b2689a59a5b6dc2a402e09b6fb3d63010b1bbb79281e4b3cafd8401964315e51f8a7fbe39e45aa250217e0ed9851c35bc7c494d
@@ -70,8 +70,19 @@ module Honeymaker
70
70
  status = e.respond_to?(:response_status) ? e.response_status : nil
71
71
  Result::Failure.new(error_message, data: { status: status })
72
72
  rescue StandardError => e
73
+ # NOT an exchange error — a bug here, in a vendor gem, or in the caller, and it arrives in the
74
+ # same Result::Failure as a genuine venue rejection. Callers classify that text to decide "out
75
+ # of funds" / "bad key" / "safe to retry", so an unlabelled TypeError gets attributed to the
76
+ # exchange: a hyperliquid-rb signing crash sat in production order history for three months
77
+ # reading exactly like a venue rejection. Name the class so the true source is unmistakable,
78
+ # and flag it so a caller can refuse to classify it as the venue's at all.
79
+ #
80
+ # The class is a PREFIX, not a replacement: the network failures that also land here
81
+ # (Net::ReadTimeout, execution expired, connection refused) are matched by substring for
82
+ # transient retry, and those matches must keep working.
73
83
  msg = e.message
74
- Result::Failure.new((msg && !msg.empty?) ? msg : "Unknown error")
84
+ labelled = (msg && !msg.empty?) ? "#{e.class}: #{msg}" : e.class.to_s
85
+ Result::Failure.new(labelled, data: { client_error: true })
75
86
  end
76
87
 
77
88
  # A business error the exchange returned inside an HTTP-200 envelope (KuCoin's non-"200000"
@@ -24,7 +24,10 @@ module Honeymaker
24
24
  quote_decimals: product["quoteAssetPrecision"],
25
25
  price_decimals: f[:price] ? Utils.decimals(f[:price]["tickSize"]) : product["quotePrecision"],
26
26
  available: true,
27
- trading_enabled: product["status"] == "TRADING"
27
+ # MEXC encodes status as "1", not Binance's "TRADING" — every symbol it returns is "1".
28
+ # isSpotTradingAllowed is the real gate: ~5% of listed symbols reject a spot order.
29
+ # `!= false` keeps the fail-open default if the key is ever absent, matching the siblings.
30
+ trading_enabled: product["status"] == "1" && product["isSpotTradingAllowed"] != false
28
31
  }
29
32
  end
30
33
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Honeymaker
4
- VERSION = "0.11.0"
4
+ VERSION = "0.11.3"
5
5
  end
@@ -1,40 +1,64 @@
1
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",
2
4
  "symbols": [
3
5
  {
4
- "symbol": "BTCUSDT",
5
- "status": "TRADING",
6
- "baseAsset": "BTC",
6
+ "symbol": "METALUSDT",
7
+ "status": "1",
8
+ "baseAsset": "METAL",
9
+ "baseAssetPrecision": 2,
7
10
  "quoteAsset": "USDT",
8
- "baseAssetPrecision": 8,
9
- "quoteAssetPrecision": 8,
10
- "quotePrecision": 2,
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
+ ],
11
25
  "filters": [
12
26
  {
13
- "filterType": "PRICE_FILTER",
14
- "tickSize": "0.01"
15
- },
16
- {
17
- "filterType": "LOT_SIZE",
18
- "minQty": "0.00001",
19
- "maxQty": "9000",
20
- "stepSize": "0.00001"
21
- },
22
- {
23
- "filterType": "MIN_NOTIONAL",
24
- "minNotional": "5",
25
- "maxNotional": "9000000"
27
+ "filterType": "PERCENT_PRICE_BY_SIDE",
28
+ "bidMultiplierUp": "0.2",
29
+ "askMultiplierDown": "0.2"
26
30
  }
27
- ]
31
+ ],
32
+ "maxQuoteAmount": "2000000"
28
33
  },
29
34
  {
30
- "symbol": "ETHUSDT",
31
- "status": "HALT",
32
- "baseAsset": "ETH",
35
+ "symbol": "PALMAIUSDT",
36
+ "status": "1",
37
+ "baseAsset": "PALMAI",
38
+ "baseAssetPrecision": 2,
33
39
  "quoteAsset": "USDT",
34
- "baseAssetPrecision": 6,
35
- "quoteAssetPrecision": 8,
36
- "quotePrecision": 2,
37
- "filters": []
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"
38
62
  }
39
63
  ]
40
64
  }
@@ -41,7 +41,44 @@ class Honeymaker::ClientTest < Minitest::Test
41
41
  client = Honeymaker::Client.new
42
42
  result = client.send(:with_rescue) { raise StandardError, "boom" }
43
43
  assert result.failure?
44
- assert_equal ["boom"], result.errors
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]
45
82
  end
46
83
 
47
84
  def test_hmac_sha256
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "test_helper"
4
+ require "digest"
4
5
 
5
6
  class Honeymaker::Exchanges::MexcTest < Minitest::Test
6
7
  include FixtureHelper
@@ -9,6 +10,23 @@ class Honeymaker::Exchanges::MexcTest < Minitest::Test
9
10
  @exchange = Honeymaker::Exchanges::Mexc.new
10
11
  end
11
12
 
13
+ # H1. MEXC's exchangeInfo has never carried Binance's status encoding. The fixture this file
14
+ # drives was a hand-edited clone of the Binance one ("TRADING"/"HALT", LOT_SIZE/PRICE_FILTER/
15
+ # MIN_NOTIONAL filters) written from the same wrong assumption as the parser it tested, so both
16
+ # agreed and both were wrong. Asserting the raw bytes is the only guard that survives an author
17
+ # who believes the wrong thing twice; the digest also binds this file to deltabadger's copy, which
18
+ # drives the same capture through the other implementation of this contract.
19
+ def test_fixture_carries_mexcs_own_status_encoding
20
+ body = load_fixture("mexc_exchange_info.json")
21
+
22
+ assert_equal "1", body["symbols"].first["status"]
23
+ refute body["symbols"].any? { |s| s["status"] == "TRADING" }
24
+ refute body["symbols"].any? { |s| s["filters"].any? { |f| f["filterType"] == "LOT_SIZE" } }
25
+
26
+ assert_equal "13a65c0c8ebde52fac4cdca9a5be9d9a18ba7b5438c496b1b30606cf52fd60d3",
27
+ Digest::SHA256.hexdigest(File.read(fixture_path("mexc_exchange_info.json")))
28
+ end
29
+
12
30
  def test_get_tickers_info_parses_response
13
31
  body = load_fixture("mexc_exchange_info.json")
14
32
  stub_connection(body)
@@ -19,40 +37,42 @@ class Honeymaker::Exchanges::MexcTest < Minitest::Test
19
37
  assert_equal 2, result.data.size
20
38
 
21
39
  ticker = result.data.first
22
- assert_equal "BTCUSDT", ticker[:ticker]
23
- assert_equal "BTC", ticker[:base]
40
+ assert_equal "METALUSDT", ticker[:ticker]
41
+ assert_equal "METAL", ticker[:base]
24
42
  assert_equal "USDT", ticker[:quote]
25
- assert_equal "0.00001", ticker[:minimum_base_size]
26
- assert_equal "5", ticker[:minimum_quote_size]
27
- assert_equal "9000", ticker[:maximum_base_size]
28
- assert_equal 5, ticker[:base_decimals]
29
- assert_equal 8, ticker[:quote_decimals]
30
- assert_equal 2, ticker[:price_decimals]
31
43
  assert ticker[:available]
32
44
  assert ticker[:trading_enabled]
33
45
  end
34
46
 
35
- def test_halted_symbol_listed_but_not_trading_enabled
47
+ # H2. status is "1" for every MEXC symbol, including the 103 the venue will not accept a spot
48
+ # order for. The status check alone re-enables all of them; isSpotTradingAllowed is what keeps
49
+ # them out of the picker.
50
+ def test_a_symbol_mexc_does_not_allow_for_spot_is_not_trading_enabled
36
51
  body = load_fixture("mexc_exchange_info.json")
37
52
  stub_connection(body)
38
53
 
39
54
  result = @exchange.get_tickers_info
40
55
 
41
- halted = result.data.find { |t| t[:ticker] == "ETHUSDT" }
42
- assert halted[:available] # still listed
43
- refute halted[:trading_enabled] # but not trading
56
+ disallowed = result.data.find { |t| t[:ticker] == "PALMAIUSDT" }
57
+ assert_equal "1", body["symbols"].last["status"]
58
+ assert disallowed[:available] # still listed
59
+ refute disallowed[:trading_enabled] # but not spot-tradable
44
60
  end
45
61
 
46
- def test_falls_back_to_precision_fields_when_no_filters
62
+ # H5. Real MEXC sends only PERCENT_PRICE_BY_SIDE, so the precision fallback is the ONLY path —
63
+ # there is no filter-bearing symbol to contrast it against.
64
+ def test_falls_back_to_precision_fields_when_no_size_filters
47
65
  body = load_fixture("mexc_exchange_info.json")
48
66
  stub_connection(body)
49
67
 
50
68
  result = @exchange.get_tickers_info
51
69
 
52
- # ETHUSDT has empty filters, should use baseAssetPrecision/quotePrecision
53
- eth = result.data.find { |t| t[:ticker] == "ETHUSDT" }
54
- assert_equal 6, eth[:base_decimals]
55
- assert_equal 2, eth[:price_decimals]
70
+ metal = result.data.find { |t| t[:ticker] == "METALUSDT" }
71
+ assert_equal 2, metal[:base_decimals] # baseAssetPrecision
72
+ assert_equal 5, metal[:quote_decimals] # quoteAssetPrecision
73
+ assert_equal 5, metal[:price_decimals] # quotePrecision
74
+ assert_nil metal[:minimum_base_size]
75
+ assert_nil metal[:minimum_quote_size]
56
76
  end
57
77
 
58
78
  def test_get_bid_ask_parses_response
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: honeymaker
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.11.0
4
+ version: 0.11.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Deltabadger