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,16 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "honeymaker_native" # rb-sys finds the extension by this package name
|
|
3
|
+
version.workspace = true
|
|
4
|
+
edition.workspace = true
|
|
5
|
+
rust-version.workspace = true
|
|
6
|
+
license.workspace = true
|
|
7
|
+
publish = false
|
|
8
|
+
|
|
9
|
+
[lib]
|
|
10
|
+
name = "honeymaker_native" # Init_honeymaker_native, loaded as honeymaker/honeymaker_native
|
|
11
|
+
crate-type = ["cdylib"]
|
|
12
|
+
|
|
13
|
+
[dependencies]
|
|
14
|
+
honeymaker-core = { package = "honeymaker", path = "../../crates/honeymaker" }
|
|
15
|
+
magnus = "0.9"
|
|
16
|
+
serde_json = { version = "1", features = ["preserve_order", "arbitrary_precision"] }
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
//! Ruby ⇄ JSON values. Root Ruby objects across calls with BoxValue; only Rust-owned JSON
|
|
2
|
+
//! values go in Rust collections. Ruby containers are filled as each element is created.
|
|
3
|
+
|
|
4
|
+
use magnus::{
|
|
5
|
+
Float, Integer, RArray, RHash, RString, Ruby, Value,
|
|
6
|
+
encoding::EncodingCapable,
|
|
7
|
+
r_hash::ForEach,
|
|
8
|
+
value::{BoxValue, ReprValue},
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
pub fn to_json(ruby: &Ruby, v: Value) -> Result<serde_json::Value, magnus::Error> {
|
|
12
|
+
use serde_json::Value as J;
|
|
13
|
+
let v = BoxValue::new(v);
|
|
14
|
+
if v.is_nil() {
|
|
15
|
+
return Ok(J::Null);
|
|
16
|
+
}
|
|
17
|
+
if v.is_kind_of(ruby.class_true_class()) {
|
|
18
|
+
return Ok(J::Bool(true));
|
|
19
|
+
}
|
|
20
|
+
if v.is_kind_of(ruby.class_false_class()) {
|
|
21
|
+
return Ok(J::Bool(false));
|
|
22
|
+
}
|
|
23
|
+
if let Some(s) = RString::from_value(*v) {
|
|
24
|
+
// Copy before any Ruby call: the borrowed slice cannot survive a GC/mutation.
|
|
25
|
+
let bytes = unsafe { s.as_slice() }.to_vec();
|
|
26
|
+
if s.enc_get() == ruby.utf8_encindex()
|
|
27
|
+
&& let Ok(text) = std::str::from_utf8(&bytes)
|
|
28
|
+
{
|
|
29
|
+
return Ok(J::String(text.to_string()));
|
|
30
|
+
}
|
|
31
|
+
let encoding: Value = s.funcall("encoding", ())?;
|
|
32
|
+
let name: String = encoding.funcall("name", ())?;
|
|
33
|
+
return Ok(serde_json::json!({honeymaker_core::semantics::RUBY_STRING: [bytes, name]}));
|
|
34
|
+
}
|
|
35
|
+
if Integer::from_value(*v).is_some() || Float::from_value(*v).is_some() {
|
|
36
|
+
// Preserve Float#to_s exactly: legacy passes these digits to BigDecimal.
|
|
37
|
+
// Non-finite floats use the sentinel understood by semantics::to_s.
|
|
38
|
+
let text: String = v.funcall("to_s", ())?;
|
|
39
|
+
return Ok(match text.parse::<serde_json::Number>() {
|
|
40
|
+
Ok(n) => J::Number(n),
|
|
41
|
+
Err(_) => J::Object(serde_json::Map::from_iter([(
|
|
42
|
+
honeymaker_core::semantics::RUBY_FLOAT.to_string(),
|
|
43
|
+
J::String(text),
|
|
44
|
+
)])),
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
if let Some(a) = RArray::from_value(*v) {
|
|
48
|
+
let a = BoxValue::new(a);
|
|
49
|
+
let mut out = Vec::with_capacity(a.len());
|
|
50
|
+
for i in 0..a.len() {
|
|
51
|
+
out.push(to_json(ruby, a.entry(i as isize)?)?);
|
|
52
|
+
}
|
|
53
|
+
return Ok(J::Array(out));
|
|
54
|
+
}
|
|
55
|
+
if let Some(h) = RHash::from_value(*v) {
|
|
56
|
+
let h = BoxValue::new(h);
|
|
57
|
+
let mut m = serde_json::Map::new();
|
|
58
|
+
h.foreach(|k: Value, val: Value| {
|
|
59
|
+
let k = BoxValue::new(k);
|
|
60
|
+
let val = BoxValue::new(val);
|
|
61
|
+
let key = RString::from_value(*k).ok_or_else(|| shape(ruby, "non-string key"))?;
|
|
62
|
+
let prefix = honeymaker_core::semantics::RUBY_KEY;
|
|
63
|
+
let key = match key.to_string() {
|
|
64
|
+
Ok(text) if !text.starts_with("\0ruby_") => text,
|
|
65
|
+
_ => format!("{prefix}{}", to_json(ruby, *k)?),
|
|
66
|
+
};
|
|
67
|
+
m.insert(key, to_json(ruby, *val)?);
|
|
68
|
+
Ok(ForEach::Continue)
|
|
69
|
+
})?;
|
|
70
|
+
return Ok(J::Object(m));
|
|
71
|
+
}
|
|
72
|
+
Err(shape(ruby, &format!("unsupported value {}", v.inspect())))
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
pub fn from_json(ruby: &Ruby, v: &serde_json::Value) -> Result<Value, magnus::Error> {
|
|
76
|
+
use serde_json::Value as J;
|
|
77
|
+
Ok(match v {
|
|
78
|
+
J::Null => ruby.qnil().as_value(),
|
|
79
|
+
J::Bool(true) => ruby.qtrue().as_value(),
|
|
80
|
+
J::Bool(false) => ruby.qfalse().as_value(),
|
|
81
|
+
J::String(s) => ruby.str_new(s).as_value(),
|
|
82
|
+
J::Number(n) => {
|
|
83
|
+
let text = n.to_string();
|
|
84
|
+
let conv = if text.contains(['.', 'e', 'E']) {
|
|
85
|
+
"Float"
|
|
86
|
+
} else {
|
|
87
|
+
"Integer"
|
|
88
|
+
};
|
|
89
|
+
ruby.module_kernel().funcall(conv, (text,))?
|
|
90
|
+
}
|
|
91
|
+
J::Array(a) => {
|
|
92
|
+
let out = BoxValue::new(ruby.ary_new_capa(a.len()));
|
|
93
|
+
for e in a {
|
|
94
|
+
out.push(from_json(ruby, e)?)?;
|
|
95
|
+
}
|
|
96
|
+
out.as_value()
|
|
97
|
+
}
|
|
98
|
+
J::Object(m) => {
|
|
99
|
+
if let Some(J::Array(parts)) = m
|
|
100
|
+
.get(honeymaker_core::semantics::RUBY_STRING)
|
|
101
|
+
.filter(|_| m.len() == 1)
|
|
102
|
+
{
|
|
103
|
+
let bytes: Vec<u8> = serde_json::from_value(parts[0].clone())
|
|
104
|
+
.map_err(|_| shape(ruby, "invalid Ruby string bytes"))?;
|
|
105
|
+
let out = BoxValue::new(ruby.str_from_slice(&bytes));
|
|
106
|
+
out.funcall::<_, _, Value>(
|
|
107
|
+
"force_encoding",
|
|
108
|
+
(parts[1].as_str().unwrap_or("UTF-8"),),
|
|
109
|
+
)?;
|
|
110
|
+
return Ok(out.as_value());
|
|
111
|
+
}
|
|
112
|
+
if let Some(J::String(text)) = m
|
|
113
|
+
.get(honeymaker_core::semantics::RUBY_FLOAT)
|
|
114
|
+
.filter(|_| m.len() == 1)
|
|
115
|
+
{
|
|
116
|
+
let f = match text.as_str() {
|
|
117
|
+
"Infinity" => f64::INFINITY,
|
|
118
|
+
"-Infinity" => f64::NEG_INFINITY,
|
|
119
|
+
_ => f64::NAN,
|
|
120
|
+
};
|
|
121
|
+
// Kernel#Float("Infinity") raises, so build the float directly.
|
|
122
|
+
return Ok(ruby.float_from_f64(f).as_value());
|
|
123
|
+
}
|
|
124
|
+
let h = BoxValue::new(ruby.hash_new());
|
|
125
|
+
for (k, val) in m {
|
|
126
|
+
let key =
|
|
127
|
+
BoxValue::new(from_json(ruby, &honeymaker_core::semantics::object_key(k))?);
|
|
128
|
+
let value = from_json(ruby, val)?;
|
|
129
|
+
h.aset(*key, value)?;
|
|
130
|
+
}
|
|
131
|
+
h.as_value()
|
|
132
|
+
}
|
|
133
|
+
})
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Used by the normalizers added in subsequent tasks.
|
|
137
|
+
#[allow(dead_code)]
|
|
138
|
+
pub fn sym(s: &str) -> Value {
|
|
139
|
+
Ruby::get()
|
|
140
|
+
.expect("called on a Ruby thread")
|
|
141
|
+
.sym_new(s)
|
|
142
|
+
.as_value()
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
pub fn shape(ruby: &Ruby, msg: &str) -> magnus::Error {
|
|
146
|
+
let class = ruby
|
|
147
|
+
.eval::<magnus::ExceptionClass>("Honeymaker::Native::ShapeError")
|
|
148
|
+
.unwrap_or_else(|_| ruby.exception_standard_error());
|
|
149
|
+
magnus::Error::new(class, msg.to_string())
|
|
150
|
+
}
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
use crate::convert::{self, sym};
|
|
2
|
+
use crate::ruby_decimal::RubyDecimal;
|
|
3
|
+
use honeymaker_core::kraken::normalize::{self, Finished};
|
|
4
|
+
use honeymaker_core::kraken::requests::{Fields, Method, layout};
|
|
5
|
+
use honeymaker_core::kraken::sign::{self, Credentials};
|
|
6
|
+
use honeymaker_core::kraken::trades::aggregate;
|
|
7
|
+
use honeymaker_core::num::NormError;
|
|
8
|
+
use magnus::{
|
|
9
|
+
RArray, RHash, RString, Ruby, Value, encoding::EncodingCapable, prelude::*, r_hash::ForEach,
|
|
10
|
+
value::BoxValue,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
#[magnus::wrap(class = "Honeymaker::Native::Ext::Kraken", free_immediately)]
|
|
14
|
+
pub struct Kraken {
|
|
15
|
+
creds: Option<Credentials>,
|
|
16
|
+
/// Legacy keys the nonce store by the raw api_key; nil gets its own slot.
|
|
17
|
+
nonce_key: Option<String>,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
pub(crate) fn norm(ruby: &Ruby, e: NormError<magnus::Error>) -> magnus::Error {
|
|
21
|
+
match e {
|
|
22
|
+
NormError::Num(err) => err, // Ruby's own ArgumentError from BigDecimal()
|
|
23
|
+
NormError::Shape(msg) => convert::shape(ruby, &msg),
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
fn headers(ruby: &Ruby, pairs: &[(String, String)]) -> Result<RHash, magnus::Error> {
|
|
28
|
+
let h = ruby.hash_new();
|
|
29
|
+
for (k, v) in pairs {
|
|
30
|
+
h.aset(k.as_str(), v.as_str())?;
|
|
31
|
+
}
|
|
32
|
+
Ok(h)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
pub(crate) fn verdict<T>(
|
|
36
|
+
ruby: &Ruby,
|
|
37
|
+
f: Finished<T>,
|
|
38
|
+
ok: impl FnOnce(T) -> Result<Value, magnus::Error>,
|
|
39
|
+
) -> Result<RArray, magnus::Error> {
|
|
40
|
+
let out = ruby.ary_new();
|
|
41
|
+
match f {
|
|
42
|
+
Finished::Ok(v) => {
|
|
43
|
+
out.push(ruby.str_new("ok"))?;
|
|
44
|
+
out.push(ok(v)?)?;
|
|
45
|
+
}
|
|
46
|
+
// The Ruby wrapper builds its Failure from result.data["error"], exactly as legacy.
|
|
47
|
+
Finished::Venue(_) => out.push(ruby.str_new("venue"))?,
|
|
48
|
+
Finished::Unreadable => out.push(ruby.str_new("unreadable"))?,
|
|
49
|
+
}
|
|
50
|
+
Ok(out)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
fn opt_dec(ruby: &Ruby, d: &Option<RubyDecimal>) -> Value {
|
|
54
|
+
d.as_ref()
|
|
55
|
+
.map(|d| d.value())
|
|
56
|
+
.unwrap_or_else(|| ruby.qnil().as_value())
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
fn side_symbol(ruby: &Ruby, value: Option<&serde_json::Value>) -> Result<Value, magnus::Error> {
|
|
60
|
+
match value {
|
|
61
|
+
Some(v) => convert::from_json(ruby, v)?.funcall("to_sym", ()),
|
|
62
|
+
None => Ok(ruby.qnil().as_value()),
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
impl Kraken {
|
|
67
|
+
fn new(api_key: Option<String>, api_secret: Option<String>) -> Self {
|
|
68
|
+
let creds = match (&api_key, &api_secret) {
|
|
69
|
+
(None, None) => None,
|
|
70
|
+
(k, s) => Some(Credentials {
|
|
71
|
+
api_key: k.clone().unwrap_or_default(),
|
|
72
|
+
api_secret: s.clone().unwrap_or_default(),
|
|
73
|
+
}),
|
|
74
|
+
};
|
|
75
|
+
Kraken {
|
|
76
|
+
creds,
|
|
77
|
+
nonce_key: api_key,
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
fn layout_for(
|
|
82
|
+
ruby: &Ruby,
|
|
83
|
+
op: &str,
|
|
84
|
+
want: Method,
|
|
85
|
+
) -> Result<(&'static str, Fields), magnus::Error> {
|
|
86
|
+
match layout(op) {
|
|
87
|
+
Some((m, path, fields)) if m == want => Ok((path, fields)),
|
|
88
|
+
_ => Err(convert::shape(
|
|
89
|
+
ruby,
|
|
90
|
+
&format!("unknown Kraken {want:?} op {op}"),
|
|
91
|
+
)),
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/// → [path, [["nonce", Integer], [wire, original value], ...]] (nils dropped, legacy order)
|
|
96
|
+
fn build_post(
|
|
97
|
+
ruby: &Ruby,
|
|
98
|
+
rb_self: &Self,
|
|
99
|
+
op: String,
|
|
100
|
+
params: RHash,
|
|
101
|
+
) -> Result<RArray, magnus::Error> {
|
|
102
|
+
let (path, fields) = Self::layout_for(ruby, &op, Method::Post)?;
|
|
103
|
+
let pairs = ruby.ary_new();
|
|
104
|
+
let nonce = ruby.ary_new();
|
|
105
|
+
nonce.push(ruby.str_new("nonce"))?;
|
|
106
|
+
nonce.push(ruby.integer_from_u64(sign::next_nonce(rb_self.nonce_key.as_deref())))?;
|
|
107
|
+
pairs.push(nonce)?;
|
|
108
|
+
for (wire, name) in fields {
|
|
109
|
+
let v: Value = params.aref(ruby.str_new(name))?;
|
|
110
|
+
if !v.is_nil() {
|
|
111
|
+
let pair = ruby.ary_new();
|
|
112
|
+
pair.push(ruby.str_new(wire))?;
|
|
113
|
+
pair.push(v)?;
|
|
114
|
+
pairs.push(pair)?;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
let out = ruby.ary_new();
|
|
118
|
+
out.push(ruby.str_new(path))?;
|
|
119
|
+
out.push(pairs)?;
|
|
120
|
+
Ok(out)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/// Signs the exact body Ruby encoded: legacy private_headers(req.path, req.body).
|
|
124
|
+
fn sign(
|
|
125
|
+
ruby: &Ruby,
|
|
126
|
+
rb_self: &Self,
|
|
127
|
+
path: String,
|
|
128
|
+
body: String,
|
|
129
|
+
) -> Result<RHash, magnus::Error> {
|
|
130
|
+
headers(
|
|
131
|
+
ruby,
|
|
132
|
+
&sign::private_headers(&path, &body, rb_self.creds.as_ref()),
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/// → [path, {wire => original value} (nils dropped), public headers]
|
|
137
|
+
fn build_get(
|
|
138
|
+
ruby: &Ruby,
|
|
139
|
+
_rb_self: &Self,
|
|
140
|
+
op: String,
|
|
141
|
+
params: RHash,
|
|
142
|
+
) -> Result<RArray, magnus::Error> {
|
|
143
|
+
let (path, fields) = Self::layout_for(ruby, &op, Method::Get)?;
|
|
144
|
+
let query = ruby.hash_new();
|
|
145
|
+
for (wire, name) in fields {
|
|
146
|
+
let v: Value = params.aref(ruby.str_new(name))?;
|
|
147
|
+
if !v.is_nil() {
|
|
148
|
+
query.aset(ruby.str_new(wire), v)?;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
let out = ruby.ary_new();
|
|
152
|
+
out.push(ruby.str_new(path))?;
|
|
153
|
+
out.push(query)?;
|
|
154
|
+
out.push(headers(ruby, &sign::public_headers())?)?;
|
|
155
|
+
Ok(out)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
fn finish(
|
|
159
|
+
ruby: &Ruby,
|
|
160
|
+
_rb_self: &Self,
|
|
161
|
+
op: String,
|
|
162
|
+
data: Value,
|
|
163
|
+
) -> Result<RArray, magnus::Error> {
|
|
164
|
+
let json = convert::to_json(ruby, data)?;
|
|
165
|
+
match op.as_str() {
|
|
166
|
+
"balances" => verdict(
|
|
167
|
+
ruby,
|
|
168
|
+
normalize::balances::<RubyDecimal>(&json).map_err(|e| norm(ruby, e))?,
|
|
169
|
+
|list| {
|
|
170
|
+
let h = ruby.hash_new();
|
|
171
|
+
for b in list {
|
|
172
|
+
let e = ruby.hash_new();
|
|
173
|
+
e.aset(sym("free"), b.free.value())?;
|
|
174
|
+
e.aset(sym("locked"), b.locked.value())?;
|
|
175
|
+
h.aset(convert::from_json(ruby, &b.asset)?, e)?;
|
|
176
|
+
}
|
|
177
|
+
Ok(h.as_value())
|
|
178
|
+
},
|
|
179
|
+
),
|
|
180
|
+
"query_orders_info" => verdict(
|
|
181
|
+
ruby,
|
|
182
|
+
normalize::orders::<RubyDecimal>(&json).map_err(|e| norm(ruby, e))?,
|
|
183
|
+
|list| {
|
|
184
|
+
let h = ruby.hash_new();
|
|
185
|
+
for o in list {
|
|
186
|
+
let e = ruby.hash_new();
|
|
187
|
+
e.aset(sym("order_id"), convert::from_json(ruby, &o.order_id)?)?;
|
|
188
|
+
e.aset(sym("status"), sym(o.status.as_str()))?;
|
|
189
|
+
e.aset(sym("side"), side_symbol(ruby, o.side.as_ref())?)?;
|
|
190
|
+
e.aset(sym("order_type"), sym(o.order_type.as_str()))?;
|
|
191
|
+
e.aset(sym("price"), opt_dec(ruby, &o.price))?;
|
|
192
|
+
e.aset(sym("amount"), opt_dec(ruby, &o.amount))?;
|
|
193
|
+
e.aset(sym("quote_amount"), opt_dec(ruby, &o.quote_amount))?;
|
|
194
|
+
e.aset(sym("amount_exec"), o.amount_exec.value())?;
|
|
195
|
+
e.aset(sym("quote_amount_exec"), o.quote_amount_exec.value())?;
|
|
196
|
+
h.aset(convert::from_json(ruby, &o.order_id)?, e)?; // the wrapper appends :raw last
|
|
197
|
+
}
|
|
198
|
+
Ok(h.as_value())
|
|
199
|
+
},
|
|
200
|
+
),
|
|
201
|
+
"add_order" => verdict(
|
|
202
|
+
ruby,
|
|
203
|
+
normalize::add_order::<RubyDecimal>(&json).map_err(|e| norm(ruby, e))?,
|
|
204
|
+
|id| convert::from_json(ruby, &id),
|
|
205
|
+
),
|
|
206
|
+
"tickers_info" => verdict(
|
|
207
|
+
ruby,
|
|
208
|
+
normalize::tickers::<RubyDecimal>(&json).map_err(|e| norm(ruby, e))?,
|
|
209
|
+
|list| {
|
|
210
|
+
let out = ruby.ary_new();
|
|
211
|
+
for t in list {
|
|
212
|
+
let h = ruby.hash_new();
|
|
213
|
+
h.aset(sym("ticker"), convert::from_json(ruby, &t.ticker)?)?;
|
|
214
|
+
h.aset(sym("base"), convert::from_json(ruby, &t.base)?)?;
|
|
215
|
+
h.aset(sym("quote"), convert::from_json(ruby, &t.quote)?)?;
|
|
216
|
+
h.aset(
|
|
217
|
+
sym("minimum_base_size"),
|
|
218
|
+
convert::from_json(ruby, &t.minimum_base_size)?,
|
|
219
|
+
)?;
|
|
220
|
+
h.aset(
|
|
221
|
+
sym("minimum_quote_size"),
|
|
222
|
+
convert::from_json(ruby, &t.minimum_quote_size)?,
|
|
223
|
+
)?;
|
|
224
|
+
h.aset(sym("maximum_base_size"), ruby.qnil())?;
|
|
225
|
+
h.aset(sym("maximum_quote_size"), ruby.qnil())?;
|
|
226
|
+
h.aset(
|
|
227
|
+
sym("base_decimals"),
|
|
228
|
+
convert::from_json(ruby, &t.base_decimals)?,
|
|
229
|
+
)?;
|
|
230
|
+
h.aset(
|
|
231
|
+
sym("quote_decimals"),
|
|
232
|
+
convert::from_json(ruby, &t.quote_decimals)?,
|
|
233
|
+
)?;
|
|
234
|
+
h.aset(
|
|
235
|
+
sym("price_decimals"),
|
|
236
|
+
convert::from_json(ruby, &t.price_decimals)?,
|
|
237
|
+
)?;
|
|
238
|
+
h.aset(sym("available"), ruby.qtrue())?;
|
|
239
|
+
h.aset(sym("trading_enabled"), t.trading_enabled)?;
|
|
240
|
+
out.push(h)?;
|
|
241
|
+
}
|
|
242
|
+
Ok(out.as_value())
|
|
243
|
+
},
|
|
244
|
+
),
|
|
245
|
+
"bid_ask" => verdict(
|
|
246
|
+
ruby,
|
|
247
|
+
normalize::bid_ask::<RubyDecimal>(&json).map_err(|e| norm(ruby, e))?,
|
|
248
|
+
|(bid, ask)| {
|
|
249
|
+
let h = ruby.hash_new();
|
|
250
|
+
h.aset(sym("bid"), bid.value())?;
|
|
251
|
+
h.aset(sym("ask"), ask.value())?;
|
|
252
|
+
Ok(h.as_value())
|
|
253
|
+
},
|
|
254
|
+
),
|
|
255
|
+
other => Err(convert::shape(ruby, &format!("unknown finish op {other}"))),
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/// by_order: { otxid => [Ruby trade hashes] } in first-seen order → legacy aggregate hashes.
|
|
260
|
+
fn aggregate_trades(ruby: &Ruby, by_order: RHash) -> Result<RHash, magnus::Error> {
|
|
261
|
+
let out = ruby.hash_new();
|
|
262
|
+
by_order.foreach(|otxid: Value, trades: RArray| {
|
|
263
|
+
let mut json = Vec::with_capacity(trades.len());
|
|
264
|
+
for i in 0..trades.len() {
|
|
265
|
+
json.push(convert::to_json(ruby, trades.entry(i as isize)?)?); // Rust values only
|
|
266
|
+
}
|
|
267
|
+
let a = aggregate::<RubyDecimal>(&json).map_err(|e| norm(ruby, e))?;
|
|
268
|
+
let first: Value = trades.entry(0)?;
|
|
269
|
+
let times = ruby.ary_new();
|
|
270
|
+
for i in 0..trades.len() {
|
|
271
|
+
let t: Value = trades.entry(i as isize)?;
|
|
272
|
+
let time: Value = t.funcall("[]", ("time",))?;
|
|
273
|
+
times.push(time.funcall::<_, _, Value>("to_f", ())?)?;
|
|
274
|
+
}
|
|
275
|
+
let h = ruby.hash_new();
|
|
276
|
+
h.aset(
|
|
277
|
+
sym("order_id"),
|
|
278
|
+
first.funcall::<_, _, Value>("[]", ("ordertxid",))?,
|
|
279
|
+
)?;
|
|
280
|
+
h.aset(sym("status"), sym("closed"))?;
|
|
281
|
+
h.aset(sym("side"), side_symbol(ruby, a.side.as_ref())?)?;
|
|
282
|
+
h.aset(sym("order_type"), sym(a.order_type.as_str()))?;
|
|
283
|
+
h.aset(sym("price"), opt_dec(ruby, &a.price))?;
|
|
284
|
+
h.aset(sym("amount"), ruby.qnil())?;
|
|
285
|
+
h.aset(sym("quote_amount"), ruby.qnil())?;
|
|
286
|
+
h.aset(sym("amount_exec"), a.vol.value())?;
|
|
287
|
+
h.aset(sym("quote_amount_exec"), a.cost.value())?;
|
|
288
|
+
h.aset(sym("fee"), a.fee.value())?;
|
|
289
|
+
h.aset(sym("pair"), first.funcall::<_, _, Value>("[]", ("pair",))?)?;
|
|
290
|
+
h.aset(sym("trade_count"), trades.len())?;
|
|
291
|
+
h.aset(
|
|
292
|
+
sym("last_trade_at"),
|
|
293
|
+
times.funcall::<_, _, Value>("max", ())?,
|
|
294
|
+
)?;
|
|
295
|
+
let raw = ruby.hash_new();
|
|
296
|
+
raw.aset(ruby.str_new("trades"), trades)?;
|
|
297
|
+
h.aset(sym("raw"), raw)?;
|
|
298
|
+
out.aset(otxid, h)?;
|
|
299
|
+
Ok(ForEach::Continue)
|
|
300
|
+
})?;
|
|
301
|
+
Ok(out)
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
fn classify_error(ruby: &Ruby, message: RString) -> Result<Value, magnus::Error> {
|
|
305
|
+
let message = BoxValue::new(message);
|
|
306
|
+
let encoding: Value = message.funcall("encoding", ())?;
|
|
307
|
+
if !encoding.funcall::<_, _, bool>("ascii_compatible?", ())?
|
|
308
|
+
|| !message.funcall::<_, _, bool>("valid_encoding?", ())?
|
|
309
|
+
{
|
|
310
|
+
// Let Ruby raise its exact regexp encoding exception before byte matching.
|
|
311
|
+
message.funcall::<_, _, Value>("match", ("",))?;
|
|
312
|
+
}
|
|
313
|
+
let bytes = unsafe { message.as_slice() }.to_vec();
|
|
314
|
+
Ok(match normalize::classify_error(&bytes) {
|
|
315
|
+
None => ruby.qnil().as_value(),
|
|
316
|
+
Some((code, caps)) => {
|
|
317
|
+
let h = ruby.hash_new();
|
|
318
|
+
h.aset(sym("code"), sym(code))?;
|
|
319
|
+
for (k, v) in caps {
|
|
320
|
+
let capture = BoxValue::new(ruby.str_from_slice(&v));
|
|
321
|
+
capture.enc_associate(message.enc_get())?;
|
|
322
|
+
h.aset(sym(k), *capture)?;
|
|
323
|
+
}
|
|
324
|
+
h.as_value()
|
|
325
|
+
}
|
|
326
|
+
})
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
fn set_fixed_nonce(n: Option<u64>) {
|
|
330
|
+
sign::set_fixed_nonce(n);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
fn reset_nonce_state() {
|
|
334
|
+
sign::reset_nonces();
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
pub fn define(ruby: &Ruby, ext: magnus::RModule) -> Result<(), magnus::Error> {
|
|
339
|
+
let c = ext.define_class("Kraken", ruby.class_object())?;
|
|
340
|
+
c.define_singleton_method("new", magnus::function!(Kraken::new, 2))?;
|
|
341
|
+
c.define_method("build_post", magnus::method!(Kraken::build_post, 2))?;
|
|
342
|
+
c.define_method("sign", magnus::method!(Kraken::sign, 2))?;
|
|
343
|
+
c.define_method("build_get", magnus::method!(Kraken::build_get, 2))?;
|
|
344
|
+
c.define_singleton_method(
|
|
345
|
+
"classify_error",
|
|
346
|
+
magnus::function!(Kraken::classify_error, 1),
|
|
347
|
+
)?;
|
|
348
|
+
c.define_method("finish", magnus::method!(Kraken::finish, 2))?;
|
|
349
|
+
c.define_singleton_method(
|
|
350
|
+
"aggregate_trades",
|
|
351
|
+
magnus::function!(Kraken::aggregate_trades, 1),
|
|
352
|
+
)?;
|
|
353
|
+
c.define_singleton_method(
|
|
354
|
+
"fixed_nonce=",
|
|
355
|
+
magnus::function!(Kraken::set_fixed_nonce, 1),
|
|
356
|
+
)?;
|
|
357
|
+
c.define_singleton_method(
|
|
358
|
+
"reset_nonce_state!",
|
|
359
|
+
magnus::function!(Kraken::reset_nonce_state, 0),
|
|
360
|
+
)?;
|
|
361
|
+
Ok(())
|
|
362
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
use magnus::{Error, Ruby, function, prelude::*};
|
|
2
|
+
|
|
3
|
+
mod convert;
|
|
4
|
+
mod kraken;
|
|
5
|
+
mod ruby_decimal;
|
|
6
|
+
|
|
7
|
+
fn version() -> &'static str {
|
|
8
|
+
env!("CARGO_PKG_VERSION")
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
fn roundtrip_for_tests(ruby: &Ruby, v: magnus::Value) -> Result<magnus::Value, Error> {
|
|
12
|
+
let j = convert::to_json(ruby, v)?;
|
|
13
|
+
convert::from_json(ruby, &j)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
fn decimal_for_tests(text: String) -> Result<magnus::Value, Error> {
|
|
17
|
+
use honeymaker_core::num::Num;
|
|
18
|
+
Ok(ruby_decimal::RubyDecimal::parse(&text)?.value())
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
fn to_s_for_tests(ruby: &Ruby, v: magnus::Value) -> Result<String, Error> {
|
|
22
|
+
let j = convert::to_json(ruby, v)?;
|
|
23
|
+
honeymaker_core::semantics::to_s(&j).map_err(|msg| convert::shape(ruby, &msg))
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
fn decimal_convert_for_tests(
|
|
27
|
+
ruby: &Ruby,
|
|
28
|
+
v: magnus::Value,
|
|
29
|
+
stringify: bool,
|
|
30
|
+
) -> Result<magnus::Value, Error> {
|
|
31
|
+
use honeymaker_core::num::Num;
|
|
32
|
+
use ruby_decimal::RubyDecimal;
|
|
33
|
+
let j = convert::to_json(ruby, v)?;
|
|
34
|
+
let decimal = if stringify {
|
|
35
|
+
RubyDecimal::parse_to_s(&j)?
|
|
36
|
+
} else {
|
|
37
|
+
RubyDecimal::parse_json(&j)?
|
|
38
|
+
};
|
|
39
|
+
Ok(decimal.value())
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
fn decimal_arithmetic_for_tests(
|
|
43
|
+
ruby: &Ruby,
|
|
44
|
+
left: String,
|
|
45
|
+
right: String,
|
|
46
|
+
) -> Result<magnus::RArray, Error> {
|
|
47
|
+
use honeymaker_core::num::Num;
|
|
48
|
+
use magnus::value::BoxValue;
|
|
49
|
+
use ruby_decimal::RubyDecimal;
|
|
50
|
+
|
|
51
|
+
// Keep the decimals on the Rust heap across collection and compaction, as normalizers will.
|
|
52
|
+
let mut decimals = vec![RubyDecimal::parse(&left)?, RubyDecimal::parse(&right)?];
|
|
53
|
+
let sum = decimals[0].add(&decimals[1])?;
|
|
54
|
+
decimals.push(sum);
|
|
55
|
+
decimals.push(decimals[0].sub(&decimals[1])?);
|
|
56
|
+
decimals.push(decimals[0].div(&decimals[1])?);
|
|
57
|
+
ruby.gc_start();
|
|
58
|
+
let gc = BoxValue::new(ruby.class_object().const_get::<_, magnus::RModule>("GC")?);
|
|
59
|
+
if gc.respond_to("compact", false)? {
|
|
60
|
+
gc.funcall::<_, _, magnus::Value>("compact", ())?;
|
|
61
|
+
}
|
|
62
|
+
let out = BoxValue::new(ruby.ary_new_capa(4));
|
|
63
|
+
for decimal in &decimals[2..] {
|
|
64
|
+
out.push(decimal.value())?;
|
|
65
|
+
}
|
|
66
|
+
out.push(decimals[0].is_zero()?)?;
|
|
67
|
+
Ok(*out)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
#[magnus::init]
|
|
71
|
+
fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
72
|
+
use magnus::value::BoxValue;
|
|
73
|
+
ruby.require("bigdecimal")?;
|
|
74
|
+
let native = BoxValue::new(ruby.define_module("Honeymaker")?.define_module("Native")?);
|
|
75
|
+
let ext = BoxValue::new(native.define_module("Ext")?);
|
|
76
|
+
kraken::define(ruby, *ext)?;
|
|
77
|
+
ext.define_singleton_method("version", function!(version, 0))?;
|
|
78
|
+
ext.define_singleton_method("roundtrip_for_tests", function!(roundtrip_for_tests, 1))?;
|
|
79
|
+
ext.define_singleton_method("decimal_for_tests", function!(decimal_for_tests, 1))?;
|
|
80
|
+
ext.define_singleton_method("to_s_for_tests", function!(to_s_for_tests, 1))?;
|
|
81
|
+
ext.define_singleton_method(
|
|
82
|
+
"decimal_convert_for_tests",
|
|
83
|
+
function!(decimal_convert_for_tests, 2),
|
|
84
|
+
)?;
|
|
85
|
+
ext.define_singleton_method(
|
|
86
|
+
"decimal_arithmetic_for_tests",
|
|
87
|
+
function!(decimal_arithmetic_for_tests, 2),
|
|
88
|
+
)?;
|
|
89
|
+
Ok(())
|
|
90
|
+
}
|