diff --git a/Cargo.lock b/Cargo.lock index ba46052..7450042 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -21,9 +21,9 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "alloy" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e15860af634cad451f598712c24ca7fd9b45d84fff68ab8d4967567fa996c64" +checksum = "f07655fedc35188f3c50ff8fc6ee45703ae14ef1bc7ae7d80e23a747012184e3" dependencies = [ "alloy-consensus", "alloy-contract", @@ -47,9 +47,9 @@ dependencies = [ [[package]] name = "alloy-chains" -version = "0.2.20" +version = "0.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bc32535569185cbcb6ad5fa64d989a47bccb9a08e27284b1f2a3ccf16e6d010" +checksum = "35d744058a9daa51a8cf22a3009607498fcf82d3cf4c5444dd8056cdf651f471" dependencies = [ "alloy-primitives 1.4.1", "num_enum", @@ -58,9 +58,9 @@ dependencies = [ [[package]] name = "alloy-consensus" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b6440213a22df93a87ed512d2f668e7dc1d62a05642d107f82d61edc9e12370" +checksum = "2e318e25fb719e747a7e8db1654170fc185024f3ed5b10f86c08d448a912f6e2" dependencies = [ "alloy-eips", "alloy-primitives 1.4.1", @@ -85,9 +85,9 @@ dependencies = [ [[package]] name = "alloy-consensus-any" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15d0bea09287942405c4f9d2a4f22d1e07611c2dbd9d5bf94b75366340f9e6e0" +checksum = "364380a845193a317bcb7a5398fc86cdb66c47ebe010771dde05f6869bf9e64a" dependencies = [ "alloy-consensus", "alloy-eips", @@ -99,9 +99,9 @@ dependencies = [ [[package]] name = "alloy-contract" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d69af404f1d00ddb42f2419788fa87746a4cd13bab271916d7726fda6c792d94" +checksum = "08d39c80ffc806f27a76ed42f3351a455f3dc4f81d6ff92c8aad2cf36b7d3a34" dependencies = [ "alloy-consensus", "alloy-dyn-abi", @@ -190,9 +190,9 @@ dependencies = [ [[package]] name = "alloy-eips" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bd2c7ae05abcab4483ce821f12f285e01c0b33804e6883dd9ca1569a87ee2be" +checksum = "a4c4d7c5839d9f3a467900c625416b24328450c65702eb3d8caff8813e4d1d33" dependencies = [ "alloy-eip2124", "alloy-eip2930", @@ -213,9 +213,9 @@ dependencies = [ [[package]] name = "alloy-genesis" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc47eaae86488b07ea8e20236184944072a78784a1f4993f8ec17b3aa5d08c21" +checksum = "1ba4b1be0988c11f0095a2380aa596e35533276b8fa6c9e06961bbfe0aebcac5" dependencies = [ "alloy-eips", "alloy-primitives 1.4.1", @@ -240,9 +240,9 @@ dependencies = [ [[package]] name = "alloy-json-rpc" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "003f46c54f22854a32b9cc7972660a476968008ad505427eabab49225309ec40" +checksum = "f72cf87cda808e593381fb9f005ffa4d2475552b7a6c5ac33d087bf77d82abd0" dependencies = [ "alloy-primitives 1.4.1", "alloy-sol-types", @@ -255,9 +255,9 @@ dependencies = [ [[package]] name = "alloy-network" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f4029954d9406a40979f3a3b46950928a0fdcfe3ea8a9b0c17490d57e8aa0e3" +checksum = "12aeb37b6f2e61b93b1c3d34d01ee720207c76fe447e2a2c217e433ac75b17f5" dependencies = [ "alloy-consensus", "alloy-consensus-any", @@ -281,9 +281,9 @@ dependencies = [ [[package]] name = "alloy-network-primitives" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7805124ad69e57bbae7731c9c344571700b2a18d351bda9e0eba521c991d1bcb" +checksum = "abd29ace62872083e30929cd9b282d82723196d196db589f3ceda67edcc05552" dependencies = [ "alloy-consensus", "alloy-eips", @@ -348,9 +348,9 @@ dependencies = [ [[package]] name = "alloy-provider" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d369e12c92870d069e0c9dc5350377067af8a056e29e3badf8446099d7e00889" +checksum = "9b710636d7126e08003b8217e24c09f0cca0b46d62f650a841736891b1ed1fc1" dependencies = [ "alloy-chains", "alloy-consensus", @@ -394,9 +394,9 @@ dependencies = [ [[package]] name = "alloy-pubsub" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f77d20cdbb68a614c7a86b3ffef607b37d087bb47a03c58f4c3f8f99bc3ace3b" +checksum = "cdd4c64eb250a18101d22ae622357c6b505e158e9165d4c7974d59082a600c5e" dependencies = [ "alloy-json-rpc", "alloy-primitives 1.4.1", @@ -438,9 +438,9 @@ dependencies = [ [[package]] name = "alloy-rpc-client" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31c89883fe6b7381744cbe80fef638ac488ead4f1956a4278956a1362c71cd2e" +checksum = "d0882e72d2c1c0c79dcf4ab60a67472d3f009a949f774d4c17d0bdb669cfde05" dependencies = [ "alloy-json-rpc", "alloy-primitives 1.4.1", @@ -464,9 +464,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64e279e6d40ee40fe8f76753b678d8d5d260cb276dc6c8a8026099b16d2b43f4" +checksum = "39cf1398cb33aacb139a960fa3d8cf8b1202079f320e77e952a0b95967bf7a9f" dependencies = [ "alloy-primitives 1.4.1", "alloy-rpc-types-anvil", @@ -481,9 +481,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-anvil" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e176c26fdd87893b6afeb5d92099d8f7e7a1fe11d6f4fe0883d6e33ac5f31ba" +checksum = "c3ce4c24e416bd0f17fceeb2f26cd8668df08fe19e1dc02f9d41c3b8ed1e93e0" dependencies = [ "alloy-primitives 1.4.1", "alloy-rpc-types-eth", @@ -493,9 +493,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-any" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b43c1622aac2508d528743fd4cfdac1dea92d5a8fa894038488ff7edd0af0b32" +checksum = "6a63fb40ed24e4c92505f488f9dd256e2afaed17faa1b7a221086ebba74f4122" dependencies = [ "alloy-consensus-any", "alloy-rpc-types-eth", @@ -504,9 +504,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-debug" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b2ca3a434a6d49910a7e8e51797eb25db42ef8a5578c52d877fcb26d0afe7bc" +checksum = "4936f579d9d10eae01772b2ab3497f9d568684f05f26f8175e12f9a1a2babc33" dependencies = [ "alloy-primitives 1.4.1", "derive_more", @@ -516,9 +516,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-engine" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4c53a8b0905d931e7921774a1830609713bd3e8222347963172b03a3ecc68" +checksum = "4c60bdce3be295924122732b7ecd0b2495ce4790bedc5370ca7019c08ad3f26e" dependencies = [ "alloy-consensus", "alloy-eips", @@ -533,9 +533,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-eth" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed5fafb741c19b3cca4cdd04fa215c89413491f9695a3e928dee2ae5657f607e" +checksum = "9eae0c7c40da20684548cbc8577b6b7447f7bf4ddbac363df95e3da220e41e72" dependencies = [ "alloy-consensus", "alloy-consensus-any", @@ -554,9 +554,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-trace" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c55324323aa634b01bdecb2d47462a8dce05f5505b14a6e5db361eef16eda476" +checksum = "ef206a4b8d436fbb7cf2e6a61c692d11df78f9382becc3c9a283bd58e64f0583" dependencies = [ "alloy-primitives 1.4.1", "alloy-rpc-types-eth", @@ -568,9 +568,9 @@ dependencies = [ [[package]] name = "alloy-rpc-types-txpool" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96b1aa28effb6854be356ce92ed64cea3b323acd04c3f8bfb5126e2839698043" +checksum = "ecb5a795264a02222f9534435b8f40dcbd88de8e9d586647884aae24f389ebf2" dependencies = [ "alloy-primitives 1.4.1", "alloy-rpc-types-eth", @@ -580,9 +580,9 @@ dependencies = [ [[package]] name = "alloy-serde" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6f180c399ca7c1e2fe17ea58343910cad0090878a696ff5a50241aee12fc529" +checksum = "c0df1987ed0ff2d0159d76b52e7ddfc4e4fbddacc54d2fbee765e0d14d7c01b5" dependencies = [ "alloy-primitives 1.4.1", "serde", @@ -591,9 +591,9 @@ dependencies = [ [[package]] name = "alloy-signer" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc39ad2c0a3d2da8891f4081565780703a593f090f768f884049aa3aa929cbc" +checksum = "6ff69deedee7232d7ce5330259025b868c5e6a52fa8dffda2c861fb3a5889b24" dependencies = [ "alloy-primitives 1.4.1", "async-trait", @@ -606,9 +606,9 @@ dependencies = [ [[package]] name = "alloy-signer-local" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "930e17cb1e46446a193a593a3bfff8d0ecee4e510b802575ebe300ae2e43ef75" +checksum = "72cfe0be3ec5a8c1a46b2e5a7047ed41121d360d97f4405bb7c1c784880c86cb" dependencies = [ "alloy-consensus", "alloy-network", @@ -695,9 +695,9 @@ dependencies = [ [[package]] name = "alloy-transport" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cae82426d98f8bc18f53c5223862907cac30ab8fc5e4cd2bb50808e6d3ab43d8" +checksum = "be98b07210d24acf5b793c99b759e9a696e4a2e67593aec0487ae3b3e1a2478c" dependencies = [ "alloy-json-rpc", "auto_impl", @@ -718,9 +718,9 @@ dependencies = [ [[package]] name = "alloy-transport-http" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90aa6825760905898c106aba9c804b131816a15041523e80b6d4fe7af6380ada" +checksum = "4198a1ee82e562cab85e7f3d5921aab725d9bd154b6ad5017f82df1695877c97" dependencies = [ "alloy-json-rpc", "alloy-transport", @@ -733,9 +733,9 @@ dependencies = [ [[package]] name = "alloy-transport-ipc" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ace83a4a6bb896e5894c3479042e6ba78aa5271dde599aa8c36a021d49cc8cc" +checksum = "d8db249779ebc20dc265920c7e706ed0d31dbde8627818d1cbde60919b875bb0" dependencies = [ "alloy-json-rpc", "alloy-pubsub", @@ -753,9 +753,9 @@ dependencies = [ [[package]] name = "alloy-transport-ws" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86c9ab4c199e3a8f3520b60ba81aa67bb21fed9ed0d8304e0569094d0758a56f" +checksum = "5ad2344a12398d7105e3722c9b7a7044ea837128e11d453604dec6e3731a86e2" dependencies = [ "alloy-pubsub", "alloy-transport", @@ -764,7 +764,7 @@ dependencies = [ "rustls", "serde_json", "tokio", - "tokio-tungstenite", + "tokio-tungstenite 0.26.2", "tracing", "ws_stream_wasm", ] @@ -787,9 +787,9 @@ dependencies = [ [[package]] name = "alloy-tx-macros" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae109e33814b49fc0a62f2528993aa8a2dd346c26959b151f05441dc0b9da292" +checksum = "333544408503f42d7d3792bfc0f7218b643d968a03d2c0ed383ae558fb4a76d0" dependencies = [ "darling", "proc-macro2", @@ -1091,9 +1091,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" -version = "1.8.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" +checksum = "0e050f626429857a27ddccb31e0aca21356bfa709c04041aefddac081a8f068a" [[package]] name = "bimap" @@ -1118,15 +1118,15 @@ checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" [[package]] name = "bitcoin-io" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b47c4ab7a93edb0c7198c5535ed9b52b63095f4e9b45279c6736cec4b856baf" +checksum = "2dee39a0ee5b4095224a0cfc6bf4cc1baf0f9624b96b367e53b66d974e51d953" [[package]] name = "bitcoin_hashes" -version = "0.14.0" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb18c03d0db0247e147a21a6faafd5a7eb851c743db062de72018b6b7e8e4d16" +checksum = "26ec84b80c482df901772e931a9a681e26a1b9ee2302edeff23cb30328745c8b" dependencies = [ "bitcoin-io", "hex-conservative", @@ -1260,9 +1260,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.48" +version = "1.2.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c481bdbf0ed3b892f6f806287d72acd515b352a4ec27a208489b8c1bc839633a" +checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215" dependencies = [ "find-msvc-tools", "shlex", @@ -1330,6 +1330,15 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -1396,9 +1405,9 @@ dependencies = [ [[package]] name = "crypto-common" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", "typenum", @@ -1493,21 +1502,23 @@ dependencies = [ [[package]] name = "derive_more" -version = "2.0.1" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +checksum = "10b768e943bed7bf2cab53df09f4bc34bfd217cdb57d971e769874c9a6710618" dependencies = [ "derive_more-impl", ] [[package]] name = "derive_more-impl" -version = "2.0.1" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" +checksum = "6d286bfdaf75e988b4a78e013ecd79c581e06399ab53fbacd2d916c2f904f30b" dependencies = [ + "convert_case", "proc-macro2", "quote", + "rustc_version 0.4.1", "syn 2.0.111", "unicode-xid", ] @@ -1864,9 +1875,9 @@ checksum = "42012b0f064e01aa58b545fe3727f90f7dd4020f4a3ea735b50344965f5a57e9" [[package]] name = "generic-array" -version = "0.14.9" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", @@ -2106,9 +2117,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.18" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52e9a2a24dc5c6821e71a7030e1e14b7b632acac55c40e9d2e082c621261bb56" +checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" dependencies = [ "base64", "bytes", @@ -2202,9 +2213,9 @@ checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" [[package]] name = "icu_properties" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" dependencies = [ "icu_collections", "icu_locale_core", @@ -2216,9 +2227,9 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" [[package]] name = "icu_provider" @@ -2420,9 +2431,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.177" +version = "0.2.178" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" +checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" [[package]] name = "libm" @@ -2453,9 +2464,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.28" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "lru" @@ -2497,9 +2508,9 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "mio" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ "libc", "wasi", @@ -3172,6 +3183,8 @@ dependencies = [ "alloy-primitives 0.8.26", "anyhow", "async-trait", + "futures", + "futures-util", "once_cell", "reqwest", "rmp-serde", @@ -3181,6 +3194,7 @@ dependencies = [ "thiserror 1.0.69", "tokio", "tokio-test", + "tokio-tungstenite 0.28.0", "tracing", "tracing-subscriber", "url", @@ -4108,10 +4122,24 @@ dependencies = [ "rustls-pki-types", "tokio", "tokio-rustls", - "tungstenite", + "tungstenite 0.26.2", "webpki-roots 0.26.11", ] +[[package]] +name = "tokio-tungstenite" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d25a406cddcc431a75d3d9afc6a7c0f7428d4891dd973e4d54c56b46127bf857" +dependencies = [ + "futures-util", + "log", + "native-tls", + "tokio", + "tokio-native-tls", + "tungstenite 0.28.0", +] + [[package]] name = "tokio-util" version = "0.7.17" @@ -4136,9 +4164,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.23.7" +version = "0.23.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" +checksum = "5d7cbc3b4b49633d57a0509303158ca50de80ae32c265093b24c414705807832" dependencies = [ "indexmap 2.12.1", "toml_datetime", @@ -4172,9 +4200,9 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.7" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf146f99d442e8e68e585f5d798ccd3cad9a7835b917e09728880a862706456" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ "bitflags", "bytes", @@ -4282,6 +4310,24 @@ dependencies = [ "utf-8", ] +[[package]] +name = "tungstenite" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442" +dependencies = [ + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "native-tls", + "rand 0.9.2", + "sha1", + "thiserror 2.0.17", + "utf-8", +] + [[package]] name = "typenum" version = "1.19.0" @@ -4318,6 +4364,12 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + [[package]] name = "unicode-xid" version = "0.2.6" @@ -4356,9 +4408,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.18.1" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" +checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" dependencies = [ "js-sys", "wasm-bindgen", diff --git a/Cargo.toml b/Cargo.toml index aafa4dd..5a23f37 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,14 +24,17 @@ alloy = { version = "1", features = ["full"] } alloy-primitives = { version = "0.8.0", features = ["serde"] } anyhow = "1.0" async-trait = "0.1" +futures = "0.3.31" +futures-util = { version = "0.3.31", features = ["sink"] } once_cell = "1.19" reqwest = { version = "=0.12.23", features = ["json", "rustls-tls"] } rmp-serde = "1.3.0" -rust_decimal = { version = "1.35", features = ["serde", "serde-float"] } +rust_decimal = { version = "1.35", features = ["serde", "serde-float", "serde-str", "serde-with-str"] } serde = { version = "1.0", features = ["derive"] } serde_json = { version = "1.0", features = ["preserve_order"] } thiserror = "1.0" tokio = { version = "1.38", features = ["full"] } +tokio-tungstenite = { version = "0.28.0", features = ["native-tls"] } tracing = "0.1" tracing-subscriber = "0.3.22" url = "2.5" @@ -46,7 +49,7 @@ missing_docs = "allow" [lints.clippy] unwrap_used = "deny" -expect_used = "deny" + panic = "deny" unreachable = "deny" arithmetic_side_effects = "deny" @@ -66,6 +69,7 @@ cast_possible_truncation = "deny" cast_precision_loss = "deny" as_conversions = "warn" +expect_used = "allow" map_unwrap_or = "allow" min_ident_chars = "allow" question_mark_used = "allow" @@ -86,6 +90,8 @@ must_use_candidate = "allow" uninlined_format_args = "allow" similar_names = "allow" redundant_closure_for_method_calls = "allow" +inconsistent_struct_constructor = "allow" +match_same_arms = "allow" pedantic = { level = "warn", priority = -1 } restriction = { level = "allow", priority = -1 } @@ -151,6 +157,10 @@ path = "examples/advanced_order.rs" name = "twap_order" path = "examples/twap_order.rs" +[[example]] +name = "subscriptions" +path = "examples/subscriptions.rs" + [profile.release] codegen-units = 1 lto = true diff --git a/examples/advanced_order.rs b/examples/advanced_order.rs index 06df320..c859ee8 100644 --- a/examples/advanced_order.rs +++ b/examples/advanced_order.rs @@ -1,4 +1,4 @@ -#![allow(clippy::all)] +#![allow(clippy::too_many_lines)] use rhyperliquid::{ example_helpers::load_signer, init_tracing::init_tracing, diff --git a/examples/subscriptions.rs b/examples/subscriptions.rs new file mode 100644 index 0000000..7cb3fb3 --- /dev/null +++ b/examples/subscriptions.rs @@ -0,0 +1,119 @@ +#![allow(clippy::too_many_lines)] +use rhyperliquid::{ + example_helpers::{testnet_client, user}, + init_tracing::init_tracing, +}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + init_tracing(); + + let client = testnet_client()?; + let mut subs = client.subscriptions().await?; + let user = user(); + + subs.subscribe_all_mids(None).await?; + subs.subscribe_candle("BTC", "5m".to_string()).await?; + subs.subscribe_l2_book("BTC", None, None).await?; + subs.subscribe_trades("BTC").await?; + subs.subscribe_notifications(user.clone()).await?; + subs.subscribe_webdata3(user.clone()).await?; + subs.subscribe_twap_states(user.clone()).await?; + subs.subscribe_clearinghouse_state(user.clone()).await?; + subs.subscribe_open_orders(user.clone()).await?; + subs.subscribe_user_events(user.clone()).await?; + subs.subscribe_user_fills(user.clone()).await?; + subs.subscribe_user_funding(user.clone()).await?; + subs.subscribe_user_non_funding_ledger_updates(user.clone()) + .await?; + subs.subscribe_active_asset_ctx("BTC").await?; + subs.subscribe_active_asset_data(user.clone(), "BTC") + .await?; + subs.subscribe_user_twap_slice_fills(user.clone()).await?; + subs.subscribe_user_twap_history(user.clone()).await?; + subs.subscribe_bbo("BTC").await?; + + // Match and receive subscription messages + while let Ok(msg) = subs.events.recv().await { + match msg { + rhyperliquid::types::ws::SubscriptionResponse::Error(e) => { + tracing::info!("Error: {:?}", e); + } + rhyperliquid::types::ws::SubscriptionResponse::SubscriptionResponse( + subscription_confirmation, + ) => { + tracing::info!("SubscriptionResponse: {:?}", subscription_confirmation); + } + rhyperliquid::types::ws::SubscriptionResponse::AllMids(ws_all_mids) => { + tracing::info!("AllMids: {:?}", ws_all_mids); + } + rhyperliquid::types::ws::SubscriptionResponse::Candle(ws_candle) => { + tracing::info!("Candle: {:?}", ws_candle); + } + rhyperliquid::types::ws::SubscriptionResponse::Trades(ws_trade) => { + tracing::info!("Trades: {:?}", ws_trade); + } + rhyperliquid::types::ws::SubscriptionResponse::L2Book(ws_book) => { + tracing::info!("L2Book: {:?}", ws_book); + } + rhyperliquid::types::ws::SubscriptionResponse::Notification(ws_notification) => { + tracing::info!("Notification: {:?}", ws_notification); + } + rhyperliquid::types::ws::SubscriptionResponse::WebData3(ws_web_data3) => { + tracing::info!("WebData3: {:?}", ws_web_data3); + } + rhyperliquid::types::ws::SubscriptionResponse::TwapStates(ws_twap_states) => { + tracing::info!("TwapStates: {:?}", ws_twap_states); + } + rhyperliquid::types::ws::SubscriptionResponse::OpenOrders(ws_open_orders) => { + tracing::info!("OpenOrders: {:?}", ws_open_orders); + } + rhyperliquid::types::ws::SubscriptionResponse::UserEvents(ws_user_event) => { + tracing::info!("UserEvents: {:?}", ws_user_event); + } + rhyperliquid::types::ws::SubscriptionResponse::UserNonFundingLedgerUpdates( + ws_user_non_funding_ledger_update, + ) => { + tracing::info!( + "UserNonFundingLedgerUpdate: {:?}", + ws_user_non_funding_ledger_update + ); + } + rhyperliquid::types::ws::SubscriptionResponse::ActiveAssetCtx(ws_asset_ctx) => { + tracing::info!("ActiveAssetCtx: {:?}", ws_asset_ctx); + } + rhyperliquid::types::ws::SubscriptionResponse::ActiveAssetData( + ws_active_asset_data, + ) => { + tracing::info!("ActiveAssetData: {:?}", ws_active_asset_data); + } + rhyperliquid::types::ws::SubscriptionResponse::UserTwapSliceFills( + ws_user_twap_slice_fills, + ) => { + tracing::info!("UserTwapSliceFills: {:?}", ws_user_twap_slice_fills); + } + rhyperliquid::types::ws::SubscriptionResponse::UserTwapHistory( + ws_user_twap_history, + ) => { + tracing::info!("UserTwapHistory: {:?}", ws_user_twap_history); + } + rhyperliquid::types::ws::SubscriptionResponse::Bbo(ws_bbo) => { + tracing::info!("Bbo: {:?}", ws_bbo); + } + rhyperliquid::types::ws::SubscriptionResponse::Pong => tracing::info!("Pong"), + rhyperliquid::types::ws::SubscriptionResponse::ClearinghouseState( + ws_clearinghouse_state, + ) => { + tracing::info!("ClearinghouseState: {:?}", ws_clearinghouse_state); + } + rhyperliquid::types::ws::SubscriptionResponse::UserFills(ws_user_fills) => { + tracing::info!("User Fills: {:?}", ws_user_fills); + } + rhyperliquid::types::ws::SubscriptionResponse::UserFundings(ws_user_fundings) => { + tracing::info!("User Fundings: {:?}", ws_user_fundings); + } + } + } + + Ok(()) +} diff --git a/src/api/mod.rs b/src/api/mod.rs index 8ebfc7b..eed3458 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -2,3 +2,6 @@ pub mod exchange; pub mod info; pub mod request_util; pub mod response; +mod subscription; + +pub use subscription::{StreamMessage, SubscriptionClient, SubscriptionConfig}; diff --git a/src/api/subscription/mod.rs b/src/api/subscription/mod.rs new file mode 100644 index 0000000..de20205 --- /dev/null +++ b/src/api/subscription/mod.rs @@ -0,0 +1,4 @@ +mod sender; +mod ws; + +pub use ws::{StreamMessage, SubscriptionClient, SubscriptionConfig}; diff --git a/src/api/subscription/sender.rs b/src/api/subscription/sender.rs new file mode 100644 index 0000000..f79a301 --- /dev/null +++ b/src/api/subscription/sender.rs @@ -0,0 +1,70 @@ +use crate::types::ws::{ + WsActiveAssetCtx, WsActiveAssetData, WsAllMids, WsBbo, WsBook, WsCandle, WsClearinghouseState, + WsNotification, WsOpenOrders, WsTrade, WsTwapStates, WsUserEvent, WsUserFills, WsUserFundings, + WsUserNonFundingLedgerUpdate, WsUserTwapHistory, WsUserTwapSliceFills, WsWebData3, +}; +use tokio::sync::broadcast::Sender; + +/// A mapping of subscription identifiers to sender channels and subscription +/// parameters. +#[derive(Clone, Default)] +pub struct StreamSenders { + pub(crate) all_mids: (Option>, Option), + pub(crate) candle: (Option>, Option, Option), + pub(crate) trades: ( + Option>, + Option, + Option, + Option, + ), + pub(crate) l2book: ( + Option>, + Option, + Option, + Option, + ), + pub(crate) notifications: (Option>, Option), + pub(crate) webdata3: (Option>, Option), + pub(crate) twap_states: (Option>, Option), + pub(crate) clearinghouse_state: (Option>, Option), + pub(crate) open_orders: (Option>, Option), + pub(crate) user_events: (Option>, Option), + pub(crate) user_fills: (Option>, Option), + pub(crate) user_funding: (Option>, Option), + pub(crate) user_non_funding_ledger_updates: + (Option>, Option), + pub(crate) active_asset_ctx: (Option>, Option), + pub(crate) active_asset_data: ( + Option>, + Option, + Option, + ), + pub(crate) user_twap_slice_fills: (Option>, Option), + pub(crate) user_twap_history: (Option>, Option), + pub(crate) bbo: (Option>, Option), +} + +impl StreamSenders { + pub fn new() -> Self { + Self { + all_mids: (None, None), + candle: (None, None, None), + trades: (None, None, None, None), + l2book: (None, None, None, None), + notifications: (None, None), + webdata3: (None, None), + twap_states: (None, None), + clearinghouse_state: (None, None), + open_orders: (None, None), + user_events: (None, None), + user_fills: (None, None), + user_funding: (None, None), + user_non_funding_ledger_updates: (None, None), + active_asset_ctx: (None, None), + active_asset_data: (None, None, None), + user_twap_slice_fills: (None, None), + user_twap_history: (None, None), + bbo: (None, None), + } + } +} diff --git a/src/api/subscription/ws.rs b/src/api/subscription/ws.rs new file mode 100644 index 0000000..04bc320 --- /dev/null +++ b/src/api/subscription/ws.rs @@ -0,0 +1,1055 @@ +use crate::api::request_util::SUPPORTED_INTERVALS; +use crate::client::HyperliquidClient; +use crate::error::{HyperliquidError, Result}; +use crate::types::ws::SubscriptionConfirmation; +use crate::types::ws::SubscriptionResponse; +use futures::{stream::SplitStream, StreamExt}; +use futures_util::stream::SplitSink; +use futures_util::SinkExt; +use serde::Serialize; +use serde_json::json; +use std::collections::HashSet; +use std::hash::{Hash, Hasher}; +use tokio::sync::broadcast::{channel, Receiver, Sender}; +use tokio::sync::RwLock; +use tokio::task::JoinHandle; +use tokio_tungstenite::connect_async; +use tokio_tungstenite::{tungstenite::Message, WebSocketStream}; + +type WsWriteStream = + SplitSink>, Message>; + +type WsReadStream = + SplitStream>>; + +pub enum StreamMessage { + Subscription(SubscriptionConfirmation), + Heartbeat, +} + +/// Configurable parameters for subscriptions. +pub struct SubscriptionConfig { + // Capacity for the underlying broadcast channel + channel_capacity: usize, +} + +impl Default for SubscriptionConfig { + fn default() -> Self { + Self { + channel_capacity: 1000, + } + } +} + +#[derive(Clone, Eq)] +pub enum SubscriptionSpec { + AllMids { + dex: Option, + }, + Candle { + coin: String, + interval: String, + }, + L2Book { + coin: String, + n_sig_figs: Option, + mantissa: Option, + }, + Trades { + coin: String, + }, + Notifications { + user: String, + }, + WebData3 { + user: String, + }, + TwapStates { + user: String, + }, + ClearinghouseState { + user: String, + }, + OpenOrders { + user: String, + }, + UserEvents { + user: String, + }, + UserFills { + user: String, + }, + UserFunding { + user: String, + }, + UserNonFundingLedgerUpdates { + user: String, + }, + ActiveAssetCtx { + coin: String, + }, + ActiveAssetData { + user: String, + coin: String, + }, + UserTwapSliceFills { + user: String, + }, + UserTwapHistory { + user: String, + }, + Bbo { + coin: String, + }, +} + +impl PartialEq for SubscriptionSpec { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::AllMids { dex: a }, Self::AllMids { dex: b }) => a == b, + + ( + Self::Candle { + coin: a1, + interval: a2, + }, + Self::Candle { + coin: b1, + interval: b2, + }, + ) => a1 == b1 && a2 == b2, + + ( + Self::L2Book { + coin: a, + n_sig_figs: a2, + mantissa: a3, + }, + Self::L2Book { + coin: b, + n_sig_figs: b2, + mantissa: b3, + }, + ) => a == b && a2 == b2 && a3 == b3, + + (Self::Trades { coin: a }, Self::Trades { coin: b }) => a == b, + + (Self::Notifications { user: a }, Self::Notifications { user: b }) => a == b, + (Self::WebData3 { user: a }, Self::WebData3 { user: b }) => a == b, + (Self::TwapStates { user: a }, Self::TwapStates { user: b }) => a == b, + (Self::ClearinghouseState { user: a }, Self::ClearinghouseState { user: b }) => a == b, + (Self::OpenOrders { user: a }, Self::OpenOrders { user: b }) => a == b, + (Self::UserEvents { user: a }, Self::UserEvents { user: b }) => a == b, + (Self::UserFills { user: a }, Self::UserFills { user: b }) => a == b, + (Self::UserFunding { user: a }, Self::UserFunding { user: b }) => a == b, + ( + Self::UserNonFundingLedgerUpdates { user: a }, + Self::UserNonFundingLedgerUpdates { user: b }, + ) => a == b, + + (Self::ActiveAssetCtx { coin: a }, Self::ActiveAssetCtx { coin: b }) => a == b, + + ( + Self::ActiveAssetData { user: a1, coin: a2 }, + Self::ActiveAssetData { user: b1, coin: b2 }, + ) => a1 == b1 && a2 == b2, + + (Self::UserTwapSliceFills { user: a }, Self::UserTwapSliceFills { user: b }) => a == b, + + (Self::UserTwapHistory { user: a }, Self::UserTwapHistory { user: b }) => a == b, + + (Self::Bbo { coin: a }, Self::Bbo { coin: b }) => a == b, + + _ => false, + } + } +} + +impl Hash for SubscriptionSpec { + fn hash(&self, state: &mut H) { + std::mem::discriminant(self).hash(state); + match self { + Self::AllMids { dex } => dex.hash(state), + + Self::Candle { coin, interval } => { + coin.hash(state); + interval.hash(state); + } + + Self::L2Book { + coin, + n_sig_figs, + mantissa, + } => { + coin.hash(state); + n_sig_figs.hash(state); + mantissa.hash(state); + } + + Self::Trades { coin } | Self::Bbo { coin } => coin.hash(state), + + Self::Notifications { user } + | Self::WebData3 { user } + | Self::TwapStates { user } + | Self::ClearinghouseState { user } + | Self::OpenOrders { user } + | Self::UserEvents { user } + | Self::UserFills { user } + | Self::UserFunding { user } + | Self::UserNonFundingLedgerUpdates { user } + | Self::UserTwapSliceFills { user } + | Self::UserTwapHistory { user } => user.hash(state), + + Self::ActiveAssetCtx { coin } => coin.hash(state), + + Self::ActiveAssetData { user, coin } => { + user.hash(state); + coin.hash(state); + } + } + } +} + +/// A WS client providing access to Hyperliquid Subscriptions API. +pub struct SubscriptionClient { + pub events: Receiver, + config: SubscriptionConfig, + active_subs: tokio::sync::RwLock>, + write_stream_tx: tokio::sync::mpsc::UnboundedSender, +} + +impl SubscriptionClient { + pub async fn new( + client: &HyperliquidClient, + config: Option, + ) -> Result { + let endpoint = client + .ws_endpoint() + .ok_or(HyperliquidError::MissingConfiguration { + parameter: "ws_endpoint".to_string(), + })?; + let (ws_stream, _response) = connect_async(endpoint).await?; + let (write_stream, read_stream) = ws_stream.split(); + + let config = config.unwrap_or_default(); + let (tx, rx) = channel::(config.channel_capacity); + let (write_stream_tx, write_stream_rx) = + tokio::sync::mpsc::unbounded_channel::(); + + Self::spawn_write_task(write_stream, write_stream_rx); + Self::spawn_heartbeat(write_stream_tx.clone()); + Self::spawn_read_task(tx, read_stream); + + Ok(Self { + config, + events: rx, + active_subs: RwLock::new(HashSet::new()), + write_stream_tx, + }) + } + + fn spawn_write_task( + mut write_stream: WsWriteStream, + mut rx: tokio::sync::mpsc::UnboundedReceiver, + ) -> JoinHandle<()> { + tokio::spawn(async move { + while let Some(msg) = rx.recv().await { + match msg { + StreamMessage::Subscription(confirmation) => { + if let Err(e) = write_stream + .send(Message::Text( + serde_json::to_string(&confirmation) + .expect("confirmation to stringify") + .into(), + )) + .await + { + tracing::error!("Failed to send subscription: {:?}", e); + break; + } + if let Err(e) = write_stream.flush().await { + tracing::error!("Failed to flush: {:?}", e); + break; + } + } + StreamMessage::Heartbeat => { + if let Err(e) = write_stream + .send(Message::Text(r#"{"method":"ping"}"#.into())) + .await + { + tracing::error!("Failed to send ping: {:?}", e); + break; + } + } + } + } + tracing::info!("Write task shutting down"); + }) + } + + pub fn spawn_heartbeat( + tx: tokio::sync::mpsc::UnboundedSender, + ) -> tokio::task::JoinHandle<()> { + tokio::task::spawn(async move { + let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(50)); + interval.tick().await; + + loop { + interval.tick().await; + + if let Err(e) = tx.send(StreamMessage::Heartbeat) { + tracing::error!("Failed to send heartbeat: {:?}", e); + break; + } + } + }) + } + + pub fn spawn_read_task( + tx: Sender, + mut read_stream: SplitStream< + WebSocketStream>, + >, + ) -> tokio::task::JoinHandle<()> { + tokio::spawn(async move { + while let Some(Ok(Message::Text(text))) = read_stream.next().await { + let value = match serde_json::from_str::(&text) { + Ok(v) => v, + Err(e) => { + tracing::error!("json error: {:?}", e); + continue; + } + }; + + if let Err(e) = tx.send(value) { + tracing::error!("{:?}", e); + } + } + }) + } + + fn subscription_channel(capacity: Option) -> (Sender, Receiver) { + channel::(capacity.unwrap_or(1000)) + } + + fn send_and_flush(&self, confirmation: SubscriptionConfirmation) -> Result<()> { + self.write_stream_tx + .send(StreamMessage::Subscription(confirmation))?; + Ok(()) + } + + fn build_subscription_json(spec: &SubscriptionSpec) -> serde_json::Value { + match spec { + SubscriptionSpec::AllMids { dex } => { + let mut v = json!({ "type": "allMids" }); + + if let Some(d) = dex { + if let Some(obj) = v.as_object_mut() { + obj.insert("dex".to_string(), json!(d)); + } + } + + v + } + + SubscriptionSpec::Candle { coin, interval } => { + json!({ + "type": "candle", + "coin": coin, + "interval": interval, + }) + } + + SubscriptionSpec::L2Book { + coin, + n_sig_figs, + mantissa, + } => { + let mut v = json!({ + "type": "l2Book", + "coin": coin, + }); + + if let Some(n) = n_sig_figs { + if let Some(obj) = v.as_object_mut() { + obj.insert("nSigFigs".to_string(), json!(n)); + } + } + + if let Some(m) = mantissa { + if let Some(obj) = v.as_object_mut() { + obj.insert("mantissa".to_string(), json!(m)); + } + } + + v + } + + SubscriptionSpec::Trades { coin } => { + json!({ + "type": "trades", + "coin": coin, + }) + } + + SubscriptionSpec::Notifications { user } => { + json!({ "type": "notification", "user": user }) + } + + SubscriptionSpec::WebData3 { user } => { + json!({ "type": "webData3", "user": user }) + } + + SubscriptionSpec::TwapStates { user } => { + json!({ "type": "twapStates", "user": user }) + } + + SubscriptionSpec::ClearinghouseState { user } => { + json!({ "type": "clearinghouseState", "user": user }) + } + + SubscriptionSpec::OpenOrders { user } => { + json!({ "type": "openOrders", "user": user }) + } + + SubscriptionSpec::UserEvents { user } => { + json!({ "type": "userEvents", "user": user }) + } + + SubscriptionSpec::UserFills { user } => { + json!({ "type": "userFills", "user": user }) + } + + SubscriptionSpec::UserFunding { user } => { + json!({ "type": "userFundings", "user": user }) + } + + SubscriptionSpec::UserNonFundingLedgerUpdates { user } => { + json!({ "type": "userNonFundingLedgerUpdates", "user": user }) + } + + SubscriptionSpec::ActiveAssetCtx { coin } => { + json!({ "type": "activeAssetCtx", "coin": coin }) + } + + SubscriptionSpec::ActiveAssetData { user, coin } => { + json!({ "type": "activeAssetData", "user": user, "coin": coin }) + } + + SubscriptionSpec::UserTwapSliceFills { user } => { + json!({ "type": "userTwapSliceFills", "user": user }) + } + + SubscriptionSpec::UserTwapHistory { user } => { + json!({ "type": "userTwapHistory", "user": user }) + } + + SubscriptionSpec::Bbo { coin } => { + json!({ "type": "bbo", "coin": coin }) + } + } + } + + pub async fn unsubscribe(&mut self, spec: &SubscriptionSpec) -> Result<()> { + if !self.active_subs.read().await.contains(spec) { + return Ok(()); + } + + let subscription = Self::build_subscription_json(spec); + + self.send_and_flush(SubscriptionConfirmation { + method: "unsubscribe".into(), + subscription, + })?; + + self.active_subs.write().await.remove(spec); + Ok(()) + } + + /// Subscribes to the [`WsAllMids`] websocket feed. + /// + /// # Arguments + /// * `dex` (optional) Represents the perp dex to source mids from. If not provided, + /// then the first perp dex is used. Spot mids are only included with the first perp dex. + /// + /// # Returns + /// A bounded `tokio::sync::broadcast::Sender` + pub async fn subscribe_all_mids(&mut self, dex: Option) -> Result<()> { + let spec = SubscriptionSpec::AllMids { dex: dex.clone() }; + + if self.active_subs.read().await.contains(&spec) { + return Ok(()); + } + + let subscription_message = SubscriptionConfirmation { + method: "subscribe".to_string(), + subscription: json!({ + "type": "allMids", + }), + }; + + if let Some(dex_param) = dex { + if let Some(dex_obj) = subscription_message.clone().subscription.get_mut("dex") { + if let Some(obj) = dex_obj.as_object_mut() { + let val = serde_json::Value::String(dex_param); + + obj.insert("dex".to_string(), val); + } + } + } + + self.send_and_flush(subscription_message)?; + + self.active_subs.write().await.insert(spec); + + Ok(()) + } + + /// Subscribes to the [`WsCandle`] websocket feed. + /// + /// # Arguments + /// * `coin` The desired asset for returning candle data. + /// * `interval` The interval at which the candle data is returned. Supported intervals include: "1m", "3m", + /// "5m", "15m", "30m", "1h", "2h", "4h", "8h", "12h", "1d", "3d", "1w", "1M" + /// + /// # Returns + /// A bounded `tokio::sync::broadcast::Sender` + pub async fn subscribe_candle( + &mut self, + coin: impl Into + Serialize + Clone, + interval: String, + ) -> Result<()> { + let coin = coin.into(); + + let spec = SubscriptionSpec::Candle { + coin, + interval: interval.clone(), + }; + if self.active_subs.read().await.contains(&spec) { + return Ok(()); + } + + if !SUPPORTED_INTERVALS.contains(interval.as_str()) { + return Err(HyperliquidError::InvalidRequestParameter { + method: "subscribe_candle".to_string(), + parameter: "interval".to_string(), + reason: format!("Supported intervals include: {:?}", SUPPORTED_INTERVALS), + }); + } + + let subscription = Self::build_subscription_json(&spec); + let subscription_message = SubscriptionConfirmation { + method: "subscribe".to_string(), + subscription, + }; + + self.send_and_flush(subscription_message)?; + + self.active_subs.write().await.insert(spec); + + Ok(()) + } + + /// Subscribes to the [`WsBook`] websocket feed. + /// + /// # Arguments + /// * `coin` The desired asset for returning l2book data. + /// * `n_sig_figs` - Optional number of significant figures. May cause subscription + /// to not receive data on testnet if set. Use `None` if unsure. + /// * `mantissa` - Optional mantissa. May cause subscription to not receive data + /// on testnet if set. Use `None` if unsure. + /// + /// # Returns + /// A bounded `tokio::sync::broadcast::Sender` + pub async fn subscribe_l2_book( + &mut self, + coin: impl Into + Serialize, + n_sig_figs: Option, + mantissa: Option, + ) -> Result<()> { + let coin = coin.into(); + let spec = SubscriptionSpec::L2Book { + coin: coin.clone(), + n_sig_figs, + mantissa, + }; + + if self.active_subs.read().await.contains(&spec) { + return Ok(()); + } + + let subscription = Self::build_subscription_json(&spec); + let subscription_message = SubscriptionConfirmation { + method: "subscribe".to_string(), + subscription, + }; + + self.send_and_flush(subscription_message)?; + + self.active_subs.write().await.insert(spec); + + Ok(()) + } + + /// Subscribes to the [`WsTrade`] websocket feed. + /// + /// # Arguments + /// * `coin` The desired asset for returning trade data. + /// + /// # Returns + /// A bounded `tokio::sync::broadcast::Sender` + pub async fn subscribe_trades( + &mut self, + coin: impl Into + Serialize + Clone, + ) -> Result<()> { + let coin = coin.into(); + let spec = SubscriptionSpec::Trades { coin: coin.clone() }; + + if self.active_subs.read().await.contains(&spec) { + return Ok(()); + } + + let subscription = Self::build_subscription_json(&spec); + let subscription_message = SubscriptionConfirmation { + method: "subscribe".to_string(), + subscription, + }; + + self.send_and_flush(subscription_message)?; + + self.active_subs.write().await.insert(spec); + + Ok(()) + } + + /// Subscribes to the [`WsNotification`] websocket feed. + /// + /// # Arguments + /// * `user` + /// + /// # Returns + /// A bounded `tokio::sync::broadcast::Sender` + pub async fn subscribe_notifications(&mut self, user: impl Into) -> Result<()> { + let user = user.into(); + + let spec = SubscriptionSpec::Notifications { user: user.clone() }; + + if self.active_subs.read().await.contains(&spec) { + return Ok(()); + } + + let subscription = Self::build_subscription_json(&spec); + let subscription_message = SubscriptionConfirmation { + method: "subscribe".to_string(), + subscription, + }; + + self.send_and_flush(subscription_message)?; + + self.active_subs.write().await.insert(spec); + + Ok(()) + } + + /// Subscribes to the [`WsWebData3`] websocket feed. + /// + /// # Arguments + /// * `user` + /// + /// # Returns + /// A bounded `tokio::sync::broadcast::Sender`. + pub async fn subscribe_webdata3(&mut self, user: impl Into) -> Result<()> { + let user = user.into(); + + let spec = SubscriptionSpec::WebData3 { user: user.clone() }; + + if self.active_subs.read().await.contains(&spec) { + return Ok(()); + } + + let subscription = Self::build_subscription_json(&spec); + let subscription_message = SubscriptionConfirmation { + method: "subscribe".to_string(), + subscription, + }; + + self.send_and_flush(subscription_message)?; + + self.active_subs.write().await.insert(spec); + + Ok(()) + } + + /// Subscribes to the [`WsTwapStates`] websocket feed. + /// + /// # Arguments + /// * `user` + /// + /// # Returns + /// A bounded `tokio::sync::broadcast::Sender`. + pub async fn subscribe_twap_states(&mut self, user: impl Into) -> Result<()> { + let user = user.into(); + + let spec = SubscriptionSpec::TwapStates { user: user.clone() }; + + if self.active_subs.read().await.contains(&spec) { + return Ok(()); + } + + let subscription = Self::build_subscription_json(&spec); + let subscription_message = SubscriptionConfirmation { + method: "subscribe".to_string(), + subscription, + }; + + self.send_and_flush(subscription_message)?; + + self.active_subs.write().await.insert(spec); + + Ok(()) + } + + /// Subscribes to the [`WsClearinghouseState`] websocket feed. + /// + /// # Arguments + /// * `user` + /// + /// # Returns + /// A bounded `tokio::sync::broadcast::Sender`. + pub async fn subscribe_clearinghouse_state(&mut self, user: impl Into) -> Result<()> { + let user = user.into(); + + let spec = SubscriptionSpec::ClearinghouseState { user: user.clone() }; + + if self.active_subs.read().await.contains(&spec) { + return Ok(()); + } + + let subscription = Self::build_subscription_json(&spec); + let subscription_message = SubscriptionConfirmation { + method: "subscribe".to_string(), + subscription, + }; + + self.send_and_flush(subscription_message)?; + + self.active_subs.write().await.insert(spec); + + Ok(()) + } + + /// Subscribes to the [`WsOpenOrders`] websocket feed. + /// + /// # Arguments + /// * `user` + /// + /// # Returns + /// A bounded `tokio::sync::broadcast::Sender`. + pub async fn subscribe_open_orders(&mut self, user: impl Into) -> Result<()> { + let user = user.into(); + + let spec = SubscriptionSpec::OpenOrders { user: user.clone() }; + + if self.active_subs.read().await.contains(&spec) { + return Ok(()); + } + + let subscription = Self::build_subscription_json(&spec); + + let subscription_message = SubscriptionConfirmation { + method: "subscribe".to_string(), + subscription, + }; + + self.send_and_flush(subscription_message)?; + self.active_subs.write().await.insert(spec); + + Ok(()) + } + + /// Subscribes to the [`WsUserEvent`] websocket feed. + /// + /// # Arguments + /// * `user` + /// + /// # Returns + /// A bounded `tokio::sync::broadcast::Sender`. + pub async fn subscribe_user_events(&mut self, user: impl Into) -> Result<()> { + let user = user.into(); + + let spec = SubscriptionSpec::UserEvents { user: user.clone() }; + + if self.active_subs.read().await.contains(&spec) { + return Ok(()); + } + + let subscription = Self::build_subscription_json(&spec); + let subscription_message = SubscriptionConfirmation { + method: "subscribe".to_string(), + subscription, + }; + + self.send_and_flush(subscription_message)?; + + self.active_subs.write().await.insert(spec); + + Ok(()) + } + + /// Subscribes to the [`WsUserFills`] websocket feed. + /// + /// # Arguments + /// * `user` + /// + /// # Returns + /// A bounded `tokio::sync::broadcast::Sender`. + pub async fn subscribe_user_fills(&mut self, user: impl Into) -> Result<()> { + let user = user.into(); + + let spec = SubscriptionSpec::UserFills { user: user.clone() }; + + if self.active_subs.read().await.contains(&spec) { + return Ok(()); + } + + let subscription = Self::build_subscription_json(&spec); + + let subscription_message = SubscriptionConfirmation { + method: "subscribe".to_string(), + subscription, + }; + + self.send_and_flush(subscription_message)?; + + self.active_subs.write().await.insert(spec); + + Ok(()) + } + + /// Subscribes to the [`WsUserFunding`] websocket feed. + /// + /// # Arguments + /// * `user` + /// + /// # Returns + /// A bounded `tokio::sync::broadcast::Sender`. + pub async fn subscribe_user_funding(&mut self, user: impl Into) -> Result<()> { + let user = user.into(); + + let spec = SubscriptionSpec::UserFunding { user: user.clone() }; + + if self.active_subs.read().await.contains(&spec) { + return Ok(()); + } + + let subscription = Self::build_subscription_json(&spec); + + let subscription_message = SubscriptionConfirmation { + method: "subscribe".to_string(), + subscription, + }; + + self.send_and_flush(subscription_message)?; + + self.active_subs.write().await.insert(spec); + + Ok(()) + } + + /// Subscribes to the [`WsUserNonFundingLedgerUpdate`] websocket feed. + /// + /// # Arguments + /// * `user` + /// + /// # Returns + /// A bounded `tokio::sync::broadcast::Sender`. + pub async fn subscribe_user_non_funding_ledger_updates( + &mut self, + + user: impl Into, + ) -> Result<()> { + let user = user.into(); + + let spec = SubscriptionSpec::UserNonFundingLedgerUpdates { user: user.clone() }; + + if self.active_subs.read().await.contains(&spec) { + return Ok(()); + } + + let subscription = Self::build_subscription_json(&spec); + + let subscription_message = SubscriptionConfirmation { + method: "subscribe".to_string(), + subscription, + }; + + self.send_and_flush(subscription_message)?; + + self.active_subs.write().await.insert(spec); + + Ok(()) + } + + /// Subscribes to the [`WsActiveAssetCtx`] websocket feed. + /// + /// # Arguments + /// * `coin` The symbol of the coin, i.e. the asset for receiving asset context + /// + /// # Returns + /// A bounded `tokio::sync::broadcast::Sender`. + pub async fn subscribe_active_asset_ctx(&mut self, coin: impl Into) -> Result<()> { + let coin = coin.into(); + + let spec = SubscriptionSpec::ActiveAssetCtx { coin: coin.clone() }; + + if self.active_subs.read().await.contains(&spec) { + return Ok(()); + } + + let subscription = Self::build_subscription_json(&spec); + + let subscription_message = SubscriptionConfirmation { + method: "subscribe".to_string(), + subscription, + }; + + self.send_and_flush(subscription_message)?; + + self.active_subs.write().await.insert(spec); + + Ok(()) + } + + /// Subscribes to the [`WsActiveAssetData`] websocket feed. + /// + /// # Arguments + /// * `coin` The symbol of the coin, i.e. the asset for receiving asset context + /// * `user` + /// + /// # Returns + /// A bounded `tokio::sync::broadcast::Sender`. + pub async fn subscribe_active_asset_data( + &mut self, + + user: impl Into, + coin: impl Into, + ) -> Result<()> { + let user = user.into(); + let coin = coin.into(); + + let spec = SubscriptionSpec::ActiveAssetData { + coin: coin.clone(), + user: user.clone(), + }; + + if self.active_subs.read().await.contains(&spec) { + return Ok(()); + } + + let subscription = Self::build_subscription_json(&spec); + + let subscription_message = SubscriptionConfirmation { + method: "subscribe".to_string(), + subscription, + }; + + self.send_and_flush(subscription_message)?; + + self.active_subs.write().await.insert(spec); + + Ok(()) + } + + /// Subscribes to the [`WsUserTwapSliceFills`] websocket feed. + /// + /// # Arguments + /// * `coin` The symbol of the coin, i.e. the asset for receiving asset context + /// * `user` + /// + /// # Returns + /// A bounded `tokio::sync::broadcast::Sender`. + pub async fn subscribe_user_twap_slice_fills(&mut self, user: impl Into) -> Result<()> { + let user = user.into(); + + let spec = SubscriptionSpec::UserTwapSliceFills { user: user.clone() }; + + if self.active_subs.read().await.contains(&spec) { + return Ok(()); + } + + let subscription = Self::build_subscription_json(&spec); + + let subscription_message = SubscriptionConfirmation { + method: "subscribe".to_string(), + subscription, + }; + + self.send_and_flush(subscription_message)?; + + self.active_subs.write().await.insert(spec); + + Ok(()) + } + + /// Subscribes to the [`WsUserTwapHistory`] websocket feed. + /// + /// # Arguments + /// * `coin` The symbol of the coin, i.e. the asset for receiving asset context + /// * `user` + /// + /// # Returns + /// A bounded `tokio::sync::broadcast::Sender`. + pub async fn subscribe_user_twap_history(&mut self, user: impl Into) -> Result<()> { + let user = user.into(); + + let spec = SubscriptionSpec::UserTwapHistory { user: user.clone() }; + + if self.active_subs.read().await.contains(&spec) { + return Ok(()); + } + + let subscription = Self::build_subscription_json(&spec); + + let subscription_message = SubscriptionConfirmation { + method: "subscribe".to_string(), + subscription, + }; + + self.send_and_flush(subscription_message)?; + + self.active_subs.write().await.insert(spec); + + Ok(()) + } + + /// Subscribes to the [`WsBbo`] websocket feed. + /// + /// # Arguments + /// * `coin` The symbol of the coin, i.e. the asset for receiving asset context + /// * `user` + /// + /// # Returns + /// A bounded `tokio::sync::broadcast::Sender`. + pub async fn subscribe_bbo(&mut self, coin: impl Into) -> Result<()> { + let coin = coin.into(); + + let spec = SubscriptionSpec::Bbo { coin: coin.clone() }; + + if self.active_subs.read().await.contains(&spec) { + return Ok(()); + } + + let subscription = Self::build_subscription_json(&spec); + + let subscription_message = SubscriptionConfirmation { + method: "subscribe".to_string(), + subscription, + }; + + self.send_and_flush(subscription_message)?; + + self.active_subs.write().await.insert(spec); + + Ok(()) + } +} diff --git a/src/client/builder.rs b/src/client/builder.rs index a142652..582d0f7 100644 --- a/src/client/builder.rs +++ b/src/client/builder.rs @@ -6,6 +6,7 @@ pub struct HyperliquidClientBuilder { base_url: Option, network: Option, wallet: Option, + ws_endpoint: Option, } impl Default for HyperliquidClientBuilder { @@ -22,6 +23,7 @@ impl HyperliquidClientBuilder { base_url: None, network: None, wallet: None, + ws_endpoint: None, } } @@ -51,18 +53,39 @@ impl HyperliquidClientBuilder { self } + pub fn with_subscriptions(&mut self) -> &mut Self { + match &self.network { + Some(network) => match network { + NetworkType::Mainnet => { + self.ws_endpoint = Some("wss://api.hyperliquid.xyz/ws".to_string()); + } + NetworkType::Testnet => { + self.ws_endpoint = Some("wss://api.hyperliquid-testnet.xyz/ws".to_string()); + } + }, + None => self.ws_endpoint = Some("wss://api.hyperliquid-testnet.xyz/ws".to_string()), + } + + self + } + #[inline] pub fn build(&self) -> Result { let base_url = self .base_url .clone() .ok_or(HyperliquidError::MissingConfiguration { - parameter: "base_url".to_owned(), + parameter: "Must create client with mainnet or testnet configuration.".to_owned(), })?; // Default to Testnet let network = self.network.clone().unwrap_or(NetworkType::Testnet); - HyperliquidClient::new(network, base_url, self.wallet.clone()) + HyperliquidClient::new( + network, + base_url, + self.ws_endpoint.clone(), + self.wallet.clone(), + ) } } diff --git a/src/client/client.rs b/src/client/client.rs index beff54d..b157bf1 100644 --- a/src/client/client.rs +++ b/src/client/client.rs @@ -1,5 +1,5 @@ use crate::{ - api::{exchange::ExchangeApi, info::InfoApi}, + api::{exchange::ExchangeApi, info::InfoApi, SubscriptionClient}, client::HyperliquidClientBuilder, error::Result, types::chain::NetworkType, @@ -18,12 +18,14 @@ pub struct Inner { base_url: String, wallet: Option, network: NetworkType, + ws_endpoint: Option, } impl Inner { pub fn new( network: NetworkType, base_url: String, + ws_endpoint: Option, wallet: Option, ) -> Result { let http_client = ClientBuilder::new().build()?; @@ -31,6 +33,7 @@ impl Inner { Ok(Self { http_client, base_url, + ws_endpoint, wallet, network, }) @@ -41,9 +44,10 @@ impl HyperliquidClient { pub fn new( network: NetworkType, base_url: String, + ws_endpoint: Option, wallet: Option, ) -> Result { - let inner = Arc::new(Inner::new(network, base_url, wallet)?); + let inner = Arc::new(Inner::new(network, base_url, ws_endpoint, wallet)?); Ok(Self { inner }) } @@ -60,6 +64,10 @@ impl HyperliquidClient { &self.inner.base_url } + pub fn ws_endpoint(&self) -> Option<&String> { + self.inner.ws_endpoint.as_ref() + } + pub fn signer(&self) -> Option<&PrivateKeySigner> { self.inner.wallet.as_ref() } @@ -76,6 +84,10 @@ impl HyperliquidClient { ExchangeApi::new(self) } + pub async fn subscriptions(&self) -> Result { + SubscriptionClient::new(self, None).await + } + pub fn is_mainnet(&self) -> bool { match self.inner.network { NetworkType::Mainnet => true, diff --git a/src/error.rs b/src/error.rs index 18cb4ea..0fb0625 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,6 +1,17 @@ use alloy::primitives::SignatureError; -use std::result::Result as StdResult; +use std::{result::Result as StdResult, string::FromUtf8Error, sync::mpsc::SendError}; use thiserror::Error; +use tokio::sync::broadcast::error::RecvError; + +use crate::{api::StreamMessage, types::ws::SubscriptionKey}; + +#[derive(Error, Debug)] +pub enum SubscriptionError { + #[error("Subscription already exist for method {method}")] + SubscriptionExist { method: String }, + #[error("Caller not subscribed to the {0} feed.")] + MissingSubscription(SubscriptionKey), +} #[derive(Error, Debug)] pub enum HyperliquidError { @@ -16,13 +27,27 @@ pub enum HyperliquidError { parameter: String, reason: String, }, + #[error("{0}")] + SubscriptionError(#[from] SubscriptionError), #[error("Function requires wallet/signer.")] SignerRequired, #[error("{0}")] + SendError(#[from] SendError), + #[error("{0}")] + RecvError(#[from] RecvError), + #[error("{0}")] + WebSocketError(String), + #[error("{0}")] AlloySignError(#[from] alloy::signers::Error), #[error("{0}")] RmpSerde(#[from] rmp_serde::encode::Error), #[error("{0}")] + FromUtf8Error(#[from] FromUtf8Error), + #[error("{0}")] + IoError(#[from] std::io::Error), + #[error("{0}")] + TungsteniteError(#[from] tokio_tungstenite::tungstenite::Error), + #[error("{0}")] SignatureError(#[from] SignatureError), #[error("Missing configuration for parameter {parameter}")] MissingConfiguration { parameter: String }, @@ -40,4 +65,10 @@ pub enum HyperliquidError { GenericParse(String), } +impl From> for HyperliquidError { + fn from(e: tokio::sync::mpsc::error::SendError) -> Self { + Self::WebSocketError(format!("Channel send failed: {}", e)) + } +} + pub type Result = StdResult; diff --git a/src/example_helpers.rs b/src/example_helpers.rs index e442d7e..496c540 100644 --- a/src/example_helpers.rs +++ b/src/example_helpers.rs @@ -1,15 +1,24 @@ use alloy::signers::local::PrivateKeySigner; use crate::client::HyperliquidClient; -use crate::error::Result; use std::env; -pub fn testnet_client() -> Result { - HyperliquidClient::builder().testnet().build() +pub fn testnet_client() -> Result> { + let signer = load_signer(); + Ok(HyperliquidClient::builder() + .testnet() + .with_wallet(signer) + .with_subscriptions() + .build()?) } -pub fn mainnet_client() -> Result { - HyperliquidClient::builder().mainnet().build() +pub fn mainnet_client() -> Result> { + let signer = load_signer(); + Ok(HyperliquidClient::builder() + .mainnet() + .with_wallet(signer) + .with_subscriptions() + .build()?) } pub fn user() -> String { diff --git a/src/lib.rs b/src/lib.rs index 52073c8..c1d0d3f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,7 +4,7 @@ mod client; mod error; mod signature; -pub use crate::api::response; +pub use crate::api::{response, SubscriptionClient, SubscriptionConfig}; pub use crate::client::{HyperliquidClient, HyperliquidClientBuilder}; pub use crate::error::{HyperliquidError, Result}; /// Utilities for examples and testing. diff --git a/src/types/info/user.rs b/src/types/info/user.rs index 253ccfb..0ce9880 100644 --- a/src/types/info/user.rs +++ b/src/types/info/user.rs @@ -334,7 +334,7 @@ pub struct Order { pub is_trigger: bool, #[serde(rename = "triggerPx")] pub trigger_px: Decimal, - pub children: Vec, + pub children: Vec, #[serde(rename = "isPositionTpsl")] pub is_position_tpsl: bool, #[serde(rename = "reduceOnly")] diff --git a/src/types/mod.rs b/src/types/mod.rs index 9bd9a51..aec320d 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -3,4 +3,4 @@ pub mod exchange; pub mod info; pub(crate) mod serialize; pub(crate) mod signature; -mod ws; +pub mod ws; diff --git a/src/types/serialize.rs b/src/types/serialize.rs index d91365c..9e678b9 100644 --- a/src/types/serialize.rs +++ b/src/types/serialize.rs @@ -1,6 +1,5 @@ -use serde::Serializer; - use crate::api::request_util::normalize_decimal; +use serde::Serializer; /// Serializes decimal values represented as strings to a valid format for the Hyperliquid /// API @@ -21,3 +20,32 @@ where { serializer.serialize_str(&format!("0x{:x}", chain_id)) } + +pub(crate) mod decimal_array { + use rust_decimal::Decimal; + use serde::{Deserialize, Deserializer, Serializer}; + + /// Serializer for a fixed array of length(2) + pub fn serialize(value: &[Decimal; 2], serializer: S) -> Result + where + S: Serializer, + { + use serde::ser::SerializeTuple; + let mut seq = serializer.serialize_tuple(2)?; + seq.serialize_element(&value[0].to_string())?; + seq.serialize_element(&value[1].to_string())?; + seq.end() + } + + /// Deserializer for a fixed array of length(2) + pub fn deserialize<'de, D>(deserializer: D) -> Result<[Decimal; 2], D::Error> + where + D: Deserializer<'de>, + { + let strings: [String; 2] = Deserialize::deserialize(deserializer)?; + Ok([ + strings[0].parse().map_err(serde::de::Error::custom)?, + strings[1].parse().map_err(serde::de::Error::custom)?, + ]) + } +} diff --git a/src/types/ws.rs b/src/types/ws.rs index 3711c2d..59db9df 100644 --- a/src/types/ws.rs +++ b/src/types/ws.rs @@ -1,8 +1,11 @@ -#![allow(dead_code)] - -/// Request and response types for WebSocket subscriptions and streaming data. +use rust_decimal::Decimal; +/// Request and response types for WebSocket subscriptions +/// and streaming data. use serde::{Deserialize, Serialize}; use std::collections::HashMap; +use std::primitive::str; + +use crate::types::serialize::decimal_array; /// WebSocket trade data #[derive(Debug, Clone, Serialize, Deserialize)] @@ -56,20 +59,20 @@ pub struct WsBbo { /// WebSocket notification #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct Notification { +pub struct WsNotification { pub notification: String, } /// All mid prices #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct AllMids { +pub struct WsAllMids { pub mids: HashMap, } /// Candlestick data #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Candle { +pub struct WsCandle { /// Open time in milliseconds pub t: u64, /// Close time in milliseconds @@ -80,15 +83,20 @@ pub struct Candle { /// Interval pub i: String, /// Open price - pub o: f64, + #[serde(with = "rust_decimal::serde::str")] + pub o: Decimal, /// Close price - pub c: f64, + #[serde(with = "rust_decimal::serde::str")] + pub c: Decimal, /// High price - pub h: f64, + #[serde(with = "rust_decimal::serde::str")] + pub h: Decimal, /// Low price - pub l: f64, + #[serde(with = "rust_decimal::serde::str")] + pub l: Decimal, /// Volume (base unit) - pub v: f64, + #[serde(with = "rust_decimal::serde::str")] + pub v: Decimal, /// Number of trades pub n: u64, } @@ -102,7 +110,7 @@ pub enum WsUserEvent { fills: Vec, }, Funding { - funding: WsUserFunding, + funding: WsUserFundings, }, Liquidation { liquidation: WsLiquidation, @@ -128,7 +136,8 @@ pub struct WsUserFills { pub struct FillLiquidation { #[serde(skip_serializing_if = "Option::is_none")] pub liquidated_user: Option, - pub mark_px: f64, + #[serde(with = "rust_decimal::serde::str")] + pub mark_px: Decimal, /// "market" | "backstop" pub method: String, } @@ -167,10 +176,18 @@ pub struct WsFill { pub builder_fee: Option, } +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct WsUserFundings { + pub is_snapshot: bool, + pub user: String, + pub fundings: Vec, +} + /// WebSocket user funding payment #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct WsUserFunding { +pub struct Fundings { pub time: u64, pub coin: String, pub usdc: String, @@ -225,60 +242,61 @@ pub struct WsBasicOrder { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SharedAssetCtx { - pub day_ntl_vlm: f64, - pub prev_day_px: f64, - pub mark_px: f64, + #[serde(with = "rust_decimal::serde::str")] + pub day_ntl_vlm: Decimal, + #[serde(with = "rust_decimal::serde::str")] + pub prev_day_px: Decimal, + #[serde(with = "rust_decimal::serde::str")] + pub mark_px: Decimal, #[serde(skip_serializing_if = "Option::is_none")] - pub mid_px: Option, + pub mid_px: Option, } /// Perpetuals asset context #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct PerpsAssetCtx { - pub day_ntl_vlm: f64, - pub prev_day_px: f64, - pub mark_px: f64, - #[serde(skip_serializing_if = "Option::is_none")] - pub mid_px: Option, - pub funding: f64, - pub open_interest: f64, - pub oracle_px: f64, + #[serde(with = "rust_decimal::serde::str")] + pub day_ntl_vlm: Decimal, + #[serde(with = "rust_decimal::serde::str")] + pub prev_day_px: Decimal, + #[serde(with = "rust_decimal::serde::str")] + pub mark_px: Decimal, + #[serde( + with = "rust_decimal::serde::str_option", + skip_serializing_if = "Option::is_none" + )] + pub mid_px: Option, + #[serde(with = "rust_decimal::serde::str")] + pub funding: Decimal, + #[serde(with = "rust_decimal::serde::str")] + pub open_interest: Decimal, + #[serde(with = "rust_decimal::serde::str")] + pub oracle_px: Decimal, } /// Spot asset context #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SpotAssetCtx { - pub day_ntl_vlm: f64, - pub prev_day_px: f64, - pub mark_px: f64, + #[serde(with = "rust_decimal::serde::str")] + pub day_ntl_vlm: Decimal, + #[serde(with = "rust_decimal::serde::str")] + pub prev_day_px: Decimal, + #[serde(with = "rust_decimal::serde::str")] + pub mark_px: Decimal, #[serde(skip_serializing_if = "Option::is_none")] - pub mid_px: Option, - pub circulating_supply: f64, -} - -/// WebSocket active perpetuals asset context -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct WsActiveAssetCtx { - pub coin: String, - pub ctx: PerpsAssetCtx, -} - -/// WebSocket active spot asset context -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct WsActiveSpotAssetCtx { - pub coin: String, - pub ctx: SpotAssetCtx, + pub mid_px: Option, + #[serde(with = "rust_decimal::serde::str")] + pub circulating_supply: Decimal, } /// Leverage information #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Leverage { - pub raw_usd: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub raw_usd: Option, #[serde(rename = "type")] pub leverage_type: String, pub value: u32, @@ -291,8 +309,10 @@ pub struct WsActiveAssetData { pub user: String, pub coin: String, pub leverage: Leverage, - pub max_trade_szs: [f64; 2], - pub available_to_trade: [f64; 2], + #[serde(with = "decimal_array")] + pub max_trade_szs: [Decimal; 2], + #[serde(with = "decimal_array")] + pub available_to_trade: [Decimal; 2], } /// WebSocket TWAP slice fill @@ -316,13 +336,16 @@ pub struct WsUserTwapSliceFills { /// TWAP state #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct TwapState { +pub struct WsTwapState { pub coin: String, pub user: String, pub side: String, - pub sz: f64, - pub executed_sz: f64, - pub executed_ntl: f64, + #[serde(with = "rust_decimal::serde::str")] + pub sz: Decimal, + #[serde(with = "rust_decimal::serde::str")] + pub executed_sz: Decimal, + #[serde(with = "rust_decimal::serde::str")] + pub executed_ntl: Decimal, pub minutes: u32, pub reduce_only: bool, pub randomize: bool, @@ -332,7 +355,7 @@ pub struct TwapState { /// TWAP status with description #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct TwapStatusInfo { +pub struct WsTwapStatusInfo { /// "activated" | "terminated" | "finished" | "error" pub status: String, pub description: String, @@ -342,8 +365,8 @@ pub struct TwapStatusInfo { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct WsTwapHistory { - pub state: TwapState, - pub status: TwapStatusInfo, + pub state: WsTwapState, + pub status: WsTwapStatusInfo, pub time: u64, } @@ -364,7 +387,8 @@ pub struct UserState { pub agent_address: Option, pub agent_valid_until: Option, pub server_time: u64, - pub cum_ledger: f64, + #[serde(with = "rust_decimal::serde::str")] + pub cum_ledger: Decimal, pub is_vault: bool, pub user: String, #[serde(skip_serializing_if = "Option::is_none")] @@ -385,7 +409,8 @@ pub struct LeadingVault { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct PerpDexState { - pub total_vault_equity: f64, + #[serde(with = "rust_decimal::serde::str")] + pub total_vault_equity: Decimal, #[serde(skip_serializing_if = "Option::is_none")] pub perps_at_open_interest_cap: Option>, #[serde(skip_serializing_if = "Option::is_none")] @@ -395,7 +420,7 @@ pub struct PerpDexState { /// WebSocket web data (`WebData3`) #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct WebData3 { +pub struct WsWebData3 { pub user_state: UserState, pub perp_dex_states: Vec, } @@ -404,10 +429,14 @@ pub struct WebData3 { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct MarginSummary { - pub account_value: f64, - pub total_ntl_pos: f64, - pub total_raw_usd: f64, - pub total_margin_used: f64, + #[serde(with = "rust_decimal::serde::str")] + pub account_value: Decimal, + #[serde(with = "rust_decimal::serde::str")] + pub total_ntl_pos: Decimal, + #[serde(with = "rust_decimal::serde::str")] + pub total_raw_usd: Decimal, + #[serde(with = "rust_decimal::serde::str")] + pub total_margin_used: Decimal, } /// Position information @@ -437,12 +466,23 @@ pub struct AssetPosition { /// Clearinghouse state #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct ClearinghouseState { +pub struct WsClearinghouseState { + pub dex: String, + pub user: String, + pub clearinghouse_state: ClearinghouseStateData, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ClearinghouseStateData { pub asset_positions: Vec, pub margin_summary: MarginSummary, pub cross_margin_summary: MarginSummary, - pub cross_maintenance_margin_used: f64, - pub withdrawable: f64, + #[serde(with = "rust_decimal::serde::str")] + pub cross_maintenance_margin_used: Decimal, + #[serde(with = "rust_decimal::serde::str")] + pub withdrawable: Decimal, + pub time: u64, } /// Order information @@ -461,7 +501,7 @@ pub struct Order { /// Open orders for a user #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct OpenOrders { +pub struct WsOpenOrders { pub dex: String, pub user: String, pub orders: Vec, @@ -470,21 +510,29 @@ pub struct OpenOrders { /// TWAP states for a user #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct TwapStates { +pub struct WsTwapStates { pub dex: String, pub user: String, - pub states: Vec<(u64, TwapState)>, + pub states: Vec<(u64, WsTwapState)>, } -/// WebSocket user non-funding ledger update #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct WsUserNonFundingLedgerUpdate { +pub struct WsNonFundingLedgerUpdate { pub time: u64, pub hash: String, pub delta: WsLedgerUpdate, } +/// WebSocket user non-funding ledger update +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct WsUserNonFundingLedgerUpdate { + pub is_snapshot: bool, + pub user: String, + pub non_funding_ledger_updates: Vec, +} + /// WebSocket ledger update /// Can be deposit, withdraw, transfer, liquidation, vault operations, etc. #[derive(Debug, Clone, Serialize, Deserialize)] @@ -524,33 +572,39 @@ pub enum WsLedgerUpdate { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct WsDeposit { - pub usdc: f64, + #[serde(with = "rust_decimal::serde::str")] + pub usdc: Decimal, } /// Withdraw ledger update #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct WsWithdraw { - pub usdc: f64, + #[serde(with = "rust_decimal::serde::str")] + pub usdc: Decimal, pub nonce: u64, - pub fee: f64, + #[serde(with = "rust_decimal::serde::str")] + pub fee: Decimal, } /// Internal transfer ledger update #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct WsInternalTransfer { - pub usdc: f64, + #[serde(with = "rust_decimal::serde::str")] + pub usdc: Decimal, pub user: String, pub destination: String, - pub fee: f64, + #[serde(with = "rust_decimal::serde::str")] + pub fee: Decimal, } /// Sub-account transfer ledger update #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct WsSubAccountTransfer { - pub usdc: f64, + #[serde(with = "rust_decimal::serde::str")] + pub usdc: Decimal, pub user: String, pub destination: String, } @@ -560,7 +614,8 @@ pub struct WsSubAccountTransfer { #[serde(rename_all = "camelCase")] pub struct LiquidatedPosition { pub coin: String, - pub szi: f64, + #[serde(with = "rust_decimal::serde::str")] + pub szi: Decimal, } /// Liquidation ledger update @@ -568,7 +623,8 @@ pub struct LiquidatedPosition { #[serde(rename_all = "camelCase")] pub struct WsLedgerLiquidation { /// For isolated positions this is the isolated account value - pub account_value: f64, + #[serde(with = "rust_decimal::serde::str")] + pub account_value: Decimal, /// "Cross" | "Isolated" pub leverage_type: String, pub liquidated_positions: Vec, @@ -579,7 +635,8 @@ pub struct WsLedgerLiquidation { #[serde(rename_all = "camelCase")] pub struct WsVaultDelta { pub vault: String, - pub usdc: f64, + #[serde(with = "rust_decimal::serde::str")] + pub usdc: Decimal, } /// Vault withdrawal ledger update @@ -588,11 +645,16 @@ pub struct WsVaultDelta { pub struct WsVaultWithdrawal { pub vault: String, pub user: String, - pub requested_usd: f64, - pub commission: f64, - pub closing_cost: f64, - pub basis: f64, - pub net_withdrawn_usd: f64, + #[serde(with = "rust_decimal::serde::str")] + pub requested_usd: Decimal, + #[serde(with = "rust_decimal::serde::str")] + pub commission: Decimal, + #[serde(with = "rust_decimal::serde::str")] + pub closing_cost: Decimal, + #[serde(with = "rust_decimal::serde::str")] + pub basis: Decimal, + #[serde(with = "rust_decimal::serde::str")] + pub net_withdrawn_usd: Decimal, } /// Vault leader commission ledger update @@ -600,7 +662,8 @@ pub struct WsVaultWithdrawal { #[serde(rename_all = "camelCase")] pub struct WsVaultLeaderCommission { pub user: String, - pub usdc: f64, + #[serde(with = "rust_decimal::serde::str")] + pub usdc: Decimal, } /// Spot transfer ledger update @@ -608,18 +671,22 @@ pub struct WsVaultLeaderCommission { #[serde(rename_all = "camelCase")] pub struct WsSpotTransfer { pub token: String, - pub amount: f64, - pub usdc_value: f64, + #[serde(with = "rust_decimal::serde::str")] + pub amount: Decimal, + #[serde(with = "rust_decimal::serde::str")] + pub usdc_value: Decimal, pub user: String, pub destination: String, - pub fee: f64, + #[serde(with = "rust_decimal::serde::str")] + pub fee: Decimal, } /// Account class transfer ledger update #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct WsAccountClassTransfer { - pub usdc: f64, + #[serde(with = "rust_decimal::serde::str")] + pub usdc: Decimal, pub to_perp: bool, } @@ -628,12 +695,145 @@ pub struct WsAccountClassTransfer { #[serde(rename_all = "camelCase")] pub struct WsSpotGenesis { pub token: String, - pub amount: f64, + #[serde(with = "rust_decimal::serde::str")] + pub amount: Decimal, } /// Rewards claim ledger update #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct WsRewardsClaim { - pub amount: f64, + #[serde(with = "rust_decimal::serde::str")] + pub amount: Decimal, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct SubscriptionError { + pub data: String, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct SubscriptionConfirmation { + pub method: String, + pub subscription: serde_json::Value, +} + +/// WebSocket active perpetuals asset context +#[derive(Serialize, Deserialize, Clone, Debug)] +#[serde(rename_all = "camelCase")] +pub struct WsActiveAssetCtx { + pub coin: String, + pub ctx: PerpsAssetCtx, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +#[serde(rename_all = "camelCase")] +pub struct WsActiveSpotAssetCtx { + pub coin: String, + pub ctx: SpotAssetCtx, +} + +/// Identifies a concrete websocket subscription type supported by the feed. +/// +/// Each variant corresponds to a distinct server-side stream and determines +/// both the subscription parameters and the shape of messages received. +#[derive(Serialize, Deserialize, Clone, Debug)] +pub enum SubscriptionKey { + AllMids, + Candle, + Trades, + L2Book, + Notification, + WebData3, + TwapStates, + OpenOrders, + UserEvents, + UserNonFundingLedgerUpdate, + ActiveAssetCtx, + ActiveAssetData, + UserTwapSliceFills, + UserTwapHistory, + Bbo, + Ping, +} + +impl std::fmt::Display for SubscriptionKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let _ = match *self { + Self::AllMids => f.write_str("allMids"), + Self::Candle => f.write_str("candle"), + Self::Trades => f.write_str("trades"), + Self::L2Book => f.write_str("l2Book"), + Self::Notification => f.write_str("notification"), + Self::WebData3 => f.write_str("webData3"), + Self::TwapStates => f.write_str("twapStates"), + Self::OpenOrders => f.write_str("openOrders"), + Self::UserEvents => f.write_str("userEvents"), + Self::UserNonFundingLedgerUpdate => f.write_str("userNonFundingLedgerUpdate"), + Self::ActiveAssetCtx => f.write_str("activeAssetCtx"), + Self::ActiveAssetData => f.write_str("activeAssetData"), + Self::UserTwapSliceFills => f.write_str("userTwapSliceFills"), + Self::UserTwapHistory => f.write_str("userTwapHistory"), + Self::Bbo => f.write_str("bbo"), + Self::Ping => f.write_str("pong"), + }; + + Ok(()) + } +} + +/// Error response from order/cancel operations +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct WsErrorResponse { + pub error: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub details: Option, +} + +/// Batch operation error +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BatchError { + #[serde(rename = "type")] + pub error_type: String, + pub message: String, +} + +/// Messages delivered over websocket subscription channels. +/// +/// The `channel` field selects the subscription stream, while `data` contains +/// the stream-specific payload deserialized into a strongly typed variant. +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(tag = "channel", content = "data", rename_all = "camelCase")] +pub enum SubscriptionResponse { + /// Server-side error emitted over the websocket connection. + #[serde(rename = "error")] + Error(WsErrorResponse), + + /// Acknowledgement or status response for subscribe / unsubscribe requests. + SubscriptionResponse(SubscriptionConfirmation), + + /// Pong response to client ping (keepalive) + #[serde(rename = "pong")] + Pong, + + AllMids(WsAllMids), + Candle(WsCandle), + Trades(Vec), + L2Book(WsBook), + Notification(WsNotification), + WebData3(WsWebData3), + ClearinghouseState(WsClearinghouseState), + TwapStates(WsTwapStates), + UserFills(WsUserFills), + OpenOrders(WsOpenOrders), + UserEvents(WsUserEvent), + UserFundings(WsUserFundings), + UserNonFundingLedgerUpdates(WsUserNonFundingLedgerUpdate), + ActiveAssetCtx(WsActiveAssetCtx), + ActiveAssetData(WsActiveAssetData), + UserTwapSliceFills(WsUserTwapSliceFills), + UserTwapHistory(WsUserTwapHistory), + Bbo(WsBbo), }