diff --git a/.gitignore b/.gitignore index c7fc172..9ac6f20 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ config.toml commit_msg +.DS_STORE target/ configs/ diff --git a/Cargo.lock b/Cargo.lock index fb56a4f..9ce13e8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,18 +4,18 @@ version = 3 [[package]] name = "addr2line" -version = "0.21.0" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" dependencies = [ "gimli", ] [[package]] -name = "adler" -version = "1.0.2" +name = "adler2" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" [[package]] name = "android-tzdata" @@ -33,54 +33,75 @@ dependencies = [ ] [[package]] -name = "async-recursion" -version = "1.0.5" +name = "assert-json-diff" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd55a5ba1179988837d24ab4c7cc8ed6efdeff578ede0416b4225a5fca35bd0" +checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", "syn", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" -version = "1.1.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "backtrace" -version = "0.3.69" +version = "0.3.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" dependencies = [ "addr2line", - "cc", "cfg-if", "libc", "miniz_oxide", "object", "rustc-demangle", + "windows-targets", ] [[package]] name = "base64" -version = "0.21.7" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bitflags" -version = "1.3.2" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" [[package]] name = "block-buffer" @@ -93,9 +114,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.14.0" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "byteorder" @@ -105,15 +126,15 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.5.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" +checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" [[package]] name = "cbadv" -version = "1.4.0" +version = "2.0.0" dependencies = [ - "async-recursion", + "assert-json-diff", "base64", "chrono", "futures", @@ -127,8 +148,10 @@ dependencies = [ "ring", "serde", "serde_json", + "serde_with", "sha2", "tokio", + "tokio-test", "tokio-tungstenite", "toml", "uuid", @@ -136,11 +159,11 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.83" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +checksum = "f34d93e62b03caf570cccc334cbc6c2fceca82f39211051345108adcba3eebdc" dependencies = [ - "libc", + "shlex", ] [[package]] @@ -151,16 +174,17 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.33" +version = "0.4.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f13690e35a5e4ace198e7beea2895d29f3a9cc55015fcebe6336bd2010af9eb" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" dependencies = [ "android-tzdata", "iana-time-zone", "js-sys", "num-traits", + "serde", "wasm-bindgen", - "windows-targets 0.52.0", + "windows-targets", ] [[package]] @@ -175,15 +199,15 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "cpufeatures" -version = "0.2.12" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3" dependencies = [ "libc", ] @@ -198,11 +222,56 @@ dependencies = [ "typenum", ] +[[package]] +name = "darling" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +dependencies = [ + "darling_core", + "quote", + "syn", +] + [[package]] name = "data-encoding" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" +checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", + "serde", +] [[package]] name = "digest" @@ -215,11 +284,22 @@ dependencies = [ "subtle", ] +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "encoding_rs" -version = "0.8.33" +version = "0.8.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" dependencies = [ "cfg-if", ] @@ -232,19 +312,19 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.8" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "fastrand" -version = "2.0.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" +checksum = "486f806e73c5707928240ddc295403b1b93c96a02038563881c4a2fd84b81ac4" [[package]] name = "fnv" @@ -278,9 +358,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" dependencies = [ "futures-channel", "futures-core", @@ -293,9 +373,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", "futures-sink", @@ -303,15 +383,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-executor" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" dependencies = [ "futures-core", "futures-task", @@ -320,15 +400,15 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-macro" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", @@ -337,21 +417,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-util" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-channel", "futures-core", @@ -377,9 +457,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.12" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", @@ -388,23 +468,23 @@ dependencies = [ [[package]] name = "gimli" -version = "0.28.1" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "h2" -version = "0.3.24" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9" +checksum = "ccae279728d634d083c00f6099cb58f01cc99c145b84b8be2f6c74618d79922e" dependencies = [ + "atomic-waker", "bytes", "fnv", "futures-core", "futures-sink", - "futures-util", "http", - "indexmap", + "indexmap 2.7.0", "slab", "tokio", "tokio-util", @@ -413,15 +493,15 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.14.3" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] -name = "hermit-abi" -version = "0.3.4" +name = "hashbrown" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d3d0e0f38255e7fa3cf31335b3a56f05febd18025f4db5ef7a0cfb4f8da651f" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" [[package]] name = "hex" @@ -440,9 +520,9 @@ dependencies = [ [[package]] name = "http" -version = "0.2.11" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" dependencies = [ "bytes", "fnv", @@ -451,69 +531,110 @@ dependencies = [ [[package]] name = "http-body" -version = "0.4.6" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", "http", - "pin-project-lite", ] [[package]] -name = "httparse" -version = "1.8.0" +name = "http-body-util" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +dependencies = [ + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", +] [[package]] -name = "httpdate" -version = "1.0.3" +name = "httparse" +version = "1.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" [[package]] name = "hyper" -version = "0.14.28" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" +checksum = "97818827ef4f364230e16705d4706e2897df2bb60617d6ca15d598025a3c481f" dependencies = [ "bytes", "futures-channel", - "futures-core", "futures-util", "h2", "http", "http-body", "httparse", - "httpdate", "itoa", "pin-project-lite", - "socket2", + "smallvec", "tokio", - "tower-service", - "tracing", "want", ] +[[package]] +name = "hyper-rustls" +version = "0.27.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" +dependencies = [ + "futures-util", + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + [[package]] name = "hyper-tls" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ "bytes", + "http-body-util", "hyper", + "hyper-util", "native-tls", "tokio", "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", ] [[package]] name = "iana-time-zone" -version = "0.1.59" +version = "0.1.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6a67363e2aa4443928ce15e57ebae94fd8949958fd1223c4cfc0cd473ad7539" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -532,70 +653,218 @@ dependencies = [ "cc", ] +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" -version = "0.5.0" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ - "unicode-bidi", - "unicode-normalization", + "autocfg", + "hashbrown 0.12.3", + "serde", ] [[package]] name = "indexmap" -version = "2.2.1" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "433de089bd45971eecf4668ee0ee8f4cec17db4f8bd8f7bc3197a6ce37aa7d9b" +checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.15.2", + "serde", ] [[package]] name = "ipnet" -version = "2.9.0" +version = "2.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" +checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" [[package]] name = "itoa" -version = "1.0.10" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] name = "js-sys" -version = "0.3.67" +version = "0.3.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a1d36f1235bc969acba30b7f5990b864423a6068a10f7c90ae8f0112e3a59d1" +checksum = "a865e038f7f6ed956f788f0d7d60c541fff74c7bd74272c5d4cf15c63743e705" dependencies = [ + "once_cell", "wasm-bindgen", ] [[package]] -name = "lazy_static" -version = "1.4.0" +name = "libc" +version = "0.2.167" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +checksum = "09d6582e104315a817dff97f75133544b2e094ee22447d2acf4a74e189ba06fc" [[package]] -name = "libc" -version = "0.2.152" +name = "linux-raw-sys" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] -name = "linux-raw-sys" -version = "0.4.13" +name = "litemap" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" +checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" [[package]] name = "lock_api" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", @@ -603,15 +872,15 @@ dependencies = [ [[package]] name = "log" -version = "0.4.20" +version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "memchr" -version = "2.7.1" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "mime" @@ -621,31 +890,30 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "miniz_oxide" -version = "0.7.1" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" dependencies = [ - "adler", + "adler2", ] [[package]] name = "mio" -version = "0.8.10" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ "libc", "wasi", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] name = "native-tls" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" dependencies = [ - "lazy_static", "libc", "log", "openssl", @@ -658,46 +926,42 @@ dependencies = [ ] [[package]] -name = "num-traits" -version = "0.2.17" +name = "num-conv" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" -dependencies = [ - "autocfg", -] +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" [[package]] -name = "num_cpus" -version = "1.16.0" +name = "num-traits" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ - "hermit-abi", - "libc", + "autocfg", ] [[package]] name = "object" -version = "0.32.2" +version = "0.36.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" dependencies = [ "memchr", ] [[package]] name = "once_cell" -version = "1.19.0" +version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] name = "openssl" -version = "0.10.63" +version = "0.10.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15c9d69dd87a29568d4d017cfe8ec518706046a05184e5aea92d0af890b803c8" +checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5" dependencies = [ - "bitflags 2.4.2", + "bitflags", "cfg-if", "foreign-types", "libc", @@ -725,9 +989,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.99" +version = "0.9.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22e1bf214306098e4832460f797824c05d25aacdf896f64a985fb0fd992454ae" +checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" dependencies = [ "cc", "libc", @@ -737,9 +1001,9 @@ dependencies = [ [[package]] name = "parking_lot" -version = "0.12.1" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ "lock_api", "parking_lot_core", @@ -747,15 +1011,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.9" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-targets 0.48.5", + "windows-targets", ] [[package]] @@ -766,9 +1030,9 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pin-project-lite" -version = "0.2.13" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" +checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" [[package]] name = "pin-utils" @@ -778,30 +1042,39 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" -version = "0.3.29" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" + +[[package]] +name = "powerfmt" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2900ede94e305130c13ddd391e0ab7cbaeb783945ae07a279c268cb05109c6cb" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" -version = "0.2.17" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] [[package]] name = "proc-macro2" -version = "1.0.78" +version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.35" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" dependencies = [ "proc-macro2", ] @@ -838,18 +1111,18 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.4.1" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" dependencies = [ - "bitflags 1.3.2", + "bitflags", ] [[package]] name = "reqwest" -version = "0.11.23" +version = "0.12.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37b1ae8d9ac08420c66222fb9096fc5de435c3c48542bc5336c51892cffafb41" +checksum = "a77c62af46e79de0a562e1a9849205ffcb7fc1238876e9bd743357570e04046f" dependencies = [ "base64", "bytes", @@ -859,8 +1132,11 @@ dependencies = [ "h2", "http", "http-body", + "http-body-util", "hyper", + "hyper-rustls", "hyper-tls", + "hyper-util", "ipnet", "js-sys", "log", @@ -869,9 +1145,11 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", + "rustls-pemfile", "serde", "serde_json", "serde_urlencoded", + "sync_wrapper", "system-configuration", "tokio", "tokio-native-tls", @@ -880,55 +1158,95 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "winreg", + "windows-registry", ] [[package]] name = "ring" -version = "0.17.7" +version = "0.17.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "688c63d65483050968b2a8937f7995f443e27041a0f7700aa59b0822aedebb74" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" dependencies = [ "cc", + "cfg-if", "getrandom", "libc", "spin", "untrusted", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] name = "rustc-demangle" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustix" -version = "0.38.30" +version = "0.38.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "322394588aaf33c24007e8bb3238ee3e4c5c09c084ab32bc73890b99ff326bca" +checksum = "d7f649912bc1495e167a6edee79151c84b1bad49748cb4f1f1167f459f6224f6" dependencies = [ - "bitflags 2.4.2", + "bitflags", "errno", "libc", "linux-raw-sys", "windows-sys 0.52.0", ] +[[package]] +name = "rustls" +version = "0.23.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "934b404430bb06b3fae2cba809eb45a1ab1aecd64491213d7c3301b88393f8d1" +dependencies = [ + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b" + +[[package]] +name = "rustls-webpki" +version = "0.102.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "ryu" -version = "1.0.16" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "schannel" -version = "0.1.23" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -939,11 +1257,11 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "security-framework" -version = "2.9.2" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 1.3.2", + "bitflags", "core-foundation", "core-foundation-sys", "libc", @@ -952,9 +1270,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.9.1" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +checksum = "fa39c7303dc58b5543c94d22c1766b0d31f2ee58306363ea622b10bbc075eaa2" dependencies = [ "core-foundation-sys", "libc", @@ -962,18 +1280,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.196" +version = "1.0.215" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32" +checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.196" +version = "1.0.215" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67" +checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" dependencies = [ "proc-macro2", "quote", @@ -982,20 +1300,21 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.113" +version = "1.0.133" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69801b70b1c3dac963ecb03a364ba0ceda9cf60c71cfe475e99864759c8b8a79" +checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" dependencies = [ "itoa", + "memchr", "ryu", "serde", ] [[package]] name = "serde_spanned" -version = "0.6.5" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" dependencies = [ "serde", ] @@ -1012,6 +1331,36 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e28bdad6db2b8340e449f7108f020b3b092e8583a9e3fb82713e1d4e71fe817" +dependencies = [ + "base64", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.7.0", + "serde", + "serde_derive", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d846214a9854ef724f3da161b426242d8de7c1fc7de2f89bb1efcb154dca79d" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "sha1" version = "0.10.6" @@ -1034,11 +1383,17 @@ dependencies = [ "digest", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "signal-hook-registry" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" dependencies = [ "libc", ] @@ -1054,18 +1409,18 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.13.1" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "socket2" -version = "0.5.5" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" +checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" dependencies = [ "libc", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -1074,39 +1429,71 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "subtle" -version = "2.5.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.48" +version = "2.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" +checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "system-configuration" -version = "0.5.1" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags 1.3.2", + "bitflags", "core-foundation", "system-configuration-sys", ] [[package]] name = "system-configuration-sys" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" dependencies = [ "core-foundation-sys", "libc", @@ -1114,31 +1501,31 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.9.0" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01ce4141aa927a6d1bd34a041795abd0db1cccba5d5f24b009f694bdf3a1f3fa" +checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c" dependencies = [ "cfg-if", "fastrand", - "redox_syscall", + "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "thiserror" -version = "1.0.56" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.56" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", @@ -1146,44 +1533,69 @@ dependencies = [ ] [[package]] -name = "tinyvec" -version = "1.6.0" +name = "time" +version = "0.3.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" dependencies = [ - "tinyvec_macros", + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", ] [[package]] -name = "tinyvec_macros" -version = "0.1.1" +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] [[package]] name = "tokio" -version = "1.35.1" +version = "1.41.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c89b4efa943be685f629b149f53829423f8f5531ea21249408e8e2f8671ec104" +checksum = "22cfb5bee7a6a52939ca9224d6ac897bb669134078daa8735560897f69de4d33" dependencies = [ "backtrace", "bytes", "libc", "mio", - "num_cpus", "parking_lot", "pin-project-lite", "signal-hook-registry", "socket2", "tokio-macros", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] name = "tokio-macros" -version = "2.2.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", @@ -1200,11 +1612,46 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-rustls" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" +dependencies = [ + "rustls", + "rustls-pki-types", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f4e6ce100d0eb49a2734f8c0812bcd324cf357d21810932c5df6b96ef2b86f1" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-test" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2468baabc3311435b55dd935f702f42cd1b8abb7e754fb7dfb16bd36aa88f9f7" +dependencies = [ + "async-stream", + "bytes", + "futures-core", + "tokio", + "tokio-stream", +] + [[package]] name = "tokio-tungstenite" -version = "0.19.0" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec509ac96e9a0c43427c74f003127d953a265737636129424288d27cb5c4b12c" +checksum = "edc5f74e248dc973e0dbb7b74c7e0d6fcc301c694ff50049504004ef4d0cdcd9" dependencies = [ "futures-util", "log", @@ -1216,23 +1663,22 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.10" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" +checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" dependencies = [ "bytes", "futures-core", "futures-sink", "pin-project-lite", "tokio", - "tracing", ] [[package]] name = "toml" -version = "0.7.8" +version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd79e69d3b627db300ff956027cc6c3798cef26d22526befdfcd12feeb6d2257" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" dependencies = [ "serde", "serde_spanned", @@ -1242,20 +1688,20 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.5" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.19.15" +version = "0.22.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" dependencies = [ - "indexmap", + "indexmap 2.7.0", "serde", "serde_spanned", "toml_datetime", @@ -1264,15 +1710,15 @@ dependencies = [ [[package]] name = "tower-service" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.40" +version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ "pin-project-lite", "tracing-core", @@ -1280,9 +1726,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.32" +version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" dependencies = [ "once_cell", ] @@ -1295,9 +1741,9 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "tungstenite" -version = "0.19.0" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15fba1a6d6bb030745759a9a2a588bfe8490fc8b4751a277db3a0be1c9ebbf67" +checksum = "18e5b8366ee7a95b16d32197d0b2604b43a0be89dc5fac9f8e96ccafbaedda8a" dependencies = [ "byteorder", "bytes", @@ -1309,7 +1755,6 @@ dependencies = [ "rand", "sha1", "thiserror", - "url", "utf-8", ] @@ -1319,26 +1764,11 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" -[[package]] -name = "unicode-bidi" -version = "0.3.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" - [[package]] name = "unicode-ident" -version = "1.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" - -[[package]] -name = "unicode-normalization" -version = "0.1.22" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" -dependencies = [ - "tinyvec", -] +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" [[package]] name = "untrusted" @@ -1348,9 +1778,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.0" +version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" dependencies = [ "form_urlencoded", "idna", @@ -1363,11 +1793,23 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "uuid" -version = "1.7.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f00cc9702ca12d3c81455259621e676d0f7251cec66a21e98fe2e9a37db93b2a" +checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" dependencies = [ "getrandom", "rand", @@ -1376,9 +1818,9 @@ dependencies = [ [[package]] name = "uuid-macro-internal" -version = "1.7.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7abb14ae1a50dad63eaa768a458ef43d298cd1bd44951677bd10b732a9ba2a2d" +checksum = "6b91f57fe13a38d0ce9e28a03463d8d3c2468ed03d75375110ec71d93b449a08" dependencies = [ "proc-macro2", "quote", @@ -1393,9 +1835,9 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "version_check" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "want" @@ -1414,19 +1856,20 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.90" +version = "0.2.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1223296a201415c7fad14792dbefaace9bd52b62d33453ade1c5b5f07555406" +checksum = "d15e63b4482863c109d70a7b8706c1e364eb6ea449b201a76c5b89cedcec2d5c" dependencies = [ "cfg-if", + "once_cell", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.90" +version = "0.2.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcdc935b63408d58a32f8cc9738a0bffd8f05cc7c002086c6ef20b7312ad9dcd" +checksum = "8d36ef12e3aaca16ddd3f67922bc63e48e953f126de60bd33ccc0101ef9998cd" dependencies = [ "bumpalo", "log", @@ -1439,21 +1882,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.40" +version = "0.4.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bde2032aeb86bdfaecc8b261eef3cba735cc426c1f3a3416d1e0791be95fc461" +checksum = "9dfaf8f50e5f293737ee323940c7d8b08a66a95a419223d9f41610ca08b0833d" dependencies = [ "cfg-if", "js-sys", + "once_cell", "wasm-bindgen", "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.90" +version = "0.2.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e4c238561b2d428924c49815533a8b9121c664599558a5d9ec51f8a1740a999" +checksum = "705440e08b42d3e4b36de7d66c944be628d579796b8090bfa3471478a2260051" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1461,9 +1905,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.90" +version = "0.2.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bae1abb6806dc1ad9e560ed242107c0f6c84335f1749dd4e8ddb012ebd5e25a7" +checksum = "98c9ae5a76e46f4deecd0f0255cc223cfa18dc9b261213b8aa0c7b36f61b3f1d" dependencies = [ "proc-macro2", "quote", @@ -1474,15 +1918,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.90" +version = "0.2.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d91413b1c31d7539ba5ef2451af3f0b833a005eb27a631cec32bc0635a8602b" +checksum = "6ee99da9c5ba11bd675621338ef6fa52296b76b83305e9b6e5c77d4c286d6d49" [[package]] name = "web-sys" -version = "0.3.67" +version = "0.3.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58cd2333b6e0be7a39605f0e255892fd7418a682d8da8fe042fe25128794d2ed" +checksum = "a98bc3c33f0fe7e59ad7cd041b89034fa82a7c2d4365ca538dda6cdaf513863c" dependencies = [ "js-sys", "wasm-bindgen", @@ -1494,16 +1938,37 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets 0.52.0", + "windows-targets", ] [[package]] -name = "windows-sys" -version = "0.48.0" +name = "windows-registry" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" +dependencies = [ + "windows-result", + "windows-strings", + "windows-targets", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" dependencies = [ - "windows-targets 0.48.5", + "windows-result", + "windows-targets", ] [[package]] @@ -1512,138 +1977,193 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.0", + "windows-targets", ] [[package]] -name = "windows-targets" -version = "0.48.5" +name = "windows-sys" +version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", + "windows-targets", ] [[package]] name = "windows-targets" -version = "0.52.0" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.0", - "windows_aarch64_msvc 0.52.0", - "windows_i686_gnu 0.52.0", - "windows_i686_msvc 0.52.0", - "windows_x86_64_gnu 0.52.0", - "windows_x86_64_gnullvm 0.52.0", - "windows_x86_64_msvc 0.52.0", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] [[package]] name = "windows_aarch64_gnullvm" -version = "0.48.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.0" +name = "windows_aarch64_msvc" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" +name = "windows_i686_gnu" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] -name = "windows_aarch64_msvc" -version = "0.52.0" +name = "windows_i686_gnullvm" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] -name = "windows_i686_gnu" -version = "0.48.5" +name = "windows_i686_msvc" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] -name = "windows_i686_gnu" -version = "0.52.0" +name = "windows_x86_64_gnu" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] -name = "windows_i686_msvc" -version = "0.48.5" +name = "windows_x86_64_gnullvm" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] -name = "windows_i686_msvc" -version = "0.52.0" +name = "windows_x86_64_msvc" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" +name = "winnow" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" +dependencies = [ + "memchr", +] + +[[package]] +name = "write16" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" [[package]] -name = "windows_x86_64_gnu" -version = "0.52.0" +name = "writeable" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" [[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" +name = "yoke" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] [[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.0" +name = "yoke-derive" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] [[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" +name = "zerocopy" +version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] [[package]] -name = "windows_x86_64_msvc" -version = "0.52.0" +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" +checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" +dependencies = [ + "zerofrom-derive", +] [[package]] -name = "winnow" -version = "0.5.35" +name = "zerofrom-derive" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1931d78a9c73861da0134f453bb1f790ce49b2e30eba8410b4b79bac72b46a2d" +checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" dependencies = [ - "memchr", + "proc-macro2", + "quote", + "syn", + "synstructure", ] [[package]] -name = "winreg" -version = "0.50.0" +name = "zeroize" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" dependencies = [ - "cfg-if", - "windows-sys 0.48.0", + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] diff --git a/Cargo.toml b/Cargo.toml index ff17aa9..95573ca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,25 +1,70 @@ [package] name = "cbadv" -license = "MIT" -version = "1.4.0" +version = "2.0.0" edition = "2021" description = "Asynchronous Coinbase Advanced REST and WebSocket API" +license = "MIT" readme = "README.md" homepage = "https://github.com/Ohkthx/cbadv-rs" repository = "https://github.com/Ohkthx/cbadv-rs" keywords = ["trading", "coinbase", "coinbasepro", "coinbaseadvanced", "crypto"] +categories = ["api-bindings", "cryptocurrency"] include = ["*/**.rs"] [features] -default = [] +default = ["config"] full = ["config"] config = ["dep:toml"] +[dependencies] +# Core dependencies +reqwest = { version = "0.12.9", features = ["json"] } +futures = "0.3.31" +tokio = { version = "1.41.1", features = ["full"] } + +# Cryptography and signing +hmac = "0.12.1" +sha2 = "0.10.8" +hex = "0.4.3" + +# Serialization and configuration +serde = { version = "1.0.215", features = ["derive"] } +serde_json = "1.0.133" +serde_with = "3.11.0" +toml = { version = "0.8.19", optional = true } + +# WebSocket support +tokio-tungstenite = { version = "0.24.0", features = ["native-tls"] } +futures-util = "0.3.31" + +# Utilities +uuid = { version = "1.11.0", features = [ + "v4", + "fast-rng", + "macro-diagnostics", +] } +chrono = "0.4.38" +num-traits = "0.2.19" +base64 = "0.22.1" +ring = "0.17.8" +rand = "0.8.5" +openssl = "0.10.68" + [[example]] name = "account_api" path = "examples/account_api.rs" required-features = ["config"] +[[example]] +name = "convert_api" +path = "examples/convert_api.rs" +required-features = ["config"] + +[[example]] +name = "payment_api" +path = "examples/payment_api.rs" +required-features = ["config"] + [[example]] name = "product_api" path = "examples/product_api.rs" @@ -36,8 +81,22 @@ path = "examples/order_api.rs" required-features = ["config"] [[example]] -name = "util_api" -path = "examples/util_api.rs" +name = "public_api" +path = "examples/public_api.rs" + +[[example]] +name = "sandbox_api" +path = "examples/sandbox_api.rs" +required-features = ["config"] + +[[example]] +name = "portfolio_api" +path = "examples/portfolio_api.rs" +required-features = ["config"] + +[[example]] +name = "data_api" +path = "examples/data_api.rs" required-features = ["config"] [[example]] @@ -45,10 +104,14 @@ name = "websocket" path = "examples/websocket.rs" required-features = ["config"] +[[example]] +name = "websocket_user" +path = "examples/websocket_user.rs" +required-features = ["config"] + [[example]] name = "watch_candles" path = "examples/watch_candles.rs" -required-features = ["config"] [[example]] name = "custom_config" @@ -61,23 +124,10 @@ lto = "fat" codegen-units = 1 opt-level = 3 -[dependencies] -reqwest = { version = "0.11", features = ["json"] } # Making HTTP requests. -futures = { version = "0.3" } # Async / await blocks -tokio = { version = "1.12.0", features = ["full"] } # Async runtime -hmac = { version = "0.12.1" } # Signing requests with a signature. -sha2 = { version = "0.10.6" } # Signing requests with a signature. -hex = { version = "0.4.3" } # Convert signature for HTTP headers. -serde_json = { version = "1.0.96" } # Converting Configuration file and Objects from API. -serde = { version = "1.0.163", features = ["derive"] } # Converting Configuration file and Objects from API. -toml = { version = "0.7.3", optional = true } # Creating Configuration file. -uuid = { version = "1.3.4", features = ["v4", "fast-rng", "macro-diagnostics"] } # Create Client ID for orders. -async-recursion = { version = "1.0.4" } # Recursive async functions require this. -tokio-tungstenite = { version = "0.19.0", features = ["native-tls"] } # WebSocket requirement. -futures-util = { version = "0.3.28" } # Required for the WebSocket client. -chrono = { version = "0.4.31" } # Used to pass current candle timestamp to candle watcher. -num-traits = "0.2.17" -openssl = "0.10.63" -base64 = "0.21.7" -ring = "0.17.7" -rand = "0.8.5" +[dev-dependencies] +tokio-test = "0.4.4" +assert-json-diff = "2.0.2" + +[badges] +travis-ci = { repository = "ohkthx/cbadv-rs", branch = "main" } +maintenance = { status = "actively-developed" } diff --git a/README.md b/README.md index 478a7a4..aa7d6f2 100644 --- a/README.md +++ b/README.md @@ -17,133 +17,166 @@ alt="crates.io downloads"> GitHub repo size

+--- + # Asynchronous CoinBase Advanced API -The objective of this crate is to grant highly performant asynchronous access to the **CoinBase Advanced** REST and WebSocket API. Included with the crate are ways to organize your API Keys and Secrets inside of a configuration file. +The **cbadv-rs** crate provides high-performance, asynchronous access to the Coinbase Advanced REST and WebSocket APIs. This project includes features to securely configure API keys and secrets, making it suitable for developers seeking robust API integration. -This project is current a work-in-progress. Changes between versions can vary greatly as this API becomes more refined and adapts to CoinBase Advances changing state. I ask you to understand that I am not liable for any issues you may encounter while this project is in this state and encourage you to verify and test before committing to using this yourself in a serious manner such as in production. +This project is currently a work-in-progress. While the crate is usable, API changes or updates may occur as Coinbase Advanced evolves. Please thoroughly test before using in production. -Contributions are encouraged! The API reference can be seen at [CoinBase Advanced API](https://docs.cloud.coinbase.com/advanced-trade-api/reference). If you wish to add this to your project, either use `cargo add cbadv` or add the following line to your dependencies section in **Cargo.toml**: +To get started, add this crate to your project using `cargo add cbadv` or manually add the following to your `Cargo.toml`: ```toml [dependencies] cbadv = { git = "https://github.com/ohkthx/cbadv-rs", branch = "main" } ``` +--- + +## Table of Contents + +- [Features](#features) +- [Documentation](#documentation) +- [Configuration](#configuration) +- [Examples](#examples) +- [API Coverage](#api-coverage) + - [WebSocket API](#websocket-api) + - [REST API](#rest-api) +- [TODO](#todo) +- [Contributing](#contributing) +- [Tips Appreciated!](#tips-appreciated) + +--- + ## Features -- Asynchronous. -- Easy-to-use REST and WebSocket clients. -- Configuration file to hold API Key and API Secret. `features = ["config"]` -- Covers all REST endpoints currently accessible (as of 20231206). -- Covers all WebSocket endpoints currently accessible (as of 20231206). -- Lots of examples! Check them out to get started. +- Asynchronous API access with support for REST and WebSocket protocols. +- Authenticated and Public REST Endpoints. +- Builders to create REST and WebSocket Clients. +- Convenient configuration file support for API keys (`features = ["config"]`). +- Comprehensive coverage of all accessible REST and WebSocket endpoints (as of **20231206**). +- Numerous examples for seamless integration and testing. + +--- ## Documentation -Most of the documentation can be accessed by clicking the following link: [docs.rs](https://docs.rs/cbadv/latest/cbadv/). That documentation is automatically generated and also accessible from [crates.io](https://crates.io/crates/cbadv). - -### Covered API requests - -#### WebSocket API - -Client: `use cbadv::WebSocketClient` - -- **Authentication** [client.connect] -- **Subscribe** [client.subscribe / client.sub] -- **Unsubscribe** [client.unsubscribe / client.unsub] -- **Channels Supported** - - Status [Channel::STATUS] - - Candles [Channel::CANDLES] - - Ticker [Channel::TICKER] - - Ticker Batch [Channel::TICKER_BATCH] - - Level2 [Channel::LEVEL2] - - User [Channel::USER] - - Market Trades [Channel::MARKET_TRADES] - -#### REST API - -Client: `use cbadv::RestClient` - -- **Accounts [client.account]** - - List Accounts [client.account.get_bulk] - - Get Account [client.account.get] -- **Products [client.product]** - - Get Best Bid / Ask [client.product.best_bid_ask] - - Get Product Book [client.product.product_book] - - List Products [client.product.get_bulk] - - Get Product [client.product.get] - - Get Product Candles [client.product.candles] - - Get Market Trades (Ticker) [client.product.ticker] -- **Orders [client.order]** - - Create Order - - Market IOC (untested) [client.order.create_market] - - Limit GTC [client.order.create_limit_gtc] - - Limit GTD (untested) [client.order.create_limit_gtd] - - Stop Limit GTC (untested) [client.order.create_stop_limit_gtc] - - Stop Limit GTD (untested) [client.order.create_stop_limit_gtd] - - Edit Orders [client.order.edit] - - Edit Orders Preview [client.order.preview_edit] - - Cancel Orders [client.order.cancel] - - List Orders [client.order.get_bulk] - - List Fills (untested) [client.order.fills] - - Get Order [client.order.get] -- **Fees [client.fee]** - - Get Transaction Summary [client.fee.get] -- **Converts [client.convert]** - - Create Quote (untested) [client.convert.create_quote] - - Get Convert (untested) [client.convert.get] - - Commit Convert (untested) [client.convert.commit] -- **Utils [client.util]** - - Get API Unix Time [client.util.unixtime] - -### Added Requests and Features - -These functions were created to cover common functionality but not initially part of the CoinBase Advanced API. They may require several API requests to accomplish their results. - -- **REST: Accounts** [client.account] - - Get Account by ID [client.account.get_by_id] - Gets an account by the ID (ex BTC or ETH) - - Get All [client.account.get_all] - Gets all accounts. -- **REST: Products** [client.product] - - Get Candles (Extended) [client.product.candles_ext] - Obtains more than the limit (300) candles. -- **REST: Orders** [client.order] - - Get All Orders [client.order.get_all] - Obtains all orders for a product. - - Cancel All Orders [client.order.cancel_all] - Cancels all OPEN orders for a product. -- **WebSocket: Watch Candles** [client.watch_candles] - - Watches candles for for updates, produces completed candles for a series. - - Candles have 5 minute granularities, this cannot be changed in the current API. - -### TODO - -Test all endpoints that are currently untested. - -## Configuration Feature - -Configuration requires you to add the 'config' feature (`features = ["config"]`) to your `Cargo.toml`. The default configuration is unusable due to the API requiring a Key and Secret. You can create, modify, and delete API Keys and Secrets with this [link](https://www.coinbase.com/settings/api). - -Copy the `config.toml.sample` to `config.toml` and add in your API information. The `config.toml` file will automatically be read on launch to access your accounts API information. Unlike the depreciated CoinBase Pro API, there's no longer access to Public API endpoints. All access requires authentication. The key and secret is authentication requirements for HTTP requests to be properly [signed](https://docs.cloud.coinbase.com/advanced-trade-api/docs/rest-api-auth) and accepted by CoinBase. - -\***\*Custom configurations\*\*** can be created with additional sections beyond just `[coinbase]`. See [custom_config.toml.sample](https://github.com/Ohkthx/cbadv-rs/tree/main/custom_config.toml.sample) for an example of the configuration file. An example of how to implement and create a custom configuration file can be seen in [custom_config.rs](https://github.com/Ohkthx/cbadv-rs/tree/main/examples/custom_config.rs). - -Example of enabled `config` feature in `Cargo.toml`. +Full API documentation is available at [docs.rs](https://docs.rs/cbadv/latest/cbadv/). You can also find helpful information on [crates.io](https://crates.io/crates/cbadv). + +--- + +## API Coverage + +### WebSocket API + +Client: `use cbadv::{WebSocketClient, WebSocketClientBuilder}` + +- **Authentication**: `client.connect` +- **Subscribe**: `client.subscribe` or `client.sub` +- **Unsubscribe**: `client.unsubscribe` or `client.unsub` +- **Channels Supported**: + - `Channel::STATUS`: Status + - `Channel::CANDLES`: Candles + - `Channel::TICKER`: Ticker + - `Channel::TICKER_BATCH`: Ticker Batch + - `Channel::LEVEL2`: Level 2 Market Data + - `Channel::USER`: User-Specific Updates + - `Channel::MARKET_TRADES`: Market Trades + - `Channel::HEARTBEATS`: Hearbeat (maintains connection.) + - `Channel::FUTURES_BALANCE_SUMMARY`: Balance Summary for Futures. + +### REST API + +Client: `use cbadv::{RestClient, RestClientBuilder}` + +- **Accounts (`client.account`)**: + - List Accounts: `client.account.get_bulk` + - Get Account: `client.account.get` +- **Products (`client.product`)**: + - Get Best Bid/Ask: `client.product.best_bid_ask` + - Get Product Book: `client.product.product_book` + - List Products: `client.product.get_bulk` + - Get Product Details: `client.product.get` + - Get Product Candles: `client.product.candles` + - Get Market Trades (Ticker): `client.product.ticker` +- **Orders (`client.order`)**: + - Create Order: `client.order.create` + - Edit Order: `client.order.edit` + - Preview Order Edit: `client.order.preview_edit` + - Preview Order Create: `client.order.preview_create` + - Cancel Order: `client.order.cancel` + - List Orders: `client.order.get_bulk` + - List Fills: `client.order.fills` + - Get Order: `client.order.get` + - Close Position (untested): `client.order.close_position` +- **Fees (`client.fee`)**: + - Get Transaction Summary: `client.fee.get` +- **Converts (`client.convert`)**: + - Create Quote: `client.convert.create_quote` + - Get Convert: `client.convert.get` + - Commit Convert (untested): `client.convert.commit` +- **Portfolios (`client.portfolio`)**: + - Create Portfolio: `client.portfolio.create` + - List Portfolios: `client.portfolio.get_all` + - Get Portfolio Breakdown: `client.portfolio.get` + - Edit Portfolio: `client.portfolio.edit` + - Delete Portfolio: `client.portfolio.delete` + - Move Funds (untested): `client.portfolio.move_funds` +- **Payments (`client.payment`)** + - List Payments: `client.payment.get_all` + - Get Payment: `client.payment.get` +- **Data (`client.data`)** + - API Key Permissions: `client.data.key_permissions` +- **Public (`client.public`)**: + - Get API Unix Server Time: `client.public.time` + - Get Product Book: `client.public.product_book` + - List Products: `client.public.products` + - Get Product: `client.public.product` + - Get Product Candles: `client.public.candles` + - Get Product Ticker: `client.public.ticker` + +--- + +## Configuration + +To enable the configuration feature, include it in your `Cargo.toml`: ```toml [dependencies] cbadv = { version = "*", features = ["config"] } ``` +Set up `config.toml` with your API credentials. A sample file can be found at `config.toml.sample`. See the [custom configuration example](https://github.com/Ohkthx/cbadv-rs/tree/main/examples/custom_config.rs) for advanced setups. + +--- + ## Examples -Check above in the **Covered API requests** section for possibly covered examples. All examples are located at [cbadv-rs/examples](https://github.com/Ohkthx/cbadv-rs/tree/main/examples/) directory. +Explore the [examples directory](https://github.com/Ohkthx/cbadv-rs/tree/main/examples/) for usage scenarios. -## Tips Appreciated! +--- -Wallet addresses are provided below, or click the badges above! +## TODO -``` -Ethereum (ETH): 0x7d75f6a9c021fcc70691fec73368198823fb0f60 -Bitcoin (BTC): bc1q75w3cgutug8qdxw3jlmqnkjlv9alt3jr7ftha0 -Binance (BNB): 0x7d75f6a9c021fcc70691fec73368198823fb0f60 -``` +- Test unverified endpoints. +- Expand examples to cover more advanced cases. + +--- + +## Contributing + +Contributions are welcome! Fork the repository, create a feature branch, and submit a pull request. + +--- + +## Tips Appreciated + +Support this project via cryptocurrency donations: + +**Ethereum (ETH):** 0x7d75f6a9c021fcc70691fec73368198823fb0f60 +**Bitcoin (BTC):** bc1q75w3cgutug8qdxw3jlmqnkjlv9alt3jr7ftha0 +**Binance (BNB):** 0x7d75f6a9c021fcc70691fec73368198823fb0f60 diff --git a/config.toml.sample b/config.toml.sample index 94cbf81..a5d7b37 100644 --- a/config.toml.sample +++ b/config.toml.sample @@ -5,3 +5,4 @@ version = 1 api_key = "YOUR_COINBASE_API_KEY_HERE" api_secret = "YOUR_COINBASE_API_SECRET_HERE" debug = false +use_sandbox = true diff --git a/custom_config.toml.sample b/custom_config.toml.sample index b4357ba..9802f5d 100644 --- a/custom_config.toml.sample +++ b/custom_config.toml.sample @@ -9,3 +9,4 @@ version = 1 api_key = "YOUR_COINBASE_API_KEY_HERE" api_secret = "YOUR_COINBASE_API_SECRET_HERE" debug = false +use_sandbox = true diff --git a/examples/README.md b/examples/README.md index bc43ba2..b8907ab 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,70 +1,215 @@

- + crates.io version - + crates.io downloads - + GitHub repo size

-# Examples +# cbadv-rs: Coinbase Advanced Trading API Wrapper -The following examples are for testing and demonstrating the use of the crate. Please review the examples before running them to fully understand what is happening and how they are used. If you have any suggestions, feel free to let me know! +Welcome to **cbadv-rs**, a Rust crate for interacting with the Coinbase Advanced Trading API. This library provides easy-to-use interfaces for various Coinbase APIs such as Account, Product, Fee, Order, Portfolio, Public, Sandbox, and WebSocket. -## Account API +## Table of Contents -Demonstrates how to use the Account API, accessbile at: [account_api.rs](https://github.com/Ohkthx/cbadv-rs/tree/main/examples/account_api.rs) +- [Examples](#examples) + - [Account API](#account-api) + - [Product API](#product-api) + - [Fee API](#fee-api) + - [Order API](#order-api) + - [Portfolio API](#portfolio-api) + - [Payment API](#payment-api) + - [Convert API](#convert-api) + - [Data API](#data-api) + - [Public API](#public-api) + - [Sandbox API](#sandbox-api) + - [WebSocket API](#websocket-api) + - [User Orders (WebSocket API)](#user-orders-websocket-api) + - [Watch Candles (WebSocket API)](#watch-candles-websocket-api) + - [Custom Configurations](#custom-configurations) +- [Contributing](#contributing) +- [License](#license) -**Command**: +--- -- `cargo run --example account_api --features="config"` +## Examples -## Product API +This section showcases example usage of the crate. Each example demonstrates a different API or functionality. Before running these examples, review the corresponding source code to understand how they work. If you have any suggestions, feel free to open an issue or submit a pull request! -Demonstrates how to use the Product API, accessbile at: [product_api.rs](https://github.com/Ohkthx/cbadv-rs/tree/main/examples/product_api.rs) +### Account API -**Command**: +Learn how to use the Account API. Example source: [account_api.rs](https://github.com/Ohkthx/cbadv-rs/tree/main/examples/account_api.rs) -- `cargo run --example product_api --features="config"` +**Run the example**: -## Fee API +```bash +cargo run --example account_api --features="config" +``` -Demonstrates how to use the Fee API, accessbile at: [fee_api.rs](https://github.com/Ohkthx/cbadv-rs/tree/main/examples/fee_api.rs) +--- -**Command**: +### Product API -- `cargo run --example fee_api --features="config"` +Learn how to use the Product API. Example source: [product_api.rs](https://github.com/Ohkthx/cbadv-rs/tree/main/examples/product_api.rs) -## Order API +**Run the example**: -Demonstrates how to use the Order API, accessbile at: [order_api.rs](https://github.com/Ohkthx/cbadv-rs/tree/main/examples/order_api.rs) +```bash +cargo run --example product_api --features="config" +``` -**Command**: +--- -- `cargo run --example order_api --features="config"` +### Fee API -## WebSocket API +Learn how to use the Fee API. Example source: [fee_api.rs](https://github.com/Ohkthx/cbadv-rs/tree/main/examples/fee_api.rs) -Demonstrates how to use the WebSocket API, accessbile at: [websocket.rs](https://github.com/Ohkthx/cbadv-rs/tree/main/examples/websocket.rs) +**Run the example**: -**Command**: +```bash +cargo run --example fee_api --features="config" +``` -- `cargo run --example websocket --features="config"` +--- -### WebSocket API - Watch Candles +### Order API -Demonstrates how to use the Watch Candles via the WebSocket API, accessbile at: [watch_candles.rs](https://github.com/Ohkthx/cbadv-rs/tree/main/examples/watch_candles.rs) These candles are limited to 5 minute granularity and cannot be currently changed (as of 20231019). +Learn how to use the Order API. Example source: [order_api.rs](https://github.com/Ohkthx/cbadv-rs/tree/main/examples/order_api.rs) -**Command**: +**Run the example**: -- `cargo run --example watch_candles --features="config"` +```bash +cargo run --example order_api --features="config" +``` -## Custom Configurations +--- -Demonstrates how to create a custom configuration file to meet your needs in integration, accessbile at: [custom_config.rs](https://github.com/Ohkthx/cbadv-rs/tree/main/examples/custom_config.rs) +### Portfolio API -**Command**: +Learn how to use the Portfolio API. Example source: [portfolio_api.rs](https://github.com/Ohkthx/cbadv-rs/tree/main/examples/portfolio_api.rs) -- `cargo run --example custom_config --features="config"` +**Run the example**: + +```bash +cargo run --example portfolio_api --features="config" +``` + +--- + +### Convert API + +Learn how to use the Convert API. Example source: [convert_api.rs](https://github.com/Ohkthx/cbadv-rs/tree/main/examples/convert_api.rs) + +**Run the example**: + +```bash +cargo run --example convert_api --features="config" +``` + +--- + +### Payment API + +Learn how to use the Payment API. Example source: [payment_api.rs](https://github.com/Ohkthx/cbadv-rs/tree/main/examples/payment_api.rs) + +**Run the example**: + +```bash +cargo run --example payment_api --features="config" +``` + +--- + +### Data API + +Learn how to use the Data API. Example source: [data_api.rs](https://github.com/Ohkthx/cbadv-rs/tree/main/examples/data_api.rs) + +**Run the example**: + +```bash +cargo run --example data_api --features="config" +``` + +--- + +### Public API + +Learn how to use the Public API. Example source: [public_api.rs](https://github.com/Ohkthx/cbadv-rs/tree/main/examples/public_api.rs) + +**Run the example**: + +```bash +cargo run --example public_api --features="config" +``` + +--- + +### Sandbox API + +Learn how to use the Sandbox API for testing without affecting real accounts. Example source: [sandbox_api.rs](https://github.com/Ohkthx/cbadv-rs/tree/main/examples/sandbox_api.rs) + +**Run the example**: + +```bash +cargo run --example sandbox_api --features="config" +``` + +--- + +### WebSocket API + +Learn how to use the WebSocket API for real-time data. Example source: [websocket.rs](https://github.com/Ohkthx/cbadv-rs/tree/main/examples/websocket.rs) + +**Run the example**: + +```bash +cargo run --example websocket --features="config" +``` + +#### User Orders (WebSocket API) + +Learn how to watch user data via the WebSocket API. Example source: [websocket_user.rs](https://github.com/Ohkthx/cbadv-rs/tree/main/examples/websocket_user.rs) + +**Run the example**: + +```bash +cargo run --example websocket_user --features="config" +``` + +--- + +#### Watch Candles (WebSocket API) + +Learn how to watch candlestick data via the WebSocket API. Currently, only 5-minute granularity is supported (as of 2023-10-19). Example source: [watch_candles.rs](https://github.com/Ohkthx/cbadv-rs/tree/main/examples/watch_candles.rs) + +**Run the example**: + +```bash +cargo run --example watch_candles --features="config" +``` + +--- + +### Custom Configurations + +Learn how to create custom configuration files tailored to your integration needs. Example source: [custom_config.rs](https://github.com/Ohkthx/cbadv-rs/tree/main/examples/custom_config.rs) + +**Run the example**: + +```bash +cargo run --example custom_config --features="config" +``` + +--- + +## Contributing + +Contributions are welcome! Feel free to open issues or submit pull requests to improve this crate. For major changes, please open an issue first to discuss what you would like to change. + +## License + +This project is licensed under the [MIT License](https://opensource.org/licenses/MIT). See the [LICENSE](LICENSE) file for details. diff --git a/examples/account_api.rs b/examples/account_api.rs index a8e2126..029bb66 100644 --- a/examples/account_api.rs +++ b/examples/account_api.rs @@ -7,9 +7,9 @@ use std::process::exit; -use cbadv::account::ListAccountsQuery; +use cbadv::account::AccountListQuery; use cbadv::config::{self, BaseConfig}; -use cbadv::RestClient; +use cbadv::RestClientBuilder; #[tokio::main] async fn main() { @@ -33,7 +33,7 @@ async fn main() { }; // Create a client to interact with the API. - let mut client = match RestClient::from_config(&config) { + let mut client = match RestClientBuilder::new().with_config(&config).build() { Ok(c) => c, Err(why) => { eprintln!("!ERROR! {}", why); @@ -43,7 +43,11 @@ async fn main() { // Pull accounts by ID. println!("Obtaining account by ID (non-standard)."); - match client.account.get_by_id(product_name, None).await { + match client + .account + .get_by_id(product_name, &AccountListQuery::new()) + .await + { Ok(account) => println!("{:#?}", account), Err(error) => println!("Unable to get account: {}", error), } @@ -51,7 +55,7 @@ async fn main() { // Pull accounts by ID. let mut account_uuid = "".to_string(); println!("\n\nObtaining ALL accounts (non-standard)."); - match client.account.get_all(None).await { + match client.account.get_all(&AccountListQuery::new()).await { Ok(accounts) => { println!("Obtained {:#?} accounts.", accounts.len()); @@ -68,10 +72,7 @@ async fn main() { } // Parameters to send to the API. - let query = ListAccountsQuery { - // limit: Some(250), - ..Default::default() - }; + let query = AccountListQuery::new(); // Pull all accounts. println!("\n\nObtaining Bulk Accounts."); diff --git a/examples/convert_api.rs b/examples/convert_api.rs new file mode 100644 index 0000000..79bcd18 --- /dev/null +++ b/examples/convert_api.rs @@ -0,0 +1,71 @@ +//! # Convert API Example, check out the Convert API for all functionality. +//! +//! Shows how to: +//! - Create a convert quote. +//! - Obtain a convert quote. + +use std::process::exit; + +use cbadv::config::{self, BaseConfig}; +use cbadv::convert::{ConvertQuery, ConvertQuoteRequest}; +use cbadv::RestClientBuilder; + +#[tokio::main] +async fn main() { + let from_product: &str = "USDC"; + let to_product: &str = "USD"; + let amount: f64 = 0.05; + + // Load the configuration file. + let config: BaseConfig = match config::load("config.toml") { + Ok(c) => c, + Err(err) => { + println!("Could not load configuration file."); + if config::exists("config.toml") { + println!("File exists, {}", err); + exit(1); + } + + // Create a new configuration file. + config::create_base_config("config.toml").unwrap(); + println!("Empty configuration file created, please update it."); + exit(1); + } + }; + + // Create a client to interact with the API. + let mut client = match RestClientBuilder::new().with_config(&config).build() { + Ok(c) => c, + Err(why) => { + eprintln!("!ERROR! {}", why); + exit(1) + } + }; + + // Create a quote to convert USDC to USD. + println!( + "Creating a quote to convert {} {} to {}.", + amount, from_product, to_product + ); + let request = ConvertQuoteRequest::new(from_product, to_product, amount); + let quote = match client.convert.create_quote(&request).await { + Ok(q) => q, + Err(why) => { + eprintln!("!ERROR! {}", why); + exit(1) + } + }; + + println!("Quote created: {:#?}", quote); + println!("\n\nObtain the quote with the quote_id: {}", quote.id); + let query = ConvertQuery::new(from_product, to_product); + match client.convert.get("e.id, &query).await { + Ok(q) => { + println!("Quote obtained: {:#?}", q); + } + Err(why) => { + eprintln!("!ERROR! {}", why); + exit(1) + } + }; +} diff --git a/examples/custom_config.rs b/examples/custom_config.rs index 4993fd3..683db6c 100644 --- a/examples/custom_config.rs +++ b/examples/custom_config.rs @@ -8,7 +8,7 @@ use std::process::exit; use serde::{Deserialize, Serialize}; use cbadv::config::{self, ApiConfig, ConfigFile}; -use cbadv::RestClient; +use cbadv::RestClientBuilder; /// `[general]` section in the configuration file. #[derive(Serialize, Deserialize, Debug, Clone)] @@ -66,7 +66,7 @@ async fn main() { }; // Create a client to interact with the API. - let mut client = match RestClient::from_config(&config) { + let mut client = match RestClientBuilder::new().with_config(&config).build() { Ok(c) => c, Err(why) => { eprintln!("!ERROR! {}", why); diff --git a/examples/util_api.rs b/examples/data_api.rs similarity index 64% rename from examples/util_api.rs rename to examples/data_api.rs index e7214bf..6b60ece 100644 --- a/examples/util_api.rs +++ b/examples/data_api.rs @@ -1,12 +1,12 @@ -//! # Util API Example, check out the Util API for all functionality. +//! # Data API Example, check out the Data API for all functionality. //! //! Shows how to: -//! - Obtain the API Unix time. +//! - Obtain API Key Permissions. use std::process::exit; use cbadv::config::{self, BaseConfig}; -use cbadv::RestClient; +use cbadv::RestClientBuilder; #[tokio::main] async fn main() { @@ -28,7 +28,7 @@ async fn main() { }; // Create a client to interact with the API. - let mut client = match RestClient::from_config(&config) { + let mut client = match RestClientBuilder::new().with_config(&config).build() { Ok(c) => c, Err(why) => { eprintln!("!ERROR! {}", why); @@ -36,10 +36,10 @@ async fn main() { } }; - // Get API Unix time. - println!("Obtaining API Unix time"); - match client.util.unixtime().await { - Ok(time) => println!("{:#?}", time), - Err(error) => println!("Unable to get the Unix time: {}", error), + // Get the API key permissions. + println!("Obtaining Key Permissions for the API key."); + match client.data.key_permissions().await { + Ok(perm) => println!("{:#?}", perm), + Err(error) => println!("Unable to get the API key permissions: {}", error), } } diff --git a/examples/fee_api.rs b/examples/fee_api.rs index da4c29c..ca26e8c 100644 --- a/examples/fee_api.rs +++ b/examples/fee_api.rs @@ -6,8 +6,9 @@ use std::process::exit; use cbadv::config::{self, BaseConfig}; -use cbadv::fee::TransactionSummaryQuery; -use cbadv::RestClient; +use cbadv::fee::FeeTransactionSummaryQuery; +use cbadv::product::ProductType; +use cbadv::RestClientBuilder; #[tokio::main] async fn main() { @@ -29,7 +30,7 @@ async fn main() { }; // Create a client to interact with the API. - let mut client = match RestClient::from_config(&config) { + let mut client = match RestClientBuilder::new().with_config(&config).build() { Ok(c) => c, Err(why) => { eprintln!("!ERROR! {}", why); @@ -38,7 +39,7 @@ async fn main() { }; // Parameters to send to the API. - let params = TransactionSummaryQuery::default(); + let params = FeeTransactionSummaryQuery::new().product_type(ProductType::Spot); // Get fee transaction summary. println!("Obtaining Transaction Fee Summary"); diff --git a/examples/order_api.rs b/examples/order_api.rs index 510e38c..686da51 100644 --- a/examples/order_api.rs +++ b/examples/order_api.rs @@ -9,21 +9,38 @@ //! - Obtain specific order by ID. use std::process::exit; +use std::thread; use cbadv::config::{self, BaseConfig}; -use cbadv::order::{ListOrdersQuery, OrderSide}; -use cbadv::RestClient; +use cbadv::order::{ + OrderCancelRequest, OrderCreateBuilder, OrderEditRequest, OrderListQuery, OrderSide, + OrderStatus, OrderType, TimeInForce, +}; +use cbadv::RestClientBuilder; +use chrono::Duration; #[tokio::main] async fn main() { - let create_trade: bool = false; - let cancel_open_orders: bool = false; - let edit_open_order_id: Option = None; - let product_pair: &str = "DOGE-USD"; - let total_size: f64 = 300.0; - let price: f64 = 100.00; - let edit_price: f64 = 50.00; - let side: &str = "SELL"; + let create_new: bool = false; + let edit_created: bool = true; + let cancel_created: bool = true; + let cancel_all: bool = false; + let product_id: &str = "ETH-USDC"; + let mut created_order_id: Option = None; + let new_order = match OrderCreateBuilder::new(product_id, &OrderSide::Buy) + .base_size(0.005) + .limit_price(100.0) + .post_only(true) + .order_type(OrderType::Limit) + .time_in_force(TimeInForce::GoodUntilCancelled) + .build() + { + Ok(order) => order, + Err(error) => { + println!("Unable to build order: {}", error); + exit(1); + } + }; // Load the configuration file. let config: BaseConfig = match config::load("config.toml") { @@ -43,7 +60,7 @@ async fn main() { }; // Create a client to interact with the API. - let mut client = match RestClient::from_config(&config) { + let mut client = match RestClientBuilder::new().with_config(&config).build() { Ok(c) => c, Err(why) => { eprintln!("!ERROR! {}", why); @@ -51,49 +68,76 @@ async fn main() { } }; - if create_trade { - println!("Creating Order for {}.", product_pair); - match client - .order - .create_limit_gtc(product_pair, side, &total_size, &price, true) - .await - { - Ok(summary) => println!("Order creation result: {:#?}", summary), + if create_new { + println!( + "Creating Order with Client ID: {}", + new_order.client_order_id + ); + match client.order.create(&new_order).await { + Ok(summary) => { + if let Some(success) = &summary.success_response { + created_order_id = Some(success.order_id.clone()); + } + println!("Order creation result: {:#?}", summary); + } Err(error) => println!("Unable to create order: {}", error), } } - if let Some(order_id) = edit_open_order_id { - println!("\n\nEditing order for {}.", order_id); - match client.order.edit(&order_id, total_size, edit_price).await { - Ok(result) => println!("{:#?}", result), - Err(error) => println!("Unable to edit order: {}", error), + if let Some(order_id) = &created_order_id { + if create_new && edit_created { + thread::sleep(Duration::seconds(1).to_std().unwrap()); + let edit_order = OrderEditRequest::new(order_id, 50.0, 0.006); + println!("\n\nEditing order for {}.", order_id); + match client.order.edit(&edit_order).await { + Ok(result) => println!("{:#?}", result), + Err(error) => println!("Unable to edit order: {}", error), + } + } + } + + if let Some(order_id) = &created_order_id { + if create_new && cancel_created { + println!("\n\nCancelling Order with ID: {}", order_id); + match client + .order + .cancel(&OrderCancelRequest::new(&[order_id.clone()])) + .await + { + Ok(summary) => println!("Order cancel result: {:#?}", summary), + Err(error) => println!("Unable to cancel order: {}", error), + } } } - if cancel_open_orders { - println!("\n\nCancelling all OPEN orders for {}.", product_pair); - match client.order.cancel_all(product_pair).await { + // Cancels all OPEN orders. + if cancel_all { + println!("\n\nCancelling all OPEN orders for {}.", product_id); + match client.order.cancel_all(product_id).await { Ok(result) => println!("{:#?}", result), Err(error) => println!("Unable to cancel orders: {}", error), } } - println!("\n\nGetting all orders for {}.", product_pair); - match client.order.get_all(product_pair, None).await { + println!("\n\nGetting all orders for {} (get_all).", product_id); + match client + .order + .get_all(product_id, &OrderListQuery::new()) + .await + { Ok(orders) => println!("Orders obtained: {:#?}", orders.len()), Err(error) => println!("Unable to obtain all orders: {}", error), } - // Get all SELLING orders. + // Get all BUYING orders. let mut order_id = "".to_string(); - let query = ListOrdersQuery { - product_id: Some(product_pair.to_string()), - order_side: Some(OrderSide::Sell), + let query = OrderListQuery { + product_ids: Some(vec![product_id.to_string()]), + order_side: Some(OrderSide::Buy), ..Default::default() }; - println!("\n\nObtaining Orders."); + println!("\n\nObtaining Orders (bulk)."); match client.order.get_bulk(&query).await { Ok(orders) => { println!("Orders obtained: {:#?}", orders.orders.len()); @@ -108,15 +152,19 @@ async fn main() { // Build list of orders to cancel. let mut order_ids: Vec = vec![]; for order in orders.orders { - if order.status == "OPEN" { + if order.status == OrderStatus::Open { order_ids.push(order.order_id); } } // Cancel the orders. - if cancel_open_orders && !order_ids.is_empty() { + if cancel_all && !order_ids.is_empty() { println!("\n\nCancelling open orders."); - match client.order.cancel(&order_ids).await { + match client + .order + .cancel(&OrderCancelRequest::new(&order_ids)) + .await + { Ok(summary) => println!("Order cancel result: {:#?}", summary), Err(error) => println!("Unable to cancel order: {}", error), } diff --git a/examples/payment_api.rs b/examples/payment_api.rs new file mode 100644 index 0000000..0b74eba --- /dev/null +++ b/examples/payment_api.rs @@ -0,0 +1,63 @@ +//! # Payment API Example, check out the Payment API for all functionality. +//! +//! Shows how to: +//! - Get all payment methods. +//! - Get a single payment method. + +use std::process::exit; + +use cbadv::config::{self, BaseConfig}; +use cbadv::RestClientBuilder; + +#[tokio::main] +async fn main() { + // Load the configuration file. + let config: BaseConfig = match config::load("config.toml") { + Ok(c) => c, + Err(err) => { + println!("Could not load configuration file."); + if config::exists("config.toml") { + println!("File exists, {}", err); + exit(1); + } + + // Create a new configuration file. + config::create_base_config("config.toml").unwrap(); + println!("Empty configuration file created, please update it."); + exit(1); + } + }; + + // Create a client to interact with the API. + let mut client = match RestClientBuilder::new().with_config(&config).build() { + Ok(c) => c, + Err(why) => { + eprintln!("!ERROR! {}", why); + exit(1) + } + }; + + let mut payment_method_id = None; + + // Get payment methods. + println!("Obtaining all payment methods."); + match client.payment.get_all().await { + Ok(methods) => { + println!("{:#?}", methods); + if let Some(method) = methods.first() { + payment_method_id = Some(method.id.clone()); + } + } + Err(error) => println!("Unable to get the Payment Methods: {}", error), + } + + // Obtain a single payment method. + if let Some(payment_method_id) = payment_method_id { + // Get a single payment method. + println!("\n\nObtaining a single payment method."); + match client.payment.get(&payment_method_id).await { + Ok(method) => println!("{:#?}", method), + Err(error) => println!("Unable to get the Payment Method: {}", error), + } + } +} diff --git a/examples/portfolio_api.rs b/examples/portfolio_api.rs new file mode 100644 index 0000000..10b90a0 --- /dev/null +++ b/examples/portfolio_api.rs @@ -0,0 +1,113 @@ +//! # Portfolio API Example, check out the Portfolio API for all functionality. +//! +//! Shows how to: +//! - Create a new portfolio. +//! - Edit an existing portfolio. +//! - Delete an existing portfolio. +//! - Obtain a list of portfolios. +//! - Obtain the breakdown of a portfolio. + +use std::process::exit; + +use cbadv::config::{self, BaseConfig}; +use cbadv::portfolio::{PortfolioBreakdownQuery, PortfolioListQuery, PortfolioModifyRequest}; +use cbadv::RestClientBuilder; + +#[tokio::main] +async fn main() { + // Set to None to not create. + let create_portfolio_name = None; + // let create_portfolio_name = Some("New Portfolio"); + + // Set to None to not edit. + let edit_portfolio_uuid = None; + // let edit_portfolio_uuid = Some("AAAAAAAA-BBBB-CCCC-DDDDDDDDDDDD"); + let edit_portfolio_name = "DeleteMe"; + + // Set to None to not delete. + let delete_portfolio_uuid = None; + // let delete_portfolio_uuid = Some("AAAAAAAA-BBBB-CCCC-DDDDDDDDDDDD"); + + // Load the configuration file. + let config: BaseConfig = match config::load("config.toml") { + Ok(c) => c, + Err(err) => { + println!("Could not load configuration file."); + if config::exists("config.toml") { + println!("File exists, {}", err); + exit(1); + } + + // Create a new configuration file. + config::create_base_config("config.toml").unwrap(); + println!("Empty configuration file created, please update it."); + exit(1); + } + }; + + // Create a client to interact with the API. + let mut client = match RestClientBuilder::new().with_config(&config).build() { + Ok(c) => c, + Err(why) => { + eprintln!("!ERROR! {}", why); + exit(1) + } + }; + + // Create a new portfolio. + if let Some(name) = create_portfolio_name { + println!("Creating Portfolio."); + match client.portfolio.create(name).await { + Ok(portfolio) => println!("{:#?}", portfolio), + Err(error) => println!("Unable to create the portfolio: {}", error), + } + } + + // Edit an existing portfolio. + if let Some(uuid) = edit_portfolio_uuid { + println!("Editing Portfolio."); + let request = PortfolioModifyRequest::new(edit_portfolio_name); + match client.portfolio.edit(uuid, &request).await { + Ok(portfolio) => println!("{:#?}", portfolio), + Err(error) => println!("Unable to edit the portfolio: {}", error), + } + } + + // Delete an existing portfolio. + if let Some(uuid) = delete_portfolio_uuid { + println!("Deleting Portfolio."); + match client.portfolio.delete(uuid).await { + Ok(_) => println!("Portfolio deleted!"), + Err(error) => println!("Unable to delete the portfolio: {}", error), + } + } + + // Parameters to send to the API. + let query = PortfolioListQuery::new(); + + // Get listed portfolios.. + println!("Obtaining Portfolios"); + let breakdown_uuid = match client.portfolio.get_all(&query).await { + Ok(portfolios) => { + println!("{:#?}", portfolios); + Some(portfolios.first().unwrap().uuid.clone()) + } + Err(error) => { + println!("Unable to get the portfolios: {}", error); + None + } + }; + + // Get the breakdown for the first portfolio. + if let Some(uuid) = breakdown_uuid { + println!("Obtaining Portfolio Breakdown for {}.", uuid); + match client + .portfolio + .get(&uuid, &PortfolioBreakdownQuery::new()) + .await + { + Ok(breakdown) => println!("{:#?}", breakdown), + Err(error) => println!("Unable to get the breakdown: {}", error), + } + } +} diff --git a/examples/product_api.rs b/examples/product_api.rs index 11fc1a1..e97cebc 100644 --- a/examples/product_api.rs +++ b/examples/product_api.rs @@ -10,8 +10,11 @@ use std::process::exit; use cbadv::config::{self, BaseConfig}; -use cbadv::product::{ListProductsQuery, TickerQuery}; -use cbadv::{time, RestClient}; +use cbadv::product::{ + ProductBidAskQuery, ProductCandleQuery, ProductListQuery, ProductTickerQuery, +}; +use cbadv::time::Granularity; +use cbadv::{time, RestClientBuilder}; #[tokio::main] async fn main() { @@ -35,7 +38,7 @@ async fn main() { }; // Create a client to interact with the API. - let mut client = match RestClient::from_config(&config) { + let mut client = match RestClientBuilder::new().with_config(&config).build() { Ok(c) => c, Err(why) => { eprintln!("!ERROR! {}", why); @@ -49,32 +52,21 @@ async fn main() { println!("{:#?}\n\n", product); println!("Getting best bids and asks."); - match client - .product - .best_bid_ask(vec!["BTC-USD".to_string()]) - .await - { + let query = ProductBidAskQuery::new().product_ids(&["BTC-USD".to_string()]); + match client.product.best_bid_ask(&query).await { Ok(bidasks) => println!("{:#?}", bidasks), Err(error) => println!("Unable to get best bids and asks: {}", error), } // NOTE: Commented out due to large amounts of output. // println!("\n\nGetting product book."); - // match client - // .product - // .product_book(product_pair.clone(), None) - // .await - // { + // match client.product.product_book(product_pair, None).await { // Ok(book) => println!("{:#?}", book), // Err(error) => println!("Unable to get product book: {}", error), // } println!("\n\nGetting multiple products."); - let query = ListProductsQuery { - // limit: Some(500), - // product_ids: Some(vec!["BTC-USD".to_string(), "ETH-USD".to_string()]), - ..Default::default() - }; + let query = ProductListQuery::new(); // Pull multiple products from the Product API. match client.product.get_bulk(&query).await { @@ -83,14 +75,16 @@ async fn main() { } // Pull candles. - println!("\n\nGetting candles for: {}.", product_pair); - let granularity = time::Granularity::OneDay; - let interval = time::Granularity::to_secs(&granularity) as u64; let end = time::now(); - let start = time::before(end, interval * 730); - let time_span = time::Span::new(start, end, &granularity); - println!("Intervals collecting: {}", time_span.count()); - match client.product.candles_ext(product_pair, &time_span).await { + let interval = Granularity::to_secs(&Granularity::OneDay) as u64; + println!("\n\nGetting candles for: {}.", product_pair); + let query = ProductCandleQuery::new( + time::before(end, interval * 365), + end, + time::Granularity::OneDay, + ); + + match client.product.candles_ext(product_pair, &query).await { Ok(candles) => { println!("Obtained {} candles.", candles.len()); match candles.first() { @@ -103,7 +97,7 @@ async fn main() { // Pull ticker. println!("\n\nGetting ticker for: {}.", product_pair); - let query = TickerQuery { limit: 200 }; + let query = ProductTickerQuery::new(200); match client.product.ticker(product_pair, &query).await { Ok(ticker) => { println!( diff --git a/examples/public_api.rs b/examples/public_api.rs new file mode 100644 index 0000000..4a6ac3d --- /dev/null +++ b/examples/public_api.rs @@ -0,0 +1,97 @@ +//! # Public API Example, check out the Public API for all functionality. +//! +//! Shows how to: +//! - Obtain the API Unix time. +//! - Obtain the Product Book for a product. +//! - Obtain multiple products. +//! - Obtain candles for a product. +//! - Obtain the ticker for a product. + +use std::process::exit; + +use cbadv::product::{ProductCandleQuery, ProductListQuery, ProductTickerQuery}; +use cbadv::time::Granularity; +use cbadv::{time, RestClientBuilder}; + +#[tokio::main] +async fn main() { + let product_pair: &str = "BTC-USD"; + + // Create a client to interact with the API. + let mut client = match RestClientBuilder::new().build() { + Ok(c) => c, + Err(why) => { + eprintln!("!ERROR! {}", why); + exit(1) + } + }; + + // Get API Unix time. + println!("Obtaining API Unix time"); + match client.public.time().await { + Ok(time) => println!("{:#?}", time), + Err(error) => println!("Unable to get the Unix time: {}", error), + } + + // NOTE: Commented out due to large amounts of output. + // Get the Product Book for BTC-USD. + // println!("\n\nObtain the Product Book for {product_pair}."); + // match client.public.product_book(product_pair, None).await { + // Ok(book) => println!("{:#?}", book), + // Err(error) => println!("Unable to get the Product Book: {}", error), + // } + + println!("\n\nGetting multiple products."); + let query = ProductListQuery { + // limit: Some(500), + // product_ids: Some(vec!["BTC-USD".to_string(), "ETH-USD".to_string()]), + // get_all_products: Some(true), + ..Default::default() + }; + + // Pull multiple products from the Product API. + match client.public.products(&query).await { + Ok(products) => println!("Obtained {:#?} products", products.len()), + Err(error) => println!("Unable to get products: {}", error), + } + + // Pull candles. + let end = time::now(); + let interval = Granularity::to_secs(&Granularity::OneDay) as u64; + println!("\n\nGetting candles for: {}.", product_pair); + let query = ProductCandleQuery::new( + time::before(end, interval * 365), + end, + time::Granularity::OneDay, + ); + + match client.public.candles_ext(product_pair, &query).await { + Ok(candles) => { + println!("Obtained {} candles.", candles.len()); + match candles.first() { + Some(candle) => println!("{:#?}", candle), + None => println!("Out of bounds, no candles obtained."), + } + } + Err(error) => println!("Unable to get candles: {}", error), + } + + // Pull ticker. + println!("\n\nGetting ticker for: {}.", product_pair); + let query = ProductTickerQuery::new(200); + match client.public.ticker(product_pair, &query).await { + Ok(ticker) => { + println!( + "best bid: {:#?}\nbest ask: {:#?}\ntrades: {:#?}", + ticker.best_bid, + ticker.best_ask, + ticker.trades.len() + ); + match ticker.trades.first() { + Some(trade) => println!("{:#?}", trade), + None => println!("Out of bounds, no trades available."), + } + } + Err(error) => println!("Unable to get ticker: {}", error), + } +} diff --git a/examples/sandbox_api.rs b/examples/sandbox_api.rs new file mode 100644 index 0000000..99f0650 --- /dev/null +++ b/examples/sandbox_api.rs @@ -0,0 +1,90 @@ +//! # Sandbox API Example +//! +//! Shows how to: +//! - Create an order. +//! - Edit an order. +//! - Cancel all OPEN orders. +//! - Obtain ALL orders. +//! - Obtain multiple orders. +//! - Obtain specific order by ID. + +use std::process::exit; + +use cbadv::config::{self, BaseConfig}; +use cbadv::order::{OrderCreateBuilder, OrderEditRequest, OrderSide, OrderType, TimeInForce}; +use cbadv::RestClientBuilder; + +#[tokio::main] +async fn main() { + let product_pair: &str = "BTC-USD"; + let total_size: f64 = 0.005; + let price: f64 = 100.00; + let side: OrderSide = OrderSide::Buy; + + // Load the configuration file. + let config: BaseConfig = match config::load("config.toml") { + Ok(c) => c, + Err(err) => { + println!("Could not load configuration file."); + if config::exists("config.toml") { + println!("File exists, {}", err); + exit(1); + } + + // Create a new configuration file. + config::create_base_config("config.toml").unwrap(); + println!("Empty configuration file created, please update it."); + exit(1); + } + }; + + // Create a client to interact with the API. + let mut client = match RestClientBuilder::new() + .with_config(&config) + .use_sandbox(true) + .build() + { + Ok(c) => c, + Err(why) => { + eprintln!("!ERROR! {}", why); + exit(1) + } + }; + + // Create an order request using the `OrderCreateBuilder`. + // This example creates a Limit Order that is Good-Til-Cancelled (GTC) and post-only. + let order = match OrderCreateBuilder::new(product_pair, &side) + .base_size(total_size) + .limit_price(price) + .post_only(true) + .order_type(OrderType::Limit) + .time_in_force(TimeInForce::GoodUntilCancelled) + .preview(true) + .build() + { + Ok(order) => order, + Err(error) => { + println!("Unable to build order: {}", error); + exit(1); + } + }; + + println!("Creating Order for {}.", product_pair); + match client.order.create(&order).await { + Ok(summary) => println!("Order creation result: {:#?}", summary), + Err(error) => println!("Unable to create order: {}", error), + } + + println!("\n\nPreviewing an order creation."); + match client.order.preview_create(&order).await { + Ok(summary) => println!("Order preview result: {:#?}", summary), + Err(error) => println!("Unable to preview order: {}", error), + } + + println!("\n\nPreviewing an order edit."); + let edit_preview = OrderEditRequest::new("order_id", 100.00, 0.005); + match client.order.preview_edit(&edit_preview).await { + Ok(summary) => println!("Order edit preview result: {:#?}", summary), + Err(error) => println!("Unable to preview order edit: {}", error), + } +} diff --git a/examples/watch_candles.rs b/examples/watch_candles.rs index db89cbe..470b709 100644 --- a/examples/watch_candles.rs +++ b/examples/watch_candles.rs @@ -8,10 +8,9 @@ use std::process::exit; -use cbadv::config::{self, BaseConfig}; -use cbadv::product::{Candle, ListProductsQuery}; +use cbadv::product::{Candle, ProductListQuery}; use cbadv::traits::CandleCallback; -use cbadv::{RestClient, WebSocketClient}; +use cbadv::{RestClient, RestClientBuilder, WebSocketClientBuilder}; /// Example of user-defined struct to pass to the candle watcher. pub struct UserStruct { @@ -30,7 +29,7 @@ impl CandleCallback for UserStruct { // Processed | Product_Id | Candle Start | Current println!( - "{:<5} {:>11} ({}): finished candle {}", + "{:<5} {:>14} ({}): finished candle {}", self.processed, product_id, candle.start, is_same ); } @@ -38,22 +37,20 @@ impl CandleCallback for UserStruct { /// Obtain product names of candles to be obtained. async fn get_products(client: &mut RestClient) -> Vec { - println!("Getting '*-USD' products."); - let query = ListProductsQuery { - ..Default::default() - }; + println!("Getting '*-USDC' products."); // Holds all of the product names. let mut product_names: Vec = vec![]; + let query = ProductListQuery::new(); // Pull multiple products from the Product API. - match client.product.get_bulk(&query).await { + match client.public.products(&query).await { Ok(products) => { product_names = products .iter() - // Filter products to only containing *-USD pairs. + // Filter products to only containing *-USDC pairs. .filter_map(|p| match p.quote_currency_id.as_str() { - "USD" => Some(p.product_id.clone()), + "USDC" => Some(p.product_id.clone()), _ => None, }) .collect(); @@ -66,25 +63,8 @@ async fn get_products(client: &mut RestClient) -> Vec { #[tokio::main] async fn main() -> Result<(), Box> { - // Load the configuration file. - let config: BaseConfig = match config::load("config.toml") { - Ok(c) => c, - Err(err) => { - println!("Could not load configuration file."); - if config::exists("config.toml") { - println!("File exists, {}", err); - exit(1); - } - - // Create a new configuration file with defaults. - config::create_base_config("config.toml").unwrap(); - println!("Empty configuration file created, please update it."); - exit(1); - } - }; - // Create a client to interact with the API. - let mut rclient = match RestClient::from_config(&config) { + let mut rclient = match RestClientBuilder::new().build() { Ok(c) => c, Err(why) => { eprintln!("!ERROR! {}", why); @@ -93,7 +73,11 @@ async fn main() -> Result<(), Box> { }; // Create a client to interact with the API. - let mut wsclient = match WebSocketClient::from_config(&config) { + let wsclient = match WebSocketClientBuilder::new() + .auto_reconnect(true) + .max_retries(20) + .build() + { Ok(c) => c, Err(why) => { eprintln!("!ERROR! {}", why); diff --git a/examples/websocket.rs b/examples/websocket.rs index 5f4d588..c59824f 100644 --- a/examples/websocket.rs +++ b/examples/websocket.rs @@ -11,8 +11,8 @@ use std::process::exit; use cbadv::config::{self, BaseConfig}; use cbadv::traits::MessageCallback; use cbadv::types::CbResult; -use cbadv::ws::{Channel, Message}; -use cbadv::WebSocketClient; +use cbadv::ws::{Channel, EndpointType, Message}; +use cbadv::WebSocketClientBuilder; /// Example of an object with an attached callback function for messages. struct CallbackObject { @@ -25,21 +25,11 @@ impl MessageCallback for CallbackObject { /// the stream. fn message_callback(&mut self, msg: CbResult) { let rcvd = match msg { - Ok(value) => match value { - Message::Status(v) => format!("{:?}", v), - Message::Candles(v) => format!("{:?}", v), - Message::Ticker(v) => format!("{:?}", v), - Message::TickerBatch(v) => format!("{:?}", v), - Message::Level2(v) => format!("{:?}", v), - Message::User(v) => format!("{:?}", v), - Message::MarketTrades(v) => format!("{:?}", v), - Message::Heartbeats(v) => format!("{:?}", v), - Message::Subscribe(v) => format!("{:?}", v), - }, - Err(error) => format!("{}", error), + Ok(message) => format!("{:?}", message), // Leverage Debug for all Message variants + Err(error) => format!("Error: {}", error), // Handle WebSocket errors }; - // Using the callback objects properties. + // Update the callback object's properties and log the message. self.total_processed += 1; println!("{:<5}> {}\n", self.total_processed, rcvd); } @@ -64,37 +54,51 @@ async fn main() { } }; - // Create a client to interact with the API. - let mut client = match WebSocketClient::from_config(&config) { - Ok(c) => c, - Err(why) => { - eprintln!("!ERROR! {}", why); - exit(1) - } - }; + let mut client = WebSocketClientBuilder::new() + .with_config(&config) + .auto_reconnect(true) + .max_retries(20) + .build() + .map_err(|e| { + eprintln!("!ERROR! {}", e); + exit(1); + }) + .unwrap(); - // Callback Object - let cb_obj: CallbackObject = CallbackObject { total_processed: 0 }; + // Callback Object. + let cb_obj = CallbackObject { total_processed: 0 }; // Connect to the websocket, a subscription needs to be sent within 5 seconds. // If a subscription is not sent, Coinbase will close the connection. - let reader = client.connect().await.unwrap(); - let listener = tokio::spawn(WebSocketClient::listener_with(reader, cb_obj)); + let mut readers = client + .connect() + .await + .expect("Could not connect to WebSocket"); + + let public = readers + .take_endpoint(&EndpointType::Public) + .expect("Could not get public reader"); + + let listened_client = client.clone(); + let listener = tokio::spawn(async move { + let mut listened_client = listened_client; + listened_client.listen_trait(public, cb_obj).await; + }); // Products of interest. let products = vec!["BTC-USD".to_string(), "ETH-USD".to_string()]; // Heartbeats is a great way to keep a connection alive and not timeout. - client.sub(Channel::Heartbeats, &[]).await.unwrap(); + client.sub(&Channel::Heartbeats, &[]).await.unwrap(); // Subscribe to user orders. - client.sub(Channel::User, &products).await.unwrap(); + client.sub(&Channel::User, &products).await.unwrap(); // Get updates (subscribe) on products and currencies. - client.sub(Channel::Candles, &products).await.unwrap(); + client.sub(&Channel::Candles, &products).await.unwrap(); // Stop obtaining (unsubscribe) updates on products and currencies. - client.unsub(Channel::Status, &products).await.unwrap(); + client.unsub(&Channel::Status, &products).await.unwrap(); // Passes the parser callback and listens for messages. listener.await.unwrap(); diff --git a/examples/websocket_user.rs b/examples/websocket_user.rs new file mode 100644 index 0000000..ae9faae --- /dev/null +++ b/examples/websocket_user.rs @@ -0,0 +1,96 @@ +//! # WebSocket User API Example, check out the WebSocket API for all functionality. +//! +//! Shows how to: +//! - Connect WebSocket Client. +//! - Setup Listener and parse messages. +//! - Subscribe to channels. +//! - Unsubscribe to channels. + +use std::process::exit; + +use cbadv::config::{self, BaseConfig}; +use cbadv::traits::MessageCallback; +use cbadv::types::CbResult; +use cbadv::ws::{Channel, EndpointType, Message}; +use cbadv::WebSocketClientBuilder; + +/// Example of an object with an attached callback function for messages. +struct CallbackObject { + /// Total amount of messages processed. + total_processed: usize, +} + +impl MessageCallback for CallbackObject { + /// This is used to parse messages. It is passed to the `listen` function to pull Messages out of + /// the stream. + fn message_callback(&mut self, msg: CbResult) { + let rcvd = match msg { + Ok(message) => format!("{:?}", message), // Leverage Debug for all Message variants + Err(error) => format!("Error: {}", error), // Handle WebSocket errors + }; + + // Update the callback object's properties and log the message. + self.total_processed += 1; + println!("{:<5}> {}\n", self.total_processed, rcvd); + } +} + +#[tokio::main] +async fn main() { + // Load the configuration file. + let config: BaseConfig = match config::load("config.toml") { + Ok(c) => c, + Err(err) => { + println!("Could not load configuration file."); + if config::exists("config.toml") { + println!("File exists, {}", err); + exit(1); + } + + // Create a new configuration file. + config::create_base_config("config.toml").unwrap(); + println!("Empty configuration file created, please update it."); + exit(1); + } + }; + + let mut client = WebSocketClientBuilder::new() + .with_config(&config) + .auto_reconnect(true) + .max_retries(20) + .build() + .map_err(|e| { + eprintln!("!ERROR! {}", e); + exit(1); + }) + .unwrap(); + + // Callback Object. + let cb_obj = CallbackObject { total_processed: 0 }; + + // Connect to the websocket, a subscription needs to be sent within 5 seconds. + // If a subscription is not sent, Coinbase will close the connection. + let mut readers = client + .connect() + .await + .expect("Could not connect to WebSocket."); + + let user = readers + .take_endpoint(&EndpointType::User) + .expect("Could not get secure user reader."); + + let listened_client = client.clone(); + let listener = tokio::spawn(async move { + let mut listened_client = listened_client; + listened_client.listen_trait(user, cb_obj).await; + }); + + // Heartbeats is a great way to keep a connection alive and not timeout. + client.sub(&Channel::Heartbeats, &[]).await.unwrap(); + + // Subscribe to user orders. + client.sub(&Channel::User, &[]).await.unwrap(); + + // Passes the parser callback and listens for messages. + listener.await.unwrap(); +} diff --git a/src/apis/account.rs b/src/apis/account.rs index 9ab431b..fcc224e 100644 --- a/src/apis/account.rs +++ b/src/apis/account.rs @@ -3,19 +3,17 @@ //! `account` gives access to the Account API and the various endpoints associated with it. //! This allows you to obtain account information either by account UUID or in bulk (all accounts). -use async_recursion::async_recursion; - -use crate::account::{Account, AccountResponse, ListAccountsQuery, ListedAccounts}; -use crate::constants::accounts::RESOURCE_ENDPOINT; -use crate::errors::CbAdvError; -use crate::signer::Signer; -use crate::traits::NoQuery; +use crate::account::{Account, AccountListQuery, AccountWrapper, PaginatedAccounts}; +use crate::constants::accounts::{LIST_ACCOUNT_MAXIMUM, RESOURCE_ENDPOINT}; +use crate::errors::CbError; +use crate::http_agent::SecureHttpAgent; +use crate::traits::{HttpAgent, NoQuery}; use crate::types::CbResult; /// Provides access to the Account API for the service. pub struct AccountApi { /// Object used to sign requests made to the API. - signer: Signer, + agent: Option, } impl AccountApi { @@ -23,10 +21,9 @@ impl AccountApi { /// /// # Arguments /// - /// * `signer` - A Signer that include the API Key & Secret along with a client to make - /// requests. - pub(crate) fn new(signer: Signer) -> Self { - Self { signer } + /// * `signer` - A Signer that include the API Key & Secret along with a client to make requests. + pub(crate) fn new(agent: Option) -> Self { + Self { agent } } /// Obtains a single account based on the Account UUID (ex. "XXXX-YYYY-ZZZZ"). This is the most @@ -43,18 +40,18 @@ impl AccountApi { /// /// pub async fn get(&mut self, account_uuid: &str) -> CbResult { + let agent = get_auth!(self.agent, "get account"); let resource = format!("{}/{}", RESOURCE_ENDPOINT, account_uuid); - match self.signer.get(&resource, &NoQuery).await { - Ok(value) => match value.json::().await { - Ok(resp) => Ok(resp.account), - Err(_) => Err(CbAdvError::BadParse("account object".to_string())), - }, - Err(error) => Err(error), - } + let response = agent.get(&resource, &NoQuery).await?; + let data: AccountWrapper = response + .json() + .await + .map_err(|e| CbError::JsonError(e.to_string()))?; + Ok(data.into()) } /// Obtains a single account based on the Account ID (ex. "BTC"). - /// This wraps `get_bulk` and recursively makes several additional requests until either the + /// This wraps `get_bulk` and iteratively makes several additional requests until either the /// account is found or there are not more accounts. This is a more expensive call, but more /// convient than `get` which requires knowing the UUID already. /// @@ -64,69 +61,67 @@ impl AccountApi { /// # Arguments /// /// * `id` - Identifier for the account, such as BTC or ETH. - /// * `query` - Optional parameters, should default to None unless you want additional control. - #[async_recursion] - pub async fn get_by_id( - &mut self, - id: &str, - query: Option, - ) -> CbResult { - let mut query = match query { - Some(p) => p, - None => ListAccountsQuery::default(), - }; - - match self.get_bulk(&query).await { - Ok(mut listed) => { - // Find the index. - match listed.accounts.iter().position(|r| r.currency == id) { - Some(index) => Ok(listed.accounts.swap_remove(index)), - None => { - // Prevent further requests if no more can be made. - if !listed.has_next { - return Err(CbAdvError::NotFound("no matching ids".to_string())); - } - - // Make another request to the API for the account. - query.cursor = Some(listed.cursor); - self.get_by_id(id, Some(query)).await - } - } + /// * `query` - Parameters to control the query, such as limit. + pub async fn get_by_id(&mut self, id: &str, query: &AccountListQuery) -> CbResult { + is_auth!(self.agent, "get account by ID"); + + let mut query = query.clone().limit(LIST_ACCOUNT_MAXIMUM); + + loop { + // Fetch accounts with the current query, propagating any errors. + let mut listed = self.get_bulk(&query).await?; + + // Check if the desired account is in the current batch. + if let Some(index) = listed.accounts.iter().position(|r| r.currency == id) { + return Ok(listed.accounts.swap_remove(index)); + } + + // If no more pages to fetch, return a "not found" error with context. + if !listed.has_next { + return Err(CbError::NotFound(format!( + "No account found with ID '{}'.", + id + ))); } - Err(error) => Err(error), + + // Update the cursor for the next API call. + query.cursor = Some(listed.cursor); } } /// Obtains all accounts available to the API Key. Use a larger limit in the query to decrease - /// the amount of API calls. Recursively makes calls to obtain all accounts. + /// the amount of API calls. Iteratively makes calls to obtain all accounts. /// /// NOTE: NOT A STANDARD API FUNCTION. QoL function that may require additional API requests than /// normal. /// /// # Arguments /// - /// * `query` - Optional parameters, should default to None unless you want additional control. - #[async_recursion] - pub async fn get_all(&mut self, query: Option) -> CbResult> { - let mut query = match query { - Some(p) => p, - None => ListAccountsQuery::default(), - }; - - // Obtain until there are not anymore accounts. - match self.get_bulk(&query).await { - Ok(mut listed) => { - if listed.has_next { - query.cursor = Some(listed.cursor); - match self.get_all(Some(query)).await { - Ok(mut accounts) => listed.accounts.append(&mut accounts), - Err(error) => return Err(error), - } - } - Ok(listed.accounts) + /// * `query` - Parameters to control the query, such as limit. + pub async fn get_all(&mut self, query: &AccountListQuery) -> CbResult> { + is_auth!(self.agent, "get all accounts"); + + let mut query = query.clone().limit(LIST_ACCOUNT_MAXIMUM); + let mut all_accounts = Vec::new(); + + loop { + // Fetch accounts with the current query, propagating any errors. + let mut listed = self.get_bulk(&query).await?; + + // Append fetched accounts to the result list. + all_accounts.append(&mut listed.accounts); + + // Check if there's more data to fetch. + if listed.has_next { + // Update the cursor for the next request. + query.cursor = Some(listed.cursor); + } else { + // No more data to fetch. + break; } - Err(error) => return Err(error), } + + Ok(all_accounts) } /// Obtains various accounts from the API. @@ -137,13 +132,13 @@ impl AccountApi { /// https://api.coinbase.com/api/v3/brokerage/accounts /// /// - pub async fn get_bulk(&mut self, query: &ListAccountsQuery) -> CbResult { - match self.signer.get(RESOURCE_ENDPOINT, query).await { - Ok(value) => match value.json::().await { - Ok(resp) => Ok(resp), - Err(_) => Err(CbAdvError::BadParse("accounts vector".to_string())), - }, - Err(error) => Err(error), - } + pub async fn get_bulk(&mut self, query: &AccountListQuery) -> CbResult { + let agent = get_auth!(self.agent, "get bulk accounts"); + let response = agent.get(RESOURCE_ENDPOINT, query).await?; + let data: PaginatedAccounts = response + .json() + .await + .map_err(|e| CbError::JsonError(e.to_string()))?; + Ok(data) } } diff --git a/src/apis/convert.rs b/src/apis/convert.rs index b119d4c..ee79c4e 100644 --- a/src/apis/convert.rs +++ b/src/apis/convert.rs @@ -3,19 +3,17 @@ //! `convert` gives access to the Convert API and the various endpoints associated with it. //! This allows for the conversion between two currencies. -use crate::constants::convert::{QUOTE_ENDPOINT, RESOURCE_ENDPOINT}; -use crate::convert::{ - ConvertQuery, ConvertQuoteQuery, ConvertResponse, Trade, TradeIncentiveMetadata, -}; -use crate::errors::CbAdvError; -use crate::signer::Signer; -use crate::traits::NoQuery; +use crate::constants::convert::{QUOTE_ENDPOINT, TRADE_ENDPOINT}; +use crate::convert::{ConvertQuery, ConvertQuoteRequest, Trade, TradeWrapper}; +use crate::errors::CbError; +use crate::http_agent::SecureHttpAgent; +use crate::traits::{HttpAgent, NoQuery}; use crate::types::CbResult; /// Provides access to the Convert API for the service. pub struct ConvertApi { /// Object used to sign requests made to the API. - signer: Signer, + agent: Option, } impl ConvertApi { @@ -23,10 +21,9 @@ impl ConvertApi { /// /// # Arguments /// - /// * `signer` - A Signer that include the API Key & Secret along with a client to make - /// requests. - pub(crate) fn new(signer: Signer) -> Self { - Self { signer } + /// * `agent` - A agent that include the API Key & Secret along with a client to make requests. + pub(crate) fn new(agent: Option) -> Self { + Self { agent } } /// Create a convert quote with a specified source currency, target currency, and amount. @@ -36,81 +33,71 @@ impl ConvertApi { /// /// Trades are valid for 10 minutes after the quote is created. /// - /// https://docs.cloud.coinbase.com/advanced-trade-api/reference/retailbrokerageapi_createconvertquote - pub async fn create_quote( - &mut self, - from_account: &str, - to_account: &str, - amount: f64, - metadata: Option, - ) -> CbResult { - let query = ConvertQuoteQuery { - from_account: from_account.to_string(), - to_account: to_account.to_string(), - amount: amount.to_string(), - trade_incentive_metadata: metadata, - }; - - match self.signer.post(QUOTE_ENDPOINT, &NoQuery, &query).await { - Ok(value) => match value.json::().await { - Ok(resp) => Ok(resp.trade), - Err(_) => Err(CbAdvError::BadParse( - "convert quote response object".to_string(), - )), - }, - Err(error) => Err(error), - } + /// # Arguments + /// + /// * `request` - The request to create a quote. + /// + /// # Endpoint / Reference + /// + #[allow(rustdoc::bare_urls)] + /// https://api.coinbase.com/api/v3/brokerage/convert/quote + /// + /// + pub async fn create_quote(&mut self, request: &ConvertQuoteRequest) -> CbResult { + let agent = get_auth!(self.agent, "create convert quote"); + let response = agent.post(QUOTE_ENDPOINT, &NoQuery, request).await?; + let data = response + .json::() + .await + .map_err(|e| CbError::JsonError(e.to_string()))?; + Ok(data.into()) } /// Gets a list of information about a convert trade with a specified trade ID, source currency, and target currency. /// - /// https://docs.cloud.coinbase.com/advanced-trade-api/reference/retailbrokerageapi_getconverttrade - pub async fn get( - &mut self, - trade_id: &str, - from_account: &str, - to_account: &str, - ) -> CbResult { - let resource = format!("{}/trade/{}", RESOURCE_ENDPOINT, trade_id); - let query = ConvertQuery { - from_account: from_account.to_string(), - to_account: to_account.to_string(), - }; - - match self.signer.get(&resource, &query).await { - Ok(value) => match value.json::().await { - Ok(resp) => Ok(resp.trade), - Err(_) => Err(CbAdvError::BadParse( - "get convert response object".to_string(), - )), - }, - Err(error) => Err(error), - } + /// # Arguments + /// + /// * `trade_id` - The trade ID to get information about. + /// * `query` - The query to obtain the trade. + /// + /// # Endpoint / Reference + /// + #[allow(rustdoc::bare_urls)] + /// https://api.coinbase.com/api/v3/brokerage/convert/trade + /// + /// + pub async fn get(&mut self, trade_id: &str, query: &ConvertQuery) -> CbResult { + let agent = get_auth!(self.agent, "get convert trade"); + let resource = format!("{}/{}", TRADE_ENDPOINT, trade_id); + let response = agent.get(&resource, query).await?; + let data: TradeWrapper = response + .json() + .await + .map_err(|e| CbError::JsonError(e.to_string()))?; + Ok(data.into()) } /// Commits a convert trade with a specified trade ID, source currency, and target currency. /// - /// https://docs.cloud.coinbase.com/advanced-trade-api/reference/retailbrokerageapi_commitconverttrade - pub async fn commit( - &mut self, - trade_id: &str, - from_account: &str, - to_account: &str, - ) -> CbResult { - let resource = format!("{}/trade/{}", RESOURCE_ENDPOINT, trade_id); - let query = ConvertQuery { - from_account: from_account.to_string(), - to_account: to_account.to_string(), - }; - - match self.signer.post(&resource, &NoQuery, &query).await { - Ok(value) => match value.json::().await { - Ok(resp) => Ok(resp.trade), - Err(_) => Err(CbAdvError::BadParse( - "convert commit response object".to_string(), - )), - }, - Err(error) => Err(error), - } + /// # Arguments + /// + /// * `trade_id` - The trade ID to get information about. + /// * `query` - The query to commit the trade. + /// + /// # Endpoint / Reference + /// + #[allow(rustdoc::bare_urls)] + /// https://api.coinbase.com/api/v3/brokerage/convert/trade + /// + /// + pub async fn commit(&mut self, trade_id: &str, query: &ConvertQuery) -> CbResult { + let agent = get_auth!(self.agent, "commit convert quote"); + let resource = format!("{}/{}", TRADE_ENDPOINT, trade_id); + let response = agent.post(&resource, &NoQuery, query).await?; + let data: TradeWrapper = response + .json() + .await + .map_err(|e| CbError::JsonError(e.to_string()))?; + Ok(data.into()) } } diff --git a/src/apis/data.rs b/src/apis/data.rs new file mode 100644 index 0000000..94549f2 --- /dev/null +++ b/src/apis/data.rs @@ -0,0 +1,45 @@ +//! # Coinbase Advanced Data API +//! +//! `data` gives access to the Data API and the various endpoints associated with it. + +use crate::constants::data::KEY_PERMISSIONS_ENDPOINT; +use crate::errors::CbError; +use crate::http_agent::SecureHttpAgent; +use crate::models::data::KeyPermissions; +use crate::traits::{HttpAgent, NoQuery}; +use crate::types::CbResult; + +/// Provides access to the Data API for the service. +pub struct DataApi { + /// Object used to sign requests made to the API. + agent: Option, +} + +impl DataApi { + /// Creates a new instance of the Data API. This grants access to various data information. + /// + /// # Arguments + /// + /// * `agent` - A agent that include the API Key & Secret along with a client to make requests. + pub(crate) fn new(agent: Option) -> Self { + Self { agent } + } + + /// Get information about your CDP API key permissions. + /// + /// # Endpoint / Reference + /// + #[allow(rustdoc::bare_urls)] + /// https://api.coinbase.com/api/v3/brokerage/key_permissions + /// + /// + pub async fn key_permissions(&mut self) -> CbResult { + let agent = get_auth!(self.agent, "get key permissions"); + let response = agent.get(KEY_PERMISSIONS_ENDPOINT, &NoQuery).await?; + let data: KeyPermissions = response + .json() + .await + .map_err(|e| CbError::JsonError(e.to_string()))?; + Ok(data) + } +} diff --git a/src/apis/fee.rs b/src/apis/fee.rs index 7dcccb9..6957deb 100644 --- a/src/apis/fee.rs +++ b/src/apis/fee.rs @@ -4,15 +4,16 @@ //! Currently the only endpoint available is the Transaction Summary endpoint. use crate::constants::fees::RESOURCE_ENDPOINT; -use crate::errors::CbAdvError; -use crate::fee::{TransactionSummary, TransactionSummaryQuery}; -use crate::signer::Signer; +use crate::errors::CbError; +use crate::fee::{FeeTransactionSummaryQuery, TransactionSummary}; +use crate::http_agent::SecureHttpAgent; +use crate::traits::HttpAgent; use crate::types::CbResult; /// Provides access to the Fee API for the service. pub struct FeeApi { /// Object used to sign requests made to the API. - signer: Signer, + agent: Option, } impl FeeApi { @@ -20,18 +21,16 @@ impl FeeApi { /// /// # Arguments /// - /// * `signer` - A Signer that include the API Key & Secret along with a client to make - /// requests. - pub(crate) fn new(signer: Signer) -> Self { - Self { signer } + /// * `agent` - A agent that include the API Key & Secret along with a client to make requests. + pub(crate) fn new(agent: Option) -> Self { + Self { agent } } /// Obtains fee transaction summary from the API. /// /// # Arguments /// - /// * `query` - Optional paramaters used to modify the resulting scope of the - /// summary. + /// * `query` - Paramaters used to modify the resulting scope of the summary. /// /// # Endpoint / Reference /// @@ -39,13 +38,16 @@ impl FeeApi { /// https://api.coinbase.com/api/v3/brokerage/transaction_summary /// /// - pub async fn get(&mut self, query: &TransactionSummaryQuery) -> CbResult { - match self.signer.get(RESOURCE_ENDPOINT, query).await { - Ok(value) => match value.json::().await { - Ok(resp) => Ok(resp), - Err(_) => Err(CbAdvError::BadParse("fee summary object".to_string())), - }, - Err(error) => Err(error), - } + pub async fn get( + &mut self, + query: &FeeTransactionSummaryQuery, + ) -> CbResult { + let agent = get_auth!(self.agent, "get fee transaction summary"); + let response = agent.get(RESOURCE_ENDPOINT, query).await?; + let data: TransactionSummary = response + .json() + .await + .map_err(|e| CbError::JsonError(e.to_string()))?; + Ok(data) } } diff --git a/src/apis/mod.rs b/src/apis/mod.rs index bfac1b1..d8b2b5a 100644 --- a/src/apis/mod.rs +++ b/src/apis/mod.rs @@ -1,13 +1,19 @@ mod account; mod convert; +mod data; mod fee; mod order; +mod payment; +mod portfolio; mod product; -mod util; +mod public; pub(crate) use account::AccountApi; pub(crate) use convert::ConvertApi; +pub(crate) use data::DataApi; pub(crate) use fee::FeeApi; pub(crate) use order::OrderApi; +pub(crate) use payment::PaymentApi; +pub(crate) use portfolio::PortfolioApi; pub(crate) use product::ProductApi; -pub(crate) use util::UtilApi; +pub(crate) use public::PublicApi; diff --git a/src/apis/order.rs b/src/apis/order.rs index eef4346..03ea3ab 100644 --- a/src/apis/order.rs +++ b/src/apis/order.rs @@ -3,27 +3,25 @@ //! `order` gives access to the Order API and the various endpoints associated with it. //! These allow you to obtain past created orders, create new orders, and cancel orders. -use uuid::Uuid; - use crate::constants::orders::{ - BATCH_ENDPOINT, CANCEL_BATCH_ENDPOINT, EDIT_ENDPOINT, EDIT_PREVIEW_ENDPOINT, FILLS_ENDPOINT, - RESOURCE_ENDPOINT, + BATCH_ENDPOINT, CANCEL_BATCH_ENDPOINT, CLOSE_POSITION_ENDPOINT, CREATE_PREVIEW_ENDPOINT, + EDIT_ENDPOINT, EDIT_PREVIEW_ENDPOINT, FILLS_ENDPOINT, RESOURCE_ENDPOINT, }; -use crate::errors::CbAdvError; +use crate::errors::CbError; +use crate::http_agent::SecureHttpAgent; use crate::order::{ - CancelOrders, CancelOrdersResponse, CreateOrder, EditOrder, EditOrderResponse, LimitGtc, - LimitGtd, ListFillsQuery, ListOrdersQuery, ListedFills, ListedOrders, MarketIoc, Order, - OrderConfiguration, OrderResponse, OrderStatus, OrderStatusResponse, PreviewEditOrderResponse, - StopLimitGtc, StopLimitGtd, + Order, OrderCancelRequest, OrderCancelResponse, OrderCancelWrapper, OrderClosePositionRequest, + OrderCreatePreview, OrderCreateRequest, OrderCreateResponse, OrderEditPreview, + OrderEditRequest, OrderEditResponse, OrderListFillsQuery, OrderListQuery, OrderStatus, + OrderWrapper, PaginatedFills, PaginatedOrders, }; -use crate::signer::Signer; -use crate::traits::NoQuery; +use crate::traits::{HttpAgent, NoQuery}; use crate::types::CbResult; /// Provides access to the Order API for the service. pub struct OrderApi { /// Object used to sign requests made to the API. - signer: Signer, + agent: Option, } impl OrderApi { @@ -31,17 +29,16 @@ impl OrderApi { /// /// # Arguments /// - /// * `signer` - A Signer that include the API Key & Secret along with a client to make - /// requests. - pub(crate) fn new(signer: Signer) -> Self { - Self { signer } + /// * `agent` - A agent that include the API Key & Secret along with a client to make requests. + pub(crate) fn new(agent: Option) -> Self { + Self { agent } } /// Cancel orders. /// /// # Arguments /// - /// * `order_ids` - A vector of strings that represents order IDs to cancel. + /// * `request` - A struct containing what orders to cancel. /// /// # Endpoint / Reference /// @@ -49,22 +46,17 @@ impl OrderApi { /// https://api.coinbase.com/api/v3/brokerage/orders/batch_cancel /// /// - pub async fn cancel(&mut self, order_ids: &[String]) -> CbResult> { - let body = CancelOrders { - order_ids: order_ids.to_vec(), - }; - - match self - .signer - .post(CANCEL_BATCH_ENDPOINT, &NoQuery, body) + pub async fn cancel( + &mut self, + request: &OrderCancelRequest, + ) -> CbResult> { + let agent = get_auth!(self.agent, "cancel orders"); + let response = agent.post(CANCEL_BATCH_ENDPOINT, &NoQuery, request).await?; + let data: OrderCancelWrapper = response + .json() .await - { - Ok(value) => match value.json::().await { - Ok(resp) => Ok(resp.results), - Err(_) => Err(CbAdvError::BadParse("cancel order object".to_string())), - }, - Err(error) => Err(error), - } + .map_err(|e| CbError::JsonError(e.to_string()))?; + Ok(data.into()) } /// Cancel all OPEN orders for a specific product ID. @@ -75,31 +67,33 @@ impl OrderApi { /// # Arguments /// /// * `product_id` - Product to cancel all OPEN orders for. - pub async fn cancel_all(&mut self, product_id: &str) -> CbResult> { - let query = ListOrdersQuery { - product_id: Some(product_id.to_string()), + pub async fn cancel_all(&mut self, product_id: &str) -> CbResult> { + is_auth!(self.agent, "cancel all orders"); + + let query = OrderListQuery { + product_ids: Some(vec![product_id.to_string()]), order_status: Some(vec![OrderStatus::Open]), ..Default::default() }; - // Obtain all open orders. - match self.get_all(product_id, Some(query)).await { - Ok(orders) => { - // Build list of orders to cancel. - let order_ids: Vec = orders.iter().map(|o| o.order_id.clone()).collect(); + // Obtain all open orders for the given product. + let open_orders = self.get_all(product_id, &query).await?; - // Do nothing since no orders found. - if order_ids.is_empty() { - return Err(CbAdvError::NothingToDo( - "no orders found to cancel".to_string(), - )); - } + // Collect the IDs of orders to cancel. + let request = OrderCancelRequest::new( + &open_orders + .iter() + .map(|order| order.order_id.clone()) + .collect::>(), + ); - // Cancel the order list. - self.cancel(&order_ids).await - } - Err(error) => Err(error), + // No orders to cancel. + if request.order_ids.is_empty() { + return Ok(vec![]); } + + // Cancel the orders and return the response. + self.cancel(&request).await } /// Edit an order with a specified new size, or new price. Only limit order types, with time @@ -109,9 +103,7 @@ impl OrderApi { /// /// # Arguments /// - /// * `order_id` - ID of the order to edit. - /// * `size` - New size of the order. - /// * `price` - New price of the order. + /// * `request` - A struct containing the order ID, new size, and new price. /// /// # Endpoint / Reference /// @@ -119,278 +111,71 @@ impl OrderApi { /// https://api.coinbase.com/api/v3/brokerage/orders/edit /// /// https://docs.cloud.coinbase.com/advanced-trade-api/reference/retailbrokerageapi_editorder - pub async fn edit( - &mut self, - order_id: &str, - size: f64, - price: f64, - ) -> CbResult { - let body = EditOrder { - order_id: order_id.to_string(), - size: size.to_string(), - price: price.to_string(), - }; - - match self.signer.post(EDIT_ENDPOINT, &NoQuery, body).await { - Ok(value) => match value.json::().await { - Ok(edits) => Ok(edits), - Err(_) => Err(CbAdvError::BadParse( - "could not parse edit order object".to_string(), - )), - }, - Err(error) => Err(error), - } - } - - /// Simulate an edit order request with a specified new size, or new price, to preview the result of an edit. Only - /// limit order types, with time in force type of good-till-cancelled can be edited. - /// - /// # Arguments - /// - /// * `order_id` - ID of the order to edit. - /// * `size` - New size of the order. - /// * `price` - New price of the order. - /// - /// # Endpoint / Reference - /// - #[allow(rustdoc::bare_urls)] - /// https://api.coinbase.com/api/v3/brokerage/orders/edit_preivew - /// - /// https://docs.cloud.coinbase.com/advanced-trade-api/reference/retailbrokerageapi_previeweditorder - pub async fn preview_edit( - &mut self, - order_id: &str, - size: f64, - price: f64, - ) -> CbResult { - let body = EditOrder { - order_id: order_id.to_string(), - size: size.to_string(), - price: price.to_string(), - }; - - match self - .signer - .post(EDIT_PREVIEW_ENDPOINT, &NoQuery, body) + pub async fn edit(&mut self, request: &OrderEditRequest) -> CbResult { + let agent = get_auth!(self.agent, "edit order"); + let response = agent.post(EDIT_ENDPOINT, &NoQuery, request).await?; + let data: OrderEditResponse = response + .json() .await - { - Ok(value) => match value.json::().await { - Ok(response) => Ok(response), - Err(_) => Err(CbAdvError::BadParse( - "could not parse preview edit order response".to_string(), - )), - }, - Err(error) => Err(error), - } + .map_err(|e| CbError::JsonError(e.to_string()))?; + Ok(data) } - /// Create an order. + /// Preview creating an order. /// /// # Arguments /// - /// * `product_id` - A string that represents the product's ID. - /// * `side` - A string that represents the side: BUY or SELL - /// * `configuration` - A OrderConfiguration containing details on type of order. + /// * `request` - A struct containing the order details to preview. /// /// # Endpoint / Reference /// #[allow(rustdoc::bare_urls)] - /// https://api.coinbase.com/api/v3/brokerage/orders + /// https://api.coinbase.com/api/v3/brokerage/orders/preview /// - /// - async fn create( + /// + pub async fn preview_create( &mut self, - product_id: &str, - side: &str, - configuration: OrderConfiguration, - ) -> CbResult { - let body = CreateOrder { - client_order_id: Uuid::new_v4().to_string(), - product_id: product_id.to_string(), - side: side.to_string(), - order_configuration: configuration, - }; - - match self.signer.post(RESOURCE_ENDPOINT, &NoQuery, body).await { - Ok(value) => match value.json::().await { - Ok(resp) => Ok(resp), - Err(_) => Err(CbAdvError::BadParse("created order object".to_string())), - }, - Err(error) => Err(error), - } - } - - /// Create a market order. - /// - /// # Arguments - /// - /// * `product_id` - A string that represents the product's ID. - /// * `side` - A string that represents the side: BUY or SELL - /// * `size` - A 64-bit float that represents the size to buy or sell. - /// - /// # Endpoint / Reference - /// - #[allow(rustdoc::bare_urls)] - /// https://api.coinbase.com/api/v3/brokerage/orders - /// - /// - pub async fn create_market( - &mut self, - product_id: &str, - side: &str, - size: &f64, - ) -> CbResult { - let market = if side == "BUY" { - MarketIoc { - quote_size: Some(size.to_string()), - base_size: None, - } - } else { - MarketIoc { - quote_size: None, - base_size: Some(size.to_string()), - } - }; - - let config = OrderConfiguration { - market_market_ioc: Some(market), - ..Default::default() - }; - - self.create(product_id, side, config).await - } - - /// Create a Good til Cancelled Limit order. - /// - /// # Arguments - /// - /// * `product_id` - A string that represents the product's ID. - /// * `side` - A string that represents the side: BUY or SELL - /// * `size` - A 64-bit float that represents the size to buy or sell. - /// * `price` - A 64-bit float that represents the price to buy or sell. - /// * `post_only` - A boolean that represents MAKER or TAKER. - /// - /// # Endpoint / Reference - /// - #[allow(rustdoc::bare_urls)] - /// https://api.coinbase.com/api/v3/brokerage/orders - /// - /// - pub async fn create_limit_gtc( - &mut self, - product_id: &str, - side: &str, - size: &f64, - price: &f64, - post_only: bool, - ) -> CbResult { - let limit = LimitGtc { - base_size: size.to_string(), - limit_price: price.to_string(), - post_only, - }; - - let config = OrderConfiguration { - limit_limit_gtc: Some(limit), - ..Default::default() - }; - - self.create(product_id, side, config).await - } - - /// Create a Good til Time (Date) Limit order. - /// - /// # Arguments - /// - /// * `product_id` - A string that represents the product's ID. - /// * `side` - A string that represents the side: BUY or SELL - /// * `size` - A 64-bit float that represents the size to buy or sell. - /// * `price` - A 64-bit float that represents the price to buy or sell. - /// * `end_time` - A string that represents the time to kill the order. - /// * `post_only` - A boolean that represents MAKER or TAKER. - /// - /// # Endpoint / Reference - /// - #[allow(rustdoc::bare_urls)] - /// https://api.coinbase.com/api/v3/brokerage/orders - /// - /// - pub async fn create_limit_gtd( - &mut self, - product_id: &str, - side: &str, - size: &f64, - price: &f64, - end_time: &str, - post_only: bool, - ) -> CbResult { - let limit = LimitGtd { - base_size: size.to_string(), - limit_price: price.to_string(), - end_time: end_time.to_string(), - post_only, - }; - - let config = OrderConfiguration { - limit_limit_gtd: Some(limit), - ..Default::default() - }; - - self.create(product_id, side, config).await + request: &OrderCreateRequest, + ) -> CbResult { + let agent = get_auth!(self.agent, "preview create order"); + let response = agent + .post(CREATE_PREVIEW_ENDPOINT, &NoQuery, request) + .await?; + let data: OrderCreatePreview = response + .json() + .await + .map_err(|e| CbError::JsonError(e.to_string()))?; + Ok(data) } - /// Create a Good til Cancelled Stop Limit order. + /// Simulate an edit order request with a specified new size, or new price, to preview the result of an edit. Only + /// limit order types, with time in force type of good-till-cancelled can be edited. /// /// # Arguments /// - /// * `product_id` - A string that represents the product's ID. - /// * `side` - A string that represents the side: BUY or SELL - /// * `size` - A 64-bit float that represents the size to buy or sell. - /// * `limit_price` - Ceiling price for which the order should get filled. - /// * `stop_price` - Price at which the order should trigger - if stop direction is Up, then the order will trigger when the last trade price goes above this, otherwise order will trigger when last trade price goes below this price. - /// * `stop_direction` - Possible values: [UNKNOWN_STOP_DIRECTION, STOP_DIRECTION_STOP_UP, STOP_DIRECTION_STOP_DOWN] + /// * `request` - A struct containing the order ID, new size, and new price. /// /// # Endpoint / Reference /// #[allow(rustdoc::bare_urls)] - /// https://api.coinbase.com/api/v3/brokerage/orders + /// https://api.coinbase.com/api/v3/brokerage/orders/edit_preivew /// - /// - pub async fn create_stop_limit_gtc( - &mut self, - product_id: &str, - side: &str, - size: &f64, - limit_price: &f64, - stop_price: &f64, - stop_direction: &str, - ) -> CbResult { - let stoplimit = StopLimitGtc { - base_size: size.to_string(), - limit_price: limit_price.to_string(), - stop_price: stop_price.to_string(), - stop_direction: stop_direction.to_string(), - }; - - let config = OrderConfiguration { - stop_limit_stop_limit_gtc: Some(stoplimit), - ..Default::default() - }; - - self.create(product_id, side, config).await + /// https://docs.cloud.coinbase.com/advanced-trade-api/reference/retailbrokerageapi_previeweditorder + pub async fn preview_edit(&mut self, request: &OrderEditRequest) -> CbResult { + let agent = get_auth!(self.agent, "preview edit order"); + let response = agent.post(EDIT_PREVIEW_ENDPOINT, &NoQuery, request).await?; + let data: OrderEditPreview = response + .json() + .await + .map_err(|e| CbError::JsonError(e.to_string()))?; + Ok(data) } - /// Create a Good til Time (Date) Stop Limit order. + /// Create an order. /// /// # Arguments /// - /// * `product_id` - A string that represents the product's ID. - /// * `side` - A string that represents the side: BUY or SELL - /// * `size` - A 64-bit float that represents the size to buy or sell. - /// * `limit_price` - Ceiling price for which the order should get filled. - /// * `stop_price` - Price at which the order should trigger - if stop direction is Up, then the order will trigger when the last trade price goes above this, otherwise order will trigger when last trade price goes below this price. - /// * `stop_direction` - Possible values: [UNKNOWN_STOP_DIRECTION, STOP_DIRECTION_STOP_UP, STOP_DIRECTION_STOP_DOWN] - /// * `end_time` - Time at which the order should be cancelled if it's not filled. + /// * `request` - A struct containing the order details to create. /// /// # Endpoint / Reference /// @@ -398,31 +183,14 @@ impl OrderApi { /// https://api.coinbase.com/api/v3/brokerage/orders /// /// - #[allow(clippy::too_many_arguments)] - pub async fn create_stop_limit_gtd( - &mut self, - product_id: &str, - side: &str, - size: &f64, - limit_price: &f64, - stop_price: &f64, - stop_direction: &str, - end_time: &str, - ) -> CbResult { - let stoplimit = StopLimitGtd { - base_size: size.to_string(), - limit_price: limit_price.to_string(), - stop_price: stop_price.to_string(), - end_time: end_time.to_string(), - stop_direction: stop_direction.to_string(), - }; - - let config = OrderConfiguration { - stop_limit_stop_limit_gtd: Some(stoplimit), - ..Default::default() - }; - - self.create(product_id, side, config).await + pub async fn create(&mut self, request: &OrderCreateRequest) -> CbResult { + let agent = get_auth!(self.agent, "create order"); + let response = agent.post(RESOURCE_ENDPOINT, &NoQuery, request).await?; + let data: OrderCreateResponse = response + .json() + .await + .map_err(|e| CbError::JsonError(e.to_string()))?; + Ok(data) } /// Obtains a single order based on the Order ID (ex. "XXXX-YYYY-ZZZZ"). @@ -438,20 +206,20 @@ impl OrderApi { /// /// pub async fn get(&mut self, order_id: &str) -> CbResult { + let agent = get_auth!(self.agent, "get order"); let resource = format!("{}/historical/{}", RESOURCE_ENDPOINT, order_id); - match self.signer.get(&resource, &NoQuery).await { - Ok(value) => match value.json::().await { - Ok(resp) => Ok(resp.order), - Err(_) => Err(CbAdvError::BadParse( - "could not parse order object".to_string(), - )), - }, - Err(error) => Err(error), - } + let response = agent.get(&resource, &NoQuery).await?; + let data: OrderWrapper = response + .json() + .await + .map_err(|e| CbError::JsonError(e.to_string()))?; + Ok(data.into()) } /// Obtains various orders from the API. /// + /// # Arguments + /// /// * `query` - A Parameters to modify what is returned by the API. /// /// # Endpoint / Reference @@ -460,61 +228,56 @@ impl OrderApi { /// https://api.coinbase.com/api/v3/brokerage/orders/historical /// /// - pub async fn get_bulk(&mut self, query: &ListOrdersQuery) -> CbResult { - match self.signer.get(BATCH_ENDPOINT, query).await { - Ok(value) => match value.json::().await { - Ok(resp) => Ok(resp), - Err(_) => Err(CbAdvError::BadParse( - "could not parse orders vector".to_string(), - )), - }, - Err(error) => Err(error), - } + pub async fn get_bulk(&mut self, query: &OrderListQuery) -> CbResult { + let agent = get_auth!(self.agent, "get bulk orders"); + let response = agent.get(BATCH_ENDPOINT, query).await?; + let data: PaginatedOrders = response + .json() + .await + .map_err(|e| CbError::JsonError(e.to_string()))?; + Ok(data) } /// Obtains all orders for a product based on the product ID. (ex. "BTC-USD"). /// This wraps `get_bulk` and makes several additional requests until there are no /// additional orders. /// - /// NOTE: NOT A STANDARD API FUNCTION. QoL function that may require additional API requests than - /// normal. + /// NOTE: NOT A STANDARD API FUNCTION. QoL function that may require additional API requests than normal. /// /// # Arguments /// /// * `product_id` - Identifier for the account, such as BTC-USD or ETH-USD. - /// * `query` - Optional parameters, should default to None unless you want additional control. + /// * `query` - A Parameters to modify what is returned by the API. pub async fn get_all( &mut self, product_id: &str, - query: Option, + query: &OrderListQuery, ) -> CbResult> { - let mut query = match query { - Some(p) => p, - None => ListOrdersQuery::default(), - }; + is_auth!(self.agent, "get all orders"); + + // Set the product ID for the query. + let mut query = query.clone().product_ids(&[product_id.to_string()]); + let mut all_orders: Vec = vec![]; - // Override product ID. - query.product_id = Some(product_id.to_string()); - let mut orders: Vec = vec![]; - let mut has_next: bool = true; + // Fetch orders until no more pages are available. + loop { + let listed_orders = self.get_bulk(&query).await?; + all_orders.extend(listed_orders.orders); - // Get the orders until there is not a next. - while has_next { - match self.get_bulk(&query).await { - Ok(listed) => { - has_next = listed.has_next; - query.cursor = Some(listed.cursor); - orders.extend(listed.orders); - } - Err(error) => return Err(error), + if listed_orders.has_next { + query.cursor = Some(listed_orders.cursor); + } else { + break; } } - Ok(orders) + Ok(all_orders) } /// Obtains fills from the API. /// + /// # Arguments + /// /// * `query` - A Parameters to modify what is returned by the API. /// /// # Endpoint / Reference @@ -523,15 +286,40 @@ impl OrderApi { /// https://api.coinbase.com/api/v3/brokerage/orders/historical/fills /// /// - pub async fn fills(&mut self, query: &ListFillsQuery) -> CbResult { - match self.signer.get(FILLS_ENDPOINT, query).await { - Ok(value) => match value.json::().await { - Ok(resp) => Ok(resp), - Err(_) => Err(CbAdvError::BadParse( - "could not parse fills vector".to_string(), - )), - }, - Err(error) => Err(error), - } + pub async fn fills(&mut self, query: &OrderListFillsQuery) -> CbResult { + let agent = get_auth!(self.agent, "get fills"); + let response = agent.get(FILLS_ENDPOINT, query).await?; + let data: PaginatedFills = response + .json() + .await + .map_err(|e| CbError::JsonError(e.to_string()))?; + Ok(data) + } + + /// Places an order to close any open positions for a specified product_id. + /// + /// # Arguments + /// + /// * `request` - A request as to what position to close. + /// + /// # Endpoint / Reference + /// + #[allow(rustdoc::bare_urls)] + /// https://api.coinbase.com/api/v3/brokerage/orders/close_position + /// + /// + pub async fn close_position( + &mut self, + request: &OrderClosePositionRequest, + ) -> CbResult { + let agent = get_auth!(self.agent, "close position"); + let response = agent + .post(CLOSE_POSITION_ENDPOINT, &NoQuery, request) + .await?; + let data: OrderCreateResponse = response + .json() + .await + .map_err(|e| CbError::JsonError(e.to_string()))?; + Ok(data) } } diff --git a/src/apis/payment.rs b/src/apis/payment.rs new file mode 100644 index 0000000..624341a --- /dev/null +++ b/src/apis/payment.rs @@ -0,0 +1,68 @@ +//! # Coinbase Advanced Payment API +//! +//! `payment` gives access to the Payment API and the various endpoints associated with it. + +use crate::constants::payments::RESOURCE_ENDPOINT; +use crate::errors::CbError; +use crate::http_agent::SecureHttpAgent; +use crate::models::payment::{PaymentMethod, PaymentMethodWrapper, PaymentMethodsWrapper}; +use crate::traits::{HttpAgent, NoQuery}; +use crate::types::CbResult; + +/// Provides access to the Payment API for the service. +pub struct PaymentApi { + /// Object used to sign requests made to the API. + agent: Option, +} + +impl PaymentApi { + /// Creates a new instance of the Payment API. This grants access to payment information. + /// + /// # Arguments + /// + /// * `agent` - A agent that include the API Key & Secret along with a client to make requests. + pub(crate) fn new(agent: Option) -> Self { + Self { agent } + } + + /// Obtains a list of payment methods for the current user from the API. + /// + /// # Endpoint / Reference + /// + #[allow(rustdoc::bare_urls)] + /// https://api.coinbase.com/api/v3/brokerage/payment_methods + /// + /// + pub async fn get_all(&mut self) -> CbResult> { + let agent = get_auth!(self.agent, "get all payment methods"); + let response = agent.get(RESOURCE_ENDPOINT, &NoQuery).await?; + let data: PaymentMethodsWrapper = response + .json() + .await + .map_err(|e| CbError::JsonError(e.to_string()))?; + Ok(data.into()) + } + + /// Obtains a single payment method by its unique identifier. + /// + /// # Arguments + /// + /// * `payment_method_id` - The unique identifier for the payment method. + /// + /// # Endpoint / Reference + /// + #[allow(rustdoc::bare_urls)] + /// https://api.coinbase.com/api/v3/brokerage/payment_methods + /// + /// + pub async fn get(&mut self, payment_method_id: &str) -> CbResult { + let agent = get_auth!(self.agent, "get payment method"); + let resource = format!("{}/{}", RESOURCE_ENDPOINT, payment_method_id); + let response = agent.get(&resource, &NoQuery).await?; + let data: PaymentMethodWrapper = response + .json() + .await + .map_err(|e| CbError::JsonError(e.to_string()))?; + Ok(data.into()) + } +} diff --git a/src/apis/portfolio.rs b/src/apis/portfolio.rs new file mode 100644 index 0000000..ebe95b4 --- /dev/null +++ b/src/apis/portfolio.rs @@ -0,0 +1,169 @@ +//! # Coinbase Advanced Portfolio API +//! +//! `portfolio` gives access to the Portfolio API and the various endpoints associated with it. +//! This allows for the management of individual portfolios. + +use crate::constants::portfolios::{MOVE_FUNDS_ENDPOINT, RESOURCE_ENDPOINT}; +use crate::errors::CbError; +use crate::http_agent::SecureHttpAgent; +use crate::models::portfolio::{Portfolio, PortfolioListQuery, PortfoliosWrapper}; +use crate::portfolio::{ + PortfolioBreakdown, PortfolioBreakdownQuery, PortfolioBreakdownWrapper, PortfolioModifyRequest, + PortfolioMoveFundsRequest, PortfolioWrapper, +}; +use crate::traits::{HttpAgent, NoQuery}; +use crate::types::CbResult; + +/// Provides access to the Portfolio API for the service. +pub struct PortfolioApi { + /// Object used to sign requests made to the API. + agent: Option, +} + +impl PortfolioApi { + /// Creates a new instance of the Portfolio API. This grants access to product information. + /// + /// # Arguments + /// + /// * `agent` - A agent that include the API Key & Secret along with a client to make requests. + pub(crate) fn new(agent: Option) -> Self { + Self { agent } + } + + /// Obtains various portfolios from the API. + /// + /// # Arguments + /// + /// * `query` - The query parameters to filter the results. + /// + /// # Endpoint / Reference + /// + #[allow(rustdoc::bare_urls)] + /// https://api.coinbase.com/api/v3/brokerage/portfolios + /// + /// + pub async fn get_all(&mut self, query: &PortfolioListQuery) -> CbResult> { + let agent = get_auth!(self.agent, "get all portfolios"); + let response = agent.get(RESOURCE_ENDPOINT, query).await?; + let data: PortfoliosWrapper = response + .json() + .await + .map_err(|e| CbError::JsonError(e.to_string()))?; + Ok(data.into()) + } + + /// Creates a new portfolio. + /// + /// # Arguments + /// + /// * `request` - The request to create a new portfolio. + /// + /// # Endpoint / Reference + /// + #[allow(rustdoc::bare_urls)] + /// https://api.coinbase.com/api/v3/brokerage/portfolios + /// + /// + pub async fn create(&mut self, request: &PortfolioModifyRequest) -> CbResult { + let agent = get_auth!(self.agent, "create portfolio"); + let response = agent.post(RESOURCE_ENDPOINT, &NoQuery, request).await?; + let data: PortfolioWrapper = response + .json() + .await + .map_err(|e| CbError::JsonError(e.to_string()))?; + Ok(data.into()) + } + + /// Edits an existing portfolio. + /// + /// # Arguments + /// + /// * `portfolio_uuid` - The UUID of the portfolio to edit. + /// * `request` - The request to edit the portfolio. + /// + /// # Endpoint / Reference + /// + #[allow(rustdoc::bare_urls)] + /// https://api.coinbase.com/api/v3/brokerage/portfolios + /// + /// + pub async fn edit( + &mut self, + portfolio_uuid: &str, + request: &PortfolioModifyRequest, + ) -> CbResult { + let agent = get_auth!(self.agent, "edit portfolio"); + let resource = format!("{}/{}", RESOURCE_ENDPOINT, portfolio_uuid); + let response = agent.put(&resource, &NoQuery, request).await?; + let data: PortfolioWrapper = response + .json() + .await + .map_err(|e| CbError::JsonError(e.to_string()))?; + Ok(data.into()) + } + + /// Edits an existing portfolio. + /// + /// # Arguments + /// + /// * `portfolio_uuid` - The UUID of the portfolio to delete. + /// + /// # Endpoint / Reference + /// + #[allow(rustdoc::bare_urls)] + /// https://api.coinbase.com/api/v3/brokerage/portfolios + /// + /// + pub async fn delete(&mut self, portfolio_uuid: &str) -> CbResult<()> { + let agent = get_auth!(self.agent, "delete portfolio"); + let resource = format!("{}/{}", RESOURCE_ENDPOINT, portfolio_uuid); + agent.delete(&resource, &NoQuery).await?; + Ok(()) + } + + /// Move funds from a source portfolio to a target portfolio. + /// + /// # Arguments + /// + /// * `request` - The request to move funds. + /// + /// # Endpoint / Reference + /// + #[allow(rustdoc::bare_urls)] + /// https://api.coinbase.com/api/v3/brokerage/portfolios/move_funds + /// + /// + pub async fn move_funds(&mut self, request: &PortfolioMoveFundsRequest) -> CbResult<()> { + let agent = get_auth!(self.agent, "move funds"); + agent.post(MOVE_FUNDS_ENDPOINT, &NoQuery, request).await?; + Ok(()) + } + + /// Obtains a breakdown of a specific portfolio. + /// + /// # Arguments + /// + /// * `portfolio_uuid` - The UUID of the portfolio to obtain a breakdown for. + /// * `query` - The query parameters to filter the results. + /// + /// # Endpoint / Reference + /// + #[allow(rustdoc::bare_urls)] + /// https://api.coinbase.com/api/v3/brokerage/portfolios + /// + /// + pub async fn get( + &mut self, + portfolio_uuid: &str, + query: &PortfolioBreakdownQuery, + ) -> CbResult { + let agent = get_auth!(self.agent, "get portfolio breakdown"); + let resource = format!("{}/{}", RESOURCE_ENDPOINT, portfolio_uuid); + let response = agent.get(&resource, query).await?; + let data: PortfolioBreakdownWrapper = response + .json() + .await + .map_err(|e| CbError::JsonError(e.to_string()))?; + Ok(data.into()) + } +} diff --git a/src/apis/product.rs b/src/apis/product.rs index 9d6bd14..82d01fd 100644 --- a/src/apis/product.rs +++ b/src/apis/product.rs @@ -7,21 +7,21 @@ use crate::constants::products::{ BID_ASK_ENDPOINT, CANDLE_MAXIMUM, PRODUCT_BOOK_ENDPOINT, RESOURCE_ENDPOINT, }; -use crate::errors::CbAdvError; +use crate::errors::CbError; +use crate::http_agent::SecureHttpAgent; use crate::models::product::{ - BidAskResponse, Candle, CandleResponse, ListProductsQuery, ListProductsResponse, Product, - ProductBook, ProductBookResponse, Ticker, TickerQuery, + Candle, CandlesWrapper, Product, ProductBook, ProductBookWrapper, ProductBooksWrapper, + ProductListQuery, ProductTickerQuery, ProductsWrapper, Ticker, }; -use crate::product::{BidAskQuery, ProductBookQuery}; -use crate::signer::Signer; -use crate::time; -use crate::traits::NoQuery; +use crate::product::{ProductBidAskQuery, ProductBookQuery, ProductCandleQuery}; +use crate::time::{self, Granularity}; +use crate::traits::{HttpAgent, NoQuery, Query}; use crate::types::CbResult; /// Provides access to the Product API for the service. pub struct ProductApi { /// Object used to sign requests made to the API. - signer: Signer, + agent: Option, } impl ProductApi { @@ -29,18 +29,16 @@ impl ProductApi { /// /// # Arguments /// - /// * `signer` - A Signer that include the API Key & Secret along with a client to make - /// requests. - pub(crate) fn new(signer: Signer) -> Self { - Self { signer } + /// * `agent` - A agent that include the API Key & Secret along with a client to make requests. + pub(crate) fn new(agent: Option) -> Self { + Self { agent } } /// Obtains best bids and asks for a vector of product IDs.. /// /// # Arguments /// - /// * `product_ids` - A vector of strings the represents the product IDs of product books to - /// obtain. + /// * `query` - A query to obtain the best bid/ask for multiple products. /// /// # Endpoint / Reference /// @@ -48,24 +46,21 @@ impl ProductApi { /// https://api.coinbase.com/api/v3/brokerage/best_bid_ask /// /// - pub async fn best_bid_ask(&mut self, product_ids: Vec) -> CbResult> { - let query = BidAskQuery { product_ids }; - - match self.signer.get(BID_ASK_ENDPOINT, &query).await { - Ok(value) => match value.json::().await { - Ok(bidasks) => Ok(bidasks.pricebooks), - Err(_) => Err(CbAdvError::BadParse("bid asks object".to_string())), - }, - Err(error) => Err(error), - } + pub async fn best_bid_ask(&mut self, query: &ProductBidAskQuery) -> CbResult> { + let agent = get_auth!(self.agent, "get best bid/ask"); + let response = agent.get(BID_ASK_ENDPOINT, query).await?; + let data: ProductBooksWrapper = response + .json() + .await + .map_err(|e| CbError::JsonError(e.to_string()))?; + Ok(data.into()) } /// Obtains the product book (bids and asks) for the product ID provided. /// /// # Arguments /// - /// * `product_id` - A string the represents the product's ID. - /// * `limit` - An integer the represents the amount to obtain, defaults to 250. + /// * `query` - A query to obtain the product book. /// /// # Endpoint / Reference /// @@ -73,23 +68,14 @@ impl ProductApi { /// https://api.coinbase.com/api/v3/brokerage/product_book /// /// - pub async fn product_book( - &mut self, - product_id: &str, - limit: Option, - ) -> CbResult { - let query = ProductBookQuery { - product_id: product_id.to_string(), - limit, - }; - - match self.signer.get(PRODUCT_BOOK_ENDPOINT, &query).await { - Ok(value) => match value.json::().await { - Ok(book) => Ok(book.pricebook), - Err(_) => Err(CbAdvError::BadParse("product book object".to_string())), - }, - Err(error) => Err(error), - } + pub async fn product_book(&mut self, query: &ProductBookQuery) -> CbResult { + let agent = get_auth!(self.agent, "get product book"); + let response = agent.get(PRODUCT_BOOK_ENDPOINT, query).await?; + let data: ProductBookWrapper = response + .json() + .await + .map_err(|e| CbError::JsonError(e.to_string()))?; + Ok(data.into()) } /// Obtains a single product based on the Product ID (ex. "BTC-USD"). @@ -105,14 +91,14 @@ impl ProductApi { /// /// pub async fn get(&mut self, product_id: &str) -> CbResult { + let agent = get_auth!(self.agent, "get product"); let resource = format!("{}/{}", RESOURCE_ENDPOINT, product_id); - match self.signer.get(&resource, &NoQuery).await { - Ok(value) => match value.json::().await { - Ok(product) => Ok(product), - Err(_) => Err(CbAdvError::BadParse("product object".to_string())), - }, - Err(error) => Err(error), - } + let response = agent.get(&resource, &NoQuery).await?; + let data: Product = response + .json() + .await + .map_err(|e| CbError::JsonError(e.to_string()))?; + Ok(data) } /// Obtains bulk products from the API. @@ -127,14 +113,14 @@ impl ProductApi { /// https://api.coinbase.com/api/v3/brokerage/products /// /// - pub async fn get_bulk(&mut self, query: &ListProductsQuery) -> CbResult> { - match self.signer.get(RESOURCE_ENDPOINT, query).await { - Ok(value) => match value.json::().await { - Ok(resp) => Ok(resp.products), - Err(_) => Err(CbAdvError::BadParse("products vector".to_string())), - }, - Err(error) => Err(error), - } + pub async fn get_bulk(&mut self, query: &ProductListQuery) -> CbResult> { + let agent = get_auth!(self.agent, "get bulk products"); + let response = agent.get(RESOURCE_ENDPOINT, query).await?; + let data: ProductsWrapper = response + .json() + .await + .map_err(|e| CbError::JsonError(e.to_string()))?; + Ok(data.into()) } /// Obtains candles for a specific product. @@ -142,7 +128,7 @@ impl ProductApi { /// # Arguments /// /// * `product_id` - A string the represents the product's ID. - /// * `query` - Span of time to obtain. + /// * `query` - A query to obtain candles within a span of time. /// /// # Endpoint / Reference /// @@ -150,15 +136,19 @@ impl ProductApi { /// https://api.coinbase.com/api/v3/brokerage/products/{product_id}/candles /// /// - pub async fn candles(&mut self, product_id: &str, query: &time::Span) -> CbResult> { + pub async fn candles( + &mut self, + product_id: &str, + query: &ProductCandleQuery, + ) -> CbResult> { + let agent = get_auth!(self.agent, "get candles"); let resource = format!("{}/{}/candles", RESOURCE_ENDPOINT, product_id); - match self.signer.get(&resource, query).await { - Ok(value) => match value.json::().await { - Ok(resp) => Ok(resp.candles), - Err(_) => Err(CbAdvError::BadParse("candle object".to_string())), - }, - Err(error) => Err(error), - } + let response = agent.get(&resource, query).await?; + let data: CandlesWrapper = response + .json() + .await + .map_err(|e| CbError::JsonError(e.to_string()))?; + Ok(data.into()) } /// Obtains candles for a specific product extended. This will exceed the 300 limit threshold @@ -174,47 +164,44 @@ impl ProductApi { pub async fn candles_ext( &mut self, product_id: &str, - query: &time::Span, + query: &ProductCandleQuery, ) -> CbResult> { - let resource = format!("{}/{}/candles", RESOURCE_ENDPOINT, product_id); - - // Make a copy of the query. - let end = query.end; - let interval = query.granularity as u64; - let maximum = CANDLE_MAXIMUM; - let granularity = &time::Granularity::from_secs(query.granularity); - - // Create new span. - let mut span = time::Span::new(query.start, end, granularity); - span.end = time::after(query.start, interval * maximum); - if span.end > end { - span.end = end; + is_auth!(self.agent, "get candles extended"); + query.check()?; + + // Extract query parameters. + let end_time = query.end; + let granularity = query.granularity.clone(); + let interval_seconds = Granularity::to_secs(&granularity) as u64; + let maximum_candles = CANDLE_MAXIMUM as u64; + + // Initialize the span. + let mut current_start = query.start; + let mut all_candles: Vec = Vec::new(); + + while current_start < end_time { + // Calculate the end time for the current batch. + let current_end = std::cmp::min( + time::after(current_start, interval_seconds * maximum_candles), + end_time, + ); + + // Create a new span for the current batch and fetch candles. + let query = ProductCandleQuery { + start: current_start, + end: current_end, + granularity: granularity.clone(), + limit: CANDLE_MAXIMUM, + }; + + let mut candles = self.candles(product_id, &query).await?; + all_candles.append(&mut candles); + + // Update the start time for the next batch. + current_start = current_end; } - let mut candles: Vec = vec![]; - while span.count() > 0 { - match self.signer.get(&resource, &span).await { - Ok(value) => match value.json::().await { - Ok(resp) => candles.extend(resp.candles), - Err(_) => return Err(CbAdvError::BadParse("candle object".to_string())), - }, - Err(error) => return Err(error), - } - - // Update to get additional candles. - span.start = span.end; - span.end = time::after(span.start, interval * CANDLE_MAXIMUM); - if span.end > end { - span.end = end; - } - - // Stop condition. - if span.start > span.end { - span.start = span.end; - } - } - - Ok(candles) + Ok(all_candles) } /// Obtains product ticker from the API. @@ -230,14 +217,18 @@ impl ProductApi { /// https://api.coinbase.com/api/v3/brokerage/products/{product_id}/ticker /// /// - pub async fn ticker(&mut self, product_id: &str, query: &TickerQuery) -> CbResult { + pub async fn ticker( + &mut self, + product_id: &str, + query: &ProductTickerQuery, + ) -> CbResult { + let agent = get_auth!(self.agent, "get ticker"); let resource = format!("{}/{}/ticker", RESOURCE_ENDPOINT, product_id); - match self.signer.get(&resource, query).await { - Ok(value) => match value.json::().await { - Ok(resp) => Ok(resp), - Err(_) => Err(CbAdvError::BadParse("ticker object".to_string())), - }, - Err(error) => Err(error), - } + let response = agent.get(&resource, query).await?; + let data: Ticker = response + .json() + .await + .map_err(|e| CbError::JsonError(e.to_string()))?; + Ok(data) } } diff --git a/src/apis/public.rs b/src/apis/public.rs new file mode 100644 index 0000000..6b274d8 --- /dev/null +++ b/src/apis/public.rs @@ -0,0 +1,222 @@ +//! # Coinbase Advanced Public API +//! +//! `public` gives access to the Public API and the various endpoints associated with it. +//! Some of the features include getting the API current time in ISO format. + +use crate::constants::products::CANDLE_MAXIMUM; +use crate::constants::public::{PRODUCT_BOOK_ENDPOINT, RESOURCE_ENDPOINT, SERVERTIME_ENDPOINT}; +use crate::errors::CbError; +use crate::http_agent::PublicHttpAgent; +use crate::models::product::{ + Candle, CandlesWrapper, Product, ProductBook, ProductBookWrapper, ProductListQuery, + ProductTickerQuery, ProductsWrapper, Ticker, +}; +use crate::models::public::ServerTime; +use crate::product::{ProductBookQuery, ProductCandleQuery}; +use crate::time::{self, Granularity}; +use crate::traits::{HttpAgent, NoQuery, Query}; +use crate::types::CbResult; + +/// Provides access to the Public API for the service. +pub struct PublicApi { + /// Object used to sign requests made to the API. + agent: PublicHttpAgent, +} + +impl PublicApi { + /// Creates a new instance of the Public API. This grants access to public information that requires no authentication. + /// + /// # Arguments + /// + /// * `agent` - A agent that include the API Key & Secret along with a client to make requests. + pub(crate) fn new(agent: PublicHttpAgent) -> Self { + Self { agent } + } + + /// Get the current time from the Coinbase Advanced API. + /// + /// # Endpoint / Reference + /// + #[allow(rustdoc::bare_urls)] + /// https://api.coinbase.com/api/v3/brokerage/time + /// + /// + pub async fn time(&mut self) -> CbResult { + let response = self.agent.get(SERVERTIME_ENDPOINT, &NoQuery).await?; + let data: ServerTime = response + .json() + .await + .map_err(|e| CbError::JsonError(e.to_string()))?; + Ok(data) + } + + /// Obtains the product book (bids and asks) for the product ID provided. + /// + /// # Arguments + /// + /// * `query` - Query used to obtain the product book. + /// + /// # Endpoint / Reference + /// + #[allow(rustdoc::bare_urls)] + /// https://api.coinbase.com/api/v3/brokerage/market/product_book + /// + /// + pub async fn product_book(&mut self, query: &ProductBookQuery) -> CbResult { + let response = self.agent.get(PRODUCT_BOOK_ENDPOINT, query).await?; + let data: ProductBookWrapper = response + .json() + .await + .map_err(|e| CbError::JsonError(e.to_string()))?; + Ok(data.into()) + } + + /// Obtains a single product based on the Product ID (ex. "BTC-USD"). + /// + /// # Arguments + /// + /// * `product_id` - A string the represents the product's ID. + /// + /// # Endpoint / Reference + /// + #[allow(rustdoc::bare_urls)] + /// https://api.coinbase.com/api/v3/brokerage/products/{product_id} + /// + /// + pub async fn product(&mut self, product_id: &str) -> CbResult { + let resource = format!("{}/{}", RESOURCE_ENDPOINT, product_id); + let response = self.agent.get(&resource, &NoQuery).await?; + let data: Product = response + .json() + .await + .map_err(|e| CbError::JsonError(e.to_string()))?; + Ok(data) + } + + /// Obtains bulk products from the API. + /// + /// # Arguments + /// + /// * `query` - Query used to obtain products. + /// + /// # Endpoint / Reference + /// + #[allow(rustdoc::bare_urls)] + /// https://api.coinbase.com/api/v3/brokerage/products + /// + /// + pub async fn products(&mut self, query: &ProductListQuery) -> CbResult> { + let response = self.agent.get(RESOURCE_ENDPOINT, query).await?; + let data: ProductsWrapper = response + .json() + .await + .map_err(|e| CbError::JsonError(e.to_string()))?; + Ok(data.into()) + } + + /// Obtains candles for a specific product. + /// + /// # Arguments + /// + /// * `product_id` - A string the represents the product's ID. + /// * `query` - Span of time to obtain. + /// + /// # Endpoint / Reference + /// + #[allow(rustdoc::bare_urls)] + /// https://api.coinbase.com/api/v3/brokerage/products/{product_id}/candles + /// + /// + pub async fn candles( + &mut self, + product_id: &str, + query: &ProductCandleQuery, + ) -> CbResult> { + let resource = format!("{}/{}/candles", RESOURCE_ENDPOINT, product_id); + let response = self.agent.get(&resource, query).await?; + let data: CandlesWrapper = response + .json() + .await + .map_err(|e| CbError::JsonError(e.to_string()))?; + Ok(data.into()) + } + + /// Obtains candles for a specific product extended. This will exceed the 300 limit threshold + /// and try to obtain the amount specified. + /// + /// NOTE: NOT A STANDARD API FUNCTION. QoL function that may require additional API requests than + /// normal. + /// + /// # Arguments + /// + /// * `product_id` - A string the represents the product's ID. + /// * `query` - Span of time to obtain. + pub async fn candles_ext( + &mut self, + product_id: &str, + query: &ProductCandleQuery, + ) -> CbResult> { + query.check()?; + + // Extract query parameters. + let end_time = query.end; + let granularity = query.granularity.clone(); + let interval_seconds = Granularity::to_secs(&granularity) as u64; + let maximum_candles = CANDLE_MAXIMUM as u64; + + // Initialize the span. + let mut current_start = query.start; + let mut all_candles: Vec = Vec::new(); + + while current_start < end_time { + // Calculate the end time for the current batch. + let current_end = std::cmp::min( + time::after(current_start, interval_seconds * maximum_candles), + end_time, + ); + + // Create a new span for the current batch and fetch candles. + let query = ProductCandleQuery { + start: current_start, + end: current_end, + granularity: granularity.clone(), + limit: CANDLE_MAXIMUM, + }; + + let mut candles = self.candles(product_id, &query).await?; + all_candles.append(&mut candles); + + // Update the start time for the next batch. + current_start = current_end; + } + + Ok(all_candles) + } + + /// Obtains product ticker from the API. + /// + /// # Arguments + /// + /// * `product_id` - A string the represents the product's ID. + /// * `query` - Amount of products to get. + /// + /// # Endpoint / Reference + /// + #[allow(rustdoc::bare_urls)] + /// https://api.coinbase.com/api/v3/brokerage/products/{product_id}/ticker + /// + /// + pub async fn ticker( + &mut self, + product_id: &str, + query: &ProductTickerQuery, + ) -> CbResult { + let resource = format!("{}/{}/ticker", RESOURCE_ENDPOINT, product_id); + let response = self.agent.get(&resource, query).await?; + let data: Ticker = response + .json() + .await + .map_err(|e| CbError::JsonError(e.to_string()))?; + Ok(data) + } +} diff --git a/src/apis/util.rs b/src/apis/util.rs deleted file mode 100644 index e4f44a6..0000000 --- a/src/apis/util.rs +++ /dev/null @@ -1,47 +0,0 @@ -//! # Coinbase Advanced Utils API -//! -//! `util` gives access to the Utils API and the various endpoints associated with it. -//! Some of the features include getting the API current time in ISO format. - -use crate::constants::utils::UNIXTIME_ENDPOINT; -use crate::errors::CbAdvError; -use crate::signer::Signer; -use crate::traits::NoQuery; -use crate::types::CbResult; -use crate::util::UnixTime; - -/// Provides access to the Utils API for the service. -pub struct UtilApi { - /// Object used to sign requests made to the API. - signer: Signer, -} - -impl UtilApi { - /// Creates a new instance of the Utils API. This grants access to product information. - /// - /// # Arguments - /// - /// * `signer` - A Signer that include the API Key & Secret along with a client to make - /// requests. - pub(crate) fn new(signer: Signer) -> Self { - Self { signer } - } - - /// Get the current time from the Coinbase Advanced API. - /// - /// # Endpoint / Reference - /// - #[allow(rustdoc::bare_urls)] - /// https://api.coinbase.com/api/v3/brokerage/time - /// - /// https://docs.cloud.coinbase.com/advanced-trade-api/reference/retailbrokerageapi_getunixtime - pub async fn unixtime(&mut self) -> CbResult { - match self.signer.get(UNIXTIME_ENDPOINT, &NoQuery).await { - Ok(value) => match value.json::().await { - Ok(resp) => Ok(resp), - Err(_) => Err(CbAdvError::BadParse("util unixtime object".to_string())), - }, - Err(error) => Err(error), - } - } -} diff --git a/src/candle_watcher.rs b/src/candle_watcher.rs new file mode 100644 index 0000000..16c37a4 --- /dev/null +++ b/src/candle_watcher.rs @@ -0,0 +1,164 @@ +//! Candle Watcher is the underlying object used to track candle updates. + +use std::cmp::Ord; +use std::collections::HashMap; + +use chrono::Utc; + +use crate::constants::websocket::GRANULARITY; +use crate::models::product::Candle; +use crate::models::websocket::{CandleUpdate, Channel, Event, Message}; +use crate::traits::{CandleCallback, MessageCallback}; +use crate::types::CbResult; +use crate::ws::Endpoint; +use crate::WebSocketClient; + +/// Tracks the candle watcher task. +pub(crate) struct CandleWatcher +where + T: CandleCallback, +{ + /// Holds the most recent candle processed for each product. [key: Product Id, value: Candle] + candles: HashMap, + /// User-defined object that implements `CandleCallback`, triggered on completed candles. + user_watcher: T, +} + +impl CandleWatcher +where + T: CandleCallback, +{ + /// Starts the task that tracks candles for completion. + /// + /// # Arguments + /// + /// * `reader` - WebSocket reader to receive updates. + /// * `user_obj` - User object that implements `CandleCallback` to receive completed candles. + pub(crate) async fn start(mut client: WebSocketClient, endpoint: Endpoint, user_obj: T) + where + T: CandleCallback + Send + Sync + 'static, + { + let tracker = Self { + candles: HashMap::new(), + user_watcher: user_obj, + }; + + // Start the listener. + client.listen_trait(endpoint, tracker).await; + } + + /// Returns a completed candle if a newer candle is received. + /// + /// # Arguments + /// + /// * `product_id` - The ID of the product this candle belongs to. + /// * `new_candle` - The new candle update received from the WebSocket. + fn check_candle(&mut self, product_id: &str, new_candle: Candle) -> Option { + // Retrieve the current candle for the product. + match self.candles.get(product_id) { + Some(existing_candle) => { + if existing_candle.start < new_candle.start { + // A newer candle has been received; replace the existing candle. + let completed_candle = self.candles.remove(product_id).unwrap(); + self.candles.insert(product_id.to_string(), new_candle); + Some(completed_candle) // Return the completed candle. + } else { + // Update the existing candle without considering it complete. + self.candles.insert(product_id.to_string(), new_candle); + None + } + } + None => { + // No existing candle; add the new candle as the initial one. + self.candles.insert(product_id.to_string(), new_candle); + None + } + } + } + + /// Extracts candle updates from a WebSocket message. + /// + /// # Arguments + /// + /// * `message` - The WebSocket message to extract updates from. + /// + /// # Returns + /// + /// A vector of `CandleUpdate` sorted by timestamp (newest first). + fn extract_candle_updates(&self, message: &Message) -> Vec { + let mut updates: Vec = message + .events + .iter() + .filter_map(|event| { + if let Event::Candles(candles_event) = event { + Some(candles_event.candles.clone()) + } else { + None + } + }) + .flatten() + .collect(); + + // Sort updates by timestamp (newest first). + updates.sort_by(|a, b| b.data.start.cmp(&a.data.start)); + updates + } + + /// Processes a vector of candle updates. + /// + /// # Arguments + /// + /// * `updates` - The sorted vector of `CandleUpdate` to process. + fn process_candle_updates(&mut self, mut updates: Vec) { + if let Some(update) = updates.pop() { + let product_id = update.product_id.clone(); + let new_candle = update.data; + + if let Some(completed_candle) = self.check_candle(&product_id, new_candle) { + self.trigger_user_callback(product_id, completed_candle); + } + } + } + + /// Triggers the user's callback with a completed candle. + /// + /// # Arguments + /// + /// * `product_id` - The ID of the product associated with the candle. + /// * `completed_candle` - The completed candle to send to the callback. + fn trigger_user_callback(&mut self, product_id: String, completed_candle: Candle) { + let now = Utc::now().timestamp() as u64; + let start_time = now - (now % (GRANULARITY * 2)); + + self.user_watcher + .candle_callback(start_time, product_id, completed_candle); + } +} + +impl MessageCallback for CandleWatcher +where + T: CandleCallback + Send + Sync, +{ + /// Handles incoming messages and processes candle updates. + fn message_callback(&mut self, msg: CbResult) { + match msg { + Ok(message) => { + if message.channel != Channel::Candles { + return; // Ignore non-candle messages. + } + + // Extract candle updates and process them. + let updates = self.extract_candle_updates(&message); + if updates.is_empty() { + return; // No updates to process. + } + + // Process the most recent update and handle completed candles. + self.process_candle_updates(updates); + } + Err(err) => { + eprintln!("!WEBSOCKET ERROR! {}", err); + } + } + } +} diff --git a/src/config.rs b/src/config.rs index 9d8e4c6..b6d7257 100644 --- a/src/config.rs +++ b/src/config.rs @@ -7,7 +7,7 @@ use serde::{de::DeserializeOwned, Deserialize, Serialize}; use std::fs; use toml; -const CURRENT_CONFIG_VERSION: u8 = 1; +const CURRENT_CONFIG_VERSION: u8 = 2; /// Generic configuration file with the minimum requirements for API access. /// This is used to implement on custom configurations and to be passed when @@ -29,6 +29,8 @@ pub struct ApiConfig { pub api_secret: String, /// Enable debug messages or not. pub debug: bool, + /// Use sandbox or not. + pub use_sandbox: bool, } impl ApiConfig { @@ -45,6 +47,7 @@ impl Default for ApiConfig { api_key: "YOUR_COINBASE_API_KEY_HERE".to_string(), api_secret: "YOUR_COINBASE_API_SECRET_HERE".to_string(), debug: false, + use_sandbox: false, } } } diff --git a/src/constants.rs b/src/constants.rs index bd0ffcc..bdadbc2 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -2,16 +2,19 @@ /// Root resource for the API pub(crate) const API_ROOT_URI: &str = "api.coinbase.com"; +pub(crate) const API_SANDBOX_ROOT_URI: &str = "api-sandbox.coinbase.com"; +pub(crate) const CRATE_USER_AGENT: &str = "cbadv/Rust"; /// Accounts API constants pub(crate) mod accounts { pub(crate) const RESOURCE_ENDPOINT: &str = "/api/v3/brokerage/accounts"; + pub(crate) const LIST_ACCOUNT_MAXIMUM: u32 = 250; } /// Convert API constants pub(crate) mod convert { - pub(crate) const RESOURCE_ENDPOINT: &str = "/api/v3/brokerage/convert"; pub(crate) const QUOTE_ENDPOINT: &str = "/api/v3/brokerage/convert/quote"; + pub(crate) const TRADE_ENDPOINT: &str = "/api/v3/brokerage/convert/trade"; } /// Fees API constants @@ -24,41 +27,58 @@ pub(crate) mod orders { pub(crate) const RESOURCE_ENDPOINT: &str = "/api/v3/brokerage/orders"; pub(crate) const CANCEL_BATCH_ENDPOINT: &str = "/api/v3/brokerage/orders/batch_cancel"; pub(crate) const EDIT_ENDPOINT: &str = "/api/v3/brokerage/orders/edit"; + pub(crate) const CREATE_PREVIEW_ENDPOINT: &str = "/api/v3/brokerage/orders/preview"; pub(crate) const EDIT_PREVIEW_ENDPOINT: &str = "/api/v3/brokerage/orders/edit_preview"; pub(crate) const BATCH_ENDPOINT: &str = "/api/v3/brokerage/orders/historical/batch"; pub(crate) const FILLS_ENDPOINT: &str = "/api/v3/brokerage/orders/historical/fills"; + pub(crate) const CLOSE_POSITION_ENDPOINT: &str = "/api/v3/brokerage/orders/close_position"; +} + +/// Portfolios API constants +pub(crate) mod portfolios { + pub(crate) const RESOURCE_ENDPOINT: &str = "/api/v3/brokerage/portfolios"; + pub(crate) const MOVE_FUNDS_ENDPOINT: &str = "/api/v3/brokerage/portfolios/move_funds"; } /// Products API constants pub(crate) mod products { - pub(crate) const CANDLE_MAXIMUM: u64 = 300; + pub(crate) const CANDLE_MAXIMUM: u32 = 350; pub(crate) const RESOURCE_ENDPOINT: &str = "/api/v3/brokerage/products"; pub(crate) const BID_ASK_ENDPOINT: &str = "/api/v3/brokerage/best_bid_ask"; pub(crate) const PRODUCT_BOOK_ENDPOINT: &str = "/api/v3/brokerage/product_book"; } -/// Utils API constants -pub(crate) mod utils { - pub(crate) const UNIXTIME_ENDPOINT: &str = "/api/v3/brokerage/time"; +/// Payment API constants +pub(crate) mod payments { + pub(crate) const RESOURCE_ENDPOINT: &str = "/api/v3/brokerage/payment_methods"; +} + +/// Data API constants +pub(crate) mod data { + pub(crate) const KEY_PERMISSIONS_ENDPOINT: &str = "/api/v3/brokerage/key_permissions"; } -/// REST API constants -pub(crate) mod rest { - pub(crate) const SERVICE: &str = "retail_rest_api_proxy"; +/// Public API constants +pub(crate) mod public { + pub(crate) const SERVERTIME_ENDPOINT: &str = "/api/v3/brokerage/time"; + pub(crate) const PRODUCT_BOOK_ENDPOINT: &str = "/api/v3/brokerage/market/product_book"; + pub(crate) const RESOURCE_ENDPOINT: &str = "/api/v3/brokerage/market/products"; } /// Websocket API constants pub(crate) mod websocket { - pub(crate) const RESOURCE_ENDPOINT: &str = "wss://advanced-trade-ws.coinbase.com"; + pub(crate) const PUBLIC_ENDPOINT: &str = "wss://advanced-trade-ws.coinbase.com"; + pub(crate) const SECURE_ENDPOINT: &str = "wss://advanced-trade-ws-user.coinbase.com"; /// Granularity of Candles from the WebSocket Candle subscription. /// NOTE: This is a restriction by CoinBase and cannot be currently changed (20240125) pub(crate) const GRANULARITY: u64 = 300; - pub(crate) const SERVICE: &str = "public_websocket_api"; } /// Amount of tokens per second refilled. pub(crate) mod ratelimits { - pub(crate) const REST_REFRESH_RATE: f64 = 30.0; - pub(crate) const WEBSOCKET_REFRESH_RATE: f64 = 750.0; + pub(crate) const SECURE_REST_REFRESH_RATE: f64 = 30.0; + pub(crate) const PUBLIC_REST_REFRESH_RATE: f64 = 10.0; + pub(crate) const SECURE_WEBSOCKET_REFRESH_RATE: f64 = 750.0; + pub(crate) const PUBLIC_WEBSOCKET_REFRESH_RATE: f64 = 8.0; } diff --git a/src/errors.rs b/src/errors.rs index f7f3c09..f5ddb00 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -1,14 +1,18 @@ //! Contains all errors produced by the crate. +use std::error::Error; use std::fmt; /// Types of errors that can occur. #[derive(Debug)] -pub enum CbAdvError { - /// Unable to parse JSON successfully. +pub enum CbError { + /// Unable to parse JSON or Builders successfully. BadParse(String), /// Non-200 status code received. - BadStatus(String), + BadStatus { + code: reqwest::StatusCode, + body: String, + }, /// Could not connect to the service. BadConnection(String), /// Nothing to do. @@ -20,23 +24,47 @@ pub enum CbAdvError { /// Could not identify the API Secret key type. BadPrivateKey(String), /// Could not serialize the body of a message. - BadSerialization, + BadSerialization(String), /// General unknown error. Unknown(String), + /// HTTP request error. + RequestError(String), + /// URL parse error. + UrlParseError(String), + /// JSON deserialization error. + JsonError(String), + /// Authentication error. + AuthenticationError(String), + /// An invalid query. + BadQuery(String), + /// An invalid request. + BadRequest(String), } -impl fmt::Display for CbAdvError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { +impl fmt::Display for CbError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - CbAdvError::Unknown(value) => write!(f, "unknown error occured: {}", value), - CbAdvError::BadSignature(value) => write!(f, "could not create signature: {}", value), - CbAdvError::BadSerialization => write!(f, "could not serialize the message body"), - CbAdvError::BadPrivateKey(value) => write!(f, "invalid private key: {}", value), - CbAdvError::BadParse(value) => write!(f, "could not parse: {}", value), - CbAdvError::NothingToDo(value) => write!(f, "nothing to do: {}", value), - CbAdvError::NotFound(value) => write!(f, "could not find: {}", value), - CbAdvError::BadStatus(value) => write!(f, "non-zero status occurred: {}", value), - CbAdvError::BadConnection(value) => write!(f, "could not connect: {}", value), + CbError::Unknown(value) => write!(f, "unknown error occurred: {}", value), + CbError::BadSignature(value) => write!(f, "could not create signature: {}", value), + CbError::BadSerialization(value) => { + write!(f, "could not serialize the message body: {}", value) + } + CbError::BadPrivateKey(value) => write!(f, "invalid private key: {}", value), + CbError::BadParse(value) => write!(f, "could not parse: {}", value), + CbError::NothingToDo(value) => write!(f, "nothing to do: {}", value), + CbError::NotFound(value) => write!(f, "could not find: {}", value), + CbError::BadStatus { code, body } => { + write!(f, "HTTP error {}: {}", code.as_u16(), body) + } + CbError::BadConnection(value) => write!(f, "could not connect: {}", value), + CbError::RequestError(value) => write!(f, "HTTP request error: {}", value), + CbError::UrlParseError(value) => write!(f, "URL parse error: {}", value), + CbError::JsonError(value) => write!(f, "JSON deserialization error: {}", value), + CbError::AuthenticationError(value) => write!(f, "authentication error: {}", value), + CbError::BadQuery(value) => write!(f, "invalid query: {}", value), + CbError::BadRequest(value) => write!(f, "invalid request: {}", value), } } } + +impl Error for CbError {} diff --git a/src/http_agent.rs b/src/http_agent.rs new file mode 100644 index 0000000..af958e2 --- /dev/null +++ b/src/http_agent.rs @@ -0,0 +1,329 @@ +//! # Authentication and signing messages. +//! +//! `http_agent` contains the backbone of the API requests in the form of the SecureHttpAgent and PublicHttpAgent struct. This signs +//! all requests to the API for ensure proper authentication. The HttpAgents are also responsible for handling +//! the GET and POST requests. + +use std::sync::Arc; + +use futures::lock::Mutex; +use reqwest::header::{CONTENT_TYPE, USER_AGENT}; +use reqwest::{Method, Response, Url}; +use serde::Serialize; + +use crate::constants::{API_ROOT_URI, API_SANDBOX_ROOT_URI, CRATE_USER_AGENT}; +use crate::errors::CbError; +use crate::jwt::Jwt; +use crate::token_bucket::TokenBucket; +use crate::traits::{HttpAgent, Query, Request}; +use crate::types::CbResult; + +/// Base HTTP Agent that is responsible for making requests and token bucket. +#[derive(Debug, Clone)] +pub(crate) struct HttpAgentBase { + /// Wrapped client that is responsible for making the requests. + client: reqwest::Client, + /// Token bucket, used for rate limiting. + bucket: Arc>, + /// Root URI for the API. + root_uri: &'static str, +} + +impl HttpAgentBase { + /// Creates a new instance of SecureHttpAgent. + /// + /// # Arguments + /// + /// * `use_sandbox` - A boolean that determines if the sandbox should be used. + /// * `shared_bucket` - Shared token bucket for all APIs. + pub(crate) fn new(use_sandbox: bool, shared_bucket: Arc>) -> CbResult { + let root_uri = if use_sandbox { + API_SANDBOX_ROOT_URI + } else { + API_ROOT_URI + }; + + let client = reqwest::Client::builder() + .timeout(std::time::Duration::from_secs(10)) + .build() + .map_err(|e| CbError::RequestError(e.to_string()))?; + + Ok(Self { + client, + bucket: shared_bucket, + root_uri, + }) + } + + /// Constructs a URL for the request being made. + /// + /// # Arguments + /// + /// * `resource` - A string representing the resource that is being accessed. + /// * `query` - A string containing options / parameters for the URL. + fn build_url(&self, resource: &str, query: &impl Query) -> CbResult { + // Ensure the query is valid. + query.check()?; + + let base_url = Url::parse(&format!("https://{}", self.root_uri)) + .map_err(|e| CbError::UrlParseError(e.to_string()))?; + let mut url = base_url + .join(resource) + .map_err(|e| CbError::UrlParseError(e.to_string()))?; + url.set_query(Some(&query.to_query())); + Ok(url) + } + + /// Converts the request to a JSON string. + /// + /// # Arguments + /// + /// * `request` - The request to convert to a JSON string. + fn convert_request<'a, T>(&self, request: &'a T) -> CbResult + where + T: Request + Serialize + 'a, + { + request.check()?; + let data = serde_json::to_string(&request) + .map_err(|e| CbError::BadSerialization(e.to_string()))?; + Ok(data) + } + + /// Handles the response from the API. + /// + /// # Arguments + /// + /// * `response` - The response from the API. + async fn handle_response(&self, response: Response) -> CbResult { + if response.status().is_success() { + Ok(response) + } else { + let status = response.status(); + let body = response + .text() + .await + .unwrap_or_else(|_| "Could not parse error message".to_string()); + Err(CbError::BadStatus { code: status, body }) + } + } + + /// Executes the request to the API. + /// + /// # Arguments + /// + /// * `method` - The method of the request, GET, POST, etc. + /// * `url` - The URL to make the request to. + /// * `body` - The body of the request, if any. + /// * `token` - The token to authenticate the request. + pub(crate) async fn execute_request( + &mut self, + method: Method, + url: Url, + body: Option, + token: Option, + ) -> CbResult { + { + let mut locked_bucket = self.bucket.lock().await; + locked_bucket.wait_on().await; + } + + let mut request = self + .client + .request(method, url) + .header(CONTENT_TYPE, "application/json") + .header(USER_AGENT, CRATE_USER_AGENT); + + if let Some(token) = token { + request = request.bearer_auth(token); + } + + if let Some(body) = body { + request = request.body(body); + } + + let response = request + .send() + .await + .map_err(|e| CbError::RequestError(e.to_string()))?; + + self.handle_response(response).await + } +} + +/// Unsigned HTTP Agent that is responsible for making requests without authentication. +#[derive(Debug, Clone)] +pub(crate) struct PublicHttpAgent { + /// Base client that is responsible for making the requests. + pub(crate) base: HttpAgentBase, +} + +impl PublicHttpAgent { + /// Creates a new instance of PublicHttpAgent. + /// + /// # Arguments + /// + /// * `use_sandbox` - A boolean that determines if the sandbox should be used. + /// * `shared_bucket` - Shared token bucket for all APIs. + pub(crate) fn new(use_sandbox: bool, shared_bucket: Arc>) -> CbResult { + Ok(Self { + base: HttpAgentBase::new(use_sandbox, shared_bucket)?, + }) + } +} + +impl HttpAgent for PublicHttpAgent { + async fn get(&mut self, resource: &str, query: &impl Query) -> CbResult { + let url = self.base.build_url(resource, query)?; + self.base + .execute_request(Method::GET, url, None, None) + .await + } + + async fn post<'a, T>( + &mut self, + resource: &str, + query: &impl Query, + body: &'a T, + ) -> CbResult + where + T: Request + Serialize + 'a, + { + let url = self.base.build_url(resource, query)?; + let data = self.base.convert_request(body)?; + self.base + .execute_request(Method::POST, url, Some(data), None) + .await + } + + async fn put<'a, T>( + &mut self, + resource: &str, + query: &impl Query, + body: &'a T, + ) -> CbResult + where + T: Request + Serialize + 'a, + { + let url = self.base.build_url(resource, query)?; + let data = self.base.convert_request(body)?; + self.base + .execute_request(Method::PUT, url, Some(data), None) + .await + } + + async fn delete(&mut self, resource: &str, query: &impl Query) -> CbResult { + let url = self.base.build_url(resource, query)?; + self.base + .execute_request(Method::DELETE, url, None, None) + .await + } +} + +/// Creates and signs HTTP Requests to the API. +#[derive(Debug, Clone)] +pub(crate) struct SecureHttpAgent { + /// JSON Webtoken Generator, disabled in sandbox mode. + jwt: Option, + /// Base client that is responsible for making the requests. + base: HttpAgentBase, +} + +/// Responsible for signing and sending HTTP requests. +impl SecureHttpAgent { + /// Creates a new instance of SecureHttpAgent. + /// + /// # Arguments + /// + /// * `api_key` - A string that holds the key for the API service. + /// * `api_secret` - A string that holds the secret for the API service. + /// * `use_sandbox` - A boolean that determines if the sandbox should be used. + /// * `shared_bucket` - Shared token bucket for all APIs. + pub(crate) fn new( + api_key: &str, + api_secret: &str, + use_sandbox: bool, + shared_bucket: Arc>, + ) -> CbResult { + let jwt = if use_sandbox { + // Do not generate JWT in sandbox mode. + None + } else { + Some( + Jwt::new(api_key, api_secret) + .map_err(|e| CbError::Unknown(format!("Error creating JWT: {}", e)))?, + ) + }; + + Ok(Self { + jwt, + base: HttpAgentBase::new(use_sandbox, shared_bucket)?, + }) + } + + /// Builds a token for the request. If JWT is not enabled, returns None. + /// + /// # Arguments + /// + /// * `method` - The method of the request, GET, POST, etc. + /// * `resource` - The resource being accessed. + fn build_token(&self, method: Method, resource: &str) -> CbResult> { + if let Some(jwt) = &self.jwt { + let uri = Jwt::build_uri(method.as_str(), self.base.root_uri, resource); + Ok(Some(jwt.encode(Some(&uri))?)) + } else { + Ok(None) + } + } +} + +impl HttpAgent for SecureHttpAgent { + async fn get(&mut self, resource: &str, query: &impl Query) -> CbResult { + let url = self.base.build_url(resource, query)?; + let token = self.build_token(Method::GET, resource)?; + self.base + .execute_request(Method::GET, url, None, token) + .await + } + + async fn post<'a, T>( + &mut self, + resource: &str, + query: &impl Query, + body: &'a T, + ) -> CbResult + where + T: Request + Serialize + 'a, + { + let url = self.base.build_url(resource, query)?; + let data = self.base.convert_request(body)?; + let token = self.build_token(Method::POST, resource)?; + self.base + .execute_request(Method::POST, url, Some(data), token) + .await + } + + async fn put<'a, T>( + &mut self, + resource: &str, + query: &impl Query, + body: &'a T, + ) -> CbResult + where + T: Request + Serialize + 'a, + { + let url = self.base.build_url(resource, query)?; + let data = self.base.convert_request(body)?; + let token = self.build_token(Method::PUT, resource)?; + self.base + .execute_request(Method::PUT, url, Some(data), token) + .await + } + + async fn delete(&mut self, resource: &str, query: &impl Query) -> CbResult { + let url = self.base.build_url(resource, query)?; + let token = self.build_token(Method::DELETE, resource)?; + self.base + .execute_request(Method::DELETE, url, None, token) + .await + } +} diff --git a/src/jwt.rs b/src/jwt.rs index a3dbec1..3a3b27d 100644 --- a/src/jwt.rs +++ b/src/jwt.rs @@ -10,7 +10,7 @@ use ring::rand::SystemRandom; use ring::signature::{self}; use serde::Serialize; -use crate::errors::CbAdvError; +use crate::errors::CbError; use crate::time; use crate::types::CbResult; @@ -19,7 +19,6 @@ struct Header { alg: String, kid: String, nonce: String, - typ: String, } #[derive(Serialize)] @@ -28,7 +27,6 @@ struct Payload { iss: String, nbf: u64, exp: u64, - aud: Vec, #[serde(skip_serializing_if = "Option::is_none")] uri: Option, } @@ -73,19 +71,17 @@ impl Jwt { alg: "ES256".to_string(), kid: self.api_key.clone(), nonce, - typ: "JWT".to_string(), } } /// Creates the payload for the message. - fn build_payload(&self, service: &str, uri: Option<&str>) -> Payload { + fn build_payload(&self, uri: Option<&str>) -> Payload { let now = time::now(); Payload { sub: self.api_key.clone(), iss: "coinbase-cloud".to_string(), nbf: now, exp: now + 120, - aud: [service.to_string()].to_vec(), uri: uri.map(|u| u.to_string()), } } @@ -99,7 +95,7 @@ impl Jwt { /// Encodes a serializable type. fn base64_encode(input: &T) -> CbResult { let raw = - serde_json::to_vec(input).map_err(|why| CbAdvError::BadSignature(why.to_string()))?; + serde_json::to_vec(input).map_err(|why| CbError::BadSignature(why.to_string()))?; Ok(Self::to_base64(&raw)) } @@ -134,13 +130,13 @@ impl Jwt { // Not in pkcs8 format, attempt conversion. let ec_key = EcKey::private_key_from_pem(key) - .map_err(|why| CbAdvError::BadPrivateKey(why.to_string()))?; + .map_err(|why| CbError::BadPrivateKey(why.to_string()))?; let pkey = - PKey::from_ec_key(ec_key).map_err(|why| CbAdvError::BadPrivateKey(why.to_string()))?; + PKey::from_ec_key(ec_key).map_err(|why| CbError::BadPrivateKey(why.to_string()))?; let new_key = pkey .private_key_to_pem_pkcs8() - .map_err(|why| CbAdvError::BadPrivateKey(why.to_string()))?; + .map_err(|why| CbError::BadPrivateKey(why.to_string()))?; Self::parse_key(&new_key) } @@ -158,22 +154,22 @@ impl Jwt { /// # Returns /// /// A `CbResult>` which is Ok containing the decoded binary key data if successful, - /// or an Err with a `CbAdvError::BadPrivateKey` containing the error message if any error occurs. + /// or an Err with a `CbError::BadPrivateKey` containing the error message if any error occurs. fn parse_key(api_secret: &[u8]) -> CbResult> { let pem_str = - str::from_utf8(api_secret).map_err(|why| CbAdvError::BadPrivateKey(why.to_string()))?; + str::from_utf8(api_secret).map_err(|why| CbError::BadPrivateKey(why.to_string()))?; // Checks for the headers and footers to remove it. let base64_encoded = if pem_str.starts_with("-----BEGIN") && pem_str.contains("-----END") { let start = pem_str .find("-----BEGIN") .and_then(|s| pem_str[s..].find('\n')) - .ok_or_else(|| CbAdvError::BadPrivateKey("No BEGIN delimiter".to_string()))? + .ok_or_else(|| CbError::BadPrivateKey("No BEGIN delimiter".to_string()))? + 1; let end = pem_str .find("-----END") - .ok_or_else(|| CbAdvError::BadPrivateKey("No END delimiter".to_string()))?; + .ok_or_else(|| CbError::BadPrivateKey("No END delimiter".to_string()))?; // Get the data between the header and footer. pem_str[start..end] @@ -187,7 +183,7 @@ impl Jwt { // Decode the key. STANDARD_NO_PAD .decode(base64_encoded) - .map_err(|why| CbAdvError::BadPrivateKey(why.to_string())) + .map_err(|why| CbError::BadPrivateKey(why.to_string())) } /// Signs a message using ECDSA with the specified private key. @@ -203,10 +199,10 @@ impl Jwt { fn sign_message(&self, message: &[u8]) -> CbResult { let signing_key = signature::EcdsaKeyPair::from_pkcs8(Self::get_alg(), &self.api_secret, &self.rng) - .map_err(|why| CbAdvError::BadSignature(why.to_string()))?; + .map_err(|why| CbError::BadSignature(why.to_string()))?; let signature = signing_key .sign(&self.rng, message) - .map_err(|why| CbAdvError::BadSignature(why.to_string()))?; + .map_err(|why| CbError::BadSignature(why.to_string()))?; Ok(Self::to_base64(signature.as_ref())) } @@ -221,20 +217,20 @@ impl Jwt { /// # Returns /// /// A `CbResult` with the JWT token if successful; otherwise, an error. - pub(crate) fn encode(&self, service: &str, uri: Option<&str>) -> CbResult { + pub(crate) fn encode(&self, uri: Option<&str>) -> CbResult { // Conver the header and payload into base64. let header = Self::base64_encode(&self.build_header())?; - let payload = Self::base64_encode(&self.build_payload(service, uri))?; + let payload = Self::base64_encode(&self.build_payload(uri))?; // Create the message w/ header and payload. let mut message = String::with_capacity(header.len() + payload.len() + 128); write!(message, "{}.{}", header, payload) - .map_err(|why| CbAdvError::BadSignature(why.to_string()))?; + .map_err(|why| CbError::BadSignature(why.to_string()))?; // Sign the message. let signature = self.sign_message(message.as_bytes())?; write!(message, ".{}", signature) - .map_err(|why| CbAdvError::BadSignature(why.to_string()))?; + .map_err(|why| CbError::BadSignature(why.to_string()))?; Ok(message) } diff --git a/src/lib.rs b/src/lib.rs index 9e065b6..4dc4fe4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,9 +14,12 @@ #[cfg(feature = "config")] pub mod config; +#[macro_use] +pub(crate) mod macros; + +mod candle_watcher; +pub(crate) mod http_agent; pub(crate) mod jwt; -pub(crate) mod signer; -mod task_tracker; mod token_bucket; pub(crate) mod constants; @@ -28,9 +31,11 @@ pub(crate) mod utils; pub(crate) mod apis; pub(crate) mod models; -pub use models::{account, convert, fee, order, product, util, websocket as ws}; +pub use models::{ + account, convert, fee, order, portfolio, product, public, shared, websocket as ws, +}; mod rest; mod websocket; -pub use rest::RestClient; -pub use websocket::WebSocketClient; +pub use rest::{RestClient, RestClientBuilder}; +pub use websocket::{WebSocketClient, WebSocketClientBuilder}; diff --git a/src/macros.rs b/src/macros.rs new file mode 100644 index 0000000..ce721af --- /dev/null +++ b/src/macros.rs @@ -0,0 +1,24 @@ +macro_rules! get_auth { + ($agent:expr, $method_name:expr) => { + match $agent { + Some(ref mut agent) => agent, + None => { + return Err(CbError::AuthenticationError(format!( + "Authentication required for '{}'.", + $method_name + ))); + } + } + }; +} + +macro_rules! is_auth { + ($agent:expr, $method_name:expr) => { + if $agent.is_none() { + return Err(CbError::AuthenticationError(format!( + "Authentication required for '{}'.", + $method_name + ))); + } + }; +} diff --git a/src/models/account.rs b/src/models/account.rs index 7140842..35254ac 100644 --- a/src/models/account.rs +++ b/src/models/account.rs @@ -5,17 +5,41 @@ use serde::{Deserialize, Serialize}; +use crate::constants::accounts::LIST_ACCOUNT_MAXIMUM; +use crate::errors::CbError; use crate::traits::Query; -use crate::utils::{deserialize_numeric, QueryBuilder}; +use crate::types::CbResult; +use crate::utils::QueryBuilder; -/// Represents a Balance for either Available or Held funds. -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct Balance { - /// Value for the currency available or held. - #[serde(deserialize_with = "deserialize_numeric")] - pub value: f64, - /// Denomination of the currency. - pub currency: String, +use super::shared::Balance; + +/// Platform that the account is associated with. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub enum Platform { + /// Spot account. + #[serde(rename = "ACCOUNT_PLATFORM_CONSUMER")] + Consumer, + /// US Derivatives account. + #[serde(rename = "ACCOUNT_PLATFORM_CFM_CONSUMER")] + CfmConsumer, + /// International Exchange account. + #[serde(rename = "ACCOUNT_PLATFORM_INTX")] + Intx, +} + +/// Possible values for the account type. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub enum AccountType { + #[serde(rename = "ACCOUNT_TYPE_UNSPECIFIED")] + Unspecified, + #[serde(rename = "ACCOUNT_TYPE_CRYPTO")] + Crypto, + #[serde(rename = "ACCOUNT_TYPE_FIAT")] + Fiat, + #[serde(rename = "ACCOUNT_TYPE_VAULT")] + Vault, + #[serde(rename = "ACCOUNT_TYPE_PERP_FUTURES")] + PerpFutures, } /// Represents an Account received from the API. @@ -40,16 +64,18 @@ pub struct Account { /// Time at which this account was deleted. pub deleted_at: Option, /// Possible values: [ACCOUNT_TYPE_UNSPECIFIED, ACCOUNT_TYPE_CRYPTO, ACCOUNT_TYPE_FIAT, ACCOUNT_TYPE_VAULT] - pub r#type: String, + pub r#type: AccountType, /// Whether or not this account is ready to trade. pub ready: bool, /// Current balance on hold. pub hold: Balance, + /// Platform that the account is associated with. + pub platform: Platform, } -/// Represents a list of accounts received from the API. +/// Response from the API that wraps a list of accounts. #[derive(Deserialize, Debug)] -pub struct ListedAccounts { +pub struct PaginatedAccounts { /// Accounts returned from the API. pub accounts: Vec, /// Whether there are additional pages for this query. @@ -60,28 +86,71 @@ pub struct ListedAccounts { pub size: u32, } -/// Represents an account response from the API. -#[derive(Deserialize, Debug)] -pub(crate) struct AccountResponse { - /// Account returned from the API. - pub(crate) account: Account, -} - /// Represents parameters that are optional for List Account API request. -#[derive(Serialize, Default, Debug, Clone)] -pub struct ListAccountsQuery { +#[derive(Serialize, Debug, Clone)] +pub struct AccountListQuery { /// Amount to obtain, default 49 maximum is 250. - pub limit: Option, + pub limit: u32, /// Returns accounts after the cursor provided. pub cursor: Option, } -impl Query for ListAccountsQuery { - /// Converts the object into HTTP request parameters. +impl Query for AccountListQuery { + fn check(&self) -> CbResult<()> { + if self.limit == 0 || self.limit > LIST_ACCOUNT_MAXIMUM { + return Err(CbError::BadQuery(format!( + "Limit must be greater than 0 with a maximum of {}", + LIST_ACCOUNT_MAXIMUM + ))); + } + Ok(()) + } + fn to_query(&self) -> String { QueryBuilder::new() - .push_u32_optional("limit", self.limit) + .push("limit", self.limit) .push_optional("cursor", &self.cursor) .build() } } + +impl Default for AccountListQuery { + fn default() -> Self { + Self { + limit: 49, + cursor: None, + } + } +} + +impl AccountListQuery { + /// Creates a new AccountListQuery with default values. + pub fn new() -> Self { + Default::default() + } + + /// Sets the limit for the query. Default is 49 and maximum is 250. + pub fn limit(mut self, limit: u32) -> Self { + self.limit = limit; + self + } + + /// Sets the cursor for the query. + pub fn cursor(mut self, cursor: String) -> Self { + self.cursor = Some(cursor); + self + } +} + +/// Response from the API that wraps a single account. +#[derive(Deserialize, Debug)] +pub(crate) struct AccountWrapper { + /// Account returned from the API. + pub(crate) account: Account, +} + +impl From for Account { + fn from(wrapper: AccountWrapper) -> Self { + wrapper.account + } +} diff --git a/src/models/convert.rs b/src/models/convert.rs index e313f48..a9bb278 100644 --- a/src/models/convert.rs +++ b/src/models/convert.rs @@ -4,97 +4,102 @@ //! This allows for the conversion between two currencies. use serde::{Deserialize, Serialize}; +use serde_with::{serde_as, DisplayFromStr}; -use crate::{traits::Query, utils::QueryBuilder}; +use crate::errors::CbError; +use crate::traits::{Query, Request}; +use crate::types::CbResult; +use crate::utils::QueryBuilder; -use super::account::Balance; +use super::shared::Balance; -#[derive(Deserialize, Debug)] -pub(crate) struct ConvertResponse { - pub(crate) trade: Trade, +/// Possible values for the trade status. +#[derive(Deserialize, Serialize, Clone, PartialEq, Debug)] +pub enum TradeStatus { + /// Unspecified trade status. + #[serde(rename = "TRADE_STATUS_UNSPECIFIED")] + Unspecified, + /// Trade has been created. + #[serde(rename = "TRADE_STATUS_CREATED")] + Created, + /// Trade has started. + #[serde(rename = "TRADE_STATUS_STARTED")] + Started, + /// Trade has been completed. + #[serde(rename = "TRADE_STATUS_COMPLETED")] + Completed, + /// Trade has been canceled. + #[serde(rename = "TRADE_STATUS_CANCELED")] + Canceled, } -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Serialize, Debug)] pub struct Trade { /// The trade id, used to get and commit the trade pub id: String, /// Possible values: [TRADE_STATUS_UNSPECIFIED, TRADE_STATUS_CREATED, TRADE_STATUS_STARTED, TRADE_STATUS_COMPLETED, TRADE_STATUS_CANCELED] - pub status: String, + pub status: TradeStatus, pub user_entered_amount: Balance, pub amount: Balance, pub subtotal: Balance, pub total: Balance, - /// List of fees associated with the trade + // List of fees associated with the trade pub fees: Vec, - pub total_fee: FeeDetail, + pub total_fee: Fee, pub source: AccountDetail, pub target: AccountDetail, - pub unit_price: UnitPrice, pub user_warnings: Vec, pub user_reference: String, - /// The currency of the source account + // The currency of the source account pub source_currency: String, - /// The currency of the target account + // The currency of the target account pub target_currency: String, - pub cancellation_reason: CancellationReason, - /// The id of the source account + // The id of the source account pub source_id: String, - /// The id of the target account + // The id of the target account pub target_id: String, pub exchange_rate: Balance, - /// Tax details for the trade + // Tax details for the trade pub tax_details: Vec, - pub trade_incentive_info: TradeIncentiveInfo, - pub total_fee_without_tax: FeeDetail, - pub fiat_denoted_total: Balance, } -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Serialize, Debug)] pub struct Fee { pub title: String, pub description: String, pub amount: Balance, pub label: String, - pub disclosure: Disclosure, + pub disclosure: Option, } -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Serialize, Debug)] pub struct Disclosure { pub title: String, pub description: String, pub link: Link, } -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Serialize, Debug)] pub struct Link { pub text: String, pub url: String, } -#[derive(Deserialize, Debug)] -pub struct FeeDetail { - pub title: String, - pub description: String, - pub amount: Balance, - pub label: String, - pub disclosure: Disclosure, -} - -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Serialize, Debug)] pub struct AccountDetail { pub r#type: String, pub network: String, pub ledger_account: LedgerAccount, } -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Serialize, Debug)] pub struct LedgerAccount { pub account_id: String, pub currency: String, pub owner: Owner, } -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Serialize, Debug)] pub struct Owner { pub id: String, pub uuid: String, @@ -102,20 +107,20 @@ pub struct Owner { pub r#type: String, } -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Serialize, Debug)] pub struct UnitPrice { pub target_to_fiat: PriceScale, pub target_to_source: PriceScale, pub source_to_fiat: PriceScale, } -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Serialize, Debug)] pub struct PriceScale { pub amount: Balance, pub scale: i32, } -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Serialize, Debug)] pub struct UserWarning { pub id: String, pub link: Link, @@ -124,14 +129,14 @@ pub struct UserWarning { pub message: String, } -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Serialize, Debug)] pub struct WarningContext { pub details: Vec, pub title: String, pub link_text: String, } -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Serialize, Debug)] pub struct CancellationReason { pub message: String, pub code: String, @@ -139,13 +144,13 @@ pub struct CancellationReason { pub error_cta: String, } -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Serialize, Debug)] pub struct TaxDetail { pub name: String, pub amount: Balance, } -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Serialize, Debug)] pub struct TradeIncentiveInfo { pub applied_incentive: bool, pub user_incentive_id: String, @@ -155,35 +160,104 @@ pub struct TradeIncentiveInfo { pub redeemed: bool, } +/// Trade incentive to waive trade fees. #[derive(Serialize, Deserialize, Debug)] pub struct TradeIncentiveMetadata { + /// The user incentive id. pub user_incentive_id: Option, + /// A promo code for waiving fees. pub code_val: Option, } -#[derive(Serialize, Deserialize, Debug)] -pub(crate) struct ConvertQuoteQuery { +/// Represents a request to create a convert quote. +#[serde_as] +#[derive(Serialize, Debug, Default)] +pub struct ConvertQuoteRequest { /// The currency of the account to convert from, e.g. USD - pub(crate) from_account: String, + pub from_account: String, /// The currency of the account to convert to, e.g. USDC - pub(crate) to_account: String, - /// The amount to convert in the currency of the from_account - pub(crate) amount: String, + pub to_account: String, + /// The amount to convert in the currency of the from_account. + #[serde_as(as = "DisplayFromStr")] + pub amount: f64, + /// Trade incentive to waive trade fees. #[serde(skip_serializing_if = "Option::is_none")] - pub(crate) trade_incentive_metadata: Option, + pub trade_incentive_metadata: Option, +} + +impl Request for ConvertQuoteRequest { + fn check(&self) -> CbResult<()> { + if self.from_account.is_empty() { + return Err(CbError::BadRequest("from_account is required".to_string())); + } else if self.to_account.is_empty() { + return Err(CbError::BadRequest("to_account is required".to_string())); + } else if self.amount <= 0.0 { + return Err(CbError::BadRequest( + "amount must be greater than 0".to_string(), + )); + } + Ok(()) + } +} + +impl ConvertQuoteRequest { + /// Creates a new instance of the ConvertQuoteRequest. + /// + /// # Arguments + /// + /// * `from_account` - The currency of the account to convert from, e.g. USD + /// * `to_account` - The currency of the account to convert to, e.g. USDC + /// * `amount` - The amount to convert in the currency of the from_account. + pub fn new(from_account: &str, to_account: &str, amount: f64) -> Self { + Self { + from_account: from_account.to_string(), + to_account: to_account.to_string(), + amount, + trade_incentive_metadata: None, + } + } + + /// Sets the trade incentive to waive trade fees. + pub fn trade_incentive_metadata(mut self, metadata: TradeIncentiveMetadata) -> Self { + self.trade_incentive_metadata = Some(metadata); + self + } } /// Represents parameters to obtain a currency conversion. +/// +/// # Required Parameters +/// +/// * `from_account` - The currency of the account to convert from, e.g. USD +/// * `to_account` - The currency of the account to convert to, e.g. USDC #[derive(Serialize, Default, Debug)] -pub(crate) struct ConvertQuery { +pub struct ConvertQuery { /// Originating account. - pub(crate) from_account: String, + pub from_account: String, /// Sending account. - pub(crate) to_account: String, + pub to_account: String, +} +impl Request for ConvertQuery { + fn check(&self) -> CbResult<()> { + if self.from_account.is_empty() { + return Err(CbError::BadRequest("from_account is required".to_string())); + } else if self.to_account.is_empty() { + return Err(CbError::BadRequest("to_account is required".to_string())); + } + Ok(()) + } } impl Query for ConvertQuery { - /// Converts the object into HTTP request parameters. + fn check(&self) -> CbResult<()> { + if self.from_account.is_empty() { + return Err(CbError::BadQuery("from_account is required".to_string())); + } else if self.to_account.is_empty() { + return Err(CbError::BadQuery("to_account is required".to_string())); + } + Ok(()) + } + fn to_query(&self) -> String { QueryBuilder::new() .push("from_account", &self.from_account) @@ -191,3 +265,44 @@ impl Query for ConvertQuery { .build() } } + +impl ConvertQuery { + /// Creates a new instance of the ConvertQuery. + /// + /// # Arguments + /// + /// * `from_account` - The currency of the account to convert from, e.g. USD + /// * `to_account` - The currency of the account to convert to, e.g. USDC + pub fn new(from_account: &str, to_account: &str) -> Self { + Self { + from_account: from_account.to_string(), + to_account: to_account.to_string(), + } + } + + /// Sets the originating account. + /// Note: This is a required field. + pub fn from_account(mut self, from_account: &str) -> Self { + self.from_account = from_account.to_string(); + self + } + + /// Sets the sending account. + /// Note: This is a required field. + pub fn to_account(mut self, to_account: &str) -> Self { + self.to_account = to_account.to_string(); + self + } +} + +/// Response from the convert API endpoint. +#[derive(Deserialize, Debug)] +pub(crate) struct TradeWrapper { + pub(crate) trade: Trade, +} + +impl From for Trade { + fn from(wrapper: TradeWrapper) -> Self { + wrapper.trade + } +} diff --git a/src/models/data.rs b/src/models/data.rs new file mode 100644 index 0000000..37fe43c --- /dev/null +++ b/src/models/data.rs @@ -0,0 +1,53 @@ +//! # Coinbase Advanced Data API +//! +//! `data` gives access to the Data API and the various endpoints associated with it. + +use core::fmt; + +use serde::{Deserialize, Serialize}; + +/// Various types of portfolios. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +#[serde(rename_all = "UPPERCASE")] +pub enum PortfolioType { + /// Undefined portfolio type. + Undefined, + /// Default portfolio type. + Default, + /// Consumer portfolio type. + Consumer, + /// Intx portfolio type. + Intx, +} + +impl fmt::Display for PortfolioType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.as_ref()) + } +} + +impl AsRef for PortfolioType { + fn as_ref(&self) -> &str { + match self { + PortfolioType::Undefined => "UNDEFINED", + PortfolioType::Default => "DEFAULT", + PortfolioType::Consumer => "CONSUMER", + PortfolioType::Intx => "INTX", + } + } +} + +/// KeyPermissions represents the permissions associated with an API key. +#[derive(Deserialize, Serialize, Debug, Clone)] +pub struct KeyPermissions { + ///Indicates whether the API key has view permissions. + pub can_view: bool, + /// Indicates whether the API key has trade permissions. + pub can_trade: bool, + /// Indicates whether the API key has deposit/withdrawal permissions. + pub can_transfer: bool, + /// The portfolio ID associated with the API key. + pub portfolio_uuid: String, + /// The type of portfolio. Possible values: [UNDEFINED, DEFAULT, CONSUMER, INTX] + pub portfolio_type: PortfolioType, +} diff --git a/src/models/fee.rs b/src/models/fee.rs index c134a2d..347e2f3 100644 --- a/src/models/fee.rs +++ b/src/models/fee.rs @@ -4,42 +4,50 @@ //! Currently the only endpoint available is the Transaction Summary endpoint. use serde::{Deserialize, Serialize}; +use serde_with::{serde_as, DisplayFromStr}; +use crate::errors::CbError; use crate::traits::Query; -use crate::utils::{deserialize_numeric, QueryBuilder}; +use crate::types::CbResult; +use crate::utils::QueryBuilder; + +use super::product::ProductType; /// Pricing tier for user, determined by notional (USD) volume. +#[serde_as] #[derive(Serialize, Deserialize, Debug, Clone)] pub struct FeeTier { /// Current fee teir for the user. pub pricing_tier: String, /// Lower bound (inclusive) of pricing tier in notional volume. - #[serde(deserialize_with = "deserialize_numeric")] + #[serde_as(as = "DisplayFromStr")] pub usd_from: u32, /// Upper bound (exclusive) of pricing tier in notional volume. - #[serde(deserialize_with = "deserialize_numeric")] + #[serde_as(as = "DisplayFromStr")] pub usd_to: u32, /// Taker fee rate, applied if the order takes liquidity. - #[serde(deserialize_with = "deserialize_numeric")] + #[serde_as(as = "DisplayFromStr")] pub taker_fee_rate: f64, /// Maker fee rate, applied if the order creates liquidity. - #[serde(deserialize_with = "deserialize_numeric")] + #[serde_as(as = "DisplayFromStr")] pub maker_fee_rate: f64, } /// Represents a decimal number with precision. +#[serde_as] #[derive(Serialize, Deserialize, Debug, Clone)] pub struct MarginRate { /// Value of the margin rate. - #[serde(deserialize_with = "deserialize_numeric")] + #[serde_as(as = "DisplayFromStr")] pub value: f64, } /// Represents a tax amount. +#[serde_as] #[derive(Serialize, Deserialize, Debug, Clone)] pub struct Tax { /// Amount of tax. - #[serde(deserialize_with = "deserialize_numeric")] + #[serde_as(as = "DisplayFromStr")] pub value: f64, /// Type of tax. Possible values: [INCLUSIVE, EXCLUSIVE] pub r#type: String, @@ -70,25 +78,39 @@ pub struct TransactionSummary { /// Represents parameters that are optional for transaction summary API request. #[derive(Serialize, Default, Debug)] -pub struct TransactionSummaryQuery { - /// Start date for the summary. - pub start_date: Option, - /// End date for the summary. - pub end_date: Option, - /// String of the users native currency, default is USD. - pub user_native_currency: Option, +pub struct FeeTransactionSummaryQuery { /// Type of products to return. Valid options: SPOT or FUTURE - pub product_type: Option, + pub product_type: Option, } -impl Query for TransactionSummaryQuery { - /// Converts the object into HTTP request parameters. +impl Query for FeeTransactionSummaryQuery { + fn check(&self) -> CbResult<()> { + if let Some(product_type) = &self.product_type { + if *product_type == ProductType::Unknown { + return Err(CbError::BadQuery( + "product_type cannot be unknown".to_string(), + )); + } + } + Ok(()) + } + fn to_query(&self) -> String { QueryBuilder::new() - .push_optional("start_date", &self.start_date) - .push_optional("end_date", &self.end_date) - .push_optional("user_native_currency", &self.user_native_currency) .push_optional("product_type", &self.product_type) .build() } } + +impl FeeTransactionSummaryQuery { + /// Creates a new instance of the query. + pub fn new() -> Self { + Self::default() + } + + /// Sets the product type for the query. + pub fn product_type(mut self, product_type: ProductType) -> Self { + self.product_type = Some(product_type); + self + } +} diff --git a/src/models/mod.rs b/src/models/mod.rs index 6f51a16..379bce0 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -1,7 +1,11 @@ pub mod account; pub mod convert; +pub mod data; pub mod fee; pub mod order; +pub mod payment; +pub mod portfolio; pub mod product; -pub mod util; +pub mod public; +pub mod shared; pub mod websocket; diff --git a/src/models/order.rs b/src/models/order.rs deleted file mode 100644 index 1341a46..0000000 --- a/src/models/order.rs +++ /dev/null @@ -1,525 +0,0 @@ -//! # Coinbase Advanced Order API -//! -//! `order` gives access to the Order API and the various endpoints associated with it. -//! These allow you to obtain past created orders, create new orders, and cancel orders. - -use std::fmt; - -use serde::{Deserialize, Serialize}; - -use crate::traits::Query; -use crate::utils::{deserialize_numeric, QueryBuilder}; - -/// Various order types. -#[derive(Serialize, Deserialize, Debug, Clone)] -pub enum OrderType { - /// A Market order. - Market, - /// A Limit order. - Limit, - /// A stop order is an order that becomes a market order when triggered. - Stop, - /// A stop order is a limit order that doesn't go on the book until it hits the stop price. - StopLimit, -} - -impl fmt::Display for OrderType { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - OrderType::Market => write!(f, "MARKET"), - OrderType::Limit => write!(f, "LIMIT"), - OrderType::Stop => write!(f, "STOP"), - OrderType::StopLimit => write!(f, "STOPLIMIT"), - } - } -} - -impl AsRef for OrderType { - fn as_ref(&self) -> &str { - match self { - OrderType::Market => "MARKET", - OrderType::Limit => "LIMIT", - OrderType::Stop => "STOP", - OrderType::StopLimit => "STOPLIMIT", - } - } -} - -/// Order side, BUY or SELL. -#[derive(Serialize, Deserialize, Debug, Clone)] -pub enum OrderSide { - /// Buying a product. - Buy, - /// Selling a product. - Sell, -} - -impl AsRef for OrderSide { - fn as_ref(&self) -> &str { - match self { - OrderSide::Buy => "BUY", - OrderSide::Sell => "SELL", - } - } -} - -impl fmt::Display for OrderSide { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - OrderSide::Buy => write!(f, "BUY"), - OrderSide::Sell => write!(f, "SELL"), - } - } -} - -/// Order status, OPEN, CANCELLED, and EXPIRED. -#[derive(Serialize, Deserialize, Debug, Clone)] -pub enum OrderStatus { - /// Implies the order is still available and not closed. - Open, - /// Order was closed by cancellation. - Cancelled, - /// Order was closed by expiration. - Expired, -} - -impl fmt::Display for OrderStatus { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - OrderStatus::Open => write!(f, "OPEN"), - OrderStatus::Cancelled => write!(f, "CANCELLED"), - OrderStatus::Expired => write!(f, "EXPIRED"), - } - } -} - -impl AsRef for OrderStatus { - fn as_ref(&self) -> &str { - match self { - OrderStatus::Open => "OPEN", - OrderStatus::Cancelled => "CANCELLED", - OrderStatus::Expired => "EXPIRED", - } - } -} - -/// Order updates for a user from a websocket. -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct OrderUpdate { - /// Type of the update. - pub r#type: String, - /// Client Order ID (Normally a UUID) - pub client_order_id: String, - #[serde(deserialize_with = "deserialize_numeric")] - pub cumulative_quantity: f64, - #[serde(deserialize_with = "deserialize_numeric")] - pub leaves_quantity: f64, - /// Average price for the order. - #[serde(deserialize_with = "deserialize_numeric")] - pub avg_price: f64, - /// Total fees for the order. - #[serde(deserialize_with = "deserialize_numeric")] - pub total_fees: f64, - /// Status of the order. - pub status: String, - /// Product ID. - pub product_id: String, - /// Date-time when the order was created. - pub creation_time: String, - /// BUY or SELL. - pub order_side: String, - /// Type of the order. - pub order_type: String, -} - -/// Market Immediate or Cancel. -#[derive(Serialize, Debug)] -pub(crate) struct MarketIoc { - /// Amount of quote currency to spend on order. Required for BUY orders. - pub(crate) quote_size: Option, - /// Amount of base currency to spend on order. Required for SELL orders. - pub(crate) base_size: Option, -} - -/// Limit Good til Cancelled. -#[derive(Serialize, Debug)] -pub(crate) struct LimitGtc { - /// Amount of base currency to spend on order. - pub(crate) base_size: String, - /// Ceiling price for which the order should get filled. - pub(crate) limit_price: String, - /// Post only limit order. - pub(crate) post_only: bool, -} - -/// Limit Good til Time (Date). -#[derive(Serialize, Debug)] -pub(crate) struct LimitGtd { - /// Amount of base currency to spend on order. - pub(crate) base_size: String, - /// Ceiling price for which the order should get filled. - pub(crate) limit_price: String, - /// Time at which the order should be cancelled if it's not filled. - pub(crate) end_time: String, - /// Post only limit order. - pub(crate) post_only: bool, -} - -/// Stop Limit Good til Cancelled. -#[derive(Serialize, Debug)] -pub(crate) struct StopLimitGtc { - /// Amount of base currency to spend on order. - pub(crate) base_size: String, - /// Ceiling price for which the order should get filled. - pub(crate) limit_price: String, - /// Price at which the order should trigger - if stop direction is Up, then the order will trigger when the last trade price goes above this, otherwise order will trigger when last trade price goes below this price. - pub(crate) stop_price: String, - /// Possible values: [UNKNOWN_STOP_DIRECTION, STOP_DIRECTION_STOP_UP, STOP_DIRECTION_STOP_DOWN] - pub(crate) stop_direction: String, -} - -/// Stop Limit Good til Time (Date). -#[derive(Serialize, Debug)] -pub(crate) struct StopLimitGtd { - /// Amount of base currency to spend on order. - pub(crate) base_size: String, - /// Ceiling price for which the order should get filled. - pub(crate) limit_price: String, - /// Price at which the order should trigger - if stop direction is Up, then the order will trigger when the last trade price goes above this, otherwise order will trigger when last trade price goes below this price. - pub(crate) stop_price: String, - /// Time at which the order should be cancelled if it's not filled. - pub(crate) end_time: String, - /// Possible values: [UNKNOWN_STOP_DIRECTION, STOP_DIRECTION_STOP_UP, STOP_DIRECTION_STOP_DOWN] - pub(crate) stop_direction: String, -} - -/// Create Order Configuration. -#[derive(Serialize, Default, Debug)] -pub(crate) struct OrderConfiguration { - /// Market Order - #[serde(skip_serializing_if = "Option::is_none")] - pub(crate) market_market_ioc: Option, - /// Limit Order, Good til Cancelled - #[serde(skip_serializing_if = "Option::is_none")] - pub(crate) limit_limit_gtc: Option, - /// Limit Order, Good til Date (time) - #[serde(skip_serializing_if = "Option::is_none")] - pub(crate) limit_limit_gtd: Option, - /// Stop Limit Order, Good til Cancelled - #[serde(skip_serializing_if = "Option::is_none")] - pub(crate) stop_limit_stop_limit_gtc: Option, - /// Stop Limit Order, Good til Date (time) - #[serde(skip_serializing_if = "Option::is_none")] - pub(crate) stop_limit_stop_limit_gtd: Option, -} - -/// Represents an order created to BUY or SELL. -#[derive(Serialize, Debug)] -pub(crate) struct CreateOrder { - /// Client Order ID (UUID) - pub(crate) client_order_id: String, - /// Product ID (pair) - pub(crate) product_id: String, - /// Order Side: BUY or SELL. - pub(crate) side: String, - /// Configuration for the order. - pub(crate) order_configuration: OrderConfiguration, -} - -/// Represents an order to be edited. -#[derive(Serialize, Debug)] -pub(crate) struct EditOrder { - /// ID of the order to edit. - pub(crate) order_id: String, - /// New price for order. - pub(crate) price: String, - /// New size for order. - pub(crate) size: String, -} - -/// Represents a vector of orders IDs to cancel. -#[derive(Serialize, Debug)] -pub(crate) struct CancelOrders { - /// Vector of Order IDs to cancel. - pub(crate) order_ids: Vec, -} - -/// Represents a single edit entry in the edit history of an order. -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct EditHistory { - /// The price associated with the edit. - pub price: String, - /// The size associated with the edit. - pub size: String, - /// The timestamp when the edit was accepted. - pub replace_accept_timestamp: String, -} - -/// Represents an Order received from the API. -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct Order { - /// The unique id for this order. - pub order_id: String, - /// Client specified ID of order. - pub client_order_id: String, - /// The product this order was created for e.g. 'BTC-USD' - pub product_id: String, - /// The id of the User owning this Order. - pub user_id: String, - /// Possible values: [UNKNOWN_ORDER_SIDE, BUY, SELL] - pub side: String, - /// Possible values: [OPEN, FILLED, CANCELLED, EXPIRED, FAILED, UNKNOWN_ORDER_STATUS] - pub status: String, - /// Possible values: [UNKNOWN_TIME_IN_FORCE, GOOD_UNTIL_DATE_TIME, GOOD_UNTIL_CANCELLED, IMMEDIATE_OR_CANCEL, FILL_OR_KILL] - pub time_in_force: String, - /// Timestamp for when the order was created. - pub created_time: String, - /// The percent of total order amount that has been filled. - #[serde(deserialize_with = "deserialize_numeric")] - pub completion_percentage: f64, - /// The portion (in base currency) of total order amount that has been filled. - #[serde(deserialize_with = "deserialize_numeric")] - pub filled_size: f64, - /// The average of all prices of fills for this order. - #[serde(deserialize_with = "deserialize_numeric")] - pub average_filled_price: f64, - /// Commission amount. - #[serde(deserialize_with = "deserialize_numeric")] - pub fee: f64, - /// Number of fills that have been posted for this order. - #[serde(deserialize_with = "deserialize_numeric")] - pub number_of_fills: u32, - /// The portion (in quote current) of total order amount that has been filled. - #[serde(deserialize_with = "deserialize_numeric")] - pub filled_value: f64, - /// Whether a cancel request has been initiated for the order, and not yet completed. - pub pending_cancel: bool, - /// Whether the order was placed with quote currency/ - pub size_in_quote: bool, - /// The total fees for the order. - #[serde(deserialize_with = "deserialize_numeric")] - pub total_fees: f64, - /// Whether the order size includes fees. - pub size_inclusive_of_fees: bool, - /// Derived field: filled_value + total_fees for buy orders and filled_value - total_fees for sell orders. - #[serde(deserialize_with = "deserialize_numeric")] - pub total_value_after_fees: f64, - /// Possible values: \[UNKNOWN_TRIGGER_STATUS, INVALID_ORDER_TYPE, STOP_PENDING, STOP_TRIGGERED\] - pub trigger_status: String, - /// Possible values: \[UNKNOWN_ORDER_TYPE, MARKET, LIMIT, STOP, STOP_LIMIT\] - pub order_type: String, - /// Possible values: \[REJECT_REASON_UNSPECIFIED\] - pub reject_reason: String, - /// True if the order is fully filled, false otherwise. - pub settled: bool, - /// Possible values: [SPOT, FUTURE] - pub product_type: String, - /// Message stating why the order was rejected. - pub reject_message: String, - /// Message stating why the order was canceled. - pub cancel_message: String, - /// An array of the latest 5 edits per order. - pub edit_history: Vec, -} - -/// Represents a fill received from the API. -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct Fill { - /// Unique identifier for the fill. - pub entry_id: String, - /// Id of the fill -- unique for all `FILL` trade_types but not unique for adjusted fills. - pub trade_id: String, - /// Id of the order the fill belongs to. - pub order_id: String, - /// Time at which this fill was completed. - pub trade_time: String, - /// String denoting what type of fill this is. Regular fills have the value `FILL`. - /// Adjusted fills have possible values `REVERSAL`, `CORRECTION`, `SYNTHETIC`. - pub trade_type: String, - /// Price the fill was posted at. - #[serde(deserialize_with = "deserialize_numeric")] - pub price: f64, - /// Amount of order that was transacted at this fill. - #[serde(deserialize_with = "deserialize_numeric")] - pub size: f64, - /// Fee amount for fill. - #[serde(deserialize_with = "deserialize_numeric")] - pub commission: f64, - /// The product this order was created for. - pub product_id: String, - /// Time at which this fill was posted. - pub sequence_timestamp: String, - /// Possible values: [UNKNOWN_LIQUIDITY_INDICATOR, MAKER, TAKER] - pub liquidity_indicator: String, - /// Whether the order was placed with quote currency. - pub size_in_quote: bool, - /// User that placed the order the fill belongs to. - pub user_id: String, - /// Possible values: [UNKNOWN_ORDER_SIDE, BUY, SELL] - pub side: String, -} - -/// Represents a list of orders received from the API. -#[derive(Deserialize, Debug)] -pub struct ListedOrders { - /// Vector of orders obtained. - pub orders: Vec, - /// If there are additional orders. - pub has_next: bool, - /// Cursor used to pull more orders. - pub cursor: String, -} - -/// Represents a list of fills received from the API. -#[derive(Deserialize, Debug)] -pub struct ListedFills { - /// Vector of filled orders. - pub orders: Vec, - /// Cursor used to pull more fills. - pub cursor: String, -} - -/// Represents a create order response from the API. -#[derive(Deserialize, Debug)] -pub struct OrderResponse { - /// Whether or not the order completed correctly. - pub success: bool, - /// Reason the order failed, if it did. - pub failure_reason: String, - /// Order Id of the order created. - pub order_id: String, -} - -/// Represents a cancel order response from the API. -#[derive(Deserialize, Debug)] -pub(crate) struct CancelOrdersResponse { - /// Vector of orders cancelled. - pub(crate) results: Vec, -} - -/// Represents an order when obtaining a single order from the API. -#[derive(Deserialize, Debug)] -pub(crate) struct OrderStatusResponse { - /// Order received. - pub(crate) order: Order, -} - -/// Represents an order when obtaining a single order from the API. -#[derive(Deserialize, Debug)] -pub struct EditOrderResponse { - /// Whether or not the order edit succeeded. - pub success: bool, - /// Errors associated with the changes. - pub errors: Vec, -} - -/// Errors associated with the changes. -#[derive(Deserialize, Debug)] -pub struct EditOrderErrors { - /// Reason the edit failed. - pub edit_failure_reason: Option, - /// Reason the preview failed. - pub preview_failure_reason: Option, -} - -/// Response from a preview edit order. -#[derive(Deserialize, Debug)] -pub struct PreviewEditOrderResponse { - /// Contains reasons for failure in the edit or preview edit operation. - pub errors: Vec, - /// The amount of slippage in the order. - #[serde(deserialize_with = "deserialize_numeric")] - pub slippage: f64, - /// The total value of the order. - #[serde(deserialize_with = "deserialize_numeric")] - pub order_total: f64, - /// The total commission for the order. - #[serde(deserialize_with = "deserialize_numeric")] - pub commission_total: f64, - /// The size of the quote currency in the order. - #[serde(deserialize_with = "deserialize_numeric")] - pub quote_size: f64, - /// The size of the base currency in the order. - #[serde(deserialize_with = "deserialize_numeric")] - pub base_size: f64, - /// The best bid price at the time of the order. - #[serde(deserialize_with = "deserialize_numeric")] - pub best_bid: f64, - /// The best ask price at the time of the order. - #[serde(deserialize_with = "deserialize_numeric")] - pub best_ask: f64, - /// The average price at which the order was filled. - #[serde(deserialize_with = "deserialize_numeric")] - pub average_filled_price: f64, -} - -/// Represents parameters that are optional for List Orders API request. -#[derive(Serialize, Default, Debug)] -pub struct ListOrdersQuery { - /// Optional string of the product ID. Defaults to null, or fetch for all products. - pub product_id: Option, - /// Note: Cannot pair OPEN orders with other order types. - pub order_status: Option>, - /// A pagination limit with no default set. If has_next is true, additional orders are available to be fetched with pagination; also the cursor value in the response can be passed as cursor parameter in the subsequent request. - pub limit: Option, - /// Start date to fetch orders from, inclusive. - pub start_date: Option, - /// An optional end date for the query window, exclusive. If provided only orders with creation time before this date will be returned. - pub end_date: Option, - /// Type of orders to return. Default is to return all order types. - pub order_type: Option, - /// Only orders matching this side are returned. Default is to return all sides. - pub order_side: Option, - /// Cursor used for pagination. When provided, the response returns responses after this cursor. - pub cursor: Option, - /// Only orders matching this product type are returned. Default is to return all product types. Valid options are SPOT or FUTURE. - pub product_type: Option, -} - -impl Query for ListOrdersQuery { - /// Converts the object into HTTP request parameters. - fn to_query(&self) -> String { - QueryBuilder::new() - .push_optional("product_id", &self.product_id) - .with_optional_vec("order_status", &self.order_status) - .push_u32_optional("limit", self.limit) - .push_optional("start_date", &self.start_date) - .push_optional("end_date", &self.end_date) - .push_optional("order_type", &self.order_type) - .push_optional("order_side", &self.order_side) - .push_optional("cursor", &self.cursor) - .push_optional("product_type", &self.product_type) - .build() - } -} - -/// Represents parameters that are optional for List Fills API request. -#[derive(Serialize, Default, Debug)] -pub struct ListFillsQuery { - /// ID of the order. - pub order_id: Option, - /// The ID of the product this order was created for. - pub product_id: Option, - /// Start date. Only fills with a trade time at or after this start date are returned. - pub start_sequence_timestamp: Option, - /// End date. Only fills with a trade time before this start date are returned. - pub end_sequence_timestamp: Option, - /// Maximum number of fills to return in response. Defaults to 100. - pub limit: Option, - /// Cursor used for pagination. When provided, the response returns responses after this cursor. - pub cursor: Option, -} - -impl Query for ListFillsQuery { - /// Converts the object into HTTP request parameters. - fn to_query(&self) -> String { - QueryBuilder::new() - .push_optional("order_id", &self.order_id) - .push_optional("product_id", &self.product_id) - .push_optional("start_sequence_timestamp", &self.start_sequence_timestamp) - .push_optional("end_sequence_timestamp", &self.end_sequence_timestamp) - .push_u32_optional("limit", self.limit) - .push_optional("cursor", &self.cursor) - .build() - } -} diff --git a/src/models/order/builders.rs b/src/models/order/builders.rs new file mode 100644 index 0000000..3ada4c2 --- /dev/null +++ b/src/models/order/builders.rs @@ -0,0 +1,501 @@ +//! # Coinbase Advanced Order API +//! +//! `order/builders` provides a builder pattern for creating `CreateOrder` instances. + +use crate::errors::CbError; +use crate::types::CbResult; + +use super::{ + LimitGtc, LimitGtd, MarketIoc, OrderConfiguration, OrderCreateRequest, OrderSide, OrderType, + StopDirection, StopLimitGtc, StopLimitGtd, TimeInForce, +}; + +/// A builder for creating `OrderCreateRequest` instances. +/// +/// This builder provides a fluent interface to construct an order by specifying the product, +/// side, order type, time-in-force, and other optional parameters. It ensures that all required +/// parameters are set before building the final order, and helps prevent invalid configurations. +pub struct OrderCreateBuilder { + product_id: String, + side: OrderSide, + is_preview: bool, + order_type: Option, + time_in_force: Option, + base_size: Option, + quote_size: Option, + limit_price: Option, + stop_price: Option, + stop_trigger_price: Option, + end_time: Option, + post_only: Option, + stop_direction: Option, + client_order_id: Option, +} + +impl OrderCreateBuilder { + /// Creates a new `OrderCreateBuilder` instance. + /// + /// # Arguments + /// + /// * `product_id` - The trading pair (e.g., "BTC-USD") for which the order will be created. + /// This must be a valid product ID supported by the exchange. + /// * `side` - The side of the order, either `BUY` or `SELL`. + /// + /// # Example + /// + /// ```rust + /// let builder = OrderCreateBuilder::new("BTC-USD", &OrderSide::Buy); + /// ``` + pub fn new(product_id: &str, side: &OrderSide) -> Self { + Self { + product_id: product_id.to_string(), + side: side.clone(), + is_preview: false, + order_type: None, + time_in_force: None, + base_size: None, + quote_size: None, + limit_price: None, + stop_price: None, + stop_trigger_price: None, + end_time: None, + post_only: None, + stop_direction: None, + client_order_id: None, + } + } + + /// Sets the order type for the order. + /// + /// The order type determines the kind of order to be placed, such as `Market`, `Limit`, + /// `StopLimit`, or `Trigger`. This setting affects which additional parameters are required. + /// + /// # Arguments + /// + /// * `order_type` - An `OrderType` enum variant specifying the type of order. + /// + /// # Example + /// + /// ```rust + /// builder.order_type(OrderType::Limit); + /// ``` + pub fn order_type(mut self, order_type: OrderType) -> Self { + self.order_type = Some(order_type); + self + } + + /// Sets the time-in-force policy for the order. + /// + /// Time-in-force specifies how long an order remains active before it is executed or expires. + /// Common values include: + /// + /// - `GTC` (Good 'til Cancelled): The order remains active until it is filled or canceled. + /// - `GTD` (Good 'til Date): The order remains active until a specified date and time. + /// - `IOC` (Immediate or Cancel): The order must be executed immediately; otherwise, any unfilled portion is canceled. + /// - `FOK` (Fill or Kill): The order must be filled entirely immediately; otherwise, it is canceled. + /// + /// # Arguments + /// + /// * `tif` - A `TimeInForce` enum variant specifying the time-in-force policy. + /// + /// # Example + /// + /// ```rust + /// builder.time_in_force(TimeInForce::GTC); + /// ``` + pub fn time_in_force(mut self, tif: TimeInForce) -> Self { + self.time_in_force = Some(tif); + self + } + + /// Sets the base size for the order. + /// + /// The base size is the amount of the base currency to buy or sell. For example, in the "BTC-USD" + /// trading pair, BTC is the base currency. + /// + /// # Arguments + /// + /// * `base_size` - The quantity of the base currency to trade. + /// + /// # Note + /// + /// This parameter is required for most order types except when specifying `quote_size` for certain market orders. + /// + /// # Example + /// + /// ```rust + /// builder.base_size(0.5); // Buying or selling 0.5 BTC + /// ``` + pub fn base_size(mut self, base_size: f64) -> Self { + self.base_size = Some(base_size); + self + } + + /// Sets the quote size for the order. + /// + /// The quote size is the amount of the quote currency to spend (for buys) or receive (for sells). + /// For example, in the "BTC-USD" trading pair, USD is the quote currency. + /// + /// # Arguments + /// + /// * `quote_size` - The amount of the quote currency to use in the order. + /// + /// # Note + /// + /// - For market orders, you can specify either `base_size` or `quote_size`. + /// - `quote_size` is not typically used with limit orders. + /// + /// # Example + /// + /// ```rust + /// builder.quote_size(1000.0); // Spending $1000 USD to buy BTC + /// ``` + pub fn quote_size(mut self, quote_size: f64) -> Self { + self.quote_size = Some(quote_size); + self + } + + /// Sets the limit price for the order. + /// + /// The limit price is the worst price at which the order will be executed: + /// + /// - For **buy** orders, it's the maximum price you're willing to pay per unit of the base currency. + /// - For **sell** orders, it's the minimum price you're willing to accept per unit of the base currency. + /// + /// # Arguments + /// + /// * `limit_price` - The limit price in terms of the quote currency. + /// + /// # Note + /// + /// This parameter is required for limit orders and stop limit orders. + /// + /// # Example + /// + /// ```rust + /// builder.limit_price(50000.0); // Limit price of $50,000 per BTC + /// ``` + pub fn limit_price(mut self, limit_price: f64) -> Self { + self.limit_price = Some(limit_price); + self + } + + /// Sets the stop price for the order. + /// + /// The stop price is the price at which a stop order is triggered and becomes active. + /// When the market reaches the stop price, the stop order is converted into a regular order (e.g., a limit order). + /// + /// # Arguments + /// + /// * `stop_price` - The price at which the stop order is triggered. + /// + /// # Note + /// + /// - Required for stop limit orders. + /// - The `stop_direction` must also be specified. + /// + /// # Example + /// + /// ```rust + /// builder.stop_price(48000.0); // Trigger the order when the price reaches $48,000 + /// ``` + pub fn stop_price(mut self, stop_price: f64) -> Self { + self.stop_price = Some(stop_price); + self + } + + /// Sets the stop trigger price for a trigger bracket order. + /// + /// The stop trigger price is the price level at which the position will be exited. + /// When the market reaches this price, a stop limit order is automatically placed. + /// + /// # Arguments + /// + /// * `stop_trigger_price` - The price level to trigger the exit order. + /// + /// # Note + /// + /// - Required for trigger bracket orders. + /// - The exit order typically has a limit price adjusted based on the side of the original order. + /// + /// # Example + /// + /// ```rust + /// builder.stop_trigger_price(47000.0); // Exit the position when the price reaches $47,000 + /// ``` + pub fn stop_trigger_price(mut self, stop_trigger_price: f64) -> Self { + self.stop_trigger_price = Some(stop_trigger_price); + self + } + + /// Sets the end time for the order. + /// + /// The end time is the timestamp at which the order will be automatically canceled if it has not been filled. + /// It is used with Good 'til Date (GTD) orders. + /// + /// # Arguments + /// + /// * `end_time` - The end time as an RFC3339 formatted timestamp (e.g., "2024-12-31T23:59:59Z"). + /// + /// # Note + /// + /// This parameter is required for orders with a time-in-force of `GTD`. + /// + /// # Example + /// + /// ```rust + /// builder.end_time("2024-12-31T23:59:59Z"); + /// ``` + pub fn end_time(mut self, end_time: &str) -> Self { + self.end_time = Some(end_time.to_string()); + self + } + + /// Sets the post-only flag for the order. + /// + /// When `post_only` is set to `true`, the order will only be posted to the order book if it does not + /// immediately match with an existing order. This ensures that the order will be a maker order, not a taker. + /// + /// # Arguments + /// + /// * `post_only` - A boolean indicating whether to enable post-only mode. + /// + /// # Note + /// + /// - Applicable to limit orders. + /// - If an order would be immediately matched (taking liquidity), it will be rejected if `post_only` is `true`. + /// + /// # Example + /// + /// ```rust + /// builder.post_only(true); + /// ``` + pub fn post_only(mut self, post_only: bool) -> Self { + self.post_only = Some(post_only); + self + } + + /// Sets the stop direction for a stop order. + /// + /// The stop direction determines whether the stop order is triggered when the market price moves up or down: + /// + /// - `StopUp`: The order triggers when the last trade price **rises** to or above the stop price. + /// - `StopDown`: The order triggers when the last trade price **falls** to or below the stop price. + /// + /// # Arguments + /// + /// * `stop_direction` - A `StopDirection` enum variant specifying the trigger direction. + /// + /// # Note + /// + /// Required for stop limit orders. + /// + /// # Example + /// + /// ```rust + /// builder.stop_direction(StopDirection::StopUp); + /// ``` + pub fn stop_direction(mut self, stop_direction: StopDirection) -> Self { + self.stop_direction = Some(stop_direction); + self + } + + /// Sets the client-defined order ID. + /// + /// The `client_order_id` is a unique identifier supplied by the client to identify the order. + /// If not provided, a random UUID will be generated. This can be useful for tracking orders in your system. + /// + /// # Arguments + /// + /// * `client_order_id` - A string representing the client-defined order ID. + /// + /// # Note + /// + /// - Must be unique to prevent conflicts. + /// - Useful for idempotency and tracking purposes. + /// + /// # Example + /// + /// ```rust + /// builder.client_order_id("my-custom-order-id-123"); + /// ``` + pub fn client_order_id(mut self, client_order_id: &str) -> Self { + self.client_order_id = Some(client_order_id.to_string()); + self + } + + /// Sets whether the order is a preview order. This will skip serializing the `client_order_id`. + /// + /// # Arguments + /// + /// * `is_preview` - A boolean indicating if it is a preview or not. + /// + /// # Note + /// + /// - By default, preview is false. + /// + /// # Example + /// + /// ```rust + /// builder.preview(true); + /// ``` + pub fn preview(mut self, is_preview: bool) -> Self { + self.is_preview = is_preview; + self + } + + /// Builds the `OrderCreateRequest` object based on the provided parameters. + /// + /// This method validates that all required parameters have been set according to the + /// specified `order_type` and `time_in_force`. If any required parameters are missing or + /// invalid, it returns an error. + /// + /// # Returns + /// + /// * `CbResult` - A result containing the `CreateOrder` object if successful, + /// or a `CbError` if validation fails. + /// + /// # Errors + /// + /// Returns `CbError::BadParse` if required parameters are missing or if the combination + /// of `order_type` and `time_in_force` is unsupported. + /// + /// # Example + /// + /// ```rust + /// let create_order = builder.build()?; + /// ``` + pub fn build(self) -> CbResult { + if self.side == OrderSide::Unknown { + return Err(CbError::BadParse( + "Order side cannot be unknown.".to_string(), + )); + } + + // Validate required fields based on order type and time-in-force. + let order_configuration = match (self.order_type, self.time_in_force) { + (Some(OrderType::Market), Some(TimeInForce::ImmediateOrCancel)) => { + // Ensure required parameters are set + if self.base_size.is_none() && self.quote_size.is_none() { + return Err(CbError::BadParse( + "Either base_size or quote_size must be provided for Market IOC orders" + .to_string(), + )); + } + + Ok(OrderConfiguration::MarketIoc(MarketIoc { + base_size: self.base_size, + quote_size: self.quote_size, + })) + } + (Some(OrderType::Limit), Some(TimeInForce::GoodUntilCancelled)) => { + let base_size = self.base_size.ok_or_else(|| { + CbError::BadParse("base_size is required for Limit GTC orders".to_string()) + })?; + let limit_price = self.limit_price.ok_or_else(|| { + CbError::BadParse("limit_price is required for Limit GTC orders".to_string()) + })?; + + Ok(OrderConfiguration::LimitGtc(LimitGtc { + base_size, + limit_price, + post_only: self.post_only.unwrap_or(false), + })) + } + (Some(OrderType::Limit), Some(TimeInForce::GoodUntilDate)) => { + let base_size = self.base_size.ok_or_else(|| { + CbError::BadParse("base_size is required for Limit GTD orders".to_string()) + })?; + let limit_price = self.limit_price.ok_or_else(|| { + CbError::BadParse("limit_price is required for Limit GTD orders".to_string()) + })?; + let end_time = self.end_time.ok_or_else(|| { + CbError::BadParse("end_time is required for Limit GTD orders".to_string()) + })?; + + Ok(OrderConfiguration::LimitGtd(LimitGtd { + base_size, + limit_price, + end_time, + post_only: self.post_only.unwrap_or(false), + })) + } + (Some(OrderType::StopLimit), Some(TimeInForce::GoodUntilCancelled)) => { + let base_size = self.base_size.ok_or_else(|| { + CbError::BadParse("base_size is required for Stop Limit GTC orders".to_string()) + })?; + let limit_price = self.limit_price.ok_or_else(|| { + CbError::BadParse( + "limit_price is required for Stop Limit GTC orders".to_string(), + ) + })?; + let stop_price = self.stop_price.ok_or_else(|| { + CbError::BadParse( + "stop_price is required for Stop Limit GTC orders".to_string(), + ) + })?; + let stop_direction = self.stop_direction.ok_or_else(|| { + CbError::BadParse( + "stop_direction is required for Stop Limit GTC orders".to_string(), + ) + })?; + + Ok(OrderConfiguration::StopLimitGtc(StopLimitGtc { + base_size, + limit_price, + stop_price, + stop_direction, + })) + } + (Some(OrderType::StopLimit), Some(TimeInForce::GoodUntilDate)) => { + let base_size = self.base_size.ok_or_else(|| { + CbError::BadParse("base_size is required for Stop Limit GTD orders".to_string()) + })?; + let limit_price = self.limit_price.ok_or_else(|| { + CbError::BadParse( + "limit_price is required for Stop Limit GTD orders".to_string(), + ) + })?; + let stop_price = self.stop_price.ok_or_else(|| { + CbError::BadParse( + "stop_price is required for Stop Limit GTD orders".to_string(), + ) + })?; + let stop_direction = self.stop_direction.ok_or_else(|| { + CbError::BadParse( + "stop_direction is required for Stop Limit GTD orders".to_string(), + ) + })?; + let end_time = self.end_time.ok_or_else(|| { + CbError::BadParse("end_time is required for Stop Limit GTD orders".to_string()) + })?; + + Ok(OrderConfiguration::StopLimitGtd(StopLimitGtd { + base_size, + limit_price, + stop_price, + stop_direction, + end_time, + })) + } + _ => Err(CbError::BadParse( + "Invalid or unsupported combination of order_type and time_in_force".to_string(), + )), + }?; + + let client_order_id = if self.is_preview { + "".to_string() + } else { + self.client_order_id + .unwrap_or_else(|| uuid::Uuid::new_v4().to_string()) + }; + + Ok(OrderCreateRequest { + client_order_id, + product_id: self.product_id, + side: self.side, + is_preview: self.is_preview, + order_configuration, + }) + } +} diff --git a/src/models/order/enums.rs b/src/models/order/enums.rs new file mode 100644 index 0000000..4de9ae0 --- /dev/null +++ b/src/models/order/enums.rs @@ -0,0 +1,314 @@ +//! # Coinbase Advanced Order API +//! +//! `order/enums` is the module containing the enums for the different order types and configurations. + +use std::fmt; + +use serde::{Deserialize, Serialize}; + +use super::{ + LimitFok, LimitGtc, LimitGtd, MarketIoc, SorLimitIoc, StopLimitGtc, StopLimitGtd, + TriggerBracketGtc, TriggerBracketGtd, +}; + +/// Various order types. +#[derive(Serialize, Debug, Clone, PartialEq)] +pub enum OrderType { + /// Unknown order type. + #[serde(rename = "UNKNOWN_ORDER_TYPE")] + Unknown, + /// Buy or sell a specified quantity of an Asset at the current best available market price. + Market, + /// Buy or sell a specified quantity of an Asset at a specified price. The Order will only post to the Order Book if it will immediately Fill; any remaining quantity is canceled. + Limit, + /// Buy or sell a specified quantity of an Asset at a specified price. The Order will only post to the Order Book if it is to immediately and completely Fill. + Stop, + /// Buy or sell a specified quantity of an Asset at a specified price. The Order will only post to the Order Book if it is to immediately and completely Fill. + StopLimit, + /// A Limit Order to buy or sell a specified quantity of an Asset at a specified price, with stop limit order parameters embedded in the order. If posted, the Order will remain on the Order Book until canceled. + Bracket, +} + +impl fmt::Display for OrderType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.as_ref()) + } +} + +impl AsRef for OrderType { + fn as_ref(&self) -> &str { + match self { + OrderType::Unknown => "UNKNOWN_ORDER_TYPE", + OrderType::Market => "MARKET", + OrderType::Limit => "LIMIT", + OrderType::Stop => "STOP", + OrderType::StopLimit => "STOP_LIMIT", + OrderType::Bracket => "BRACKET", + } + } +} + +/// Order side, BUY or SELL. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +#[serde(rename_all = "UPPERCASE")] +pub enum OrderSide { + /// Unknown order side. Only used by remote API. + #[serde(rename = "UNKNOWN_ORDER_SIDE")] + Unknown, + /// Buy order. + Buy, + /// Sell order. + Sell, +} + +impl fmt::Display for OrderSide { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.as_ref()) + } +} + +impl AsRef for OrderSide { + fn as_ref(&self) -> &str { + match self { + OrderSide::Unknown => "UNKNOWN_ORDER_SIDE", + OrderSide::Buy => "BUY", + OrderSide::Sell => "SELL", + } + } +} + +/// Used to sort results. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum OrderSortBy { + /// Unknown sort by. + #[serde(rename = "UNKNOWN_SORT_BY")] + Unknown, + /// Sort by price. + Price, + /// Sort by trade time. + TradeTime, + /// Sort by limit price. + LimitPrice, + /// Sort by last fill time. + LastFillTime, +} + +impl fmt::Display for OrderSortBy { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.as_ref()) + } +} + +impl AsRef for OrderSortBy { + fn as_ref(&self) -> &str { + match self { + OrderSortBy::Unknown => "UNKNOWN_SORT_BY", + OrderSortBy::Price => "PRICE", + OrderSortBy::TradeTime => "TRADE_TIME", + OrderSortBy::LimitPrice => "LIMIT_PRICE", + OrderSortBy::LastFillTime => "LAST_FILL_TIME", + } + } +} + +/// Order status, OPEN, CANCELLED, and EXPIRED. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum OrderStatus { + /// Order is pending. + Pending, + /// Order is open. + Open, + /// Order is filled. + Filled, + /// Order is cancelled. + Cancelled, + /// Order is expired. + Expired, + /// Order failed. + Failed, + /// Unknown order status. + #[serde(rename = "UNKNOWN_ORDER_STATUS")] + Unknown, + /// Order is queued. + Queued, + /// Order is queued to be cancelled. + CancelQueued, +} + +impl fmt::Display for OrderStatus { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{:?}", self) + } +} + +impl AsRef for OrderStatus { + fn as_ref(&self) -> &str { + match self { + OrderStatus::Pending => "PENDING", + OrderStatus::Open => "OPEN", + OrderStatus::Filled => "FILLED", + OrderStatus::Cancelled => "CANCELLED", + OrderStatus::Expired => "EXPIRED", + OrderStatus::Failed => "FAILED", + OrderStatus::Unknown => "UNKNOWN_ORDER_STATUS", + OrderStatus::Queued => "QUEUED", + OrderStatus::CancelQueued => "CANCEL_QUEUED", + } + } +} +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub enum StopDirection { + /// Unknown stop direction. + #[serde(rename = "UNKNOWN_STOP_DIRECTION")] + Unknown, + /// Stop up direction. + #[serde(rename = "STOP_DIRECTION_STOP_UP")] + StopUp, + /// Stop down direction. + #[serde(rename = "STOP_DIRECTION_STOP_DOWN")] + StopDown, +} + +impl fmt::Display for StopDirection { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{:?}", self) + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum TimeInForce { + /// Unknown time in force. + #[serde(rename = "UNKNOWN_TIME_IN_FORCE")] + Unknown, + /// Good 'til Cancelled + GoodUntilCancelled, + /// Good 'til Date + #[serde(rename = "GOOD_UNTIL_DATE_TIME")] + GoodUntilDate, + /// Immediate or Cancel + ImmediateOrCancel, + /// Fill or Kill + FillOrKill, +} + +impl fmt::Display for TimeInForce { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.as_ref()) + } +} + +impl AsRef for TimeInForce { + fn as_ref(&self) -> &str { + match self { + TimeInForce::Unknown => "UNKNOWN_TIME_IN_FORCE", + TimeInForce::GoodUntilCancelled => "GOOD_TIL_CANCELLED", + TimeInForce::GoodUntilDate => "GOOD_TIL_DATE_TIME", + TimeInForce::ImmediateOrCancel => "IMMEDIATE_OR_CANCEL", + TimeInForce::FillOrKill => "FILL_OR_KILL", + } + } +} + +/// Enum representing the different possible trigger statuses. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum TriggerStatus { + /// Unknown time in force. + #[serde(rename = "UNKNOWN_TRIGGER_STATUS")] + Unknown, + /// Invalid order type. + InvalidOrderType, + /// Stop pending. + StopPending, + /// Stop triggered. + StopTriggered, +} + +impl fmt::Display for TriggerStatus { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{:?}", self) + } +} + +impl AsRef for TriggerStatus { + fn as_ref(&self) -> &str { + match self { + TriggerStatus::Unknown => "UNKNOWN_TRIGGER_STATUS", + TriggerStatus::InvalidOrderType => "INVALID_ORDER_TYPE", + TriggerStatus::StopPending => "STOP_PENDING", + TriggerStatus::StopTriggered => "STOP_TRIGGERED", + } + } +} + +/// Enum representing reasons for rejecting an order. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum RejectReason { + /// Unspecified reject reason. + #[serde(rename = "REJECT_REASON_UNSPECIFIED")] + Unspecified, + /// Hold failure reject reason. + #[serde(rename = "HOLD_FAILURE")] + HoldFailure, + /// Too many open orders reject reason. + TooManyOpenOrders, + /// Insufficient funds reject reason. + #[serde(rename = "REJECT_REASON_INSUFFICIENT_FUNDS")] + InsufficientFunds, + /// Rate limit exceeded reject reason. + RateLimitExceeded, +} + +impl fmt::Display for RejectReason { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{:?}", self) + } +} + +impl AsRef for RejectReason { + fn as_ref(&self) -> &str { + match self { + RejectReason::Unspecified => "REJECT_REASON_UNSPECIFIED", + RejectReason::HoldFailure => "HOLD_FAILURE", + RejectReason::TooManyOpenOrders => "TOO_MANY_OPEN_ORDERS", + RejectReason::InsufficientFunds => "REJECT_REASON_INSUFFICIENT_FUNDS", + RejectReason::RateLimitExceeded => "RATE_LIMIT_EXCEEDED", + } + } +} + +/// Enum representing the different possible order configurations. +#[derive(Serialize, Debug, Clone)] +pub enum OrderConfiguration { + /// Market Immediate or Cancel Order. + #[serde(rename = "market_market_ioc")] + MarketIoc(MarketIoc), + /// Only posts if it will immediately fill, remaining quantity is cancelled. + #[serde(rename = "sor_limit_ioc")] + SorLimitIoc(SorLimitIoc), + /// Limit Good 'til Cancelled Order. + #[serde(rename = "limit_limit_gtc")] + LimitGtc(LimitGtc), + /// Limit Good 'til Date (time) Order. + #[serde(rename = "limit_limit_gtd")] + LimitGtd(LimitGtd), + /// Order only posts if it is fill entirely, otherwise cancelled. + #[serde(rename = "limit_limit_fok")] + LimitFok(LimitFok), + /// Stop Limit Good 'til Cancelled Order. + #[serde(rename = "stop_limit_stop_limit_gtc")] + StopLimitGtc(StopLimitGtc), + /// Stop Limit Good 'til Date (time) Order. + #[serde(rename = "stop_limit_stop_limit_gtd")] + StopLimitGtd(StopLimitGtd), + /// Trigger Bracket 'til Cancelled Order. + #[serde(rename = "trigger_bracket_gtc")] + TriggerBracketGtc(TriggerBracketGtc), + /// Trigger Bracket 'til Date (time) Order. + #[serde(rename = "trigger_bracket_gtd")] + TriggerBracketGtd(TriggerBracketGtd), +} diff --git a/src/models/order/mod.rs b/src/models/order/mod.rs new file mode 100644 index 0000000..78f6276 --- /dev/null +++ b/src/models/order/mod.rs @@ -0,0 +1,12 @@ +mod builders; +mod enums; +mod queries; +mod requests; +mod serde_utils; +mod types; + +pub use builders::*; +pub use enums::*; +pub use queries::*; +pub use requests::*; +pub use types::*; diff --git a/src/models/order/queries.rs b/src/models/order/queries.rs new file mode 100644 index 0000000..d9db055 --- /dev/null +++ b/src/models/order/queries.rs @@ -0,0 +1,308 @@ +//! # Coinbase Advanced Order API +//! +//! `order/queries` contains the query parameters for the various endpoints associated with the Order API. + +use serde::Serialize; + +use crate::errors::CbError; +use crate::product::ProductType; +use crate::utils::QueryBuilder; +use crate::{traits::Query, types::CbResult}; + +use super::{OrderSide, OrderSortBy, OrderStatus, OrderType, TimeInForce}; + +/// Represents parameters that are optional for List Orders API request. +#[derive(Serialize, Default, Debug, Clone)] +pub struct OrderListQuery { + /// ID(s) of order(s). + pub order_ids: Option>, + /// Optional string of the product ID(s). Defaults to null, or fetch for all products. + pub product_ids: Option>, + /// Only orders matching this product type are returned. Default is to return all product types. Valid options are SPOT or FUTURE. + pub product_type: Option, + /// Note: Cannot pair OPEN orders with other order types. + pub order_status: Option>, + /// Only orders matching this time in force(s) are returned. Default is to return all time in forces. + pub time_in_forces: Option>, + /// Type of orders to return. Default is to return all order types. + pub order_types: Option>, + /// Only orders matching this side are returned. Default is to return all sides. + pub order_side: Option, + /// Start date to fetch orders from, inclusive. + pub start_date: Option, + /// An optional end date for the query window, exclusive. If provided only orders with creation time before this date will be returned. + pub end_date: Option, + /// Only returns the orders where the quote, base or underlying asset matches the provided asset filter(s) (e.g. 'BTC'). + pub asset_filters: Option>, + /// A pagination limit with no default set. If has_next is true, additional orders are available to be fetched with pagination; also the cursor value in the response can be passed as cursor parameter in the subsequent request. + pub limit: Option, + /// Cursor used for pagination. When provided, the response returns responses after this cursor. + pub cursor: Option, + // Sort results by a field, results use unstable pagination. Default is sort by creation time. + pub sort_by: Option, +} + +impl Query for OrderListQuery { + fn check(&self) -> CbResult<()> { + if let Some(product_type) = &self.product_type { + if *product_type == ProductType::Unknown { + return Err(CbError::BadQuery( + "product_type must not be unknown".to_string(), + )); + } + } else if let Some(limit) = self.limit { + if limit == 0 { + return Err(CbError::BadQuery( + "limit must be greater than 0".to_string(), + )); + } + } else if let (Some(start), Some(end)) = (&self.start_date, &self.end_date) { + if start > end { + return Err(CbError::BadQuery( + "start_date must be before end_date".to_string(), + )); + } + } else if let Some(sort_by) = &self.sort_by { + if *sort_by == OrderSortBy::Unknown { + return Err(CbError::BadQuery("sort_by must not be unknown".to_string())); + } + } + Ok(()) + } + + /// Converts the object into HTTP request parameters. + fn to_query(&self) -> String { + QueryBuilder::new() + .push_optional_vec("order_ids", &self.order_ids) + .push_optional_vec("product_ids", &self.product_ids) + .push_optional("product_type", &self.product_type) + .push_optional_vec("order_status", &self.order_status) + .push_optional_vec("time_in_forces", &self.time_in_forces) + .push_optional_vec("order_types", &self.order_types) + .push_optional("order_side", &self.order_side) + .push_optional("start_date", &self.start_date) + .push_optional("end_date", &self.end_date) + .push_optional_vec("asset_filters", &self.asset_filters) + .push_optional("limit", &self.limit) + .push_optional("cursor", &self.cursor) + .push_optional("sort_by", &self.sort_by) + .build() + } +} + +impl OrderListQuery { + /// Creates a new instance of a Query to list orders. + pub fn new() -> Self { + Self::default() + } + + /// The ID(s) of order(s). + pub fn order_ids(mut self, order_ids: &[String]) -> Self { + self.order_ids = Some(order_ids.to_vec()); + self + } + + /// The ID(s) of the product(s) to filter orders by. + pub fn product_ids(mut self, product_ids: &[String]) -> Self { + self.product_ids = Some(product_ids.to_vec()); + self + } + + /// Only orders matching this product type are returned. Default is to return all product types. Valid options are SPOT or FUTURE. + pub fn product_type(mut self, product_type: ProductType) -> Self { + self.product_type = Some(product_type); + self + } + + /// Only orders matching this order status are returned. Default is to return all order statuses. + pub fn order_status(mut self, order_status: &[OrderStatus]) -> Self { + self.order_status = Some(order_status.to_vec()); + self + } + + /// Only orders matching this time in force(s) are returned. Default is to return all time in forces. + pub fn time_in_forces(mut self, time_in_forces: &[TimeInForce]) -> Self { + self.time_in_forces = Some(time_in_forces.to_vec()); + self + } + + /// Type of orders to return. Default is to return all order types. + pub fn order_types(mut self, order_types: &[OrderType]) -> Self { + self.order_types = Some(order_types.to_vec()); + self + } + + /// Only orders matching this side are returned. Default is to return all sides. + pub fn order_side(mut self, order_side: OrderSide) -> Self { + self.order_side = Some(order_side); + self + } + + /// Start date to fetch orders from, inclusive. + pub fn start_date(mut self, start_date: String) -> Self { + self.start_date = Some(start_date); + self + } + + /// An optional end date for the query window, exclusive. If provided only orders with creation time before this date will be returned. + pub fn end_date(mut self, end_date: String) -> Self { + self.end_date = Some(end_date); + self + } + + /// Only returns the orders where the quote, base or underlying asset matches the provided asset filter(s) (e.g. 'BTC'). + pub fn asset_filters(mut self, asset_filters: &[String]) -> Self { + self.asset_filters = Some(asset_filters.to_vec()); + self + } + + /// A pagination limit with no default set. If has_next is true, additional orders are available to be fetched with pagination; also the cursor value in the response can be passed as cursor parameter in the subsequent request. + pub fn limit(mut self, limit: u32) -> Self { + self.limit = Some(limit); + self + } + + /// Cursor used for pagination. When provided, the response returns responses after this cursor. + pub fn cursor(mut self, cursor: String) -> Self { + self.cursor = Some(cursor); + self + } + + /// Sort results by a field, results use unstable pagination. Default is sort by creation time. + pub fn sort_by(mut self, sort_by: OrderSortBy) -> Self { + self.sort_by = Some(sort_by); + self + } +} + +/// Represents parameters that are optional for List Fills API request. +/// +/// # Required Fields +/// +#[derive(Serialize, Debug, Clone)] +pub struct OrderListFillsQuery { + /// The ID(s) of order(s). + pub order_ids: Option>, + /// The ID(s) of the trades of fills. + pub trade_ids: Option>, + /// The ID(s) of the product(s) to filter fills by. + pub product_ids: Option>, + /// Start date. Only fills with a trade time at or after this start date are returned. + pub start_sequence_timestamp: Option, + /// End date. Only fills with a trade time before this start date are returned. + pub end_sequence_timestamp: Option, + /// Maximum number of fills to return in response. Defaults to 100. + pub limit: u32, + /// Cursor used for pagination. When provided, the response returns responses after this cursor. + pub cursor: Option, + /// Sort results by a field, results use unstable pagination. Default is sort by creation time. + pub sort_by: Option, +} + +impl Query for OrderListFillsQuery { + fn check(&self) -> CbResult<()> { + if self.limit == 0 { + return Err(CbError::BadQuery( + "limit must be greater than 0".to_string(), + )); + } else if let (Some(start), Some(end)) = + (&self.start_sequence_timestamp, &self.end_sequence_timestamp) + { + if start > end { + return Err(CbError::BadQuery( + "start_sequence_timestamp must be before end_sequence_timestamp".to_string(), + )); + } + } else if let Some(sort_by) = &self.sort_by { + if *sort_by == OrderSortBy::Unknown { + return Err(CbError::BadQuery("sort_by must not be unknown".to_string())); + } + } + + Ok(()) + } + + /// Converts the object into HTTP request parameters. + fn to_query(&self) -> String { + QueryBuilder::new() + .push_optional_vec("order_ids", &self.order_ids) + .push_optional_vec("trade_ids", &self.trade_ids) + .push_optional_vec("product_ids", &self.product_ids) + .push_optional("start_sequence_timestamp", &self.start_sequence_timestamp) + .push_optional("end_sequence_timestamp", &self.end_sequence_timestamp) + .push("limit", self.limit) + .push_optional("cursor", &self.cursor) + .push_optional("sort_by", &self.sort_by) + .build() + } +} + +impl Default for OrderListFillsQuery { + fn default() -> Self { + Self { + order_ids: None, + trade_ids: None, + product_ids: None, + start_sequence_timestamp: None, + end_sequence_timestamp: None, + limit: 100, + cursor: None, + sort_by: None, + } + } +} + +impl OrderListFillsQuery { + /// Creates a new instance of a Query to list fills. + pub fn new() -> Self { + Self::default() + } + + /// The ID(s) of order(s). + pub fn order_ids(mut self, order_ids: &[String]) -> Self { + self.order_ids = Some(order_ids.to_vec()); + self + } + + /// The ID(s) of the trades of fills. + pub fn trade_ids(mut self, trade_ids: &[String]) -> Self { + self.trade_ids = Some(trade_ids.to_vec()); + self + } + + /// The ID(s) of the product(s) to filter. + pub fn product_ids(mut self, product_ids: &[String]) -> Self { + self.product_ids = Some(product_ids.to_vec()); + self + } + + /// Start date. Only fills with a trade time at or after this start date are returned. + pub fn start_sequence_timestamp(mut self, start_sequence_timestamp: String) -> Self { + self.start_sequence_timestamp = Some(start_sequence_timestamp); + self + } + + /// End date. Only fills with a trade time before this start date are returned. + pub fn end_sequence_timestamp(mut self, end_sequence_timestamp: String) -> Self { + self.end_sequence_timestamp = Some(end_sequence_timestamp); + self + } + + /// Maximum number of fills to return in response. Defaults to 100. + pub fn limit(mut self, limit: u32) -> Self { + self.limit = limit; + self + } + + /// Cursor used for pagination. When provided, the response returns responses after this cursor. + pub fn cursor(mut self, cursor: String) -> Self { + self.cursor = Some(cursor); + self + } + + /// Sort results by a field, results use unstable pagination. Default is sort by creation time. + pub fn sort_by(mut self, sort_by: OrderSortBy) -> Self { + self.sort_by = Some(sort_by); + self + } +} diff --git a/src/models/order/requests.rs b/src/models/order/requests.rs new file mode 100644 index 0000000..8c09d44 --- /dev/null +++ b/src/models/order/requests.rs @@ -0,0 +1,178 @@ +//! # Coinbase Advanced Order API +//! +//! `order/requests` contains requests that are sent to the Order API. + +use serde::Serialize; +use serde_with::{serde_as, DisplayFromStr}; + +use crate::{errors::CbError, traits::Request, types::CbResult}; + +use super::{OrderConfiguration, OrderSide}; + +/// A request send to the Order API to cancel orders. +#[derive(Serialize, Debug)] +pub struct OrderCancelRequest { + /// Vector of Order IDs to cancel. + pub order_ids: Vec, +} + +impl Request for OrderCancelRequest { + fn check(&self) -> CbResult<()> { + if self.order_ids.is_empty() { + return Err(CbError::BadRequest("no order IDs provided".to_string())); + } + Ok(()) + } +} + +impl OrderCancelRequest { + pub fn new(order_ids: &[String]) -> Self { + Self { + order_ids: order_ids.to_vec(), + } + } +} + +/// A request send to the Order API to create an order. +#[derive(Serialize, Debug)] +pub struct OrderCreateRequest { + /// Client Order ID (UUID). Skipped if creating a preview order. + #[serde(skip_serializing_if = "str::is_empty")] + pub client_order_id: String, + /// Product ID (pair) + pub product_id: String, + /// Order Side: BUY or SELL. + pub side: OrderSide, + #[serde(skip_serializing)] + #[serde(default)] + pub(crate) is_preview: bool, + /// Configuration for the order. + pub order_configuration: OrderConfiguration, +} + +impl Request for OrderCreateRequest { + fn check(&self) -> CbResult<()> { + if self.client_order_id.is_empty() && !self.is_preview { + return Err(CbError::BadRequest( + "no client order ID provided".to_string(), + )); + } else if self.product_id.is_empty() { + return Err(CbError::BadRequest("no product ID provided".to_string())); + } + Ok(()) + } +} + +/// A request send to the Order API to edit an order. +#[serde_as] +#[derive(Serialize, Debug)] +pub struct OrderEditRequest { + /// ID of the order to edit. + pub order_id: String, + /// New price for order. + #[serde_as(as = "DisplayFromStr")] + pub price: f64, + /// New size for order. + #[serde_as(as = "DisplayFromStr")] + pub size: f64, +} + +impl Request for OrderEditRequest { + fn check(&self) -> CbResult<()> { + if self.order_id.is_empty() { + return Err(CbError::BadRequest("no order ID provided".to_string())); + } else if self.price < 0.0 { + return Err(CbError::BadRequest( + "price cannot be less than 0".to_string(), + )); + } else if self.size <= 0.0 { + return Err(CbError::BadRequest( + "size must be greater than 0".to_string(), + )); + } + Ok(()) + } +} + +impl OrderEditRequest { + /// Creates a new `OrderEditRequest`. + pub fn new(order_id: &str, price: f64, size: f64) -> Self { + Self { + order_id: order_id.to_string(), + price, + size, + } + } +} + +/// Represents parameters that are needed to close positions. +/// +/// # Required Fields +/// +/// * `client_order_id` - The unique ID provided for the order (used for identification purposes). +/// * `product_id` - The trading pair (e.g. 'BIT-28JUL23-CDE'). +#[derive(Serialize, Debug)] +pub struct OrderClosePositionRequest { + /// The unique ID provided for the order (used for identification purposes). + pub client_order_id: String, + /// The trading pair (e.g. 'BIT-28JUL23-CDE'). + pub product_id: String, + /// The amount of contracts that should be closed. + pub size: Option, +} + +impl Request for OrderClosePositionRequest { + fn check(&self) -> CbResult<()> { + if self.client_order_id.is_empty() { + return Err(CbError::BadRequest( + "client_order_id is required".to_string(), + )); + } else if self.product_id.is_empty() { + return Err(CbError::BadRequest("product_id is required".to_string())); + } else if let Some(size) = self.size { + if size == 0 { + return Err(CbError::BadRequest( + "size must be greater than 0".to_string(), + )); + } + } + + Ok(()) + } +} + +impl OrderClosePositionRequest { + /// Creates a new instance of a Query to close a position. + /// + /// # Arguments + /// + /// * `client_order_id` - The unique ID provided for the order (used for identification purposes). + /// * `product_id` - The trading pair (e.g. 'BIT-28JUL23-CDE + pub fn new(client_order_id: &str, product_id: &str) -> Self { + Self { + client_order_id: client_order_id.to_string(), + product_id: product_id.to_string(), + size: None, + } + } + + /// Sets the client order ID. + /// Note: This is a required field. + pub fn client_order_id(mut self, client_order_id: String) -> Self { + self.client_order_id = client_order_id; + self + } + + /// Sets the product ID. + /// Note: This is a required field. + pub fn product_id(mut self, product_id: String) -> Self { + self.product_id = product_id; + self + } + + /// Sets the size. + pub fn size(mut self, size: u32) -> Self { + self.size = Some(size); + self + } +} diff --git a/src/models/order/serde_utils.rs b/src/models/order/serde_utils.rs new file mode 100644 index 0000000..159e22a --- /dev/null +++ b/src/models/order/serde_utils.rs @@ -0,0 +1,53 @@ +//! # Coinbase Advanced Order API +//! +//! `order/serde_utils` is the module containing the serde utility functions for the OrderType enum. + +use std::fmt; + +use serde::de::{self, Deserialize as DeDeserialize, Deserializer, Visitor}; + +use super::OrderType; + +impl<'de> DeDeserialize<'de> for OrderType { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_str(OrderTypeVisitor) + } +} + +struct OrderTypeVisitor; + +impl<'de> Visitor<'de> for OrderTypeVisitor { + type Value = OrderType; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a string representing an OrderType") + } + + fn visit_str(self, value: &str) -> Result + where + E: de::Error, + { + match value.to_uppercase().as_str() { + "UNKNOWN_ORDER_TYPE" => Ok(OrderType::Unknown), + "MARKET" => Ok(OrderType::Market), + "LIMIT" => Ok(OrderType::Limit), + "STOP" => Ok(OrderType::Stop), + "STOP_LIMIT" => Ok(OrderType::StopLimit), + "BRACKET" => Ok(OrderType::Bracket), + _ => Err(de::Error::unknown_variant( + value, + &[ + "UnknownOrderType", + "Market", + "Limit", + "Stop", + "StopLimit", + "Bracket", + ], + )), + } + } +} diff --git a/src/models/order/types.rs b/src/models/order/types.rs new file mode 100644 index 0000000..cbab589 --- /dev/null +++ b/src/models/order/types.rs @@ -0,0 +1,508 @@ +//! # Coinbase Advanced Order API +//! +//! `order/types` is the module containing the structs for the different order types and configurations. + +use serde::{Deserialize, Serialize}; +use serde_with::{serde_as, DefaultOnError, DisplayFromStr}; + +use crate::product::ProductType; + +use super::{ + OrderSide, OrderStatus, OrderType, RejectReason, StopDirection, TimeInForce, TriggerStatus, +}; + +/// Buy or sell a specified quantity of an Asset at the current best available market price. +#[serde_as] +#[derive(Serialize, Debug, Clone)] +pub struct MarketIoc { + /// Amount of quote currency to spend on order. Required for BUY orders. + #[serde_as(as = "Option")] + #[serde(default)] + pub quote_size: Option, + /// Amount of base currency to spend on order. Required for SELL orders. + #[serde_as(as = "Option")] + #[serde(default)] + pub base_size: Option, +} + +/// Buy or sell a specified quantity of an Asset at a specified price. The Order will only post to the Order Book if it will immediately Fill; any remaining quantity is canceled. +#[serde_as] +#[derive(Serialize, Debug, Clone)] +pub struct SorLimitIoc { + /// Amount of base currency to spend on order. + #[serde_as(as = "DisplayFromStr")] + pub base_size: f64, + /// Ceiling price for which the order should get filled. + #[serde_as(as = "DisplayFromStr")] + pub limit_price: f64, +} + +/// Limit Good til Cancelled. +#[serde_as] +#[derive(Serialize, Debug, Clone)] +pub struct LimitGtc { + /// Amount of base currency to spend on order. + #[serde_as(as = "DisplayFromStr")] + pub base_size: f64, + /// Ceiling price for which the order should get filled. + #[serde_as(as = "DisplayFromStr")] + pub limit_price: f64, + /// Post only limit order. + pub post_only: bool, +} + +/// Limit Good til Time (Date). +#[serde_as] +#[derive(Serialize, Debug, Clone)] +pub struct LimitGtd { + /// Amount of base currency to spend on order. + #[serde_as(as = "DisplayFromStr")] + pub base_size: f64, + /// Ceiling price for which the order should get filled. + #[serde_as(as = "DisplayFromStr")] + pub limit_price: f64, + /// Time at which the order should be cancelled if it's not filled. + pub end_time: String, + /// Post only limit order. + pub post_only: bool, +} + +/// Buy or sell a specified quantity of an Asset at a specified price. The Order will only post to the Order Book if it is to immediately and completely Fill. +#[serde_as] +#[derive(Serialize, Debug, Clone)] +pub struct LimitFok { + /// Amount of base currency to spend on order. + #[serde_as(as = "DisplayFromStr")] + pub base_size: f64, + /// Ceiling price for which the order should get filled. + #[serde_as(as = "DisplayFromStr")] + pub limit_price: f64, +} + +/// Stop Limit Good til Cancelled. +#[serde_as] +#[derive(Serialize, Debug, Clone)] +pub struct StopLimitGtc { + /// Amount of base currency to spend on order. + #[serde_as(as = "DisplayFromStr")] + pub base_size: f64, + /// Ceiling price for which the order should get filled. + #[serde_as(as = "DisplayFromStr")] + pub limit_price: f64, + /// Price at which the order should trigger - if stop direction is Up, then the order will trigger when the last trade price goes above this, otherwise order will trigger when last trade price goes below this price. + #[serde_as(as = "DisplayFromStr")] + pub stop_price: f64, + /// Possible values: [UNKNOWN_STOP_DIRECTION, STOP_DIRECTION_STOP_UP, STOP_DIRECTION_STOP_DOWN] + pub stop_direction: StopDirection, +} + +/// Stop Limit Good til Time (Date). +#[serde_as] +#[derive(Serialize, Debug, Clone)] +pub struct StopLimitGtd { + /// Amount of base currency to spend on order. + #[serde_as(as = "DisplayFromStr")] + pub base_size: f64, + /// Ceiling price for which the order should get filled. + #[serde_as(as = "DisplayFromStr")] + pub limit_price: f64, + /// Price at which the order should trigger - if stop direction is Up, then the order will trigger when the last trade price goes above this, otherwise order will trigger when last trade price goes below this price. + #[serde_as(as = "DisplayFromStr")] + pub stop_price: f64, + /// Time at which the order should be cancelled if it's not filled. + pub end_time: String, + /// Possible values: [UNKNOWN_STOP_DIRECTION, STOP_DIRECTION_STOP_UP, STOP_DIRECTION_STOP_DOWN] + pub stop_direction: StopDirection, +} + +/// A Limit Order to buy or sell a specified quantity of an Asset at a specified price, with stop limit order parameters embedded in the order. If posted, the Order will remain on the Order Book until canceled. +#[serde_as] +#[derive(Serialize, Debug, Clone)] +pub struct TriggerBracketGtc { + /// Amount of base currency to spend on order. + #[serde_as(as = "DisplayFromStr")] + pub base_size: f64, + /// Ceiling price for which the order should get filled. + #[serde_as(as = "DisplayFromStr")] + pub limit_price: f64, + /// The price level (in quote currency) where the position will be exited. When triggered, a stop limit order is automatically placed with a limit price 5% higher for BUYS and 5% lower for SELLS. + #[serde_as(as = "DisplayFromStr")] + pub stop_trigger_price: f64, +} + +/// A Limit Order to buy or sell a specified quantity of an Asset at a specified price, with stop limit order parameters embedded in the order. If posted, the Order will remain on the Order Book until a certain time is reached or the Order is canceled. +#[serde_as] +#[derive(Serialize, Debug, Clone)] +pub struct TriggerBracketGtd { + /// Amount of base currency to spend on order. + #[serde_as(as = "DisplayFromStr")] + pub base_size: f64, + /// Ceiling price for which the order should get filled. + #[serde_as(as = "DisplayFromStr")] + pub limit_price: f64, + /// The price level (in quote currency) where the position will be exited. When triggered, a stop limit order is automatically placed with a limit price 5% higher for BUYS and 5% lower for SELLS. + #[serde_as(as = "DisplayFromStr")] + pub stop_trigger_price: f64, + /// Time at which the order should be cancelled if it's not filled. + pub end_time: String, +} + +/// Represents a single edit entry in the edit history of an order. +#[serde_as] +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct EditHistory { + /// The price associated with the edit. + #[serde_as(as = "DefaultOnError")] + #[serde(default)] + pub price: f64, + /// The size associated with the edit. + #[serde_as(as = "DefaultOnError")] + #[serde(default)] + pub size: f64, + /// The timestamp when the edit was accepted. + pub replace_accept_timestamp: String, +} + +/// Represents an Order received from the API. +#[serde_as] +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct Order { + /// The unique id for this order. + pub order_id: String, + /// Client specified ID of order. + pub client_order_id: String, + /// The product this order was created for e.g. 'BTC-USD' + pub product_id: String, + /// The id of the User owning this Order. + pub user_id: String, + /// Possible values: [UNKNOWN_ORDER_SIDE, BUY, SELL] + pub side: OrderSide, + /// Possible values: [OPEN, FILLED, CANCELLED, EXPIRED, FAILED, UNKNOWN_ORDER_STATUS] + pub status: OrderStatus, + /// Possible values: [UNKNOWN_TIME_IN_FORCE, GOOD_UNTIL_DATE_TIME, GOOD_UNTIL_CANCELLED, IMMEDIATE_OR_CANCEL, FILL_OR_KILL] + pub time_in_force: TimeInForce, + /// Timestamp for when the order was created. + pub created_time: String, + /// The percent of total order amount that has been filled. + #[serde_as(as = "DefaultOnError")] + #[serde(default)] + pub completion_percentage: f64, + /// The portion (in base currency) of total order amount that has been filled. + #[serde_as(as = "DefaultOnError")] + #[serde(default)] + pub filled_size: f64, + /// The average of all prices of fills for this order. + #[serde_as(as = "DefaultOnError")] + #[serde(default)] + pub average_filled_price: f64, + /// Commission amount. + #[serde_as(as = "DefaultOnError")] + #[serde(default)] + pub fee: f64, + /// Number of fills that have been posted for this order. + #[serde_as(as = "DefaultOnError")] + #[serde(default)] + pub number_of_fills: u32, + /// The portion (in quote current) of total order amount that has been filled. + #[serde_as(as = "DefaultOnError")] + #[serde(default)] + pub filled_value: f64, + /// Whether a cancel request has been initiated for the order, and not yet completed. + pub pending_cancel: bool, + /// Whether the order was placed with quote currency/ + pub size_in_quote: bool, + /// The total fees for the order. + #[serde_as(as = "DefaultOnError")] + #[serde(default)] + pub total_fees: f64, + /// Whether the order size includes fees. + pub size_inclusive_of_fees: bool, + /// Derived field: filled_value + total_fees for buy orders and filled_value - total_fees for sell orders. + #[serde_as(as = "DefaultOnError")] + #[serde(default)] + pub total_value_after_fees: f64, + /// Possible values: \[UNKNOWN_TRIGGER_STATUS, INVALID_ORDER_TYPE, STOP_PENDING, STOP_TRIGGERED\] + pub trigger_status: TriggerStatus, + /// Possible values: \[UNKNOWN_ORDER_TYPE, MARKET, LIMIT, STOP, STOP_LIMIT\] + pub order_type: OrderType, + /// Possible values: \[REJECT_REASON_UNSPECIFIED\] + pub reject_reason: RejectReason, + /// True if the order is fully filled, false otherwise. + pub settled: bool, + /// Possible values: [SPOT, FUTURE] + pub product_type: ProductType, + /// Message stating why the order was rejected. + pub reject_message: String, + /// Message stating why the order was canceled. + pub cancel_message: String, + /// An array of the latest 5 edits per order. + pub edit_history: Vec, +} + +/// Represents a fill received from the API. +#[serde_as] +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct Fill { + /// Unique identifier for the fill. + pub entry_id: String, + /// Id of the fill -- unique for all `FILL` trade_types but not unique for adjusted fills. + pub trade_id: String, + /// Id of the order the fill belongs to. + pub order_id: String, + /// Time at which this fill was completed. + pub trade_time: String, + /// String denoting what type of fill this is. Regular fills have the value `FILL`. + /// Adjusted fills have possible values `REVERSAL`, `CORRECTION`, `SYNTHETIC`. + pub trade_type: String, + /// Price the fill was posted at. + #[serde_as(as = "DisplayFromStr")] + pub price: f64, + /// Amount of order that was transacted at this fill. + #[serde_as(as = "DisplayFromStr")] + pub size: f64, + /// Fee amount for fill. + #[serde_as(as = "DisplayFromStr")] + #[serde(default)] + pub commission: f64, + /// The product this order was created for. + pub product_id: String, + /// Time at which this fill was posted. + pub sequence_timestamp: String, + /// Possible values: [UNKNOWN_LIQUIDITY_INDICATOR, MAKER, TAKER] + pub liquidity_indicator: String, + /// Whether the order was placed with quote currency. + pub size_in_quote: bool, + /// User that placed the order the fill belongs to. + pub user_id: String, + /// Possible values: [UNKNOWN_ORDER_SIDE, BUY, SELL] + pub side: OrderSide, +} + +/// Represents a list of orders received from the API. +#[derive(Deserialize, Debug)] +pub struct PaginatedOrders { + /// Vector of orders obtained. + pub orders: Vec, + /// If there are additional orders. + pub has_next: bool, + /// Cursor used to pull more orders. + pub cursor: String, +} + +/// Represents a list of fills received from the API. +#[derive(Deserialize, Debug)] +pub struct PaginatedFills { + /// Vector of filled orders. + pub orders: Vec, + /// Cursor used to pull more fills. + pub cursor: String, +} + +/// Contains information when an order is successfully created. +#[derive(Serialize, Deserialize, Debug)] +pub struct SuccessResponse { + /// The ID of the order. + pub order_id: String, + /// The trading pair (e.g., 'BTC-USD'). + pub product_id: String, + /// The side of the market that the order is on ('BUY' or 'SELL'). + pub side: OrderSide, + /// The unique ID provided for the order (used for identification purposes). + pub client_order_id: String, +} + +/// Contains error information when an order fails to be created. +#[derive(Serialize, Deserialize, Debug)] +pub struct ErrorResponse { + /// **(Deprecated)** The reason the order failed to be created. + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, + /// Generic error message explaining why the order was not created. + pub message: Option, + /// Descriptive error message explaining why the order was not created. + pub error_details: Option, + /// **(Deprecated)** The reason the order failed during preview. + #[serde(skip_serializing_if = "Option::is_none")] + pub preview_failure_reason: Option, + /// The reason the order failed to be created. + pub new_order_failure_reason: String, +} + +/// Represents a create, edit, or cancel order response from the API. +#[derive(Deserialize, Debug)] +pub struct OrderCreateResponse { + /// Whether the order was successfully created. + pub success: bool, + /// Contains information if the order was successful. + #[serde(skip_serializing_if = "Option::is_none")] + pub success_response: Option, + /// Contains error information if the order failed. + #[serde(skip_serializing_if = "Option::is_none")] + pub error_response: Option, +} + +/// Represents a cancel order response from the API. +#[derive(Deserialize, Debug)] +pub struct OrderCancelResponse { + /// Whether the order was successfully cancelled. + pub success: bool, + /// Failure reason. + pub failure_reason: String, + /// Order ID. + pub order_id: String, +} + +/// Represents an order when obtaining a single order from the API. +#[derive(Deserialize, Debug)] +pub struct OrderEditResponse { + /// Whether or not the order edit succeeded. + pub success: bool, + /// Errors associated with the changes. + pub errors: Vec, +} + +/// Errors associated with the changes. +#[derive(Deserialize, Serialize, Debug, Clone)] +pub struct OrderEditError { + /// Reason the edit failed. + pub edit_failure_reason: Option, + /// Reason the preview failed. + pub preview_failure_reason: Option, +} + +/// Response from a preview edit order. +#[serde_as] +#[derive(Deserialize, Serialize, Debug, Clone)] +pub struct OrderEditPreview { + /// Contains reasons for failure in the edit or preview edit operation. + pub errors: Vec, + /// The amount of slippage in the order. + #[serde_as(as = "DisplayFromStr")] + #[serde(default)] + pub slippage: f64, + /// The total value of the order. + #[serde_as(as = "DisplayFromStr")] + #[serde(default)] + pub order_total: f64, + /// The total commission for the order. + #[serde_as(as = "DisplayFromStr")] + #[serde(default)] + pub commission_total: f64, + /// The size of the quote currency in the order. + #[serde_as(as = "DisplayFromStr")] + #[serde(default)] + pub quote_size: f64, + /// The size of the base currency in the order. + #[serde_as(as = "DisplayFromStr")] + #[serde(default)] + pub base_size: f64, + /// The best bid price at the time of the order. + #[serde_as(as = "DisplayFromStr")] + #[serde(default)] + pub best_bid: f64, + /// The best ask price at the time of the order. + #[serde_as(as = "DisplayFromStr")] + #[serde(default)] + pub best_ask: f64, + /// The average price at which the order was filled. + #[serde_as(as = "DisplayFromStr")] + #[serde(default)] + pub average_filled_price: f64, +} + +/// Represents the response for a preview of creating an order. +#[serde_as] +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct OrderCreatePreview { + /// The total value of the order. + #[serde_as(as = "DisplayFromStr")] + #[serde(default)] + pub order_total: f64, + /// The total commission for the order. + #[serde_as(as = "DisplayFromStr")] + #[serde(default)] + pub commission_total: f64, + /// List of errors encountered during the preview. + pub errs: Vec, + /// List of warnings related to the order preview. + pub warning: Vec, + /// The best bid price at the time of the preview. + #[serde_as(as = "DisplayFromStr")] + #[serde(default)] + pub best_bid: f64, + /// The best ask price at the time of the preview. + #[serde_as(as = "DisplayFromStr")] + #[serde(default)] + pub best_ask: f64, + /// The size of the quote currency in the order. + /// NOTE: There were issues deserializing this in the past. + #[serde_as(as = "DisplayFromStr")] + #[serde(default)] + pub quote_size: f64, + /// The size of the base currency in the order. + /// NOTE: There were issues deserializing this in the past. + #[serde_as(as = "DisplayFromStr")] + pub base_size: f64, + /// Indicates whether the maximum allowed amount was used. + pub is_max: bool, + /// The total margin required for the order. + #[serde_as(as = "DisplayFromStr")] + #[serde(default)] + pub order_margin_total: f64, + /// The leverage applied to the order. + #[serde_as(as = "DisplayFromStr")] + #[serde(default)] + pub leverage: f64, + /// The long leverage available for the order. + #[serde_as(as = "DisplayFromStr")] + #[serde(default)] + pub long_leverage: f64, + /// The short leverage available for the order. + #[serde_as(as = "DisplayFromStr")] + #[serde(default)] + pub short_leverage: f64, + /// The projected slippage for the order. + #[serde_as(as = "DisplayFromStr")] + #[serde(default)] + pub slippage: f64, + /// The unique identifier for the order preview. + pub preview_id: String, + /// The current liquidation buffer for the account. + #[serde_as(as = "DisplayFromStr")] + #[serde(default)] + pub current_liquidation_buffer: f64, + /// The projected liquidation buffer after the order. + #[serde_as(as = "DisplayFromStr")] + #[serde(default)] + pub projected_liquidation_buffer: f64, + /// The maximum leverage available for the order. + #[serde_as(as = "DefaultOnError>")] + #[serde(default)] + pub max_leverage: Option, +} + +/// Represents a cancel order response from the API. +#[derive(Deserialize, Debug)] +pub(crate) struct OrderCancelWrapper { + /// Vector of orders cancelled. + pub(crate) results: Vec, +} + +impl From for Vec { + fn from(wrapper: OrderCancelWrapper) -> Self { + wrapper.results + } +} + +/// Represents an order when obtaining a single order from the API. +#[derive(Deserialize, Debug)] +pub(crate) struct OrderWrapper { + /// Order received. + pub(crate) order: Order, +} + +impl From for Order { + fn from(wrapper: OrderWrapper) -> Self { + wrapper.order + } +} diff --git a/src/models/payment.rs b/src/models/payment.rs new file mode 100644 index 0000000..d77afb7 --- /dev/null +++ b/src/models/payment.rs @@ -0,0 +1,59 @@ +//! # Coinbase Advanced Payment API +//! +//! `payment` gives access to the Payment API and the various endpoints associated with it. + +use serde::{Deserialize, Serialize}; + +/// A type of payment method available to the user for use. +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct PaymentMethod { + /// Unique identifier for the payment method. + pub id: String, + /// The payment method type. + #[serde(rename = "type")] + pub r#type: String, + /// Name for the payment method. + pub name: String, + /// Currency symbol for the payment method. + pub currency: String, + /// The verified status of the payment method. + pub verified: bool, + /// Whether or not this payment method can perform buys. + pub allow_buy: bool, + /// Whether or not this payment method can perform sells. + pub allow_sell: bool, + /// Whether or not this payment method can perform deposits. + pub allow_deposit: bool, + /// Whether or not this payment method can perform withdrawals. + pub allow_withdraw: bool, + /// Time at which this payment method was created. + pub created_at: String, + /// Time at which this payment method was updated. + pub updated_at: Option, +} + +/// Response from the API that wraps a list of payment methods. +#[derive(Deserialize, Debug, Clone)] +pub(crate) struct PaymentMethodsWrapper { + /// List of payment methods available to the user. + pub(crate) payment_methods: Vec, +} + +impl From for Vec { + fn from(wrapper: PaymentMethodsWrapper) -> Self { + wrapper.payment_methods + } +} + +/// Response from the API that wraps a single payment method. +#[derive(Deserialize, Debug, Clone)] +pub(crate) struct PaymentMethodWrapper { + /// A payment method requested by the user. + pub(crate) payment_method: PaymentMethod, +} + +impl From for PaymentMethod { + fn from(wrapper: PaymentMethodWrapper) -> Self { + wrapper.payment_method + } +} diff --git a/src/models/portfolio.rs b/src/models/portfolio.rs new file mode 100644 index 0000000..e6f235d --- /dev/null +++ b/src/models/portfolio.rs @@ -0,0 +1,417 @@ +//! # Coinbase Advanced Portfolio API +//! +//! `portfolio` gives access to the Potfolio API and the various endpoints associated with it. +//! This allows for the management of individual portfolios. + +use core::fmt; + +use serde::{Deserialize, Serialize}; +use serde_with::{serde_as, DefaultOnError, DisplayFromStr}; + +use super::shared::Balance; +use crate::errors::CbError; +use crate::traits::{Query, Request}; +use crate::types::CbResult; +use crate::utils::QueryBuilder; + +/// Portfolio type for a user's portfolio. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +#[serde(rename_all = "UPPERCASE")] +pub enum PortfolioType { + /// Portfolio type for a user's default portfolio. + Default, + /// Portfolios created by the user. + Consumer, + /// /// International Exchange portfolios. + Intx, + /// Fallback for undefined or unrecognized values. + Undefined, +} + +impl AsRef for PortfolioType { + fn as_ref(&self) -> &str { + match self { + PortfolioType::Default => "DEFAULT", + PortfolioType::Consumer => "CONSUMER", + PortfolioType::Intx => "INTX", + PortfolioType::Undefined => "UNDEFINED", + } + } +} + +impl fmt::Display for PortfolioType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.as_ref()) + } +} + +/// Enum for `PositionSide` values. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub enum PositionSide { + #[serde(rename = "FUTURES_POSITION_SIDE_UNSPECIFIED")] + Unspecified, + #[serde(rename = "FUTURES_POSITION_SIDE_LONG")] + Long, + #[serde(rename = "FUTURES_POSITION_SIDE_SHORT")] + Short, +} + +/// Enum for `MarginType` values. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum MarginType { + #[serde(rename = "MARGIN_TYPE_UNSPECIFIED")] + Unspecified, + #[serde(rename = "MARGIN_TYPE_CROSS")] + Cross, + #[serde(rename = "MARGIN_TYPE_ISOLATED")] + Isolated, +} + +/// Portfolio information. +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct Portfolio { + /// Name of the portfolio. + pub name: String, + /// UUID of the portfolio. + pub uuid: String, + /// Type of the portfolio. + pub r#type: PortfolioType, + /// Indicates if the portfolio is deleted. + pub deleted: bool, +} + +/// Portfolio balances for different categories. +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct PortfolioBalances { + /// Total balance across all portfolio types. + pub total_balance: Balance, + /// Total balance in futures. + pub total_futures_balance: Balance, + /// Total balance in cash or cash-equivalent assets. + pub total_cash_equivalent_balance: Balance, + /// Total balance in cryptocurrencies. + pub total_crypto_balance: Balance, + /// Unrealized profit and loss in futures trading. + pub futures_unrealized_pnl: Balance, + /// Unrealized profit and loss in perpetual trading. + pub perp_unrealized_pnl: Balance, +} + +/// Spot position details. +#[serde_as] +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct SpotPosition { + /// The asset symbol (e.g., BTC, ETH). + pub asset: String, + /// The account UUID associated with the asset. + pub account_uuid: String, + /// Total balance of the asset in fiat currency. + pub total_balance_fiat: f64, + /// Total balance of the asset in cryptocurrency. + pub total_balance_crypto: f64, + /// Amount available for trading in fiat currency. + pub available_to_trade_fiat: f64, + /// Percentage of the portfolio allocated to this asset in decimal form. + pub allocation: f64, + /// Change in value of the asset over one day. + /// NOTE: This field currently is not returned by the API. + #[serde_as(as = "DefaultOnError")] + #[serde(default)] + pub one_day_change: f64, + /// Cost basis of the asset. + pub cost_basis: Balance, + /// URL of the asset's image. + pub asset_img_url: String, + /// Indicates if this position is cash or equivalent. + pub is_cash: bool, +} + +/// Represents monetary data with user and raw currency values. +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct MonetaryDetails { + /// The monetary value in the user's native currency. + #[serde(rename = "userNativeCurrency")] + pub user_native_currency: Balance, + /// The raw monetary value in the specified currency. + #[serde(rename = "rawCurrency")] + pub raw_currency: Balance, +} + +/// Perpetual position details. +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct PerpPosition { + /// The product ID associated with the perpetual position. + pub product_id: String, + /// The UUID of the product. + pub product_uuid: String, + /// The symbol representing the perpetual position (e.g., BTC-PERP). + pub symbol: String, + /// URL of the asset's image. + pub asset_image_url: String, + /// The volume-weighted average price (VWAP). + pub vwap: MonetaryDetails, + /// The side of the position (e.g., long, short). + pub position_side: PositionSide, + /// The net size of the position. + pub net_size: f64, + /// Size of buy orders in the position. + pub buy_order_size: f64, + /// Size of sell orders in the position. + pub sell_order_size: f64, + /// Initial margin contribution for the position. + pub im_contribution: String, + /// Unrealized profit and loss for the position. + pub unrealized_pnl: MonetaryDetails, + /// The mark price of the position. + pub mark_price: MonetaryDetails, + /// The liquidation price of the position. + pub liquidation_price: MonetaryDetails, + /// Leverage used in the position. + pub leverage: String, + /// Initial margin notional value. + pub im_notional: MonetaryDetails, + /// Maintenance margin notional value. + pub mm_notional: MonetaryDetails, + /// Total notional value of the position. + pub position_notional: MonetaryDetails, + /// The margin type for the position (e.g., cross, isolated). + pub margin_type: MarginType, + /// The liquidation buffer for the position. + pub liquidation_buffer: String, + /// The liquidation percentage for the position. + pub liquidation_percentage: f64, +} + +/// Futures position details. +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct FuturesPosition { + /// The product ID associated with the futures position. + pub product_id: String, + /// The contract size of the futures position. + pub contract_size: String, + /// The side of the futures position (e.g., long, short). + pub side: PositionSide, + /// The amount of the futures position. + pub amount: f64, + /// The average entry price for the position. + pub avg_entry_price: f64, + /// The current price of the futures position. + pub current_price: f64, + /// Unrealized profit and loss for the futures position. + pub unrealized_pnl: String, + /// Expiry date of the futures contract. + pub expiry: String, + /// The underlying asset for the futures contract. + pub underlying_asset: String, + /// URL of the underlying asset's image. + pub asset_img_url: String, + /// The product name of the futures contract. + pub product_name: String, + /// The trading venue for the futures position. + pub venue: String, + /// The notional value of the futures position. + pub notional_value: String, +} + +/// Represents the breakdown of the portfolio returned by the API. +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct PortfolioBreakdown { + /// The portfolio associated with the breakdown. + pub portfolio: Portfolio, + /// Balances across different portfolio categories. + pub portfolio_balances: PortfolioBalances, + /// Spot positions held in the portfolio. + pub spot_positions: Vec, + /// Perpetual positions held in the portfolio. + pub perp_positions: Vec, + /// Futures positions held in the portfolio. + pub futures_positions: Vec, +} + +/// Create or Edit an existing portfolio. +#[derive(Serialize, Default, Debug)] +pub struct PortfolioModifyRequest { + /// New name of the portfolio. + pub name: String, +} + +impl Request for PortfolioModifyRequest { + fn check(&self) -> CbResult<()> { + if self.name.is_empty() { + return Err(CbError::BadRequest( + "portfolio name cannot be empty".to_string(), + )); + } + Ok(()) + } +} + +impl PortfolioModifyRequest { + /// Creates a new instance with the default values. + pub fn new(name: &str) -> Self { + Self { + name: name.to_string(), + } + } +} + +/// Parameters for moving funds between portfolios. +#[derive(Serialize, Debug)] +pub struct PortfolioMoveFundsRequest { + /// Funds to move between portfolios. + pub funds: Balance, + /// Portfolio funds to be removed from. + pub source_portfolio_uuid: String, + /// Portfolio funds to be added to. + pub target_portfolio_uuid: String, +} + +impl Request for PortfolioMoveFundsRequest { + fn check(&self) -> CbResult<()> { + if self.funds.value <= 0.0 { + return Err(CbError::BadRequest( + "funds to move must be greater than zero".to_string(), + )); + } else if self.funds.currency.is_empty() { + return Err(CbError::BadRequest( + "funds currency cannot be empty".to_string(), + )); + } else if self.source_portfolio_uuid.is_empty() { + return Err(CbError::BadRequest( + "source portfolio UUID cannot be empty".to_string(), + )); + } else if self.target_portfolio_uuid.is_empty() { + return Err(CbError::BadRequest( + "target portfolio UUID cannot be empty".to_string(), + )); + } + Ok(()) + } +} + +impl PortfolioMoveFundsRequest { + /// Creates a new instance of a request to move funds. + /// + /// # Arguements + /// + /// * `funds` - The amount of funds to move. + /// * `source_portfolio_uuid` - The UUID of the source portfolio. + //// * `target_portfolio_uuid` - The UUID of the target portfolio. + pub fn new(funds: &Balance, source_portfolio_uuid: &str, target_portfolio_uuid: &str) -> Self { + Self { + funds: funds.clone(), + source_portfolio_uuid: source_portfolio_uuid.to_string(), + target_portfolio_uuid: target_portfolio_uuid.to_string(), + } + } +} + +/// Query parameters for listing portfolios. +#[derive(Serialize, Default, Debug)] +pub struct PortfolioListQuery { + /// Type of portfolios to list. + pub portfolio_type: Option, +} + +impl Query for PortfolioListQuery { + fn check(&self) -> CbResult<()> { + if let Some(portfolio_type) = &self.portfolio_type { + if *portfolio_type == PortfolioType::Undefined { + return Err(CbError::BadQuery( + "portfolio type cannot be undefined".to_string(), + )); + } + } + Ok(()) + } + + fn to_query(&self) -> String { + QueryBuilder::new() + .push_optional("portfolio_type", &self.portfolio_type) + .build() + } +} + +impl PortfolioListQuery { + /// Creates a new instance with the default values. + pub fn new() -> Self { + Self::default() + } + + /// Sets the type of portfolios to list. + pub fn portfolio_type(mut self, portfolio_type: PortfolioType) -> Self { + self.portfolio_type = Some(portfolio_type); + self + } +} + +/// Query parameters for a portfolio breakdown. +#[derive(Serialize, Default, Debug)] +pub struct PortfolioBreakdownQuery { + /// Currency to use for the breakdown. + pub currency: Option, +} + +impl Query for PortfolioBreakdownQuery { + fn check(&self) -> CbResult<()> { + Ok(()) + } + + fn to_query(&self) -> String { + QueryBuilder::new() + .push_optional("currency", &self.currency) + .build() + } +} + +impl PortfolioBreakdownQuery { + /// Creates a new instance with the default values. + pub fn new() -> Self { + Self::default() + } + + /// Sets the currency to use for the breakdown. + pub fn currency(mut self, currency: &str) -> Self { + self.currency = Some(currency.to_string()); + self + } +} + +/// Response for creating or editing a portfolio. +#[derive(Deserialize, Debug)] +pub(crate) struct PortfolioWrapper { + /// Updated portfolio from the API. + pub(crate) portfolio: Portfolio, +} + +impl From for Portfolio { + fn from(wrapper: PortfolioWrapper) -> Self { + wrapper.portfolio + } +} + +/// Portfolio information returned from the API. +#[derive(Deserialize, Debug)] +pub(crate) struct PortfoliosWrapper { + pub(crate) portfolios: Vec, +} + +impl From for Vec { + fn from(wrapper: PortfoliosWrapper) -> Self { + wrapper.portfolios + } +} + +/// Represents a response for a portfolio breakdown. +#[derive(Serialize, Deserialize, Debug, Clone)] +pub(crate) struct PortfolioBreakdownWrapper { + /// The portfolio breakdown details. + pub(crate) breakdown: PortfolioBreakdown, +} + +impl From for PortfolioBreakdown { + fn from(wrapper: PortfolioBreakdownWrapper) -> Self { + wrapper.breakdown + } +} diff --git a/src/models/product.rs b/src/models/product.rs index 7215cb1..247ed8e 100644 --- a/src/models/product.rs +++ b/src/models/product.rs @@ -4,59 +4,97 @@ //! This allows you to obtain product information such as: Ticker (Market Trades), Product and //! Currency information, Product Book, and Best Bids and Asks for multiple products. +use core::fmt; + use serde::{Deserialize, Serialize}; +use serde_with::{serde_as, DefaultOnError, DisplayFromStr}; +use crate::constants::products::CANDLE_MAXIMUM; +use crate::errors::CbError; +use crate::time::{self, Granularity}; use crate::traits::Query; -use crate::utils::{deserialize_numeric, QueryBuilder}; +use crate::types::CbResult; +use crate::utils::QueryBuilder; -/// Represents a Product received from the Websocket API. -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct ProductUpdate { - /// Type of the product. - pub product_type: String, - /// ID of the product. - pub id: String, - /// Symbol of the base currency. - pub base_currency: String, - /// Symbol of the quote currency. - pub quote_currency: String, - /// Minimum amount base value can be increased or decreased at once. - #[serde(deserialize_with = "deserialize_numeric")] - pub base_increment: f64, - /// Minimum amount quote value can be increased or decreased at once. - #[serde(deserialize_with = "deserialize_numeric")] - pub quote_increment: f64, - /// Name of the product. - pub display_name: String, - /// Status of the product. - pub status: String, - /// Additional status message. - pub status_message: String, - /// Minimum amount of funds. - #[serde(deserialize_with = "deserialize_numeric")] - pub min_market_funds: f64, +use super::order::OrderSide; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum ProductType { + /// Unknown product type. + #[serde(rename = "UNKNOWN_PRODUCT_TYPE")] + Unknown, + /// Spot product type. + Spot, + /// Future product type. + Future, } -/// Represents a Market Trade received from the Websocket API. +impl fmt::Display for ProductType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.as_ref()) + } +} + +impl AsRef for ProductType { + fn as_ref(&self) -> &str { + match self { + ProductType::Unknown => "UNKNOWN_PRODUCT_TYPE", + ProductType::Spot => "SPOT", + ProductType::Future => "FUTURE", + } + } +} + +/// Represents the trading session state. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub enum SessionState { + #[serde(rename = "FCM_TRADING_SESSION_STATE_UNDEFINED")] + Undefined, + #[serde(rename = "FCM_TRADING_SESSION_STATE_PRE_OPEN")] + PreOpen, + #[serde(rename = "FCM_TRADING_SESSION_STATE_PRE_OPEN_NO_CANCEL")] + PreOpenNoCancel, + #[serde(rename = "FCM_TRADING_SESSION_STATE_OPEN")] + Open, + #[serde(rename = "FCM_TRADING_SESSION_STATE_CLOSE")] + Close, +} + +/// Reasons for a trading session to close. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub enum CloseReason { + #[serde(rename = "FCM_TRADING_SESSION_CLOSED_REASON_UNDEFINED")] + Undefined, + #[serde(rename = "FCM_TRADING_SESSION_CLOSED_REASON_REGULAR_MARKET_CLOSE")] + RegularMarketClose, + #[serde(rename = "FCM_TRADING_SESSION_CLOSED_REASON_EXCHANGE_MAINTENANCE")] + ExchangeMaintenance, + #[serde(rename = "FCM_TRADING_SESSION_CLOSED_REASON_VENDOR_MAINTENANCE")] + VendorMaintenance, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum ProductVenue { + #[serde(rename = "UNKNOWN_VENUE_TYPE")] + Unknown, + Cbe, + Fcm, + Intx, +} + +/// Fcm specific scheduled maintenance details. #[derive(Serialize, Deserialize, Debug, Clone)] -pub struct MarketTradesUpdate { - /// Trade identity. - pub trade_id: String, - /// ID of the product. - pub product_id: String, - /// Price of the product. - #[serde(deserialize_with = "deserialize_numeric")] - pub price: f64, - /// Size for the trade. - #[serde(deserialize_with = "deserialize_numeric")] - pub size: f64, - /// Side: BUY or SELL. - pub side: String, - /// Time for the market trade. - pub time: String, +pub struct Maintenance { + /// Start time of the maintenance. + pub start: String, + /// End time of the maintenance. + pub end: String, } /// Session details for the product. +#[serde_as] #[derive(Serialize, Deserialize, Debug, Clone)] pub struct SessionDetails { /// Whether or not the session is currently open. @@ -65,6 +103,16 @@ pub struct SessionDetails { pub open_time: String, /// Time the session closed. pub close_time: String, + /// The current state of the session. + pub session_state: SessionState, + /// Whether or not after-hours order entry + pub after_hours_order_entry_disabled: bool, + /// Reason the session closed. + pub closed_reason: CloseReason, + /// Whether or not the session is in maintenance. + #[serde_as(as = "DefaultOnError")] + #[serde(default)] + pub maintenance: Option, } /// Perpetual details for the product. @@ -73,6 +121,9 @@ pub struct PerpetualDetails { pub open_interest: String, pub funding_rate: String, pub funding_time: String, + pub max_leverage: String, + pub base_asset_uuid: String, + pub underlying_type: String, } /// Future details for the product. @@ -95,42 +146,47 @@ pub struct FutureDetails { pub perpetual_details: Option, /// Name of the contract. pub contract_display_name: String, + pub time_to_expiry_ms: String, + pub non_crypto: bool, + pub contract_expiry_name: String, + pub twenty_four_by_seven: bool, } /// Represents a Product received from the REST API. +#[serde_as] #[derive(Serialize, Deserialize, Debug, Clone)] pub struct Product { /// The trading pair. pub product_id: String, /// The current price for the product, in quote currency. - #[serde(deserialize_with = "deserialize_numeric")] + #[serde_as(as = "DisplayFromStr")] pub price: f64, /// The amount the price of the product has changed, in percent, in the last 24 hours. - #[serde(deserialize_with = "deserialize_numeric")] + #[serde_as(as = "DisplayFromStr")] pub price_percentage_change_24h: f64, /// The trading volume for the product in the last 24 hours. - #[serde(deserialize_with = "deserialize_numeric")] + #[serde_as(as = "DisplayFromStr")] pub volume_24h: f64, /// The percentage amount the volume of the product has changed in the last 24 hours. - #[serde(deserialize_with = "deserialize_numeric")] + #[serde_as(as = "DisplayFromStr")] pub volume_percentage_change_24h: f64, /// Minimum amount base value can be increased or decreased at once. - #[serde(deserialize_with = "deserialize_numeric")] + #[serde_as(as = "DisplayFromStr")] pub base_increment: f64, /// Minimum amount quote value can be increased or decreased at once. - #[serde(deserialize_with = "deserialize_numeric")] + #[serde_as(as = "DisplayFromStr")] pub quote_increment: f64, /// Minimum size that can be represented of quote currency. - #[serde(deserialize_with = "deserialize_numeric")] + #[serde_as(as = "DisplayFromStr")] pub quote_min_size: f64, /// Maximum size that can be represented of quote currency. - #[serde(deserialize_with = "deserialize_numeric")] + #[serde_as(as = "DisplayFromStr")] pub quote_max_size: f64, /// Minimum size that can be represented of base currency. - #[serde(deserialize_with = "deserialize_numeric")] + #[serde_as(as = "DisplayFromStr")] pub base_min_size: f64, /// Maximum size that can be represented of base currency. - #[serde(deserialize_with = "deserialize_numeric")] + #[serde_as(as = "DisplayFromStr")] pub base_max_size: f64, /// Name of the base currency. pub base_name: String, @@ -155,7 +211,7 @@ pub struct Product { /// Whether or not the product is in auction mode. pub auction_mode: bool, /// Possible values: [SPOT, FUTURE] - pub product_type: String, + pub product_type: ProductType, /// Symbol of the quote currency. pub quote_currency_id: String, /// Symbol of the base currency. @@ -175,20 +231,29 @@ pub struct Product { /// Whether or not the product is in view only mode. pub view_only: bool, /// Minimum amount price can be increased or decreased at once. - #[serde(deserialize_with = "deserialize_numeric")] + #[serde_as(as = "DisplayFromStr")] pub price_increment: f64, + /// Display name of the product. + pub display_name: String, + /// The sole venue id for the product. Defaults to CBE if the product is not specific to a single venue + pub product_venue: ProductVenue, + /// Approximate 24-hour trading volume in quote currency. + #[serde_as(as = "DefaultOnError")] + #[serde(default)] + pub approximate_quote_24h_volume: f64, /// Future product details. pub future_product_details: Option, } /// Represents a Bid or an Ask entry for a product. +#[serde_as] #[derive(Serialize, Deserialize, Debug, Clone)] pub struct BidAsk { /// Current bid or ask price. - #[serde(deserialize_with = "deserialize_numeric")] + #[serde_as(as = "DisplayFromStr")] pub price: f64, /// Current bid or ask size. - #[serde(deserialize_with = "deserialize_numeric")] + #[serde_as(as = "DisplayFromStr")] pub size: f64, } @@ -203,32 +268,42 @@ pub struct ProductBook { pub bids: Vec, /// Array of current asks. pub asks: Vec, + #[serde(default)] + pub last: String, + #[serde(default)] + pub mid_market: String, + #[serde(default)] + pub spread_bps: String, + #[serde(default)] + pub spread_absolute: String, } /// Represents a candle for a product. +#[serde_as] #[derive(Serialize, Deserialize, Debug, Clone)] pub struct Candle { /// Timestamp for bucket start time, in UNIX time. - #[serde(deserialize_with = "deserialize_numeric")] + #[serde_as(as = "DisplayFromStr")] pub start: u64, /// Lowest price during the bucket interval. - #[serde(deserialize_with = "deserialize_numeric")] + #[serde_as(as = "DisplayFromStr")] pub low: f64, /// Highest price during the bucket interval. - #[serde(deserialize_with = "deserialize_numeric")] + #[serde_as(as = "DisplayFromStr")] pub high: f64, /// Opening price (first trade) in the bucket interval. - #[serde(deserialize_with = "deserialize_numeric")] + #[serde_as(as = "DisplayFromStr")] pub open: f64, /// Closing price (last trade) in the bucket interval. - #[serde(deserialize_with = "deserialize_numeric")] + #[serde_as(as = "DisplayFromStr")] pub close: f64, /// Volume of trading activity during the bucket interval. - #[serde(deserialize_with = "deserialize_numeric")] + #[serde_as(as = "DisplayFromStr")] pub volume: f64, } /// Represents a trade for a product. +#[serde_as] #[derive(Serialize, Deserialize, Debug, Clone)] pub struct Trade { /// The ID of the trade that was placed. @@ -236,175 +311,465 @@ pub struct Trade { /// The trading pair. pub product_id: String, /// The price of the trade, in quote currency. - #[serde(deserialize_with = "deserialize_numeric")] + #[serde_as(as = "DisplayFromStr")] pub price: f64, /// The size of the trade, in base currency. - #[serde(deserialize_with = "deserialize_numeric")] + #[serde_as(as = "DisplayFromStr")] pub size: f64, /// The time of the trade. pub time: String, /// Possible values: [UNKNOWN_ORDER_SIDE, BUY, SELL] - pub side: String, - /// The best bid for the `product_id`, in quote currency. - /// NOTE: (20230705) API gives an empty string not a number. - pub bid: String, - /// The best ask for the `product_id`, in quote currency. - /// NOTE: (20230705) API gives an empty string not a number. - pub ask: String, -} - -/// Represents a Candle update received from the Websocket API. -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct CandleUpdate { - /// Product ID (Pair, ex 'BTC-USD') - pub product_id: String, - /// Candle for the update. - #[serde(flatten)] - pub data: Candle, -} - -/// Represents a Ticker update received from the Websocket API. -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct TickerUpdate { - /// Ticker update type. - pub r#type: String, - /// Product ID (Pair, ex 'BTC-USD') - pub product_id: String, - /// Current price for the product. - #[serde(deserialize_with = "deserialize_numeric")] - pub price: f64, - /// 24hr Volume for the product. - #[serde(deserialize_with = "deserialize_numeric")] - pub volume_24_h: f64, - /// 24hr Lowest price. - #[serde(deserialize_with = "deserialize_numeric")] - pub low_24_h: f64, - /// 24hr Highest price. - #[serde(deserialize_with = "deserialize_numeric")] - pub high_24_h: f64, - /// 52w (52 weeks) Lowest price. - #[serde(deserialize_with = "deserialize_numeric")] - pub low_52_w: f64, - /// 52w (52 weeks) Highest price. - #[serde(deserialize_with = "deserialize_numeric")] - pub high_52_w: f64, - /// 24hr Price percentage change. - #[serde(deserialize_with = "deserialize_numeric")] - pub price_percent_chg_24_h: f64, + pub side: OrderSide, + /// The exchange where the trade was placed. + pub exchange: String, } /// Represents a ticker for a product. +#[serde_as] #[derive(Serialize, Deserialize, Debug, Clone)] pub struct Ticker { /// List of trades for the product. pub trades: Vec, /// The best bid for the `product_id`, in quote currency. - #[serde(deserialize_with = "deserialize_numeric")] + #[serde_as(as = "DisplayFromStr")] pub best_bid: f64, /// The best ask for the `product_id`, in quote currency. - #[serde(deserialize_with = "deserialize_numeric")] + #[serde_as(as = "DisplayFromStr")] pub best_ask: f64, } -/// Represents a list of Products received from the API. -#[derive(Deserialize, Debug)] -pub(crate) struct ListProductsResponse { - /// Array of objects, each representing one product. - pub(crate) products: Vec, - // Number of products that were returned. - // NOTE: Disabled because `.len()` exists on the vector. - // num_products: i32, -} - -/// Represents a candle response from the API. -#[derive(Deserialize, Debug)] -pub(crate) struct CandleResponse { - /// Array of candles for the product. - pub(crate) candles: Vec, -} - -/// Represents a best bid and ask response from the API. -#[derive(Deserialize, Debug)] -pub(crate) struct BidAskResponse { - /// Array of product books. - pub(crate) pricebooks: Vec, -} - -/// Represents a product book response from the API. -#[derive(Deserialize, Debug)] -pub(crate) struct ProductBookResponse { - /// Price book for the product. - pub(crate) pricebook: ProductBook, -} - /// Represents parameters that are optional for List Products API request. #[derive(Serialize, Default, Debug)] -pub struct ListProductsQuery { +pub struct ProductListQuery { /// A limit describing how many products to return. pub limit: Option, /// Number of products to offset before returning. pub offset: Option, /// Type of products to return. Valid options: SPOT or FUTURE - pub product_type: Option, + pub product_type: Option, /// List of product IDs to return. pub product_ids: Option>, + /// If true, return all products of all product types (including expired futures contracts). + pub get_all_products: Option, + /// Whether or not to populate view_only with the tradability status of the product. This is only enabled for SPOT products. + pub get_tradability_status: Option, } -impl Query for ListProductsQuery { - /// Converts the object into HTTP request parameters. +impl Query for ProductListQuery { + fn check(&self) -> CbResult<()> { + if let Some(limit) = self.limit { + if limit == 0 { + return Err(CbError::BadQuery( + "limit must be greater than 0".to_string(), + )); + } + } else if let Some(offset) = self.offset { + if offset == 0 { + return Err(CbError::BadQuery( + "offset must be greater than 0".to_string(), + )); + } + } else if let Some(product_type) = &self.product_type { + if *product_type == ProductType::Unknown { + return Err(CbError::BadQuery( + "product_type cannot be unknown".to_string(), + )); + } + } + Ok(()) + } + fn to_query(&self) -> String { QueryBuilder::new() - .push_u32_optional("limit", self.limit) - .push_u32_optional("offset", self.offset) + .push_optional("limit", &self.limit) + .push_optional("offset", &self.offset) .push_optional("product_type", &self.product_type) - .with_optional_vec("product_ids", &self.product_ids) + .push_optional_vec("product_ids", &self.product_ids) + .push_optional("get_all_products", &self.get_all_products) + .push_optional("get_tradability_status", &self.get_tradability_status) .build() } } +impl ProductListQuery { + /// Creates a new ProductListQuery object with default values. + pub fn new() -> Self { + Self::default() + } + + /// Number of products to return. + pub fn limit(mut self, limit: u32) -> Self { + self.limit = Some(limit); + self + } + + /// Number of products to offset before returning. + pub fn offset(mut self, offset: u32) -> Self { + self.offset = Some(offset); + self + } + + /// Type of products to return. Valid options: SPOT or FUTURE. + pub fn product_type(mut self, product_type: ProductType) -> Self { + self.product_type = Some(product_type); + self + } + + /// List of product IDs to return. + pub fn product_ids(mut self, product_ids: &[String]) -> Self { + self.product_ids = Some(product_ids.to_vec()); + self + } + + /// If true, return all products of all product types (including expired futures contracts. + pub fn get_all_products(mut self, get_all_products: bool) -> Self { + self.get_all_products = Some(get_all_products); + self + } + + /// Whether or not to populate view_only with the tradability status of the product. This is only enabled for SPOT products. + pub fn get_tradability_status(mut self, get_tradability_status: bool) -> Self { + self.get_tradability_status = Some(get_tradability_status); + self + } +} + /// Represents parameters for Ticker Product API request. #[derive(Serialize, Debug)] -pub struct TickerQuery { +pub struct ProductTickerQuery { /// Number of trades to return. pub limit: u32, + /// The UNIX timestamp indicating the start of the time interval. + pub start: Option, + /// The UNIX timestamp indicating the end of the time interval. + pub end: Option, } -impl Query for TickerQuery { +impl Query for ProductTickerQuery { + fn check(&self) -> CbResult<()> { + if self.limit == 0 { + return Err(CbError::BadQuery( + "limit must be greater than 0".to_string(), + )); + } else if let (Some(start), Some(end)) = (&self.start, &self.end) { + if start >= end { + return Err(CbError::BadQuery("start must be less than end".to_string())); + } + } + Ok(()) + } + /// Converts the object into HTTP request parameters. fn to_query(&self) -> String { - QueryBuilder::new().push("limit", self.limit).build() + QueryBuilder::new() + .push("limit", self.limit) + .push_optional("start", &self.start) + .push_optional("end", &self.end) + .build() + } +} + +impl Default for ProductTickerQuery { + fn default() -> Self { + Self { + limit: 100, + start: None, + end: None, + } + } +} + +impl ProductTickerQuery { + /// Creates a new ProductTickerQuery object with default values. + /// + /// # Arguments + /// + /// * `limit` - Number of trades to return. + pub fn new(limit: u32) -> Self { + Self { + limit, + ..Default::default() + } + } + + /// Number of trades to return. + pub fn limit(mut self, limit: u32) -> Self { + self.limit = limit; + self + } + + /// The UNIX timestamp indicating the start of the time interval. + pub fn start(mut self, start: &str) -> Self { + self.start = Some(start.to_string()); + self + } + + /// The UNIX timestamp indicating the end of the time interval. + pub fn end(mut self, end: &str) -> Self { + self.end = Some(end.to_string()); + self } } /// Represents parameters for Ticker Product API request. -#[derive(Serialize, Debug)] -pub struct BidAskQuery { +#[derive(Serialize, Debug, Default)] +pub struct ProductBidAskQuery { + /// The list of trading pairs (e.g. 'BTC-USD'). pub product_ids: Vec, } -impl Query for BidAskQuery { - /// Converts the object into HTTP request parameters. +impl Query for ProductBidAskQuery { + fn check(&self) -> CbResult<()> { + Ok(()) + } + fn to_query(&self) -> String { QueryBuilder::new() - .with_optional_vec("product_ids", &Some(self.product_ids.clone())) + .push_optional_vec("product_ids", &Some(self.product_ids.clone())) .build() } } +impl ProductBidAskQuery { + /// Creates a new ProductBidAskQuery object with default values. + pub fn new() -> Self { + Self::default() + } + + /// The list of trading pairs (e.g. 'BTC-USD'). + pub fn product_ids(mut self, product_ids: &[String]) -> Self { + self.product_ids = product_ids.to_vec(); + self + } +} + /// Represents parameters for Ticker Product API request. -#[derive(Serialize, Debug)] +#[derive(Serialize, Debug, Default)] pub struct ProductBookQuery { + /// The trading pair (e.g. 'BTC-USD'). pub product_id: String, - /// Number of products to return. + /// The number of bid/asks to be returned. pub limit: Option, + /// The minimum price intervals at which buy and sell orders are grouped or combined in the order book. + pub aggregation_price_increment: Option, } impl Query for ProductBookQuery { - /// Converts the object into HTTP request parameters. + fn check(&self) -> CbResult<()> { + if self.product_id.is_empty() { + return Err(CbError::BadQuery("product_id is required".to_string())); + } else if let Some(limit) = self.limit { + if limit == 0 { + return Err(CbError::BadQuery( + "limit must be greater than 0".to_string(), + )); + } + } else if let Some(aggregation_price_increment) = self.aggregation_price_increment { + if aggregation_price_increment <= 0.0 { + return Err(CbError::BadQuery( + "aggregation_price_increment must be greater than 0".to_string(), + )); + } + } + Ok(()) + } + fn to_query(&self) -> String { QueryBuilder::new() .push("product_id", &self.product_id) - .push_u32_optional("limit", self.limit) + .push_optional("limit", &self.limit) + .push_optional( + "aggregation_price_increment", + &self.aggregation_price_increment, + ) .build() } } + +impl ProductBookQuery { + /// Creates a new ProductBookQuery object with default values. + /// + /// # Arguments + /// + /// * `product_id` - The trading pair (e.g. 'BTC-USD'). + pub fn new(product_id: &str) -> Self { + Self { + product_id: product_id.to_string(), + ..Default::default() + } + } + + /// The trading pair (e.g. 'BTC-USD'). + pub fn product_id(mut self, product_id: &str) -> Self { + self.product_id = product_id.to_string(); + self + } + + /// The number of bid/asks to be returned. + pub fn limit(mut self, limit: u32) -> Self { + self.limit = Some(limit); + self + } + + /// The minimum price intervals at which buy and sell orders are grouped or combined in the order book. + pub fn aggregation_price_increment(mut self, aggregation_price_increment: f64) -> Self { + self.aggregation_price_increment = Some(aggregation_price_increment); + self + } +} + +/// Represents parameters for Candles Product API request. +/// +/// # Required Parameters +/// +/// * `start` - The start time of the time range. +/// * `end` - The end time of the time range. +/// * `granularity` - The granularity of the candles. +#[derive(Serialize, Debug, Clone)] +pub struct ProductCandleQuery { + /// The start time of the time range. + pub start: u64, + /// The end time of the time range. + pub end: u64, + /// The granularity of the candles. + pub granularity: Granularity, + /// The number of candles to return. Maximum is 350. + pub limit: u32, +} + +impl Query for ProductCandleQuery { + fn check(&self) -> CbResult<()> { + if self.limit == 0 { + return Err(CbError::BadQuery( + "limit must be greater than 0".to_string(), + )); + } else if self.start >= self.end { + return Err(CbError::BadQuery("start must be less than end".to_string())); + } else if self.granularity == Granularity::Unknown { + return Err(CbError::BadQuery( + "granularity cannot be unknown or unset".to_string(), + )); + } + Ok(()) + } + + fn to_query(&self) -> String { + QueryBuilder::new() + .push("start", self.start) + .push("end", self.end) + .push("granularity", &self.granularity) + .push("limit", self.limit) + .build() + } +} + +impl Default for ProductCandleQuery { + fn default() -> Self { + Self { + start: time::now() - Granularity::to_secs(&Granularity::OneDay) as u64, + end: time::now(), + granularity: Granularity::FiveMinute, + limit: CANDLE_MAXIMUM, + } + } +} + +impl ProductCandleQuery { + /// Creates a new ProductCandleQuery object with default values. + /// + /// # Arguments + /// + /// * `start` - The start time of the time range. + /// * `end` - The end time of the time range. + /// * `granularity` - The granularity of the candles. + pub fn new(start: u64, end: u64, granularity: Granularity) -> Self { + Self { + start, + end, + granularity, + limit: CANDLE_MAXIMUM, + } + } + + /// The start time of the time range. + /// Note: This is a required field. + pub fn start(mut self, start: u64) -> Self { + self.start = start; + self + } + + /// The end time of the time range. + /// Note: This is a required field. + pub fn end(mut self, end: u64) -> Self { + self.end = end; + self + } + + /// The granularity of the candles. + /// Note: This is a required field. + pub fn granularity(mut self, granularity: Granularity) -> Self { + self.granularity = granularity; + self + } + + /// The number of candles to return. Maximum is 350. + pub fn limit(mut self, limit: u32) -> Self { + self.limit = limit; + self + } +} + +/// Represents a list of Products received from the API. +#[derive(Deserialize, Debug)] +pub(crate) struct ProductsWrapper { + /// Array of objects, each representing one product. + pub(crate) products: Vec, + // Number of products that were returned. + // NOTE: Disabled because `.len()` exists on the vector. + // num_products: i32, +} + +impl From for Vec { + fn from(wrapper: ProductsWrapper) -> Self { + wrapper.products + } +} + +/// Represents a candle response from the API. +#[derive(Deserialize, Debug)] +pub(crate) struct CandlesWrapper { + /// Array of candles for the product. + pub(crate) candles: Vec, +} + +impl From for Vec { + fn from(wrapper: CandlesWrapper) -> Self { + wrapper.candles + } +} + +/// Represents a best bid and ask response from the API. +#[derive(Deserialize, Debug)] +pub(crate) struct ProductBooksWrapper { + /// Array of product books. + pub(crate) pricebooks: Vec, +} + +impl From for Vec { + fn from(wrapper: ProductBooksWrapper) -> Self { + wrapper.pricebooks + } +} + +/// Represents a product book response from the API. +#[derive(Deserialize, Debug)] +pub(crate) struct ProductBookWrapper { + /// Price book for the product. + pub(crate) pricebook: ProductBook, +} + +impl From for ProductBook { + fn from(wrapper: ProductBookWrapper) -> Self { + wrapper.pricebook + } +} diff --git a/src/models/util.rs b/src/models/public.rs similarity index 66% rename from src/models/util.rs rename to src/models/public.rs index b8eaf7a..716eacb 100644 --- a/src/models/util.rs +++ b/src/models/public.rs @@ -1,23 +1,23 @@ -//! # Coinbase Advanced Util API +//! # Coinbase Advanced Public API //! -//! `util` gives access to the Util API and the various endpoints associated with it. +//! `public` gives access to the Public API and the various endpoints associated with it. //! Some of the features include getting the API current time in ISO format. use serde::{Deserialize, Serialize}; - -use crate::utils::deserialize_numeric; +use serde_with::{serde_as, DisplayFromStr}; /// Get the current time from the Coinbase Advanced API. +#[serde_as] #[derive(Serialize, Deserialize, Debug, Clone)] -pub struct UnixTime { +pub struct ServerTime { /// An ISO-8601 representation of the timestamp. pub iso: String, /// A second-precision representation of the timestamp. #[serde(rename(deserialize = "epochSeconds"))] - #[serde(deserialize_with = "deserialize_numeric")] + #[serde_as(as = "DisplayFromStr")] pub epoch_seconds: u64, /// A millisecond-precision representation of the timestamp. #[serde(rename(deserialize = "epochMillis"))] - #[serde(deserialize_with = "deserialize_numeric")] + #[serde_as(as = "DisplayFromStr")] pub epoch_millis: u64, } diff --git a/src/models/shared.rs b/src/models/shared.rs new file mode 100644 index 0000000..8ed75d7 --- /dev/null +++ b/src/models/shared.rs @@ -0,0 +1,24 @@ +//! # Coinbase Advanced Account API +//! +//! `shared` gives access to utilities that will be reused throughout the API and user. + +use serde::{Deserialize, Serialize}; +use serde_with::{serde_as, DisplayFromStr}; + +/// Represents a Balance for either Available or Held funds. +#[serde_as] +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct Balance { + /// Value for the currency available or held. + #[serde_as(as = "DisplayFromStr")] + pub value: f64, + /// Denomination of the currency. + pub currency: String, +} + +impl Balance { + /// Creates a new Balance object that represents the value and currency. + pub fn new(value: f64, currency: String) -> Self { + Self { value, currency } + } +} diff --git a/src/models/websocket.rs b/src/models/websocket.rs deleted file mode 100644 index 8405710..0000000 --- a/src/models/websocket.rs +++ /dev/null @@ -1,251 +0,0 @@ -//! # Coinbase Advanced Websocket Client -//! -//! `websocket` gives access to the websocket stream to receive updates in a streamlined fashion. -//! Many parts of the REST API suggest using websockets instead due to ratelimits and being quicker -//! for large amount of constantly changing data. - -use std::fmt; - -use serde::{Deserialize, Serialize}; - -use crate::models::order::OrderUpdate; -use crate::models::product::{CandleUpdate, MarketTradesUpdate, ProductUpdate, TickerUpdate}; -use crate::utils::deserialize_numeric; - -/// WebSocket Channels that can be subscribed to. -#[derive(Serialize, Deserialize, Debug)] -pub enum Channel { - /// Sends all products and currencies on a preset interval. - Status, - /// Updates every second. Candles are grouped into buckets (granularities) of five minutes. - Candles, - /// Real-time price updates every time a match happens. - Ticker, - /// Real-time price updates every 5000 milli-seconds. - TickerBatch, - /// All updates and easiest way to keep order book snapshot - Level2, - /// Only sends messages that include the authenticated user. - User, - /// Real-time updates every time a market trade happens. - MarketTrades, - /// Real-time pings from server to keep connections open. - Heartbeats, -} - -impl fmt::Display for Channel { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - Channel::Status => write!(f, "status"), - Channel::Candles => write!(f, "candles"), - Channel::Ticker => write!(f, "ticker"), - Channel::TickerBatch => write!(f, "ticker_batch"), - Channel::Level2 => write!(f, "level2"), - Channel::User => write!(f, "user"), - Channel::MarketTrades => write!(f, "market_trades"), - Channel::Heartbeats => write!(f, "heartbeats"), - } - } -} - -/// Messages that could be received from the WebSocket. -#[derive(Deserialize, Debug)] -#[serde(untagged)] -pub enum Message { - /// Sends all products and currencies on a preset interval. - Status(StatusMessage), - /// Updates every second. Candles are grouped into buckets (granularities) of five minutes. - Candles(CandlesMessage), - /// Real-time price updates every time a match happens. - Ticker(TickerMessage), - /// All updates and easiest way to keep order book snapshot - TickerBatch(TickerMessage), - /// All updates and easiest way to keep order book snapshot - Level2(Level2Message), - /// Only sends messages that include the authenticated user. - User(UserMessage), - /// Real-time updates every time a market trade happens. - MarketTrades(MarketTradesMessage), - /// Real-time pings from server to keep connections open. - Heartbeats(HeartbeatsMessage), - /// Subscription updates. - Subscribe(SubscribeMessage), -} - -/// Data received from the WebSocket for Level2 Events. -#[derive(Deserialize, Debug)] -pub struct Level2Update { - pub side: String, - pub event_time: String, - #[serde(deserialize_with = "deserialize_numeric")] - pub price_level: f64, - #[serde(deserialize_with = "deserialize_numeric")] - pub new_quantity: f64, -} - -/// Data received from the WebSocket for Subscription Update Events. -#[derive(Deserialize, Debug, Default)] -pub struct SubscribeUpdate { - #[serde(default)] - pub status: Vec, - #[serde(default)] - pub ticker: Vec, - #[serde(default)] - pub ticker_batch: Vec, - #[serde(default)] - pub level2: Option>, - #[serde(default)] - pub user: Option>, - #[serde(default)] - pub market_trades: Option>, - #[serde(default)] - pub heartbeats: Option>, -} - -/// Status Event received from the WebSocket, contained inside the Status Message. -#[derive(Deserialize, Debug)] -pub struct StatusEvent { - pub r#type: String, - pub products: Vec, -} - -/// Candles Event received from the WebSocket, contained inside the Candles Message. -#[derive(Deserialize, Debug)] -pub struct CandlesEvent { - pub r#type: String, - pub candles: Vec, -} - -/// Ticker Event received from the WebSocket, contained inside the Ticker Message. -#[derive(Deserialize, Debug)] -pub struct TickerEvent { - pub r#type: String, - pub tickers: Vec, -} - -/// Level2 Event received from the WebSocket, contained inside the Level2 Message. -#[derive(Deserialize, Debug)] -pub struct Level2Event { - pub r#type: String, - pub product_id: String, - pub updates: Vec, -} - -/// User Event received from the WebSocket, contained inside the User Message. -#[derive(Deserialize, Debug)] -pub struct UserEvent { - pub r#type: String, - pub orders: Vec, -} - -/// Market Trades Event received from the WebSocket, contained inside the Market Trades Message. -#[derive(Deserialize, Debug)] -pub struct MarketTradesEvent { - pub r#type: String, - pub trades: Vec, -} - -/// Heartbeats Event received from the WebSocket, contained inside the Heartbeats Message. -#[derive(Deserialize, Debug)] -pub struct HeartbeatsEvent { - pub current_time: String, - pub heartbeat_counter: u64, -} - -/// Subscribe Event received from the WebSocket, contained inside the Subscribe Message. -#[derive(Deserialize, Debug)] -pub struct SubscribeEvent { - pub subscriptions: SubscribeUpdate, -} - -/// Message received from the WebSocket API. Contains updates on product statuses. -#[derive(Deserialize, Debug)] -pub struct StatusMessage { - pub channel: String, - pub client_id: String, - pub timestamp: String, - pub sequence_num: u64, - pub events: Vec, -} - -/// Message received from the WebSocket API. Contains updates on candles. -#[derive(Deserialize, Debug)] -pub struct CandlesMessage { - pub channel: String, - pub client_id: String, - pub timestamp: String, - pub sequence_num: u64, - pub events: Vec, -} - -/// Message received from the WebSocket API. Contains updates on products and currencies. -#[derive(Deserialize, Debug)] -pub struct TickerMessage { - pub channel: String, - pub client_id: String, - pub timestamp: String, - pub sequence_num: u64, - pub events: Vec, -} - -/// Message received from the WebSocket API. All order updates for a products. Best way to -/// keep a snapshot of the order book. -#[derive(Deserialize, Debug)] -pub struct Level2Message { - pub channel: String, - pub client_id: String, - pub timestamp: String, - pub sequence_num: u64, - pub events: Vec, -} - -/// Message received from the WebSocket API. Contains order updates strictly for the user. -#[derive(Deserialize, Debug)] -pub struct UserMessage { - pub channel: String, - pub client_id: String, - pub timestamp: String, - pub sequence_num: u64, - pub events: Vec, -} - -/// Message received from the WebSocket API. Real-time updates everytime a market trade happens. -#[derive(Deserialize, Debug)] -pub struct MarketTradesMessage { - pub channel: String, - pub client_id: String, - pub timestamp: String, - pub sequence_num: u64, - pub events: Vec, -} - -/// Message received from the WebSocket API. Real-time pings from the server to keep connections -/// open. -#[derive(Deserialize, Debug)] -pub struct HeartbeatsMessage { - pub channel: String, - pub client_id: String, - pub timestamp: String, - pub sequence_num: u64, - pub events: Vec, -} - -/// Message received from the WebSocket API. Provides updates for the current subscriptions. -#[derive(Deserialize, Debug)] -pub struct SubscribeMessage { - pub channel: String, - pub client_id: String, - pub timestamp: String, - pub sequence_num: u64, - pub events: Vec, -} - -/// Subscription is sent to the WebSocket to enable updates for specified Channels. -#[derive(Serialize, Debug)] -pub(crate) struct Subscription { - pub(crate) r#type: String, - pub(crate) product_ids: Vec, - pub(crate) channel: String, - pub(crate) jwt: String, - pub(crate) timestamp: String, -} diff --git a/src/models/websocket/enums.rs b/src/models/websocket/enums.rs new file mode 100644 index 0000000..8706281 --- /dev/null +++ b/src/models/websocket/enums.rs @@ -0,0 +1,66 @@ +use serde::{Deserialize as SerdeDeserialize, Serialize}; + +use crate::types::WebSocketReader; + +use super::{SecureSubscription, UnsignedSubscription}; + +/// WebSocket Channels that can be subscribed to. +#[derive(Serialize, SerdeDeserialize, PartialEq, Debug, Eq, Hash, Clone)] +#[serde(rename_all = "snake_case")] +pub enum Channel { + /// Sends all products and currencies on a preset interval. + Status, + /// Updates every second. Candles are grouped into buckets (granularities) of five minutes. + Candles, + /// Real-time price updates every time a match happens. + Ticker, + /// Real-time price updates every 5000 milli-seconds. + TickerBatch, + /// All updates and easiest way to keep order book snapshot + Level2, + /// Real-time updates every time a market trade happens. + MarketTrades, + /// Real-time pings from server to keep connections open. + Heartbeats, + /// Only sends messages that include the authenticated user. + User, + /// Real-time updates every time a user's futures balance changes. + FuturesBalanceSummary, + /// Updates to subscription status. + Subscriptions, +} + +#[derive(Serialize, SerdeDeserialize, PartialEq, Debug)] +#[serde(rename_all = "snake_case")] +pub enum EventType { + Snapshot, + Update, +} + +#[derive(Serialize, SerdeDeserialize, PartialEq, Debug)] +#[serde(rename_all = "snake_case")] +pub enum Level2Side { + Bid, + Ask, +} + +/// Types for the endpoints. +#[derive(PartialEq, Debug, Eq, Clone, Hash)] +pub enum EndpointType { + Public, + User, +} + +/// WebSocket Reader Endpoints. +#[derive(Debug)] +pub enum Endpoint { + Public((EndpointType, WebSocketReader)), + User((EndpointType, WebSocketReader)), +} + +#[derive(Serialize, Debug)] +#[serde(untagged)] +pub(crate) enum Subscription { + Secure(SecureSubscription), + Unsigned(UnsignedSubscription), +} diff --git a/src/models/websocket/events.rs b/src/models/websocket/events.rs new file mode 100644 index 0000000..3fb86ef --- /dev/null +++ b/src/models/websocket/events.rs @@ -0,0 +1,76 @@ +use serde::Deserialize; + +use super::{ + CandleUpdate, EventType, Level2Update, MarketTradesUpdate, OrderUpdate, ProductUpdate, + SubscribeUpdate, TickerUpdate, +}; + +/// Events that could be received in a message. +#[derive(Debug)] +pub enum Event { + Status(StatusEvent), + Candles(CandlesEvent), + Ticker(TickerEvent), + TickerBatch(TickerEvent), + Level2(Level2Event), + User(UserEvent), + MarketTrades(MarketTradesEvent), + Heartbeats(HeartbeatsEvent), + Subscribe(SubscribeEvent), +} + +/// The status event containing updates to products. +#[derive(Deserialize, Debug)] +pub struct StatusEvent { + pub r#type: EventType, + pub products: Vec, +} + +/// The candles event containing updates to candles. +#[derive(Deserialize, Debug)] +pub struct CandlesEvent { + pub r#type: EventType, + pub candles: Vec, +} + +/// The ticker event containing updates to tickers. +#[derive(Deserialize, Debug)] +pub struct TickerEvent { + pub r#type: EventType, + pub tickers: Vec, +} + +/// The level2 event containing updates to the order book. +#[derive(Deserialize, Debug)] +pub struct Level2Event { + pub r#type: EventType, + pub product_id: String, + pub updates: Vec, +} + +/// The user event containing updates to orders. +#[derive(Deserialize, Debug)] +pub struct UserEvent { + pub r#type: EventType, + pub orders: Vec, +} + +/// The market trades event containing updates to trades. +#[derive(Deserialize, Debug)] +pub struct MarketTradesEvent { + pub r#type: EventType, + pub trades: Vec, +} + +/// The heartbeats event containing the current time and heartbeat counter. +#[derive(Deserialize, Debug)] +pub struct HeartbeatsEvent { + pub current_time: String, + pub heartbeat_counter: u64, +} + +/// The subscribe event containing the current subscriptions. +#[derive(Deserialize, Debug)] +pub struct SubscribeEvent { + pub subscriptions: SubscribeUpdate, +} diff --git a/src/models/websocket/message.rs b/src/models/websocket/message.rs new file mode 100644 index 0000000..002a1ce --- /dev/null +++ b/src/models/websocket/message.rs @@ -0,0 +1,165 @@ +use std::fmt; + +use serde::de::{self, Deserialize, Deserializer, MapAccess, Visitor}; +use serde_json::Value; + +use super::{ + CandlesEvent, Channel, Event, HeartbeatsEvent, Level2Event, MarketTradesEvent, StatusEvent, + SubscribeEvent, TickerEvent, UserEvent, +}; + +/// Message from the WebSocket containing event updates. +#[derive(Debug)] +pub struct Message { + /// The channel the message is from. + pub channel: Channel, + /// The client ID for the message. + pub client_id: String, + /// The timestamp for the message. + pub timestamp: String, + /// The sequence number for the message + pub sequence_num: u64, + /// The events in the message. + pub events: Vec, +} + +/// Custom deserialization for Message. +impl<'de> Deserialize<'de> for Message { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_map(MessageVisitor) + } +} + +/// Visitor struct for custom deserialization for Message. +struct MessageVisitor; + +impl<'de> Visitor<'de> for MessageVisitor { + type Value = Message; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a WebSocket message") + } + + fn visit_map(self, mut map: M) -> Result + where + M: MapAccess<'de>, + { + let mut channel = None; + let mut client_id = None; + let mut timestamp = None; + let mut sequence_num = None; + let mut events_value = None; + + // Extract common fields and events. + while let Some(key) = map.next_key::()? { + match key.as_str() { + "channel" => { + if channel.is_some() { + return Err(de::Error::duplicate_field("channel")); + } + channel = Some(map.next_value()?); + } + "client_id" => { + if client_id.is_some() { + return Err(de::Error::duplicate_field("client_id")); + } + client_id = Some(map.next_value()?); + } + "timestamp" => { + if timestamp.is_some() { + return Err(de::Error::duplicate_field("timestamp")); + } + timestamp = Some(map.next_value()?); + } + "sequence_num" => { + if sequence_num.is_some() { + return Err(de::Error::duplicate_field("sequence_num")); + } + sequence_num = Some(map.next_value()?); + } + "events" => { + if events_value.is_some() { + return Err(de::Error::duplicate_field("events")); + } + events_value = Some(map.next_value()?); + } + _ => { + // Skip unknown fields or handle as needed. + let _ = map.next_value::()?; + } + } + } + + let channel: Channel = channel.ok_or_else(|| de::Error::missing_field("channel"))?; + let client_id = client_id.ok_or_else(|| de::Error::missing_field("client_id"))?; + let timestamp = timestamp.ok_or_else(|| de::Error::missing_field("timestamp"))?; + let sequence_num = sequence_num.ok_or_else(|| de::Error::missing_field("sequence_num"))?; + let events_value = events_value.ok_or_else(|| de::Error::missing_field("events"))?; + + // Deserialize events based on the channel. + let events = match channel { + Channel::Status => { + let events: Vec = + serde_json::from_value(events_value).map_err(de::Error::custom)?; + events.into_iter().map(Event::Status).collect() + } + Channel::Candles => { + let events: Vec = + serde_json::from_value(events_value).map_err(de::Error::custom)?; + events.into_iter().map(Event::Candles).collect() + } + Channel::Ticker => { + let events: Vec = + serde_json::from_value(events_value).map_err(de::Error::custom)?; + events.into_iter().map(Event::Ticker).collect() + } + Channel::TickerBatch => { + let events: Vec = + serde_json::from_value(events_value).map_err(de::Error::custom)?; + events.into_iter().map(Event::TickerBatch).collect() + } + Channel::Level2 => { + let events: Vec = + serde_json::from_value(events_value).map_err(de::Error::custom)?; + events.into_iter().map(Event::Level2).collect() + } + Channel::User => { + let events: Vec = + serde_json::from_value(events_value).map_err(de::Error::custom)?; + events.into_iter().map(Event::User).collect() + } + Channel::MarketTrades => { + let events: Vec = + serde_json::from_value(events_value).map_err(de::Error::custom)?; + events.into_iter().map(Event::MarketTrades).collect() + } + Channel::Heartbeats => { + let events: Vec = + serde_json::from_value(events_value).map_err(de::Error::custom)?; + events.into_iter().map(Event::Heartbeats).collect() + } + Channel::Subscriptions => { + let events: Vec = + serde_json::from_value(events_value).map_err(de::Error::custom)?; + events.into_iter().map(Event::Subscribe).collect() + } + _ => { + return Err(de::Error::custom(format!( + "Unsupported channel: {:?}", + channel + ))); + } + }; + + Ok(Message { + channel, + client_id, + timestamp, + sequence_num, + events, + }) + } +} diff --git a/src/models/websocket/mod.rs b/src/models/websocket/mod.rs new file mode 100644 index 0000000..cba6c79 --- /dev/null +++ b/src/models/websocket/mod.rs @@ -0,0 +1,17 @@ +//! # Coinbase Advanced Websocket Client +//! +//! `websocket` gives access to the websocket stream to receive updates in a streamlined fashion. +//! Many parts of the REST API suggest using websockets instead due to ratelimits and being quicker +//! for large amount of constantly changing data. + +mod enums; +mod events; +mod message; +mod responses; +mod types; + +pub use enums::*; +pub use events::*; +pub use message::*; +pub use responses::*; +pub use types::*; diff --git a/src/models/websocket/responses.rs b/src/models/websocket/responses.rs new file mode 100644 index 0000000..f60b5dd --- /dev/null +++ b/src/models/websocket/responses.rs @@ -0,0 +1,175 @@ +use serde::{Deserialize, Serialize}; +use serde_with::{serde_as, DefaultOnError, DisplayFromStr}; + +use crate::order::{OrderSide, OrderStatus, OrderType, TimeInForce, TriggerStatus}; +use crate::product::{Candle, ProductType}; + +use super::Level2Side; + +#[serde_as] +#[derive(Deserialize, Debug)] +pub struct Level2Update { + pub side: Level2Side, + pub event_time: String, + #[serde_as(as = "DisplayFromStr")] + pub price_level: f64, + #[serde_as(as = "DisplayFromStr")] + pub new_quantity: f64, +} + +#[derive(Deserialize, Debug, Default)] +pub struct SubscribeUpdate { + #[serde(default)] + pub status: Vec, + #[serde(default)] + pub ticker: Vec, + #[serde(default)] + pub ticker_batch: Vec, + #[serde(default)] + pub level2: Option>, + #[serde(default)] + pub user: Option>, + #[serde(default)] + pub market_trades: Option>, + #[serde(default)] + pub heartbeats: Option>, +} + +/// Represents a Product received from the Websocket API. +#[serde_as] +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct ProductUpdate { + /// Type of the product. + pub product_type: ProductType, + /// ID of the product. + pub id: String, + /// Symbol of the base currency. + pub base_currency: String, + /// Symbol of the quote currency. + pub quote_currency: String, + /// Minimum amount base value can be increased or decreased at once. + #[serde_as(as = "DisplayFromStr")] + pub base_increment: f64, + /// Minimum amount quote value can be increased or decreased at once. + #[serde_as(as = "DisplayFromStr")] + pub quote_increment: f64, + /// Name of the product. + pub display_name: String, + /// Status of the product. + pub status: String, + /// Additional status message. + pub status_message: String, + /// Minimum amount of funds. + #[serde_as(as = "DisplayFromStr")] + pub min_market_funds: f64, +} + +/// Represents a Market Trade received from the Websocket API. +#[serde_as] +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct MarketTradesUpdate { + /// Trade identity. + pub trade_id: String, + /// ID of the product. + pub product_id: String, + /// Price of the product. + #[serde_as(as = "DisplayFromStr")] + pub price: f64, + /// Size for the trade. + #[serde_as(as = "DisplayFromStr")] + pub size: f64, + /// Side: BUY or SELL. + pub side: OrderSide, + /// Time for the market trade. + pub time: String, +} + +/// Represents a Candle update received from the Websocket API. +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct CandleUpdate { + /// Product ID (Pair, ex 'BTC-USD') + pub product_id: String, + /// Candle for the update. + #[serde(flatten)] + pub data: Candle, +} + +/// Represents a Ticker update received from the Websocket API. +#[serde_as] +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct TickerUpdate { + /// Ticker update type. + pub r#type: String, + /// Product ID (Pair, ex 'BTC-USD') + pub product_id: String, + /// Current price for the product. + #[serde_as(as = "DisplayFromStr")] + pub price: f64, + /// 24hr Volume for the product. + #[serde_as(as = "DisplayFromStr")] + pub volume_24_h: f64, + /// 24hr Lowest price. + #[serde_as(as = "DisplayFromStr")] + pub low_24_h: f64, + /// 24hr Highest price. + #[serde_as(as = "DisplayFromStr")] + pub high_24_h: f64, + /// 52w (52 weeks) Lowest price. + #[serde_as(as = "DisplayFromStr")] + pub low_52_w: f64, + /// 52w (52 weeks) Highest price. + #[serde_as(as = "DisplayFromStr")] + pub high_52_w: f64, + /// 24hr Price percentage change. + #[serde_as(as = "DisplayFromStr")] + pub price_percent_chg_24_h: f64, +} + +/// Order updates for a user from a websocket. +#[serde_as] +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct OrderUpdate { + #[serde_as(as = "DisplayFromStr")] + pub avg_price: f64, + pub cancel_reason: String, + pub client_order_id: String, + #[serde_as(as = "DisplayFromStr")] + pub completion_percentage: f64, + pub contract_expiry_type: String, + #[serde_as(as = "DisplayFromStr")] + pub cumulative_quantity: f64, + #[serde_as(as = "DisplayFromStr")] + pub filled_value: f64, + #[serde_as(as = "DisplayFromStr")] + pub leaves_quantity: f64, + #[serde_as(as = "DefaultOnError")] + #[serde(default)] + pub limit_price: f64, + #[serde_as(as = "DisplayFromStr")] + pub number_of_fills: u32, + pub order_id: String, + pub order_side: OrderSide, + pub order_type: OrderType, + #[serde_as(as = "DisplayFromStr")] + pub outstanding_hold_amount: f64, + #[serde_as(as = "DisplayFromStr")] + pub post_only: bool, + pub product_id: String, + pub product_type: ProductType, + pub reject_reason: Option, + pub retail_portfolio_id: String, + pub risk_managed_by: String, + pub status: OrderStatus, + #[serde_as(as = "DefaultOnError>")] + #[serde(default)] + pub stop_price: Option, + pub time_in_force: TimeInForce, + #[serde_as(as = "DisplayFromStr")] + pub total_fees: f64, + #[serde_as(as = "DisplayFromStr")] + pub total_value_after_fees: f64, + pub trigger_status: TriggerStatus, + pub creation_time: String, + pub end_time: String, + pub start_time: String, +} diff --git a/src/models/websocket/types.rs b/src/models/websocket/types.rs new file mode 100644 index 0000000..866f5a7 --- /dev/null +++ b/src/models/websocket/types.rs @@ -0,0 +1,197 @@ +use std::collections::{HashMap, HashSet}; +use std::sync::Arc; + +use serde::Serialize; +use tokio::sync::Mutex; + +use super::{Channel, Endpoint, EndpointType}; +use crate::types::WebSocketReader; + +type ChannelSubscriptions = HashMap>; + +/// Secure (authenticated) Subscription is sent to the WebSocket to enable updates for specified Channels. +#[derive(Serialize, Debug)] +pub(crate) struct SecureSubscription { + /// Subscribing or unsubscribing. + pub(crate) r#type: String, + /// Product IDs to (un)subscribe to. + #[serde(skip_serializing_if = "Vec::is_empty")] + pub(crate) product_ids: Vec, + /// Channel to (un)subscribe to. + pub(crate) channel: Channel, + /// JWT token for authentication. + pub(crate) jwt: String, +} + +/// Unsigned (public) Subscription is sent to the WebSocket to enable updates for specified Channels. +#[derive(Serialize, Debug)] +pub(crate) struct UnsignedSubscription { + /// Subscribing or unsubscribing. + pub(crate) r#type: String, + /// Product IDs to (un)subscribe to. + #[serde(skip_serializing_if = "Vec::is_empty")] + pub(crate) product_ids: Vec, + /// Channel to (un)subscribe to. + pub(crate) channel: Channel, + /// Timestamp for the subscription. + pub(crate) timestamp: String, +} + +/// Holds all WebSocket endpoints. +#[derive(Debug, Default)] +pub struct WebSocketEndpoints { + /// Endpoints accessible by key. + pub(crate) endpoints: HashMap, +} + +impl WebSocketEndpoints { + /// Create a new WebSocketEndpoints. + pub fn new() -> Self { + Self::default() + } + + /// Add an endpoint to the WebSocketEndpoints. + /// + /// # Arguments + /// + /// * `endpoint_type` - The type of endpoint. + pub fn add(&mut self, endpoint_type: EndpointType, endpoint: Endpoint) { + self.endpoints.insert(endpoint_type, endpoint); + } + + /// Check if the WebSocketEndpoints contains an endpoint. + /// + /// # Arguments + /// + /// * `endpoint_type` - The type of endpoint. + pub fn get(&self, endpoint_type: &EndpointType) -> Option<&Endpoint> { + self.endpoints.get(endpoint_type) + } + + /// Check if the WebSocketEndpoints contains a mutable reference to an endpoint. + /// + /// # Arguments + /// + /// * `endpoint_type` - The type of endpoint. + pub fn get_mut(&mut self, endpoint_type: &EndpointType) -> Option<&mut Endpoint> { + self.endpoints.get_mut(endpoint_type) + } + + /// Take an endpoint from the WebSocketEndpoints. + /// + /// # Arguments + /// + /// * `endpoint_type` - The type of endpoint. + pub fn take_endpoint(&mut self, endpoint_type: &EndpointType) -> Option { + self.endpoints.remove(endpoint_type) + } + + /// Get the public endpoint. + pub fn public(&self) -> Option<&Endpoint> { + self.get(&EndpointType::Public) + } + + /// Get the user endpoint. + pub fn user(&self) -> Option<&Endpoint> { + self.get(&EndpointType::User) + } + + /// Converts the WebSocketEndpoints into a vector of WebSocketReaders. + pub(crate) fn extract_to_vec(&mut self) -> Vec { + let mut readers = Vec::new(); + let keys: Vec = self.endpoints.keys().cloned().collect(); + for key in keys { + if let Some(endpoint) = self.endpoints.remove(&key) { + match endpoint { + Endpoint::Public((_route, reader)) => readers.push(reader), + Endpoint::User((_route, reader)) => readers.push(reader), + } + } + } + + readers + } +} + +/// Stores the current subscriptions for each channel for each endpoint. +#[derive(Debug, Clone)] +pub(crate) struct WebSocketSubscriptions { + /// The subscriptions for each channel for each endpoint. + pub(crate) data: HashMap>>, +} + +impl Default for WebSocketSubscriptions { + fn default() -> Self { + let data = HashMap::new(); + Self { data } + } +} + +impl WebSocketSubscriptions { + /// Create a new WebSocketSubscriptions. + pub(crate) fn new() -> Self { + Self::default() + } + + /// Add subscriptions to the specified channel. + + pub(crate) async fn add( + &mut self, + channel: &Channel, + product_ids: &[String], + endpoint: &EndpointType, + ) { + // Get or insert the Arc> for the endpoint. + let subs_mutex = self + .data + .entry(endpoint.clone()) + .or_insert_with(|| Arc::new(Mutex::new(HashMap::new()))) + .clone(); + + // Add the product IDs to the subscriptions. + let mut subs = subs_mutex.lock().await; + subs.entry(channel.clone()) + .and_modify(|ids| { + let existing_ids: HashSet = ids.iter().cloned().collect(); + for id in product_ids { + if !existing_ids.contains(id) { + ids.push(id.clone()); + } + } + }) + .or_insert_with(|| product_ids.to_vec()); + } + + /// Remove the specified product IDs from the subscriptions. + pub(crate) async fn remove( + &mut self, + channel: &Channel, + product_ids: &[String], + endpoint: &EndpointType, + ) { + if let Some(subs_mutex) = self.data.get(endpoint) { + let mut subs = subs_mutex.lock().await; + + // Remove the product IDs from the subscriptions. + if let Some(ids) = subs.get_mut(channel) { + ids.retain(|id| !product_ids.contains(id)); + } + } + } + + /// Get the subscriptions for the specified endpoint. + pub(crate) async fn get(&self, endpoint: &EndpointType) -> HashMap> { + if let Some(subs_mutex) = self.data.get(endpoint) { + let subs = subs_mutex.lock().await; + subs.clone() + } else { + HashMap::new() + } + } + + /// Obtains all of the keys (endpoints) that have subscriptions. + pub(crate) async fn get_keys(&self) -> Vec { + let keys: Vec = self.data.keys().cloned().collect(); + keys + } +} diff --git a/src/rest.rs b/src/rest.rs index f643f6d..74cef36 100644 --- a/src/rest.rs +++ b/src/rest.rs @@ -4,57 +4,141 @@ //! This is the primary method of accessing the endpoints and handles all of the configurations and //! negotiations for the user. -use crate::apis::{AccountApi, ConvertApi, FeeApi, OrderApi, ProductApi, UtilApi}; -use crate::signer::Signer; +use std::sync::Arc; + +use futures::lock::Mutex; + +use crate::apis::{ + AccountApi, ConvertApi, DataApi, FeeApi, OrderApi, PaymentApi, PortfolioApi, ProductApi, + PublicApi, +}; +use crate::http_agent::{PublicHttpAgent, SecureHttpAgent}; #[cfg(feature = "config")] use crate::config::ConfigFile; +use crate::token_bucket::{RateLimits, TokenBucket}; use crate::types::CbResult; -/// Represents a Client for the API. -pub struct RestClient { - /// Gives access to the Account API. - pub account: AccountApi, - /// Gives access to the Product API. - pub product: ProductApi, - /// Gives access to the Fee API. - pub fee: FeeApi, - /// Gives access to the Order API. - pub order: OrderApi, - /// Gives access to the Convert API. - pub convert: ConvertApi, - /// Gives access to the Util API. - pub util: UtilApi, +/// Used to create a new RestClient. +#[derive(Default)] +pub struct RestClientBuilder { + api_key: Option, + api_secret: Option, + use_sandbox: bool, } -impl RestClient { - /// Creates a new instance of a Client. This is a wrapper for the various APIs. - /// - /// # Arguments - /// - /// * `key` - A string that holds the key for the API service. - /// * `secret` - A string that holds the secret for the API service. - pub fn new(key: &str, secret: &str) -> CbResult { - Ok(Self { - account: AccountApi::new(Signer::new(key, secret, true)?), - product: ProductApi::new(Signer::new(key, secret, true)?), - fee: FeeApi::new(Signer::new(key, secret, true)?), - order: OrderApi::new(Signer::new(key, secret, true)?), - convert: ConvertApi::new(Signer::new(key, secret, true)?), - util: UtilApi::new(Signer::new(key, secret, true)?), - }) +impl RestClientBuilder { + /// Creates a new instance of a RestClientBuilder. + pub fn new() -> Self { + Self { + api_key: None, + api_secret: None, + use_sandbox: false, + } } - /// Creates a new instance of a Client using a configuration file. This is a wrapper for the various APIs and Signer. + /// Uses the configuration file to set up the client. /// /// # Arguments /// /// * `config` - Configuration that implements ConfigFile trait. #[cfg(feature = "config")] - pub fn from_config(config: &T) -> CbResult + pub fn with_config(mut self, config: &T) -> Self where T: ConfigFile, { - Self::new(&config.coinbase().api_key, &config.coinbase().api_secret) + self.api_key = Some(config.coinbase().api_key.clone()); + self.api_secret = Some(config.coinbase().api_secret.clone()); + self.use_sandbox = config.coinbase().use_sandbox; + self + } + + /// Uses the provided key and secret to initialize the authentication. + /// + /// # Arguments + /// + /// * `key` - API key. + /// * `secret` - API secret. + pub fn with_authentication(mut self, key: &str, secret: &str) -> Self { + self.api_key = Some(key.to_string()); + self.api_secret = Some(secret.to_string()); + self + } + + /// Sets the use_sandbox flag for the client. + /// + /// # Arguments + /// + /// * `use_sandbox` - A boolean that determines if the sandbox should be enabled. + pub fn use_sandbox(mut self, use_sandbox: bool) -> Self { + self.use_sandbox = use_sandbox; + self } + + /// Builds the RestClient. + pub fn build(self) -> CbResult { + // Initialize token buckets + let secure_bucket = Arc::new(Mutex::new(TokenBucket::new( + RateLimits::max_tokens(true, false), + RateLimits::refresh_rate(true, false), + ))); + + let public_bucket = Arc::new(Mutex::new(TokenBucket::new( + RateLimits::max_tokens(true, true), + RateLimits::refresh_rate(true, true), + ))); + + // Determine if authentication is enabled. + let is_authenticated = self.api_key.is_some() && self.api_secret.is_some(); + + // Initialize agents. + let secure_agent = if is_authenticated { + Some(SecureHttpAgent::new( + &self.api_key.unwrap(), + &self.api_secret.unwrap(), + self.use_sandbox, + secure_bucket, + )?) + } else { + None + }; + + // Public agent used to access public endpoints. + let public_agent = PublicHttpAgent::new(self.use_sandbox, public_bucket)?; + + // Initialize APIs. + Ok(RestClient { + account: AccountApi::new(secure_agent.clone()), + product: ProductApi::new(secure_agent.clone()), + fee: FeeApi::new(secure_agent.clone()), + order: OrderApi::new(secure_agent.clone()), + portfolio: PortfolioApi::new(secure_agent.clone()), + convert: ConvertApi::new(secure_agent.clone()), + payment: PaymentApi::new(secure_agent.clone()), + data: DataApi::new(secure_agent.clone()), + public: PublicApi::new(public_agent), + }) + } +} + +/// Represents a Client for the API. +pub struct RestClient { + /// Gives access to the Account API. + pub account: AccountApi, + /// Gives access to the Product API. + pub product: ProductApi, + /// Gives access to the Fee API. + pub fee: FeeApi, + /// Gives access to the Order API. + pub order: OrderApi, + /// Gives access to the Portfolio API. + pub portfolio: PortfolioApi, + /// Gives access to the Convert API. + pub convert: ConvertApi, + /// Gives access to the Payment API. + pub payment: PaymentApi, + /// Gives access to the Data API. + pub data: DataApi, + /// Gives access to the Public API. + pub public: PublicApi, } diff --git a/src/signer.rs b/src/signer.rs deleted file mode 100644 index b90362c..0000000 --- a/src/signer.rs +++ /dev/null @@ -1,188 +0,0 @@ -//! # Authentication and signing messages. -//! -//! `signer` contains the backbone of the API requests in the form of the Signer struct. This signs -//! all requests to the API for ensure proper authentication. Signer is also responsible for handling -//! the GET and POST requests. - -use reqwest::header::CONTENT_TYPE; -use reqwest::{Response, StatusCode}; -use serde::Serialize; - -use crate::constants::API_ROOT_URI; -use crate::constants::{ratelimits, rest}; -use crate::errors::CbAdvError; -use crate::jwt::Jwt; -use crate::token_bucket::TokenBucket; -use crate::traits::Query; -use crate::types::CbResult; - -/// Rate Limits for REST and WebSocket requests. -/// -/// # Endpoint / Reference -/// -/// * REST: -/// * WebSocket: -struct RateLimits {} -impl RateLimits { - /// Maximum amount of tokens per bucket. - const REST_MAX_TOKENS: f64 = ratelimits::REST_REFRESH_RATE; - const WEBSOCKET_MAX_TOKENS: f64 = ratelimits::WEBSOCKET_REFRESH_RATE; - - /// Amount of tokens refreshed per second. - /// - /// # Arguments - /// - /// * `is_rest` - Requester is REST Client, true, otherwise false. - fn refresh_rate(is_rest: bool) -> f64 { - if is_rest { - ratelimits::REST_REFRESH_RATE - } else { - ratelimits::WEBSOCKET_REFRESH_RATE - } - } - - /// Maximum amount of tokens for a bucket. - /// - /// # Arguments - /// - /// * `is_rest` - Requester is REST Client, true, otherwise false. - fn max_tokens(is_rest: bool) -> f64 { - if is_rest { - RateLimits::REST_MAX_TOKENS - } else { - RateLimits::WEBSOCKET_MAX_TOKENS - } - } -} - -/// Creates and signs HTTP Requests to the API. -#[derive(Debug, Clone)] -pub(crate) struct Signer { - /// JSON Webtoken Generator - jwt: Jwt, - /// Wrapped client that is responsible for making the requests. - client: reqwest::Client, - /// Token bucket, used for rate limiting. - pub(crate) bucket: TokenBucket, -} - -/// Responsible for signing and sending HTTP requests. -impl Signer { - /// Creates a new instance of Signer. - /// - /// # Arguments - /// - /// * `api_key` - A string that holds the key for the API service. - /// * `api_secret` - A string that holds the secret for the API service. - /// * `is_rest` - Signer for REST Client, true, otherwise false. - pub(crate) fn new(api_key: &str, api_secret: &str, is_rest: bool) -> CbResult { - Ok(Self { - jwt: Jwt::new(api_key, api_secret)?, - client: reqwest::Client::new(), - bucket: TokenBucket::new( - RateLimits::max_tokens(is_rest), - RateLimits::refresh_rate(is_rest), - ), - }) - } - - /// Gets a JSON Webtoken based on the service and URI provided. - pub(crate) fn get_jwt(&self, service: &str, uri: Option<&str>) -> CbResult { - self.jwt.encode(service, uri) - } - - /// Performs a HTTP GET Request. - /// - /// # Arguments - /// - /// * `resource` - A string representing the resource that is being accessed. - /// * `query` - A string containing options / parameters for the URL. - pub(crate) async fn get(&mut self, resource: &str, query: &impl Query) -> CbResult { - // Efficiently construct the URL. - let url = match query.to_query() { - value if !value.is_empty() => format!("https://{}{}{}", API_ROOT_URI, resource, value), - _ => format!("https://{}{}", API_ROOT_URI, resource), - }; - - // Create the signature and submit the request. - // let uri = format!("{} {}", Method::GET, resource); - let uri = Jwt::build_uri("GET", API_ROOT_URI, resource); - let token = self.get_jwt(rest::SERVICE, Some(&uri))?; - - // Wait until a token is available to make the request. Immediately consume it. - self.bucket.wait_on().await; - - // Send the request and handle the response. - let response = self - .client - .get(&url) - .bearer_auth(token) - .header(CONTENT_TYPE, "application/json") - .send() - .await - .map_err(|_| CbAdvError::Unknown("GET request to API".to_string()))?; - - if response.status() == StatusCode::OK { - Ok(response) - } else { - let code = format!("Status Code: {}", response.status().as_u16()); - let text = response - .text() - .await - .unwrap_or_else(|_| "could not parse error message".to_string()); - Err(CbAdvError::BadStatus(format!("{}, {}", code, text))) - } - } - - /// Performs a HTTP POST Request. - /// - /// # Arguments - /// - /// * `resource` - A string representing the resource that is being accessed. - /// * `query` - A string containing options / parameters for the URL. - /// * `body` - An object to send to the URL via POST request. - pub(crate) async fn post( - &mut self, - resource: &str, - query: &impl Query, - body: T, - ) -> CbResult { - // Efficiently construct the URL. - let url = match query.to_query() { - value if !value.is_empty() => format!("https://{}{}{}", API_ROOT_URI, resource, value), - _ => format!("https://{}{}", API_ROOT_URI, resource), - }; - - // Serialize the body and handle potential serialization errors. - let body_str = serde_json::to_string(&body).map_err(|_| CbAdvError::BadSerialization)?; - - // Create the signature and handle potential errors. - let uri = Jwt::build_uri("POST", API_ROOT_URI, resource); - let token = self.get_jwt(rest::SERVICE, Some(&uri))?; - - // Wait until a token is available to make the request. Immediately consume it. - self.bucket.wait_on().await; - - // Send the request and handle the response. - let response = self - .client - .post(&url) - .bearer_auth(token) - .header(CONTENT_TYPE, "application/json") - .body(body_str) - .send() - .await - .map_err(|_| CbAdvError::Unknown("POST request to API".to_string()))?; - - if response.status() == StatusCode::OK { - Ok(response) - } else { - let code = format!("Status Code: {}", response.status().as_u16()); - let text = response - .text() - .await - .unwrap_or_else(|_| "could not parse error message".to_string()); - Err(CbAdvError::BadStatus(format!("{}, {}", code, text))) - } - } -} diff --git a/src/task_tracker.rs b/src/task_tracker.rs deleted file mode 100644 index 8762dc1..0000000 --- a/src/task_tracker.rs +++ /dev/null @@ -1,132 +0,0 @@ -//! Task Tracker is the underlying object used to track candle updates. - -use std::cmp::{Ord, Ordering}; -use std::collections::HashMap; - -use chrono::Utc; - -use crate::constants::websocket::GRANULARITY; -use crate::models::product::{Candle, CandleUpdate}; -use crate::models::websocket::{CandlesEvent, Message}; -use crate::traits::{CandleCallback, MessageCallback}; -use crate::types::{CbResult, WebSocketReader}; -use crate::WebSocketClient; - -/// Tracks the candle watcher task. -pub(crate) struct TaskTracker -where - T: CandleCallback, -{ - /// Holds most recent candle processed for each product. [key: Product Id, value: Candle] - candles: HashMap, - /// User-defined object that implements `CandleCallback``, triggered on completed candles. - user_watcher: T, -} - -impl TaskTracker -where - T: CandleCallback, -{ - /// Starts the task that tracks candles for completion. - /// - /// # Arguments - /// - /// * `reader` - WebSocket reader to receive updates. - /// * `user_obj` - User object that implements `CandleCallback` to receive completed candles. - pub(crate) async fn start(reader: WebSocketReader, user_obj: T) - where - T: CandleCallback, - { - let tracker = Self { - candles: HashMap::new(), - user_watcher: user_obj, - }; - - // Start the listener. - WebSocketClient::listener_with(reader, tracker).await; - } - - /// Returns a completed candle. A completed candle means a candle with a newer timestamp was passed - /// to the function indicating the prior candle is done. - /// - /// # Arguements - /// - /// * `product_id` - Product the candle belongs to. - /// * `new_candle` - New candles obtained from the WebSocket. - fn check_candle(&mut self, product_id: &str, new_candle: Candle) -> Option { - // Obtain candle from the tracked candles. - match self.candles.get(product_id) { - Some(candle) => { - if candle.start < new_candle.start { - // Remove/eject complete candle, and replace with new series candle. - let old = self.candles.remove(product_id).unwrap(); - self.candles.insert(product_id.to_string(), new_candle); - return Some(old as Candle); - } else { - // Replace existing. - self.candles.insert(product_id.to_string(), new_candle); - } - } - None => { - // Candle not found, create first instance. - self.candles.insert(product_id.to_string(), new_candle); - } - } - None - } -} - -impl MessageCallback for TaskTracker -where - T: CandleCallback, -{ - /// Required to pass TaskTracker to the websocket listener. - fn message_callback(&mut self, msg: CbResult) { - // Filter all non-candle and empty updates. - let ev: Vec = match msg { - Ok(value) => match value { - Message::Candles(value) => { - if value.events.is_empty() { - // No events / updates to process. - return; - } - // Events being worked with. - value.events - } - // Non-candle message. - _ => return, - }, - // WebSocket error. - Err(err) => { - println!("!WEBSOCKET ERROR! {}", err); - return; - } - }; - - // Combine all updates and sort by more recent -> oldest. - let mut updates: Vec = ev.iter().flat_map(|c| c.candles.clone()).collect(); - - match updates.len().cmp(&1usize) { - // Sort if there are more than 1 update. - Ordering::Greater => updates.sort_by(|a, b| b.data.start.cmp(&a.data.start)), - // No updates to process. - Ordering::Less => return, - _ => (), - }; - - // Check the most recent candle based on `start`, see if there is a completed series. - let update = updates.remove(0); - let product_id: String = update.product_id; - let candle = match self.check_candle(&product_id, update.data) { - Some(c) => c, - None => return, - }; - - // Get the current "start" time for a candle. - let now: u64 = Utc::now().timestamp() as u64; - let start: u64 = now - (now % (GRANULARITY * 2)); - - // Call the users object. - self.user_watcher.candle_callback(start, product_id, candle); - } -} diff --git a/src/time.rs b/src/time.rs index f9c81ac..5b60918 100644 --- a/src/time.rs +++ b/src/time.rs @@ -3,10 +3,13 @@ //! `time` plays an important role in authentication for API requests and obtaining data between //! spans of time such as in the Product API for obtaining Candles. -use serde::Serialize; +use core::fmt; +use serde::{Deserialize, Serialize}; use std::time::{SystemTime, UNIX_EPOCH}; +use crate::errors::CbError; use crate::traits::Query; +use crate::types::CbResult; use crate::utils::QueryBuilder; /// One minute of time in seconds. @@ -24,8 +27,11 @@ const SIX_HOUR: u32 = ONE_HOUR * 6; const ONE_DAY: u32 = ONE_HOUR * 24; /// Span of time in seconds. +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum Granularity { - UnknownGranularity, + #[serde(rename = "UNKNOWN_GRANULARITY")] + Unknown, OneMinute, FiveMinute, FifteenMinute, @@ -36,6 +42,28 @@ pub enum Granularity { OneDay, } +impl fmt::Display for Granularity { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.as_ref()) + } +} + +impl AsRef for Granularity { + fn as_ref(&self) -> &str { + match self { + Granularity::OneMinute => "ONE_MINUTE", + Granularity::FiveMinute => "FIVE_MINUTE", + Granularity::FifteenMinute => "FIFTEEN_MINUTE", + Granularity::ThirtyMinute => "THIRTY_MINUTE", + Granularity::OneHour => "ONE_HOUR", + Granularity::TwoHour => "TWO_HOUR", + Granularity::SixHour => "SIX_HOUR", + Granularity::OneDay => "ONE_DAY", + Granularity::Unknown => "UNKNOWN_GRANULARITY", + } + } +} + impl Granularity { /// Converts a Granularity into seconds. pub fn to_secs(granularity: &Granularity) -> u32 { @@ -51,7 +79,7 @@ impl Granularity { Granularity::OneDay => ONE_DAY, - Granularity::UnknownGranularity => 0, + Granularity::Unknown => 0, } } @@ -70,7 +98,7 @@ impl Granularity { ONE_DAY => Granularity::OneDay, // UnknownGranularity is defined in the API. - _ => Granularity::UnknownGranularity, + _ => Granularity::Unknown, } } } @@ -119,27 +147,23 @@ impl Span { } impl Query for Span { + fn check(&self) -> CbResult<()> { + if self.start >= self.end { + return Err(CbError::BadQuery("start must be before end".to_string())); + } else if self.granularity == 0 { + return Err(CbError::BadQuery( + "granularity must be greater than 0".to_string(), + )); + } + Ok(()) + } + fn to_query(&self) -> String { let granularity = Granularity::from_secs(self.granularity); - let granularity_str: &str = match granularity { - Granularity::OneMinute => "ONE_MINUTE", - Granularity::FiveMinute => "FIVE_MINUTE", - Granularity::FifteenMinute => "FIFTEEN_MINUTE", - Granularity::ThirtyMinute => "THIRTY_MINUTE", - - Granularity::OneHour => "ONE_HOUR", - Granularity::TwoHour => "TWO_HOUR", - Granularity::SixHour => "SIX_HOUR", - - Granularity::OneDay => "ONE_DAY", - - Granularity::UnknownGranularity => "UNKNOWN_GRANULARITY", - }; - QueryBuilder::new() .push("start", self.start) .push("end", self.end) - .push("granularity", granularity_str) + .push("granularity", granularity) .build() } } diff --git a/src/token_bucket.rs b/src/token_bucket.rs index 9306d69..35a4892 100644 --- a/src/token_bucket.rs +++ b/src/token_bucket.rs @@ -3,6 +3,59 @@ use std::time::{Duration, Instant}; use tokio::time::sleep as async_sleep; +use crate::constants::ratelimits; + +/// Rate Limits for REST and WebSocket requests. +/// +/// # Endpoint / Reference +/// +/// * REST: +/// * WebSocket: +pub(crate) struct RateLimits {} +impl RateLimits { + /// Maximum amount of tokens per bucket. + const PUBLIC_REST_MAX_TOKENS: f64 = ratelimits::PUBLIC_REST_REFRESH_RATE; + const SECURE_REST_MAX_TOKENS: f64 = ratelimits::SECURE_REST_REFRESH_RATE; + const PUBLIC_WEBSOCKET_MAX_TOKENS: f64 = ratelimits::PUBLIC_WEBSOCKET_REFRESH_RATE; + const SECURE_WEBSOCKET_MAX_TOKENS: f64 = ratelimits::SECURE_WEBSOCKET_REFRESH_RATE; + + /// Amount of tokens refreshed per second. + /// + /// # Arguments + /// + /// * `is_rest` - Requester is REST Client, true, otherwise false. + /// * `is_public` - Requester is Public Client, true, otherwise false. + pub(crate) fn refresh_rate(is_rest: bool, is_public: bool) -> f64 { + if is_rest { + is_public + .then(|| ratelimits::PUBLIC_REST_REFRESH_RATE) + .unwrap_or(ratelimits::SECURE_REST_REFRESH_RATE) + } else { + is_public + .then(|| ratelimits::PUBLIC_WEBSOCKET_REFRESH_RATE) + .unwrap_or(ratelimits::SECURE_WEBSOCKET_REFRESH_RATE) + } + } + + /// Maximum amount of tokens for a bucket. + /// + /// # Arguments + /// + /// * `is_rest` - Requester is REST Client, true, otherwise false. + /// * `is_public` - Requester is Public Client, true, otherwise false. + pub(crate) fn max_tokens(is_rest: bool, is_public: bool) -> f64 { + if is_rest { + is_public + .then(|| RateLimits::PUBLIC_REST_MAX_TOKENS) + .unwrap_or(RateLimits::SECURE_REST_MAX_TOKENS) + } else { + is_public + .then(|| RateLimits::PUBLIC_WEBSOCKET_MAX_TOKENS) + .unwrap_or(RateLimits::SECURE_WEBSOCKET_MAX_TOKENS) + } + } +} + /// Contains and tracks token usage for rate limits. #[derive(Debug, Clone)] pub(crate) struct TokenBucket { diff --git a/src/traits.rs b/src/traits.rs index c70e6dc..ded14d7 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -1,5 +1,8 @@ //! Traits used to allow interfacing with advanced functionality for end-users. +use reqwest::Response; +use serde::Serialize; + use crate::models::{product::Candle, websocket::Message}; use crate::types::CbResult; @@ -27,14 +30,77 @@ pub trait MessageCallback { /// Used to pass query/paramters for a URL. pub(crate) trait Query { + /// Checks that the query is valid and the required fields are present. + fn check(&self) -> CbResult<()>; /// Used to convert a struct into query/paramters for a URL. fn to_query(&self) -> String; } +/// Used to pass a request body to an endpoint. +pub(crate) trait Request { + /// Checks that the request is valid and the required fields are present. + fn check(&self) -> CbResult<()>; +} + /// Represents an empty query. pub(crate) struct NoQuery; impl Query for NoQuery { + fn check(&self) -> CbResult<()> { + Ok(()) + } + fn to_query(&self) -> String { String::new() } } + +/// Trait for the HttpAgent that is responsible for making HTTP requests and managing the token bucket. +pub(crate) trait HttpAgent { + /// Performs a HTTP GET Request. + /// + /// # Arguments + /// + /// * `resource` - A string representing the resource that is being accessed. + /// * `query` - A string containing options / parameters for the URL. + async fn get(&mut self, resource: &str, query: &impl Query) -> CbResult; + + /// Performs a HTTP POST Request. + /// + /// # Arguments + /// + /// * `resource` - A string representing the resource that is being accessed. + /// * `query` - A string containing options / parameters for the URL. + /// * `body` - An object to send to the URL via POST request. + async fn post<'a, T>( + &mut self, + resource: &str, + query: &impl Query, + body: &'a T, + ) -> CbResult + where + T: Request + Serialize + 'a; + + /// Performs a HTTP PUT Request. + /// + /// # Arguments + /// + /// * `resource` - A string representing the resource that is being accessed. + /// * `query` - A string containing options / parameters for the URL. + /// * `body` - An object to send to the URL via POST request. + async fn put<'a, T>( + &mut self, + resource: &str, + query: &impl Query, + body: &'a T, + ) -> CbResult + where + T: Request + Serialize + 'a; + + /// Performs a HTTP DELETE Request. + /// + /// # Arguments + /// + /// * `resource` - A string representing the resource that is being accessed. + /// * `query` - A string containing options / parameters for the URL. + async fn delete(&mut self, resource: &str, query: &impl Query) -> CbResult; +} diff --git a/src/types.rs b/src/types.rs index 9642d1b..d7f1f0f 100644 --- a/src/types.rs +++ b/src/types.rs @@ -4,10 +4,10 @@ use futures_util::stream::SplitStream; use tokio::net::TcpStream; use tokio_tungstenite::{MaybeTlsStream, WebSocketStream}; -use crate::{errors::CbAdvError, models::websocket::Message}; +use crate::{errors::CbError, models::websocket::Message}; /// Used to return objects from the API to the end-user. -pub type CbResult = Result; +pub type CbResult = Result; pub(crate) type Socket = WebSocketStream>; diff --git a/src/utils.rs b/src/utils.rs index 5ef804e..0a1af50 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -2,84 +2,7 @@ //! //! `utils` is a collection of helpful tools that may be required throughout the rest of the API. -use std::fmt::{self, Write}; -use std::str::{self, FromStr}; - -use num_traits::Zero; -use serde::de::Visitor; -use serde::{de, Deserialize, Deserializer}; - -/// Enum representing different types of inputs. -#[derive(Deserialize)] -#[serde(untagged)] -enum StringNumericOrNull { - Numeric(serde_json::Value), - String(String), - Null, -} - -/// Custom deserializer function for any numeric type. -pub(crate) fn deserialize_numeric<'de, N, D>(deserializer: D) -> Result -where - N: FromStr + Zero, - N::Err: fmt::Display, - D: Deserializer<'de>, -{ - struct StringNumericOrNullVisitor(std::marker::PhantomData); - - impl<'de, N> Visitor<'de> for StringNumericOrNullVisitor - where - N: FromStr + Zero, - N::Err: fmt::Display, - { - type Value = N; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("a string, numeric type, null, or empty string") - } - - fn visit_i64(self, value: i64) -> Result - where - E: de::Error, - { - N::from_str(&value.to_string()).map_err(de::Error::custom) - } - - fn visit_u64(self, value: u64) -> Result - where - E: de::Error, - { - N::from_str(&value.to_string()).map_err(de::Error::custom) - } - - fn visit_f64(self, value: f64) -> Result - where - E: de::Error, - { - N::from_str(&value.to_string()).map_err(de::Error::custom) - } - - fn visit_str(self, value: &str) -> Result - where - E: de::Error, - { - if value.is_empty() { - Ok(N::zero()) - } else { - N::from_str(value).map_err(de::Error::custom) - } - } - - fn visit_unit(self) -> Result - where - E: de::Error, - { - Ok(N::zero()) - } - } - - deserializer.deserialize_any(StringNumericOrNullVisitor(std::marker::PhantomData)) -} +use std::fmt::{Display, Write}; /// Prints out a debug message, wraps `println!` macro. #[macro_export] @@ -102,12 +25,6 @@ impl Default for QueryBuilder { impl QueryBuilder { /// Constructs a new `QueryBuilder`. - /// - /// # Examples - /// - /// ``` - /// let query_builder = QueryBuilder::new(); - /// ``` pub(crate) fn new() -> Self { Self { query: String::new(), @@ -115,98 +32,39 @@ impl QueryBuilder { } /// Adds a key-value pair to the query string. - /// - /// # Arguments - /// - /// * `key` - The key of the query parameter. - /// * `value` - The value of the query parameter. - /// - /// # Returns - /// - /// A mutable reference to the `QueryBuilder` for chaining. - /// - /// # Examples - /// - /// ``` - /// let mut query_builder = QueryBuilder::new(); - /// query_builder.push("key", "value"); - /// ``` - pub(crate) fn push(mut self, key: &str, value: T) -> Self { + pub(crate) fn push(mut self, key: &str, value: T) -> Self { if !self.query.is_empty() { self.query.push('&'); - } else { - self.query.push('?'); } - write!(self.query, "{}={}", key, value.to_string()).unwrap(); + write!(self.query, "{}={}", key, value).unwrap(); self } - /// Adds a key-value pair to the query string, if the value is present (Some). - /// - /// # Arguments - /// - /// * `key` - The key of the query parameter. - /// * `value` - An optional value of the query parameter. - /// - /// # Returns - /// - /// A mutable reference to the `QueryBuilder` for chaining. - pub(crate) fn push_optional>(mut self, key: &str, value: &Option) -> Self { + /// Adds a key-value pair to the query string if the value is present. + pub(crate) fn push_optional(self, key: &str, value: &Option) -> Self { if let Some(v) = value { - self = self.push(key, v.as_ref()); - } - self - } - /// Adds a key-value pair to the query string, if the value is present (Some). - /// - /// # Arguments - /// - /// * `key` - The key of the query parameter. - /// * `value` - An optional value of the query parameter. - /// - /// # Returns - /// - /// A mutable reference to the `QueryBuilder` for chaining. - pub(crate) fn push_u32_optional(mut self, key: &str, value: Option) -> Self { - if let Some(v) = value { - self = self.push(key, v.to_string()); + self.push(key, v) + } else { + self } - self } - /// Adds multiple query parameters from an array of tuples. - /// - /// # Arguments - /// - /// * `key` - The key of the query parameter. - /// * `values` - An optional array of values to assign to the key. - pub(crate) fn with_optional_vec>( + /// Adds multiple key-value pairs from an optional vector. + pub(crate) fn push_optional_vec( mut self, key: &str, values: &Option>, ) -> Self { if let Some(values) = values { for value in values { - self = self.push(key.as_ref(), value.as_ref()); + self = self.push(key, value); } } self } - /// Builds the final query string. - /// - /// # Returns - /// - /// A `String` containing the constructed query. - /// - /// # Examples - /// - /// ``` - /// let mut query_builder = QueryBuilder::new(); - /// query_builder.push("key1", "value1").push("key2", "value2"); - /// let query = query_builder.build(); - /// ``` + /// Builds and returns the final query string. pub(crate) fn build(self) -> String { self.query } diff --git a/src/websocket.rs b/src/websocket.rs index a871c7f..586bd8f 100644 --- a/src/websocket.rs +++ b/src/websocket.rs @@ -4,148 +4,568 @@ //! Many parts of the REST API suggest using websockets instead due to ratelimits and being quicker //! for large amount of constantly changing data. -use futures_util::stream::SplitSink; +use std::sync::Arc; + +use futures_util::stream::{self, SplitSink}; use futures_util::{SinkExt, StreamExt}; use tokio::net::TcpStream; +use tokio::sync::Mutex; use tokio::task::JoinHandle; -use tokio_tungstenite::{connect_async, tungstenite, MaybeTlsStream, WebSocketStream}; - -use crate::constants::websocket::{RESOURCE_ENDPOINT, SERVICE}; -use crate::errors::CbAdvError; -use crate::models::websocket::{Channel, Subscription}; -use crate::signer::Signer; -use crate::task_tracker::TaskTracker; +use tokio_tungstenite::tungstenite::{Error as WsError, Message as WsMessage}; +use tokio_tungstenite::{connect_async, MaybeTlsStream, WebSocketStream}; + +use crate::candle_watcher::CandleWatcher; +use crate::constants::websocket::{PUBLIC_ENDPOINT, SECURE_ENDPOINT}; +use crate::errors::CbError; +use crate::jwt::Jwt; +use crate::models::websocket::{ + Channel, EndpointType, SecureSubscription, Subscription, UnsignedSubscription, + WebSocketEndpoints, +}; use crate::time; +use crate::token_bucket::{RateLimits, TokenBucket}; use crate::traits::{CandleCallback, MessageCallback}; -use crate::types::{CbResult, MessageCallbackFn, WebSocketReader}; +use crate::types::{CbResult, MessageCallbackFn}; +use crate::ws::{Endpoint, Message, WebSocketSubscriptions}; #[cfg(feature = "config")] use crate::config::ConfigFile; type Socket = WebSocketStream>; -/// Represents a Client for the Websocket API. Provides easy-access to subscribing and listening to -/// the WebSocket. -pub struct WebSocketClient { - /// Signs the messages sent. - signer: Signer, - /// Writes data to the stream, gets sent to the API. - socket_tx: Option>, +/// Obtains the endpoint associated with the channel. +fn get_channel_endpoint(channel: &Channel) -> EndpointType { + match channel { + Channel::Subscriptions => EndpointType::Public, + Channel::Heartbeats => EndpointType::Public, + Channel::Status => EndpointType::Public, + Channel::Ticker => EndpointType::Public, + Channel::TickerBatch => EndpointType::Public, + Channel::MarketTrades => EndpointType::Public, + Channel::Level2 => EndpointType::Public, + Channel::Candles => EndpointType::Public, + Channel::User => EndpointType::User, + Channel::FuturesBalanceSummary => EndpointType::User, + } } -impl WebSocketClient { - /// Creates a new instance of a Client. This is a wrapper for Signer and contains a socket to - /// the WebSocket. - /// - /// # Arguments - /// - /// * `key` - A string that holds the key for the API service. - /// * `secret` - A string that holds the secret for the API service. - pub fn new(key: &str, secret: &str) -> CbResult { - Ok(Self { - signer: Signer::new(key, secret, false)?, - socket_tx: None, - }) +/// Builder to create a WebSocketClient. +pub struct WebSocketClientBuilder { + api_key: Option, + api_secret: Option, + enable_public: bool, + enable_user: bool, + max_retries: u32, + public_bucket: Arc>, + secure_bucket: Arc>, +} + +impl Default for WebSocketClientBuilder { + fn default() -> Self { + Self { + api_key: None, + api_secret: None, + enable_public: true, // By default, enable public connection. + enable_user: false, // By default, do not enable secure connection. + max_retries: 0, // By default, do not auto-reconnect. + public_bucket: Arc::new(Mutex::new(TokenBucket::new( + RateLimits::max_tokens(false, true), + RateLimits::refresh_rate(false, true), + ))), + secure_bucket: Arc::new(Mutex::new(TokenBucket::new( + RateLimits::max_tokens(false, false), + RateLimits::refresh_rate(false, false), + ))), + } + } +} + +impl WebSocketClientBuilder { + /// Creates a new WebSocketClientBuilder. + pub fn new() -> Self { + Self::default() } - /// Creates a new instance of a Client using a configuration file. This is a wrapper for - /// Signer and contains a socket to the WebSocket. + /// Uses a configuration to initialize the the authentication. /// /// # Arguments /// /// * `config` - Configuration that implements ConfigFile trait. #[cfg(feature = "config")] - pub fn from_config(config: &T) -> CbResult + pub fn with_config(mut self, config: &T) -> Self where T: ConfigFile, { - Self::new(&config.coinbase().api_key, &config.coinbase().api_secret) + self.api_key = Some(config.coinbase().api_key.to_string()); + self.api_secret = Some(config.coinbase().api_secret.to_string()); + self.enable_user = true; + self } - /// Connects to the WebSocket. This is required before subscribing, unsubscribing, and - /// listening for updates. A reader is returned to allow for `listener` to parse events. - pub async fn connect(&mut self) -> CbResult { - match connect_async(RESOURCE_ENDPOINT).await { - Ok((socket, _)) => { - let (sink, stream) = socket.split(); - self.socket_tx = Some(sink); - Ok(stream) - } - Err(_) => Err(CbAdvError::BadConnection( - "unable to get handshake".to_string(), - )), + /// Uses the provided key and secret to initialize the authentication. + /// + /// # Arguments + /// + /// * `key` - API key. + /// * `secret` - API secret. + pub fn with_authentication(mut self, key: &str, secret: &str) -> Self { + self.api_key = Some(key.to_string()); + self.api_secret = Some(secret.to_string()); + self.enable_user = true; + self + } + + /// Enables or disables the public connection. + /// + /// # Arguments + /// + /// * `enable` - Enable or disable the public connection. + pub fn enable_public(mut self, enable: bool) -> Self { + self.enable_public = enable; + self + } + + /// Enables or disables the secure user connection. + /// + /// # Arguments + /// + /// * `enable` - Enable or disable the secure user connection. + pub fn enable_user(mut self, enable: bool) -> Self { + self.enable_user = enable; + self + } + + /// Enables or disables auto-reconnecting the WebSocket. + /// + /// # Arguments + /// + /// * `enable` - Enable or disable auto-reconnecting the WebSocket. + pub fn auto_reconnect(mut self, enable: bool) -> Self { + if enable { + self.max_retries = 14; + } else { + self.max_retries = 0; } + self } - /// Starts the listener which returns Messages via a callback function provided by the user. - /// This allows the user to get objects out of the WebSocket stream for additional processing. - /// the WebSocket. If it is unable to parse an object received, the user is supplied - /// CBAdvError::BadParse along with the data it failed to parse. + /// Sets the maximum number of retries for auto-reconnecting the WebSocket. /// /// # Arguments /// - /// * `reader` - Allows the listener to receive messages. `Obtained from connect``. - /// * `callback` - A callback function that is trigger and passed the Message received via - /// WebSocket, if an error occurred. - pub async fn listener(reader: WebSocketReader, callback: MessageCallbackFn) { - // Read messages and send to the callback as they come in. - let read_future = reader.for_each(|message| { - let data: String = match message { - Ok(value) => value.to_string(), - Err(err) => format!("websocket sent the following error, {}", err), - }; + /// * `max_retries` - Maximum number of retries. + pub fn max_retries(mut self, max_retries: u32) -> Self { + self.max_retries = max_retries; + self + } + + /// Builds the WebSocketClient. + pub fn build(self) -> CbResult { + // Ensure at least one connection is enabled. + if !self.enable_public && !self.enable_user { + return Err(CbError::BadConnection( + "At least one of public or secure connections must be enabled.".to_string(), + )); + } + + // Create JWT if user connection is enabled. + let jwt = if self.enable_user { + let key = self.api_key.ok_or_else(|| { + CbError::BadPrivateKey("API key is required for authentication.".to_string()) + })?; + let secret = self.api_secret.ok_or_else(|| { + CbError::BadPrivateKey("API secret is required for authentication.".to_string()) + })?; + Some(Jwt::new(&key, &secret)?) + } else { + None + }; + + Ok(WebSocketClient { + jwt, + public_bucket: self.public_bucket, + secure_bucket: self.secure_bucket, + public_tx: Arc::new(Mutex::new(None)), + secure_tx: Arc::new(Mutex::new(None)), + enable_public: self.enable_public, + enable_user: self.enable_user, + max_retries: self.max_retries, + subscriptions: Arc::new(Mutex::new(WebSocketSubscriptions::new())), + }) + } +} + +/// Represents a Client for the Websocket API. Provides easy-access to subscribing and listening to +/// the WebSocket. +pub struct WebSocketClient { + /// Signs the messages sent. + pub(crate) jwt: Option, + /// Public bucket. + pub(crate) public_bucket: Arc>, + /// Secure bucket. + pub(crate) secure_bucket: Arc>, + /// Writes data to the public stream, gets sent to the API. + pub(crate) public_tx: Arc>>>, + /// Writes data to the secure stream, gets sent to the API. + pub(crate) secure_tx: Arc>>>, + /// Enable public connection. + pub(crate) enable_public: bool, + /// Enable secure user connection. + pub(crate) enable_user: bool, + /// Automatically reconnect the WebSocket after a disconnection. + pub(crate) max_retries: u32, + /// Tracked subscriptions. + pub(crate) subscriptions: Arc>, +} + +impl Clone for WebSocketClient { + fn clone(&self) -> Self { + Self { + jwt: self.jwt.clone(), + public_bucket: self.public_bucket.clone(), + secure_bucket: self.secure_bucket.clone(), + public_tx: self.public_tx.clone(), + secure_tx: self.secure_tx.clone(), + enable_public: self.enable_public, + enable_user: self.enable_user, + max_retries: self.max_retries, + subscriptions: self.subscriptions.clone(), + } + } +} + +impl WebSocketClient { + /// Connects to the endpoints specified in the builder. This is required before subscribing to any channels. + pub async fn connect(&mut self) -> CbResult { + let mut endpoints = WebSocketEndpoints::default(); + + if self.enable_public { + let endpoint = self.connect_endpoint(&EndpointType::Public).await?; + endpoints.add(EndpointType::Public, endpoint); + } + + if self.enable_user { + let endpoint = self.connect_endpoint(&EndpointType::User).await?; + endpoints.add(EndpointType::User, endpoint); + } + + Ok(endpoints) + } + + /// Connects to the WebSocket endpoint. + async fn connect_endpoint(&mut self, endpoint_type: &EndpointType) -> CbResult { + match endpoint_type { + EndpointType::Public => { + let (public_socket, _) = connect_async(PUBLIC_ENDPOINT).await.map_err(|e| { + CbError::BadConnection(format!( + "Unable to establish public WebSocket connection: {}", + e + )) + })?; + let (public_sink, stream) = public_socket.split(); + { + let mut tx = self.public_tx.lock().await; + *tx = Some(public_sink); + } + Ok(Endpoint::Public((EndpointType::Public, stream))) + } + EndpointType::User => { + let (secure_socket, _) = connect_async(SECURE_ENDPOINT).await.map_err(|e| { + CbError::BadConnection(format!( + "Unable to establish secure user WebSocket connection: {}", + e + )) + })?; + let (secure_sink, stream) = secure_socket.split(); + { + let mut tx = self.secure_tx.lock().await; + *tx = Some(secure_sink); + } + Ok(Endpoint::User((EndpointType::User, stream))) + } + } + } + + /// Reconnects to a specific endpoint. Returns the reader of the endpoint. + async fn reconnect(&mut self, endpoint_type: &EndpointType) -> CbResult { + let endpoint = self.connect_endpoint(endpoint_type).await?; + + // Re-subscribe to previous channels for this endpoint. + let subs = { + let subscriptions = self.subscriptions.lock().await; + subscriptions.get(endpoint_type).await + }; + + // Add the subscriptions back. + for (channel, product_ids) in subs { + self.sub(&channel, &product_ids).await?; + } + + Ok(endpoint) + } + + /// Waits for a reconnection to occur. This is used when a WebSocket connection is lost. + async fn wait_on_reconnect(&mut self, endpoint_type: &EndpointType) -> CbResult { + if self.max_retries == 0 { + return Err(CbError::BadConnection( + "Auto-reconnect is disabled. Exiting...".to_string(), + )); + } - // Parse the message. - match serde_json::from_str(&data) { - Ok(message) => callback(Ok(message)), - _ => callback(Err(CbAdvError::BadParse(format!( - "unable to parse message: {}", - data - )))), + let mut retries = 0; + let mut retry_delay = 2; + + while retries < self.max_retries { + match self.reconnect(endpoint_type).await { + Ok(endpoint) => return Ok(endpoint), + Err(e) => { + eprintln!( + "Failed to reconnect WebSocket: {}. Retrying in {} seconds...", + e, retry_delay + ); + tokio::time::sleep(tokio::time::Duration::from_secs(retry_delay)).await; + retries += 1; + retry_delay = (retry_delay * 2).min(60); + } } + } - async {} - }); + Err(CbError::BadConnection(format!( + "Failed to reconnect WebSocket after {} attempts.", + retries, + ))) + } - read_future.await + /// Waits for a token to be consumable for the correct bucket. + async fn wait_on_bucket(&mut self, endpoint: &EndpointType) { + match endpoint { + EndpointType::Public => { + let mut locked_bucket = self.public_bucket.lock().await; + locked_bucket.wait_on().await; + } + EndpointType::User => { + let mut locked_bucket = self.secure_bucket.lock().await; + locked_bucket.wait_on().await; + } + } } - /// Starts the listener with a callback object that implements the `MessageCallback` trait. - /// This allows the user to get objects out of the WebSocket stream for additional processing. - /// the WebSocket. If it is unable to parse an object received, the user is supplied - /// CBAdvError::BadParse along with the data it failed to parse. + /// Processes WebSocket messages and applies a callback. Created to ignore alternative message types. /// /// # Arguments /// - /// * `reader` - Allows the listener to receive messages. `Obtained from connect``. - /// * `callback_obj` - A callback object that implements `MessageCallback` trait. - pub async fn listener_with(reader: WebSocketReader, callback_obj: T) + /// * `message` - A WebSocket message to process. + /// * `callback` - A closure or function that processes parsed messages or errors. + async fn process_message(message: Result) -> Option> { + match message { + Ok(msg) => match msg { + WsMessage::Text(data) => { + let result = serde_json::from_str::(&data).map_err(|e| { + CbError::BadParse(format!( + "Unable to parse message: {}. Error: {}", + data, e + )) + }); + Some(result) + } + WsMessage::Ping(_) | WsMessage::Pong(_) | WsMessage::Binary(_) => None, // Ignored. + WsMessage::Close(frame) => { + eprintln!("WebSocket closed: {:?}", frame); + Some(Err(CbError::BadConnection("WebSocket closed".to_string()))) + } + _ => { + eprintln!("Received an unrecognized message type: {:?}", msg); + None + } + }, + Err(err) => Some(Err(CbError::BadConnection(format!( + "WebSocket error: {}", + err + )))), + } + } + + /// Listens to a single WebSocket reader using a function callback. + /// + /// # Arguments + /// + /// * `endpoint` - WebSocket reader for a single channel (e.g., public or user). + /// * `callback` - A function callback that processes WebSocket messages. + pub async fn listen_fn(&mut self, mut endpoint: Endpoint, callback: MessageCallbackFn) { + loop { + let (route, reader) = match &mut endpoint { + Endpoint::Public((route, reader)) => (route, reader), + Endpoint::User((route, reader)) => (route, reader), + }; + + while let Some(message) = reader.next().await { + if let Some(result) = Self::process_message(message).await { + if let Err(CbError::BadConnection(_)) = &result { + // Attempt to reconnect. + match self.wait_on_reconnect(route).await { + Ok(new_endpoint) => { + endpoint = new_endpoint; + // Exit the inner loop to restart with the new endpoint. + break; + } + Err(e) => { + eprintln!("{}", e); + return; + } + } + } else { + callback(result); + } + } + } + } + } + + /// Listens to a single WebSocket reader using a callback object that implements `MessageCallback`. + /// + /// # Arguments + /// + /// * `endpoint` - WebSocket reader for a single channel (e.g., public or user). + /// * `callback_obj` - A callback object that implements the `MessageCallback` trait. + pub async fn listen_trait(&mut self, mut endpoint: Endpoint, mut callback_obj: T) where - T: MessageCallback, + T: MessageCallback + Send + 'static, { - // Make the callback object mutable. - let mut obj: T = callback_obj; - - // Read messages and send to the callback as they come in. - let read_future = reader.for_each(|message| { - let data: String = match message { - Ok(value) => value.to_string(), - Err(err) => format!("websocket sent the following error, {}", err), + loop { + let (route, reader) = match &mut endpoint { + Endpoint::Public((route, reader)) => (route, reader), + Endpoint::User((route, reader)) => (route, reader), }; - // Parse the message. - match serde_json::from_str(&data) { - Ok(message) => obj.message_callback(Ok(message)), - _ => obj.message_callback(Err(CbAdvError::BadParse(format!( - "unable to parse message: {}", - data - )))), + while let Some(message) = reader.next().await { + if let Some(result) = Self::process_message(message).await { + if let Err(CbError::BadConnection(_)) = &result { + // Attempt to reconnect. + match self.wait_on_reconnect(route).await { + Ok(new_endpoint) => { + endpoint = new_endpoint; + // Exit the inner loop to restart with the new endpoint. + break; + } + Err(e) => { + eprintln!("{}", e); + return; + } + } + } else { + callback_obj.message_callback(result); + } + } + } + } + } + + /// Listens to WebSocket readers using a function callback. + /// + /// # Arguments + /// + /// * `endpoints` - WebSocket readers for the public and user. + /// * `callback` - A function callback that processes WebSocket messages. + pub async fn multi_listen_fn( + &mut self, + mut endpoints: WebSocketEndpoints, + callback: MessageCallbackFn, + ) { + loop { + let streams = endpoints.extract_to_vec(); + if streams.is_empty() { + // No streams to listen to, exit the loop. + return; + } + + let mut stream = stream::select_all(streams); + while let Some(message) = stream.next().await { + if let Some(result) = Self::process_message(message).await { + if let Err(CbError::BadConnection(_)) = &result { + // Obtain the endpoints to reconnect. + let keys = { + let subs = self.subscriptions.lock().await; + subs.get_keys().await + }; + + // Attempt to reconnect all endpoints. + let mut new_endpoints = WebSocketEndpoints::default(); + for endpoint_type in keys { + match self.wait_on_reconnect(&endpoint_type).await { + Ok(new_endpoint) => { + new_endpoints.add(endpoint_type.clone(), new_endpoint); + } + Err(e) => { + eprintln!("Failed to reconnect: {}", e); + return; + } + } + } + + // Break to restart the loop with new endpoints. + endpoints = new_endpoints; + break; + } else { + callback(result); + } + } } + } + } - async {} - }); + /// Listens to WebSocket readers using a callback object that implements `MessageCallback`. + /// + /// # Arguments + /// + /// * `endpoints` - WebSocket readers for the public and user. + /// * `callback_obj` - A callback object that implements the `MessageCallback` trait. + pub async fn multi_listen_trait( + &mut self, + mut endpoints: WebSocketEndpoints, + mut callback_obj: T, + ) where + T: MessageCallback + Send + 'static, + { + loop { + let streams = endpoints.extract_to_vec(); + if streams.is_empty() { + // No streams to listen to, exit the loop. + return; + } - read_future.await + let mut stream = stream::select_all(streams); + while let Some(message) = stream.next().await { + if let Some(result) = Self::process_message(message).await { + if let Err(CbError::BadConnection(_)) = &result { + // Obtain the endpoints to reconnect. + let keys = { + let subs = self.subscriptions.lock().await; + subs.get_keys().await + }; + + // Attempt to reconnect all endpoints. + let mut new_endpoints = WebSocketEndpoints::default(); + for endpoint_type in keys { + match self.wait_on_reconnect(&endpoint_type).await { + Ok(new_endpoint) => { + new_endpoints.add(endpoint_type.clone(), new_endpoint); + } + Err(e) => { + eprintln!("Failed to reconnect: {}", e); + return; + } + } + } + + // Break to restart the loop with new endpoints. + endpoints = new_endpoints; + break; + } else { + callback_obj.message_callback(result); + } + } + } + } } /// Updates the WebSocket with either additional subscriptions or unsubscriptions. This is @@ -155,46 +575,78 @@ impl WebSocketClient { /// /// * `channel` - The Channel that is being updated. /// * `product_ids` - A vector of product IDs that are being changed. - /// * `subscribe` - Subscription updates, this is true. Unsubscribing this is false. - async fn update( + /// * `action` - The action being taken (either "subscribe" or "unsubscribe"). + /// * `endpoint` - The endpoint type (either public or user). + pub(crate) async fn update( &mut self, - channel: Channel, + channel: &Channel, product_ids: &[String], - subscribe: bool, + action: &str, + endpoint: &EndpointType, ) -> CbResult<()> { - // Set the correct direction for the update. - let update = match subscribe { - true => "subscribe".to_string(), - false => "unsubscribe".to_string(), - }; - - let timestamp = time::now().to_string(); - let channel = channel.to_string(); - // Create the subscription/unsubscription. - let sub = Subscription { - r#type: update, - product_ids: product_ids.to_vec(), - channel, - jwt: self.signer.get_jwt(SERVICE, None)?, - timestamp, + let sub = if *endpoint == EndpointType::Public { + Subscription::Unsigned(UnsignedSubscription { + r#type: action.to_string(), + product_ids: product_ids.to_vec(), + channel: channel.clone(), + timestamp: time::now().to_string(), + }) + } else { + Subscription::Secure(SecureSubscription { + r#type: action.to_string(), + product_ids: product_ids.to_vec(), + channel: channel.clone(), + jwt: self + .jwt + .as_ref() + .ok_or_else(|| { + CbError::BadPrivateKey("User authentication required.".to_string()) + }) + .unwrap() + .encode(None)?, + }) }; - match self.socket_tx { - None => Err(CbAdvError::BadConnection( - "need to connect first.".to_string(), - )), - - Some(ref mut socket) => { - // Serialize and send the update to the API. - let body_str = serde_json::to_string(&sub).unwrap(); - - // Wait until a token is available to make the request. Immediately consume it. - self.signer.bucket.wait_on().await; - - match socket.send(tungstenite::Message::text(body_str)).await { - Ok(_) => Ok(()), - _ => Ok(()), + let body = serde_json::to_string(&sub).map_err(|e| { + CbError::BadSerialization(format!("Failed to serialize subscription: {}", e)) + })?; + let body_message = WsMessage::text(body); + + // Wait until a token is available to make the request. Immediately consume it. + self.wait_on_bucket(endpoint).await; + + match endpoint { + EndpointType::Public => { + let mut tx = self.public_tx.lock().await; + if let Some(socket) = tx.as_mut() { + socket.send(body_message).await.map_err(|e| { + CbError::BadConnection(format!( + "Failed to send message over WebSocket: {}", + e + )) + }) + } else { + Err(CbError::BadConnection( + "Public WebSocket connection not established. Call `connect()` first." + .to_string(), + )) + } + } + EndpointType::User => { + let mut tx = self.secure_tx.lock().await; + if let Some(socket) = tx.as_mut() { + socket.send(body_message).await.map_err(|e| { + CbError::BadConnection(format!( + "Failed to send message over WebSocket: {}", + e + )) + }) + } else { + Err(CbError::BadConnection( + "Secure WebSocket connection not established. Call `connect()` first." + .to_string(), + )) } } } @@ -208,8 +660,32 @@ impl WebSocketClient { /// /// * `channel` - The Channel that is being subscribed to. /// * `product_ids` - A vector of product IDs to listen for. - pub async fn subscribe(&mut self, channel: Channel, product_ids: &[String]) -> CbResult<()> { - self.update(channel, product_ids, true).await + pub async fn subscribe(&mut self, channel: &Channel, product_ids: &[String]) -> CbResult<()> { + let route = &get_channel_endpoint(channel); + match route { + EndpointType::Public if !self.enable_public => { + return Err(CbError::BadConnection( + "Public connection is not enabled.".to_string(), + )); + } + EndpointType::User if !self.enable_user => { + return Err(CbError::BadConnection( + "Secure user connection is not enabled.".to_string(), + )); + } + _ => {} + } + + // Send the subscription. + self.update(channel, product_ids, "subscribe", route) + .await?; + + { + // Update the subscriptions. + let mut subs = self.subscriptions.lock().await; + subs.add(channel, product_ids, route).await; + } + Ok(()) } /// Shorthand version of `subscribe`. @@ -218,7 +694,7 @@ impl WebSocketClient { /// /// * `channel` - The Channel that is being subscribed to. /// * `product_ids` - A vector of product IDs to listen for. - pub async fn sub(&mut self, channel: Channel, product_ids: &[String]) -> CbResult<()> { + pub async fn sub(&mut self, channel: &Channel, product_ids: &[String]) -> CbResult<()> { self.subscribe(channel, product_ids).await } @@ -229,8 +705,32 @@ impl WebSocketClient { /// /// * `channel` - The Channel that is being changed to. /// * `product_ids` - A vector of product IDs to no longer listen for. - pub async fn unsubscribe(&mut self, channel: Channel, product_ids: &[String]) -> CbResult<()> { - self.update(channel, product_ids, false).await + pub async fn unsubscribe(&mut self, channel: &Channel, product_ids: &[String]) -> CbResult<()> { + let route = &get_channel_endpoint(channel); + match route { + EndpointType::Public if !self.enable_public => { + return Err(CbError::BadConnection( + "Public connection is not enabled.".to_string(), + )); + } + EndpointType::User if !self.enable_user => { + return Err(CbError::BadConnection( + "Secure user connection is not enabled.".to_string(), + )); + } + _ => {} + } + + // Send the unsubscription. + self.update(channel, product_ids, "unsubscribe", route) + .await?; + + { + // Update the subscriptions. + let mut subs = self.subscriptions.lock().await; + subs.remove(channel, product_ids, route).await; + } + Ok(()) } /// Shorthand version of `unsubscribe`. @@ -239,7 +739,7 @@ impl WebSocketClient { /// /// * `channel` - The Channel that is being changed to. /// * `product_ids` - A vector of product IDs to no longer listen for. - pub async fn unsub(&mut self, channel: Channel, product_ids: &[String]) -> CbResult<()> { + pub async fn unsub(&mut self, channel: &Channel, product_ids: &[String]) -> CbResult<()> { self.unsubscribe(channel, product_ids).await } @@ -250,34 +750,33 @@ impl WebSocketClient { /// * `products` - Products to watch for candles for. /// * `watcher` - User-defined struct that implements `CandleCallback` to send completed candles to. pub async fn watch_candles( - &mut self, + mut self, products: &[String], watcher: T, ) -> CbResult> where - T: CandleCallback + Send + 'static, + T: CandleCallback + Send + Sync + 'static, { - // Connect and spawn a task. - let reader = match self.connect().await { - Ok(reader) => reader, - Err(err) => return Err(err), - }; - - // Starts task to watch candles using users watcher. - let listener = tokio::spawn(TaskTracker::start(reader, watcher)); - - // Keep the connection open. - match self.sub(Channel::Heartbeats, &[]).await { - Ok(_) => (), - Err(err) => return Err(err), - }; - - // Subscribe to the candle updates for the products. - match self.sub(Channel::Candles, products).await { - Ok(_) => (), - Err(err) => return Err(err), - }; + if !self.enable_public { + return Err(CbError::BadConnection( + "Public connection is not enabled.".to_string(), + )); + } - Ok(listener) + // Connect and spawn a task. + match self.connect().await?.take_endpoint(&EndpointType::Public) { + Some(public) => { + // Keep the connection open by subscribing to heartbeats and sub to candles. + self.sub(&Channel::Heartbeats, &[]).await?; + self.sub(&Channel::Candles, products).await?; + + // Start task to watch candles using user's watcher. + let listener = tokio::spawn(CandleWatcher::start(self, public, watcher)); + Ok(listener) + } + None => Err(CbError::BadConnection( + "Public connection is not connected.".to_string(), + )), + } } }