honeymaker 0.12.3 → 0.12.5

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 (95) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/native-gems.yml +100 -0
  3. data/.github/workflows/release.yml +36 -0
  4. data/.github/workflows/test.yml +19 -4
  5. data/Cargo.lock +1078 -0
  6. data/Cargo.toml +14 -0
  7. data/README.md +39 -0
  8. data/Rakefile +28 -16
  9. data/crates/honeymaker/Cargo.toml +18 -0
  10. data/crates/honeymaker/src/encode.rs +106 -0
  11. data/crates/honeymaker/src/kraken/lookup.rs +140 -0
  12. data/crates/honeymaker/src/kraken/mod.rs +5 -0
  13. data/crates/honeymaker/src/kraken/normalize.rs +536 -0
  14. data/crates/honeymaker/src/kraken/requests.rs +286 -0
  15. data/crates/honeymaker/src/kraken/sign.rs +166 -0
  16. data/crates/honeymaker/src/kraken/trades.rs +201 -0
  17. data/crates/honeymaker/src/lib.rs +10 -0
  18. data/crates/honeymaker/src/num.rs +115 -0
  19. data/crates/honeymaker/src/semantics.rs +100 -0
  20. data/crates/honeymaker/src/types.rs +85 -0
  21. data/crates/honeymaker/tests/kraken_stage2.rs +592 -0
  22. data/crates/honeymaker/tests/kraken_trades_pager.rs +307 -0
  23. data/crates/honeymaker/tests/kraken_vectors.rs +78 -0
  24. data/crates/honeymaker/tests/vectors/decimal.json +146 -0
  25. data/crates/honeymaker/tests/vectors/kraken_add_order_wire.json +74 -0
  26. data/crates/honeymaker/tests/vectors/kraken_signing.json +3314 -0
  27. data/crates/honeymaker/tests/vectors/kraken_trades_pager.json +1583 -0
  28. data/crates/honeymaker-client/Cargo.toml +30 -0
  29. data/crates/honeymaker-client/examples/probe.rs +147 -0
  30. data/crates/honeymaker-client/src/kraken.rs +514 -0
  31. data/crates/honeymaker-client/src/lib.rs +4 -0
  32. data/crates/honeymaker-client/src/transport/connect.rs +583 -0
  33. data/crates/honeymaker-client/src/transport/io_timeout.rs +352 -0
  34. data/crates/honeymaker-client/src/transport/response.rs +169 -0
  35. data/crates/honeymaker-client/src/transport.rs +467 -0
  36. data/crates/honeymaker-client/tests/classification.rs +203 -0
  37. data/crates/honeymaker-client/tests/kraken_client.rs +949 -0
  38. data/crates/honeymaker-client/tests/response.rs +522 -0
  39. data/crates/honeymaker-client/tests/support/mod.rs +550 -0
  40. data/crates/honeymaker-client/tests/transport.rs +828 -0
  41. data/ext/honeymaker_native/Cargo.toml +16 -0
  42. data/ext/honeymaker_native/extconf.rb +6 -0
  43. data/ext/honeymaker_native/src/convert.rs +150 -0
  44. data/ext/honeymaker_native/src/kraken.rs +362 -0
  45. data/ext/honeymaker_native/src/lib.rs +90 -0
  46. data/ext/honeymaker_native/src/ruby_decimal.rs +90 -0
  47. data/lib/honeymaker/clients/bingx.rb +10 -6
  48. data/lib/honeymaker/clients/bitrue.rb +43 -13
  49. data/lib/honeymaker/clients/bitvavo.rb +9 -4
  50. data/lib/honeymaker/clients/bybit.rb +19 -2
  51. data/lib/honeymaker/clients/coinbase.rb +3 -2
  52. data/lib/honeymaker/clients/gemini.rb +10 -0
  53. data/lib/honeymaker/clients/hyperliquid.rb +62 -3
  54. data/lib/honeymaker/clients/kraken.rb +6 -1
  55. data/lib/honeymaker/clients/mexc.rb +16 -2
  56. data/lib/honeymaker/exchanges/coinbase.rb +5 -4
  57. data/lib/honeymaker/exchanges/mexc.rb +8 -3
  58. data/lib/honeymaker/native/kraken.rb +263 -0
  59. data/lib/honeymaker/native.rb +42 -0
  60. data/lib/honeymaker/version.rb +1 -1
  61. data/lib/honeymaker.rb +12 -2
  62. data/script/parity/kraken_client_live.rb +191 -0
  63. data/script/parity/kraken_live.rb +70 -0
  64. data/script/parity/kraken_transport.rb +604 -0
  65. data/script/parity/resolve.c +49 -0
  66. data/script/release/push_gems.sh +28 -0
  67. data/script/vectors/kraken.rb +50 -0
  68. data/script/vectors/kraken_stage2.rb +146 -0
  69. data/test/fixtures/coinbase_products.json +1 -1
  70. data/test/honeymaker/clients/bingx_client_test.rb +19 -6
  71. data/test/honeymaker/clients/bitrue_client_test.rb +48 -0
  72. data/test/honeymaker/clients/bitvavo_client_test.rb +15 -0
  73. data/test/honeymaker/clients/bybit_client_test.rb +25 -0
  74. data/test/honeymaker/clients/coinbase_client_test.rb +14 -0
  75. data/test/honeymaker/clients/gemini_client_test.rb +15 -0
  76. data/test/honeymaker/clients/hyperliquid_client_test.rb +120 -4
  77. data/test/honeymaker/clients/kraken_client_test.rb +15 -0
  78. data/test/honeymaker/clients/mexc_client_test.rb +12 -0
  79. data/test/honeymaker/exchanges/coinbase_test.rb +22 -2
  80. data/test/honeymaker/exchanges/mexc_test.rb +14 -2
  81. data/test/native/backend_switch_test.rb +53 -0
  82. data/test/native/convert_test.rb +137 -0
  83. data/test/native/gem_isolation_test.rb +17 -0
  84. data/test/native/loader_test.rb +29 -0
  85. data/test/native/lockstep_test.rb +23 -0
  86. data/test/native/signing_smoke_test.rb +28 -0
  87. data/test/parity/kraken_client_parity_test.rb +398 -0
  88. data/test/parity/kraken_exchange_parity_test.rb +172 -0
  89. data/test/release/branch_hardening_test.rb +34 -0
  90. data/test/release/push_gems_test.rb +79 -0
  91. data/test/smoke.rb +24 -0
  92. data/test/support/kraken_signing_smoke.rb +21 -0
  93. data/test/support/parity_helper.rb +62 -0
  94. data/test/support/venue_double.rb +100 -0
  95. metadata +84 -4
