honeymaker 0.12.2 → 0.12.4
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 +4 -4
- data/.github/workflows/native-gems.yml +100 -0
- data/.github/workflows/release.yml +36 -0
- data/.github/workflows/test.yml +19 -4
- data/Cargo.lock +1078 -0
- data/Cargo.toml +14 -0
- data/README.md +39 -0
- data/Rakefile +28 -16
- data/crates/honeymaker/Cargo.toml +18 -0
- data/crates/honeymaker/src/encode.rs +106 -0
- data/crates/honeymaker/src/kraken/lookup.rs +140 -0
- data/crates/honeymaker/src/kraken/mod.rs +5 -0
- data/crates/honeymaker/src/kraken/normalize.rs +536 -0
- data/crates/honeymaker/src/kraken/requests.rs +286 -0
- data/crates/honeymaker/src/kraken/sign.rs +166 -0
- data/crates/honeymaker/src/kraken/trades.rs +201 -0
- data/crates/honeymaker/src/lib.rs +10 -0
- data/crates/honeymaker/src/num.rs +115 -0
- data/crates/honeymaker/src/semantics.rs +100 -0
- data/crates/honeymaker/src/types.rs +85 -0
- data/crates/honeymaker/tests/kraken_stage2.rs +592 -0
- data/crates/honeymaker/tests/kraken_trades_pager.rs +307 -0
- data/crates/honeymaker/tests/kraken_vectors.rs +78 -0
- data/crates/honeymaker/tests/vectors/decimal.json +146 -0
- data/crates/honeymaker/tests/vectors/kraken_add_order_wire.json +74 -0
- data/crates/honeymaker/tests/vectors/kraken_signing.json +3314 -0
- data/crates/honeymaker/tests/vectors/kraken_trades_pager.json +1583 -0
- data/crates/honeymaker-client/Cargo.toml +30 -0
- data/crates/honeymaker-client/examples/probe.rs +147 -0
- data/crates/honeymaker-client/src/kraken.rs +514 -0
- data/crates/honeymaker-client/src/lib.rs +4 -0
- data/crates/honeymaker-client/src/transport/connect.rs +583 -0
- data/crates/honeymaker-client/src/transport/io_timeout.rs +352 -0
- data/crates/honeymaker-client/src/transport/response.rs +169 -0
- data/crates/honeymaker-client/src/transport.rs +467 -0
- data/crates/honeymaker-client/tests/classification.rs +203 -0
- data/crates/honeymaker-client/tests/kraken_client.rs +949 -0
- data/crates/honeymaker-client/tests/response.rs +522 -0
- data/crates/honeymaker-client/tests/support/mod.rs +550 -0
- data/crates/honeymaker-client/tests/transport.rs +828 -0
- data/ext/honeymaker_native/Cargo.toml +16 -0
- data/ext/honeymaker_native/extconf.rb +6 -0
- data/ext/honeymaker_native/src/convert.rs +150 -0
- data/ext/honeymaker_native/src/kraken.rs +362 -0
- data/ext/honeymaker_native/src/lib.rs +90 -0
- data/ext/honeymaker_native/src/ruby_decimal.rs +90 -0
- data/lib/honeymaker/clients/bybit.rb +19 -2
- data/lib/honeymaker/clients/coinbase.rb +3 -2
- data/lib/honeymaker/clients/kraken.rb +17 -1
- data/lib/honeymaker/exchanges/coinbase.rb +5 -4
- data/lib/honeymaker/native/kraken.rb +263 -0
- data/lib/honeymaker/native.rb +42 -0
- data/lib/honeymaker/version.rb +1 -1
- data/lib/honeymaker.rb +12 -2
- data/script/parity/kraken_client_live.rb +191 -0
- data/script/parity/kraken_live.rb +70 -0
- data/script/parity/kraken_transport.rb +604 -0
- data/script/parity/resolve.c +49 -0
- data/script/release/push_gems.sh +28 -0
- data/script/vectors/kraken.rb +50 -0
- data/script/vectors/kraken_stage2.rb +146 -0
- data/test/fixtures/coinbase_products.json +1 -1
- data/test/honeymaker/clients/bybit_client_test.rb +25 -0
- data/test/honeymaker/clients/coinbase_client_test.rb +14 -0
- data/test/honeymaker/clients/kraken_client_test.rb +27 -0
- data/test/honeymaker/exchanges/coinbase_test.rb +22 -2
- data/test/native/backend_switch_test.rb +53 -0
- data/test/native/convert_test.rb +137 -0
- data/test/native/gem_isolation_test.rb +17 -0
- data/test/native/loader_test.rb +29 -0
- data/test/native/lockstep_test.rb +23 -0
- data/test/native/signing_smoke_test.rb +28 -0
- data/test/parity/kraken_client_parity_test.rb +398 -0
- data/test/parity/kraken_exchange_parity_test.rb +172 -0
- data/test/release/branch_hardening_test.rb +34 -0
- data/test/release/push_gems_test.rb +79 -0
- data/test/smoke.rb +24 -0
- data/test/support/kraken_signing_smoke.rb +21 -0
- data/test/support/parity_helper.rb +62 -0
- data/test/support/venue_double.rb +100 -0
- 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
|
+
]
|