diff --git a/apps/hermes/server/Cargo.lock b/apps/hermes/server/Cargo.lock index ef9b6e46aa..6ea05d5f3f 100644 --- a/apps/hermes/server/Cargo.lock +++ b/apps/hermes/server/Cargo.lock @@ -255,7 +255,7 @@ checksum = "7abe79b0e4288889c4574159ab790824d0033b9fdcb2a112a3182fac2e514565" dependencies = [ "num-bigint 0.4.4", "num-traits", - "proc-macro2 1.0.79", + "proc-macro2 1.0.92", "quote 1.0.35", "syn 1.0.109", ] @@ -291,7 +291,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae3281bc6d0fd7e549af32b52511e1302185bd688fd3359fa36423346ff682ea" dependencies = [ - "proc-macro2 1.0.79", + "proc-macro2 1.0.92", "quote 1.0.35", "syn 1.0.109", ] @@ -352,7 +352,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "726535892e8eae7e70657b4c8ea93d26b8553afb1ce617caee529ef96d7dee6c" dependencies = [ - "proc-macro2 1.0.79", + "proc-macro2 1.0.92", "quote 1.0.35", "syn 1.0.109", "synstructure", @@ -364,7 +364,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2777730b2039ac0f95f093556e61b6d26cebed5393ca6f152717777cec3a42ed" dependencies = [ - "proc-macro2 1.0.79", + "proc-macro2 1.0.92", "quote 1.0.35", "syn 1.0.109", ] @@ -426,9 +426,9 @@ version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ - "proc-macro2 1.0.79", + "proc-macro2 1.0.92", "quote 1.0.35", - "syn 2.0.55", + "syn 2.0.89", ] [[package]] @@ -437,9 +437,9 @@ version = "0.1.79" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a507401cad91ec6a857ed5513a2073c82a9b9048762b885bb98655b306964681" dependencies = [ - "proc-macro2 1.0.79", + "proc-macro2 1.0.92", "quote 1.0.35", - "syn 2.0.55", + "syn 2.0.89", ] [[package]] @@ -519,9 +519,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdca6a10ecad987bda04e95606ef85a5417dcaac1a78455242d72e031e2b6b62" dependencies = [ "heck 0.4.1", - "proc-macro2 1.0.79", + "proc-macro2 1.0.92", "quote 1.0.35", - "syn 2.0.55", + "syn 2.0.89", ] [[package]] @@ -593,6 +593,18 @@ dependencies = [ "typenum", ] +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "blake3" version = "1.5.1" @@ -652,6 +664,16 @@ dependencies = [ "hashbrown 0.13.2", ] +[[package]] +name = "borsh" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2506947f73ad44e344215ccd6403ac2ae18cd8e046e581a441bf8d199f257f03" +dependencies = [ + "borsh-derive 1.5.3", + "cfg_aliases", +] + [[package]] name = "borsh-derive" version = "0.9.3" @@ -661,7 +683,7 @@ dependencies = [ "borsh-derive-internal 0.9.3", "borsh-schema-derive-internal 0.9.3", "proc-macro-crate 0.1.5", - "proc-macro2 1.0.79", + "proc-macro2 1.0.92", "syn 1.0.109", ] @@ -674,17 +696,30 @@ dependencies = [ "borsh-derive-internal 0.10.3", "borsh-schema-derive-internal 0.10.3", "proc-macro-crate 0.1.5", - "proc-macro2 1.0.79", + "proc-macro2 1.0.92", "syn 1.0.109", ] +[[package]] +name = "borsh-derive" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2593a3b8b938bd68373196c9832f516be11fa487ef4ae745eb282e6a56a7244" +dependencies = [ + "once_cell", + "proc-macro-crate 3.1.0", + "proc-macro2 1.0.92", + "quote 1.0.35", + "syn 2.0.89", +] + [[package]] name = "borsh-derive-internal" version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5449c28a7b352f2d1e592a8a28bf139bc71afb0764a14f3c02500935d8c44065" dependencies = [ - "proc-macro2 1.0.79", + "proc-macro2 1.0.92", "quote 1.0.35", "syn 1.0.109", ] @@ -695,7 +730,7 @@ version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "afb438156919598d2c7bad7e1c0adf3d26ed3840dbc010db1a882a65583ca2fb" dependencies = [ - "proc-macro2 1.0.79", + "proc-macro2 1.0.92", "quote 1.0.35", "syn 1.0.109", ] @@ -706,7 +741,7 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdbd5696d8bfa21d53d9fe39a714a18538bad11492a42d066dbbc395fb1951c0" dependencies = [ - "proc-macro2 1.0.79", + "proc-macro2 1.0.92", "quote 1.0.35", "syn 1.0.109", ] @@ -717,7 +752,7 @@ version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "634205cc43f74a1b9046ef87c4540ebda95696ec0f315024860cad7c5b0f5ccd" dependencies = [ - "proc-macro2 1.0.79", + "proc-macro2 1.0.92", "quote 1.0.35", "syn 1.0.109", ] @@ -776,6 +811,28 @@ dependencies = [ "serde", ] +[[package]] +name = "bytecheck" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" +dependencies = [ + "proc-macro2 1.0.92", + "quote 1.0.35", + "syn 1.0.109", +] + [[package]] name = "bytemuck" version = "1.15.0" @@ -791,9 +848,9 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4da9a32f3fed317401fa3c862968128267c3106685286e15d5aaa3d7389c2f60" dependencies = [ - "proc-macro2 1.0.79", + "proc-macro2 1.0.92", "quote 1.0.35", - "syn 2.0.55", + "syn 2.0.89", ] [[package]] @@ -834,6 +891,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chrono" version = "0.4.35" @@ -918,9 +981,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" dependencies = [ "heck 0.5.0", - "proc-macro2 1.0.79", + "proc-macro2 1.0.92", "quote 1.0.35", - "syn 2.0.55", + "syn 2.0.89", ] [[package]] @@ -1152,10 +1215,10 @@ checksum = "9c2cf1c23a687a1feeb728783b993c4e1ad83d99f351801977dd809b48d0a70f" dependencies = [ "fnv", "ident_case", - "proc-macro2 1.0.79", + "proc-macro2 1.0.92", "quote 1.0.35", "strsim 0.10.0", - "syn 2.0.55", + "syn 2.0.89", ] [[package]] @@ -1166,7 +1229,7 @@ checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f" dependencies = [ "darling_core", "quote 1.0.35", - "syn 2.0.55", + "syn 2.0.89", ] [[package]] @@ -1232,7 +1295,7 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" dependencies = [ - "proc-macro2 1.0.79", + "proc-macro2 1.0.92", "quote 1.0.35", "syn 1.0.109", ] @@ -1244,7 +1307,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" dependencies = [ "convert_case", - "proc-macro2 1.0.79", + "proc-macro2 1.0.92", "quote 1.0.35", "rustc_version", "syn 1.0.109", @@ -1308,9 +1371,9 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" dependencies = [ - "proc-macro2 1.0.79", + "proc-macro2 1.0.92", "quote 1.0.35", - "syn 2.0.55", + "syn 2.0.89", ] [[package]] @@ -1425,9 +1488,9 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03cdc46ec28bd728e67540c528013c6a10eb69a02eb31078a1bda695438cbfb8" dependencies = [ - "proc-macro2 1.0.79", + "proc-macro2 1.0.92", "quote 1.0.35", - "syn 2.0.55", + "syn 2.0.89", ] [[package]] @@ -1545,6 +1608,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futures" version = "0.3.30" @@ -1599,9 +1668,9 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ - "proc-macro2 1.0.79", + "proc-macro2 1.0.92", "quote 1.0.35", - "syn 2.0.55", + "syn 2.0.89", ] [[package]] @@ -1766,6 +1835,9 @@ name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash 0.7.8", +] [[package]] name = "hashbrown" @@ -1796,7 +1868,7 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermes" -version = "0.7.2" +version = "0.8.0" dependencies = [ "anyhow", "async-trait", @@ -1828,6 +1900,7 @@ dependencies = [ "pythnet-sdk", "rand 0.8.5", "reqwest", + "rust_decimal", "secp256k1", "serde", "serde_json", @@ -2569,7 +2642,7 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" dependencies = [ - "proc-macro2 1.0.79", + "proc-macro2 1.0.92", "quote 1.0.35", "syn 1.0.109", ] @@ -2580,9 +2653,9 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ - "proc-macro2 1.0.79", + "proc-macro2 1.0.92", "quote 1.0.35", - "syn 2.0.55", + "syn 2.0.89", ] [[package]] @@ -2673,9 +2746,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96667db765a921f7b295ffee8b60472b686a51d4f21c2ee4ffdb94c7013b65a6" dependencies = [ "proc-macro-crate 1.3.1", - "proc-macro2 1.0.79", + "proc-macro2 1.0.92", "quote 1.0.35", - "syn 2.0.55", + "syn 2.0.89", ] [[package]] @@ -2685,9 +2758,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "681030a937600a36906c185595136d26abfebb4aa9c65701cefcaf8578bb982b" dependencies = [ "proc-macro-crate 3.1.0", - "proc-macro2 1.0.79", + "proc-macro2 1.0.92", "quote 1.0.35", - "syn 2.0.55", + "syn 2.0.89", ] [[package]] @@ -2747,9 +2820,9 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ - "proc-macro2 1.0.79", + "proc-macro2 1.0.92", "quote 1.0.35", - "syn 2.0.55", + "syn 2.0.89", ] [[package]] @@ -2878,9 +2951,9 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ - "proc-macro2 1.0.79", + "proc-macro2 1.0.92", "quote 1.0.35", - "syn 2.0.55", + "syn 2.0.89", ] [[package]] @@ -2954,8 +3027,8 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d3928fb5db768cb86f891ff014f0144589297e3c6a1aba6ed7cecfdace270c7" dependencies = [ - "proc-macro2 1.0.79", - "syn 2.0.55", + "proc-macro2 1.0.92", + "syn 2.0.89", ] [[package]] @@ -2993,7 +3066,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" dependencies = [ "proc-macro-error-attr", - "proc-macro2 1.0.79", + "proc-macro2 1.0.92", "quote 1.0.35", "syn 1.0.109", "version_check", @@ -3005,7 +3078,7 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" dependencies = [ - "proc-macro2 1.0.79", + "proc-macro2 1.0.92", "quote 1.0.35", "version_check", ] @@ -3021,9 +3094,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.79" +version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" dependencies = [ "unicode-ident", ] @@ -3046,9 +3119,9 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "440f724eba9f6996b75d63681b0a92b06947f1457076d503a4d2e2c8f56442b8" dependencies = [ - "proc-macro2 1.0.79", + "proc-macro2 1.0.92", "quote 1.0.35", - "syn 2.0.55", + "syn 2.0.89", ] [[package]] @@ -3078,7 +3151,7 @@ dependencies = [ "prost", "prost-types", "regex", - "syn 2.0.55", + "syn 2.0.89", "tempfile", "which", ] @@ -3091,9 +3164,9 @@ checksum = "efb6c9a1dd1def8e2124d17e83a20af56f1570d6c2d2bd9e266ccb768df3840e" dependencies = [ "anyhow", "itertools 0.11.0", - "proc-macro2 1.0.79", + "proc-macro2 1.0.92", "quote 1.0.35", - "syn 2.0.55", + "syn 2.0.89", ] [[package]] @@ -3105,6 +3178,26 @@ dependencies = [ "prost", ] +[[package]] +name = "ptr_meta" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" +dependencies = [ + "proc-macro2 1.0.92", + "quote 1.0.35", + "syn 1.0.109", +] + [[package]] name = "pyth-sdk" version = "0.8.0" @@ -3243,9 +3336,15 @@ version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" dependencies = [ - "proc-macro2 1.0.79", + "proc-macro2 1.0.92", ] +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + [[package]] name = "rand" version = "0.7.3" @@ -3431,6 +3530,15 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +[[package]] +name = "rend" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" +dependencies = [ + "bytecheck", +] + [[package]] name = "reqwest" version = "0.11.27" @@ -3507,6 +3615,35 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rkyv" +version = "0.7.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9008cd6385b9e161d8229e1f6549dd23c3d022f132a2ea37ac3a10ac4935779b" +dependencies = [ + "bitvec", + "bytecheck", + "bytes", + "hashbrown 0.12.3", + "ptr_meta", + "rend", + "rkyv_derive", + "seahash", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.7.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "503d1d27590a2b0a3a4ca4c94755aa2875657196ecbf401a42eff41d7de532c0" +dependencies = [ + "proc-macro2 1.0.92", + "quote 1.0.35", + "syn 1.0.109", +] + [[package]] name = "rpassword" version = "7.3.1" @@ -3545,11 +3682,11 @@ version = "6.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49b94b81e5b2c284684141a2fb9e2a31be90638caf040bf9afbc5a0416afe1ac" dependencies = [ - "proc-macro2 1.0.79", + "proc-macro2 1.0.92", "quote 1.0.35", "rust-embed-utils", "shellexpand", - "syn 2.0.55", + "syn 2.0.89", "walkdir", ] @@ -3563,6 +3700,22 @@ dependencies = [ "walkdir", ] +[[package]] +name = "rust_decimal" +version = "1.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b082d80e3e3cc52b2ed634388d436fe1f4de6af5786cc2de9ba9737527bdf555" +dependencies = [ + "arrayvec", + "borsh 1.5.3", + "bytes", + "num-traits", + "rand 0.8.5", + "rkyv", + "serde", + "serde_json", +] + [[package]] name = "rustc-demangle" version = "0.1.23" @@ -3709,7 +3862,7 @@ version = "0.8.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c767fd6fa65d9ccf9cf026122c1b555f2ef9a4f0cea69da4d7dbc3e258d30967" dependencies = [ - "proc-macro2 1.0.79", + "proc-macro2 1.0.92", "quote 1.0.35", "serde_derive_internals", "syn 1.0.109", @@ -3736,9 +3889,9 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1db149f81d46d2deba7cd3c50772474707729550221e69588478ebf9ada425ae" dependencies = [ - "proc-macro2 1.0.79", + "proc-macro2 1.0.92", "quote 1.0.35", - "syn 2.0.55", + "syn 2.0.89", ] [[package]] @@ -3751,6 +3904,12 @@ dependencies = [ "untrusted 0.9.0", ] +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + [[package]] name = "secp256k1" version = "0.27.0" @@ -3824,9 +3983,9 @@ version = "1.0.197" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" dependencies = [ - "proc-macro2 1.0.79", + "proc-macro2 1.0.92", "quote 1.0.35", - "syn 2.0.55", + "syn 2.0.89", ] [[package]] @@ -3835,7 +3994,7 @@ version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85bf8229e7920a9f636479437026331ce11aa132b4dde37d121944a44d6e5f3c" dependencies = [ - "proc-macro2 1.0.79", + "proc-macro2 1.0.92", "quote 1.0.35", "syn 1.0.109", ] @@ -3903,9 +4062,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "881b6f881b17d13214e5d494c939ebab463d01264ce1811e9d4ac3a882e7695f" dependencies = [ "darling", - "proc-macro2 1.0.79", + "proc-macro2 1.0.92", "quote 1.0.35", - "syn 2.0.55", + "syn 2.0.89", ] [[package]] @@ -4027,6 +4186,12 @@ version = "1.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "sized-chunks" version = "0.6.5" @@ -4252,10 +4417,10 @@ version = "1.16.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b02411fefc004154edf3fe61cedb1dfb26ef82b659148b0a4b21ce3184d40ebc" dependencies = [ - "proc-macro2 1.0.79", + "proc-macro2 1.0.92", "quote 1.0.35", "rustc_version", - "syn 2.0.55", + "syn 2.0.89", ] [[package]] @@ -4628,10 +4793,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dbca599523925e4c55e0326d93eef0a8e1918df5f93897811abf3f5972e21bec" dependencies = [ "bs58", - "proc-macro2 1.0.79", + "proc-macro2 1.0.92", "quote 1.0.35", "rustversion", - "syn 2.0.55", + "syn 2.0.89", ] [[package]] @@ -4900,7 +5065,7 @@ checksum = "07fd7858fc4ff8fb0e34090e41d7eb06a823e1057945c26d480bfc21d2338a93" dependencies = [ "quote 1.0.35", "spl-discriminator-syn", - "syn 2.0.55", + "syn 2.0.89", ] [[package]] @@ -4909,10 +5074,10 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "18fea7be851bd98d10721782ea958097c03a0c2a07d8d4997041d0ece6319a63" dependencies = [ - "proc-macro2 1.0.79", + "proc-macro2 1.0.92", "quote 1.0.35", "sha2 0.10.8", - "syn 2.0.55", + "syn 2.0.89", "thiserror", ] @@ -4957,10 +5122,10 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1845dfe71fd68f70382232742e758557afe973ae19e6c06807b2c30f5d5cb474" dependencies = [ - "proc-macro2 1.0.79", + "proc-macro2 1.0.92", "quote 1.0.35", "sha2 0.10.8", - "syn 2.0.55", + "syn 2.0.89", ] [[package]] @@ -5091,7 +5256,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" dependencies = [ "heck 0.4.1", - "proc-macro2 1.0.79", + "proc-macro2 1.0.92", "quote 1.0.35", "rustversion", "syn 1.0.109", @@ -5120,18 +5285,18 @@ version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ - "proc-macro2 1.0.79", + "proc-macro2 1.0.92", "quote 1.0.35", "unicode-ident", ] [[package]] name = "syn" -version = "2.0.55" +version = "2.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "002a1b3dbf967edfafc32655d0f377ab0bb7b994aa1d32c8cc7e9b8bf3ebb8f0" +checksum = "44d46482f1c1c87acd84dea20c1bf5ebff4c757009ed6bf19cfd36fb10e92c4e" dependencies = [ - "proc-macro2 1.0.79", + "proc-macro2 1.0.92", "quote 1.0.35", "unicode-ident", ] @@ -5148,7 +5313,7 @@ version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" dependencies = [ - "proc-macro2 1.0.79", + "proc-macro2 1.0.92", "quote 1.0.35", "syn 1.0.109", "unicode-xid 0.2.4", @@ -5175,6 +5340,12 @@ dependencies = [ "libc", ] +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "tempfile" version = "3.10.1" @@ -5226,9 +5397,9 @@ version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" dependencies = [ - "proc-macro2 1.0.79", + "proc-macro2 1.0.92", "quote 1.0.35", - "syn 2.0.55", + "syn 2.0.89", ] [[package]] @@ -5341,9 +5512,9 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ - "proc-macro2 1.0.79", + "proc-macro2 1.0.92", "quote 1.0.35", - "syn 2.0.55", + "syn 2.0.89", ] [[package]] @@ -5505,10 +5676,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d021fc044c18582b9a2408cd0dd05b1596e3ecdb5c4df822bb0183545683889" dependencies = [ "prettyplease", - "proc-macro2 1.0.79", + "proc-macro2 1.0.92", "prost-build", "quote 1.0.35", - "syn 2.0.55", + "syn 2.0.89", ] [[package]] @@ -5579,9 +5750,9 @@ version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ - "proc-macro2 1.0.79", + "proc-macro2 1.0.92", "quote 1.0.35", - "syn 2.0.55", + "syn 2.0.89", ] [[package]] @@ -5820,10 +5991,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05d96dcd6fc96f3df9b3280ef480770af1b7c5d14bc55192baa9b067976d920c" dependencies = [ "proc-macro-error", - "proc-macro2 1.0.79", + "proc-macro2 1.0.92", "quote 1.0.35", "regex", - "syn 2.0.55", + "syn 2.0.89", ] [[package]] @@ -5842,6 +6013,12 @@ dependencies = [ "zip", ] +[[package]] +name = "uuid" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" + [[package]] name = "valuable" version = "0.1.0" @@ -5922,9 +6099,9 @@ dependencies = [ "bumpalo", "log", "once_cell", - "proc-macro2 1.0.79", + "proc-macro2 1.0.92", "quote 1.0.35", - "syn 2.0.55", + "syn 2.0.89", "wasm-bindgen-shared", ] @@ -5956,9 +6133,9 @@ version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ - "proc-macro2 1.0.79", + "proc-macro2 1.0.92", "quote 1.0.35", - "syn 2.0.55", + "syn 2.0.89", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -6278,6 +6455,15 @@ dependencies = [ "thiserror", ] +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + [[package]] name = "x509-parser" version = "0.14.0" @@ -6320,9 +6506,9 @@ version = "0.7.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" dependencies = [ - "proc-macro2 1.0.79", + "proc-macro2 1.0.92", "quote 1.0.35", - "syn 2.0.55", + "syn 2.0.89", ] [[package]] @@ -6340,9 +6526,9 @@ version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ - "proc-macro2 1.0.79", + "proc-macro2 1.0.92", "quote 1.0.35", - "syn 2.0.55", + "syn 2.0.89", ] [[package]] diff --git a/apps/hermes/server/Cargo.toml b/apps/hermes/server/Cargo.toml index 6df64185ec..d302a14907 100644 --- a/apps/hermes/server/Cargo.toml +++ b/apps/hermes/server/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hermes" -version = "0.7.2" +version = "0.8.0" description = "Hermes is an agent that provides Verified Prices from the Pythnet Pyth Oracle." edition = "2021" @@ -34,6 +34,7 @@ pyth-sdk-solana = { version = "0.10.2" } pythnet-sdk = { path = "../../../pythnet/pythnet_sdk/", version = "2.0.0", features = ["strum"] } rand = { version = "0.8.5" } reqwest = { version = "0.11.14", features = ["blocking", "json"] } +rust_decimal = { version = "1.36.0" } secp256k1 = { version = "0.27.0", features = ["rand", "recovery", "serde"] } serde = { version = "1.0.152", features = ["derive"] } serde_json = { version = "1.0.93" } @@ -47,7 +48,7 @@ tonic = { version = "0.10.1", features = ["tls"] } tower-http = { version = "0.4.0", features = ["cors"] } tracing = { version = "0.1.37", features = ["log"] } tracing-subscriber = { version = "0.3.17", features = ["env-filter", "json"] } -utoipa = { version = "3.4.0", features = ["axum_extras"] } +utoipa = { version = "3.4.0", features = ["axum_extras", "decimal"] } utoipa-swagger-ui = { version = "3.1.4", features = ["axum"] } wormhole-sdk = { git = "https://github.com/wormhole-foundation/wormhole", tag = "v2.17.1" } diff --git a/apps/hermes/server/src/api.rs b/apps/hermes/server/src/api.rs index 0484576d49..03365e8b5e 100644 --- a/apps/hermes/server/src/api.rs +++ b/apps/hermes/server/src/api.rs @@ -101,6 +101,7 @@ where rest::latest_vaas, rest::price_feed_ids, rest::latest_price_updates, + rest::latest_twaps, rest::latest_publisher_stake_caps, rest::timestamp_price_updates, rest::price_feeds_metadata, @@ -126,6 +127,8 @@ where types::ParsedPublisherStakeCapsUpdate, types::ParsedPublisherStakeCap, types::AssetType, + types::TwapsResponse, + types::ParsedPriceFeedTwap, ) ), tags( @@ -152,6 +155,15 @@ where get(rest::price_stream_sse_handler), ) .route("/v2/updates/price/latest", get(rest::latest_price_updates)) + .route( + "/v2/updates/twap/:window_seconds/latest", + get(rest::latest_twaps), + ) + // TODO(Tejas) + // .route( + // "/v2/updates/twap/:window_seconds/:publish_time", + // get(rest::latest_twaps), + // ) .route( "/v2/updates/publisher_stake_caps/latest", get(rest::latest_publisher_stake_caps), diff --git a/apps/hermes/server/src/api/rest.rs b/apps/hermes/server/src/api/rest.rs index d4a381a25c..44c8db77f7 100644 --- a/apps/hermes/server/src/api/rest.rs +++ b/apps/hermes/server/src/api/rest.rs @@ -30,8 +30,8 @@ pub use { price_feed_ids::*, ready::*, v2::{ - latest_price_updates::*, latest_publisher_stake_caps::*, price_feeds_metadata::*, sse::*, - timestamp_price_updates::*, + latest_price_updates::*, latest_publisher_stake_caps::*, latest_twaps::*, + price_feeds_metadata::*, sse::*, timestamp_price_updates::*, }, }; @@ -125,7 +125,7 @@ mod tests { crate::state::{ aggregate::{ AggregationEvent, PriceFeedsWithUpdateData, PublisherStakeCapsWithUpdateData, - ReadinessMetadata, RequestTime, Update, + ReadinessMetadata, RequestTime, TwapsWithUpdateData, Update, }, benchmarks::BenchmarksState, cache::CacheState, @@ -198,6 +198,14 @@ mod tests { ) -> Result { unimplemented!("Not needed for this test") } + async fn get_twaps_with_update_data( + &self, + _price_ids: &[PriceIdentifier], + _start_time: RequestTime, + _end_time: RequestTime, + ) -> Result { + unimplemented!("Not needed for this test") + } } #[tokio::test] diff --git a/apps/hermes/server/src/api/rest/index.rs b/apps/hermes/server/src/api/rest/index.rs index fee938984b..fc6d398f05 100644 --- a/apps/hermes/server/src/api/rest/index.rs +++ b/apps/hermes/server/src/api/rest/index.rs @@ -13,9 +13,12 @@ pub async fn index() -> impl IntoResponse { "/api/get_price_feed?id=&publish_time=(&verbose=true)(&binary=true)", "/api/get_vaa?id=&publish_time=", "/api/get_vaa_ccip?data=<0x+>", + "/v2/updates/price/latest?ids[]=&ids[]=&..(&encoding=hex|base64)(&parsed=false)", "/v2/updates/price/stream?ids[]=&ids[]=&..(&encoding=hex|base64)(&parsed=false)(&allow_unordered=false)(&benchmarks_only=false)", "/v2/updates/price/?ids[]=&ids[]=&..(&encoding=hex|base64)(&parsed=false)", "/v2/price_feeds?(query=btc)(&asset_type=crypto|equity|fx|metal|rates)", + "/v2/updates/twap//latest?ids[]=&ids[]=&..(&encoding=hex|base64)(&parsed=false)", + "/v2/updates/twap//?ids[]=&ids[]=&..(&encoding=hex|base64)(&parsed=false)", ]) } diff --git a/apps/hermes/server/src/api/rest/v2/latest_twaps.rs b/apps/hermes/server/src/api/rest/v2/latest_twaps.rs new file mode 100644 index 0000000000..34c10c81cd --- /dev/null +++ b/apps/hermes/server/src/api/rest/v2/latest_twaps.rs @@ -0,0 +1,165 @@ +use { + crate::{ + api::{ + rest::{validate_price_ids, RestError}, + types::{BinaryUpdate, EncodingType, ParsedPriceFeedTwap, PriceIdInput, TwapsResponse}, + ApiState, + }, + state::aggregate::{Aggregates, RequestTime}, + }, + anyhow::Result, + axum::{ + extract::{Path, State}, + Json, + }, + base64::{engine::general_purpose::STANDARD as base64_standard_engine, Engine as _}, + pyth_sdk::{DurationInSeconds, PriceIdentifier, UnixTimestamp}, + serde::Deserialize, + serde_qs::axum::QsQuery, + utoipa::IntoParams, +}; + +#[derive(Debug, Deserialize, IntoParams)] +#[into_params(parameter_in=Path)] +pub struct LatestTwapsPathParams { + /// The time window in seconds over which to calculate the TWAP, ending at the current time. + /// For example, a value of 300 would return the most recent 5 minute TWAP. + /// Must be greater than 0 and less than or equal to 600 seconds (10 minutes). + #[param(example = "300")] + #[serde(deserialize_with = "validate_twap_window")] + window_seconds: u64, +} + +#[derive(Debug, Deserialize, IntoParams)] +#[into_params(parameter_in=Query)] +pub struct LatestTwapsQueryParams { + /// Get the most recent TWAP (time weighted average price) for this set of price feed ids. + /// The `binary` data contains the signed start & end cumulative price updates needed to calculate + /// the TWAPs on-chain. The `parsed` data contains the calculated TWAPs. + /// + /// This parameter can be provided multiple times to retrieve multiple price updates, + /// for example see the following query string: + /// + /// ``` + /// ?ids[]=a12...&ids[]=b4c... + /// ``` + #[param(rename = "ids[]")] + #[param(example = "e62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43")] + ids: Vec, + + /// Optional encoding type. If true, return the cumulative price updates in the encoding specified by the encoding parameter. Default is `hex`. + #[serde(default)] + encoding: EncodingType, + + /// If true, include the calculated TWAP in the `parsed` field of each returned feed. Default is `true`. + #[serde(default = "default_true")] + parsed: bool, + + /// If true, invalid price IDs in the `ids` parameter are ignored. Only applicable to the v2 APIs. Default is `false`. + #[serde(default)] + ignore_invalid_price_ids: bool, +} + +fn validate_twap_window<'de, D>(deserializer: D) -> Result +where + D: serde::Deserializer<'de>, +{ + use serde::de::Error; + let seconds = DurationInSeconds::deserialize(deserializer)?; + if seconds == 0 || seconds > 600 { + return Err(D::Error::custom( + "twap_window_seconds must be in range (0, 600]", + )); + } + Ok(seconds) +} +fn default_true() -> bool { + true +} + +/// Get the latest TWAP by price feed id with a custom time window. +/// +/// Given a collection of price feed ids, retrieve the latest Pyth TWAP price for each price feed. +#[utoipa::path( + get, + path = "/v2/updates/twap/{window_seconds}/latest", + responses( + (status = 200, description = "TWAPs retrieved successfully", body = TwapsResponse), + (status = 404, description = "Price ids not found", body = String) + ), + params( + LatestTwapsPathParams, + LatestTwapsQueryParams + ) +)] +pub async fn latest_twaps( + State(state): State>, + Path(path_params): Path, + QsQuery(params): QsQuery, +) -> Result, RestError> +where + S: Aggregates, +{ + let price_id_inputs: Vec = + params.ids.into_iter().map(|id| id.into()).collect(); + let price_ids: Vec = + validate_price_ids(&state, &price_id_inputs, params.ignore_invalid_price_ids).await?; + + // Collect start and end bounds for the TWAP window + let window_seconds = path_params.window_seconds as i64; + let current_time = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs() as UnixTimestamp; + let start_time = current_time - window_seconds; + + // Calculate the average + let twaps_with_update_data = Aggregates::get_twaps_with_update_data( + &*state.state, + &price_ids, + RequestTime::FirstAfter(start_time), + RequestTime::Latest, + ) + .await + .map_err(|e| { + tracing::warn!( + "Error getting TWAPs for price IDs {:?} with update data: {:?}", + price_ids, + e + ); + RestError::UpdateDataNotFound + })?; + + let twap_update_data = twaps_with_update_data.update_data; + let binary: Vec = twap_update_data + .into_iter() + .map(|data_vec| { + let encoded_data = data_vec + .into_iter() + .map(|data| match params.encoding { + EncodingType::Base64 => base64_standard_engine.encode(data), + EncodingType::Hex => hex::encode(data), + }) + .collect(); + BinaryUpdate { + encoding: params.encoding, + data: encoded_data, + } + }) + .collect(); + + let parsed: Option> = if params.parsed { + Some( + twaps_with_update_data + .twaps + .into_iter() + .map(Into::into) + .collect(), + ) + } else { + None + }; + + let twap_resp = TwapsResponse { binary, parsed }; + Ok(Json(twap_resp)) +} diff --git a/apps/hermes/server/src/api/rest/v2/mod.rs b/apps/hermes/server/src/api/rest/v2/mod.rs index 4777e0ebbc..3ec0ee1f20 100644 --- a/apps/hermes/server/src/api/rest/v2/mod.rs +++ b/apps/hermes/server/src/api/rest/v2/mod.rs @@ -1,5 +1,6 @@ pub mod latest_price_updates; pub mod latest_publisher_stake_caps; +pub mod latest_twaps; pub mod price_feeds_metadata; pub mod sse; pub mod timestamp_price_updates; diff --git a/apps/hermes/server/src/api/types.rs b/apps/hermes/server/src/api/types.rs index 9b1c134e7b..cdb6124351 100644 --- a/apps/hermes/server/src/api/types.rs +++ b/apps/hermes/server/src/api/types.rs @@ -1,11 +1,14 @@ use { super::doc_examples, - crate::state::aggregate::{PriceFeedUpdate, PriceFeedsWithUpdateData, Slot, UnixTimestamp}, + crate::state::aggregate::{ + PriceFeedTwap, PriceFeedUpdate, PriceFeedsWithUpdateData, Slot, UnixTimestamp, + }, anyhow::Result, base64::{engine::general_purpose::STANDARD as base64_standard_engine, Engine as _}, borsh::{BorshDeserialize, BorshSerialize}, derive_more::{Deref, DerefMut}, pyth_sdk::{Price, PriceFeed, PriceIdentifier}, + rust_decimal::Decimal, serde::{Deserialize, Serialize}, std::{ collections::BTreeMap, @@ -140,7 +143,7 @@ pub struct RpcPrice { pub conf: u64, /// The exponent associated with both the price and confidence interval. Multiply those values /// by `10^expo` to get the real value. - #[schema(example=-8)] + #[schema(example = -8)] pub expo: i32, /// When the price was published. The `publish_time` is a unix timestamp, i.e., the number of /// seconds since the Unix epoch (00:00:00 UTC on 1 Jan 1970). @@ -244,6 +247,48 @@ impl From for ParsedPriceUpdate { } } } +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +pub struct ParsedPriceFeedTwap { + pub id: RpcPriceIdentifier, + /// The start unix timestamp of the window + pub start_timestamp: i64, + /// The end unix timestamp of the window + pub end_timestamp: i64, + /// The calculated time weighted average price over the window + pub twap: RpcPrice, + /// The % of slots where the network was down over the TWAP window. + /// A value of zero indicates no slots were missed over the window, and + /// a value of one indicates that every slot was missed over the window. + /// This is a float value stored as a string to avoid precision loss. + pub down_slots_ratio: Decimal, +} +impl From for ParsedPriceFeedTwap { + fn from(pft: PriceFeedTwap) -> Self { + Self { + id: RpcPriceIdentifier::from(pft.id), + start_timestamp: pft.start_timestamp, + end_timestamp: pft.end_timestamp, + twap: RpcPrice { + price: pft.twap.price, + conf: pft.twap.conf, + expo: pft.twap.expo, + publish_time: pft.twap.publish_time, + }, + down_slots_ratio: pft.down_slots_ratio, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +pub struct TwapsResponse { + /// Each BinaryUpdate contains the start & end cumulative price updates used to + /// calculate a given price feed's TWAP. + pub binary: Vec, + + /// The calculated TWAPs for each price ID + #[serde(skip_serializing_if = "Option::is_none")] + pub parsed: Option>, +} #[derive(Debug, PartialEq, serde::Serialize, serde::Deserialize, Clone, ToSchema)] pub struct ParsedPublisherStakeCapsUpdate { diff --git a/apps/hermes/server/src/state/aggregate.rs b/apps/hermes/server/src/state/aggregate.rs index 0232628a73..8845331755 100644 --- a/apps/hermes/server/src/state/aggregate.rs +++ b/apps/hermes/server/src/state/aggregate.rs @@ -1,7 +1,10 @@ #[cfg(test)] use mock_instant::{SystemTime, UNIX_EPOCH}; +use pythnet_sdk::messages::TwapMessage; + #[cfg(not(test))] use std::time::{SystemTime, UNIX_EPOCH}; + use { self::wormhole_merkle::{ construct_message_states_proofs, construct_update_data, @@ -29,6 +32,7 @@ use { v1::{WormholeMessage, WormholePayload}, }, }, + rust_decimal::Decimal, serde::Serialize, solana_sdk::pubkey::Pubkey, std::{collections::HashSet, time::Duration}, @@ -169,6 +173,15 @@ pub enum Update { AccumulatorMessages(AccumulatorMessages), } +#[derive(Debug, PartialEq)] +pub struct PriceFeedTwap { + pub id: PriceIdentifier, + pub start_timestamp: UnixTimestamp, + pub end_timestamp: UnixTimestamp, + pub twap: Price, + pub down_slots_ratio: Decimal, +} + #[derive(Debug, PartialEq)] pub struct PriceFeedUpdate { pub price_feed: PriceFeed, @@ -190,6 +203,12 @@ pub struct PublisherStakeCapsWithUpdateData { pub update_data: Vec>, } +#[derive(Debug)] +pub struct TwapsWithUpdateData { + pub twaps: Vec, + pub update_data: Vec>>, +} + #[derive(Debug, Serialize)] pub struct ReadinessMetadata { pub has_completed_recently: bool, @@ -220,6 +239,12 @@ where async fn get_latest_publisher_stake_caps_with_update_data( &self, ) -> Result; + async fn get_twaps_with_update_data( + &self, + price_ids: &[PriceIdentifier], + start_time: RequestTime, + end_time: RequestTime, + ) -> Result; } /// Allow downcasting State into CacheState for functions that depend on the `Cache` service. @@ -364,6 +389,29 @@ where Ok(()) } + async fn get_twaps_with_update_data( + &self, + price_ids: &[PriceIdentifier], + start_time: RequestTime, + end_time: RequestTime, + ) -> Result { + match get_verified_twaps_with_update_data( + self, + price_ids, + start_time.clone(), + end_time.clone(), + ) + .await + { + Ok(twaps_with_update_data) => Ok(twaps_with_update_data), + Err(e) => { + // TODO: Hit benchmarks if data not found in the cache + tracing::debug!("Update data not found in cache, falling back to Benchmarks"); + Err(e) + } + } + } + async fn get_price_feeds_with_update_data( &self, price_ids: &[PriceIdentifier], @@ -568,6 +616,149 @@ where }) } +async fn get_verified_twaps_with_update_data( + state: &S, + price_ids: &[PriceIdentifier], + start_time: RequestTime, + end_time: RequestTime, +) -> Result +where + S: Cache, +{ + // Get all start messages for all price IDs + let start_messages = state + .fetch_message_states( + price_ids.iter().map(|id| id.to_bytes()).collect(), + start_time.clone(), + MessageStateFilter::Only(MessageType::TwapMessage), + ) + .await?; + + // Get all end messages for all price IDs + let end_messages = state + .fetch_message_states( + price_ids.iter().map(|id| id.to_bytes()).collect(), + end_time.clone(), + MessageStateFilter::Only(MessageType::TwapMessage), + ) + .await?; + + // Verify we have matching start and end messages. + // The cache should throw an error earlier, but checking just in case. + if start_messages.len() != end_messages.len() { + return Err(anyhow!( + "Update data not found for the specified timestamps" + )); + } + + let mut twaps = Vec::new(); + let mut update_data = Vec::new(); + + // Iterate through start and end messages together + for (start_message, end_message) in start_messages.iter().zip(end_messages.iter()) { + if let (Message::TwapMessage(start_twap), Message::TwapMessage(end_twap)) = + (&start_message.message, &end_message.message) + { + match calculate_twap(start_twap, end_twap) { + Ok(twap_price) => { + // down_slots_ratio describes the % of slots where the network was down + // over the TWAP window. A value closer to zero indicates higher confidence. + let total_slots = end_twap.publish_slot - start_twap.publish_slot; + let total_down_slots = end_twap.num_down_slots - start_twap.num_down_slots; + let down_slots_ratio = + Decimal::from(total_down_slots) / Decimal::from(total_slots); + + // Add to calculated TWAPs + twaps.push(PriceFeedTwap { + id: PriceIdentifier::new(start_twap.feed_id), + twap: twap_price, + start_timestamp: start_twap.publish_time, + end_timestamp: end_twap.publish_time, + down_slots_ratio, + }); + + // Combine messages for update data + let mut messages = Vec::new(); + messages.push(start_message.clone().into()); + messages.push(end_message.clone().into()); + + if let Ok(update) = construct_update_data(messages) { + update_data.push(update); + } else { + tracing::warn!( + "Failed to construct update data for price feed {:?}", + start_twap.feed_id + ); + continue; + } + } + Err(e) => { + tracing::warn!( + "Failed to calculate TWAP for price feed {:?}: {}", + start_twap.feed_id, + e + ); + continue; + } + } + } + } + + Ok(TwapsWithUpdateData { twaps, update_data }) +} + +fn calculate_twap(start_message: &TwapMessage, end_message: &TwapMessage) -> Result { + if end_message.publish_slot <= start_message.publish_slot { + return Err(anyhow!( + "Cannot calculate TWAP - end slot must be greater than start slot" + )); + } + + // Validate that messages are the first ones in their timestamp + // This is necessary to ensure that this TWAP is deterministic, + // Since there can be multiple messages in a single second. + if start_message.prev_publish_time >= start_message.publish_time { + return Err(anyhow!( + "Start message is not the first update for its timestamp" + )); + } + + if end_message.prev_publish_time >= end_message.publish_time { + return Err(anyhow!( + "End message is not the first update for its timestamp" + )); + } + + let slot_diff = end_message + .publish_slot + .checked_sub(start_message.publish_slot) + .ok_or_else(|| anyhow!("Slot difference overflow"))?; + + let price_diff = end_message + .cumulative_price + .checked_sub(start_message.cumulative_price) + .ok_or_else(|| anyhow!("Price difference overflow"))?; + + let conf_diff = end_message + .cumulative_conf + .checked_sub(start_message.cumulative_conf) + .ok_or_else(|| anyhow!("Confidence difference overflow"))?; + + // Perform division before casting to maintain precision + // Cast slot_diff to the same type as price / conf diff before division + let price = i64::try_from(price_diff / i128::from(slot_diff)) + .map_err(|e| anyhow!("Price overflow after division: {}", e))?; + let conf = u64::try_from(conf_diff / u128::from(slot_diff)) + .map_err(|e| anyhow!("Confidence overflow after division: {}", e))?; + + Ok(Price { + price, + conf, + expo: end_message.exponent, + publish_time: end_message.publish_time, + }) +} + #[cfg(test)] mod test { use { @@ -588,6 +779,7 @@ mod test { wire::v1::{AccumulatorUpdateData, Proof, WormholeMerkleRoot}, }, rand::seq::SliceRandom, + rust_decimal::prelude::FromPrimitive, serde_wormhole::RawMessage, std::sync::Arc, wormhole_sdk::{Address, Chain}, @@ -644,7 +836,6 @@ mod test { updates } - /// Create a dummy price feed base on the given seed for all the fields except /// `publish_time` and `prev_publish_time`. Those are set to the given value. pub fn create_dummy_price_feed_message( @@ -1007,4 +1198,263 @@ mod test { .is_err()); } } + + /// Helper function to create a TWAP message with basic defaults + pub(crate) fn create_basic_twap_message( + feed_id: [u8; 32], + cumulative_price: i128, + num_down_slots: u64, + publish_time: i64, + prev_publish_time: i64, + publish_slot: u64, + ) -> Message { + Message::TwapMessage(TwapMessage { + feed_id, + cumulative_price, + cumulative_conf: 100, + num_down_slots, + exponent: 8, + publish_time, + prev_publish_time, + publish_slot, + }) + } + + #[tokio::test] + async fn test_get_verified_twaps_with_update_data_returns_correct_prices() { + let (state, _update_rx) = setup_state(10).await; + let feed_id_1 = [1u8; 32]; + let feed_id_2 = [2u8; 32]; + + // Store start TWAP messages for both feeds + store_multiple_concurrent_valid_updates( + state.clone(), + generate_update( + vec![ + create_basic_twap_message( + feed_id_1, 100, // cumulative_price + 0, // num_down_slots + 100, // publish_time + 90, // prev_publish_time + 1000, // publish_slot + ), + create_basic_twap_message( + feed_id_2, 500, // cumulative_price + 10, // num_down_slots + 100, // publish_time + 90, // prev_publish_time + 1000, // publish_slot + ), + ], + 1000, + 20, + ), + ) + .await; + + // Store end TWAP messages for both feeds + store_multiple_concurrent_valid_updates( + state.clone(), + generate_update( + vec![ + create_basic_twap_message( + feed_id_1, 300, // cumulative_price + 50, // num_down_slots + 200, // publish_time + 180, // prev_publish_time + 1100, // publish_slot + ), + create_basic_twap_message( + feed_id_2, 900, // cumulative_price + 30, // num_down_slots + 200, // publish_time + 180, // prev_publish_time + 1100, // publish_slot + ), + ], + 1100, + 21, + ), + ) + .await; + + // Get TWAPs over timestamp window 100 -> 200 for both feeds + let result = get_verified_twaps_with_update_data( + &*state, + &[ + PriceIdentifier::new(feed_id_1), + PriceIdentifier::new(feed_id_2), + ], + RequestTime::FirstAfter(100), // Start time + RequestTime::FirstAfter(200), // End time + ) + .await + .unwrap(); + + // Verify calculations are accurate for both feeds + assert_eq!(result.twaps.len(), 2); + + // Verify feed 1 + let twap_1 = result + .twaps + .iter() + .find(|t| t.id == PriceIdentifier::new(feed_id_1)) + .unwrap(); + assert_eq!(twap_1.twap.price, 2); // (300-100)/(1100-1000) = 2 + assert_eq!(twap_1.down_slots_ratio, Decimal::from_f64(0.5).unwrap()); // (50-0)/(1100-1000) = 0.5 + assert_eq!(twap_1.start_timestamp, 100); + assert_eq!(twap_1.end_timestamp, 200); + + // Verify feed 2 + let twap_2 = result + .twaps + .iter() + .find(|t| t.id == PriceIdentifier::new(feed_id_2)) + .unwrap(); + assert_eq!(twap_2.twap.price, 4); // (900-500)/(1100-1000) = 4 + assert_eq!(twap_2.down_slots_ratio, Decimal::from_f64(0.2).unwrap()); // (30-10)/(1100-1000) = 0.2 + assert_eq!(twap_2.start_timestamp, 100); + assert_eq!(twap_2.end_timestamp, 200); + + // Verify update data contains both start and end messages for both feeds + assert_eq!(result.update_data.len(), 2); + assert_eq!(result.update_data[0].len(), 2); // Should contain 2 messages + assert_eq!(result.update_data[1].len(), 2); // Should contain 2 messages + } + #[tokio::test] + + async fn test_get_verified_twaps_with_missing_messages_throws_error() { + let (state, _update_rx) = setup_state(10).await; + let feed_id_1 = [1u8; 32]; + let feed_id_2 = [2u8; 32]; + + // Store both messages for feed_1 + store_multiple_concurrent_valid_updates( + state.clone(), + generate_update( + vec![ + create_basic_twap_message( + feed_id_1, 100, // cumulative_price + 0, // num_down_slots + 100, // publish_time + 90, // prev_publish_time + 1000, // publish_slot + ), + create_basic_twap_message( + feed_id_2, 500, // cumulative_price + 0, // num_down_slots + 100, // publish_time + 90, // prev_publish_time + 1000, // publish_slot + ), + ], + 1000, + 20, + ), + ) + .await; + + // Store end message only for feed_1 (feed_2 missing end message) + store_multiple_concurrent_valid_updates( + state.clone(), + generate_update( + vec![create_basic_twap_message( + feed_id_1, 300, // cumulative_price + 0, // num_down_slots + 200, // publish_time + 180, // prev_publish_time + 1100, // publish_slot + )], + 1100, + 21, + ), + ) + .await; + + let result = get_verified_twaps_with_update_data( + &*state, + &[ + PriceIdentifier::new(feed_id_1), + PriceIdentifier::new(feed_id_2), + ], + RequestTime::FirstAfter(100), + RequestTime::FirstAfter(200), + ) + .await; + + assert_eq!(result.unwrap_err().to_string(), "Message not found"); + } +} +#[cfg(test)] +/// Unit tests for the core TWAP calculation logic in `calculate_twap` +mod calculate_twap_unit_tests { + use super::*; + + fn create_basic_twap_message( + cumulative_price: i128, + publish_time: i64, + prev_publish_time: i64, + publish_slot: u64, + ) -> TwapMessage { + TwapMessage { + feed_id: [0; 32], + cumulative_price, + cumulative_conf: 100, + num_down_slots: 0, + exponent: 8, + publish_time, + prev_publish_time, + publish_slot, + } + } + + #[test] + fn test_valid_twap() { + let start = create_basic_twap_message(100, 100, 90, 1000); + let end = create_basic_twap_message(300, 200, 180, 1100); + + let price = calculate_twap(&start, &end).unwrap(); + assert_eq!(price.price, 2); // (300-100)/(1100-1000) = 2 + } + #[test] + fn test_invalid_slot_order() { + let start = create_basic_twap_message(100, 100, 90, 1100); + let end = create_basic_twap_message(300, 200, 180, 1000); + + let err = calculate_twap(&start, &end).unwrap_err(); + assert_eq!( + err.to_string(), + "Cannot calculate TWAP - end slot must be greater than start slot" + ); + } + + #[test] + fn test_invalid_timestamps() { + let start = create_basic_twap_message(100, 100, 110, 1000); + let end = create_basic_twap_message(300, 200, 180, 1100); + + let err = calculate_twap(&start, &end).unwrap_err(); + assert_eq!( + err.to_string(), + "Start message is not the first update for its timestamp" + ); + + let start = create_basic_twap_message(100, 100, 90, 1000); + let end = create_basic_twap_message(300, 200, 200, 1100); + + let err = calculate_twap(&start, &end).unwrap_err(); + assert_eq!( + err.to_string(), + "End message is not the first update for its timestamp" + ); + } + + #[test] + fn test_overflow() { + let start = create_basic_twap_message(i128::MIN, 100, 90, 1000); + let end = create_basic_twap_message(i128::MAX, 200, 180, 1100); + + let err = calculate_twap(&start, &end).unwrap_err(); + assert_eq!(err.to_string(), "Price difference overflow"); + } }