templar_common/oracle/redstone/
adapter.rs

1use near_sdk::{near, store::IterableMap, BorshStorageKey, IntoStorageKey};
2use primitive_types::U256;
3use redstone::{
4    contract::verification,
5    core::{process_payload, processor_result::ValidatedPayload},
6    network::{error::Error as RedStoneError, StdEnv},
7    ConfigFactory, FeedValue,
8};
9use templar_primitives::time::Nanoseconds;
10
11use super::{
12    config::{Config, DATA_STALENESS},
13    feed_data::FeedData,
14    FeedId, GetPrices,
15};
16
17#[derive(BorshStorageKey)]
18#[near(serializers = [json, borsh])]
19pub enum Role {
20    ModifyRoles,
21    TrustedUpdater,
22}
23
24#[derive(Debug)]
25#[near(serializers = [borsh])]
26pub struct RedStoneAdapter {
27    pub config: Config,
28    feeds: IterableMap<FeedId, FeedData>,
29}
30
31impl RedStoneAdapter {
32    pub fn new(prefix: impl IntoStorageKey, config: Config) -> Self {
33        Self {
34            config,
35            feeds: IterableMap::new(prefix),
36        }
37    }
38
39    /// Retrieves fresh feed data from storage.
40    ///
41    /// # Errors
42    ///
43    /// - Feed does not exist.
44    /// - [`RedStoneError`] (e.g. data too stale)
45    pub fn feed_data<'a>(
46        &'a self,
47        feed_id: &FeedId,
48        timestamp: Nanoseconds,
49    ) -> Option<Result<&'a FeedData, RedStoneError>> {
50        let f = self.feeds.get(feed_id)?;
51
52        Some(
53            verification::verify_data_staleness(
54                f.write_timestamp.as_ms().into(),
55                timestamp.into(),
56                DATA_STALENESS,
57            )
58            .map(|()| f),
59        )
60    }
61
62    fn update_feed(
63        &mut self,
64        is_trusted: bool,
65        feed_id: &FeedId,
66        feed_data: FeedData,
67    ) -> Result<FeedData, RedStoneError> {
68        let now = feed_data.write_timestamp.as_ms().into();
69        let new_pkg = feed_data.package_timestamp.as_ms().into();
70
71        let old = self.feeds.get(feed_id);
72        let old_write = old.map(|d| d.write_timestamp.as_ms().into());
73        let old_pkg = old.map(|d| d.package_timestamp.as_ms().into());
74
75        if is_trusted {
76            verification::verify_trusted_update(now, old_write, old_pkg, new_pkg)?;
77        } else {
78            let interval = self.config.min_interval_between_updates_ms.into();
79            verification::verify_untrusted_update(now, old_write, interval, old_pkg, new_pkg)?;
80        }
81
82        self.feeds.insert(feed_id.clone(), feed_data.clone());
83
84        Ok(feed_data)
85    }
86
87    /// Validates a payload and extracts feed values.
88    ///
89    /// # Errors
90    ///
91    /// - [`RedStoneError`]
92    pub fn validate_payload(
93        &self,
94        feed_ids: &[FeedId],
95        payload: &[u8],
96        timestamp: Nanoseconds,
97    ) -> Result<ValidatedPayload, RedStoneError> {
98        let feed_ids = feed_ids
99            .iter()
100            .map(|id| id.as_bytes().to_vec().into())
101            .collect();
102
103        let mut config = self
104            .config
105            .redstone_config::<StdEnv>((), feed_ids, timestamp.into())?;
106        process_payload(&mut config, payload.to_vec())
107    }
108
109    /// Validates prices given a payload. For pull-style usage (payload is
110    /// attached to original transaction). Does not write to storage.
111    ///
112    /// # Errors
113    ///
114    /// - Feed ID is unknown or missing from payload.
115    /// - [`RedStoneError`]
116    pub fn get_prices(
117        &self,
118        feed_ids: &[FeedId],
119        payload: &[u8],
120        timestamp: Nanoseconds,
121    ) -> Result<GetPrices, RedStoneError> {
122        let ValidatedPayload { timestamp, values } =
123            self.validate_payload(feed_ids, payload, timestamp)?;
124
125        Ok(GetPrices {
126            timestamp: timestamp.into(),
127            prices: values
128                .into_iter()
129                .map(|f| {
130                    let value = U256::from_big_endian(f.value.as_be_bytes()).into();
131                    (f.feed.into(), value)
132                })
133                .collect(),
134        })
135    }
136
137    /// Write price data to storage.
138    ///
139    /// # Errors
140    ///
141    /// - [`RedStoneError`]
142    pub fn write_prices(
143        &mut self,
144        is_trusted: bool,
145        payload: ValidatedPayload,
146        timestamp: Nanoseconds,
147    ) -> Vec<(FeedId, Result<FeedData, RedStoneError>)> {
148        payload
149            .values
150            .into_iter()
151            .map(|FeedValue { feed, value }| {
152                let feed_id: super::FeedId = feed.into();
153                let feed_data = FeedData {
154                    price: U256::from_big_endian(value.as_be_bytes()).into(),
155                    package_timestamp: payload.timestamp.into(),
156                    write_timestamp: timestamp,
157                };
158                let update = self.update_feed(is_trusted, &feed_id, feed_data);
159
160                (feed_id, update)
161            })
162            .collect()
163    }
164}
165
166#[allow(clippy::cast_sign_loss)]
167#[cfg(test)]
168mod tests {
169    use hex_literal::hex;
170    use primitive_types::U256;
171
172    use super::*;
173
174    use crate::oracle::redstone::config;
175
176    #[rstest::rstest]
177    #[case::stellar(1_770_985_144_000, &hex!("45544800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002d9030a710019c56f0bec0000000200000015d1cb1a708c63264741b00ce097176e45f708914b8cfdca26b079877a70604e25aa0bcfa3a41df8212eddd51db3496b95c7c3dc4caa9ac9705602af0515db1b31c45544800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002d9028ed04019c56f0bec000000020000001dcaf484941c0d206f1898185b953c6a92d7fd188b347505c0f5beb2030e06e3e1b2f7dfb45929ac7676136af93fee7f14a614b40fa4dc2d1e625dbece02eaca21c45544800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002d9028ed04019c56f0bec00000002000000199bd54930138268baad2869e9ceb99b6bc67cd6b8a4cc98e05f0b1cd9b7f07066008208399a728fac3d1dc3ca407cb8199a0209377bceb0c48f2cc3d756078051b4254430000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006179a92ab8c019c56f0bec000000020000001f08af53ed34046f7f64cc02ffb7973252954d7c395e440693c896bffdbc2de1e31cf5675bf66583d3e3438f5002ae9c10870d4dc45de05c560b239aa3a2d50a41b425443000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000617a1187473019c56f0bec0000000200000011b96dc2763a692e3245ce4f1b0c16ea245c240204e99ebd323b340e58bfb14fb5f0465ce11b8dd52ff839547cc949d20e4e8ba0be43dd6417cade2a8ebfd8c9e1c425443000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000617a1187473019c56f0bec00000002000000114a02710892325b13afc74bbd350dd9ec80342b2d6c0c94df7b7a60dbf67a1b91b182fa4555e0e0db91e6258b279f00b7eeb8f5de9930e352d5321a6b8b64a031c00063137373039383531343539383223302e392e30237374656c6c61722d636f6e6e6563746f72000025000002ed57011e0000"))]
178    #[case::js_sdk(1_771_336_150_000, &hex!("42544300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000062f31d92220019c6bdcabf000000020000001bcf952b4490f2e1d3eb14363fafb17785628ff35574c473a425c1b91bc7d69b32f5076efc05eeb87bd731cdd4573991e18b2e51d32cb6d338d72ec63b495b1be1c42544300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000062f31d92220019c6bdcabf0000000200000012d94c6893f264b770f3af4e27f1053fef182f4fec1349e28ef0827b99a846409178e4d0629f89da7dacc2b923459d761629f7c2535f41419649eafa3f4b8f2901b42544300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000062f2fbbb4d0019c6bdcabf000000020000001f4399f7127decb4c5f5d51e3fbdc70ea877f790cff4a262c3b8856ece72365273dcfcfe62aafda679c928c7aba4bed48e36e0d8d06f3eef2dea8c69696863aa01c45544800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002e22882520019c6bdcabf0000000200000018df63ce1bea5fc8738cb02cb09141604906fcba269e9fb1f873bc6a9edb705f07f963841bc56c4bd7c8b9748d243bc0113acfb59f6c6973b91a61550c260736f1b45544800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002e22882520019c6bdcabf0000000200000014060619a0ce0de8d714d0b0e0c9f86be2e2a8d841a63261079fa8e5bddc951b672aa9eeebf0780ff442ef3dc65530b1af3817eb8bc87ddd5bf44851702cfdc671c45544800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002e22882520019c6bdcabf0000000200000011c86e4b37fa2cd9a229594abe55056d8c55b7c1662cdbb4fff15881cf6e0a6043929c9b3965f63715de745cbcfd7292547609a015d5129352c5c1d699262fb241c0006000000000002ed57011e0000"))]
179    #[should_panic = "called `Option::unwrap()` on a `None` value"]
180    #[case::stellar_feed_id(1_770_985_144_000, &hex!("A5544800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002d9030a710019c56f0bec0000000200000015d1cb1a708c63264741b00ce097176e45f708914b8cfdca26b079877a70604e25aa0bcfa3a41df8212eddd51db3496b95c7c3dc4caa9ac9705602af0515db1b31c45544800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002d9028ed04019c56f0bec000000020000001dcaf484941c0d206f1898185b953c6a92d7fd188b347505c0f5beb2030e06e3e1b2f7dfb45929ac7676136af93fee7f14a614b40fa4dc2d1e625dbece02eaca21c45544800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002d9028ed04019c56f0bec00000002000000199bd54930138268baad2869e9ceb99b6bc67cd6b8a4cc98e05f0b1cd9b7f07066008208399a728fac3d1dc3ca407cb8199a0209377bceb0c48f2cc3d756078051b4254430000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006179a92ab8c019c56f0bec000000020000001f08af53ed34046f7f64cc02ffb7973252954d7c395e440693c896bffdbc2de1e31cf5675bf66583d3e3438f5002ae9c10870d4dc45de05c560b239aa3a2d50a41b425443000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000617a1187473019c56f0bec0000000200000011b96dc2763a692e3245ce4f1b0c16ea245c240204e99ebd323b340e58bfb14fb5f0465ce11b8dd52ff839547cc949d20e4e8ba0be43dd6417cade2a8ebfd8c9e1c425443000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000617a1187473019c56f0bec00000002000000114a02710892325b13afc74bbd350dd9ec80342b2d6c0c94df7b7a60dbf67a1b91b182fa4555e0e0db91e6258b279f00b7eeb8f5de9930e352d5321a6b8b64a031c00063137373039383531343539383223302e392e30237374656c6c61722d636f6e6e6563746f72000025000002ed57011e0000"))]
181    #[should_panic = "TooOld"]
182    #[case::stellar_timestamp_old(2_770_985_144_000, &hex!("45544800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002d9030a710019c56f0bec0000000200000015d1cb1a708c63264741b00ce097176e45f708914b8cfdca26b079877a70604e25aa0bcfa3a41df8212eddd51db3496b95c7c3dc4caa9ac9705602af0515db1b31c45544800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002d9028ed04019c56f0bec000000020000001dcaf484941c0d206f1898185b953c6a92d7fd188b347505c0f5beb2030e06e3e1b2f7dfb45929ac7676136af93fee7f14a614b40fa4dc2d1e625dbece02eaca21c45544800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002d9028ed04019c56f0bec00000002000000199bd54930138268baad2869e9ceb99b6bc67cd6b8a4cc98e05f0b1cd9b7f07066008208399a728fac3d1dc3ca407cb8199a0209377bceb0c48f2cc3d756078051b4254430000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006179a92ab8c019c56f0bec000000020000001f08af53ed34046f7f64cc02ffb7973252954d7c395e440693c896bffdbc2de1e31cf5675bf66583d3e3438f5002ae9c10870d4dc45de05c560b239aa3a2d50a41b425443000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000617a1187473019c56f0bec0000000200000011b96dc2763a692e3245ce4f1b0c16ea245c240204e99ebd323b340e58bfb14fb5f0465ce11b8dd52ff839547cc949d20e4e8ba0be43dd6417cade2a8ebfd8c9e1c425443000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000617a1187473019c56f0bec00000002000000114a02710892325b13afc74bbd350dd9ec80342b2d6c0c94df7b7a60dbf67a1b91b182fa4555e0e0db91e6258b279f00b7eeb8f5de9930e352d5321a6b8b64a031c00063137373039383531343539383223302e392e30237374656c6c61722d636f6e6e6563746f72000025000002ed57011e0000"))]
183    #[should_panic = "TooFuture"]
184    #[case::stellar_timestamp_future(1_270_985_144_000, &hex!("45544800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002d9030a710019c56f0bec0000000200000015d1cb1a708c63264741b00ce097176e45f708914b8cfdca26b079877a70604e25aa0bcfa3a41df8212eddd51db3496b95c7c3dc4caa9ac9705602af0515db1b31c45544800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002d9028ed04019c56f0bec000000020000001dcaf484941c0d206f1898185b953c6a92d7fd188b347505c0f5beb2030e06e3e1b2f7dfb45929ac7676136af93fee7f14a614b40fa4dc2d1e625dbece02eaca21c45544800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002d9028ed04019c56f0bec00000002000000199bd54930138268baad2869e9ceb99b6bc67cd6b8a4cc98e05f0b1cd9b7f07066008208399a728fac3d1dc3ca407cb8199a0209377bceb0c48f2cc3d756078051b4254430000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006179a92ab8c019c56f0bec000000020000001f08af53ed34046f7f64cc02ffb7973252954d7c395e440693c896bffdbc2de1e31cf5675bf66583d3e3438f5002ae9c10870d4dc45de05c560b239aa3a2d50a41b425443000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000617a1187473019c56f0bec0000000200000011b96dc2763a692e3245ce4f1b0c16ea245c240204e99ebd323b340e58bfb14fb5f0465ce11b8dd52ff839547cc949d20e4e8ba0be43dd6417cade2a8ebfd8c9e1c425443000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000617a1187473019c56f0bec00000002000000114a02710892325b13afc74bbd350dd9ec80342b2d6c0c94df7b7a60dbf67a1b91b182fa4555e0e0db91e6258b279f00b7eeb8f5de9930e352d5321a6b8b64a031c00063137373039383531343539383223302e392e30237374656c6c61722d636f6e6e6563746f72000025000002ed57011e0000"))]
185    #[should_panic = "called `Option::unwrap()` on a `None` value"]
186    #[case::js_sdk_feed_id(1_771_336_150_000, &hex!("A2544300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000062f31d92220019c6bdcabf000000020000001bcf952b4490f2e1d3eb14363fafb17785628ff35574c473a425c1b91bc7d69b32f5076efc05eeb87bd731cdd4573991e18b2e51d32cb6d338d72ec63b495b1be1c42544300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000062f31d92220019c6bdcabf0000000200000012d94c6893f264b770f3af4e27f1053fef182f4fec1349e28ef0827b99a846409178e4d0629f89da7dacc2b923459d761629f7c2535f41419649eafa3f4b8f2901b42544300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000062f2fbbb4d0019c6bdcabf000000020000001f4399f7127decb4c5f5d51e3fbdc70ea877f790cff4a262c3b8856ece72365273dcfcfe62aafda679c928c7aba4bed48e36e0d8d06f3eef2dea8c69696863aa01c45544800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002e22882520019c6bdcabf0000000200000018df63ce1bea5fc8738cb02cb09141604906fcba269e9fb1f873bc6a9edb705f07f963841bc56c4bd7c8b9748d243bc0113acfb59f6c6973b91a61550c260736f1b45544800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002e22882520019c6bdcabf0000000200000014060619a0ce0de8d714d0b0e0c9f86be2e2a8d841a63261079fa8e5bddc951b672aa9eeebf0780ff442ef3dc65530b1af3817eb8bc87ddd5bf44851702cfdc671c45544800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002e22882520019c6bdcabf0000000200000011c86e4b37fa2cd9a229594abe55056d8c55b7c1662cdbb4fff15881cf6e0a6043929c9b3965f63715de745cbcfd7292547609a015d5129352c5c1d699262fb241c0006000000000002ed57011e0000"))]
187    fn payload(#[case] timestamp: u64, #[case] input: &[u8]) {
188        let timestamp = Nanoseconds::from_ms(timestamp);
189        let mut ra = RedStoneAdapter::new(b"a", config::prod());
190
191        let eth = FeedId::from("ETH");
192        let btc = FeedId::from("BTC");
193
194        let prices = vec![eth.clone(), btc.clone()];
195
196        let p = ra.validate_payload(&prices, input, timestamp).unwrap();
197        ra.write_prices(true, p, timestamp)
198            .into_iter()
199            .for_each(|(_, r)| {
200                r.unwrap();
201            });
202
203        let _eth_data = ra.feed_data(&eth, timestamp).unwrap();
204        let _btc_data = ra.feed_data(&btc, timestamp).unwrap();
205    }
206
207    #[rstest::rstest]
208    fn output() {
209        let timestamp = Nanoseconds::from_ms(1_770_985_144_000_u64);
210        let input = hex!("45544800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002d9030a710019c56f0bec0000000200000015d1cb1a708c63264741b00ce097176e45f708914b8cfdca26b079877a70604e25aa0bcfa3a41df8212eddd51db3496b95c7c3dc4caa9ac9705602af0515db1b31c45544800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002d9028ed04019c56f0bec000000020000001dcaf484941c0d206f1898185b953c6a92d7fd188b347505c0f5beb2030e06e3e1b2f7dfb45929ac7676136af93fee7f14a614b40fa4dc2d1e625dbece02eaca21c45544800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002d9028ed04019c56f0bec00000002000000199bd54930138268baad2869e9ceb99b6bc67cd6b8a4cc98e05f0b1cd9b7f07066008208399a728fac3d1dc3ca407cb8199a0209377bceb0c48f2cc3d756078051b4254430000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006179a92ab8c019c56f0bec000000020000001f08af53ed34046f7f64cc02ffb7973252954d7c395e440693c896bffdbc2de1e31cf5675bf66583d3e3438f5002ae9c10870d4dc45de05c560b239aa3a2d50a41b425443000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000617a1187473019c56f0bec0000000200000011b96dc2763a692e3245ce4f1b0c16ea245c240204e99ebd323b340e58bfb14fb5f0465ce11b8dd52ff839547cc949d20e4e8ba0be43dd6417cade2a8ebfd8c9e1c425443000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000617a1187473019c56f0bec00000002000000114a02710892325b13afc74bbd350dd9ec80342b2d6c0c94df7b7a60dbf67a1b91b182fa4555e0e0db91e6258b279f00b7eeb8f5de9930e352d5321a6b8b64a031c00063137373039383531343539383223302e392e30237374656c6c61722d636f6e6e6563746f72000025000002ed57011e0000");
211
212        let mut ra = RedStoneAdapter::new(b"a", config::prod());
213
214        let eth = FeedId::from("ETH");
215        let btc = FeedId::from("BTC");
216
217        let prices = vec![eth.clone(), btc.clone()];
218
219        let p = ra.validate_payload(&prices, &input, timestamp).unwrap();
220        let written = ra
221            .write_prices(true, p, timestamp)
222            .into_iter()
223            .map(|(_, r)| r.unwrap())
224            .collect::<Vec<_>>();
225        assert_eq!(written.len(), 2);
226
227        let eth_data = ra.feed_data(&eth, timestamp).unwrap().unwrap();
228        let btc_data = ra.feed_data(&btc, timestamp).unwrap().unwrap();
229
230        assert_eq!(
231            eth_data,
232            &FeedData {
233                price: U256::from(195_692_129_540_u128).into(),
234                package_timestamp: Nanoseconds::from_ms(1_770_985_144_000),
235                write_timestamp: Nanoseconds::from_ms(1_770_985_144_000),
236            },
237        );
238
239        assert_eq!(
240            btc_data,
241            &FeedData {
242                price: U256::from(6_698_556_748_915_u128).into(),
243                package_timestamp: Nanoseconds::from_ms(1_770_985_144_000),
244                write_timestamp: Nanoseconds::from_ms(1_770_985_144_000),
245            },
246        );
247    }
248}