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};
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 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 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 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 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(ð, 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(ð, 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}