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