honeymaker 0.10.0 → 0.11.0

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: bbfab70631b45882e86a1f4af9ea69b9c32c98840a383183b69ebae133b2f3bf
4
- data.tar.gz: 3d65a28ae18b85f5de60959aac90e327471ff617488956538442b6841ddc4bd9
3
+ metadata.gz: c8386a3ce395f193a10d210d48e62f48ffb5f2c2cf6ab2dbf77cd8a6466700c9
4
+ data.tar.gz: 9a4bd23725e93deb9a4b8e6aad1f9e86f6b27f13ecf0507031a9fbd53b2f65ba
5
5
  SHA512:
6
- metadata.gz: 00f3d5d33b5c1d231be4f1bafb5815083bc26cafae31a8cfe2c4403be217ece9120f7195bc6f5fa15d14286950c9f8817762767a4824f0c29396a6b679ca483e
7
- data.tar.gz: 3209e5756f1aab9c2af0537c533b3b2dbadf468c635c5f882d4f85362679597495d78f9ec77a2f7ca1b10a8c1c534687d19b95be73989ce88fe23fbf5f03120a
6
+ metadata.gz: 76f414d30b7ae364453bdb8b0522e2c1be01adc6db1126addfc2d0f3fc7111df3d3de41714dab6ec98b25dba8116605f9a9c064a58fe907aba2a4eb24f3f81f5
7
+ data.tar.gz: b6d38bc14208cadd407faa789607deddba96a9bd0a5c63d0ed5efbbc6fc86bef6fadda21f30814051e0cae5c1ded2a1bba8a8f5e2a99f06074dffb9c545f9229
@@ -74,6 +74,17 @@ module Honeymaker
74
74
  Result::Failure.new((msg && !msg.empty?) ? msg : "Unknown error")
75
75
  end
76
76
 
77
+ # A business error the exchange returned inside an HTTP-200 envelope (KuCoin's non-"200000"
78
+ # code, Bitget's non-"00000"). Keep the exchange's own code and message: callers classify on
79
+ # this text to tell insufficient funds from a bad key from a stale timestamp, so collapsing it
80
+ # to a bare constant silently disables every one of those checks and leaves the operator with
81
+ # an unactionable log line. Falls back to the constant only when the body carries no detail.
82
+ def api_error(exchange, body)
83
+ detail = body.is_a?(Hash) ? [body["code"], body["msg"] || body["message"]] : []
84
+ detail = detail.compact.map(&:to_s).reject(&:empty?).join(": ")
85
+ Result::Failure.new(detail.empty? ? "#{exchange} API error" : "#{exchange} API error #{detail}")
86
+ end
87
+
77
88
  def connection
78
89
  @connection ||= build_client_connection(self.class::URL)
79
90
  end
@@ -57,6 +57,11 @@ module Honeymaker
57
57
  return result if result.failure?
58
58
 
59
59
  raw = result.data
60
+ # BingX rejects with HTTP 200 and a non-zero `code`. Without this the rejection fell
61
+ # through as a Success carrying order_id "#{symbol}-" (nil id): the bot recorded a trade
62
+ # that was never placed, then polled a malformed id forever.
63
+ return api_error("BingX", raw) unless raw.is_a?(Hash) && raw["code"].to_i.zero?
64
+
60
65
  order_id = raw.dig("data", "orderId") || raw.dig("data", "data", "orderId")
61
66
  Result::Success.new({ order_id: "#{symbol}-#{order_id}", raw: raw })
62
67
  end
@@ -66,8 +71,9 @@ module Honeymaker
66
71
  symbol: symbol, orderId: order_id, clientOrderID: client_order_id
67
72
  })
68
73
  return result if result.failure?
74
+ return api_error("BingX", result.data) unless result.data.is_a?(Hash) && result.data["code"].to_i.zero?
69
75
 
70
- raw = result.data.is_a?(Hash) && result.data.key?("data") ? result.data["data"] : result.data
76
+ raw = result.data.key?("data") ? result.data["data"] : result.data
71
77
  Result::Success.new(normalize_order("#{symbol}-#{raw['orderId']}", raw))
72
78
  end
73
79
 