@@ -0,0 +1,307 @@
1
+ //! The strict Rust scan against the gem's Ruby loop (script/vectors/kraken_stage2.rb, part B).
2
+ //! "same": identical requests and the same end (aggregates, refusal, unreadable, or an exception,
3
+ //! which is a shape error here). "incomplete" / "error": Ruby returned a partial answer, or skipped a
4
+ //! malformed record; Rust must end Incomplete / with a shape error, having made no request Ruby
5
+ //! did not make.
6
+ use bigdecimal::BigDecimal;
7
+ use honeymaker::kraken::requests::Param;
8
+ use honeymaker::kraken::trades::{TradesPager, TradesStep};
9
+ use serde_json::{Value, json};
10
+ use std::collections::{BTreeMap, VecDeque};
11
+ use std::str::FromStr;
12
+
13
+ fn load() -> Value {
14
+ serde_json::from_str(
15
+ &std::fs::read_to_string(format!(
16
+ "{}/tests/vectors/kraken_trades_pager.json",
17
+ env!("CARGO_MANIFEST_DIR")
18
+ ))
19
+ .unwrap(),
20
+ )
21
+ .unwrap()
22
+ }
23
+ fn int(p: &BTreeMap<String, Param>, k: &str) -> Value {
24
+ match p.get(k) {
25
+ Some(Param::One(s)) => json!(s.parse::<i64>().unwrap()),
26
+ _ => Value::Null,
27
+ }
28
+ }
29
+ fn dec(v: &Value) -> BigDecimal {
30
+ BigDecimal::from_str(v.as_str().unwrap()).unwrap()
31
+ }
32
+
33
+ /// Runs the pager over the pages; returns the (start, ofs) of each request and how it ended.
34
+ fn scan(
35
+ ids: Vec<String>,
36
+ start: Option<i64>,
37
+ max_pages: u32,
38
+ pages: Vec<Value>,
39
+ ) -> (Vec<Value>, Value) {
40
+ let mut pages: VecDeque<Value> = pages.into();
41
+ let mut pager = TradesPager::new(ids, start, max_pages);
42
+ let mut requests = vec![];
43
+ let mut step = pager.start::<BigDecimal>();
44
+ let end = loop {
45
+ match step {
46
+ Err(_) => break json!({ "raise": true }),
47
+ Ok(TradesStep::Call(p)) => {
48
+ assert!(
49
+ !p.contains_key("end_time"),
50
+ "no `end`: it would filter out fills on clock skew (R22)"
51
+ );
52
+ requests.push(json!([int(&p, "start"), int(&p, "ofs")]));
53
+ step = pager.feed::<BigDecimal>(&pages.pop_front().unwrap_or(Value::Null)); // legacy: queue.shift || "null"
54
+ }
55
+ Ok(TradesStep::Done(v)) => {
56
+ break json!({ "done": v.iter().map(|(t, a)| json!({
57
+ "txid": t, "vol": a.vol.to_string(), "cost": a.cost.to_string(), "fee": a.fee.to_string(),
58
+ "price": a.price.as_ref().map(|p| p.to_string()), "side": a.side, "order_type": a.order_type.as_str() })).collect::<Vec<_>>() });
59
+ }
60
+ Ok(TradesStep::Venue(e)) => break json!({ "venue": e }),
61
+ Ok(TradesStep::Unreadable) => break json!({ "unreadable": true }),
62
+ Ok(TradesStep::Incomplete(_)) => break json!({ "incomplete": true }),
63
+ }
64
+ };
65
+ (requests, end)
66
+ }
67
+
68
+ #[test]
69
+ fn the_strict_scan_matches_the_ruby_loop_or_refuses_its_partial_answers() {
70
+ for case in load().as_array().unwrap() {
71
+ let name = case["name"].as_str().unwrap();
72
+ let ids = case["order_ids"]
73
+ .as_array()
74
+ .unwrap()
75
+ .iter()
76
+ .map(|v| v.as_str().unwrap().to_string())
77
+ .collect();
78
+ let pages = case["pages"]
79
+ .as_array()
80
+ .unwrap()
81
+ .iter()
82
+ .map(|t| serde_json::from_str(t.as_str().unwrap()).unwrap())
83
+ .collect();
84
+ let (requests, outcome) = scan(
85
+ ids,
86
+ case["start"].as_i64(),
87
+ case["max_pages"].as_u64().unwrap() as u32,
88
+ pages,
89
+ );
90
+ let ruby_requests = case["requests"].as_array().unwrap();
91
+ let refused = match case["strict"].as_str().unwrap() {
92
+ "incomplete" => Some(json!({ "incomplete": true })),
93
+ "error" => Some(json!({ "raise": true })),
94
+ _ => None,
95
+ };
96
+ if let Some(want) = refused {
97
+ assert_eq!(
98
+ outcome, want,
99
+ "{name}: Ruby's answer is partial or skipped a malformed record; Rust must fail"
100
+ );
101
+ assert!(
102
+ requests.len() <= ruby_requests.len()
103
+ && requests[..] == ruby_requests[..requests.len()],
104
+ "{name}: a prefix of Ruby's requests"
105
+ );
106
+ continue;
107
+ }
108
+ assert_eq!(
109
+ &Value::Array(requests),
110
+ &case["requests"],
111
+ "{name}: requests"
112
+ );
113
+ let want = &case["outcome"];
114
+ if want.get("raise").is_some() {
115
+ assert_eq!(
116
+ outcome,
117
+ json!({ "raise": true }),
118
+ "{name}: Ruby raised {}",
119
+ want["raise"]
120
+ );
121
+ } else if let Some(ok) = want.get("ok") {
122
+ let got = outcome["done"]
123
+ .as_array()
124
+ .unwrap_or_else(|| panic!("{name}: {outcome}"));
125
+ assert_eq!(got.len(), ok.as_array().unwrap().len(), "{name}");
126
+ for (g, w) in got.iter().zip(ok.as_array().unwrap()) {
127
+ let (txid, w) = (&w[0], &w[1]);
128
+ assert_eq!(&g["txid"], txid, "{name}: order of first appearance");
129
+ for k in ["vol", "cost", "fee"] {
130
+ assert_eq!(dec(&g[k]), dec(&w[k]), "{name}: {k}");
131
+ }
132
+ match (&w["price"], &g["price"]) {
133
+ (Value::Null, Value::Null) => {}
134
+ (r, g) if !g.is_null() => assert_eq!(dec(g), dec(r), "{name}: price"),
135
+ other => panic!("{name}: price {other:?}"),
136
+ }
137
+ assert_eq!(
138
+ (&g["side"], &g["order_type"]),
139
+ (&w["side"], &w["order_type"]),
140
+ "{name}"
141
+ );
142
+ }
143
+ } else {
144
+ assert_eq!(&outcome, want, "{name}");
145
+ }
146
+ }
147
+ }
148
+
149
+ fn page(trades: Value, count: u64) -> Value {
150
+ json!({ "error": [], "result": { "trades": trades, "count": count } })
151
+ }
152
+ fn t(o: &str, vol: &str, cost: &str) -> Value {
153
+ json!({ "ordertxid": o, "vol": vol, "cost": cost, "fee": "0.1", "type": "buy", "ordertype": "market" })
154
+ }
155
+
156
+ #[test]
157
+ fn a_fill_beyond_the_cap_is_an_error() {
158
+ let (requests, end) = scan(
159
+ vec!["O1".into()],
160
+ Some(1),
161
+ 2,
162
+ vec![
163
+ page(json!({ "T1": t("O9", "1", "1") }), 3),
164
+ page(json!({ "T2": t("O9", "1", "1") }), 3),
165
+ page(json!({ "T3": t("O1", "1", "1") }), 3),
166
+ ],
167
+ );
168
+ assert_eq!(
169
+ (requests.len(), end),
170
+ (2, json!({ "incomplete": true })),
171
+ "never Ok([]) with O1's fill unread"
172
+ );
173
+ }
174
+
175
+ #[test]
176
+ fn a_trade_on_two_pages_counts_once_and_completion_needs_every_distinct_trade() {
177
+ let (_, end) = scan(
178
+ vec!["O1".into()],
179
+ Some(1),
180
+ 20,
181
+ vec![
182
+ page(
183
+ json!({ "T1": t("O1", "1", "2"), "T2": t("O1", "3", "9") }),
184
+ 3,
185
+ ),
186
+ page(
187
+ json!({ "T2": t("O1", "3", "9"), "T3": t("O1", "1", "1") }),
188
+ 3,
189
+ ),
190
+ ],
191
+ );
192
+ let o1 = &end["done"][0];
193
+ assert_eq!(
194
+ (dec(&o1["vol"]), dec(&o1["cost"]), dec(&o1["fee"])),
195
+ (dec(&json!("5")), dec(&json!("12")), dec(&json!("0.3")))
196
+ );
197
+ // Rows served reach count 3, but only two distinct trades were seen: never Done.
198
+ let (_, end) = scan(
199
+ vec!["O1".into()],
200
+ Some(1),
201
+ 20,
202
+ vec![
203
+ page(
204
+ json!({ "T1": t("O1", "1", "2"), "T2": t("O1", "3", "9") }),
205
+ 3,
206
+ ),
207
+ page(json!({ "T2": t("O1", "3", "9") }), 3),
208
+ ],
209
+ );
210
+ assert_eq!(end, json!({ "incomplete": true }));
211
+ }
212
+
213
+ #[test]
214
+ fn trades_arriving_mid_scan_fail_closed() {
215
+ // A new trade lands at the top after page 1: rows shift, T2 is served again, count grows to 4,
216
+ // and the newcomer is never read. Never Done with a fill possibly missing.
217
+ let (requests, end) = scan(
218
+ vec!["O1".into()],
219
+ Some(1),
220
+ 20,
221
+ vec![
222
+ page(
223
+ json!({ "T1": t("O1", "1", "2"), "T2": t("O1", "1", "2") }),
224
+ 3,
225
+ ),
226
+ page(
227
+ json!({ "T2": t("O1", "1", "2"), "T3": t("O1", "1", "2") }),
228
+ 4,
229
+ ),
230
+ ],
231
+ );
232
+ assert_eq!((requests.len(), end), (2, json!({ "incomplete": true })));
233
+ // A count that shrinks between pages never lowers the bar.
234
+ let (_, end) = scan(
235
+ vec!["O1".into()],
236
+ Some(1),
237
+ 20,
238
+ vec![
239
+ page(
240
+ json!({ "T1": t("O1", "1", "2"), "T2": t("O1", "1", "2") }),
241
+ 4,
242
+ ),
243
+ page(json!({ "T3": t("O1", "1", "2") }), 3),
244
+ page(json!({}), 3),
245
+ ],
246
+ );
247
+ assert_eq!(
248
+ end,
249
+ json!({ "incomplete": true }),
250
+ "3 distinct trades against the 4 first reported"
251
+ );
252
+ }
253
+
254
+ #[test]
255
+ fn malformed_fills_data_is_never_an_empty_answer() {
256
+ for trades in [
257
+ json!(null),
258
+ json!(false),
259
+ json!([]),
260
+ json!(""),
261
+ json!("x"),
262
+ json!([1]),
263
+ ] {
264
+ let (_, end) = scan(
265
+ vec!["O1".into()],
266
+ Some(1),
267
+ 20,
268
+ vec![page(trades.clone(), 0)],
269
+ );
270
+ assert_eq!(
271
+ end,
272
+ json!({ "incomplete": true }),
273
+ "trades {trades} with count 0"
274
+ );
275
+ }
276
+ let (_, end) = scan(
277
+ vec!["O1".into()],
278
+ Some(1),
279
+ 20,
280
+ vec![json!({ "error": [], "result": { "count": 0 } })],
281
+ );
282
+ assert_eq!(
283
+ end,
284
+ json!({ "incomplete": true }),
285
+ "trades missing with count 0"
286
+ );
287
+ for record in [
288
+ json!("broken"),
289
+ json!(5),
290
+ json!(null),
291
+ json!({ "vol": "1" }),
292
+ json!({ "ordertxid": 5 }),
293
+ json!({ "ordertxid": null }),
294
+ ] {
295
+ let (_, end) = scan(
296
+ vec!["O1".into()],
297
+ Some(1),
298
+ 20,
299
+ vec![page(json!({ "T1": record.clone() }), 1)],
300
+ );
301
+ assert_eq!(
302
+ end,
303
+ json!({ "raise": true }),
304
+ "record {record}: an error, never skipped"
305
+ );
306
+ }
307
+ }
@@ -0,0 +1,78 @@
1
+ use bigdecimal::BigDecimal;
2
+ use honeymaker::encode::{ruby_decode64, www_form};
3
+ use honeymaker::kraken::sign::{Credentials, private_headers};
4
+ use honeymaker::num::Num;
5
+ use serde_json::Value;
6
+ use std::str::FromStr;
7
+
8
+ fn load(name: &str) -> Value {
9
+ serde_json::from_str(
10
+ &std::fs::read_to_string(format!(
11
+ "{}/tests/vectors/{name}",
12
+ env!("CARGO_MANIFEST_DIR")
13
+ ))
14
+ .unwrap(),
15
+ )
16
+ .unwrap()
17
+ }
18
+
19
+ fn pairs(v: &Value) -> Vec<(String, String)> {
20
+ v.as_array()
21
+ .unwrap()
22
+ .iter()
23
+ .map(|p| {
24
+ (
25
+ p[0].as_str().unwrap().to_string(),
26
+ p[1].as_str().unwrap().to_string(),
27
+ )
28
+ })
29
+ .collect()
30
+ }
31
+
32
+ #[test]
33
+ fn signing_is_byte_identical_to_legacy() {
34
+ for case in load("kraken_signing.json").as_array().unwrap() {
35
+ let body = www_form(&pairs(&case["pairs"]));
36
+ assert_eq!(body, case["body"].as_str().unwrap(), "body");
37
+ let secret = case["api_secret"].as_str().unwrap();
38
+ let hex: String = ruby_decode64(secret.as_bytes())
39
+ .iter()
40
+ .map(|b| format!("{b:02x}"))
41
+ .collect();
42
+ assert_eq!(
43
+ hex,
44
+ case["decoded_secret_hex"].as_str().unwrap(),
45
+ "decode64({secret:?})"
46
+ );
47
+ let creds = Credentials {
48
+ api_key: case["api_key"].as_str().unwrap().into(),
49
+ api_secret: secret.into(),
50
+ };
51
+ let mut got = private_headers(case["path"].as_str().unwrap(), &body, Some(&creds));
52
+ got.sort();
53
+ let mut want: Vec<(String, String)> = case["headers"]
54
+ .as_object()
55
+ .unwrap()
56
+ .iter()
57
+ .map(|(k, v)| (k.clone(), v.as_str().unwrap().to_string()))
58
+ .collect();
59
+ want.sort();
60
+ assert_eq!(got, want, "headers for {case}");
61
+ }
62
+ }
63
+
64
+ #[test]
65
+ fn rust_decimal_impl_agrees_with_ruby_on_finite_values() {
66
+ for case in load("decimal.json").as_array().unwrap() {
67
+ let got = <BigDecimal as Num>::parse(case["input"].as_str().unwrap());
68
+ if !case["ok"].as_bool().unwrap() {
69
+ assert!(got.is_err(), "{case} must not parse");
70
+ } else if case["finite"].as_bool().unwrap() {
71
+ assert_eq!(
72
+ got.unwrap(),
73
+ BigDecimal::from_str(case["plain"].as_str().unwrap()).unwrap(),
74
+ "{case}"
75
+ );
76
+ }
77
+ }
78
+ }
@@ -0,0 +1,146 @@
1
+ [
2
+ {
3
+ "input": "0.5",
4
+ "ok": true,
5
+ "finite": true,
6
+ "plain": "0.5",
7
+ "sign": 2
8
+ },
9
+ {
10
+ "input": "1",
11
+ "ok": true,
12
+ "finite": true,
13
+ "plain": "1.0",
14
+ "sign": 2
15
+ },
16
+ {
17
+ "input": "-0",
18
+ "ok": true,
19
+ "finite": true,
20
+ "plain": "-0.0",
21
+ "sign": -1
22
+ },
23
+ {
24
+ "input": "-0.0000",
25
+ "ok": true,
26
+ "finite": true,
27
+ "plain": "-0.0",
28
+ "sign": -1
29
+ },
30
+ {
31
+ "input": "0.0000000001",
32
+ "ok": true,
33
+ "finite": true,
34
+ "plain": "0.0000000001",
35
+ "sign": 2
36
+ },
37
+ {
38
+ "input": "123456789012345678901234567890.123456789",
39
+ "ok": true,
40
+ "finite": true,
41
+ "plain": "123456789012345678901234567890.123456789",
42
+ "sign": 2
43
+ },
44
+ {
45
+ "input": "1e-9",
46
+ "ok": true,
47
+ "finite": true,
48
+ "plain": "0.000000001",
49
+ "sign": 2
50
+ },
51
+ {
52
+ "input": "1E5",
53
+ "ok": true,
54
+ "finite": true,
55
+ "plain": "100000.0",
56
+ "sign": 2
57
+ },
58
+ {
59
+ "input": " 1.5 ",
60
+ "ok": true,
61
+ "finite": true,
62
+ "plain": "1.5",
63
+ "sign": 2
64
+ },
65
+ {
66
+ "input": "1_000.5",
67
+ "ok": true,
68
+ "finite": true,
69
+ "plain": "1000.5",
70
+ "sign": 2
71
+ },
72
+ {
73
+ "input": ".5",
74
+ "ok": true,
75
+ "finite": true,
76
+ "plain": "0.5",
77
+ "sign": 2
78
+ },
79
+ {
80
+ "input": "5.",
81
+ "ok": true,
82
+ "finite": true,
83
+ "plain": "5.0",
84
+ "sign": 2
85
+ },
86
+ {
87
+ "input": "+2",
88
+ "ok": true,
89
+ "finite": true,
90
+ "plain": "2.0",
91
+ "sign": 2
92
+ },
93
+ {
94
+ "input": "",
95
+ "ok": false,
96
+ "error": "invalid value for BigDecimal(): \"\""
97
+ },
98
+ {
99
+ "input": "abc",
100
+ "ok": false,
101
+ "error": "invalid value for BigDecimal(): \"abc\""
102
+ },
103
+ {
104
+ "input": "1,5",
105
+ "ok": false,
106
+ "error": "invalid value for BigDecimal(): \"1,5\""
107
+ },
108
+ {
109
+ "input": "0x10",
110
+ "ok": false,
111
+ "error": "invalid value for BigDecimal(): \"0x10\""
112
+ },
113
+ {
114
+ "input": "NaN",
115
+ "ok": true,
116
+ "finite": false,
117
+ "plain": "NaN",
118
+ "sign": 0
119
+ },
120
+ {
121
+ "input": "Infinity",
122
+ "ok": true,
123
+ "finite": false,
124
+ "plain": "Infinity",
125
+ "sign": 3
126
+ },
127
+ {
128
+ "input": "-Infinity",
129
+ "ok": true,
130
+ "finite": false,
131
+ "plain": "-Infinity",
132
+ "sign": -3
133
+ },
134
+ {
135
+ "input": "1d2",
136
+ "ok": true,
137
+ "finite": true,
138
+ "plain": "100.0",
139
+ "sign": 2
140
+ },
141
+ {
142
+ "input": "true",
143
+ "ok": false,
144
+ "error": "invalid value for BigDecimal(): \"true\""
145
+ }
146
+ ]
@@ -0,0 +1,74 @@
1
+ [
2
+ {
3
+ "args": {
4
+ "ordertype": "market",
5
+ "type": "buy",
6
+ "volume": "0.0012",
7
+ "pair": "XBTEUR",
8
+ "oflags": [
9
+ "viqc"
10
+ ],
11
+ "cl_ord_id": "6f1c1a52-7c8e-4d0e-9a57-0b6f0f1d2e3a",
12
+ "deadline": "2026-09-30T12:00:10.000Z"
13
+ },
14
+ "nonce": 1727000000000001,
15
+ "api_key": "key",
16
+ "api_secret": "dGVzdF9zZWNyZXRfa2V5XzEyMzQ1Njc4OTAxMjM0NTY=",
17
+ "body": "nonce=1727000000000001&ordertype=market&type=buy&volume=0.0012&pair=XBTEUR&cl_ord_id=6f1c1a52-7c8e-4d0e-9a57-0b6f0f1d2e3a&oflags=viqc&deadline=2026-09-30T12%3A00%3A10.000Z",
18
+ "headers": {
19
+ "API-Key": "key",
20
+ "API-Sign": "WIj+HLMKiN3JisS0SDtgnfgkztRD9vRgVUoeuKogUr/BVsObdxT8KBIMJ87j9x6P5y0cdyrkcjIzm6F1LciAVA==",
21
+ "Accept": "application/json",
22
+ "Content-Type": "application/x-www-form-urlencoded",
23
+ "User-Agent": "Honeymaker Ruby"
24
+ }
25
+ },
26
+ {
27
+ "args": {
28
+ "ordertype": "limit",
29
+ "type": "buy",
30
+ "volume": "0.00119",
31
+ "pair": "XBTEUR",
32
+ "price": "49870.1",
33
+ "oflags": [],
34
+ "cl_ord_id": "0d9e2c1b-2f4a-4b8c-8d1e-5a6b7c8d9e0f",
35
+ "deadline": "2026-09-30T12:00:10.123Z"
36
+ },
37
+ "nonce": 1727000000000001,
38
+ "api_key": "key",
39
+ "api_secret": "dGVzdF9zZWNyZXRfa2V5XzEyMzQ1Njc4OTAxMjM0NTY=",
40
+ "body": "nonce=1727000000000001&ordertype=limit&type=buy&volume=0.00119&pair=XBTEUR&cl_ord_id=0d9e2c1b-2f4a-4b8c-8d1e-5a6b7c8d9e0f&price=49870.1&deadline=2026-09-30T12%3A00%3A10.123Z",
41
+ "headers": {
42
+ "API-Key": "key",
43
+ "API-Sign": "szaVyZ0WTnm6BQtuK3TWp6/52uxNxx6U3OCugNj5+pXBf8nPB7qf1dg9wpxWURUf962W+ikCiAv2zJqYijbnhg==",
44
+ "Accept": "application/json",
45
+ "Content-Type": "application/x-www-form-urlencoded",
46
+ "User-Agent": "Honeymaker Ruby"
47
+ }
48
+ },
49
+ {
50
+ "args": {
51
+ "ordertype": "market",
52
+ "type": "buy",
53
+ "volume": "60",
54
+ "pair": "XBTEUR",
55
+ "oflags": [
56
+ "viqc"
57
+ ],
58
+ "cl_ord_id": "6f1c1a52-7c8e-4d0e-9a57-0b6f0f1d2e3a",
59
+ "deadline": "2026-09-30T12:00:10.000Z",
60
+ "validate": true
61
+ },
62
+ "nonce": 1727000000000001,
63
+ "api_key": "key",
64
+ "api_secret": "dGVzdF9zZWNyZXRfa2V5XzEyMzQ1Njc4OTAxMjM0NTY=",
65
+ "body": "nonce=1727000000000001&ordertype=market&type=buy&volume=60&pair=XBTEUR&cl_ord_id=6f1c1a52-7c8e-4d0e-9a57-0b6f0f1d2e3a&oflags=viqc&deadline=2026-09-30T12%3A00%3A10.000Z&validate=true",
66
+ "headers": {
67
+ "API-Key": "key",
68
+ "API-Sign": "us2T6Ya8LlwwoSU6xZFflnv5yBe7Qvee7Dk4UoHLTRYzIZeDJ/mJo1Tm1Mm4hmnvW9wRuWSvaMD/XlLOei+A5g==",
69
+ "Accept": "application/json",
70
+ "Content-Type": "application/x-www-form-urlencoded",
71
+ "User-Agent": "Honeymaker Ruby"
72
+ }
73
+ }
74
+ ]