honeymaker 0.9.0 → 0.9.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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8751e8712e47b27bf82073fa2b7de4ec7d7404e9b6195a6cbcd06a2a48749acd
|
|
4
|
+
data.tar.gz: 414e8fd8e266d7d8cd7acc3abd4b39f18d450fb5ba08ebf0b9121c599fc7ff9f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ccf214595dccd90388f8a77dffca7cb0618047c025531c219c1030d2af821d96b682f67f991ff701de013f847cea045b5c7a9b4f5f1a6a96f19490549c18a6ef
|
|
7
|
+
data.tar.gz: e724a084bfc8bd6bca47bd6bced65a12eb75818e58b05f31022d549d246bb8fa1920763bbc27179b1d0339938b899309ee8b8be0f8718a96163834b9f8107a8d
|
|
@@ -7,6 +7,11 @@ module Honeymaker
|
|
|
7
7
|
ACCESS_WINDOW = "10000"
|
|
8
8
|
RATE_LIMITS = { default: 100, orders: 100 }.freeze
|
|
9
9
|
|
|
10
|
+
# Bitvavo requires a mandatory operatorId on every order create/cancel
|
|
11
|
+
# (errorCode 203). It's a positive int64 the caller sets themselves to
|
|
12
|
+
# attribute an order to a trader/bot for audit; the value is cosmetic.
|
|
13
|
+
DEFAULT_OPERATOR_ID = 1
|
|
14
|
+
|
|
10
15
|
def get_assets
|
|
11
16
|
get_public("/v2/assets")
|
|
12
17
|
end
|
|
@@ -51,11 +56,12 @@ module Honeymaker
|
|
|
51
56
|
end
|
|
52
57
|
|
|
53
58
|
def place_order(market:, side:, order_type:, amount: nil, amount_quote: nil, price: nil,
|
|
54
|
-
time_in_force: nil, client_order_id: nil)
|
|
59
|
+
time_in_force: nil, client_order_id: nil, operator_id: nil)
|
|
55
60
|
result = post_signed("/v2/order", {
|
|
56
61
|
market: market, side: side, orderType: order_type,
|
|
57
62
|
amount: amount, amountQuote: amount_quote, price: price,
|
|
58
|
-
timeInForce: time_in_force, clientOrderId: client_order_id
|
|
63
|
+
timeInForce: time_in_force, clientOrderId: client_order_id,
|
|
64
|
+
operatorId: operator_id || DEFAULT_OPERATOR_ID
|
|
59
65
|
})
|
|
60
66
|
return result if result.failure?
|
|
61
67
|
|
|
@@ -71,8 +77,10 @@ module Honeymaker
|
|
|
71
77
|
Result::Success.new(normalize_order("#{raw['market']}-#{raw['orderId']}", raw))
|
|
72
78
|
end
|
|
73
79
|
|
|
74
|
-
def cancel_order(market:, order_id:)
|
|
75
|
-
delete_signed("/v2/order", {
|
|
80
|
+
def cancel_order(market:, order_id:, operator_id: nil)
|
|
81
|
+
delete_signed("/v2/order", {
|
|
82
|
+
market: market, orderId: order_id, operatorId: operator_id || DEFAULT_OPERATOR_ID
|
|
83
|
+
})
|
|
76
84
|
end
|
|
77
85
|
|
|
78
86
|
def get_trades(market:, limit: nil, start_time: nil, end_time: nil, trade_id_from: nil, trade_id_to: nil)
|
|
@@ -237,8 +237,25 @@ module Honeymaker
|
|
|
237
237
|
end
|
|
238
238
|
end
|
|
239
239
|
|
|
240
|
+
# Kraken rejects any request whose nonce is <= the last nonce it saw for the
|
|
241
|
+
# same API key. We protect against in-process collisions (microsecond bursts,
|
|
242
|
+
# backward clock corrections) by tracking the last issued nonce per API key
|
|
243
|
+
# under a mutex. Cross-process reuse of the same API key (other workers,
|
|
244
|
+
# other containers, other tools) is not protected here — affected users
|
|
245
|
+
# should increase the Nonce Window on their Kraken API key.
|
|
246
|
+
@@nonce_mutex = Mutex.new
|
|
247
|
+
@@last_nonces = {}
|
|
248
|
+
|
|
249
|
+
def self.reset_nonce_state!
|
|
250
|
+
@@nonce_mutex.synchronize { @@last_nonces.clear }
|
|
251
|
+
end
|
|
252
|
+
|
|
240
253
|
def nonce
|
|
241
|
-
(
|
|
254
|
+
key = @api_key ? Digest::SHA256.hexdigest(@api_key) : :__no_api_key__
|
|
255
|
+
@@nonce_mutex.synchronize do
|
|
256
|
+
candidate = (Time.now.utc.to_f * 1_000_000).to_i
|
|
257
|
+
@@last_nonces[key] = [candidate, (@@last_nonces[key] || 0) + 1].max
|
|
258
|
+
end
|
|
242
259
|
end
|
|
243
260
|
|
|
244
261
|
def private_headers(path, body)
|
data/lib/honeymaker/version.rb
CHANGED
|
@@ -47,6 +47,36 @@ class Honeymaker::Clients::BitvavoTest < Minitest::Test
|
|
|
47
47
|
assert result.success?
|
|
48
48
|
end
|
|
49
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
|
+
|
|
50
80
|
def test_withdraw
|
|
51
81
|
stub_connection(:post, { "success" => true })
|
|
52
82
|
result = @client.withdraw(symbol: "BTC", amount: "0.1", address: "addr")
|
|
@@ -102,4 +132,25 @@ class Honeymaker::Clients::BitvavoTest < Minitest::Test
|
|
|
102
132
|
connection.stubs(method).returns(response)
|
|
103
133
|
@client.instance_variable_set(:@connection, connection)
|
|
104
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
|
|
105
156
|
end
|
|
@@ -4,6 +4,7 @@ require "test_helper"
|
|
|
4
4
|
|
|
5
5
|
class Honeymaker::Clients::KrakenTest < Minitest::Test
|
|
6
6
|
def setup
|
|
7
|
+
Honeymaker::Clients::Kraken.reset_nonce_state! if Honeymaker::Clients::Kraken.respond_to?(:reset_nonce_state!)
|
|
7
8
|
@client = Honeymaker::Clients::Kraken.new(
|
|
8
9
|
api_key: "test_key",
|
|
9
10
|
api_secret: Base64.strict_encode64("test_secret_key_1234567890123456")
|
|
@@ -74,8 +75,65 @@ class Honeymaker::Clients::KrakenTest < Minitest::Test
|
|
|
74
75
|
refute headers.key?(:"API-Key")
|
|
75
76
|
end
|
|
76
77
|
|
|
78
|
+
def test_reset_nonce_state_is_available_for_test_isolation
|
|
79
|
+
assert_respond_to Honeymaker::Clients::Kraken, :reset_nonce_state!
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def test_nonce_is_strictly_increasing_in_a_tight_loop
|
|
83
|
+
nonces = Array.new(10_000) { @client.send(:nonce) }
|
|
84
|
+
|
|
85
|
+
assert_strictly_increasing nonces
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def test_nonce_is_strictly_increasing_when_clock_is_frozen
|
|
89
|
+
frozen_time = Time.utc(2026, 1, 1, 12, 0, 0)
|
|
90
|
+
Time.stubs(:now).returns(frozen_time)
|
|
91
|
+
|
|
92
|
+
nonces = Array.new(3) { @client.send(:nonce) }
|
|
93
|
+
|
|
94
|
+
assert_strictly_increasing nonces
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def test_nonce_is_strictly_increasing_across_clients_with_the_same_api_key
|
|
98
|
+
frozen_time = Time.utc(2026, 1, 1, 12, 0, 0)
|
|
99
|
+
Time.stubs(:now).returns(frozen_time)
|
|
100
|
+
other_client = Honeymaker::Clients::Kraken.new(
|
|
101
|
+
api_key: "test_key",
|
|
102
|
+
api_secret: Base64.strict_encode64("test_secret_key_1234567890123456")
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
nonces = [
|
|
106
|
+
@client.send(:nonce),
|
|
107
|
+
other_client.send(:nonce),
|
|
108
|
+
@client.send(:nonce),
|
|
109
|
+
other_client.send(:nonce)
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
assert_strictly_increasing nonces
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def test_nonce_sequences_are_independent_for_different_api_keys
|
|
116
|
+
frozen_time = Time.utc(2026, 1, 1, 12, 0, 0)
|
|
117
|
+
Time.stubs(:now).returns(frozen_time)
|
|
118
|
+
first_key_client = Honeymaker::Clients::Kraken.new(api_key: "first_key", api_secret: @client.api_secret)
|
|
119
|
+
second_key_client = Honeymaker::Clients::Kraken.new(api_key: "second_key", api_secret: @client.api_secret)
|
|
120
|
+
|
|
121
|
+
first_key_first_nonce = first_key_client.send(:nonce)
|
|
122
|
+
first_key_second_nonce = first_key_client.send(:nonce)
|
|
123
|
+
second_key_first_nonce = second_key_client.send(:nonce)
|
|
124
|
+
|
|
125
|
+
assert_operator first_key_second_nonce, :>, first_key_first_nonce
|
|
126
|
+
assert_equal first_key_first_nonce, second_key_first_nonce
|
|
127
|
+
end
|
|
128
|
+
|
|
77
129
|
private
|
|
78
130
|
|
|
131
|
+
def assert_strictly_increasing(values)
|
|
132
|
+
values.each_cons(2) do |previous, current|
|
|
133
|
+
assert_operator current, :>, previous
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
79
137
|
def stub_connection(method, body)
|
|
80
138
|
response = stub(body: body)
|
|
81
139
|
connection = stub
|