From 104d0810d9873f8d0f88c42fc6985525764da806 Mon Sep 17 00:00:00 2001 From: Vadim Nicolai Date: Sat, 1 Nov 2025 20:38:21 +0200 Subject: [PATCH 01/10] Small improvement. --- Cargo.lock | 694 ++++++++++++++++++++- Cargo.toml | 3 + crates/adapters/dydx/Cargo.toml | 95 +++ crates/adapters/dydx/LICENSE | 164 +++++ crates/adapters/dydx/src/common/consts.rs | 22 + crates/adapters/dydx/src/common/mod.rs | 31 + crates/adapters/dydx/src/common/testing.rs | 16 + crates/adapters/dydx/src/common/types.rs | 32 + crates/adapters/dydx/src/common/urls.rs | 37 ++ crates/adapters/dydx/src/config.rs | 45 ++ crates/adapters/dydx/src/error.rs | 56 ++ crates/adapters/dydx/src/http/mod.rs | 29 + crates/adapters/dydx/src/lib.rs | 60 ++ crates/adapters/dydx/src/proto/mod.rs | 22 + crates/adapters/dydx/src/python/mod.rs | 29 + 15 files changed, 1301 insertions(+), 34 deletions(-) create mode 100644 crates/adapters/dydx/Cargo.toml create mode 100644 crates/adapters/dydx/LICENSE create mode 100644 crates/adapters/dydx/src/common/consts.rs create mode 100644 crates/adapters/dydx/src/common/mod.rs create mode 100644 crates/adapters/dydx/src/common/testing.rs create mode 100644 crates/adapters/dydx/src/common/types.rs create mode 100644 crates/adapters/dydx/src/common/urls.rs create mode 100644 crates/adapters/dydx/src/config.rs create mode 100644 crates/adapters/dydx/src/error.rs create mode 100644 crates/adapters/dydx/src/http/mod.rs create mode 100644 crates/adapters/dydx/src/lib.rs create mode 100644 crates/adapters/dydx/src/proto/mod.rs create mode 100644 crates/adapters/dydx/src/python/mod.rs diff --git a/Cargo.lock b/Cargo.lock index c35c5304456a..7ac606622e2f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -110,7 +110,7 @@ dependencies = [ "k256", "once_cell", "rand 0.8.5", - "secp256k1", + "secp256k1 0.30.0", "serde", "serde_json", "serde_with", @@ -235,7 +235,7 @@ dependencies = [ "either", "serde", "serde_with", - "sha2", + "sha2 0.10.9", "thiserror 2.0.17", ] @@ -405,7 +405,7 @@ dependencies = [ "serde_json", "tokio", "tokio-stream", - "tower", + "tower 0.5.2", "tracing", "wasmtimer", ] @@ -577,7 +577,7 @@ dependencies = [ "serde_json", "thiserror 2.0.17", "tokio", - "tower", + "tower 0.5.2", "tracing", "url", "wasmtimer", @@ -1239,13 +1239,40 @@ dependencies = [ "fs_extra", ] +[[package]] +name = "axum" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" +dependencies = [ + "async-trait", + "axum-core 0.4.5", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "itoa", + "matchit 0.7.3", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "sync_wrapper", + "tower 0.5.2", + "tower-layer", + "tower-service", +] + [[package]] name = "axum" version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a18ed336352031311f4e0b4dd2ff392d4fbb370777c9d18d7fc9d7359f73871" dependencies = [ - "axum-core", + "axum-core 0.5.5", "base64 0.22.1", "bytes", "form_urlencoded", @@ -1256,7 +1283,7 @@ dependencies = [ "hyper", "hyper-util", "itoa", - "matchit", + "matchit 0.8.4", "memchr", "mime", "percent-encoding", @@ -1269,7 +1296,27 @@ dependencies = [ "sync_wrapper", "tokio", "tokio-tungstenite", - "tower", + "tower 0.5.2", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", "tower-layer", "tower-service", ] @@ -1331,6 +1378,12 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445" +[[package]] +name = "bech32" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d965446196e3b7decd44aa7ee49e31d630118f90ef12f97900f262eb915c951d" + [[package]] name = "bigdecimal" version = "0.4.9" @@ -1382,6 +1435,25 @@ dependencies = [ "syn 2.0.108", ] +[[package]] +name = "bip32" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db40d3dfbeab4e031d78c844642fa0caa0b0db11ce1607ac9d2986dff1405c69" +dependencies = [ + "bs58", + "hmac", + "k256", + "once_cell", + "pbkdf2", + "rand_core 0.6.4", + "ripemd", + "secp256k1 0.27.0", + "sha2 0.10.9", + "subtle", + "zeroize", +] + [[package]] name = "bit-set" version = "0.8.0" @@ -1434,6 +1506,15 @@ dependencies = [ "wyz", ] +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -1551,7 +1632,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" dependencies = [ - "sha2", + "sha2 0.10.9", "tinyvec", ] @@ -1844,7 +1925,7 @@ dependencies = [ "hmac", "k256", "serde", - "sha2", + "sha2 0.10.9", "thiserror 1.0.69", ] @@ -1860,7 +1941,7 @@ dependencies = [ "once_cell", "pbkdf2", "rand 0.8.5", - "sha2", + "sha2 0.10.9", "thiserror 1.0.69", ] @@ -1871,14 +1952,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62b962ad8545e43a28e14e87377812ba9ae748dd4fd963f4c10e9fcc6d13475b" dependencies = [ "base64 0.21.7", - "bech32", + "bech32 0.9.1", "bs58", "const-hex", "digest 0.10.7", "generic-array", "ripemd", "serde", - "sha2", + "sha2 0.10.9", "sha3", "thiserror 1.0.69", ] @@ -2038,6 +2119,47 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "cosmos-sdk-proto" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "462e1f6a8e005acc8835d32d60cbd7973ed65ea2a8d8473830e675f050956427" +dependencies = [ + "prost", + "tendermint-proto", +] + +[[package]] +name = "cosmos-sdk-proto" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95ac39be7373404accccaede7cc1ec942ccef14f0ca18d209967a756bf1dbb1f" +dependencies = [ + "prost", + "tendermint-proto", + "tonic 0.13.1", +] + +[[package]] +name = "cosmrs" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1394c263335da09e8ba8c4b2c675d804e3e0deb44cce0866a5f838d3ddd43d02" +dependencies = [ + "bip32", + "cosmos-sdk-proto 0.26.1", + "ecdsa", + "eyre", + "k256", + "rand_core 0.6.4", + "serde", + "serde_json", + "signature", + "subtle-encoding", + "tendermint", + "thiserror 1.0.69", +] + [[package]] name = "cpufeatures" version = "0.2.17" @@ -2214,6 +2336,19 @@ dependencies = [ "syn 2.0.108", ] +[[package]] +name = "curve25519-dalek-ng" +version = "4.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c359b7249347e46fb28804470d071c921156ad62b3eef5d34e2ba867533dec8" +dependencies = [ + "byteorder", + "digest 0.9.0", + "rand_core 0.6.4", + "subtle-ng", + "zeroize", +] + [[package]] name = "darling" version = "0.20.11" @@ -2318,7 +2453,7 @@ dependencies = [ "reqwest", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "thiserror 2.0.17", "time", "tokio", @@ -2789,7 +2924,7 @@ dependencies = [ "log", "parking_lot", "paste", - "petgraph", + "petgraph 0.8.3", ] [[package]] @@ -3089,7 +3224,7 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", + "block-buffer 0.10.4", "const-oid", "crypto-common", "subtle", @@ -3118,6 +3253,22 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" +[[package]] +name = "dydx-proto" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "945e74a43dd3e251849f5a87169aaa457d70d6506a27adf365a7d418c71c42d1" +dependencies = [ + "cosmos-sdk-proto 0.27.0", + "prost", + "prost-build", + "prost-types", + "regex", + "tonic 0.13.1", + "tonic-buf-build", + "tonic-build", +] + [[package]] name = "dyn-clone" version = "1.0.20" @@ -3149,6 +3300,19 @@ dependencies = [ "signature", ] +[[package]] +name = "ed25519-consensus" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c8465edc8ee7436ffea81d21a019b16676ee3db267aa8d5a8d729581ecf998b" +dependencies = [ + "curve25519-dalek-ng", + "hex", + "rand_core 0.6.4", + "sha2 0.9.9", + "zeroize", +] + [[package]] name = "ed25519-dalek" version = "2.2.0" @@ -3158,7 +3322,7 @@ dependencies = [ "curve25519-dalek", "ed25519", "serde", - "sha2", + "sha2 0.10.9", "subtle", "zeroize", ] @@ -3292,6 +3456,16 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "eyre" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" +dependencies = [ + "indenter", + "once_cell", +] + [[package]] name = "fallible-streaming-iterator" version = "0.1.9" @@ -3408,6 +3582,15 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "flex-error" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c606d892c9de11507fa0dcffc116434f94e105d0bbdc4e405b61519464c49d7b" +dependencies = [ + "paste", +] + [[package]] name = "flume" version = "0.11.1" @@ -3874,6 +4057,19 @@ dependencies = [ "webpki-roots 1.0.4", ] +[[package]] +name = "hyper-timeout" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" +dependencies = [ + "hyper", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", +] + [[package]] name = "hyper-util" version = "0.1.17" @@ -3892,7 +4088,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2", + "socket2 0.6.1", "tokio", "tower-service", "tracing", @@ -4141,6 +4337,12 @@ dependencies = [ "bon", ] +[[package]] +name = "indenter" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" + [[package]] name = "indexmap" version = "1.9.3" @@ -4305,7 +4507,7 @@ dependencies = [ "elliptic-curve", "once_cell", "serdect", - "sha2", + "sha2 0.10.9", "signature", ] @@ -4610,6 +4812,12 @@ dependencies = [ "regex-automata", ] +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + [[package]] name = "matchit" version = "0.8.4" @@ -4693,6 +4901,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "multimap" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" + [[package]] name = "multiversion" version = "0.7.4" @@ -4761,7 +4975,7 @@ dependencies = [ "async-stream", "async-trait", "aws-lc-rs", - "axum", + "axum 0.8.6", "chrono", "criterion", "dashmap", @@ -4856,7 +5070,7 @@ dependencies = [ "async-stream", "async-trait", "aws-lc-rs", - "axum", + "axum 0.8.6", "chrono", "criterion", "dashmap", @@ -5095,6 +5309,109 @@ dependencies = [ "ustr", ] +[[package]] +name = "nautilus-dydx" +version = "0.52.0" +dependencies = [ + "ahash 0.8.12", + "anyhow", + "async-stream", + "async-trait", + "aws-lc-rs", + "axum 0.8.6", + "base64 0.22.1", + "chrono", + "criterion", + "dashmap", + "derive_builder", + "dydx-proto", + "futures-util", + "indexmap 2.12.0", + "log", + "nautilus-common", + "nautilus-core", + "nautilus-data", + "nautilus-execution", + "nautilus-live", + "nautilus-model", + "nautilus-network", + "nautilus-testkit", + "pyo3", + "pyo3-async-runtimes", + "reqwest", + "rstest", + "rust_decimal", + "rust_decimal_macros", + "serde", + "serde_json", + "serde_urlencoded", + "strum 0.27.2", + "thiserror 2.0.17", + "tokio", + "tokio-tungstenite", + "tokio-util", + "tracing", + "tracing-subscriber", + "tracing-test", + "url", + "ustr", + "zeroize", +] + +[[package]] +name = "nautilus-dydx-private" +version = "0.52.0" +dependencies = [ + "anyhow", + "async-trait", + "axum 0.8.6", + "base64 0.22.1", + "bech32 0.11.0", + "bip32", + "chrono", + "cosmrs", + "criterion", + "derive_builder", + "dydx-proto", + "futures-util", + "hex", + "http-body-util", + "nautilus-common", + "nautilus-core", + "nautilus-data", + "nautilus-execution", + "nautilus-live", + "nautilus-model", + "nautilus-network", + "nautilus-serialization", + "nautilus-testkit", + "pbkdf2", + "prost", + "prost-types", + "pyo3", + "pyo3-async-runtimes", + "reqwest", + "rstest", + "rust_decimal", + "rustls", + "serde", + "serde_json", + "serde_urlencoded", + "sha2 0.10.9", + "strum 0.27.2", + "thiserror 2.0.17", + "time", + "tokio", + "tokio-tungstenite", + "tokio-util", + "tonic 0.12.3", + "tracing", + "tracing-subscriber", + "tracing-test", + "url", + "ustr", +] + [[package]] name = "nautilus-execution" version = "0.52.0" @@ -5292,7 +5609,7 @@ version = "0.52.0" dependencies = [ "ahash 0.8.12", "anyhow", - "axum", + "axum 0.8.6", "bytes", "criterion", "dashmap", @@ -5335,7 +5652,7 @@ dependencies = [ "async-stream", "async-trait", "aws-lc-rs", - "axum", + "axum 0.8.6", "base64 0.22.1", "chrono", "criterion", @@ -5563,7 +5880,7 @@ version = "0.52.0" dependencies = [ "anyhow", "aws-lc-rs", - "axum", + "axum 0.8.6", "hex", "nautilus-common", "nautilus-core", @@ -5909,6 +6226,12 @@ version = "11.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + [[package]] name = "openssl-probe" version = "0.1.6" @@ -6105,6 +6428,16 @@ dependencies = [ "ucd-trie", ] +[[package]] +name = "petgraph" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" +dependencies = [ + "fixedbitset", + "indexmap 2.12.0", +] + [[package]] name = "petgraph" version = "0.8.3" @@ -6516,6 +6849,58 @@ dependencies = [ "unarray", ] +[[package]] +name = "prost" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-build" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf" +dependencies = [ + "heck", + "itertools 0.14.0", + "log", + "multimap", + "once_cell", + "petgraph 0.7.1", + "prettyplease", + "prost", + "prost-types", + "regex", + "syn 2.0.108", + "tempfile", +] + +[[package]] +name = "prost-derive" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" +dependencies = [ + "anyhow", + "itertools 0.14.0", + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "prost-types" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16" +dependencies = [ + "prost", +] + [[package]] name = "psm" version = "0.1.28" @@ -6703,7 +7088,7 @@ dependencies = [ "quinn-udp", "rustc-hash 2.1.1", "rustls", - "socket2", + "socket2 0.6.1", "thiserror 2.0.17", "tokio", "tracing", @@ -6740,7 +7125,7 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2", + "socket2 0.6.1", "tracing", "windows-sys 0.60.2", ] @@ -6904,7 +7289,7 @@ dependencies = [ "rustls-native-certs", "ryu", "sha1_smol", - "socket2", + "socket2 0.6.1", "tokio", "tokio-rustls", "tokio-util", @@ -7018,7 +7403,7 @@ dependencies = [ "tokio", "tokio-rustls", "tokio-util", - "tower", + "tower 0.5.2", "tower-http", "tower-service", "url", @@ -7515,6 +7900,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "secp256k1" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25996b82292a7a57ed3508f052cfff8640d38d32018784acd714758b43da9c8f" +dependencies = [ + "secp256k1-sys 0.8.2", +] + [[package]] name = "secp256k1" version = "0.30.0" @@ -7523,10 +7917,19 @@ checksum = "b50c5943d326858130af85e049f2661ba3c78b26589b8ab98e65e80ae44a1252" dependencies = [ "bitcoin_hashes", "rand 0.8.5", - "secp256k1-sys", + "secp256k1-sys 0.10.1", "serde", ] +[[package]] +name = "secp256k1-sys" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4473013577ec77b4ee3668179ef1186df3146e2cf2d927bd200974c6fe60fd99" +dependencies = [ + "cc", +] + [[package]] name = "secp256k1-sys" version = "0.10.1" @@ -7599,6 +8002,16 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde_bytes" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" +dependencies = [ + "serde", + "serde_core", +] + [[package]] name = "serde_core" version = "1.0.228" @@ -7706,6 +8119,19 @@ dependencies = [ "syn 2.0.108", ] +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap 2.12.0", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + [[package]] name = "serdect" version = "0.2.0" @@ -7733,6 +8159,19 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" +[[package]] +name = "sha2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if", + "cpufeatures", + "digest 0.9.0", + "opaque-debug", +] + [[package]] name = "sha2" version = "0.10.9" @@ -7860,6 +8299,16 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b6b67fb9a61334225b5b790716f609cd58395f895b3fe8b328786812a40bc3b" +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "socket2" version = "0.6.1" @@ -7948,7 +8397,7 @@ dependencies = [ "percent-encoding", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "smallvec", "thiserror 2.0.17", "tokio", @@ -7985,7 +8434,7 @@ dependencies = [ "quote", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "sqlx-core", "sqlx-mysql", "sqlx-postgres", @@ -8028,7 +8477,7 @@ dependencies = [ "rsa", "serde", "sha1", - "sha2", + "sha2 0.10.9", "smallvec", "sqlx-core", "stringprep", @@ -8065,7 +8514,7 @@ dependencies = [ "rand 0.8.5", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "smallvec", "sqlx-core", "stringprep", @@ -8207,6 +8656,21 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "subtle-encoding" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dcb1ed7b8330c5eed5441052651dd7a12c75e2ed88f2ec024ae1fa3a5e59945" +dependencies = [ + "zeroize", +] + +[[package]] +name = "subtle-ng" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "734676eb262c623cec13c3155096e08d1f8f29adce39ba17948b18dad1e54142" + [[package]] name = "syn" version = "1.0.109" @@ -8330,6 +8794,51 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "tendermint" +version = "0.40.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc997743ecfd4864bbca8170d68d9b2bee24653b034210752c2d883ef4b838b1" +dependencies = [ + "bytes", + "digest 0.10.7", + "ed25519", + "ed25519-consensus", + "flex-error", + "futures", + "k256", + "num-traits", + "once_cell", + "prost", + "ripemd", + "serde", + "serde_bytes", + "serde_json", + "serde_repr", + "sha2 0.10.9", + "signature", + "subtle", + "subtle-encoding", + "tendermint-proto", + "time", + "zeroize", +] + +[[package]] +name = "tendermint-proto" +version = "0.40.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2c40e13d39ca19082d8a7ed22de7595979350319833698f8b1080f29620a094" +dependencies = [ + "bytes", + "flex-error", + "prost", + "serde", + "serde_bytes", + "subtle-encoding", + "time", +] + [[package]] name = "testing_table" version = "0.3.0" @@ -8503,7 +9012,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2", + "socket2 0.6.1", "tokio-macros", "windows-sys 0.61.2", ] @@ -8550,6 +9059,7 @@ dependencies = [ "futures-util", "log", "rustls", + "rustls-native-certs", "rustls-pki-types", "tokio", "tokio-rustls", @@ -8622,6 +9132,111 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2" +[[package]] +name = "tonic" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52" +dependencies = [ + "async-stream", + "async-trait", + "axum 0.7.9", + "base64 0.22.1", + "bytes", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-timeout", + "hyper-util", + "percent-encoding", + "pin-project", + "prost", + "socket2 0.5.10", + "tokio", + "tokio-stream", + "tower 0.4.13", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tonic" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e581ba15a835f4d9ea06c55ab1bd4dce26fc53752c69a04aac00703bfb49ba9" +dependencies = [ + "async-trait", + "base64 0.22.1", + "bytes", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-timeout", + "hyper-util", + "percent-encoding", + "pin-project", + "prost", + "socket2 0.5.10", + "tokio", + "tokio-stream", + "tower 0.5.2", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tonic-buf-build" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39018a5b437322acf53301ed58ce4768c8f263e2547fb73e47b81c805fa59395" +dependencies = [ + "scopeguard", + "serde", + "serde_yaml", + "tonic-build", + "uuid", +] + +[[package]] +name = "tonic-build" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac6f67be712d12f0b41328db3137e0d0757645d8904b4cb7d51cd9c2279e847" +dependencies = [ + "prettyplease", + "proc-macro2", + "prost-build", + "prost-types", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "indexmap 1.9.3", + "pin-project", + "pin-project-lite", + "rand 0.8.5", + "slab", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "tower" version = "0.5.2" @@ -8630,11 +9245,15 @@ checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" dependencies = [ "futures-core", "futures-util", + "indexmap 2.12.0", "pin-project-lite", + "slab", "sync_wrapper", "tokio", + "tokio-util", "tower-layer", "tower-service", + "tracing", ] [[package]] @@ -8650,7 +9269,7 @@ dependencies = [ "http-body", "iri-string", "pin-project-lite", - "tower", + "tower 0.5.2", "tower-layer", "tower-service", ] @@ -8980,6 +9599,12 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + [[package]] name = "untrusted" version = "0.9.0" @@ -9043,6 +9668,7 @@ checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" dependencies = [ "getrandom 0.3.4", "js-sys", + "rand 0.9.2", "serde", "wasm-bindgen", ] diff --git a/Cargo.toml b/Cargo.toml index 57a7caffa9c4..d7a121e22e27 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,8 @@ members = [ "crates/adapters/bybit", "crates/adapters/coinbase_intx", "crates/adapters/databento", + "crates/adapters/dydx", + "crates/adapters/dydx_private", "crates/adapters/hyperliquid", "crates/adapters/okx", "crates/adapters/tardis", @@ -141,6 +143,7 @@ derive_builder = { version = "0.20.2", default-features = false, features = [ "alloc", ] } dotenvy = "0.15.7" +dydx-proto = "0.4.0" ed25519-dalek = "2.2.0" enum_dispatch = "0.3.13" evalexpr = "=11.3.1" # Pinned to v11.3.1 for MIT licensing diff --git a/crates/adapters/dydx/Cargo.toml b/crates/adapters/dydx/Cargo.toml new file mode 100644 index 000000000000..6532e04cb248 --- /dev/null +++ b/crates/adapters/dydx/Cargo.toml @@ -0,0 +1,95 @@ +[package] +name = "nautilus-dydx" +readme = "README.md" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +authors.workspace = true +license.workspace = true +description = "dYdX v4 exchange integration adapter for the Nautilus trading engine" +categories.workspace = true +keywords.workspace = true +documentation.workspace = true +repository.workspace = true +homepage.workspace = true + +[lints] +workspace = true + +[lib] +name = "nautilus_dydx_v4" +crate-type = ["rlib", "cdylib"] + +[features] +default = [] +extension-module = [ + "nautilus-common/extension-module", + "nautilus-core/extension-module", + "nautilus-data/extension-module", + "nautilus-model/extension-module", + "nautilus-network/extension-module", + "python", + "pyo3/extension-module", +] +python = [ + "nautilus-common/python", + "nautilus-core/python", + "nautilus-data/python", + "nautilus-model/python", + "nautilus-network/python", + "pyo3", + "pyo3-async-runtimes", +] + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + +[dependencies] +nautilus-common = { workspace = true } +nautilus-core = { workspace = true } +nautilus-data = { workspace = true } +nautilus-execution = { workspace = true } +nautilus-live = { workspace = true } +nautilus-model = { workspace = true } +nautilus-network = { workspace = true } + +ahash = { workspace = true } +anyhow = { workspace = true } +async-stream = { workspace = true } +async-trait = { workspace = true } +aws-lc-rs = { workspace = true } +base64 = { workspace = true } +chrono = { workspace = true } +dashmap = { workspace = true } +derive_builder = { workspace = true } +dydx-proto = { workspace = true } +futures-util = { workspace = true } +indexmap = { workspace = true } +log = { workspace = true } +reqwest = { workspace = true } +rust_decimal = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +serde_urlencoded = { workspace = true } +strum = { workspace = true } +thiserror = { workspace = true } +tokio = { workspace = true } +tokio-tungstenite = { workspace = true } +tokio-util = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } # Needed for example binaries +ustr = { workspace = true } +zeroize = { workspace = true } + +pyo3 = { workspace = true, optional = true } +pyo3-async-runtimes = { workspace = true, optional = true } + +[dev-dependencies] +nautilus-testkit = { workspace = true } +criterion = { workspace = true } +rstest = { workspace = true } +rust_decimal_macros = { workspace = true } +tracing-test = { workspace = true } +url = { workspace = true } +axum = { workspace = true } diff --git a/crates/adapters/dydx/LICENSE b/crates/adapters/dydx/LICENSE new file mode 100644 index 000000000000..5550e2db15f2 --- /dev/null +++ b/crates/adapters/dydx/LICENSE @@ -0,0 +1,164 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff --git a/crates/adapters/dydx/src/common/consts.rs b/crates/adapters/dydx/src/common/consts.rs new file mode 100644 index 000000000000..498c67f67b9d --- /dev/null +++ b/crates/adapters/dydx/src/common/consts.rs @@ -0,0 +1,22 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved. +// https://nautechsystems.io +// +// Licensed under the GNU Lesser General Public License Version 3.0 (the "License"); +// You may not use this file except in compliance with the License. +// You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------------------------------- + +//! Core constants shared across the dYdX adapter components. + +/// dYdX mainnet chain ID. +pub const DYDX_CHAIN_ID: &str = "dydx-mainnet-1"; + +/// dYdX testnet chain ID. +pub const DYDX_TESTNET_CHAIN_ID: &str = "dydx-testnet-4"; diff --git a/crates/adapters/dydx/src/common/mod.rs b/crates/adapters/dydx/src/common/mod.rs new file mode 100644 index 000000000000..5b447c923283 --- /dev/null +++ b/crates/adapters/dydx/src/common/mod.rs @@ -0,0 +1,31 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved. +// https://nautechsystems.io +// +// Licensed under the GNU Lesser General Public License Version 3.0 (the "License"); +// You may not use this file except in compliance with the License. +// You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------------------------------- + +//! Common functionality shared across the dYdX adapter. +//! +//! This module provides core utilities, constants, and data structures used throughout +//! the dYdX integration, including: +//! +//! - Common constants for venues and identifiers. +//! - URL management for HTTP and WebSocket endpoints. +//! - Shared data types and models. +//! - Test utilities for development. + +pub mod consts; +pub mod types; +pub mod urls; + +#[cfg(test)] +pub(crate) mod testing; diff --git a/crates/adapters/dydx/src/common/testing.rs b/crates/adapters/dydx/src/common/testing.rs new file mode 100644 index 000000000000..8faf820e5912 --- /dev/null +++ b/crates/adapters/dydx/src/common/testing.rs @@ -0,0 +1,16 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved. +// https://nautechsystems.io +// +// Licensed under the GNU Lesser General Public License Version 3.0 (the "License"); +// You may not use this file except in compliance with the License. +// You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------------------------------- + +//! Test utilities for the dYdX adapter. diff --git a/crates/adapters/dydx/src/common/types.rs b/crates/adapters/dydx/src/common/types.rs new file mode 100644 index 000000000000..d1b76f75cb4b --- /dev/null +++ b/crates/adapters/dydx/src/common/types.rs @@ -0,0 +1,32 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved. +// https://nautechsystems.io +// +// Licensed under the GNU Lesser General Public License Version 3.0 (the "License"); +// You may not use this file except in compliance with the License. +// You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------------------------------- + +//! Common types and models for the dYdX adapter. + +use serde::{Deserialize, Serialize}; + +/// dYdX account information. +/// +/// Represents a Cosmos SDK account with its address, account number, +/// and current sequence (nonce) for transaction ordering. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DydxAccount { + /// Cosmos SDK address (dydx...). + pub address: String, + /// Account number from the blockchain. + pub account_number: u64, + /// Current sequence number (nonce) for transactions. + pub sequence: u64, +} diff --git a/crates/adapters/dydx/src/common/urls.rs b/crates/adapters/dydx/src/common/urls.rs new file mode 100644 index 000000000000..c3b6c6f31412 --- /dev/null +++ b/crates/adapters/dydx/src/common/urls.rs @@ -0,0 +1,37 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved. +// https://nautechsystems.io +// +// Licensed under the GNU Lesser General Public License Version 3.0 (the "License"); +// You may not use this file except in compliance with the License. +// You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------------------------------- + +//! URL constants for dYdX API endpoints. + +// HTTP API URLs +/// dYdX v4 mainnet HTTP API base URL. +pub const DYDX_HTTP_URL: &str = "https://indexer.dydx.trade"; + +/// dYdX v4 testnet HTTP API base URL. +pub const DYDX_TESTNET_HTTP_URL: &str = "https://indexer.v4testnet.dydx.exchange"; + +// WebSocket URLs +/// dYdX v4 mainnet WebSocket URL. +pub const DYDX_WS_URL: &str = "wss://indexer.dydx.trade/v4/ws"; + +/// dYdX v4 testnet WebSocket URL. +pub const DYDX_TESTNET_WS_URL: &str = "wss://indexer.v4testnet.dydx.exchange/v4/ws"; + +// gRPC URLs +/// dYdX v4 mainnet gRPC URL (public node). +pub const DYDX_GRPC_URL: &str = "https://dydx-grpc.publicnode.com:443"; + +/// dYdX v4 testnet gRPC URL. +pub const DYDX_TESTNET_GRPC_URL: &str = "https://dydx-testnet-grpc.publicnode.com:443"; diff --git a/crates/adapters/dydx/src/config.rs b/crates/adapters/dydx/src/config.rs new file mode 100644 index 000000000000..f9f420ee2944 --- /dev/null +++ b/crates/adapters/dydx/src/config.rs @@ -0,0 +1,45 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved. +// https://nautechsystems.io +// +// Licensed under the GNU Lesser General Public License Version 3.0 (the "License"); +// You may not use this file except in compliance with the License. +// You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------------------------------- + +//! Configuration structures for the dYdX adapter. + +use serde::{Deserialize, Serialize}; + +/// Configuration for the dYdX adapter. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DydxAdapterConfig { + /// Base URL for the HTTP API. + pub base_url: String, + /// Base URL for the WebSocket API. + pub ws_url: String, + /// Base URL for the gRPC API (Cosmos SDK transactions). + pub grpc_url: String, + /// Chain ID (e.g., "dydx-mainnet-1" for mainnet, "dydx-testnet-4" for testnet). + pub chain_id: String, + /// Request timeout in seconds. + pub timeout_secs: u64, +} + +impl Default for DydxAdapterConfig { + fn default() -> Self { + Self { + base_url: "https://api.dydx.exchange".to_string(), + ws_url: "wss://api.dydx.exchange/v4/ws".to_string(), + grpc_url: "https://dydx-grpc.publicnode.com:443".to_string(), + chain_id: "dydx-mainnet-1".to_string(), + timeout_secs: 30, + } + } +} diff --git a/crates/adapters/dydx/src/error.rs b/crates/adapters/dydx/src/error.rs new file mode 100644 index 000000000000..ee23fa9a4281 --- /dev/null +++ b/crates/adapters/dydx/src/error.rs @@ -0,0 +1,56 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved. +// https://nautechsystems.io +// +// Licensed under the GNU Lesser General Public License Version 3.0 (the "License"); +// You may not use this file except in compliance with the License. +// You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------------------------------- + +//! Error handling for the dYdX adapter. +//! +//! This module provides error types for all dYdX operations, including +//! HTTP and WebSocket errors. + +use thiserror::Error; + +/// Result type for dYdX operations. +pub type DydxResult = Result; + +/// The main error type for all dYdX adapter operations. +#[derive(Debug, Error)] +pub enum DydxError { + /// HTTP client errors. + #[error("HTTP error: {0}")] + Http(String), + + /// WebSocket connection errors. + #[error("WebSocket error: {0}")] + WebSocket(String), + + /// JSON serialization/deserialization errors. + #[error("JSON error: {message}")] + Json { + message: String, + /// The raw JSON that failed to parse, if available. + raw: Option, + }, + + /// Configuration errors. + #[error("Configuration error: {0}")] + Config(String), + + /// Invalid data errors. + #[error("Invalid data: {0}")] + InvalidData(String), + + /// Nautilus core errors. + #[error("Nautilus error: {0}")] + Nautilus(#[from] anyhow::Error), +} diff --git a/crates/adapters/dydx/src/http/mod.rs b/crates/adapters/dydx/src/http/mod.rs new file mode 100644 index 000000000000..1300f8ad2ab8 --- /dev/null +++ b/crates/adapters/dydx/src/http/mod.rs @@ -0,0 +1,29 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved. +// https://nautechsystems.io +// +// Licensed under the GNU Lesser General Public License Version 3.0 (the "License"); +// You may not use this file except in compliance with the License. +// You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------------------------------- + +//! HTTP client bindings for the dYdX adapter. +//! +//! This module provides an HTTP client for interacting with the dYdX v4 Indexer REST API. +//! It handles: +//! - Request signing and authentication. +//! - Rate limiting and retry logic. +//! - Request/response models. +//! - Parsing dYdX data into Nautilus domain models. +//! +//! The client supports dYdX REST endpoints including: +//! - Market data (instruments, trades, candles). +//! - Account data (subaccounts, positions, fills). +//! - Order management queries. +//! - Historical data. diff --git a/crates/adapters/dydx/src/lib.rs b/crates/adapters/dydx/src/lib.rs new file mode 100644 index 000000000000..6bf7cb58c342 --- /dev/null +++ b/crates/adapters/dydx/src/lib.rs @@ -0,0 +1,60 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved. +// https://nautechsystems.io +// +// Licensed under the GNU Lesser General Public License Version 3.0 (the "License"); +// You may not use this file except in compliance with the License. +// You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------------------------------- + +//! [NautilusTrader](http://nautilustrader.io) adapter for the [dYdX](https://dydx.trade/) decentralized derivatives exchange. +//! +//! The `nautilus-dydx` crate provides client bindings (HTTP & WebSocket), data +//! models, and helper utilities that wrap the official **dYdX v4 API**. +//! +//! The official dYdX v4 documentation can be found at . +//! All public links inside this crate reference the official documentation. +//! +//! # Platform +//! +//! [NautilusTrader](http://nautilustrader.io) is an open-source, high-performance, production-grade +//! algorithmic trading platform, providing quantitative traders with the ability to backtest +//! portfolios of automated trading strategies on historical data with an event-driven engine, +//! and also deploy those same strategies live, with no code changes. +//! +//! NautilusTrader's design, architecture, and implementation philosophy prioritizes software +//! correctness and safety at the highest level, with the aim of supporting mission-critical trading +//! system backtesting and live deployment workloads. +//! +//! # Feature flags +//! +//! This crate provides feature flags to control source code inclusion during compilation, +//! depending on the intended use case, i.e. whether to provide Python bindings +//! for the [nautilus_trader](https://pypi.org/project/nautilus_trader) Python package, +//! or as part of a Rust only build. +//! +//! - `python`: Enables Python bindings from [PyO3](https://pyo3.rs). +//! - `extension-module`: Builds as a Python extension module (used with `python`). + +#![warn(rustc::all)] +#![deny(unsafe_code)] +#![deny(nonstandard_style)] +#![deny(missing_debug_implementations)] +#![deny(clippy::missing_errors_doc)] +#![deny(clippy::missing_panics_doc)] +#![deny(rustdoc::broken_intra_doc_links)] + +pub mod common; +pub mod config; +pub mod error; +pub mod http; +pub mod proto; + +#[cfg(feature = "python")] +pub mod python; diff --git a/crates/adapters/dydx/src/proto/mod.rs b/crates/adapters/dydx/src/proto/mod.rs new file mode 100644 index 000000000000..22c1c9ea8b27 --- /dev/null +++ b/crates/adapters/dydx/src/proto/mod.rs @@ -0,0 +1,22 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved. +// https://nautechsystems.io +// +// Licensed under the GNU Lesser General Public License Version 3.0 (the "License"); +// You may not use this file except in compliance with the License. +// You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------------------------------- + +//! Re-exports of dYdX Protocol Buffer definitions. +//! +//! This module provides convenient re-exports of types from the `dydx-proto` crate, +//! organized by their proto package structure. + +/// Re-export the entire dydx-proto crate for full access to all types. +pub use dydx_proto::*; diff --git a/crates/adapters/dydx/src/python/mod.rs b/crates/adapters/dydx/src/python/mod.rs new file mode 100644 index 000000000000..65420f692ab1 --- /dev/null +++ b/crates/adapters/dydx/src/python/mod.rs @@ -0,0 +1,29 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved. +// https://nautechsystems.io +// +// Licensed under the GNU Lesser General Public License Version 3.0 (the "License"); +// You may not use this file except in compliance with the License. +// You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------------------------------- + +//! Python bindings from `pyo3`. + +use pyo3::prelude::*; + +/// Loaded as `nautilus_pyo3.dydx`. +/// +/// # Errors +/// +/// Returns an error if any bindings fail to register with the Python module. +#[pymodule] +pub fn dydx(_: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + m.add("__package__", "nautilus_trader.core.nautilus_pyo3.dydx")?; + Ok(()) +} From adf4f2c655cec1477438ec33ee58ebe793c401d3 Mon Sep 17 00:00:00 2001 From: Vadim Nicolai Date: Sat, 1 Nov 2025 20:45:37 +0200 Subject: [PATCH 02/10] Small improvement. --- Cargo.lock | 403 ++------------------- Cargo.toml | 1 - crates/adapters/dydx/src/common/testing.rs | 2 - 3 files changed, 33 insertions(+), 373 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7ac606622e2f..4273358dbc0b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -110,7 +110,7 @@ dependencies = [ "k256", "once_cell", "rand 0.8.5", - "secp256k1 0.30.0", + "secp256k1", "serde", "serde_json", "serde_with", @@ -235,7 +235,7 @@ dependencies = [ "either", "serde", "serde_with", - "sha2 0.10.9", + "sha2", "thiserror 2.0.17", ] @@ -405,7 +405,7 @@ dependencies = [ "serde_json", "tokio", "tokio-stream", - "tower 0.5.2", + "tower", "tracing", "wasmtimer", ] @@ -577,7 +577,7 @@ dependencies = [ "serde_json", "thiserror 2.0.17", "tokio", - "tower 0.5.2", + "tower", "tracing", "url", "wasmtimer", @@ -1239,40 +1239,13 @@ dependencies = [ "fs_extra", ] -[[package]] -name = "axum" -version = "0.7.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" -dependencies = [ - "async-trait", - "axum-core 0.4.5", - "bytes", - "futures-util", - "http", - "http-body", - "http-body-util", - "itoa", - "matchit 0.7.3", - "memchr", - "mime", - "percent-encoding", - "pin-project-lite", - "rustversion", - "serde", - "sync_wrapper", - "tower 0.5.2", - "tower-layer", - "tower-service", -] - [[package]] name = "axum" version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a18ed336352031311f4e0b4dd2ff392d4fbb370777c9d18d7fc9d7359f73871" dependencies = [ - "axum-core 0.5.5", + "axum-core", "base64 0.22.1", "bytes", "form_urlencoded", @@ -1283,7 +1256,7 @@ dependencies = [ "hyper", "hyper-util", "itoa", - "matchit 0.8.4", + "matchit", "memchr", "mime", "percent-encoding", @@ -1296,27 +1269,7 @@ dependencies = [ "sync_wrapper", "tokio", "tokio-tungstenite", - "tower 0.5.2", - "tower-layer", - "tower-service", -] - -[[package]] -name = "axum-core" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" -dependencies = [ - "async-trait", - "bytes", - "futures-util", - "http", - "http-body", - "http-body-util", - "mime", - "pin-project-lite", - "rustversion", - "sync_wrapper", + "tower", "tower-layer", "tower-service", ] @@ -1378,12 +1331,6 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445" -[[package]] -name = "bech32" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d965446196e3b7decd44aa7ee49e31d630118f90ef12f97900f262eb915c951d" - [[package]] name = "bigdecimal" version = "0.4.9" @@ -1435,25 +1382,6 @@ dependencies = [ "syn 2.0.108", ] -[[package]] -name = "bip32" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db40d3dfbeab4e031d78c844642fa0caa0b0db11ce1607ac9d2986dff1405c69" -dependencies = [ - "bs58", - "hmac", - "k256", - "once_cell", - "pbkdf2", - "rand_core 0.6.4", - "ripemd", - "secp256k1 0.27.0", - "sha2 0.10.9", - "subtle", - "zeroize", -] - [[package]] name = "bit-set" version = "0.8.0" @@ -1506,15 +1434,6 @@ dependencies = [ "wyz", ] -[[package]] -name = "block-buffer" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" -dependencies = [ - "generic-array", -] - [[package]] name = "block-buffer" version = "0.10.4" @@ -1632,7 +1551,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" dependencies = [ - "sha2 0.10.9", + "sha2", "tinyvec", ] @@ -1925,7 +1844,7 @@ dependencies = [ "hmac", "k256", "serde", - "sha2 0.10.9", + "sha2", "thiserror 1.0.69", ] @@ -1941,7 +1860,7 @@ dependencies = [ "once_cell", "pbkdf2", "rand 0.8.5", - "sha2 0.10.9", + "sha2", "thiserror 1.0.69", ] @@ -1952,14 +1871,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62b962ad8545e43a28e14e87377812ba9ae748dd4fd963f4c10e9fcc6d13475b" dependencies = [ "base64 0.21.7", - "bech32 0.9.1", + "bech32", "bs58", "const-hex", "digest 0.10.7", "generic-array", "ripemd", "serde", - "sha2 0.10.9", + "sha2", "sha3", "thiserror 1.0.69", ] @@ -2119,16 +2038,6 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" -[[package]] -name = "cosmos-sdk-proto" -version = "0.26.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "462e1f6a8e005acc8835d32d60cbd7973ed65ea2a8d8473830e675f050956427" -dependencies = [ - "prost", - "tendermint-proto", -] - [[package]] name = "cosmos-sdk-proto" version = "0.27.0" @@ -2137,27 +2046,7 @@ checksum = "95ac39be7373404accccaede7cc1ec942ccef14f0ca18d209967a756bf1dbb1f" dependencies = [ "prost", "tendermint-proto", - "tonic 0.13.1", -] - -[[package]] -name = "cosmrs" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1394c263335da09e8ba8c4b2c675d804e3e0deb44cce0866a5f838d3ddd43d02" -dependencies = [ - "bip32", - "cosmos-sdk-proto 0.26.1", - "ecdsa", - "eyre", - "k256", - "rand_core 0.6.4", - "serde", - "serde_json", - "signature", - "subtle-encoding", - "tendermint", - "thiserror 1.0.69", + "tonic", ] [[package]] @@ -2336,19 +2225,6 @@ dependencies = [ "syn 2.0.108", ] -[[package]] -name = "curve25519-dalek-ng" -version = "4.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c359b7249347e46fb28804470d071c921156ad62b3eef5d34e2ba867533dec8" -dependencies = [ - "byteorder", - "digest 0.9.0", - "rand_core 0.6.4", - "subtle-ng", - "zeroize", -] - [[package]] name = "darling" version = "0.20.11" @@ -2453,7 +2329,7 @@ dependencies = [ "reqwest", "serde", "serde_json", - "sha2 0.10.9", + "sha2", "thiserror 2.0.17", "time", "tokio", @@ -3224,7 +3100,7 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer 0.10.4", + "block-buffer", "const-oid", "crypto-common", "subtle", @@ -3259,12 +3135,12 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "945e74a43dd3e251849f5a87169aaa457d70d6506a27adf365a7d418c71c42d1" dependencies = [ - "cosmos-sdk-proto 0.27.0", + "cosmos-sdk-proto", "prost", "prost-build", "prost-types", "regex", - "tonic 0.13.1", + "tonic", "tonic-buf-build", "tonic-build", ] @@ -3300,19 +3176,6 @@ dependencies = [ "signature", ] -[[package]] -name = "ed25519-consensus" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c8465edc8ee7436ffea81d21a019b16676ee3db267aa8d5a8d729581ecf998b" -dependencies = [ - "curve25519-dalek-ng", - "hex", - "rand_core 0.6.4", - "sha2 0.9.9", - "zeroize", -] - [[package]] name = "ed25519-dalek" version = "2.2.0" @@ -3322,7 +3185,7 @@ dependencies = [ "curve25519-dalek", "ed25519", "serde", - "sha2 0.10.9", + "sha2", "subtle", "zeroize", ] @@ -3456,16 +3319,6 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "eyre" -version = "0.6.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" -dependencies = [ - "indenter", - "once_cell", -] - [[package]] name = "fallible-streaming-iterator" version = "0.1.9" @@ -4337,12 +4190,6 @@ dependencies = [ "bon", ] -[[package]] -name = "indenter" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" - [[package]] name = "indexmap" version = "1.9.3" @@ -4507,7 +4354,7 @@ dependencies = [ "elliptic-curve", "once_cell", "serdect", - "sha2 0.10.9", + "sha2", "signature", ] @@ -4812,12 +4659,6 @@ dependencies = [ "regex-automata", ] -[[package]] -name = "matchit" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" - [[package]] name = "matchit" version = "0.8.4" @@ -4975,7 +4816,7 @@ dependencies = [ "async-stream", "async-trait", "aws-lc-rs", - "axum 0.8.6", + "axum", "chrono", "criterion", "dashmap", @@ -5070,7 +4911,7 @@ dependencies = [ "async-stream", "async-trait", "aws-lc-rs", - "axum 0.8.6", + "axum", "chrono", "criterion", "dashmap", @@ -5318,7 +5159,7 @@ dependencies = [ "async-stream", "async-trait", "aws-lc-rs", - "axum 0.8.6", + "axum", "base64 0.22.1", "chrono", "criterion", @@ -5358,60 +5199,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "nautilus-dydx-private" -version = "0.52.0" -dependencies = [ - "anyhow", - "async-trait", - "axum 0.8.6", - "base64 0.22.1", - "bech32 0.11.0", - "bip32", - "chrono", - "cosmrs", - "criterion", - "derive_builder", - "dydx-proto", - "futures-util", - "hex", - "http-body-util", - "nautilus-common", - "nautilus-core", - "nautilus-data", - "nautilus-execution", - "nautilus-live", - "nautilus-model", - "nautilus-network", - "nautilus-serialization", - "nautilus-testkit", - "pbkdf2", - "prost", - "prost-types", - "pyo3", - "pyo3-async-runtimes", - "reqwest", - "rstest", - "rust_decimal", - "rustls", - "serde", - "serde_json", - "serde_urlencoded", - "sha2 0.10.9", - "strum 0.27.2", - "thiserror 2.0.17", - "time", - "tokio", - "tokio-tungstenite", - "tokio-util", - "tonic 0.12.3", - "tracing", - "tracing-subscriber", - "tracing-test", - "url", - "ustr", -] - [[package]] name = "nautilus-execution" version = "0.52.0" @@ -5609,7 +5396,7 @@ version = "0.52.0" dependencies = [ "ahash 0.8.12", "anyhow", - "axum 0.8.6", + "axum", "bytes", "criterion", "dashmap", @@ -5652,7 +5439,7 @@ dependencies = [ "async-stream", "async-trait", "aws-lc-rs", - "axum 0.8.6", + "axum", "base64 0.22.1", "chrono", "criterion", @@ -5880,7 +5667,7 @@ version = "0.52.0" dependencies = [ "anyhow", "aws-lc-rs", - "axum 0.8.6", + "axum", "hex", "nautilus-common", "nautilus-core", @@ -6226,12 +6013,6 @@ version = "11.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" -[[package]] -name = "opaque-debug" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" - [[package]] name = "openssl-probe" version = "0.1.6" @@ -7403,7 +7184,7 @@ dependencies = [ "tokio", "tokio-rustls", "tokio-util", - "tower 0.5.2", + "tower", "tower-http", "tower-service", "url", @@ -7900,15 +7681,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "secp256k1" -version = "0.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25996b82292a7a57ed3508f052cfff8640d38d32018784acd714758b43da9c8f" -dependencies = [ - "secp256k1-sys 0.8.2", -] - [[package]] name = "secp256k1" version = "0.30.0" @@ -7917,19 +7689,10 @@ checksum = "b50c5943d326858130af85e049f2661ba3c78b26589b8ab98e65e80ae44a1252" dependencies = [ "bitcoin_hashes", "rand 0.8.5", - "secp256k1-sys 0.10.1", + "secp256k1-sys", "serde", ] -[[package]] -name = "secp256k1-sys" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4473013577ec77b4ee3668179ef1186df3146e2cf2d927bd200974c6fe60fd99" -dependencies = [ - "cc", -] - [[package]] name = "secp256k1-sys" version = "0.10.1" @@ -8159,19 +7922,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" -[[package]] -name = "sha2" -version = "0.9.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" -dependencies = [ - "block-buffer 0.9.0", - "cfg-if", - "cpufeatures", - "digest 0.9.0", - "opaque-debug", -] - [[package]] name = "sha2" version = "0.10.9" @@ -8397,7 +8147,7 @@ dependencies = [ "percent-encoding", "serde", "serde_json", - "sha2 0.10.9", + "sha2", "smallvec", "thiserror 2.0.17", "tokio", @@ -8434,7 +8184,7 @@ dependencies = [ "quote", "serde", "serde_json", - "sha2 0.10.9", + "sha2", "sqlx-core", "sqlx-mysql", "sqlx-postgres", @@ -8477,7 +8227,7 @@ dependencies = [ "rsa", "serde", "sha1", - "sha2 0.10.9", + "sha2", "smallvec", "sqlx-core", "stringprep", @@ -8514,7 +8264,7 @@ dependencies = [ "rand 0.8.5", "serde", "serde_json", - "sha2 0.10.9", + "sha2", "smallvec", "sqlx-core", "stringprep", @@ -8665,12 +8415,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "subtle-ng" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "734676eb262c623cec13c3155096e08d1f8f29adce39ba17948b18dad1e54142" - [[package]] name = "syn" version = "1.0.109" @@ -8794,36 +8538,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "tendermint" -version = "0.40.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc997743ecfd4864bbca8170d68d9b2bee24653b034210752c2d883ef4b838b1" -dependencies = [ - "bytes", - "digest 0.10.7", - "ed25519", - "ed25519-consensus", - "flex-error", - "futures", - "k256", - "num-traits", - "once_cell", - "prost", - "ripemd", - "serde", - "serde_bytes", - "serde_json", - "serde_repr", - "sha2 0.10.9", - "signature", - "subtle", - "subtle-encoding", - "tendermint-proto", - "time", - "zeroize", -] - [[package]] name = "tendermint-proto" version = "0.40.4" @@ -9059,7 +8773,6 @@ dependencies = [ "futures-util", "log", "rustls", - "rustls-native-certs", "rustls-pki-types", "tokio", "tokio-rustls", @@ -9132,36 +8845,6 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2" -[[package]] -name = "tonic" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52" -dependencies = [ - "async-stream", - "async-trait", - "axum 0.7.9", - "base64 0.22.1", - "bytes", - "h2", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-timeout", - "hyper-util", - "percent-encoding", - "pin-project", - "prost", - "socket2 0.5.10", - "tokio", - "tokio-stream", - "tower 0.4.13", - "tower-layer", - "tower-service", - "tracing", -] - [[package]] name = "tonic" version = "0.13.1" @@ -9184,7 +8867,7 @@ dependencies = [ "socket2 0.5.10", "tokio", "tokio-stream", - "tower 0.5.2", + "tower", "tower-layer", "tower-service", "tracing", @@ -9217,26 +8900,6 @@ dependencies = [ "syn 2.0.108", ] -[[package]] -name = "tower" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" -dependencies = [ - "futures-core", - "futures-util", - "indexmap 1.9.3", - "pin-project", - "pin-project-lite", - "rand 0.8.5", - "slab", - "tokio", - "tokio-util", - "tower-layer", - "tower-service", - "tracing", -] - [[package]] name = "tower" version = "0.5.2" @@ -9269,7 +8932,7 @@ dependencies = [ "http-body", "iri-string", "pin-project-lite", - "tower 0.5.2", + "tower", "tower-layer", "tower-service", ] diff --git a/Cargo.toml b/Cargo.toml index d7a121e22e27..3b781a00837c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,6 @@ members = [ "crates/adapters/coinbase_intx", "crates/adapters/databento", "crates/adapters/dydx", - "crates/adapters/dydx_private", "crates/adapters/hyperliquid", "crates/adapters/okx", "crates/adapters/tardis", diff --git a/crates/adapters/dydx/src/common/testing.rs b/crates/adapters/dydx/src/common/testing.rs index 8faf820e5912..e4e33f4864b1 100644 --- a/crates/adapters/dydx/src/common/testing.rs +++ b/crates/adapters/dydx/src/common/testing.rs @@ -12,5 +12,3 @@ // See the License for the specific language governing permissions and // limitations under the License. // ------------------------------------------------------------------------------------------------- - -//! Test utilities for the dYdX adapter. From 4d85e80889a11fae69d3afcfdf3caf6673ee9d39 Mon Sep 17 00:00:00 2001 From: Vadim Nicolai Date: Sat, 1 Nov 2025 21:15:29 +0200 Subject: [PATCH 03/10] Small improvement. --- Cargo.lock | 211 ++++++++++- crates/adapters/dydx/Cargo.toml | 11 +- crates/adapters/dydx/src/common/consts.rs | 3 + crates/adapters/dydx/src/common/mod.rs | 10 - crates/adapters/dydx/src/error.rs | 18 +- crates/adapters/dydx/src/grpc/builder.rs | 163 ++++++++ crates/adapters/dydx/src/grpc/client.rs | 352 +++++++++++++++++ crates/adapters/dydx/src/grpc/mod.rs | 53 +++ crates/adapters/dydx/src/grpc/order.rs | 442 ++++++++++++++++++++++ crates/adapters/dydx/src/grpc/types.rs | 44 +++ crates/adapters/dydx/src/grpc/wallet.rs | 192 ++++++++++ crates/adapters/dydx/src/lib.rs | 1 + deny.toml | 1 + 13 files changed, 1473 insertions(+), 28 deletions(-) create mode 100644 crates/adapters/dydx/src/grpc/builder.rs create mode 100644 crates/adapters/dydx/src/grpc/client.rs create mode 100644 crates/adapters/dydx/src/grpc/mod.rs create mode 100644 crates/adapters/dydx/src/grpc/order.rs create mode 100644 crates/adapters/dydx/src/grpc/types.rs create mode 100644 crates/adapters/dydx/src/grpc/wallet.rs diff --git a/Cargo.lock b/Cargo.lock index 4273358dbc0b..012bce2a915f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -110,7 +110,7 @@ dependencies = [ "k256", "once_cell", "rand 0.8.5", - "secp256k1", + "secp256k1 0.30.0", "serde", "serde_json", "serde_with", @@ -235,7 +235,7 @@ dependencies = [ "either", "serde", "serde_with", - "sha2", + "sha2 0.10.9", "thiserror 2.0.17", ] @@ -1382,6 +1382,25 @@ dependencies = [ "syn 2.0.108", ] +[[package]] +name = "bip32" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db40d3dfbeab4e031d78c844642fa0caa0b0db11ce1607ac9d2986dff1405c69" +dependencies = [ + "bs58", + "hmac", + "k256", + "once_cell", + "pbkdf2", + "rand_core 0.6.4", + "ripemd", + "secp256k1 0.27.0", + "sha2 0.10.9", + "subtle", + "zeroize", +] + [[package]] name = "bit-set" version = "0.8.0" @@ -1434,6 +1453,15 @@ dependencies = [ "wyz", ] +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -1551,7 +1579,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" dependencies = [ - "sha2", + "sha2 0.10.9", "tinyvec", ] @@ -1844,7 +1872,7 @@ dependencies = [ "hmac", "k256", "serde", - "sha2", + "sha2 0.10.9", "thiserror 1.0.69", ] @@ -1860,7 +1888,7 @@ dependencies = [ "once_cell", "pbkdf2", "rand 0.8.5", - "sha2", + "sha2 0.10.9", "thiserror 1.0.69", ] @@ -1878,7 +1906,7 @@ dependencies = [ "generic-array", "ripemd", "serde", - "sha2", + "sha2 0.10.9", "sha3", "thiserror 1.0.69", ] @@ -2038,6 +2066,16 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "cosmos-sdk-proto" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "462e1f6a8e005acc8835d32d60cbd7973ed65ea2a8d8473830e675f050956427" +dependencies = [ + "prost", + "tendermint-proto", +] + [[package]] name = "cosmos-sdk-proto" version = "0.27.0" @@ -2049,6 +2087,26 @@ dependencies = [ "tonic", ] +[[package]] +name = "cosmrs" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1394c263335da09e8ba8c4b2c675d804e3e0deb44cce0866a5f838d3ddd43d02" +dependencies = [ + "bip32", + "cosmos-sdk-proto 0.26.1", + "ecdsa", + "eyre", + "k256", + "rand_core 0.6.4", + "serde", + "serde_json", + "signature", + "subtle-encoding", + "tendermint", + "thiserror 1.0.69", +] + [[package]] name = "cpufeatures" version = "0.2.17" @@ -2225,6 +2283,19 @@ dependencies = [ "syn 2.0.108", ] +[[package]] +name = "curve25519-dalek-ng" +version = "4.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c359b7249347e46fb28804470d071c921156ad62b3eef5d34e2ba867533dec8" +dependencies = [ + "byteorder", + "digest 0.9.0", + "rand_core 0.6.4", + "subtle-ng", + "zeroize", +] + [[package]] name = "darling" version = "0.20.11" @@ -2329,7 +2400,7 @@ dependencies = [ "reqwest", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "thiserror 2.0.17", "time", "tokio", @@ -3100,7 +3171,7 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", + "block-buffer 0.10.4", "const-oid", "crypto-common", "subtle", @@ -3135,7 +3206,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "945e74a43dd3e251849f5a87169aaa457d70d6506a27adf365a7d418c71c42d1" dependencies = [ - "cosmos-sdk-proto", + "cosmos-sdk-proto 0.27.0", "prost", "prost-build", "prost-types", @@ -3176,6 +3247,19 @@ dependencies = [ "signature", ] +[[package]] +name = "ed25519-consensus" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c8465edc8ee7436ffea81d21a019b16676ee3db267aa8d5a8d729581ecf998b" +dependencies = [ + "curve25519-dalek-ng", + "hex", + "rand_core 0.6.4", + "sha2 0.9.9", + "zeroize", +] + [[package]] name = "ed25519-dalek" version = "2.2.0" @@ -3185,7 +3269,7 @@ dependencies = [ "curve25519-dalek", "ed25519", "serde", - "sha2", + "sha2 0.10.9", "subtle", "zeroize", ] @@ -3319,6 +3403,16 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "eyre" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" +dependencies = [ + "indenter", + "once_cell", +] + [[package]] name = "fallible-streaming-iterator" version = "0.1.9" @@ -4190,6 +4284,12 @@ dependencies = [ "bon", ] +[[package]] +name = "indenter" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" + [[package]] name = "indexmap" version = "1.9.3" @@ -4354,7 +4454,7 @@ dependencies = [ "elliptic-curve", "once_cell", "serdect", - "sha2", + "sha2 0.10.9", "signature", ] @@ -5161,7 +5261,9 @@ dependencies = [ "aws-lc-rs", "axum", "base64 0.22.1", + "bip32", "chrono", + "cosmrs", "criterion", "dashmap", "derive_builder", @@ -5177,6 +5279,8 @@ dependencies = [ "nautilus-model", "nautilus-network", "nautilus-testkit", + "prost", + "prost-types", "pyo3", "pyo3-async-runtimes", "reqwest", @@ -5191,6 +5295,7 @@ dependencies = [ "tokio", "tokio-tungstenite", "tokio-util", + "tonic", "tracing", "tracing-subscriber", "tracing-test", @@ -6013,6 +6118,12 @@ version = "11.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + [[package]] name = "openssl-probe" version = "0.1.6" @@ -7681,6 +7792,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "secp256k1" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25996b82292a7a57ed3508f052cfff8640d38d32018784acd714758b43da9c8f" +dependencies = [ + "secp256k1-sys 0.8.2", +] + [[package]] name = "secp256k1" version = "0.30.0" @@ -7689,10 +7809,19 @@ checksum = "b50c5943d326858130af85e049f2661ba3c78b26589b8ab98e65e80ae44a1252" dependencies = [ "bitcoin_hashes", "rand 0.8.5", - "secp256k1-sys", + "secp256k1-sys 0.10.1", "serde", ] +[[package]] +name = "secp256k1-sys" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4473013577ec77b4ee3668179ef1186df3146e2cf2d927bd200974c6fe60fd99" +dependencies = [ + "cc", +] + [[package]] name = "secp256k1-sys" version = "0.10.1" @@ -7922,6 +8051,19 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" +[[package]] +name = "sha2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if", + "cpufeatures", + "digest 0.9.0", + "opaque-debug", +] + [[package]] name = "sha2" version = "0.10.9" @@ -8147,7 +8289,7 @@ dependencies = [ "percent-encoding", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "smallvec", "thiserror 2.0.17", "tokio", @@ -8184,7 +8326,7 @@ dependencies = [ "quote", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "sqlx-core", "sqlx-mysql", "sqlx-postgres", @@ -8227,7 +8369,7 @@ dependencies = [ "rsa", "serde", "sha1", - "sha2", + "sha2 0.10.9", "smallvec", "sqlx-core", "stringprep", @@ -8264,7 +8406,7 @@ dependencies = [ "rand 0.8.5", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "smallvec", "sqlx-core", "stringprep", @@ -8415,6 +8557,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "subtle-ng" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "734676eb262c623cec13c3155096e08d1f8f29adce39ba17948b18dad1e54142" + [[package]] name = "syn" version = "1.0.109" @@ -8538,6 +8686,36 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "tendermint" +version = "0.40.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc997743ecfd4864bbca8170d68d9b2bee24653b034210752c2d883ef4b838b1" +dependencies = [ + "bytes", + "digest 0.10.7", + "ed25519", + "ed25519-consensus", + "flex-error", + "futures", + "k256", + "num-traits", + "once_cell", + "prost", + "ripemd", + "serde", + "serde_bytes", + "serde_json", + "serde_repr", + "sha2 0.10.9", + "signature", + "subtle", + "subtle-encoding", + "tendermint-proto", + "time", + "zeroize", +] + [[package]] name = "tendermint-proto" version = "0.40.4" @@ -8852,6 +9030,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e581ba15a835f4d9ea06c55ab1bd4dce26fc53752c69a04aac00703bfb49ba9" dependencies = [ "async-trait", + "axum", "base64 0.22.1", "bytes", "h2", diff --git a/crates/adapters/dydx/Cargo.toml b/crates/adapters/dydx/Cargo.toml index 6532e04cb248..e9a6206dc369 100644 --- a/crates/adapters/dydx/Cargo.toml +++ b/crates/adapters/dydx/Cargo.toml @@ -17,7 +17,7 @@ homepage.workspace = true workspace = true [lib] -name = "nautilus_dydx_v4" +name = "nautilus_dydx" crate-type = ["rlib", "cdylib"] [features] @@ -60,13 +60,21 @@ async-stream = { workspace = true } async-trait = { workspace = true } aws-lc-rs = { workspace = true } base64 = { workspace = true } +bip32 = { version = "0.5", default-features = false, features = [ + "bip39", + "alloc", + "secp256k1", +] } chrono = { workspace = true } +cosmrs = "0.21" dashmap = { workspace = true } derive_builder = { workspace = true } dydx-proto = { workspace = true } futures-util = { workspace = true } indexmap = { workspace = true } log = { workspace = true } +prost = "0.13" +prost-types = "0.13" reqwest = { workspace = true } rust_decimal = { workspace = true } serde = { workspace = true } @@ -77,6 +85,7 @@ thiserror = { workspace = true } tokio = { workspace = true } tokio-tungstenite = { workspace = true } tokio-util = { workspace = true } +tonic = "0.13" tracing = { workspace = true } tracing-subscriber = { workspace = true } # Needed for example binaries ustr = { workspace = true } diff --git a/crates/adapters/dydx/src/common/consts.rs b/crates/adapters/dydx/src/common/consts.rs index 498c67f67b9d..0f4e118f5de4 100644 --- a/crates/adapters/dydx/src/common/consts.rs +++ b/crates/adapters/dydx/src/common/consts.rs @@ -15,6 +15,9 @@ //! Core constants shared across the dYdX adapter components. +/// dYdX adapter name. +pub const DYDX: &str = "DYDX"; + /// dYdX mainnet chain ID. pub const DYDX_CHAIN_ID: &str = "dydx-mainnet-1"; diff --git a/crates/adapters/dydx/src/common/mod.rs b/crates/adapters/dydx/src/common/mod.rs index 5b447c923283..6dde695fd475 100644 --- a/crates/adapters/dydx/src/common/mod.rs +++ b/crates/adapters/dydx/src/common/mod.rs @@ -13,16 +13,6 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -//! Common functionality shared across the dYdX adapter. -//! -//! This module provides core utilities, constants, and data structures used throughout -//! the dYdX integration, including: -//! -//! - Common constants for venues and identifiers. -//! - URL management for HTTP and WebSocket endpoints. -//! - Shared data types and models. -//! - Test utilities for development. - pub mod consts; pub mod types; pub mod urls; diff --git a/crates/adapters/dydx/src/error.rs b/crates/adapters/dydx/src/error.rs index ee23fa9a4281..9dfb3bc3ed75 100644 --- a/crates/adapters/dydx/src/error.rs +++ b/crates/adapters/dydx/src/error.rs @@ -16,7 +16,7 @@ //! Error handling for the dYdX adapter. //! //! This module provides error types for all dYdX operations, including -//! HTTP and WebSocket errors. +//! HTTP, WebSocket, and gRPC errors. use thiserror::Error; @@ -34,6 +34,22 @@ pub enum DydxError { #[error("WebSocket error: {0}")] WebSocket(String), + /// gRPC errors from Cosmos SDK node. + #[error("gRPC error: {0}")] + Grpc(#[from] tonic::Status), + + /// Transaction signing errors. + #[error("Signing error: {0}")] + Signing(String), + + /// Protocol buffer encoding errors. + #[error("Encoding error: {0}")] + Encoding(#[from] prost::EncodeError), + + /// Protocol buffer decoding errors. + #[error("Decoding error: {0}")] + Decoding(#[from] prost::DecodeError), + /// JSON serialization/deserialization errors. #[error("JSON error: {message}")] Json { diff --git a/crates/adapters/dydx/src/grpc/builder.rs b/crates/adapters/dydx/src/grpc/builder.rs new file mode 100644 index 000000000000..de73f4c34b63 --- /dev/null +++ b/crates/adapters/dydx/src/grpc/builder.rs @@ -0,0 +1,163 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved. +// https://nautechsystems.io +// +// Licensed under the GNU Lesser General Public License Version 3.0 (the "License"); +// You may not use this file except in compliance with the License. +// You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------------------------------- + +//! Transaction builder for dYdX v4 protocol. +//! +//! This module provides utilities for building and signing Cosmos SDK transactions +//! for the dYdX v4 protocol. + +use std::fmt::Debug; + +use cosmrs::{ + Any, Coin, + tx::{self, Fee, SignDoc, SignerInfo}, +}; +use rust_decimal::{Decimal, prelude::ToPrimitive}; + +use super::{types::ChainId, wallet::Account}; + +/// Gas adjustment value to avoid rejected transactions caused by gas underestimation. +const GAS_MULTIPLIER: f64 = 1.8; + +/// Transaction builder. +/// +/// Handles fee calculation, transaction construction, and signing. +pub struct TxBuilder { + chain_id: cosmrs::tendermint::chain::Id, + fee_denom: String, +} + +impl TxBuilder { + /// Create a new transaction builder. + /// + /// # Errors + /// + /// Returns an error if the chain ID cannot be converted. + pub fn new(chain_id: ChainId, fee_denom: String) -> Result { + Ok(Self { + chain_id: chain_id.try_into()?, + fee_denom, + }) + } + + /// Estimate a transaction fee. + /// + /// See also [What Are Crypto Gas Fees?](https://dydx.exchange/crypto-learning/what-are-crypto-gas-fees). + /// + /// # Errors + /// + /// Returns an error if fee calculation fails. + pub fn calculate_fee(&self, gas_used: Option) -> Result { + if let Some(gas) = gas_used { + self.calculate_fee_from_gas(gas) + } else { + Ok(Self::default_fee()) + } + } + + /// Calculate fee from gas usage. + fn calculate_fee_from_gas(&self, gas_used: u64) -> Result { + let gas_multiplier = Decimal::try_from(GAS_MULTIPLIER)?; + let gas_limit = Decimal::from(gas_used) * gas_multiplier; + + // Gas price for dYdX (typically 0.025 adydx per gas) + let gas_price = Decimal::new(25, 3); // 0.025 + let amount = (gas_price * gas_limit).ceil(); + + let gas_limit_u64 = gas_limit + .to_u64() + .ok_or_else(|| anyhow::anyhow!("Failed converting gas limit to u64"))?; + + let amount_u128 = amount + .to_u128() + .ok_or_else(|| anyhow::anyhow!("Failed converting gas cost to u128"))?; + + Ok(Fee::from_amount_and_gas( + Coin { + amount: amount_u128, + denom: self + .fee_denom + .parse() + .map_err(|e| anyhow::anyhow!("Invalid fee denom: {e}"))?, + }, + gas_limit_u64, + )) + } + + /// Get default fee (zero fee). + fn default_fee() -> Fee { + Fee { + amount: vec![], + gas_limit: 0, + payer: None, + granter: None, + } + } + + /// Build a transaction for given messages. + /// + /// # Errors + /// + /// Returns an error if transaction building or signing fails. + pub fn build_transaction( + &self, + account: &Account, + msgs: impl IntoIterator, + fee: Option, + ) -> Result { + let mut builder = tx::BodyBuilder::new(); + builder.msgs(msgs).memo(""); + let tx_body = builder.finish(); + + let fee = fee.unwrap_or_else(|| { + self.calculate_fee(None) + .unwrap_or_else(|_| Self::default_fee()) + }); + + let auth_info = + SignerInfo::single_direct(Some(account.public_key()), account.sequence_number) + .auth_info(fee); + + let sign_doc = SignDoc::new(&tx_body, &auth_info, &self.chain_id, account.account_number) + .map_err(|e| anyhow::anyhow!("Cannot create sign doc: {e}"))?; + + account.sign(sign_doc) + } + + /// Build and simulate a transaction to estimate gas. + /// + /// Returns the raw transaction bytes suitable for simulation. + /// + /// # Errors + /// + /// Returns an error if transaction building fails. + pub fn build_for_simulation( + &self, + account: &Account, + msgs: impl IntoIterator, + ) -> Result, anyhow::Error> { + let tx_raw = self.build_transaction(account, msgs, None)?; + tx_raw.to_bytes().map_err(|e| anyhow::anyhow!("{e}")) + } +} + +impl Debug for TxBuilder { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("TxBuilder") + .field("chain_id", &self.chain_id) + .field("fee_denom", &self.fee_denom) + .finish() + } +} diff --git a/crates/adapters/dydx/src/grpc/client.rs b/crates/adapters/dydx/src/grpc/client.rs new file mode 100644 index 000000000000..8a6ff060a633 --- /dev/null +++ b/crates/adapters/dydx/src/grpc/client.rs @@ -0,0 +1,352 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved. +// https://nautechsystems.io +// +// Licensed under the GNU Lesser General Public License Version 3.0 (the "License"); +// You may not use this file except in compliance with the License. +// You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------------------------------- + +//! gRPC client implementation for dYdX v4 protocol. +//! +//! This module provides the main gRPC client for interacting with dYdX v4 validator nodes. +//! It handles transaction signing, broadcasting, and querying account state. + +use dydx_proto::{ + cosmos_sdk_proto::cosmos::{ + auth::v1beta1::{ + BaseAccount, QueryAccountRequest, query_client::QueryClient as AuthClient, + }, + bank::v1beta1::{QueryAllBalancesRequest, query_client::QueryClient as BankClient}, + base::{ + tendermint::v1beta1::{ + Block, GetLatestBlockRequest, GetNodeInfoRequest, GetNodeInfoResponse, + service_client::ServiceClient as BaseClient, + }, + v1beta1::Coin, + }, + tx::v1beta1::{ + BroadcastMode, BroadcastTxRequest, GetTxRequest, SimulateRequest, + service_client::ServiceClient as TxClient, + }, + }, + dydxprotocol::{ + clob::{ClobPair, QueryAllClobPairRequest, query_client::QueryClient as ClobClient}, + perpetuals::{ + Perpetual, QueryAllPerpetualsRequest, query_client::QueryClient as PerpetualsClient, + }, + subaccounts::{ + QueryGetSubaccountRequest, Subaccount as SubaccountInfo, + query_client::QueryClient as SubaccountsClient, + }, + }, +}; +use prost::Message as ProstMessage; +use tonic::transport::Channel; + +use crate::error::DydxError; + +/// Transaction hash type (internally uses tendermint::Hash). +pub type TxHash = String; + +/// Block height. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub struct Height(pub u32); + +/// gRPC client for dYdX v4 protocol operations. +/// +/// This client handles: +/// - Transaction signing and broadcasting. +/// - Account query operations. +/// - Order placement and management via Cosmos SDK messages. +/// +/// # Examples +/// +/// ```no_run +/// use nautilus_dydx::grpc::DydxGrpcClient; +/// +/// # async fn example() -> Result<(), Box> { +/// let grpc_url = "https://dydx-grpc.publicnode.com:443".to_string(); +/// let client = DydxGrpcClient::new(grpc_url).await?; +/// # Ok(()) +/// # } +/// ``` +#[derive(Debug, Clone)] +pub struct DydxGrpcClient { + channel: Channel, + auth: AuthClient, + bank: BankClient, + base: BaseClient, + tx: TxClient, + clob: ClobClient, + perpetuals: PerpetualsClient, + subaccounts: SubaccountsClient, +} + +impl DydxGrpcClient { + /// Create a new gRPC client. + /// + /// # Errors + /// + /// Returns an error if the gRPC connection cannot be established. + pub async fn new(grpc_url: String) -> Result { + let channel = Channel::from_shared(grpc_url) + .map_err(|e| DydxError::Config(format!("Invalid gRPC URL: {e}")))? + .connect() + .await + .map_err(|e| { + DydxError::Grpc(tonic::Status::unavailable(format!( + "Connection failed: {e}" + ))) + })?; + + Ok(Self { + auth: AuthClient::new(channel.clone()), + bank: BankClient::new(channel.clone()), + base: BaseClient::new(channel.clone()), + tx: TxClient::new(channel.clone()), + clob: ClobClient::new(channel.clone()), + perpetuals: PerpetualsClient::new(channel.clone()), + subaccounts: SubaccountsClient::new(channel.clone()), + channel, + }) + } + + /// Get the underlying gRPC channel. + /// + /// This can be used to create custom gRPC service clients. + #[must_use] + pub fn channel(&self) -> &Channel { + &self.channel + } + + /// Query account information for a given address. + /// + /// Returns the account number and sequence number needed for transaction signing. + /// + /// # Errors + /// + /// Returns an error if the query fails or the account does not exist. + pub async fn query_address(&mut self, address: &str) -> Result<(u64, u64), anyhow::Error> { + let req = QueryAccountRequest { + address: address.to_string(), + }; + let resp = self + .auth + .account(req) + .await? + .into_inner() + .account + .ok_or_else(|| { + anyhow::anyhow!("Query account request failure, account should exist") + })?; + + let account = BaseAccount::decode(&*resp.value)?; + Ok((account.account_number, account.sequence)) + } + + /// Query for [an account](https://github.com/cosmos/cosmos-sdk/tree/main/x/auth#account-1) + /// by its address. + /// + /// # Errors + /// + /// Returns an error if the query fails or the account does not exist. + pub async fn get_account(&mut self, address: &str) -> Result { + let req = QueryAccountRequest { + address: address.to_string(), + }; + let resp = self + .auth + .account(req) + .await? + .into_inner() + .account + .ok_or_else(|| { + anyhow::anyhow!("Query account request failure, account should exist") + })?; + + Ok(BaseAccount::decode(&*resp.value)?) + } + + /// Query for [account balances](https://github.com/cosmos/cosmos-sdk/tree/main/x/bank#allbalances) + /// by address for all denominations. + /// + /// # Errors + /// + /// Returns an error if the query fails. + pub async fn get_account_balances( + &mut self, + address: &str, + ) -> Result, anyhow::Error> { + let req = QueryAllBalancesRequest { + address: address.to_string(), + resolve_denom: false, + pagination: None, + }; + let balances = self.bank.all_balances(req).await?.into_inner().balances; + Ok(balances) + } + + /// Query for node info. + /// + /// # Errors + /// + /// Returns an error if the query fails. + pub async fn get_node_info(&mut self) -> Result { + let req = GetNodeInfoRequest {}; + let info = self.base.get_node_info(req).await?.into_inner(); + Ok(info) + } + + /// Query for the latest block. + /// + /// # Errors + /// + /// Returns an error if the query fails. + pub async fn latest_block(&mut self) -> Result { + let req = GetLatestBlockRequest::default(); + let latest_block = self + .base + .get_latest_block(req) + .await? + .into_inner() + .sdk_block + .ok_or_else(|| anyhow::anyhow!("The latest block is empty"))?; + Ok(latest_block) + } + + /// Query for the latest block height. + /// + /// # Errors + /// + /// Returns an error if the query fails. + pub async fn latest_block_height(&mut self) -> Result { + let latest_block = self.latest_block().await?; + let header = latest_block + .header + .ok_or_else(|| anyhow::anyhow!("The block doesn't contain a header"))?; + let height = Height(header.height.try_into()?); + Ok(height) + } + + /// Query for all perpetual markets. + /// + /// # Errors + /// + /// Returns an error if the query fails. + pub async fn get_perpetuals(&mut self) -> Result, anyhow::Error> { + let req = QueryAllPerpetualsRequest { pagination: None }; + let response = self.perpetuals.all_perpetuals(req).await?.into_inner(); + Ok(response.perpetual) + } + + /// Query for all CLOB pairs. + /// + /// # Errors + /// + /// Returns an error if the query fails. + pub async fn get_clob_pairs(&mut self) -> Result, anyhow::Error> { + let req = QueryAllClobPairRequest { pagination: None }; + let pairs = self.clob.clob_pair_all(req).await?.into_inner().clob_pair; + Ok(pairs) + } + + /// Query for subaccount information. + /// + /// # Errors + /// + /// Returns an error if the query fails. + pub async fn get_subaccount( + &mut self, + address: &str, + number: u32, + ) -> Result { + let req = QueryGetSubaccountRequest { + owner: address.to_string(), + number, + }; + let subaccount = self + .subaccounts + .subaccount(req) + .await? + .into_inner() + .subaccount + .ok_or_else(|| { + anyhow::anyhow!("Subaccount query response does not contain subaccount") + })?; + Ok(subaccount) + } + + /// Simulate a transaction to estimate gas usage. + /// + /// # Errors + /// + /// Returns an error if simulation fails. + #[allow(deprecated)] + pub async fn simulate_tx(&mut self, tx_bytes: Vec) -> Result { + let req = SimulateRequest { tx_bytes, tx: None }; + let gas_used = self + .tx + .simulate(req) + .await? + .into_inner() + .gas_info + .ok_or_else(|| anyhow::anyhow!("Simulation response does not contain gas info"))? + .gas_used; + Ok(gas_used) + } + + /// Broadcast a signed transaction. + /// + /// # Errors + /// + /// Returns an error if broadcasting fails. + pub async fn broadcast_tx(&mut self, tx_bytes: Vec) -> Result { + let req = BroadcastTxRequest { + tx_bytes, + mode: BroadcastMode::Sync as i32, + }; + let response = self.tx.broadcast_tx(req).await?.into_inner(); + + if let Some(tx_response) = response.tx_response { + if tx_response.code != 0 { + anyhow::bail!( + "Transaction broadcast failed: code={}, log={}", + tx_response.code, + tx_response.raw_log + ); + } + Ok(tx_response.txhash) + } else { + Err(anyhow::anyhow!( + "Broadcast response does not contain tx_response" + )) + } + } + + /// Query transaction by hash. + /// + /// # Errors + /// + /// Returns an error if the query fails. + pub async fn get_tx(&mut self, hash: &str) -> Result { + let req = GetTxRequest { + hash: hash.to_string(), + }; + let response = self.tx.get_tx(req).await?.into_inner(); + + if let Some(tx) = response.tx { + // Convert through bytes since the types are incompatible + let tx_bytes = tx.encode_to_vec(); + cosmrs::Tx::try_from(tx_bytes.as_slice()).map_err(|e| anyhow::anyhow!("{}", e)) + } else { + anyhow::bail!("Transaction not found") + } + } +} diff --git a/crates/adapters/dydx/src/grpc/mod.rs b/crates/adapters/dydx/src/grpc/mod.rs new file mode 100644 index 000000000000..95ae225f964e --- /dev/null +++ b/crates/adapters/dydx/src/grpc/mod.rs @@ -0,0 +1,53 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved. +// https://nautechsystems.io +// +// Licensed under the GNU Lesser General Public License Version 3.0 (the "License"); +// You may not use this file except in compliance with the License. +// You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------------------------------- + +//! gRPC client implementation for the dYdX v4 protocol. +//! +//! This module provides gRPC client functionality for interacting with the dYdX v4 protocol +//! via the Cosmos SDK. It handles: +//! +//! - Transaction signing and broadcasting using `cosmrs`. +//! - gRPC communication with validator nodes. +//! - Protocol Buffer message encoding/decoding. +//! - Cosmos SDK account management. +//! +//! The client supports dYdX trading operations including: +//! +//! - Order placement, modification, and cancellation. +//! - Transfer operations between subaccounts. +//! - Subaccount management. +//! - Transaction signing with secp256k1 keys. +//! +//! # Architecture +//! +//! dYdX v4 is built on the Cosmos SDK and uses gRPC for all state-changing operations +//! (placing orders, transfers, etc.). The HTTP/REST API (Indexer) is read-only and used +//! for querying market data and historical information. + +pub mod builder; +pub mod client; +pub mod order; +pub mod types; +pub mod wallet; + +// Re-exports +pub use builder::TxBuilder; +pub use client::{DydxGrpcClient, Height, TxHash}; +pub use order::{ + DEFAULT_RUST_CLIENT_METADATA, OrderBuilder, OrderFlags, OrderGoodUntil, OrderMarketParams, + OrderType, SHORT_TERM_ORDER_MAXIMUM_LIFETIME, +}; +pub use types::ChainId; +pub use wallet::{Account, Subaccount, Wallet}; diff --git a/crates/adapters/dydx/src/grpc/order.rs b/crates/adapters/dydx/src/grpc/order.rs new file mode 100644 index 000000000000..d17c95ed1ee7 --- /dev/null +++ b/crates/adapters/dydx/src/grpc/order.rs @@ -0,0 +1,442 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved. +// https://nautechsystems.io +// +// Licensed under the GNU Lesser General Public License Version 3.0 (the "License"); +// You may not use this file except in compliance with the License. +// You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------------------------------- + +//! Order types and builders for dYdX v4. +//! +//! This module provides order construction utilities for placing orders on dYdX v4. +//! dYdX supports two order lifetime types: +//! +//! - **Short-term orders**: Expire by block height (max 20 blocks). +//! - **Long-term orders**: Expire by timestamp. +//! +//! See [dYdX order types](https://help.dydx.trade/en/articles/166985-short-term-vs-long-term-order-types). + +use chrono::{DateTime, Utc}; +use dydx_proto::dydxprotocol::clob::{ + Order, OrderId, + order::{ConditionType, Side as OrderSide, TimeInForce as OrderTimeInForce}, +}; +use rust_decimal::{Decimal, prelude::ToPrimitive}; + +/// Maximum short-term order lifetime in blocks. +/// +/// See also [short-term vs long-term orders](https://help.dydx.trade/en/articles/166985-short-term-vs-long-term-order-types). +pub const SHORT_TERM_ORDER_MAXIMUM_LIFETIME: u32 = 20; + +/// Value used to identify the Rust client in order metadata. +pub const DEFAULT_RUST_CLIENT_METADATA: u32 = 4; + +/// Order [expiration types](https://docs.dydx.xyz/concepts/trading/orders#comparison). +#[derive(Clone, Debug)] +pub enum OrderGoodUntil { + /// Block expiration is used for short-term orders. + /// The order expires after the specified block height. + Block(u32), + /// Time expiration is used for long-term orders. + /// The order expires at the specified timestamp. + Time(DateTime), +} + +/// Order type enumeration. +#[derive(Clone, Debug)] +pub enum OrderType { + /// Limit order. + Limit, + /// Market order. + Market, + /// Stop limit order. + StopLimit, + /// Stop market order. + StopMarket, + /// Take profit order. + TakeProfit, + /// Take profit market order. + TakeProfitMarket, +} + +/// Order flags indicating order lifetime and execution type. +#[derive(Clone, Debug)] +pub enum OrderFlags { + /// Short-term order (expires by block height). + ShortTerm, + /// Long-term order (expires by timestamp). + LongTerm, + /// Conditional order (triggered by trigger price). + Conditional, +} + +/// Market parameters required for price and size quantizations. +/// +/// These quantizations are required for `Order` placement. +/// See also [how to interpret block data for trades](https://docs.dydx.exchange/api_integration-guides/how_to_interpret_block_data_for_trades). +#[derive(Clone, Debug)] +pub struct OrderMarketParams { + /// Atomic resolution. + pub atomic_resolution: i32, + /// CLOB pair ID. + pub clob_pair_id: u32, + /// Oracle price. + pub oracle_price: Option, + /// Quantum conversion exponent. + pub quantum_conversion_exponent: i32, + /// Step base quantums. + pub step_base_quantums: u64, + /// Subticks per tick. + pub subticks_per_tick: u32, +} + +impl OrderMarketParams { + /// Convert price into subticks. + /// + /// # Errors + /// + /// Returns an error if conversion fails. + pub fn quantize_price(&self, price: Decimal) -> Result { + const QUOTE_QUANTUMS_ATOMIC_RESOLUTION: i32 = -6; + let scale = -(self.atomic_resolution + - self.quantum_conversion_exponent + - QUOTE_QUANTUMS_ATOMIC_RESOLUTION); + + let factor = Decimal::new(1, scale.unsigned_abs()); + let raw_subticks = price * factor; + let subticks_per_tick = Decimal::from(self.subticks_per_tick); + let quantums = Self::quantize(&raw_subticks, &subticks_per_tick); + let result = quantums.max(subticks_per_tick); + + result + .to_u64() + .ok_or_else(|| anyhow::anyhow!("Failed to convert price to u64")) + } + + /// Convert decimal into quantums. + /// + /// # Errors + /// + /// Returns an error if conversion fails. + pub fn quantize_quantity(&self, quantity: Decimal) -> Result { + let factor = Decimal::new(1, self.atomic_resolution.unsigned_abs()); + let raw_quantums = quantity * factor; + let step_base_quantums = Decimal::from(self.step_base_quantums); + let quantums = Self::quantize(&raw_quantums, &step_base_quantums); + let result = quantums.max(step_base_quantums); + + result + .to_u64() + .ok_or_else(|| anyhow::anyhow!("Failed to convert quantity to u64")) + } + + /// A `round`-like function that quantizes a `value` to the `fraction`. + fn quantize(value: &Decimal, fraction: &Decimal) -> Decimal { + (value / fraction).round() * fraction + } + + /// Get orderbook pair id. + #[must_use] + pub fn clob_pair_id(&self) -> u32 { + self.clob_pair_id + } +} + +/// [`Order`] builder. +/// +/// Note that the price input to the `OrderBuilder` is in the "common" units of the perpetual/currency, +/// not the quantized/atomic value. +/// +/// Two main classes of orders in dYdX from persistence perspective are +/// [short-term and long-term (stateful) orders](https://docs.dydx.xyz/concepts/trading/orders#short-term-vs-long-term). +/// +/// For different types of orders see also [Stop-Limit Versus Stop-Loss](https://dydx.exchange/crypto-learning/stop-limit-versus-stop-loss) +/// and [Perpetual order types on dYdX Chain](https://help.dydx.trade/en/articles/166981-perpetual-order-types-on-dydx-chain). +#[derive(Clone, Debug)] +pub struct OrderBuilder { + market_params: OrderMarketParams, + subaccount_owner: String, + subaccount_number: u32, + client_id: u32, + flags: OrderFlags, + side: Option, + order_type: Option, + size: Option, + price: Option, + time_in_force: Option, + reduce_only: Option, + until: Option, + trigger_price: Option, + condition_type: Option, +} + +impl OrderBuilder { + /// Create a new [`Order`] builder. + #[must_use] + pub fn new( + market_params: OrderMarketParams, + subaccount_owner: String, + subaccount_number: u32, + client_id: u32, + ) -> Self { + Self { + market_params, + subaccount_owner, + subaccount_number, + client_id, + flags: OrderFlags::ShortTerm, + side: Some(OrderSide::Buy), + order_type: Some(OrderType::Market), + size: None, + price: None, + time_in_force: None, + reduce_only: None, + until: None, + trigger_price: None, + condition_type: None, + } + } + + /// Set as Market order. + /// + /// An instruction to immediately buy or sell an asset at the best available price when the order is placed. + pub fn market(mut self, side: OrderSide, size: Decimal) -> Self { + self.order_type = Some(OrderType::Market); + self.side = Some(side); + self.size = Some(size); + self + } + + /// Set as Limit order. + /// + /// With a limit order, a trader specifies the price at which they're willing to buy or sell an asset. + /// Unlike market orders, limit orders don't go into effect until the market price hits a trader's "limit price." + pub fn limit(mut self, side: OrderSide, price: Decimal, size: Decimal) -> Self { + self.order_type = Some(OrderType::Limit); + self.price = Some(price); + self.side = Some(side); + self.size = Some(size); + self + } + + /// Set as Stop Limit order. + /// + /// Stop-limit orders use a stop `trigger_price` and a limit `price` to give investors greater control over their trades. + pub fn stop_limit( + mut self, + side: OrderSide, + price: Decimal, + trigger_price: Decimal, + size: Decimal, + ) -> Self { + self.order_type = Some(OrderType::StopLimit); + self.price = Some(price); + self.trigger_price = Some(trigger_price); + self.side = Some(side); + self.size = Some(size); + self.conditional() + } + + /// Set as Stop Market order. + /// + /// When using a stop order, the trader sets a `trigger_price` to trigger a buy or sell order on their exchange. + pub fn stop_market(mut self, side: OrderSide, trigger_price: Decimal, size: Decimal) -> Self { + self.order_type = Some(OrderType::StopMarket); + self.trigger_price = Some(trigger_price); + self.side = Some(side); + self.size = Some(size); + self.conditional() + } + + /// Set as Take Profit Limit order. + /// + /// The order enters in force if the price reaches `trigger_price` and is executed at `price` after that. + pub fn take_profit_limit( + mut self, + side: OrderSide, + price: Decimal, + trigger_price: Decimal, + size: Decimal, + ) -> Self { + self.order_type = Some(OrderType::TakeProfit); + self.price = Some(price); + self.trigger_price = Some(trigger_price); + self.side = Some(side); + self.size = Some(size); + self.conditional() + } + + /// Set as Take Profit Market order. + /// + /// The order enters in force if the price reaches `trigger_price` and converts to an ordinary market order. + pub fn take_profit_market( + mut self, + side: OrderSide, + trigger_price: Decimal, + size: Decimal, + ) -> Self { + self.order_type = Some(OrderType::TakeProfitMarket); + self.trigger_price = Some(trigger_price); + self.side = Some(side); + self.size = Some(size); + self.conditional() + } + + /// Set order as a long-term order. + pub fn long_term(mut self) -> Self { + self.flags = OrderFlags::LongTerm; + self + } + + /// Set order as a short-term order. + pub fn short_term(mut self) -> Self { + self.flags = OrderFlags::ShortTerm; + self + } + + /// Set order as a conditional order, triggered using `trigger_price`. + pub fn conditional(mut self) -> Self { + self.flags = OrderFlags::Conditional; + self + } + + /// Set the limit price for Limit orders. + pub fn price(mut self, price: Decimal) -> Self { + self.price = Some(price); + self + } + + /// Set position size. + pub fn size(mut self, size: Decimal) -> Self { + self.size = Some(size); + self + } + + /// Set [time execution options](https://docs.dydx.xyz/types/time_in_force#time-in-force). + pub fn time_in_force(mut self, tif: OrderTimeInForce) -> Self { + self.time_in_force = Some(tif); + self + } + + /// Set an order as [reduce-only](https://docs.dydx.xyz/concepts/trading/orders#types). + pub fn reduce_only(mut self, reduce: bool) -> Self { + self.reduce_only = Some(reduce); + self + } + + /// Set order's expiration. + pub fn until(mut self, gtof: OrderGoodUntil) -> Self { + self.until = Some(gtof); + self + } + + /// Build the order. + /// + /// # Errors + /// + /// Returns an error if the order parameters are invalid. + pub fn build(self) -> Result { + let side = self + .side + .ok_or_else(|| anyhow::anyhow!("Order side not set"))?; + let size = self + .size + .ok_or_else(|| anyhow::anyhow!("Order size not set"))?; + + // Quantize size + let quantums = self.market_params.quantize_quantity(size)?; + + // Build order ID + let order_id = Some(OrderId { + subaccount_id: Some(dydx_proto::dydxprotocol::subaccounts::SubaccountId { + owner: self.subaccount_owner.clone(), + number: self.subaccount_number, + }), + client_id: self.client_id, + order_flags: match self.flags { + OrderFlags::ShortTerm => 0, + OrderFlags::LongTerm => 64, + OrderFlags::Conditional => 32, + }, + clob_pair_id: self.market_params.clob_pair_id, + }); + + // Set good til oneof + let good_til_oneof = if let Some(until) = self.until { + match until { + OrderGoodUntil::Block(height) => { + Some(dydx_proto::dydxprotocol::clob::order::GoodTilOneof::GoodTilBlock(height)) + } + OrderGoodUntil::Time(time) => Some( + dydx_proto::dydxprotocol::clob::order::GoodTilOneof::GoodTilBlockTime( + time.timestamp().try_into()?, + ), + ), + } + } else { + None + }; + + // Quantize price if provided + let subticks = if let Some(price) = self.price { + self.market_params.quantize_price(price)? + } else { + 0 + }; + + Ok(Order { + order_id, + side: side as i32, + quantums, + subticks, + good_til_oneof, + time_in_force: self.time_in_force.map_or(0, |tif| tif as i32), + reduce_only: self.reduce_only.unwrap_or(false), + client_metadata: DEFAULT_RUST_CLIENT_METADATA, + condition_type: self.condition_type.map_or(0, |ct| ct as i32), + conditional_order_trigger_subticks: self + .trigger_price + .map(|tp| self.market_params.quantize_price(tp)) + .transpose()? + .unwrap_or(0), + twap_parameters: None, + builder_code_parameters: None, + order_router_address: String::new(), + }) + } +} + +impl Default for OrderBuilder { + fn default() -> Self { + Self { + market_params: OrderMarketParams { + atomic_resolution: -10, + clob_pair_id: 0, + oracle_price: None, + quantum_conversion_exponent: -9, + step_base_quantums: 1_000_000, + subticks_per_tick: 100_000, + }, + subaccount_owner: String::new(), + subaccount_number: 0, + client_id: 0, + flags: OrderFlags::ShortTerm, + side: Some(OrderSide::Buy), + order_type: Some(OrderType::Market), + size: None, + price: None, + time_in_force: None, + reduce_only: None, + until: None, + trigger_price: None, + condition_type: None, + } + } +} diff --git a/crates/adapters/dydx/src/grpc/types.rs b/crates/adapters/dydx/src/grpc/types.rs new file mode 100644 index 000000000000..be049fb9e170 --- /dev/null +++ b/crates/adapters/dydx/src/grpc/types.rs @@ -0,0 +1,44 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved. +// https://nautechsystems.io +// +// Licensed under the GNU Lesser General Public License Version 3.0 (the "License"); +// You may not use this file except in compliance with the License. +// You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------------------------------- + +//! Type definitions for dYdX v4 gRPC operations. + +use cosmrs::tendermint::{Error, chain::Id}; +use serde::{Deserialize, Serialize}; +use strum::{AsRefStr, Display}; + +/// [Chain ID](https://docs.dydx.xyz/nodes/network-constants#chain-id) +/// serves as a unique chain identifier to prevent replay attacks. +/// +/// See also [Cosmos ecosystem](https://cosmos.directory/). +#[derive(Debug, Eq, PartialEq, Clone, Display, AsRefStr, Deserialize, Serialize)] +pub enum ChainId { + /// Testnet. + #[strum(serialize = "dydx-testnet-4")] + #[serde(rename = "dydx-testnet-4")] + Testnet4, + /// Mainnet. + #[strum(serialize = "dydx-mainnet-1")] + #[serde(rename = "dydx-mainnet-1")] + Mainnet1, +} + +impl TryFrom for Id { + type Error = Error; + + fn try_from(chain_id: ChainId) -> Result { + chain_id.as_ref().parse() + } +} diff --git a/crates/adapters/dydx/src/grpc/wallet.rs b/crates/adapters/dydx/src/grpc/wallet.rs new file mode 100644 index 000000000000..9eaa2e5ce4ab --- /dev/null +++ b/crates/adapters/dydx/src/grpc/wallet.rs @@ -0,0 +1,192 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved. +// https://nautechsystems.io +// +// Licensed under the GNU Lesser General Public License Version 3.0 (the "License"); +// You may not use this file except in compliance with the License. +// You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------------------------------- + +//! Wallet and account management for dYdX v4. +//! +//! This module provides wallet functionality for deriving accounts from BIP-39 mnemonics +//! and managing signing keys for Cosmos SDK transactions. + +use std::{fmt::Debug, str::FromStr}; + +use bip32::{DerivationPath, Language, Mnemonic, Seed}; +use cosmrs::{ + AccountId, + crypto::{PublicKey, secp256k1::SigningKey}, + tx, +}; + +/// Account prefix for dYdX addresses. +/// +/// See [Cosmos accounts](https://docs.cosmos.network/main/learn/beginner/accounts). +const BECH32_PREFIX_DYDX: &str = "dydx"; + +/// Hierarchical Deterministic (HD) [wallet](https://dydx.exchange/crypto-learning/glossary?#wallet) +/// which allows multiple addresses and signing keys from one master seed. +/// +/// [BIP-44](https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki) introduced a wallet +/// standard to derive multiple accounts for different chains from a single seed (which allows +/// recovery of the whole tree of keys). +/// +/// This `Wallet` uses the Cosmos ATOM derivation path to generate dYdX addresses. +/// +/// See also [Mastering Bitcoin](https://github.com/bitcoinbook/bitcoinbook/blob/develop/ch05_wallets.adoc). +pub struct Wallet { + seed: Seed, +} + +impl Debug for Wallet { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Wallet") + .field("seed", &"") + .finish() + } +} + +impl Wallet { + /// Derive a seed from a 24-word English mnemonic phrase. + /// + /// # Errors + /// + /// Returns an error if the mnemonic is invalid or cannot be converted to a seed. + pub fn from_mnemonic(mnemonic: &str) -> Result { + let seed = Mnemonic::new(mnemonic, Language::English)?.to_seed(""); + Ok(Self { seed }) + } + + /// Derive a dYdX account with zero account and sequence numbers. + /// + /// Account and sequence numbers must be fetched from the chain before signing transactions. + /// + /// # Errors + /// + /// Returns an error if the account derivation fails. + pub fn account_offline(&self, index: u32) -> Result { + self.derive_account(index, BECH32_PREFIX_DYDX) + } + + fn derive_account(&self, index: u32, prefix: &str) -> Result { + // BIP-44 derivation path for Cosmos (coin type 118) + // See https://github.com/satoshilabs/slips/blob/master/slip-0044.md + let derivation_str = format!("m/44'/118'/0'/0/{index}"); + let derivation_path = DerivationPath::from_str(&derivation_str)?; + let private_key = SigningKey::derive_from_path(&self.seed, &derivation_path)?; + let public_key = private_key.public_key(); + let account_id = public_key.account_id(prefix).map_err(anyhow::Error::msg)?; + let address = account_id.to_string(); + + Ok(Account { + index, + address, + account_id, + key: private_key, + account_number: 0, + sequence_number: 0, + }) + } +} + +/// Represents a derived dYdX account. +/// +/// An account contains the signing key and metadata needed to sign and broadcast transactions. +/// The `account_number` and `sequence_number` must be set from on-chain data before signing. +/// +/// See also [`Wallet`]. +pub struct Account { + /// Derivation index of the account. + pub index: u32, + /// dYdX address (bech32 encoded). + pub address: String, + /// Cosmos SDK account ID. + pub account_id: AccountId, + /// Private signing key. + key: SigningKey, + /// On-chain account number (must be fetched before signing). + pub account_number: u64, + /// Transaction sequence number (must be fetched before signing). + pub sequence_number: u64, +} + +impl Debug for Account { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Account") + .field("index", &self.index) + .field("address", &self.address) + .field("account_id", &self.account_id) + .field("key", &"") + .field("account_number", &self.account_number) + .field("sequence_number", &self.sequence_number) + .finish() + } +} + +impl Account { + /// Get the public key associated with this account. + #[must_use] + pub fn public_key(&self) -> PublicKey { + self.key.public_key() + } + + /// Sign a [`SignDoc`](tx::SignDoc) with the private key. + /// + /// # Errors + /// + /// Returns an error if signing fails. + pub fn sign(&self, doc: tx::SignDoc) -> Result { + doc.sign(&self.key) + .map_err(|e| anyhow::anyhow!("Failed to sign transaction: {e}")) + } + + /// Update account and sequence numbers from on-chain data. + pub fn set_account_info(&mut self, account_number: u64, sequence_number: u64) { + self.account_number = account_number; + self.sequence_number = sequence_number; + } + + /// Increment the sequence number (used after successful transaction broadcast). + pub fn increment_sequence(&mut self) { + self.sequence_number += 1; + } + + /// Derive a subaccount for this account. + /// + /// # Errors + /// + /// Returns an error if the subaccount number is invalid. + pub fn subaccount(&self, number: u32) -> Result { + Ok(Subaccount { + address: self.address.clone(), + number, + }) + } +} + +/// A subaccount within a dYdX account. +/// +/// Each account can have multiple subaccounts for organizing positions and balances. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Subaccount { + /// Parent account address. + pub address: String, + /// Subaccount number. + pub number: u32, +} + +impl Subaccount { + /// Create a new subaccount. + #[must_use] + pub fn new(address: String, number: u32) -> Self { + Self { address, number } + } +} diff --git a/crates/adapters/dydx/src/lib.rs b/crates/adapters/dydx/src/lib.rs index 6bf7cb58c342..5729578b50aa 100644 --- a/crates/adapters/dydx/src/lib.rs +++ b/crates/adapters/dydx/src/lib.rs @@ -53,6 +53,7 @@ pub mod common; pub mod config; pub mod error; +pub mod grpc; pub mod http; pub mod proto; diff --git a/deny.toml b/deny.toml index 6fb7c316bf8f..3fa3fad68012 100644 --- a/deny.toml +++ b/deny.toml @@ -65,6 +65,7 @@ allow = [ "LGPL-3.0-or-later", "OpenSSL", "Unlicense", + "LicenseRef-dYdX-Custom", # dydx-proto custom license (Apache-2.0 based) ] confidence-threshold = 0.8 From 66441355be828c0a04ef65bc163ac10774e8eda6 Mon Sep 17 00:00:00 2001 From: Vadim Nicolai Date: Mon, 3 Nov 2025 11:14:55 +0200 Subject: [PATCH 04/10] Symlinked licence and set publish to false --- crates/adapters/dydx/Cargo.toml | 1 + crates/adapters/dydx/LICENSE | 165 +------------------------------- 2 files changed, 2 insertions(+), 164 deletions(-) mode change 100644 => 120000 crates/adapters/dydx/LICENSE diff --git a/crates/adapters/dydx/Cargo.toml b/crates/adapters/dydx/Cargo.toml index e9a6206dc369..db5101081e22 100644 --- a/crates/adapters/dydx/Cargo.toml +++ b/crates/adapters/dydx/Cargo.toml @@ -1,6 +1,7 @@ [package] name = "nautilus-dydx" readme = "README.md" +publish = false # Do not publish to crates.io for now version.workspace = true edition.workspace = true rust-version.workspace = true diff --git a/crates/adapters/dydx/LICENSE b/crates/adapters/dydx/LICENSE deleted file mode 100644 index 5550e2db15f2..000000000000 --- a/crates/adapters/dydx/LICENSE +++ /dev/null @@ -1,164 +0,0 @@ - GNU LESSER GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - This version of the GNU Lesser General Public License incorporates -the terms and conditions of version 3 of the GNU General Public -License, supplemented by the additional permissions listed below. - - 0. Additional Definitions. - - As used herein, "this License" refers to version 3 of the GNU Lesser -General Public License, and the "GNU GPL" refers to version 3 of the GNU -General Public License. - - "The Library" refers to a covered work governed by this License, -other than an Application or a Combined Work as defined below. - - An "Application" is any work that makes use of an interface provided -by the Library, but which is not otherwise based on the Library. -Defining a subclass of a class defined by the Library is deemed a mode -of using an interface provided by the Library. - - A "Combined Work" is a work produced by combining or linking an -Application with the Library. The particular version of the Library -with which the Combined Work was made is also called the "Linked -Version". - - The "Minimal Corresponding Source" for a Combined Work means the -Corresponding Source for the Combined Work, excluding any source code -for portions of the Combined Work that, considered in isolation, are -based on the Application, and not on the Linked Version. - - The "Corresponding Application Code" for a Combined Work means the -object code and/or source code for the Application, including any data -and utility programs needed for reproducing the Combined Work from the -Application, but excluding the System Libraries of the Combined Work. - - 1. Exception to Section 3 of the GNU GPL. - - You may convey a covered work under sections 3 and 4 of this License -without being bound by section 3 of the GNU GPL. - - 2. Conveying Modified Versions. - - If you modify a copy of the Library, and, in your modifications, a -facility refers to a function or data to be supplied by an Application -that uses the facility (other than as an argument passed when the -facility is invoked), then you may convey a copy of the modified -version: - - a) under this License, provided that you make a good faith effort to - ensure that, in the event an Application does not supply the - function or data, the facility still operates, and performs - whatever part of its purpose remains meaningful, or - - b) under the GNU GPL, with none of the additional permissions of - this License applicable to that copy. - - 3. Object Code Incorporating Material from Library Header Files. - - The object code form of an Application may incorporate material from -a header file that is part of the Library. You may convey such object -code under terms of your choice, provided that, if the incorporated -material is not limited to numerical parameters, data structure -layouts and accessors, or small macros, inline functions and templates -(ten or fewer lines in length), you do both of the following: - - a) Give prominent notice with each copy of the object code that the - Library is used in it and that the Library and its use are - covered by this License. - - b) Accompany the object code with a copy of the GNU GPL and this license - document. - - 4. Combined Works. - - You may convey a Combined Work under terms of your choice that, -taken together, effectively do not restrict modification of the -portions of the Library contained in the Combined Work and reverse -engineering for debugging such modifications, if you also do each of -the following: - - a) Give prominent notice with each copy of the Combined Work that - the Library is used in it and that the Library and its use are - covered by this License. - - b) Accompany the Combined Work with a copy of the GNU GPL and this license - document. - - c) For a Combined Work that displays copyright notices during - execution, include the copyright notice for the Library among - these notices, as well as a reference directing the user to the - copies of the GNU GPL and this license document. - - d) Do one of the following: - - 0) Convey the Minimal Corresponding Source under the terms of this - License, and the Corresponding Application Code in a form - suitable for, and under terms that permit, the user to - recombine or relink the Application with a modified version of - the Linked Version to produce a modified Combined Work, in the - manner specified by section 6 of the GNU GPL for conveying - Corresponding Source. - - 1) Use a suitable shared library mechanism for linking with the - Library. A suitable mechanism is one that (a) uses at run time - a copy of the Library already present on the user's computer - system, and (b) will operate properly with a modified version - of the Library that is interface-compatible with the Linked - Version. - - e) Provide Installation Information, but only if you would otherwise - be required to provide such information under section 6 of the - GNU GPL, and only to the extent that such information is - necessary to install and execute a modified version of the - Combined Work produced by recombining or relinking the - Application with a modified version of the Linked Version. (If - you use option 4d0, the Installation Information must accompany - the Minimal Corresponding Source and Corresponding Application - Code. If you use option 4d1, you must provide the Installation - Information in the manner specified by section 6 of the GNU GPL - for conveying Corresponding Source.) - - 5. Combined Libraries. - - You may place library facilities that are a work based on the -Library side by side in a single library together with other library -facilities that are not Applications and are not covered by this -License, and convey such a combined library under terms of your -choice, if you do both of the following: - - a) Accompany the combined library with a copy of the same work based - on the Library, uncombined with any other library facilities, - conveyed under the terms of this License. - - b) Give prominent notice with the combined library that part of it - is a work based on the Library, and explaining where to find the - accompanying uncombined form of the same work. - - 6. Revised Versions of the GNU Lesser General Public License. - - The Free Software Foundation may publish revised and/or new versions -of the GNU Lesser General Public License from time to time. Such new -versions will be similar in spirit to the present version, but may -differ in detail to address new problems or concerns. - - Each version is given a distinguishing version number. If the -Library as you received it specifies that a certain numbered version -of the GNU Lesser General Public License "or any later version" -applies to it, you have the option of following the terms and -conditions either of that published version or of any later version -published by the Free Software Foundation. If the Library as you -received it does not specify a version number of the GNU Lesser -General Public License, you may choose any version of the GNU Lesser -General Public License ever published by the Free Software Foundation. - - If the Library as you received it specifies that a proxy can decide -whether future versions of the GNU Lesser General Public License shall -apply, that proxy's public statement of acceptance of any version is -permanent authorization for you to choose that version for the -Library. diff --git a/crates/adapters/dydx/LICENSE b/crates/adapters/dydx/LICENSE new file mode 120000 index 000000000000..5853aaea53bc --- /dev/null +++ b/crates/adapters/dydx/LICENSE @@ -0,0 +1 @@ +../../../LICENSE \ No newline at end of file From 73bfd2cbfaf12e25baa8b65c9d311c32d6464cf3 Mon Sep 17 00:00:00 2001 From: Vadim Nicolai Date: Mon, 3 Nov 2025 14:35:08 +0200 Subject: [PATCH 05/10] Implement HTTP client foundation and remove dydx-proto dependency --- Cargo.lock | 115 +------ Cargo.toml | 1 - crates/adapters/dydx/Cargo.toml | 1 - crates/adapters/dydx/src/common/consts.rs | 60 ++++ crates/adapters/dydx/src/http/client.rs | 381 ++++++++++++++++++++++ crates/adapters/dydx/src/http/error.rs | 144 ++++++++ crates/adapters/dydx/src/http/mod.rs | 33 +- crates/adapters/dydx/src/http/models.rs | 42 +++ crates/adapters/dydx/src/http/parse.rs | 41 +++ crates/adapters/dydx/src/http/query.rs | 44 +++ crates/adapters/dydx/src/lib.rs | 2 - deny.toml | 1 - 12 files changed, 736 insertions(+), 129 deletions(-) create mode 100644 crates/adapters/dydx/src/http/client.rs create mode 100644 crates/adapters/dydx/src/http/error.rs create mode 100644 crates/adapters/dydx/src/http/models.rs create mode 100644 crates/adapters/dydx/src/http/parse.rs create mode 100644 crates/adapters/dydx/src/http/query.rs diff --git a/Cargo.lock b/Cargo.lock index 012bce2a915f..db85b8dcd37d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2076,17 +2076,6 @@ dependencies = [ "tendermint-proto", ] -[[package]] -name = "cosmos-sdk-proto" -version = "0.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95ac39be7373404accccaede7cc1ec942ccef14f0ca18d209967a756bf1dbb1f" -dependencies = [ - "prost", - "tendermint-proto", - "tonic", -] - [[package]] name = "cosmrs" version = "0.21.1" @@ -2094,7 +2083,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1394c263335da09e8ba8c4b2c675d804e3e0deb44cce0866a5f838d3ddd43d02" dependencies = [ "bip32", - "cosmos-sdk-proto 0.26.1", + "cosmos-sdk-proto", "ecdsa", "eyre", "k256", @@ -2871,7 +2860,7 @@ dependencies = [ "log", "parking_lot", "paste", - "petgraph 0.8.3", + "petgraph", ] [[package]] @@ -3200,22 +3189,6 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" -[[package]] -name = "dydx-proto" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "945e74a43dd3e251849f5a87169aaa457d70d6506a27adf365a7d418c71c42d1" -dependencies = [ - "cosmos-sdk-proto 0.27.0", - "prost", - "prost-build", - "prost-types", - "regex", - "tonic", - "tonic-buf-build", - "tonic-build", -] - [[package]] name = "dyn-clone" version = "1.0.20" @@ -4842,12 +4815,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "multimap" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" - [[package]] name = "multiversion" version = "0.7.4" @@ -5267,7 +5234,6 @@ dependencies = [ "criterion", "dashmap", "derive_builder", - "dydx-proto", "futures-util", "indexmap 2.12.0", "log", @@ -6320,16 +6286,6 @@ dependencies = [ "ucd-trie", ] -[[package]] -name = "petgraph" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" -dependencies = [ - "fixedbitset", - "indexmap 2.12.0", -] - [[package]] name = "petgraph" version = "0.8.3" @@ -6751,26 +6707,6 @@ dependencies = [ "prost-derive", ] -[[package]] -name = "prost-build" -version = "0.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf" -dependencies = [ - "heck", - "itertools 0.14.0", - "log", - "multimap", - "once_cell", - "petgraph 0.7.1", - "prettyplease", - "prost", - "prost-types", - "regex", - "syn 2.0.108", - "tempfile", -] - [[package]] name = "prost-derive" version = "0.13.5" @@ -8011,19 +7947,6 @@ dependencies = [ "syn 2.0.108", ] -[[package]] -name = "serde_yaml" -version = "0.9.34+deprecated" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" -dependencies = [ - "indexmap 2.12.0", - "itoa", - "ryu", - "serde", - "unsafe-libyaml", -] - [[package]] name = "serdect" version = "0.2.0" @@ -9052,33 +8975,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "tonic-buf-build" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39018a5b437322acf53301ed58ce4768c8f263e2547fb73e47b81c805fa59395" -dependencies = [ - "scopeguard", - "serde", - "serde_yaml", - "tonic-build", - "uuid", -] - -[[package]] -name = "tonic-build" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac6f67be712d12f0b41328db3137e0d0757645d8904b4cb7d51cd9c2279e847" -dependencies = [ - "prettyplease", - "proc-macro2", - "prost-build", - "prost-types", - "quote", - "syn 2.0.108", -] - [[package]] name = "tower" version = "0.5.2" @@ -9441,12 +9337,6 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" -[[package]] -name = "unsafe-libyaml" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" - [[package]] name = "untrusted" version = "0.9.0" @@ -9510,7 +9400,6 @@ checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" dependencies = [ "getrandom 0.3.4", "js-sys", - "rand 0.9.2", "serde", "wasm-bindgen", ] diff --git a/Cargo.toml b/Cargo.toml index 3b781a00837c..325a9b05c4ae 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -142,7 +142,6 @@ derive_builder = { version = "0.20.2", default-features = false, features = [ "alloc", ] } dotenvy = "0.15.7" -dydx-proto = "0.4.0" ed25519-dalek = "2.2.0" enum_dispatch = "0.3.13" evalexpr = "=11.3.1" # Pinned to v11.3.1 for MIT licensing diff --git a/crates/adapters/dydx/Cargo.toml b/crates/adapters/dydx/Cargo.toml index db5101081e22..0b79cb901096 100644 --- a/crates/adapters/dydx/Cargo.toml +++ b/crates/adapters/dydx/Cargo.toml @@ -70,7 +70,6 @@ chrono = { workspace = true } cosmrs = "0.21" dashmap = { workspace = true } derive_builder = { workspace = true } -dydx-proto = { workspace = true } futures-util = { workspace = true } indexmap = { workspace = true } log = { workspace = true } diff --git a/crates/adapters/dydx/src/common/consts.rs b/crates/adapters/dydx/src/common/consts.rs index 0f4e118f5de4..fd4579091c09 100644 --- a/crates/adapters/dydx/src/common/consts.rs +++ b/crates/adapters/dydx/src/common/consts.rs @@ -15,6 +15,8 @@ //! Core constants shared across the dYdX adapter components. +use reqwest::StatusCode; + /// dYdX adapter name. pub const DYDX: &str = "DYDX"; @@ -23,3 +25,61 @@ pub const DYDX_CHAIN_ID: &str = "dydx-mainnet-1"; /// dYdX testnet chain ID. pub const DYDX_TESTNET_CHAIN_ID: &str = "dydx-testnet-4"; + +/// dYdX mainnet Indexer HTTP API base URL. +pub const DYDX_HTTP_URL_MAINNET: &str = "https://indexer.dydx.trade"; + +/// dYdX testnet Indexer HTTP API base URL. +pub const DYDX_HTTP_URL_TESTNET: &str = "https://indexer.v4testnet.dydx.exchange"; + +/// Determines if an HTTP status code should trigger a retry. +/// +/// Retries on: +/// - 429 (Too Many Requests) +/// - 500-599 (Server Errors) +/// +/// Does NOT retry on: +/// - 400 (Bad Request) - indicates client error that won't be fixed by retrying +/// - 401 (Unauthorized) - not applicable for dYdX Indexer (no auth required) +/// - 403 (Forbidden) - typically compliance/screening issues +/// - 404 (Not Found) - resource doesn't exist +#[must_use] +pub const fn should_retry_error_code(status: &StatusCode) -> bool { + matches!(status.as_u16(), 429 | 500..=599) +} + +//////////////////////////////////////////////////////////////////////////////// +// Tests +//////////////////////////////////////////////////////////////////////////////// + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_should_retry_429() { + assert!(should_retry_error_code(&StatusCode::TOO_MANY_REQUESTS)); + } + + #[test] + fn test_should_retry_server_errors() { + assert!(should_retry_error_code(&StatusCode::INTERNAL_SERVER_ERROR)); + assert!(should_retry_error_code(&StatusCode::BAD_GATEWAY)); + assert!(should_retry_error_code(&StatusCode::SERVICE_UNAVAILABLE)); + assert!(should_retry_error_code(&StatusCode::GATEWAY_TIMEOUT)); + } + + #[test] + fn test_should_not_retry_client_errors() { + assert!(!should_retry_error_code(&StatusCode::BAD_REQUEST)); + assert!(!should_retry_error_code(&StatusCode::UNAUTHORIZED)); + assert!(!should_retry_error_code(&StatusCode::FORBIDDEN)); + assert!(!should_retry_error_code(&StatusCode::NOT_FOUND)); + } + + #[test] + fn test_should_not_retry_success() { + assert!(!should_retry_error_code(&StatusCode::OK)); + assert!(!should_retry_error_code(&StatusCode::CREATED)); + } +} diff --git a/crates/adapters/dydx/src/http/client.rs b/crates/adapters/dydx/src/http/client.rs new file mode 100644 index 000000000000..21aa0747ce2a --- /dev/null +++ b/crates/adapters/dydx/src/http/client.rs @@ -0,0 +1,381 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved. +// https://nautechsystems.io +// +// Licensed under the GNU Lesser General Public License Version 3.0 (the "License"); +// You may not use this file except in compliance with the License. +// You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------------------------------- + +//! Provides an ergonomic wrapper around the **dYdX v4 Indexer REST API** – +//! . +//! +//! The core type exported by this module is [`DydxRawHttpClient`]. It offers an +//! interface to all exchange endpoints currently required by NautilusTrader. +//! +//! Key responsibilities handled internally: +//! • Rate-limiting based on the public dYdX specification. +//! • Zero-copy deserialization of large JSON payloads into domain models. +//! • Conversion of raw exchange errors into the rich [`DydxHttpError`] enum. +//! +//! # Important Note +//! +//! The dYdX v4 Indexer REST API does **NOT** require authentication or request signing. +//! All endpoints are publicly accessible using only wallet addresses and subaccount numbers +//! as query parameters. Order submission and trading operations use gRPC with blockchain +//! transaction signing, not REST API. +//! +//! # Official documentation +//! +//! | Endpoint | Reference | +//! |--------------------------------------|--------------------------------------------------------| +//! | Market data | | +//! | Account data | | +//! | Utility endpoints | | + +use std::{fmt::Debug, num::NonZeroU32, sync::LazyLock}; + +use nautilus_core::consts::NAUTILUS_USER_AGENT; +use nautilus_network::{ + http::HttpClient, + ratelimiter::quota::Quota, + retry::{RetryConfig, RetryManager}, +}; +use reqwest::{Method, header::USER_AGENT}; +use serde::{Deserialize, Serialize, de::DeserializeOwned}; +use tokio_util::sync::CancellationToken; + +use super::error::DydxHttpError; +use crate::common::consts::DYDX_HTTP_URL_MAINNET; + +/// Default dYdX Indexer REST API rate limit. +/// +/// The dYdX Indexer API rate limits are generous for read-only operations: +/// - General: 100 requests per 10 seconds per IP +/// - We use a conservative 10 requests per second as the default quota. +pub static DYDX_REST_QUOTA: LazyLock = + LazyLock::new(|| Quota::per_second(NonZeroU32::new(10).unwrap())); + +/// Represents a dYdX HTTP response wrapper. +/// +/// Most dYdX Indexer API endpoints return data directly without a wrapper, +/// but some endpoints may use this structure for consistency. +#[derive(Debug, Serialize, Deserialize)] +pub struct DydxResponse { + /// The typed data returned by the dYdX endpoint. + pub data: T, +} + +/// Provides a raw HTTP client for interacting with the [dYdX v4](https://dydx.exchange) Indexer REST API. +/// +/// This client wraps the underlying [`HttpClient`] to handle functionality +/// specific to dYdX Indexer API, such as rate-limiting, forming request URLs, +/// and deserializing responses into dYdX specific data models. +/// +/// **Note**: Unlike traditional centralized exchanges, the dYdX v4 Indexer REST API +/// does NOT require authentication, API keys, or request signing. All endpoints are +/// publicly accessible. +pub struct DydxRawHttpClient { + base_url: String, + client: HttpClient, + retry_manager: RetryManager, + cancellation_token: CancellationToken, + is_testnet: bool, +} + +impl Default for DydxRawHttpClient { + fn default() -> Self { + Self::new(None, Some(60), None, false, None) + .expect("Failed to create default DydxRawHttpClient") + } +} + +impl Debug for DydxRawHttpClient { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct(stringify!(DydxRawHttpClient)) + .field("base_url", &self.base_url) + .field("is_testnet", &self.is_testnet) + .finish_non_exhaustive() + } +} + +impl DydxRawHttpClient { + /// Cancel all pending HTTP requests. + pub fn cancel_all_requests(&self) { + self.cancellation_token.cancel(); + } + + /// Get the cancellation token for this client. + pub fn cancellation_token(&self) -> &CancellationToken { + &self.cancellation_token + } + + /// Creates a new [`DydxRawHttpClient`] using the default dYdX Indexer HTTP URL, + /// optionally overridden with a custom base URL. + /// + /// **Note**: No credentials are required as the dYdX Indexer API is publicly accessible. + /// + /// # Errors + /// + /// Returns an error if the retry manager cannot be created. + pub fn new( + base_url: Option, + timeout_secs: Option, + proxy_url: Option, + is_testnet: bool, + retry_config: Option, + ) -> anyhow::Result { + let base_url = base_url.unwrap_or_else(|| DYDX_HTTP_URL_MAINNET.to_string()); + + let retry_manager = RetryManager::new(retry_config.unwrap_or_default()) + .map_err(|e| DydxHttpError::ValidationError(e.to_string()))?; + + // Build headers + let mut headers = std::collections::HashMap::new(); + headers.insert(USER_AGENT.to_string(), NAUTILUS_USER_AGENT.to_string()); + + let client = HttpClient::new( + headers, + vec![], // No specific headers to extract from responses + vec![], // No keyed quotas (we use a single global quota) + Some(*DYDX_REST_QUOTA), + timeout_secs, + proxy_url, + ) + .map_err(|e| { + DydxHttpError::ValidationError(format!("Failed to create HTTP client: {e}")) + })?; + + Ok(Self { + base_url, + client, + retry_manager, + cancellation_token: CancellationToken::new(), + is_testnet, + }) + } + + /// Check if this client is configured for testnet. + #[must_use] + pub const fn is_testnet(&self) -> bool { + self.is_testnet + } + + /// Get the base URL being used by this client. + #[must_use] + pub fn base_url(&self) -> &str { + &self.base_url + } + + /// Send a request to a dYdX Indexer API endpoint. + /// + /// **Note**: dYdX Indexer API does not require authentication headers. + /// + /// # Errors + /// + /// Returns an error if: + /// - The HTTP request fails + /// - The response has a non-success HTTP status code + /// - The response body cannot be deserialized to type `T` + /// - The request is canceled + pub async fn send_request( + &self, + method: Method, + endpoint: &str, + query_params: Option<&str>, + ) -> Result + where + T: DeserializeOwned, + { + let url = if let Some(params) = query_params { + format!("{}{endpoint}?{params}", self.base_url) + } else { + format!("{}{endpoint}", self.base_url) + }; + + let operation = || async { + let request = self + .client + .request_with_ustr_keys( + method.clone(), + url.clone(), + None, // No additional headers + None, // No body for GET requests + None, // Use default timeout + None, // No specific rate limit keys (using global quota) + ) + .await + .map_err(|e| DydxHttpError::HttpClientError(e.to_string()))?; + + // Check for HTTP errors + if !request.status.is_success() { + return Err(DydxHttpError::HttpStatus { + status: request.status.as_u16(), + message: String::from_utf8_lossy(&request.body).to_string(), + }); + } + + Ok(request) + }; + + // Retry strategy for dYdX Indexer API: + // 1. Network errors: always retry (transient connection issues) + // 2. HTTP 429/5xx: rate limiting and server errors should be retried + // 3. Client errors (4xx except 429): should NOT be retried + let should_retry = |error: &DydxHttpError| -> bool { + match error { + DydxHttpError::HttpClientError(_) => true, + DydxHttpError::HttpStatus { status, .. } => *status == 429 || *status >= 500, + _ => false, + } + }; + + let create_error = |msg: String| -> DydxHttpError { + if msg == "canceled" { + DydxHttpError::Canceled("Adapter disconnecting or shutting down".to_string()) + } else { + DydxHttpError::ValidationError(msg) + } + }; + + // Execute request with retry logic + let response = self + .retry_manager + .execute_with_retry_with_cancel( + endpoint, + operation, + should_retry, + create_error, + &self.cancellation_token, + ) + .await?; + + // Deserialize response + serde_json::from_slice(&response.body).map_err(|e| DydxHttpError::Deserialization { + error: e.to_string(), + body: String::from_utf8_lossy(&response.body).to_string(), + }) + } + + /// Send a POST request to a dYdX Indexer API endpoint. + /// + /// Note: Most dYdX Indexer endpoints are GET-based. POST is rarely used. + /// + /// # Errors + /// + /// Returns an error if: + /// - The request body cannot be serialized to JSON + /// - The HTTP request fails + /// - The response has a non-success HTTP status code + /// - The response body cannot be deserialized to type `T` + /// - The request is canceled + pub async fn send_post_request( + &self, + endpoint: &str, + body: &B, + ) -> Result + where + T: DeserializeOwned, + B: Serialize, + { + let url = format!("{}{endpoint}", self.base_url); + + let body_bytes = serde_json::to_vec(body).map_err(|e| DydxHttpError::Serialization { + error: e.to_string(), + })?; + + let operation = || async { + let request = self + .client + .request_with_ustr_keys( + Method::POST, + url.clone(), + None, // No additional headers (content-type handled by body) + Some(body_bytes.clone()), + None, // Use default timeout + None, // No specific rate limit keys (using global quota) + ) + .await + .map_err(|e| DydxHttpError::HttpClientError(e.to_string()))?; + + // Check for HTTP errors + if !request.status.is_success() { + return Err(DydxHttpError::HttpStatus { + status: request.status.as_u16(), + message: String::from_utf8_lossy(&request.body).to_string(), + }); + } + + Ok(request) + }; + + // Retry strategy (same as GET requests) + let should_retry = |error: &DydxHttpError| -> bool { + match error { + DydxHttpError::HttpClientError(_) => true, + DydxHttpError::HttpStatus { status, .. } => *status == 429 || *status >= 500, + _ => false, + } + }; + + let create_error = |msg: String| -> DydxHttpError { + if msg == "canceled" { + DydxHttpError::Canceled("Adapter disconnecting or shutting down".to_string()) + } else { + DydxHttpError::ValidationError(msg) + } + }; + + // Execute request with retry logic + let response = self + .retry_manager + .execute_with_retry_with_cancel( + endpoint, + operation, + should_retry, + create_error, + &self.cancellation_token, + ) + .await?; + + // Deserialize response + serde_json::from_slice(&response.body).map_err(|e| DydxHttpError::Deserialization { + error: e.to_string(), + body: String::from_utf8_lossy(&response.body).to_string(), + }) + } +} + +//////////////////////////////////////////////////////////////////////////////// +// Tests +//////////////////////////////////////////////////////////////////////////////// + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_client_creation() { + let client = DydxRawHttpClient::new(None, Some(30), None, false, None); + assert!(client.is_ok()); + + let client = client.unwrap(); + assert!(!client.is_testnet()); + assert_eq!(client.base_url(), DYDX_HTTP_URL_MAINNET); + } + + #[tokio::test] + async fn test_testnet_client() { + let client = DydxRawHttpClient::new(None, Some(30), None, true, None); + assert!(client.is_ok()); + + let client = client.unwrap(); + assert!(client.is_testnet()); + } +} diff --git a/crates/adapters/dydx/src/http/error.rs b/crates/adapters/dydx/src/http/error.rs new file mode 100644 index 000000000000..ff74a99eb2f2 --- /dev/null +++ b/crates/adapters/dydx/src/http/error.rs @@ -0,0 +1,144 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved. +// https://nautechsystems.io +// +// Licensed under the GNU Lesser General Public License Version 3.0 (the "License"); +// You may not use this file except in compliance with the License. +// You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------------------------------- + +//! Error structures and enumerations for the dYdX integration. +//! +//! The dYdX v4 Indexer API error responses are typically returned with +//! appropriate HTTP status codes and error messages in the response body. + +use serde::Deserialize; +use thiserror::Error; + +/// Represents a build error for query parameter validation. +#[derive(Debug, Error)] +pub enum BuildError { + /// Missing required address parameter. + #[error("Missing required address parameter")] + MissingAddress, + /// Missing required market ticker parameter. + #[error("Missing required market ticker parameter")] + MissingTicker, + /// Missing required subaccount number. + #[error("Missing required subaccount number")] + MissingSubaccountNumber, + /// Both createdBeforeOrAt and createdBeforeOrAtHeight specified. + #[error("Cannot specify both 'createdBeforeOrAt' and 'createdBeforeOrAtHeight' parameters")] + BothCreatedBeforeParams, + /// Both createdOnOrAfter and createdOnOrAfterHeight specified. + #[error("Cannot specify both 'createdOnOrAfter' and 'createdOnOrAfterHeight' parameters")] + BothCreatedAfterParams, + /// Invalid time range. + #[error("Invalid time range: from_iso must be before to_iso")] + InvalidTimeRange, + /// Limit exceeds maximum allowed value. + #[error("Limit exceeds maximum allowed value")] + LimitTooHigh, + /// Invalid resolution parameter. + #[error("Invalid resolution parameter: {0}")] + InvalidResolution(String), +} + +/// Represents the JSON structure of an error response returned by the dYdX Indexer API. +#[derive(Clone, Debug, Deserialize)] +pub struct DydxErrorResponse { + /// HTTP status code. + #[serde(default)] + pub status: Option, + /// Error message describing what went wrong. + pub message: String, + /// Additional error details if provided. + #[serde(default)] + pub details: Option, +} + +/// A typed error enumeration for the dYdX HTTP client. +#[derive(Debug, Error)] +pub enum DydxHttpError { + /// Errors returned by the dYdX Indexer API with a specific HTTP status. + #[error("dYdX API error {status}: {message}")] + HttpStatus { status: u16, message: String }, + /// Failure during JSON serialization. + #[error("Serialization error: {error}")] + Serialization { error: String }, + /// Failure during JSON deserialization. + #[error("Deserialization error: {error}, body: {body}")] + Deserialization { error: String, body: String }, + /// Parameter validation error. + #[error("Parameter validation error: {0}")] + ValidationError(String), + /// Request was canceled, typically due to shutdown or disconnect. + #[error("Request canceled: {0}")] + Canceled(String), + /// Wrapping the underlying HttpClientError from the network crate. + #[error("Network error: {0}")] + HttpClientError(String), + /// Any unknown HTTP status or unexpected response from dYdX. + #[error("Unexpected HTTP status code {status}: {body}")] + UnexpectedStatus { status: u16, body: String }, +} + +impl From for DydxHttpError { + fn from(error: String) -> Self { + Self::ValidationError(error) + } +} + +impl From for DydxHttpError { + fn from(error: BuildError) -> Self { + Self::ValidationError(error.to_string()) + } +} + +//////////////////////////////////////////////////////////////////////////////// +// Tests +//////////////////////////////////////////////////////////////////////////////// + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_build_error_display() { + let error = BuildError::MissingAddress; + assert_eq!(error.to_string(), "Missing required address parameter"); + + let error = BuildError::MissingTicker; + assert_eq!( + error.to_string(), + "Missing required market ticker parameter" + ); + } + + #[test] + fn test_dydx_http_error_from_string() { + let error: DydxHttpError = "Invalid parameter".to_string().into(); + match error { + DydxHttpError::ValidationError(msg) => assert_eq!(msg, "Invalid parameter"), + _ => panic!("Expected ValidationError"), + } + } + + #[test] + fn test_dydx_http_error_from_build_error() { + let build_error = BuildError::MissingSubaccountNumber; + let http_error: DydxHttpError = build_error.into(); + match http_error { + DydxHttpError::ValidationError(msg) => { + assert_eq!(msg, "Missing required subaccount number"); + } + _ => panic!("Expected ValidationError"), + } + } +} diff --git a/crates/adapters/dydx/src/http/mod.rs b/crates/adapters/dydx/src/http/mod.rs index 1300f8ad2ab8..f67289b48d63 100644 --- a/crates/adapters/dydx/src/http/mod.rs +++ b/crates/adapters/dydx/src/http/mod.rs @@ -13,17 +13,28 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -//! HTTP client bindings for the dYdX adapter. +//! HTTP/REST client implementation for the dYdX v4 Indexer API. //! -//! This module provides an HTTP client for interacting with the dYdX v4 Indexer REST API. -//! It handles: -//! - Request signing and authentication. +//! This module provides an HTTP client for interacting with dYdX's Indexer REST endpoints, including: +//! +//! - Market data queries (perpetual markets, trades, candles). +//! - Account information (subaccounts, positions, fills). +//! - Order queries and historical data. //! - Rate limiting and retry logic. -//! - Request/response models. -//! - Parsing dYdX data into Nautilus domain models. //! -//! The client supports dYdX REST endpoints including: -//! - Market data (instruments, trades, candles). -//! - Account data (subaccounts, positions, fills). -//! - Order management queries. -//! - Historical data. +//! # Important Note +//! +//! The dYdX v4 Indexer REST API is **publicly accessible** and does NOT require +//! authentication or request signing. All endpoints use wallet addresses and subaccount +//! numbers as query parameters. Order submission and trading operations use gRPC with +//! blockchain transaction signing, not REST API. +//! +//! # Official documentation +//! +//! See: + +pub mod client; +pub mod error; +pub mod models; +pub mod parse; +pub mod query; diff --git a/crates/adapters/dydx/src/http/models.rs b/crates/adapters/dydx/src/http/models.rs new file mode 100644 index 000000000000..8905fd9cb24e --- /dev/null +++ b/crates/adapters/dydx/src/http/models.rs @@ -0,0 +1,42 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved. +// https://nautechsystems.io +// +// Licensed under the GNU Lesser General Public License Version 3.0 (the "License"); +// You may not use this file except in compliance with the License. +// You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------------------------------- + +//! Data models for dYdX v4 Indexer REST API responses. +//! +//! This module contains Rust types that mirror the JSON structures returned +//! by the dYdX v4 Indexer API endpoints. + +use serde::{Deserialize, Serialize}; + +// TODO: Add data models for: +// - Markets (PerpetualMarketResponse, OrderBookResponse, TradeResponse, CandleResponse) +// - Accounts (SubaccountResponse, PositionResponse, OrderResponse, FillResponse) +// - Utility (TimeResponse, HeightResponse, ComplianceResponse) + +/// Placeholder for market data models. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DydxMarketData { + /// Market ticker symbol. + pub ticker: String, +} + +/// Placeholder for account data models. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DydxAccountData { + /// Wallet address. + pub address: String, + /// Subaccount number. + pub subaccount_number: u32, +} diff --git a/crates/adapters/dydx/src/http/parse.rs b/crates/adapters/dydx/src/http/parse.rs new file mode 100644 index 000000000000..3ac2fae96581 --- /dev/null +++ b/crates/adapters/dydx/src/http/parse.rs @@ -0,0 +1,41 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved. +// https://nautechsystems.io +// +// Licensed under the GNU Lesser General Public License Version 3.0 (the "License"); +// You may not use this file except in compliance with the License. +// You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------------------------------- + +//! Parsing utilities for converting dYdX v4 Indexer API responses into Nautilus domain models. +//! +//! This module contains functions that transform raw JSON data structures +//! from the dYdX Indexer API into strongly-typed Nautilus data types such as +//! instruments, trades, bars, account states, etc. + +use super::models::DydxMarketData; + +// TODO: Implement parsing functions for: +// - parse_instrument_any: Convert dYdX perpetual market to InstrumentAny +// - parse_trade_tick: Convert dYdX trade to TradeTick +// - parse_candlestick: Convert dYdX candle to Bar +// - parse_account_state: Convert dYdX subaccount to AccountState +// - parse_order_status_report: Convert dYdX order to OrderStatusReport +// - parse_position_status_report: Convert dYdX position to PositionStatusReport +// - parse_fill_report: Convert dYdX fill to FillReport + +/// Placeholder parsing function. +/// +/// # Errors +/// +/// Returns an error if parsing fails. +pub fn parse_market_data(_data: &DydxMarketData) -> anyhow::Result<()> { + // TODO: Implement actual parsing logic + Ok(()) +} diff --git a/crates/adapters/dydx/src/http/query.rs b/crates/adapters/dydx/src/http/query.rs new file mode 100644 index 000000000000..315c189d5bda --- /dev/null +++ b/crates/adapters/dydx/src/http/query.rs @@ -0,0 +1,44 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved. +// https://nautechsystems.io +// +// Licensed under the GNU Lesser General Public License Version 3.0 (the "License"); +// You may not use this file except in compliance with the License. +// You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------------------------------- + +//! Query parameter builders for dYdX v4 Indexer REST API endpoints. +//! +//! This module provides type-safe builders for constructing query parameters +//! that are sent to various dYdX Indexer API endpoints. + +use derive_builder::Builder; +use serde::Serialize; + +// TODO: Add query parameter builders for: +// - GetPerpetualMarketsParams +// - GetOrderbookParams +// - GetTradesParams +// - GetCandlesParams +// - GetSubaccountParams +// - GetOrdersParams +// - GetFillsParams +// etc. + +/// Query parameters for fetching markets from the dYdX Indexer API. +#[derive(Debug, Clone, Default, Serialize, Builder)] +#[builder(setter(into, strip_option), default)] +pub struct GetMarketsParams { + /// Optional ticker filter. + #[serde(skip_serializing_if = "Option::is_none")] + pub ticker: Option, + /// Optional limit for number of results. + #[serde(skip_serializing_if = "Option::is_none")] + pub limit: Option, +} diff --git a/crates/adapters/dydx/src/lib.rs b/crates/adapters/dydx/src/lib.rs index 5729578b50aa..b1e43a01f04e 100644 --- a/crates/adapters/dydx/src/lib.rs +++ b/crates/adapters/dydx/src/lib.rs @@ -53,9 +53,7 @@ pub mod common; pub mod config; pub mod error; -pub mod grpc; pub mod http; -pub mod proto; #[cfg(feature = "python")] pub mod python; diff --git a/deny.toml b/deny.toml index 3fa3fad68012..6fb7c316bf8f 100644 --- a/deny.toml +++ b/deny.toml @@ -65,7 +65,6 @@ allow = [ "LGPL-3.0-or-later", "OpenSSL", "Unlicense", - "LicenseRef-dYdX-Custom", # dydx-proto custom license (Apache-2.0 based) ] confidence-threshold = 0.8 From 683e7c8b6d7d501874d02759d5537e9ff59c3495 Mon Sep 17 00:00:00 2001 From: Vadim Nicolai Date: Mon, 3 Nov 2025 15:19:07 +0200 Subject: [PATCH 06/10] Implement HTTP client foundation --- crates/adapters/dydx/src/common/consts.rs | 6 ------ crates/adapters/dydx/src/http/client.rs | 11 ++++++++--- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/crates/adapters/dydx/src/common/consts.rs b/crates/adapters/dydx/src/common/consts.rs index fd4579091c09..048231bbd2f1 100644 --- a/crates/adapters/dydx/src/common/consts.rs +++ b/crates/adapters/dydx/src/common/consts.rs @@ -26,12 +26,6 @@ pub const DYDX_CHAIN_ID: &str = "dydx-mainnet-1"; /// dYdX testnet chain ID. pub const DYDX_TESTNET_CHAIN_ID: &str = "dydx-testnet-4"; -/// dYdX mainnet Indexer HTTP API base URL. -pub const DYDX_HTTP_URL_MAINNET: &str = "https://indexer.dydx.trade"; - -/// dYdX testnet Indexer HTTP API base URL. -pub const DYDX_HTTP_URL_TESTNET: &str = "https://indexer.v4testnet.dydx.exchange"; - /// Determines if an HTTP status code should trigger a retry. /// /// Retries on: diff --git a/crates/adapters/dydx/src/http/client.rs b/crates/adapters/dydx/src/http/client.rs index 21aa0747ce2a..2329c3b413cd 100644 --- a/crates/adapters/dydx/src/http/client.rs +++ b/crates/adapters/dydx/src/http/client.rs @@ -52,7 +52,7 @@ use serde::{Deserialize, Serialize, de::DeserializeOwned}; use tokio_util::sync::CancellationToken; use super::error::DydxHttpError; -use crate::common::consts::DYDX_HTTP_URL_MAINNET; +use crate::common::urls::{DYDX_HTTP_URL, DYDX_TESTNET_HTTP_URL}; /// Default dYdX Indexer REST API rate limit. /// @@ -131,7 +131,11 @@ impl DydxRawHttpClient { is_testnet: bool, retry_config: Option, ) -> anyhow::Result { - let base_url = base_url.unwrap_or_else(|| DYDX_HTTP_URL_MAINNET.to_string()); + let base_url = if is_testnet { + base_url.unwrap_or_else(|| DYDX_TESTNET_HTTP_URL.to_string()) + } else { + base_url.unwrap_or_else(|| DYDX_HTTP_URL.to_string()) + }; let retry_manager = RetryManager::new(retry_config.unwrap_or_default()) .map_err(|e| DydxHttpError::ValidationError(e.to_string()))?; @@ -367,7 +371,7 @@ mod tests { let client = client.unwrap(); assert!(!client.is_testnet()); - assert_eq!(client.base_url(), DYDX_HTTP_URL_MAINNET); + assert_eq!(client.base_url(), DYDX_HTTP_URL); } #[tokio::test] @@ -377,5 +381,6 @@ mod tests { let client = client.unwrap(); assert!(client.is_testnet()); + assert_eq!(client.base_url(), DYDX_TESTNET_HTTP_URL); } } From 4e8692497e3c398b9096d846d76665c2659fea5f Mon Sep 17 00:00:00 2001 From: Vadim Nicolai Date: Tue, 4 Nov 2025 09:56:03 +0200 Subject: [PATCH 07/10] Change requests applied --- Cargo.lock | 7 ------- crates/adapters/dydx/Cargo.toml | 7 ------- crates/adapters/dydx/src/grpc/client.rs | 22 +++++----------------- crates/adapters/dydx/src/grpc/order.rs | 24 ++++++++++++------------ crates/adapters/dydx/src/proto/mod.rs | 9 +++------ 5 files changed, 20 insertions(+), 49 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index db85b8dcd37d..e867e0805e38 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5225,14 +5225,11 @@ dependencies = [ "anyhow", "async-stream", "async-trait", - "aws-lc-rs", "axum", - "base64 0.22.1", "bip32", "chrono", "cosmrs", "criterion", - "dashmap", "derive_builder", "futures-util", "indexmap 2.12.0", @@ -5255,19 +5252,15 @@ dependencies = [ "rust_decimal_macros", "serde", "serde_json", - "serde_urlencoded", "strum 0.27.2", "thiserror 2.0.17", "tokio", - "tokio-tungstenite", "tokio-util", "tonic", "tracing", - "tracing-subscriber", "tracing-test", "url", "ustr", - "zeroize", ] [[package]] diff --git a/crates/adapters/dydx/Cargo.toml b/crates/adapters/dydx/Cargo.toml index 0b79cb901096..4be5a9a14d99 100644 --- a/crates/adapters/dydx/Cargo.toml +++ b/crates/adapters/dydx/Cargo.toml @@ -59,8 +59,6 @@ ahash = { workspace = true } anyhow = { workspace = true } async-stream = { workspace = true } async-trait = { workspace = true } -aws-lc-rs = { workspace = true } -base64 = { workspace = true } bip32 = { version = "0.5", default-features = false, features = [ "bip39", "alloc", @@ -68,7 +66,6 @@ bip32 = { version = "0.5", default-features = false, features = [ ] } chrono = { workspace = true } cosmrs = "0.21" -dashmap = { workspace = true } derive_builder = { workspace = true } futures-util = { workspace = true } indexmap = { workspace = true } @@ -79,17 +76,13 @@ reqwest = { workspace = true } rust_decimal = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } -serde_urlencoded = { workspace = true } strum = { workspace = true } thiserror = { workspace = true } tokio = { workspace = true } -tokio-tungstenite = { workspace = true } tokio-util = { workspace = true } tonic = "0.13" tracing = { workspace = true } -tracing-subscriber = { workspace = true } # Needed for example binaries ustr = { workspace = true } -zeroize = { workspace = true } pyo3 = { workspace = true, optional = true } pyo3-async-runtimes = { workspace = true, optional = true } diff --git a/crates/adapters/dydx/src/grpc/client.rs b/crates/adapters/dydx/src/grpc/client.rs index 8a6ff060a633..10d7b5e56808 100644 --- a/crates/adapters/dydx/src/grpc/client.rs +++ b/crates/adapters/dydx/src/grpc/client.rs @@ -18,7 +18,11 @@ //! This module provides the main gRPC client for interacting with dYdX v4 validator nodes. //! It handles transaction signing, broadcasting, and querying account state. -use dydx_proto::{ +use prost::Message as ProstMessage; +use tonic::transport::Channel; + +use crate::error::DydxError; +use crate::proto::{ cosmos_sdk_proto::cosmos::{ auth::v1beta1::{ BaseAccount, QueryAccountRequest, query_client::QueryClient as AuthClient, @@ -47,10 +51,6 @@ use dydx_proto::{ }, }, }; -use prost::Message as ProstMessage; -use tonic::transport::Channel; - -use crate::error::DydxError; /// Transaction hash type (internally uses tendermint::Hash). pub type TxHash = String; @@ -65,18 +65,6 @@ pub struct Height(pub u32); /// - Transaction signing and broadcasting. /// - Account query operations. /// - Order placement and management via Cosmos SDK messages. -/// -/// # Examples -/// -/// ```no_run -/// use nautilus_dydx::grpc::DydxGrpcClient; -/// -/// # async fn example() -> Result<(), Box> { -/// let grpc_url = "https://dydx-grpc.publicnode.com:443".to_string(); -/// let client = DydxGrpcClient::new(grpc_url).await?; -/// # Ok(()) -/// # } -/// ``` #[derive(Debug, Clone)] pub struct DydxGrpcClient { channel: Channel, diff --git a/crates/adapters/dydx/src/grpc/order.rs b/crates/adapters/dydx/src/grpc/order.rs index d17c95ed1ee7..fec274922b51 100644 --- a/crates/adapters/dydx/src/grpc/order.rs +++ b/crates/adapters/dydx/src/grpc/order.rs @@ -24,12 +24,16 @@ //! See [dYdX order types](https://help.dydx.trade/en/articles/166985-short-term-vs-long-term-order-types). use chrono::{DateTime, Utc}; -use dydx_proto::dydxprotocol::clob::{ - Order, OrderId, - order::{ConditionType, Side as OrderSide, TimeInForce as OrderTimeInForce}, -}; use rust_decimal::{Decimal, prelude::ToPrimitive}; +use crate::proto::dydxprotocol::{ + clob::{ + Order, OrderId, + order::{ConditionType, GoodTilOneof, Side as OrderSide, TimeInForce as OrderTimeInForce}, + }, + subaccounts::SubaccountId, +}; + /// Maximum short-term order lifetime in blocks. /// /// See also [short-term vs long-term orders](https://help.dydx.trade/en/articles/166985-short-term-vs-long-term-order-types). @@ -355,7 +359,7 @@ impl OrderBuilder { // Build order ID let order_id = Some(OrderId { - subaccount_id: Some(dydx_proto::dydxprotocol::subaccounts::SubaccountId { + subaccount_id: Some(SubaccountId { owner: self.subaccount_owner.clone(), number: self.subaccount_number, }), @@ -371,14 +375,10 @@ impl OrderBuilder { // Set good til oneof let good_til_oneof = if let Some(until) = self.until { match until { - OrderGoodUntil::Block(height) => { - Some(dydx_proto::dydxprotocol::clob::order::GoodTilOneof::GoodTilBlock(height)) + OrderGoodUntil::Block(height) => Some(GoodTilOneof::GoodTilBlock(height)), + OrderGoodUntil::Time(time) => { + Some(GoodTilOneof::GoodTilBlockTime(time.timestamp().try_into()?)) } - OrderGoodUntil::Time(time) => Some( - dydx_proto::dydxprotocol::clob::order::GoodTilOneof::GoodTilBlockTime( - time.timestamp().try_into()?, - ), - ), } } else { None diff --git a/crates/adapters/dydx/src/proto/mod.rs b/crates/adapters/dydx/src/proto/mod.rs index 22c1c9ea8b27..8b84131bca35 100644 --- a/crates/adapters/dydx/src/proto/mod.rs +++ b/crates/adapters/dydx/src/proto/mod.rs @@ -13,10 +13,7 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -//! Re-exports of dYdX Protocol Buffer definitions. +//! Protocol Buffer definitions for dYdX v4. //! -//! This module provides convenient re-exports of types from the `dydx-proto` crate, -//! organized by their proto package structure. - -/// Re-export the entire dydx-proto crate for full access to all types. -pub use dydx_proto::*; +//! This module will contain generated protobuf code for the dYdX protocol. +//! Generated files will be included here once proto compilation is set up. From e3a9a5c662a3036cfc268918e61be041fd5849e4 Mon Sep 17 00:00:00 2001 From: Vadim Nicolai Date: Tue, 4 Nov 2025 20:01:52 +0200 Subject: [PATCH 08/10] Add REST client, wallet signing, and data models --- Cargo.lock | 10 +- crates/adapters/dydx/Cargo.toml | 18 +- crates/adapters/dydx/src/common/consts.rs | 24 + crates/adapters/dydx/src/common/credential.rs | 267 +++++++++ crates/adapters/dydx/src/common/enums.rs | 417 +++++++++++++ crates/adapters/dydx/src/common/mod.rs | 2 + crates/adapters/dydx/src/grpc/wallet.rs | 2 +- crates/adapters/dydx/src/http/client.rs | 186 +++++- crates/adapters/dydx/src/http/models.rs | 563 +++++++++++++++++- crates/adapters/dydx/src/http/parse.rs | 12 - crates/adapters/dydx/src/http/query.rs | 46 +- 11 files changed, 1486 insertions(+), 61 deletions(-) create mode 100644 crates/adapters/dydx/src/common/credential.rs create mode 100644 crates/adapters/dydx/src/common/enums.rs diff --git a/Cargo.lock b/Cargo.lock index e867e0805e38..22ded13e9eb5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5221,19 +5221,14 @@ dependencies = [ name = "nautilus-dydx" version = "0.52.0" dependencies = [ - "ahash 0.8.12", "anyhow", - "async-stream", - "async-trait", "axum", "bip32", "chrono", "cosmrs", "criterion", "derive_builder", - "futures-util", - "indexmap 2.12.0", - "log", + "hex", "nautilus-common", "nautilus-core", "nautilus-data", @@ -5252,15 +5247,16 @@ dependencies = [ "rust_decimal_macros", "serde", "serde_json", + "serde_with", "strum 0.27.2", "thiserror 2.0.17", "tokio", "tokio-util", "tonic", - "tracing", "tracing-test", "url", "ustr", + "zeroize", ] [[package]] diff --git a/crates/adapters/dydx/Cargo.toml b/crates/adapters/dydx/Cargo.toml index 4be5a9a14d99..dfc5f75ddadc 100644 --- a/crates/adapters/dydx/Cargo.toml +++ b/crates/adapters/dydx/Cargo.toml @@ -55,34 +55,26 @@ nautilus-live = { workspace = true } nautilus-model = { workspace = true } nautilus-network = { workspace = true } -ahash = { workspace = true } anyhow = { workspace = true } -async-stream = { workspace = true } -async-trait = { workspace = true } -bip32 = { version = "0.5", default-features = false, features = [ - "bip39", - "alloc", - "secp256k1", -] } +bip32 = "0.5" chrono = { workspace = true } -cosmrs = "0.21" +cosmrs = { version = "0.21", features = ["bip32"] } derive_builder = { workspace = true } -futures-util = { workspace = true } -indexmap = { workspace = true } -log = { workspace = true } +hex = "0.4" prost = "0.13" prost-types = "0.13" reqwest = { workspace = true } rust_decimal = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } +serde_with = "3.11" strum = { workspace = true } thiserror = { workspace = true } tokio = { workspace = true } tokio-util = { workspace = true } tonic = "0.13" -tracing = { workspace = true } ustr = { workspace = true } +zeroize = { version = "1.8", features = ["derive"] } pyo3 = { workspace = true, optional = true } pyo3-async-runtimes = { workspace = true, optional = true } diff --git a/crates/adapters/dydx/src/common/consts.rs b/crates/adapters/dydx/src/common/consts.rs index 048231bbd2f1..766cc8088835 100644 --- a/crates/adapters/dydx/src/common/consts.rs +++ b/crates/adapters/dydx/src/common/consts.rs @@ -15,17 +15,41 @@ //! Core constants shared across the dYdX adapter components. +use std::sync::LazyLock; + +use nautilus_model::identifiers::Venue; use reqwest::StatusCode; +use ustr::Ustr; /// dYdX adapter name. pub const DYDX: &str = "DYDX"; +/// dYdX venue identifier. +pub static DYDX_VENUE: LazyLock = LazyLock::new(|| Venue::new(Ustr::from(DYDX))); + /// dYdX mainnet chain ID. pub const DYDX_CHAIN_ID: &str = "dydx-mainnet-1"; /// dYdX testnet chain ID. pub const DYDX_TESTNET_CHAIN_ID: &str = "dydx-testnet-4"; +/// Cosmos SDK bech32 address prefix for dYdX. +pub const DYDX_BECH32_PREFIX: &str = "dydx"; + +/// USDC gas denomination (native chain token). +pub const USDC_GAS_DENOM: &str = + "ibc/8E27BA2D5493AF5636760E354E46004562C46AB7EC0CC4C1CA14E9E20E2545B5"; + +/// USDC asset denomination for transfers. +pub const USDC_DENOM: &str = "uusdc"; + +/// HD wallet derivation path for dYdX accounts (Cosmos SLIP-0044). +/// Format: m/44'/118'/0'/0/{account_index} +pub const DYDX_DERIVATION_PATH_PREFIX: &str = "m/44'/118'/0'/0"; + +/// Coin type for Cosmos ecosystem (SLIP-0044). +pub const COSMOS_COIN_TYPE: u32 = 118; + /// Determines if an HTTP status code should trigger a retry. /// /// Retries on: diff --git a/crates/adapters/dydx/src/common/credential.rs b/crates/adapters/dydx/src/common/credential.rs new file mode 100644 index 000000000000..73739f57b1c6 --- /dev/null +++ b/crates/adapters/dydx/src/common/credential.rs @@ -0,0 +1,267 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved. +// https://nautechsystems.io +// +// Licensed under the GNU Lesser General Public License Version 3.0 (the "License"); +// You may not use this file except in compliance with the License. +// You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------------------------------- + +//! dYdX credential storage and wallet-based transaction signing helpers. +//! +//! dYdX v4 uses Cosmos SDK-style wallet signing rather than API key authentication. +//! Trading operations require signing transactions with a secp256k1 private key. + +#![allow(unused_assignments)] // Fields are accessed externally, false positive from nightly + +use std::fmt::Debug; + +use anyhow::Context; +use cosmrs::{AccountId, crypto::secp256k1::SigningKey, tx::SignDoc}; + +use crate::common::consts::DYDX_BECH32_PREFIX; + +/// dYdX wallet credentials for signing blockchain transactions. +/// +/// Uses secp256k1 for signing as per Cosmos SDK specifications. +pub struct DydxCredential { + /// The secp256k1 signing key. + signing_key: SigningKey, + /// Bech32-encoded account address (e.g., dydx1...). + pub address: String, + /// Optional authenticator IDs for permissioned key trading. + pub authenticator_ids: Vec, +} + +impl Drop for DydxCredential { + fn drop(&mut self) { + // Note: SigningKey doesn't implement Zeroize directly + // Its memory will be securely cleared by cosmrs on drop + } +} + +impl Debug for DydxCredential { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct(stringify!(DydxCredential)) + .field("address", &self.address) + .field("authenticator_ids", &self.authenticator_ids) + .field("signing_key", &"") + .finish() + } +} + +impl DydxCredential { + /// Creates a new [`DydxCredential`] from a mnemonic phrase. + /// + /// # Arguments + /// + /// * `mnemonic` - BIP-39 mnemonic phrase (12 or 24 words) + /// * `account_index` - HD wallet account index (typically 0) + /// * `authenticator_ids` - Optional authenticator IDs for permissioned keys + /// + /// # Errors + /// + /// Returns an error if the mnemonic is invalid or key derivation fails. + pub fn from_mnemonic( + mnemonic_phrase: &str, + account_index: u32, + authenticator_ids: Vec, + ) -> anyhow::Result { + use std::str::FromStr; + + use bip32::{DerivationPath, Language, Mnemonic}; + + // Derive seed from mnemonic + let mnemonic = + Mnemonic::new(mnemonic_phrase, Language::English).context("Invalid mnemonic phrase")?; + let seed = mnemonic.to_seed(""); + + // BIP-44 derivation path: m/44'/118'/0'/0/{account_index} + // 118 is the Cosmos SLIP-0044 coin type + let derivation_path = format!("m/44'/118'/0'/0/{account_index}"); + let path = DerivationPath::from_str(&derivation_path).context("Invalid derivation path")?; + + // Derive signing key + let signing_key = + SigningKey::derive_from_path(&seed, &path).context("Failed to derive signing key")?; + + // Derive bech32 address + let public_key = signing_key.public_key(); + let account_id = public_key + .account_id(DYDX_BECH32_PREFIX) + .map_err(|e| anyhow::anyhow!("Failed to derive account ID: {}", e))?; + let address = account_id.to_string(); + + Ok(Self { + signing_key, + address, + authenticator_ids, + }) + } + + /// Creates a new [`DydxCredential`] from a raw private key. + /// + /// # Arguments + /// + /// * `private_key_hex` - Hexadecimal-encoded secp256k1 private key + /// * `authenticator_ids` - Optional authenticator IDs for permissioned keys + /// + /// # Errors + /// + /// Returns an error if private key is invalid. + pub fn from_private_key( + private_key_hex: &str, + authenticator_ids: Vec, + ) -> anyhow::Result { + // Decode hex private key + let key_bytes = hex::decode(private_key_hex.trim_start_matches("0x")) + .context("Invalid hex private key")?; + + let signing_key = SigningKey::from_slice(&key_bytes) + .map_err(|e| anyhow::anyhow!("Invalid secp256k1 private key: {}", e))?; + + // Derive bech32 address + let public_key = signing_key.public_key(); + let account_id = public_key + .account_id(DYDX_BECH32_PREFIX) + .map_err(|e| anyhow::anyhow!("Failed to derive account ID: {}", e))?; + let address = account_id.to_string(); + + Ok(Self { + signing_key, + address, + authenticator_ids, + }) + } + + /// Returns the account ID for this credential. + /// + /// # Errors + /// + /// Returns error if the address cannot be parsed as a valid account ID. + pub fn account_id(&self) -> anyhow::Result { + self.address + .parse() + .map_err(|e| anyhow::anyhow!("Failed to parse account ID: {}", e)) + } + + /// Signs a transaction SignDoc. + /// + /// This produces the signature bytes that will be included in the transaction. + /// + /// # Errors + /// + /// Returns error if SignDoc serialization or signing fails. + pub fn sign(&self, sign_doc: &SignDoc) -> anyhow::Result> { + let sign_bytes = sign_doc + .clone() + .into_bytes() + .map_err(|e| anyhow::anyhow!("Failed to serialize SignDoc: {}", e))?; + + let signature = self + .signing_key + .sign(&sign_bytes) + .map_err(|e| anyhow::anyhow!("Failed to sign: {}", e))?; + Ok(signature.to_bytes().to_vec()) + } + + /// Signs raw message bytes. + /// + /// Used for custom signing operations outside of standard transaction flow. + /// + /// # Errors + /// + /// Returns error if signing fails. + pub fn sign_bytes(&self, message: &[u8]) -> anyhow::Result> { + let signature = self + .signing_key + .sign(message) + .map_err(|e| anyhow::anyhow!("Failed to sign: {}", e))?; + Ok(signature.to_bytes().to_vec()) + } + + /// Returns the public key for this credential. + pub fn public_key(&self) -> cosmrs::crypto::PublicKey { + self.signing_key.public_key() + } +} + +//////////////////////////////////////////////////////////////////////////////// +// Tests +//////////////////////////////////////////////////////////////////////////////// + +#[cfg(test)] +mod tests { + use super::*; + + // Test mnemonic from dYdX v4 client examples + const TEST_MNEMONIC: &str = "mirror actor skill push coach wait confirm orchard lunch mobile athlete gossip awake miracle matter bus reopen team ladder lazy list timber render wait"; + + #[test] + fn test_from_mnemonic() { + let credential = DydxCredential::from_mnemonic(TEST_MNEMONIC, 0, vec![]) + .expect("Failed to create credential"); + + assert!(credential.address.starts_with("dydx")); + assert!(credential.authenticator_ids.is_empty()); + } + + #[test] + fn test_from_mnemonic_with_authenticators() { + let credential = DydxCredential::from_mnemonic(TEST_MNEMONIC, 0, vec![1, 2, 3]) + .expect("Failed to create credential"); + + assert_eq!(credential.authenticator_ids, vec![1, 2, 3]); + } + + #[test] + fn test_from_private_key() { + // Use a test private key (all zeros - not a real key) + let test_key = "0".repeat(64); + + let credential = DydxCredential::from_private_key(&test_key, vec![]) + .expect("Failed to create credential from private key"); + + assert!(credential.address.starts_with("dydx")); + assert!(credential.authenticator_ids.is_empty()); + } + + #[test] + fn test_account_id() { + let credential = DydxCredential::from_mnemonic(TEST_MNEMONIC, 0, vec![]) + .expect("Failed to create credential"); + + let account_id = credential.account_id().expect("Failed to get account ID"); + assert_eq!(account_id.to_string(), credential.address); + } + + #[test] + fn test_sign_bytes() { + let credential = DydxCredential::from_mnemonic(TEST_MNEMONIC, 0, vec![]) + .expect("Failed to create credential"); + + let message = b"test message"; + let signature = credential + .sign_bytes(message) + .expect("Failed to sign bytes"); + + // secp256k1 signatures are 64 bytes + assert_eq!(signature.len(), 64); + } + + #[test] + fn test_debug_redacts_key() { + let credential = DydxCredential::from_mnemonic(TEST_MNEMONIC, 0, vec![]) + .expect("Failed to create credential"); + + let debug_str = format!("{:?}", credential); + assert!(debug_str.contains("")); + assert!(!debug_str.contains("signing_key")); + } +} diff --git a/crates/adapters/dydx/src/common/enums.rs b/crates/adapters/dydx/src/common/enums.rs new file mode 100644 index 000000000000..2ece8f7db43a --- /dev/null +++ b/crates/adapters/dydx/src/common/enums.rs @@ -0,0 +1,417 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved. +// https://nautechsystems.io +// +// Licensed under the GNU Lesser General Public License Version 3.0 (the "License"); +// You may not use this file except in compliance with the License. +// You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------------------------------- + +//! Enumerations mapping dYdX v4 concepts onto idiomatic Nautilus variants. + +use nautilus_model::enums::{LiquiditySide, OrderSide, OrderStatus, PositionSide}; +use serde::{Deserialize, Serialize}; +use strum::{AsRefStr, Display, EnumIter, EnumString}; + +/// Represents the side of an order or trade (Buy/Sell). +#[derive( + Copy, + Clone, + Debug, + Display, + PartialEq, + Eq, + Hash, + AsRefStr, + EnumIter, + EnumString, + Serialize, + Deserialize, +)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum DydxOrderSide { + /// Buy side of a trade or order. + Buy, + /// Sell side of a trade or order. + Sell, +} + +impl From for DydxOrderSide { + fn from(value: OrderSide) -> Self { + match value { + OrderSide::Buy => Self::Buy, + OrderSide::Sell => Self::Sell, + OrderSide::NoOrderSide => Self::Buy, // Default fallback + } + } +} + +impl From for OrderSide { + fn from(value: DydxOrderSide) -> Self { + match value { + DydxOrderSide::Buy => Self::Buy, + DydxOrderSide::Sell => Self::Sell, + } + } +} + +/// dYdX order status throughout its lifecycle. +#[derive( + Copy, + Clone, + Debug, + Display, + PartialEq, + Eq, + Hash, + AsRefStr, + EnumIter, + EnumString, + Serialize, + Deserialize, +)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum DydxOrderStatus { + /// Order is open and active. + Open, + /// Order is filled completely. + Filled, + /// Order is canceled. + Canceled, + /// Order is best effort canceled (short-term orders). + BestEffortCanceled, + /// Order is partially filled. + PartiallyFilled, + /// Order is best effort opened (pending confirmation). + BestEffortOpened, + /// Order is untriggered (conditional orders). + Untriggered, +} + +impl From for OrderStatus { + fn from(value: DydxOrderStatus) -> Self { + match value { + DydxOrderStatus::Open | DydxOrderStatus::BestEffortOpened => Self::Accepted, + DydxOrderStatus::PartiallyFilled => Self::PartiallyFilled, + DydxOrderStatus::Filled => Self::Filled, + DydxOrderStatus::Canceled | DydxOrderStatus::BestEffortCanceled => Self::Canceled, + DydxOrderStatus::Untriggered => Self::PendingUpdate, + } + } +} + +/// dYdX time-in-force specifications. +#[derive( + Copy, + Clone, + Debug, + Display, + PartialEq, + Eq, + Hash, + AsRefStr, + EnumIter, + EnumString, + Serialize, + Deserialize, +)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum DydxTimeInForce { + /// Good-Til-Time (GTT) - order expires at specified time. + Gtt, + /// Fill-Or-Kill (FOK) - must fill completely immediately or cancel. + Fok, + /// Immediate-Or-Cancel (IOC) - fill immediately, cancel remainder. + Ioc, +} + +/// dYdX order type. +#[derive( + Copy, + Clone, + Debug, + Display, + PartialEq, + Eq, + Hash, + AsRefStr, + EnumIter, + EnumString, + Serialize, + Deserialize, +)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum DydxOrderType { + /// Limit order with specified price. + Limit, + /// Market order (executed at best available price). + Market, + /// Stop-limit order (triggered at stop price, executed as limit). + StopLimit, + /// Stop-market order (triggered at stop price, executed as market). + StopMarket, + /// Take-profit order (limit). + TakeProfitLimit, + /// Take-profit order (market). + TakeProfitMarket, + /// Trailing stop order. + TrailingStop, +} + +/// dYdX position status. +#[derive( + Copy, + Clone, + Debug, + Display, + PartialEq, + Eq, + Hash, + AsRefStr, + EnumIter, + EnumString, + Serialize, + Deserialize, +)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum DydxPositionStatus { + /// Position is open. + Open, + /// Position is closed. + Closed, + /// Position was liquidated. + Liquidated, +} + +impl From for PositionSide { + fn from(value: DydxPositionStatus) -> Self { + match value { + DydxPositionStatus::Open => Self::Long, // Default, actual side from position size + DydxPositionStatus::Closed => Self::Flat, + DydxPositionStatus::Liquidated => Self::Flat, + } + } +} + +/// dYdX perpetual market status. +#[derive( + Copy, + Clone, + Debug, + Display, + PartialEq, + Eq, + Hash, + AsRefStr, + EnumIter, + EnumString, + Serialize, + Deserialize, +)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum DydxMarketStatus { + /// Market is active and trading. + Active, + /// Market is paused (no trading). + Paused, + /// Cancel-only mode (no new orders). + CancelOnly, + /// Post-only mode (only maker orders). + PostOnly, + /// Market is initializing. + Initializing, + /// Market is in final settlement. + FinalSettlement, +} + +/// dYdX fill type. +#[derive( + Copy, + Clone, + Debug, + Display, + PartialEq, + Eq, + Hash, + AsRefStr, + EnumIter, + EnumString, + Serialize, + Deserialize, +)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum DydxFillType { + /// Normal limit order fill. + Limit, + /// Liquidation (taker side). + Liquidated, + /// Liquidation (maker side). + Liquidation, + /// Deleveraging (deleveraged account). + Deleveraged, + /// Deleveraging (offsetting account). + Offsetting, +} + +/// dYdX liquidity side (maker/taker). +#[derive( + Copy, + Clone, + Debug, + Display, + PartialEq, + Eq, + Hash, + AsRefStr, + EnumIter, + EnumString, + Serialize, + Deserialize, +)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum DydxLiquidity { + /// Maker (provides liquidity). + Maker, + /// Taker (removes liquidity). + Taker, +} + +impl From for LiquiditySide { + fn from(value: DydxLiquidity) -> Self { + match value { + DydxLiquidity::Maker => Self::Maker, + DydxLiquidity::Taker => Self::Taker, + } + } +} + +impl From for DydxLiquidity { + fn from(value: LiquiditySide) -> Self { + match value { + LiquiditySide::Maker => Self::Maker, + LiquiditySide::Taker => Self::Taker, + LiquiditySide::NoLiquiditySide => Self::Taker, // Default fallback + } + } +} + +/// dYdX ticker type for market data. +#[derive( + Copy, + Clone, + Debug, + Display, + PartialEq, + Eq, + Hash, + AsRefStr, + EnumIter, + EnumString, + Serialize, + Deserialize, +)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum DydxTickerType { + /// Perpetual market ticker. + Perpetual, +} + +/// dYdX candlestick resolution. +#[derive( + Copy, + Clone, + Debug, + Display, + PartialEq, + Eq, + Hash, + AsRefStr, + EnumIter, + EnumString, + Serialize, + Deserialize, +)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +#[derive(Default)] +pub enum DydxCandleResolution { + /// 1 minute candles. + #[serde(rename = "1MIN")] + #[strum(serialize = "1MIN")] + #[default] + OneMinute, + /// 5 minute candles. + #[serde(rename = "5MINS")] + #[strum(serialize = "5MINS")] + FiveMinutes, + /// 15 minute candles. + #[serde(rename = "15MINS")] + #[strum(serialize = "15MINS")] + FifteenMinutes, + /// 30 minute candles. + #[serde(rename = "30MINS")] + #[strum(serialize = "30MINS")] + ThirtyMinutes, + /// 1 hour candles. + #[serde(rename = "1HOUR")] + #[strum(serialize = "1HOUR")] + OneHour, + /// 4 hour candles. + #[serde(rename = "4HOURS")] + #[strum(serialize = "4HOURS")] + FourHours, + /// 1 day candles. + #[serde(rename = "1DAY")] + #[strum(serialize = "1DAY")] + OneDay, +} + +//////////////////////////////////////////////////////////////////////////////// +// Tests +//////////////////////////////////////////////////////////////////////////////// + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_order_side_conversion() { + assert_eq!(OrderSide::from(DydxOrderSide::Buy), OrderSide::Buy); + assert_eq!(OrderSide::from(DydxOrderSide::Sell), OrderSide::Sell); + assert_eq!(DydxOrderSide::from(OrderSide::Buy), DydxOrderSide::Buy); + assert_eq!(DydxOrderSide::from(OrderSide::Sell), DydxOrderSide::Sell); + } + + #[test] + fn test_order_status_conversion() { + assert_eq!( + OrderStatus::from(DydxOrderStatus::Open), + OrderStatus::Accepted + ); + assert_eq!( + OrderStatus::from(DydxOrderStatus::Filled), + OrderStatus::Filled + ); + assert_eq!( + OrderStatus::from(DydxOrderStatus::Canceled), + OrderStatus::Canceled + ); + } + + #[test] + fn test_liquidity_conversion() { + assert_eq!( + LiquiditySide::from(DydxLiquidity::Maker), + LiquiditySide::Maker + ); + assert_eq!( + LiquiditySide::from(DydxLiquidity::Taker), + LiquiditySide::Taker + ); + } +} diff --git a/crates/adapters/dydx/src/common/mod.rs b/crates/adapters/dydx/src/common/mod.rs index 6dde695fd475..49e0fda567d6 100644 --- a/crates/adapters/dydx/src/common/mod.rs +++ b/crates/adapters/dydx/src/common/mod.rs @@ -14,6 +14,8 @@ // ------------------------------------------------------------------------------------------------- pub mod consts; +pub mod credential; +pub mod enums; pub mod types; pub mod urls; diff --git a/crates/adapters/dydx/src/grpc/wallet.rs b/crates/adapters/dydx/src/grpc/wallet.rs index 9eaa2e5ce4ab..adc11c912f0e 100644 --- a/crates/adapters/dydx/src/grpc/wallet.rs +++ b/crates/adapters/dydx/src/grpc/wallet.rs @@ -20,9 +20,9 @@ use std::{fmt::Debug, str::FromStr}; -use bip32::{DerivationPath, Language, Mnemonic, Seed}; use cosmrs::{ AccountId, + bip32::{DerivationPath, Language, Mnemonic, Seed}, crypto::{PublicKey, secp256k1::SigningKey}, tx, }; diff --git a/crates/adapters/dydx/src/http/client.rs b/crates/adapters/dydx/src/http/client.rs index 2329c3b413cd..95ac01f29f53 100644 --- a/crates/adapters/dydx/src/http/client.rs +++ b/crates/adapters/dydx/src/http/client.rs @@ -52,7 +52,10 @@ use serde::{Deserialize, Serialize, de::DeserializeOwned}; use tokio_util::sync::CancellationToken; use super::error::DydxHttpError; -use crate::common::urls::{DYDX_HTTP_URL, DYDX_TESTNET_HTTP_URL}; +use crate::common::{ + enums::DydxCandleResolution, + urls::{DYDX_HTTP_URL, DYDX_TESTNET_HTTP_URL}, +}; /// Default dYdX Indexer REST API rate limit. /// @@ -354,6 +357,187 @@ impl DydxRawHttpClient { body: String::from_utf8_lossy(&response.body).to_string(), }) } + + // ======================================================================== + // Markets Endpoints + // ======================================================================== + + /// Fetch all perpetual markets. + /// + /// # Errors + /// + /// Returns an error if the HTTP request fails or response parsing fails. + pub async fn get_markets(&self) -> Result { + self.send_request(Method::GET, "/v4/perpetualMarkets", None) + .await + } + + /// Fetch orderbook for a specific market. + /// + /// # Errors + /// + /// Returns an error if the HTTP request fails or response parsing fails. + pub async fn get_orderbook( + &self, + ticker: &str, + ) -> Result { + let endpoint = format!("/v4/orderbooks/perpetualMarket/{ticker}"); + self.send_request(Method::GET, &endpoint, None).await + } + + /// Fetch recent trades for a market. + /// + /// # Errors + /// + /// Returns an error if the HTTP request fails or response parsing fails. + pub async fn get_trades( + &self, + ticker: &str, + limit: Option, + ) -> Result { + let endpoint = format!("/v4/trades/perpetualMarket/{ticker}"); + let query = limit.map(|l| format!("limit={l}")); + self.send_request(Method::GET, &endpoint, query.as_deref()) + .await + } + + /// Fetch candles/klines for a market. + /// + /// # Errors + /// + /// Returns an error if the HTTP request fails or response parsing fails. + pub async fn get_candles( + &self, + ticker: &str, + resolution: DydxCandleResolution, + limit: Option, + ) -> Result { + let endpoint = format!("/v4/candles/perpetualMarkets/{ticker}"); + let mut query_parts = vec![format!("resolution={}", resolution)]; + if let Some(l) = limit { + query_parts.push(format!("limit={l}")); + } + let query = query_parts.join("&"); + self.send_request(Method::GET, &endpoint, Some(&query)) + .await + } + + // ======================================================================== + // Account Endpoints + // ======================================================================== + + /// Fetch subaccount information. + /// + /// # Errors + /// + /// Returns an error if the HTTP request fails or response parsing fails. + pub async fn get_subaccount( + &self, + address: &str, + subaccount_number: u32, + ) -> Result { + let endpoint = format!("/v4/addresses/{address}/subaccountNumber/{subaccount_number}"); + self.send_request(Method::GET, &endpoint, None).await + } + + /// Fetch fills for a subaccount. + /// + /// # Errors + /// + /// Returns an error if the HTTP request fails or response parsing fails. + pub async fn get_fills( + &self, + address: &str, + subaccount_number: u32, + market: Option<&str>, + limit: Option, + ) -> Result { + let endpoint = "/v4/fills"; + let mut query_parts = vec![ + format!("address={address}"), + format!("subaccountNumber={subaccount_number}"), + ]; + if let Some(m) = market { + query_parts.push(format!("market={m}")); + } + if let Some(l) = limit { + query_parts.push(format!("limit={l}")); + } + let query = query_parts.join("&"); + self.send_request(Method::GET, endpoint, Some(&query)).await + } + + /// Fetch orders for a subaccount. + /// + /// # Errors + /// + /// Returns an error if the HTTP request fails or response parsing fails. + pub async fn get_orders( + &self, + address: &str, + subaccount_number: u32, + market: Option<&str>, + limit: Option, + ) -> Result { + let endpoint = "/v4/orders"; + let mut query_parts = vec![ + format!("address={address}"), + format!("subaccountNumber={subaccount_number}"), + ]; + if let Some(m) = market { + query_parts.push(format!("market={m}")); + } + if let Some(l) = limit { + query_parts.push(format!("limit={l}")); + } + let query = query_parts.join("&"); + self.send_request(Method::GET, endpoint, Some(&query)).await + } + + /// Fetch transfers for a subaccount. + /// + /// # Errors + /// + /// Returns an error if the HTTP request fails or response parsing fails. + pub async fn get_transfers( + &self, + address: &str, + subaccount_number: u32, + limit: Option, + ) -> Result { + let endpoint = "/v4/transfers"; + let mut query_parts = vec![ + format!("address={address}"), + format!("subaccountNumber={subaccount_number}"), + ]; + if let Some(l) = limit { + query_parts.push(format!("limit={l}")); + } + let query = query_parts.join("&"); + self.send_request(Method::GET, endpoint, Some(&query)).await + } + + // ======================================================================== + // Utility Endpoints + // ======================================================================== + + /// Get current server time. + /// + /// # Errors + /// + /// Returns an error if the HTTP request fails or response parsing fails. + pub async fn get_time(&self) -> Result { + self.send_request(Method::GET, "/v4/time", None).await + } + + /// Get current blockchain height. + /// + /// # Errors + /// + /// Returns an error if the HTTP request fails or response parsing fails. + pub async fn get_height(&self) -> Result { + self.send_request(Method::GET, "/v4/height", None).await + } } //////////////////////////////////////////////////////////////////////////////// diff --git a/crates/adapters/dydx/src/http/models.rs b/crates/adapters/dydx/src/http/models.rs index 8905fd9cb24e..66d5ea6a93bc 100644 --- a/crates/adapters/dydx/src/http/models.rs +++ b/crates/adapters/dydx/src/http/models.rs @@ -17,26 +17,569 @@ //! //! This module contains Rust types that mirror the JSON structures returned //! by the dYdX v4 Indexer API endpoints. +//! +//! # API Documentation +//! +//! - Indexer HTTP API: +//! - Markets: +//! - Accounts: +use chrono::{DateTime, Utc}; +use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; +use serde_with::{DisplayFromStr, serde_as}; + +use crate::common::enums::{ + DydxCandleResolution, DydxFillType, DydxLiquidity, DydxMarketStatus, DydxOrderSide, + DydxOrderStatus, DydxPositionStatus, DydxTickerType, DydxTimeInForce, +}; + +//////////////////////////////////////////////////////////////////////////////// +// Markets +//////////////////////////////////////////////////////////////////////////////// + +/// Response wrapper for markets endpoint. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MarketsResponse { + /// Map of market ticker to perpetual market data. + pub markets: std::collections::HashMap, +} + +/// Perpetual market definition. +#[serde_as] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PerpetualMarket { + /// Unique identifier for the CLOB pair. + #[serde_as(as = "DisplayFromStr")] + pub clob_pair_id: u32, + /// Market ticker (e.g., "BTC-USD"). + pub ticker: String, + /// Market status (ACTIVE, PAUSED, etc.). + pub status: DydxMarketStatus, + /// Base asset symbol. + pub base_asset: String, + /// Quote asset symbol. + pub quote_asset: String, + /// Step size for order quantities (minimum increment). + #[serde_as(as = "DisplayFromStr")] + pub step_size: Decimal, + /// Tick size for order prices (minimum increment). + #[serde_as(as = "DisplayFromStr")] + pub tick_size: Decimal, + /// Index price for the market. + #[serde_as(as = "DisplayFromStr")] + pub index_price: Decimal, + /// Oracle price for the market. + #[serde_as(as = "DisplayFromStr")] + pub oracle_price: Decimal, + /// Price change over 24 hours. + #[serde_as(as = "DisplayFromStr")] + pub price_change_24h: Decimal, + /// Next funding rate. + #[serde_as(as = "DisplayFromStr")] + pub next_funding_rate: Decimal, + /// Next funding time (ISO8601). + pub next_funding_at: DateTime, + /// Minimum order size in base currency. + #[serde_as(as = "DisplayFromStr")] + pub min_order_size: Decimal, + /// Market type (always PERPETUAL for dYdX v4). + #[serde(rename = "type")] + pub market_type: DydxTickerType, + /// Initial margin fraction. + #[serde_as(as = "DisplayFromStr")] + pub initial_margin_fraction: Decimal, + /// Maintenance margin fraction. + #[serde_as(as = "DisplayFromStr")] + pub maintenance_margin_fraction: Decimal, + /// Base position notional value. + #[serde_as(as = "DisplayFromStr")] + pub base_position_notional: Decimal, + /// Incremental position size for margin scaling. + #[serde_as(as = "DisplayFromStr")] + pub incremental_position_size: Decimal, + /// Incremental initial margin fraction. + #[serde_as(as = "DisplayFromStr")] + pub incremental_initial_margin_fraction: Decimal, + /// Maximum position size. + #[serde_as(as = "DisplayFromStr")] + pub max_position_size: Decimal, + /// Open interest in base currency. + #[serde_as(as = "DisplayFromStr")] + pub open_interest: Decimal, + /// Atomic resolution (power of 10 for quantum conversion). + pub atomic_resolution: i32, + /// Quantum conversion exponent (deprecated, use atomic_resolution). + pub quantum_conversion_exponent: i32, + /// Subticks per tick. + pub subticks_per_tick: u32, + /// Step base quantums. + pub step_base_quantums: u64, + /// Is the market in reduce-only mode. + #[serde(default)] + pub is_reduce_only: bool, +} + +/// Orderbook snapshot response. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct OrderbookResponse { + /// Bids (buy orders). + pub bids: Vec, + /// Asks (sell orders). + pub asks: Vec, +} + +/// Single level in the orderbook. +#[serde_as] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct OrderbookLevel { + /// Price level. + #[serde_as(as = "DisplayFromStr")] + pub price: Decimal, + /// Size at this level. + #[serde_as(as = "DisplayFromStr")] + pub size: Decimal, +} + +/// Response wrapper for trades endpoint. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TradesResponse { + /// List of trades. + pub trades: Vec, +} -// TODO: Add data models for: -// - Markets (PerpetualMarketResponse, OrderBookResponse, TradeResponse, CandleResponse) -// - Accounts (SubaccountResponse, PositionResponse, OrderResponse, FillResponse) -// - Utility (TimeResponse, HeightResponse, ComplianceResponse) +/// Individual trade. +#[serde_as] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Trade { + /// Unique trade ID. + pub id: String, + /// Order side that was the taker. + pub side: DydxOrderSide, + /// Trade size in base currency. + #[serde_as(as = "DisplayFromStr")] + pub size: Decimal, + /// Trade price. + #[serde_as(as = "DisplayFromStr")] + pub price: Decimal, + /// Trade timestamp. + pub created_at: DateTime, + /// Height of block containing this trade. + #[serde_as(as = "DisplayFromStr")] + pub created_at_height: u64, + /// Market ticker. + #[serde(rename = "type")] + pub market_type: DydxTickerType, +} + +/// Response wrapper for candles endpoint. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CandlesResponse { + /// List of candles. + pub candles: Vec, +} -/// Placeholder for market data models. +/// OHLCV candle data. +#[serde_as] #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct DydxMarketData { - /// Market ticker symbol. +#[serde(rename_all = "camelCase")] +pub struct Candle { + /// Candle start time. + pub started_at: DateTime, + /// Market ticker. pub ticker: String, + /// Candle resolution. + pub resolution: DydxCandleResolution, + /// Opening price. + #[serde_as(as = "DisplayFromStr")] + pub open: Decimal, + /// Highest price. + #[serde_as(as = "DisplayFromStr")] + pub high: Decimal, + /// Lowest price. + #[serde_as(as = "DisplayFromStr")] + pub low: Decimal, + /// Closing price. + #[serde_as(as = "DisplayFromStr")] + pub close: Decimal, + /// Base asset volume. + #[serde_as(as = "DisplayFromStr")] + pub base_token_volume: Decimal, + /// Quote asset volume (USD). + #[serde_as(as = "DisplayFromStr")] + pub usd_volume: Decimal, + /// Number of trades in this candle. + pub trades: u64, + /// Block height at candle start. + #[serde_as(as = "DisplayFromStr")] + pub starting_open_interest: Decimal, +} + +//////////////////////////////////////////////////////////////////////////////// +// Accounts +//////////////////////////////////////////////////////////////////////////////// + +/// Response for subaccount endpoint. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SubaccountResponse { + /// Subaccount data. + pub subaccount: Subaccount, +} + +/// Subaccount information. +#[serde_as] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Subaccount { + /// Subaccount address (dydx...). + pub address: String, + /// Subaccount number. + pub subaccount_number: u32, + /// Account equity in USD. + #[serde_as(as = "DisplayFromStr")] + pub equity: Decimal, + /// Free collateral. + #[serde_as(as = "DisplayFromStr")] + pub free_collateral: Decimal, + /// Open perpetual positions. + #[serde(default)] + pub open_perpetual_positions: std::collections::HashMap, + /// Asset positions (e.g., USDC). + #[serde(default)] + pub asset_positions: std::collections::HashMap, + /// Margin enabled flag. + #[serde(default)] + pub margin_enabled: bool, + /// Last updated height. + #[serde_as(as = "DisplayFromStr")] + pub updated_at_height: u64, +} + +/// Perpetual position. +#[serde_as] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PerpetualPosition { + /// Market ticker. + pub market: String, + /// Position status. + pub status: DydxPositionStatus, + /// Position side (determined by size sign). + pub side: DydxOrderSide, + /// Position size (negative for short). + #[serde_as(as = "DisplayFromStr")] + pub size: Decimal, + /// Maximum size reached. + #[serde_as(as = "DisplayFromStr")] + pub max_size: Decimal, + /// Average entry price. + #[serde_as(as = "DisplayFromStr")] + pub entry_price: Decimal, + /// Exit price (if closed). + #[serde_as(as = "Option")] + #[serde(skip_serializing_if = "Option::is_none")] + pub exit_price: Option, + /// Realized PnL. + #[serde_as(as = "DisplayFromStr")] + pub realized_pnl: Decimal, + /// Creation height. + #[serde_as(as = "DisplayFromStr")] + pub created_at_height: u64, + /// Creation time. + pub created_at: DateTime, + /// Sum of all open order sizes. + #[serde_as(as = "DisplayFromStr")] + pub sum_open: Decimal, + /// Sum of all close order sizes. + #[serde_as(as = "DisplayFromStr")] + pub sum_close: Decimal, + /// Net funding paid/received. + #[serde_as(as = "DisplayFromStr")] + pub net_funding: Decimal, + /// Unrealized PnL. + #[serde_as(as = "DisplayFromStr")] + pub unrealized_pnl: Decimal, + /// Closed time (if closed). + #[serde(skip_serializing_if = "Option::is_none")] + pub closed_at: Option>, +} + +/// Asset position (e.g., USDC balance). +#[serde_as] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AssetPosition { + /// Asset symbol. + pub symbol: String, + /// Position side (always LONG for assets). + pub side: DydxOrderSide, + /// Asset size (balance). + #[serde_as(as = "DisplayFromStr")] + pub size: Decimal, + /// Asset ID. + pub asset_id: String, +} + +/// Response for orders endpoint. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct OrdersResponse { + /// List of orders. + pub orders: Vec, +} + +/// Order information. +#[serde_as] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Order { + /// Unique order ID. + pub id: String, + /// Subaccount ID. + pub subaccount_id: String, + /// Client-provided order ID. + pub client_id: String, + /// CLOB pair ID. + #[serde_as(as = "DisplayFromStr")] + pub clob_pair_id: u32, + /// Order side. + pub side: DydxOrderSide, + /// Order size. + #[serde_as(as = "DisplayFromStr")] + pub size: Decimal, + /// Remaining size to be filled. + #[serde_as(as = "DisplayFromStr")] + pub remaining_size: Decimal, + /// Limit price. + #[serde_as(as = "DisplayFromStr")] + pub price: Decimal, + /// Order status. + pub status: DydxOrderStatus, + /// Order type (LIMIT, MARKET, etc.). + #[serde(rename = "type")] + pub order_type: String, + /// Time-in-force. + pub time_in_force: DydxTimeInForce, + /// Reduce-only flag. + pub reduce_only: bool, + /// Post-only flag. + pub post_only: bool, + /// Order flags (bitfield). + pub order_flags: u32, + /// Good-til-block (for short-term orders). + #[serde_as(as = "Option")] + #[serde(skip_serializing_if = "Option::is_none")] + pub good_til_block: Option, + /// Good-til-time (ISO8601). + #[serde(skip_serializing_if = "Option::is_none")] + pub good_til_block_time: Option>, + /// Creation height. + #[serde_as(as = "DisplayFromStr")] + pub created_at_height: u64, + /// Client metadata. + pub client_metadata: u32, + /// Trigger price (for stop orders). + #[serde_as(as = "Option")] + #[serde(skip_serializing_if = "Option::is_none")] + pub trigger_price: Option, + /// Updated timestamp. + pub updated_at: DateTime, + /// Updated height. + #[serde_as(as = "DisplayFromStr")] + pub updated_at_height: u64, +} + +/// Response for fills endpoint. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FillsResponse { + /// List of fills. + pub fills: Vec, } -/// Placeholder for account data models. +/// Order fill information. +#[serde_as] #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct DydxAccountData { - /// Wallet address. +#[serde(rename_all = "camelCase")] +pub struct Fill { + /// Unique fill ID. + pub id: String, + /// Order side. + pub side: DydxOrderSide, + /// Liquidity side (MAKER/TAKER). + pub liquidity: DydxLiquidity, + /// Fill type. + #[serde(rename = "type")] + pub fill_type: DydxFillType, + /// Market ticker. + pub market: String, + /// Market type. + pub market_type: DydxTickerType, + /// Fill price. + #[serde_as(as = "DisplayFromStr")] + pub price: Decimal, + /// Fill size. + #[serde_as(as = "DisplayFromStr")] + pub size: Decimal, + /// Fee paid. + #[serde_as(as = "DisplayFromStr")] + pub fee: Decimal, + /// Fill timestamp. + pub created_at: DateTime, + /// Fill height. + #[serde_as(as = "DisplayFromStr")] + pub created_at_height: u64, + /// Order ID. + pub order_id: String, + /// Client order ID. + pub client_metadata: u32, +} + +/// Response for transfers endpoint. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TransfersResponse { + /// List of transfers. + pub transfers: Vec, +} + +/// Transfer information. +#[serde_as] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Transfer { + /// Unique transfer ID. + pub id: String, + /// Transfer type (DEPOSIT, WITHDRAWAL, TRANSFER_OUT, TRANSFER_IN). + #[serde(rename = "type")] + pub transfer_type: String, + /// Sender address. + pub sender: TransferAccount, + /// Recipient address. + pub recipient: TransferAccount, + /// Asset symbol. + pub asset: String, + /// Transfer amount. + #[serde_as(as = "DisplayFromStr")] + pub amount: Decimal, + /// Creation timestamp. + pub created_at: DateTime, + /// Creation height. + #[serde_as(as = "DisplayFromStr")] + pub created_at_height: u64, + /// Transaction hash. + pub transaction_hash: String, +} + +/// Transfer account information. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TransferAccount { + /// Address. pub address: String, /// Subaccount number. pub subaccount_number: u32, } + +//////////////////////////////////////////////////////////////////////////////// +// Utility +//////////////////////////////////////////////////////////////////////////////// + +/// Response for time endpoint. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TimeResponse { + /// Current ISO8601 timestamp. + pub iso: DateTime, + /// Current Unix timestamp in milliseconds. + #[serde(rename = "epoch")] + pub epoch_ms: i64, +} + +/// Response for height endpoint. +#[serde_as] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct HeightResponse { + /// Current blockchain height. + #[serde_as(as = "DisplayFromStr")] + pub height: u64, + /// Timestamp of the block. + pub time: DateTime, +} + +//////////////////////////////////////////////////////////////////////////////// +// Execution Models (Node API) +//////////////////////////////////////////////////////////////////////////////// + +/// Request to place an order via Node API. +#[serde_as] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PlaceOrderRequest { + /// Subaccount placing the order. + pub subaccount: SubaccountId, + /// Client-generated order ID. + pub client_id: u32, + /// Order type flags (bitfield for short-term, reduce-only, etc.). + pub order_flags: u32, + /// CLOB pair ID. + pub clob_pair_id: u32, + /// Order side. + pub side: DydxOrderSide, + /// Order size in quantums. + pub quantums: u64, + /// Order subticks (price representation). + pub subticks: u64, + /// Time-in-force. + pub time_in_force: DydxTimeInForce, + /// Good-til-block (for short-term orders). + #[serde(skip_serializing_if = "Option::is_none")] + pub good_til_block: Option, + /// Good-til-block-time (Unix seconds, for stateful orders). + #[serde(skip_serializing_if = "Option::is_none")] + pub good_til_block_time: Option, + /// Reduce-only flag. + pub reduce_only: bool, + /// Optional authenticator IDs for permissioned keys. + #[serde(skip_serializing_if = "Option::is_none")] + pub authenticator_ids: Option>, +} + +/// Subaccount identifier. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SubaccountId { + /// Owner address. + pub owner: String, + /// Subaccount number. + pub number: u32, +} + +/// Request to cancel an order. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CancelOrderRequest { + /// Subaccount ID. + pub subaccount_id: SubaccountId, + /// Client order ID to cancel. + pub client_id: u32, + /// CLOB pair ID. + pub clob_pair_id: u32, + /// Order flags. + pub order_flags: u32, + /// Good-til-block or good-til-block-time for the cancel. + pub good_til_block: Option, + pub good_til_block_time: Option, +} + +/// Transaction response from Node. +#[serde_as] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TransactionResponse { + /// Transaction hash. + pub tx_hash: String, + /// Block height. + #[serde_as(as = "DisplayFromStr")] + pub height: u64, + /// Result code (0 = success). + pub code: u32, + /// Raw log output. + pub raw_log: String, +} diff --git a/crates/adapters/dydx/src/http/parse.rs b/crates/adapters/dydx/src/http/parse.rs index 3ac2fae96581..9dff9cd56eef 100644 --- a/crates/adapters/dydx/src/http/parse.rs +++ b/crates/adapters/dydx/src/http/parse.rs @@ -19,8 +19,6 @@ //! from the dYdX Indexer API into strongly-typed Nautilus data types such as //! instruments, trades, bars, account states, etc. -use super::models::DydxMarketData; - // TODO: Implement parsing functions for: // - parse_instrument_any: Convert dYdX perpetual market to InstrumentAny // - parse_trade_tick: Convert dYdX trade to TradeTick @@ -29,13 +27,3 @@ use super::models::DydxMarketData; // - parse_order_status_report: Convert dYdX order to OrderStatusReport // - parse_position_status_report: Convert dYdX position to PositionStatusReport // - parse_fill_report: Convert dYdX fill to FillReport - -/// Placeholder parsing function. -/// -/// # Errors -/// -/// Returns an error if parsing fails. -pub fn parse_market_data(_data: &DydxMarketData) -> anyhow::Result<()> { - // TODO: Implement actual parsing logic - Ok(()) -} diff --git a/crates/adapters/dydx/src/http/query.rs b/crates/adapters/dydx/src/http/query.rs index 315c189d5bda..7b7d06a1dcc5 100644 --- a/crates/adapters/dydx/src/http/query.rs +++ b/crates/adapters/dydx/src/http/query.rs @@ -14,31 +14,43 @@ // ------------------------------------------------------------------------------------------------- //! Query parameter builders for dYdX v4 Indexer REST API endpoints. -//! -//! This module provides type-safe builders for constructing query parameters -//! that are sent to various dYdX Indexer API endpoints. use derive_builder::Builder; use serde::Serialize; -// TODO: Add query parameter builders for: -// - GetPerpetualMarketsParams -// - GetOrderbookParams -// - GetTradesParams -// - GetCandlesParams -// - GetSubaccountParams -// - GetOrdersParams -// - GetFillsParams -// etc. +use crate::common::enums::DydxCandleResolution; -/// Query parameters for fetching markets from the dYdX Indexer API. +/// Query parameters for fetching orderbook. #[derive(Debug, Clone, Default, Serialize, Builder)] #[builder(setter(into, strip_option), default)] -pub struct GetMarketsParams { - /// Optional ticker filter. +pub struct GetOrderbookParams { + pub ticker: String, +} + +/// Query parameters for fetching trades. +#[derive(Debug, Clone, Default, Serialize, Builder)] +#[builder(setter(into, strip_option), default)] +pub struct GetTradesParams { + pub ticker: String, #[serde(skip_serializing_if = "Option::is_none")] - pub ticker: Option, - /// Optional limit for number of results. + pub limit: Option, +} + +/// Query parameters for fetching candles. +#[derive(Debug, Clone, Default, Serialize, Builder)] +#[builder(setter(into, strip_option), default)] +pub struct GetCandlesParams { + pub ticker: String, + pub resolution: DydxCandleResolution, #[serde(skip_serializing_if = "Option::is_none")] pub limit: Option, } + +/// Query parameters for fetching subaccount. +#[derive(Debug, Clone, Default, Serialize, Builder)] +#[builder(setter(into, strip_option), default)] +pub struct GetSubaccountParams { + pub address: String, + #[serde(rename = "subaccountNumber")] + pub subaccount_number: u32, +} From 068525ec9b7a3fc157558a05d6e303f0bd0b21f8 Mon Sep 17 00:00:00 2001 From: Vadim Nicolai Date: Wed, 5 Nov 2025 07:28:15 +0200 Subject: [PATCH 09/10] Change requests applied --- crates/adapters/dydx/src/common/consts.rs | 50 ++++++++ crates/adapters/dydx/src/common/credential.rs | 11 +- crates/adapters/dydx/src/common/enums.rs | 67 +++------- crates/adapters/dydx/src/common/urls.rs | 116 +++++++++++++++--- crates/adapters/dydx/src/config.rs | 36 +++++- crates/adapters/dydx/src/grpc/client.rs | 45 ++++++- crates/adapters/dydx/src/grpc/mod.rs | 2 +- crates/adapters/dydx/src/grpc/order.rs | 22 +--- crates/adapters/dydx/src/http/client.rs | 2 +- crates/adapters/dydx/src/http/models.rs | 17 +-- 10 files changed, 267 insertions(+), 101 deletions(-) diff --git a/crates/adapters/dydx/src/common/consts.rs b/crates/adapters/dydx/src/common/consts.rs index 766cc8088835..545b4f878075 100644 --- a/crates/adapters/dydx/src/common/consts.rs +++ b/crates/adapters/dydx/src/common/consts.rs @@ -50,6 +50,56 @@ pub const DYDX_DERIVATION_PATH_PREFIX: &str = "m/44'/118'/0'/0"; /// Coin type for Cosmos ecosystem (SLIP-0044). pub const COSMOS_COIN_TYPE: u32 = 118; +// Mainnet URLs +/// dYdX v4 mainnet HTTP API base URL. +pub const DYDX_HTTP_URL: &str = "https://indexer.dydx.trade"; + +/// dYdX v4 mainnet WebSocket URL. +pub const DYDX_WS_URL: &str = "wss://indexer.dydx.trade/v4/ws"; + +/// dYdX v4 mainnet gRPC URLs (public validator nodes with fallbacks). +/// +/// Multiple nodes are provided for redundancy. The client should attempt to connect +/// to nodes in order, falling back to the next if connection fails. This is critical +/// for DEX environments where individual nodes can fail or become unavailable. +pub const DYDX_GRPC_URLS: &[&str] = &[ + "https://dydx-grpc.publicnode.com:443", + "https://dydx-ops-grpc.kingnodes.com:443", + "https://dydx-mainnet-grpc.autostake.com:443", +]; + +/// dYdX v4 mainnet gRPC URL (primary public node). +/// +/// # Notes +/// +/// For production use, consider using `DYDX_GRPC_URLS` array with fallback logic +/// via `DydxGrpcClient::new_with_fallback()`. +pub const DYDX_GRPC_URL: &str = DYDX_GRPC_URLS[0]; + +// Testnet URLs +/// dYdX v4 testnet HTTP API base URL. +pub const DYDX_TESTNET_HTTP_URL: &str = "https://indexer.v4testnet.dydx.exchange"; + +/// dYdX v4 testnet WebSocket URL. +pub const DYDX_TESTNET_WS_URL: &str = "wss://indexer.v4testnet.dydx.exchange/v4/ws"; + +/// dYdX v4 testnet gRPC URLs (public validator nodes with fallbacks). +/// +/// Multiple nodes are provided for redundancy. The client should attempt to connect +/// to nodes in order, falling back to the next if connection fails. +pub const DYDX_TESTNET_GRPC_URLS: &[&str] = &[ + "https://dydx-testnet-grpc.publicnode.com:443", + "https://test-dydx-grpc.kingnodes.com:443", +]; + +/// dYdX v4 testnet gRPC URL (primary public node). +/// +/// # Notes +/// +/// For production use, consider using `DYDX_TESTNET_GRPC_URLS` array with fallback logic +/// via `DydxGrpcClient::new_with_fallback()`. +pub const DYDX_TESTNET_GRPC_URL: &str = DYDX_TESTNET_GRPC_URLS[0]; + /// Determines if an HTTP status code should trigger a retry. /// /// Retries on: diff --git a/crates/adapters/dydx/src/common/credential.rs b/crates/adapters/dydx/src/common/credential.rs index 73739f57b1c6..cc0b3fed4b5d 100644 --- a/crates/adapters/dydx/src/common/credential.rs +++ b/crates/adapters/dydx/src/common/credential.rs @@ -222,8 +222,9 @@ mod tests { #[test] fn test_from_private_key() { - // Use a test private key (all zeros - not a real key) - let test_key = "0".repeat(64); + // Use a valid test private key (small non-zero value) + // This is a valid secp256k1 private key: 32 bytes with value 1 + let test_key = format!("{:0>64}", "1"); let credential = DydxCredential::from_private_key(&test_key, vec![]) .expect("Failed to create credential from private key"); @@ -261,7 +262,11 @@ mod tests { .expect("Failed to create credential"); let debug_str = format!("{:?}", credential); + // Should contain redacted marker assert!(debug_str.contains("")); - assert!(!debug_str.contains("signing_key")); + // Should contain the struct name + assert!(debug_str.contains("DydxCredential")); + // Should show address + assert!(debug_str.contains(&credential.address)); } } diff --git a/crates/adapters/dydx/src/common/enums.rs b/crates/adapters/dydx/src/common/enums.rs index 2ece8f7db43a..597a3b356c07 100644 --- a/crates/adapters/dydx/src/common/enums.rs +++ b/crates/adapters/dydx/src/common/enums.rs @@ -15,52 +15,10 @@ //! Enumerations mapping dYdX v4 concepts onto idiomatic Nautilus variants. -use nautilus_model::enums::{LiquiditySide, OrderSide, OrderStatus, PositionSide}; +use nautilus_model::enums::{LiquiditySide, OrderStatus, PositionSide}; use serde::{Deserialize, Serialize}; use strum::{AsRefStr, Display, EnumIter, EnumString}; -/// Represents the side of an order or trade (Buy/Sell). -#[derive( - Copy, - Clone, - Debug, - Display, - PartialEq, - Eq, - Hash, - AsRefStr, - EnumIter, - EnumString, - Serialize, - Deserialize, -)] -#[serde(rename_all = "SCREAMING_SNAKE_CASE")] -pub enum DydxOrderSide { - /// Buy side of a trade or order. - Buy, - /// Sell side of a trade or order. - Sell, -} - -impl From for DydxOrderSide { - fn from(value: OrderSide) -> Self { - match value { - OrderSide::Buy => Self::Buy, - OrderSide::Sell => Self::Sell, - OrderSide::NoOrderSide => Self::Buy, // Default fallback - } - } -} - -impl From for OrderSide { - fn from(value: DydxOrderSide) -> Self { - match value { - DydxOrderSide::Buy => Self::Buy, - DydxOrderSide::Sell => Self::Sell, - } - } -} - /// dYdX order status throughout its lifecycle. #[derive( Copy, @@ -378,13 +336,26 @@ pub enum DydxCandleResolution { #[cfg(test)] mod tests { use super::*; + use nautilus_model::enums::OrderSide; #[test] - fn test_order_side_conversion() { - assert_eq!(OrderSide::from(DydxOrderSide::Buy), OrderSide::Buy); - assert_eq!(OrderSide::from(DydxOrderSide::Sell), OrderSide::Sell); - assert_eq!(DydxOrderSide::from(OrderSide::Buy), DydxOrderSide::Buy); - assert_eq!(DydxOrderSide::from(OrderSide::Sell), DydxOrderSide::Sell); + fn test_order_side_serialization() { + // Test that OrderSide serializes to SCREAMING_SNAKE_CASE as expected by dYdX API + let buy = OrderSide::Buy; + let sell = OrderSide::Sell; + + assert_eq!(serde_json::to_string(&buy).unwrap(), r#""BUY""#); + assert_eq!(serde_json::to_string(&sell).unwrap(), r#""SELL""#); + + // Test deserialization + assert_eq!( + serde_json::from_str::(r#""BUY""#).unwrap(), + OrderSide::Buy + ); + assert_eq!( + serde_json::from_str::(r#""SELL""#).unwrap(), + OrderSide::Sell + ); } #[test] diff --git a/crates/adapters/dydx/src/common/urls.rs b/crates/adapters/dydx/src/common/urls.rs index c3b6c6f31412..0f2c17e6a99e 100644 --- a/crates/adapters/dydx/src/common/urls.rs +++ b/crates/adapters/dydx/src/common/urls.rs @@ -13,25 +13,109 @@ // limitations under the License. // ------------------------------------------------------------------------------------------------- -//! URL constants for dYdX API endpoints. +//! URL helpers for dYdX services. -// HTTP API URLs -/// dYdX v4 mainnet HTTP API base URL. -pub const DYDX_HTTP_URL: &str = "https://indexer.dydx.trade"; +use super::consts::{ + DYDX_GRPC_URLS, DYDX_HTTP_URL, DYDX_TESTNET_GRPC_URLS, DYDX_TESTNET_HTTP_URL, + DYDX_TESTNET_WS_URL, DYDX_WS_URL, +}; -/// dYdX v4 testnet HTTP API base URL. -pub const DYDX_TESTNET_HTTP_URL: &str = "https://indexer.v4testnet.dydx.exchange"; +/// Gets the HTTP base URL for the specified network. +#[must_use] +pub const fn http_base_url(is_testnet: bool) -> &'static str { + if is_testnet { + DYDX_TESTNET_HTTP_URL + } else { + DYDX_HTTP_URL + } +} -// WebSocket URLs -/// dYdX v4 mainnet WebSocket URL. -pub const DYDX_WS_URL: &str = "wss://indexer.dydx.trade/v4/ws"; +/// Gets the WebSocket URL for the specified network. +#[must_use] +pub const fn ws_url(is_testnet: bool) -> &'static str { + if is_testnet { + DYDX_TESTNET_WS_URL + } else { + DYDX_WS_URL + } +} -/// dYdX v4 testnet WebSocket URL. -pub const DYDX_TESTNET_WS_URL: &str = "wss://indexer.v4testnet.dydx.exchange/v4/ws"; +/// Gets the gRPC URLs with fallback support for the specified network. +/// +/// Returns an array of gRPC endpoints that should be tried in order. +/// This is important for DEX environments where individual validator nodes +/// can become unavailable or fail. +#[must_use] +pub const fn grpc_urls(is_testnet: bool) -> &'static [&'static str] { + if is_testnet { + DYDX_TESTNET_GRPC_URLS + } else { + DYDX_GRPC_URLS + } +} -// gRPC URLs -/// dYdX v4 mainnet gRPC URL (public node). -pub const DYDX_GRPC_URL: &str = "https://dydx-grpc.publicnode.com:443"; +/// Gets the primary gRPC URL for the specified network. +/// +/// # Notes +/// +/// For production use, consider using `grpc_urls()` to get all available +/// endpoints and implement fallback logic via `DydxGrpcClient::new_with_fallback()`. +#[must_use] +pub const fn grpc_url(is_testnet: bool) -> &'static str { + grpc_urls(is_testnet)[0] +} -/// dYdX v4 testnet gRPC URL. -pub const DYDX_TESTNET_GRPC_URL: &str = "https://dydx-testnet-grpc.publicnode.com:443"; +//////////////////////////////////////////////////////////////////////////////// +// Tests +//////////////////////////////////////////////////////////////////////////////// + +#[cfg(test)] +mod tests { + use rstest::rstest; + + use super::*; + + #[rstest] + fn test_http_urls() { + assert_eq!(http_base_url(false), "https://indexer.dydx.trade"); + assert_eq!( + http_base_url(true), + "https://indexer.v4testnet.dydx.exchange" + ); + } + + #[rstest] + fn test_ws_urls() { + assert_eq!(ws_url(false), "wss://indexer.dydx.trade/v4/ws"); + assert_eq!(ws_url(true), "wss://indexer.v4testnet.dydx.exchange/v4/ws"); + } + + #[rstest] + fn test_grpc_urls() { + let mainnet_urls = grpc_urls(false); + assert_eq!(mainnet_urls.len(), 3); + assert_eq!(mainnet_urls[0], "https://dydx-grpc.publicnode.com:443"); + assert_eq!(mainnet_urls[1], "https://dydx-ops-grpc.kingnodes.com:443"); + assert_eq!( + mainnet_urls[2], + "https://dydx-mainnet-grpc.autostake.com:443" + ); + + let testnet_urls = grpc_urls(true); + assert_eq!(testnet_urls.len(), 2); + assert_eq!( + testnet_urls[0], + "https://dydx-testnet-grpc.publicnode.com:443" + ); + assert_eq!(testnet_urls[1], "https://test-dydx-grpc.kingnodes.com:443"); + } + + #[rstest] + fn test_grpc_url() { + assert_eq!(grpc_url(false), "https://dydx-grpc.publicnode.com:443"); + assert_eq!( + grpc_url(true), + "https://dydx-testnet-grpc.publicnode.com:443" + ); + } +} diff --git a/crates/adapters/dydx/src/config.rs b/crates/adapters/dydx/src/config.rs index f9f420ee2944..0ddbb12b511f 100644 --- a/crates/adapters/dydx/src/config.rs +++ b/crates/adapters/dydx/src/config.rs @@ -17,6 +17,8 @@ use serde::{Deserialize, Serialize}; +use crate::common::consts::{DYDX_CHAIN_ID, DYDX_GRPC_URLS, DYDX_HTTP_URL, DYDX_WS_URL}; + /// Configuration for the dYdX adapter. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DydxAdapterConfig { @@ -25,20 +27,46 @@ pub struct DydxAdapterConfig { /// Base URL for the WebSocket API. pub ws_url: String, /// Base URL for the gRPC API (Cosmos SDK transactions). + /// + /// For backwards compatibility, a single URL can be provided. + /// Consider using `grpc_urls` for fallback support. pub grpc_url: String, + /// List of gRPC URLs with fallback support. + /// + /// If provided, the client will attempt to connect to each URL in order + /// until a successful connection is established. This is recommended for + /// production use in DEX environments where nodes can fail. + #[serde(default)] + pub grpc_urls: Vec, /// Chain ID (e.g., "dydx-mainnet-1" for mainnet, "dydx-testnet-4" for testnet). pub chain_id: String, /// Request timeout in seconds. pub timeout_secs: u64, } +impl DydxAdapterConfig { + /// Get the list of gRPC URLs to use for connection with fallback support. + /// + /// Returns `grpc_urls` if non-empty, otherwise falls back to a single-element + /// vector containing `grpc_url`. + #[must_use] + pub fn get_grpc_urls(&self) -> Vec { + if !self.grpc_urls.is_empty() { + self.grpc_urls.clone() + } else { + vec![self.grpc_url.clone()] + } + } +} + impl Default for DydxAdapterConfig { fn default() -> Self { Self { - base_url: "https://api.dydx.exchange".to_string(), - ws_url: "wss://api.dydx.exchange/v4/ws".to_string(), - grpc_url: "https://dydx-grpc.publicnode.com:443".to_string(), - chain_id: "dydx-mainnet-1".to_string(), + base_url: DYDX_HTTP_URL.to_string(), + ws_url: DYDX_WS_URL.to_string(), + grpc_url: DYDX_GRPC_URLS[0].to_string(), + grpc_urls: DYDX_GRPC_URLS.iter().map(|&s| s.to_string()).collect(), + chain_id: DYDX_CHAIN_ID.to_string(), timeout_secs: 30, } } diff --git a/crates/adapters/dydx/src/grpc/client.rs b/crates/adapters/dydx/src/grpc/client.rs index 10d7b5e56808..2240c03e2ab4 100644 --- a/crates/adapters/dydx/src/grpc/client.rs +++ b/crates/adapters/dydx/src/grpc/client.rs @@ -78,7 +78,7 @@ pub struct DydxGrpcClient { } impl DydxGrpcClient { - /// Create a new gRPC client. + /// Create a new gRPC client with a single URL. /// /// # Errors /// @@ -106,6 +106,49 @@ impl DydxGrpcClient { }) } + /// Create a new gRPC client with fallback support. + /// + /// Attempts to connect to each URL in the provided list until a successful + /// connection is established. This is useful for DEX environments where nodes + /// can fail and fallback options are needed. + /// + /// # Errors + /// + /// Returns an error if none of the provided URLs can establish a connection. + pub async fn new_with_fallback(grpc_urls: &[impl AsRef]) -> Result { + if grpc_urls.is_empty() { + return Err(DydxError::Config("No gRPC URLs provided".to_string())); + } + + let mut last_error = None; + + for (idx, url) in grpc_urls.iter().enumerate() { + let url_str = url.as_ref(); + tracing::debug!( + "Attempting to connect to gRPC node: {url_str} (attempt {}/{})", + idx + 1, + grpc_urls.len() + ); + + match Self::new(url_str.to_string()).await { + Ok(client) => { + tracing::info!("Successfully connected to gRPC node: {url_str}"); + return Ok(client); + } + Err(e) => { + tracing::warn!("Failed to connect to gRPC node {url_str}: {e}"); + last_error = Some(e); + } + } + } + + Err(last_error.unwrap_or_else(|| { + DydxError::Grpc(tonic::Status::unavailable( + "All gRPC connection attempts failed".to_string(), + )) + })) + } + /// Get the underlying gRPC channel. /// /// This can be used to create custom gRPC service clients. diff --git a/crates/adapters/dydx/src/grpc/mod.rs b/crates/adapters/dydx/src/grpc/mod.rs index 95ae225f964e..9e3b4f98b294 100644 --- a/crates/adapters/dydx/src/grpc/mod.rs +++ b/crates/adapters/dydx/src/grpc/mod.rs @@ -47,7 +47,7 @@ pub use builder::TxBuilder; pub use client::{DydxGrpcClient, Height, TxHash}; pub use order::{ DEFAULT_RUST_CLIENT_METADATA, OrderBuilder, OrderFlags, OrderGoodUntil, OrderMarketParams, - OrderType, SHORT_TERM_ORDER_MAXIMUM_LIFETIME, + SHORT_TERM_ORDER_MAXIMUM_LIFETIME, }; pub use types::ChainId; pub use wallet::{Account, Subaccount, Wallet}; diff --git a/crates/adapters/dydx/src/grpc/order.rs b/crates/adapters/dydx/src/grpc/order.rs index fec274922b51..939450424a31 100644 --- a/crates/adapters/dydx/src/grpc/order.rs +++ b/crates/adapters/dydx/src/grpc/order.rs @@ -24,6 +24,7 @@ //! See [dYdX order types](https://help.dydx.trade/en/articles/166985-short-term-vs-long-term-order-types). use chrono::{DateTime, Utc}; +use nautilus_model::enums::OrderType; use rust_decimal::{Decimal, prelude::ToPrimitive}; use crate::proto::dydxprotocol::{ @@ -53,23 +54,6 @@ pub enum OrderGoodUntil { Time(DateTime), } -/// Order type enumeration. -#[derive(Clone, Debug)] -pub enum OrderType { - /// Limit order. - Limit, - /// Market order. - Market, - /// Stop limit order. - StopLimit, - /// Stop market order. - StopMarket, - /// Take profit order. - TakeProfit, - /// Take profit market order. - TakeProfitMarket, -} - /// Order flags indicating order lifetime and execution type. #[derive(Clone, Debug)] pub enum OrderFlags { @@ -269,7 +253,7 @@ impl OrderBuilder { trigger_price: Decimal, size: Decimal, ) -> Self { - self.order_type = Some(OrderType::TakeProfit); + self.order_type = Some(OrderType::LimitIfTouched); self.price = Some(price); self.trigger_price = Some(trigger_price); self.side = Some(side); @@ -286,7 +270,7 @@ impl OrderBuilder { trigger_price: Decimal, size: Decimal, ) -> Self { - self.order_type = Some(OrderType::TakeProfitMarket); + self.order_type = Some(OrderType::MarketIfTouched); self.trigger_price = Some(trigger_price); self.side = Some(side); self.size = Some(size); diff --git a/crates/adapters/dydx/src/http/client.rs b/crates/adapters/dydx/src/http/client.rs index 95ac01f29f53..015ac1082baf 100644 --- a/crates/adapters/dydx/src/http/client.rs +++ b/crates/adapters/dydx/src/http/client.rs @@ -53,8 +53,8 @@ use tokio_util::sync::CancellationToken; use super::error::DydxHttpError; use crate::common::{ + consts::{DYDX_HTTP_URL, DYDX_TESTNET_HTTP_URL}, enums::DydxCandleResolution, - urls::{DYDX_HTTP_URL, DYDX_TESTNET_HTTP_URL}, }; /// Default dYdX Indexer REST API rate limit. diff --git a/crates/adapters/dydx/src/http/models.rs b/crates/adapters/dydx/src/http/models.rs index 66d5ea6a93bc..526456efd130 100644 --- a/crates/adapters/dydx/src/http/models.rs +++ b/crates/adapters/dydx/src/http/models.rs @@ -25,13 +25,14 @@ //! - Accounts: use chrono::{DateTime, Utc}; +use nautilus_model::enums::OrderSide; use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; use serde_with::{DisplayFromStr, serde_as}; use crate::common::enums::{ - DydxCandleResolution, DydxFillType, DydxLiquidity, DydxMarketStatus, DydxOrderSide, - DydxOrderStatus, DydxPositionStatus, DydxTickerType, DydxTimeInForce, + DydxCandleResolution, DydxFillType, DydxLiquidity, DydxMarketStatus, DydxOrderStatus, + DydxPositionStatus, DydxTickerType, DydxTimeInForce, }; //////////////////////////////////////////////////////////////////////////////// @@ -157,7 +158,7 @@ pub struct Trade { /// Unique trade ID. pub id: String, /// Order side that was the taker. - pub side: DydxOrderSide, + pub side: OrderSide, /// Trade size in base currency. #[serde_as(as = "DisplayFromStr")] pub size: Decimal, @@ -267,7 +268,7 @@ pub struct PerpetualPosition { /// Position status. pub status: DydxPositionStatus, /// Position side (determined by size sign). - pub side: DydxOrderSide, + pub side: OrderSide, /// Position size (negative for short). #[serde_as(as = "DisplayFromStr")] pub size: Decimal, @@ -314,7 +315,7 @@ pub struct AssetPosition { /// Asset symbol. pub symbol: String, /// Position side (always LONG for assets). - pub side: DydxOrderSide, + pub side: OrderSide, /// Asset size (balance). #[serde_as(as = "DisplayFromStr")] pub size: Decimal, @@ -344,7 +345,7 @@ pub struct Order { #[serde_as(as = "DisplayFromStr")] pub clob_pair_id: u32, /// Order side. - pub side: DydxOrderSide, + pub side: OrderSide, /// Order size. #[serde_as(as = "DisplayFromStr")] pub size: Decimal, @@ -405,7 +406,7 @@ pub struct Fill { /// Unique fill ID. pub id: String, /// Order side. - pub side: DydxOrderSide, + pub side: OrderSide, /// Liquidity side (MAKER/TAKER). pub liquidity: DydxLiquidity, /// Fill type. @@ -523,7 +524,7 @@ pub struct PlaceOrderRequest { /// CLOB pair ID. pub clob_pair_id: u32, /// Order side. - pub side: DydxOrderSide, + pub side: OrderSide, /// Order size in quantums. pub quantums: u64, /// Order subticks (price representation). From aad34a113b65c4a94f8b5971b46c2085b0045e2b Mon Sep 17 00:00:00 2001 From: Vadim Nicolai Date: Wed, 5 Nov 2025 07:55:05 +0200 Subject: [PATCH 10/10] Reconnects --- crates/adapters/dydx/src/common/enums.rs | 21 ---- crates/adapters/dydx/src/common/urls.rs | 55 --------- crates/adapters/dydx/src/grpc/client.rs | 148 ++++++++++++++++++++++- 3 files changed, 147 insertions(+), 77 deletions(-) diff --git a/crates/adapters/dydx/src/common/enums.rs b/crates/adapters/dydx/src/common/enums.rs index 597a3b356c07..4c2eeac7d9d3 100644 --- a/crates/adapters/dydx/src/common/enums.rs +++ b/crates/adapters/dydx/src/common/enums.rs @@ -336,27 +336,6 @@ pub enum DydxCandleResolution { #[cfg(test)] mod tests { use super::*; - use nautilus_model::enums::OrderSide; - - #[test] - fn test_order_side_serialization() { - // Test that OrderSide serializes to SCREAMING_SNAKE_CASE as expected by dYdX API - let buy = OrderSide::Buy; - let sell = OrderSide::Sell; - - assert_eq!(serde_json::to_string(&buy).unwrap(), r#""BUY""#); - assert_eq!(serde_json::to_string(&sell).unwrap(), r#""SELL""#); - - // Test deserialization - assert_eq!( - serde_json::from_str::(r#""BUY""#).unwrap(), - OrderSide::Buy - ); - assert_eq!( - serde_json::from_str::(r#""SELL""#).unwrap(), - OrderSide::Sell - ); - } #[test] fn test_order_status_conversion() { diff --git a/crates/adapters/dydx/src/common/urls.rs b/crates/adapters/dydx/src/common/urls.rs index 0f2c17e6a99e..c23a1dda347d 100644 --- a/crates/adapters/dydx/src/common/urls.rs +++ b/crates/adapters/dydx/src/common/urls.rs @@ -64,58 +64,3 @@ pub const fn grpc_urls(is_testnet: bool) -> &'static [&'static str] { pub const fn grpc_url(is_testnet: bool) -> &'static str { grpc_urls(is_testnet)[0] } - -//////////////////////////////////////////////////////////////////////////////// -// Tests -//////////////////////////////////////////////////////////////////////////////// - -#[cfg(test)] -mod tests { - use rstest::rstest; - - use super::*; - - #[rstest] - fn test_http_urls() { - assert_eq!(http_base_url(false), "https://indexer.dydx.trade"); - assert_eq!( - http_base_url(true), - "https://indexer.v4testnet.dydx.exchange" - ); - } - - #[rstest] - fn test_ws_urls() { - assert_eq!(ws_url(false), "wss://indexer.dydx.trade/v4/ws"); - assert_eq!(ws_url(true), "wss://indexer.v4testnet.dydx.exchange/v4/ws"); - } - - #[rstest] - fn test_grpc_urls() { - let mainnet_urls = grpc_urls(false); - assert_eq!(mainnet_urls.len(), 3); - assert_eq!(mainnet_urls[0], "https://dydx-grpc.publicnode.com:443"); - assert_eq!(mainnet_urls[1], "https://dydx-ops-grpc.kingnodes.com:443"); - assert_eq!( - mainnet_urls[2], - "https://dydx-mainnet-grpc.autostake.com:443" - ); - - let testnet_urls = grpc_urls(true); - assert_eq!(testnet_urls.len(), 2); - assert_eq!( - testnet_urls[0], - "https://dydx-testnet-grpc.publicnode.com:443" - ); - assert_eq!(testnet_urls[1], "https://test-dydx-grpc.kingnodes.com:443"); - } - - #[rstest] - fn test_grpc_url() { - assert_eq!(grpc_url(false), "https://dydx-grpc.publicnode.com:443"); - assert_eq!( - grpc_url(true), - "https://dydx-testnet-grpc.publicnode.com:443" - ); - } -} diff --git a/crates/adapters/dydx/src/grpc/client.rs b/crates/adapters/dydx/src/grpc/client.rs index 2240c03e2ab4..e7e960a6fed1 100644 --- a/crates/adapters/dydx/src/grpc/client.rs +++ b/crates/adapters/dydx/src/grpc/client.rs @@ -65,6 +65,7 @@ pub struct Height(pub u32); /// - Transaction signing and broadcasting. /// - Account query operations. /// - Order placement and management via Cosmos SDK messages. +/// - Connection management and automatic failover to fallback nodes. #[derive(Debug, Clone)] pub struct DydxGrpcClient { channel: Channel, @@ -75,6 +76,7 @@ pub struct DydxGrpcClient { clob: ClobClient, perpetuals: PerpetualsClient, subaccounts: SubaccountsClient, + current_url: String, } impl DydxGrpcClient { @@ -84,7 +86,7 @@ impl DydxGrpcClient { /// /// Returns an error if the gRPC connection cannot be established. pub async fn new(grpc_url: String) -> Result { - let channel = Channel::from_shared(grpc_url) + let channel = Channel::from_shared(grpc_url.clone()) .map_err(|e| DydxError::Config(format!("Invalid gRPC URL: {e}")))? .connect() .await @@ -103,6 +105,7 @@ impl DydxGrpcClient { perpetuals: PerpetualsClient::new(channel.clone()), subaccounts: SubaccountsClient::new(channel.clone()), channel, + current_url: grpc_url, }) } @@ -149,6 +152,89 @@ impl DydxGrpcClient { })) } + /// Reconnect to a different gRPC node from the fallback list. + /// + /// Attempts to establish a new connection to each URL in the provided list + /// until successful. This is useful when the current node fails and you need + /// to failover to a different validator node. + /// + /// # Errors + /// + /// Returns an error if none of the provided URLs can establish a connection. + pub async fn reconnect_with_fallback( + &mut self, + grpc_urls: &[impl AsRef], + ) -> Result<(), DydxError> { + if grpc_urls.is_empty() { + return Err(DydxError::Config("No gRPC URLs provided".to_string())); + } + + let mut last_error = None; + + for (idx, url) in grpc_urls.iter().enumerate() { + let url_str = url.as_ref(); + + // Skip if it's the same URL we're currently connected to + if url_str == self.current_url { + tracing::debug!("Skipping current URL: {url_str}"); + continue; + } + + tracing::debug!( + "Attempting to reconnect to gRPC node: {url_str} (attempt {}/{})", + idx + 1, + grpc_urls.len() + ); + + let channel = match Channel::from_shared(url_str.to_string()) + .map_err(|e| DydxError::Config(format!("Invalid gRPC URL: {e}"))) + { + Ok(ch) => ch, + Err(e) => { + last_error = Some(e); + continue; + } + }; + + match channel.connect().await { + Ok(connected_channel) => { + tracing::info!("Successfully reconnected to gRPC node: {url_str}"); + + // Update all service clients with the new channel + self.channel = connected_channel.clone(); + self.auth = AuthClient::new(connected_channel.clone()); + self.bank = BankClient::new(connected_channel.clone()); + self.base = BaseClient::new(connected_channel.clone()); + self.tx = TxClient::new(connected_channel.clone()); + self.clob = ClobClient::new(connected_channel.clone()); + self.perpetuals = PerpetualsClient::new(connected_channel.clone()); + self.subaccounts = SubaccountsClient::new(connected_channel); + self.current_url = url_str.to_string(); + + return Ok(()); + } + Err(e) => { + tracing::warn!("Failed to reconnect to gRPC node {url_str}: {e}"); + last_error = Some(DydxError::Grpc(tonic::Status::unavailable(format!( + "Connection failed: {e}" + )))); + } + } + } + + Err(last_error.unwrap_or_else(|| { + DydxError::Grpc(tonic::Status::unavailable( + "All gRPC reconnection attempts failed".to_string(), + )) + })) + } + + /// Get the currently connected gRPC node URL. + #[must_use] + pub fn current_url(&self) -> &str { + &self.current_url + } + /// Get the underlying gRPC channel. /// /// This can be used to create custom gRPC service clients. @@ -381,3 +467,63 @@ impl DydxGrpcClient { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_current_url_tracked() { + // Test that we can track the current URL + let url = "https://example.com:9090".to_string(); + let client = DydxGrpcClient { + channel: Channel::from_static("https://example.com:9090"), + auth: AuthClient::new(Channel::from_static("https://example.com:9090")), + bank: BankClient::new(Channel::from_static("https://example.com:9090")), + base: BaseClient::new(Channel::from_static("https://example.com:9090")), + tx: TxClient::new(Channel::from_static("https://example.com:9090")), + clob: ClobClient::new(Channel::from_static("https://example.com:9090")), + perpetuals: PerpetualsClient::new(Channel::from_static("https://example.com:9090")), + subaccounts: SubaccountsClient::new(Channel::from_static("https://example.com:9090")), + current_url: url.clone(), + }; + + assert_eq!(client.current_url(), &url); + } + + #[tokio::test] + async fn test_new_with_fallback_empty_urls() { + let urls: Vec = vec![]; + let result = DydxGrpcClient::new_with_fallback(&urls).await; + + assert!(result.is_err()); + match result.unwrap_err() { + DydxError::Config(msg) => assert_eq!(msg, "No gRPC URLs provided"), + _ => panic!("Expected Config error"), + } + } + + #[tokio::test] + async fn test_reconnect_with_fallback_empty_urls() { + let mut client = DydxGrpcClient { + channel: Channel::from_static("https://example.com:9090"), + auth: AuthClient::new(Channel::from_static("https://example.com:9090")), + bank: BankClient::new(Channel::from_static("https://example.com:9090")), + base: BaseClient::new(Channel::from_static("https://example.com:9090")), + tx: TxClient::new(Channel::from_static("https://example.com:9090")), + clob: ClobClient::new(Channel::from_static("https://example.com:9090")), + perpetuals: PerpetualsClient::new(Channel::from_static("https://example.com:9090")), + subaccounts: SubaccountsClient::new(Channel::from_static("https://example.com:9090")), + current_url: "https://example.com:9090".to_string(), + }; + + let urls: Vec = vec![]; + let result = client.reconnect_with_fallback(&urls).await; + + assert!(result.is_err()); + match result.unwrap_err() { + DydxError::Config(msg) => assert_eq!(msg, "No gRPC URLs provided"), + _ => panic!("Expected Config error"), + } + } +}