Эх сурвалжийг харах

feat(hermes): create latest TWAP endpoint (#2131)

* feat(hermes): init latest twap endpoint

* feat(hermes): update twaps rpc types

* feat(hermes): update data model

* refactor(hermes): seperate out domain & rpc models

* feat(hermes): update twap data model

* chore(hermes): remove commented code

* feat(hermes): use Decimal type for down_slots_ratio

* feat(hermes): add validation for first message in a timestamp, remove unnecesary struct

* feat(hermes): address pr comments

* test(hermes): add tests for twap
Tejas Badadare 1 жил өмнө
parent
commit
d6ba2b6ba9

+ 284 - 98
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]]

+ 3 - 2
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" }
 

+ 12 - 0
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),

+ 11 - 3
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<PublisherStakeCapsWithUpdateData> {
             unimplemented!("Not needed for this test")
         }
+        async fn get_twaps_with_update_data(
+            &self,
+            _price_ids: &[PriceIdentifier],
+            _start_time: RequestTime,
+            _end_time: RequestTime,
+        ) -> Result<TwapsWithUpdateData> {
+            unimplemented!("Not needed for this test")
+        }
     }
 
     #[tokio::test]

+ 3 - 0
apps/hermes/server/src/api/rest/index.rs

@@ -13,9 +13,12 @@ pub async fn index() -> impl IntoResponse {
         "/api/get_price_feed?id=<price_feed_id>&publish_time=<publish_time_in_unix_timestamp>(&verbose=true)(&binary=true)",
         "/api/get_vaa?id=<price_feed_id>&publish_time=<publish_time_in_unix_timestamp>",
         "/api/get_vaa_ccip?data=<0x<price_feed_id_32_bytes>+<publish_time_unix_timestamp_be_8_bytes>>",
+
         "/v2/updates/price/latest?ids[]=<price_feed_id>&ids[]=<price_feed_id_2>&..(&encoding=hex|base64)(&parsed=false)",
         "/v2/updates/price/stream?ids[]=<price_feed_id>&ids[]=<price_feed_id_2>&..(&encoding=hex|base64)(&parsed=false)(&allow_unordered=false)(&benchmarks_only=false)",
         "/v2/updates/price/<timestamp>?ids[]=<price_feed_id>&ids[]=<price_feed_id_2>&..(&encoding=hex|base64)(&parsed=false)",
         "/v2/price_feeds?(query=btc)(&asset_type=crypto|equity|fx|metal|rates)",
+        "/v2/updates/twap/<window_seconds>/latest?ids[]=<price_feed_id>&ids[]=<price_feed_id_2>&..(&encoding=hex|base64)(&parsed=false)",
+        "/v2/updates/twap/<window_seconds>/<timestamp>?ids[]=<price_feed_id>&ids[]=<price_feed_id_2>&..(&encoding=hex|base64)(&parsed=false)",
     ])
 }

+ 165 - 0
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<PriceIdInput>,
+
+    /// 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<DurationInSeconds, D::Error>
+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<S>(
+    State(state): State<ApiState<S>>,
+    Path(path_params): Path<LatestTwapsPathParams>,
+    QsQuery(params): QsQuery<LatestTwapsQueryParams>,
+) -> Result<Json<TwapsResponse>, RestError>
+where
+    S: Aggregates,
+{
+    let price_id_inputs: Vec<PriceIdentifier> =
+        params.ids.into_iter().map(|id| id.into()).collect();
+    let price_ids: Vec<PriceIdentifier> =
+        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<BinaryUpdate> = 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<Vec<ParsedPriceFeedTwap>> = 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))
+}

+ 1 - 0
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;

+ 47 - 2
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<PriceFeedUpdate> 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<PriceFeedTwap> 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<BinaryUpdate>,
+
+    /// The calculated TWAPs for each price ID
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub parsed: Option<Vec<ParsedPriceFeedTwap>>,
+}
 
 #[derive(Debug, PartialEq, serde::Serialize, serde::Deserialize, Clone, ToSchema)]
 pub struct ParsedPublisherStakeCapsUpdate {

+ 451 - 1
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<Vec<u8>>,
 }
 
+#[derive(Debug)]
+pub struct TwapsWithUpdateData {
+    pub twaps: Vec<PriceFeedTwap>,
+    pub update_data: Vec<Vec<Vec<u8>>>,
+}
+
 #[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<PublisherStakeCapsWithUpdateData>;
+    async fn get_twaps_with_update_data(
+        &self,
+        price_ids: &[PriceIdentifier],
+        start_time: RequestTime,
+        end_time: RequestTime,
+    ) -> Result<TwapsWithUpdateData>;
 }
 
 /// 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<TwapsWithUpdateData> {
+        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<S>(
+    state: &S,
+    price_ids: &[PriceIdentifier],
+    start_time: RequestTime,
+    end_time: RequestTime,
+) -> Result<TwapsWithUpdateData>
+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<Price> {
+    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");
+    }
 }