@@ -44,7 +44,7 @@ module Honeymaker
44
44
  result = get_account_assets
45
45
  return result if result.failure?
46
46
 
47
- return Result::Failure.new("Bitget API error") unless result.data["code"] == "00000"
47
+ return api_error("Bitget", result.data) unless result.data["code"] == "00000"
48
48
 
49
49
  balances = {}
50
50
  (result.data["data"] || []).each do |asset|
@@ -64,7 +64,7 @@ module Honeymaker
64
64
  size: size, quoteSize: quote_size, price: price, force: force, clientOid: client_oid
65
65
  })
66
66
  return result if result.failure?
67
- return Result::Failure.new("Bitget API error") unless result.data["code"] == "00000"
67
+ return api_error("Bitget", result.data) unless result.data["code"] == "00000"
68
68
 
69
69
  order_id = result.data.dig("data", "orderId")
70
70
  Result::Success.new({ order_id: "#{symbol}-#{order_id}", raw: result.data })
@@ -73,7 +73,7 @@ module Honeymaker
73
73
  def get_order(order_id: nil, client_oid: nil)
74
74
  result = get_signed("/api/v2/spot/trade/orderInfo", { orderId: order_id, clientOid: client_oid })
75
75
  return result if result.failure?
76
- return Result::Failure.new("Bitget API error") unless result.data["code"] == "00000"
76
+ return api_error("Bitget", result.data) unless result.data["code"] == "00000"
77
77
 
78
78
  order_list = result.data["data"]
79
79
  raw = order_list.is_a?(Array) ? order_list.first : order_list
@@ -39,7 +39,7 @@ module Honeymaker
39
39
  result = get_accounts(type: type)
40
40
  return result if result.failure?
41
41
 
42
- return Result::Failure.new("KuCoin API error") unless result.data["code"] == "200000"
42
+ return api_error("KuCoin", result.data) unless result.data["code"] == "200000"
43
43
 
44
44
  balances = {}
45
45
  (result.data["data"] || []).each do |account|
@@ -61,7 +61,7 @@ module Honeymaker
61
61
  timeInForce: time_in_force, stp: stp
62
62
  })
63
63
  return result if result.failure?
64
- return Result::Failure.new("KuCoin API error") unless result.data["code"] == "200000"
64
+ return api_error("KuCoin", result.data) unless result.data["code"] == "200000"
65
65
 
66
66
  order_id = result.data.dig("data", "orderId")
67
67
  Result::Success.new({ order_id: order_id, raw: result.data })
@@ -70,7 +70,7 @@ module Honeymaker
70
70
  def get_order(order_id:)
71
71
  result = get_signed("/api/v1/orders/#{order_id}")
72
72
  return result if result.failure?
73
- return Result::Failure.new("KuCoin API error") unless result.data["code"] == "200000"
73
+ return api_error("KuCoin", result.data) unless result.data["code"] == "200000"
74
74
 
75
75
  raw = result.data["data"]
76
76
  return Result::Failure.new("Order not found") unless raw
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Honeymaker
4
- VERSION = "0.10.0"
4
+ VERSION = "0.11.0"
5
5
  end
@@ -41,6 +41,30 @@ class Honeymaker::Clients::BingXTest < Minitest::Test
41
41
  assert result.success?
42
42
  end
43
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
+
44
68
  def test_cancel_order
45
69
  stub_connection(:post, { "data" => { "orderId" => "123" } })
46
70
  result = @client.cancel_order(symbol: "BTC-USDT", order_id: "123")
@@ -104,6 +104,37 @@ class Honeymaker::Clients::BitgetTest < Minitest::Test
104
104
  assert headers.key?(:"ACCESS-SIGN")
105
105
  end
106
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
+
107
138
  private
108
139
 
109
140
  def stub_connection(method, body)
@@ -107,6 +107,37 @@ class Honeymaker::Clients::KucoinTest < Minitest::Test
107
107
  assert_equal expected, headers[:"KC-API-PASSPHRASE"]
108
108
  end
109
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
+
110
141
  private
111
142
 
112
143
  def stub_connection(method, body)
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.10.0
4
+ version: 0.11.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Deltabadger