templar_common/oracle/redstone/
adapter.rs1use 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 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 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 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 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(ð, 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(ð, 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}