|
| 1 | +module pyth::batch_price_attestation { |
| 2 | + use pyth::price_feed::{Self}; |
| 3 | + use pyth::price; |
| 4 | + use pyth::error; |
| 5 | + use pyth::i64; |
| 6 | + use pyth::price_info::{Self, PriceInfo}; |
| 7 | + use pyth::price_identifier::{Self}; |
| 8 | + use pyth::price_status; |
| 9 | + use pyth::deserialize::{Self}; |
| 10 | + use aptos_framework::account; |
| 11 | + use aptos_framework::timestamp; |
| 12 | + use wormhole::cursor::{Self, Cursor}; |
| 13 | + use std::vector::{Self}; |
| 14 | + |
| 15 | + const MAGIC: u64 = 0x50325748; // "P2WH" (Pyth2Wormhole) raw ASCII bytes |
| 16 | + |
| 17 | + struct BatchPriceAttestation { |
| 18 | + header: Header, |
| 19 | + attestation_size: u64, |
| 20 | + attestation_count: u64, |
| 21 | + price_infos: vector<PriceInfo>, |
| 22 | + } |
| 23 | + |
| 24 | + struct Header { |
| 25 | + magic: u64, |
| 26 | + version_major: u64, |
| 27 | + version_minor: u64, |
| 28 | + header_size: u64, |
| 29 | + payload_id: u8, |
| 30 | + } |
| 31 | + |
| 32 | + fun deserialize_header(cur: &mut Cursor<u8>): Header { |
| 33 | + let magic = deserialize::deserialize_u32(cur); |
| 34 | + assert!(magic == MAGIC, error::invalid_attestation_magic_value()); |
| 35 | + let version_major = deserialize::deserialize_u16(cur); |
| 36 | + let version_minor = deserialize::deserialize_u16(cur); |
| 37 | + let header_size = deserialize::deserialize_u16(cur); |
| 38 | + let payload_id = deserialize::deserialize_u8(cur); |
| 39 | + |
| 40 | + assert!(header_size >= 1, error::invalid_batch_attestation_header_size()); |
| 41 | + let unknown_header_bytes = header_size - 1; |
| 42 | + let _unknown = deserialize::deserialize_vector(cur, unknown_header_bytes); |
| 43 | + |
| 44 | + Header { |
| 45 | + magic: magic, |
| 46 | + header_size: header_size, |
| 47 | + version_minor: version_minor, |
| 48 | + version_major: version_major, |
| 49 | + payload_id: payload_id, |
| 50 | + } |
| 51 | + } |
| 52 | + |
| 53 | + public fun destroy(batch: BatchPriceAttestation): vector<PriceInfo> { |
| 54 | + let BatchPriceAttestation { |
| 55 | + header: Header { |
| 56 | + magic: _, |
| 57 | + version_major: _, |
| 58 | + version_minor: _, |
| 59 | + header_size: _, |
| 60 | + payload_id: _, |
| 61 | + }, |
| 62 | + attestation_size: _, |
| 63 | + attestation_count: _, |
| 64 | + price_infos, |
| 65 | + } = batch; |
| 66 | + price_infos |
| 67 | + } |
| 68 | + |
| 69 | + public fun get_attestation_count(batch: &BatchPriceAttestation): u64 { |
| 70 | + batch.attestation_count |
| 71 | + } |
| 72 | + |
| 73 | + public fun get_price_info(batch: &BatchPriceAttestation, index: u64): &PriceInfo { |
| 74 | + vector::borrow(&batch.price_infos, index) |
| 75 | + } |
| 76 | + |
| 77 | + public fun deserialize(bytes: vector<u8>): BatchPriceAttestation { |
| 78 | + let cur = cursor::init(bytes); |
| 79 | + let header = deserialize_header(&mut cur); |
| 80 | + |
| 81 | + let attestation_count = deserialize::deserialize_u16(&mut cur); |
| 82 | + let attestation_size = deserialize::deserialize_u16(&mut cur); |
| 83 | + let price_infos = vector::empty(); |
| 84 | + |
| 85 | + let i = 0; |
| 86 | + while (i < attestation_count) { |
| 87 | + let price_info = deserialize_price_info(&mut cur); |
| 88 | + vector::push_back(&mut price_infos, price_info); |
| 89 | + |
| 90 | + // Consume any excess bytes |
| 91 | + let parsed_bytes = 32+32+8+8+4+8+8+1+4+4+8+8+8+8+8; |
| 92 | + let _excess = deserialize::deserialize_vector(&mut cur, attestation_size - parsed_bytes); |
| 93 | + |
| 94 | + i = i + 1; |
| 95 | + }; |
| 96 | + cursor::destroy_empty(cur); |
| 97 | + |
| 98 | + BatchPriceAttestation { |
| 99 | + header, |
| 100 | + attestation_count: attestation_count, |
| 101 | + attestation_size: attestation_size, |
| 102 | + price_infos: price_infos, |
| 103 | + } |
| 104 | + } |
| 105 | + |
| 106 | + fun deserialize_price_info(cur: &mut Cursor<u8>): PriceInfo { |
| 107 | + |
| 108 | + // Skip obselete field |
| 109 | + let _product_identifier = deserialize::deserialize_vector(cur, 32); |
| 110 | + let price_identifier = price_identifier::from_byte_vec(deserialize::deserialize_vector(cur, 32)); |
| 111 | + let price = deserialize::deserialize_i64(cur); |
| 112 | + let conf = deserialize::deserialize_u64(cur); |
| 113 | + let expo = deserialize::deserialize_i32(cur); |
| 114 | + let ema_price = deserialize::deserialize_i64(cur); |
| 115 | + let ema_conf = deserialize::deserialize_u64(cur); |
| 116 | + let status = price_status::from_u64((deserialize::deserialize_u8(cur) as u64)); |
| 117 | + |
| 118 | + // Skip obselete fields |
| 119 | + let _num_publishers = deserialize::deserialize_u32(cur); |
| 120 | + let _max_num_publishers = deserialize::deserialize_u32(cur); |
| 121 | + |
| 122 | + let attestation_time = deserialize::deserialize_u64(cur); |
| 123 | + let publish_time = deserialize::deserialize_u64(cur); // |
| 124 | + let prev_publish_time = deserialize::deserialize_u64(cur); |
| 125 | + let prev_price = deserialize::deserialize_i64(cur); |
| 126 | + let prev_conf = deserialize::deserialize_u64(cur); |
| 127 | + |
| 128 | + // Handle the case where the status is not trading. This logic will soon be moved into |
| 129 | + // the attester. |
| 130 | + |
| 131 | + // If status is trading, use the current price. |
| 132 | + // If not, use the the last known trading price. |
| 133 | + let current_price = pyth::price::new(price, conf, expo, publish_time); |
| 134 | + if (status != price_status::new_trading()) { |
| 135 | + current_price = pyth::price::new(prev_price, prev_conf, expo, prev_publish_time); |
| 136 | + }; |
| 137 | + |
| 138 | + // If status is trading, use the timestamp of the aggregate as the timestamp for the |
| 139 | + // EMA price. If not, the EMA will have last been updated when the aggregate last had |
| 140 | + // trading status, so use prev_publish_time (the time when the aggregate last had trading status). |
| 141 | + let ema_timestamp = publish_time; |
| 142 | + if (status != price_status::new_trading()) { |
| 143 | + ema_timestamp = prev_publish_time; |
| 144 | + }; |
| 145 | + |
| 146 | + price_info::new( |
| 147 | + attestation_time, |
| 148 | + timestamp::now_seconds(), |
| 149 | + price_feed::new( |
| 150 | + price_identifier, |
| 151 | + current_price, |
| 152 | + pyth::price::new(ema_price, ema_conf, expo, ema_timestamp), |
| 153 | + ) |
| 154 | + ) |
| 155 | + } |
| 156 | + |
| 157 | + #[test] |
| 158 | + #[expected_failure(abort_code = 65560)] |
| 159 | + fun test_deserialize_batch_price_attestation_invalid_magic() { |
| 160 | + // A batch price attestation with a magic number of 0x50325749 |
| 161 | + let bytes = x"5032574900030000000102000400951436e0be37536be96f0896366089506a59763d036728332d3e3038047851aea7c6c75c89f14810ec1c54c03ab8f1864a4c4032791f05747f560faec380a695d1000000000000049a0000000000000008fffffffb00000000000005dc0000000000000003000000000100000001000000006329c0eb000000006329c0e9000000006329c0e400000000000006150000000000000007215258d81468614f6b7e194c5d145609394f67b041e93e6695dcc616faadd0603b9551a68d01d954d6387aff4df1529027ffb2fee413082e509feb29cc4904fe000000000000041a0000000000000003fffffffb00000000000005cb0000000000000003010000000100000001000000006329c0eb000000006329c0e9000000006329c0e4000000000000048600000000000000078ac9cf3ab299af710d735163726fdae0db8465280502eb9f801f74b3c1bd190333832fad6e36eb05a8972fe5f219b27b5b2bb2230a79ce79beb4c5c5e7ecc76d00000000000003f20000000000000002fffffffb00000000000005e70000000000000003010000000100000001000000006329c0eb000000006329c0e9000000006329c0e40000000000000685000000000000000861db714e9ff987b6fedf00d01f9fea6db7c30632d6fc83b7bc9459d7192bc44a21a28b4c6619968bd8c20e95b0aaed7df2187fd310275347e0376a2cd7427db800000000000006cb0000000000000001fffffffb00000000000005e40000000000000003010000000100000001000000006329c0eb000000006329c0e9000000006329c0e400000000000007970000000000000001"; |
| 162 | + destroy(deserialize(bytes)); |
| 163 | + } |
| 164 | + |
| 165 | + #[test(aptos_framework = @aptos_framework)] |
| 166 | + fun test_deserialize_batch_price_attestation(aptos_framework: signer) { |
| 167 | + |
| 168 | + // Set the arrival time |
| 169 | + account::create_account_for_test(@aptos_framework); |
| 170 | + timestamp::set_time_has_started_for_testing(&aptos_framework); |
| 171 | + let arrival_time = 1663074349; |
| 172 | + timestamp::update_global_time_for_test(1663074349 * 1000000); |
| 173 | + |
| 174 | + // A raw batch price attestation |
| 175 | + // The first attestation has a status of UNKNOWN |
| 176 | + let bytes = x"5032574800030000000102000400951436e0be37536be96f0896366089506a59763d036728332d3e3038047851aea7c6c75c89f14810ec1c54c03ab8f1864a4c4032791f05747f560faec380a695d1000000000000049a0000000000000008fffffffb00000000000005dc0000000000000003000000000100000001000000006329c0eb000000006329c0e9000000006329c0e400000000000006150000000000000007215258d81468614f6b7e194c5d145609394f67b041e93e6695dcc616faadd0603b9551a68d01d954d6387aff4df1529027ffb2fee413082e509feb29cc4904fe000000000000041a0000000000000003fffffffb00000000000005cb0000000000000003010000000100000001000000006329c0eb000000006329c0e9000000006329c0e4000000000000048600000000000000078ac9cf3ab299af710d735163726fdae0db8465280502eb9f801f74b3c1bd190333832fad6e36eb05a8972fe5f219b27b5b2bb2230a79ce79beb4c5c5e7ecc76d00000000000003f20000000000000002fffffffb00000000000005e70000000000000003010000000100000001000000006329c0eb000000006329c0e9000000006329c0e40000000000000685000000000000000861db714e9ff987b6fedf00d01f9fea6db7c30632d6fc83b7bc9459d7192bc44a21a28b4c6619968bd8c20e95b0aaed7df2187fd310275347e0376a2cd7427db800000000000006cb0000000000000001fffffffb00000000000005e40000000000000003010000000100000001000000006329c0eb000000006329c0e9000000006329c0e400000000000007970000000000000001"; |
| 177 | + |
| 178 | + let expected = BatchPriceAttestation { |
| 179 | + header: Header { |
| 180 | + magic: 0x50325748, |
| 181 | + version_major: 3, |
| 182 | + version_minor: 0, |
| 183 | + payload_id: 2, |
| 184 | + header_size: 1, |
| 185 | + }, |
| 186 | + attestation_count: 4, |
| 187 | + attestation_size: 149, |
| 188 | + price_infos: vector<PriceInfo>[ |
| 189 | + price_info::new( |
| 190 | + 1663680747, |
| 191 | + arrival_time, |
| 192 | + price_feed::new( |
| 193 | + price_identifier::from_byte_vec(x"c6c75c89f14810ec1c54c03ab8f1864a4c4032791f05747f560faec380a695d1"), |
| 194 | + price::new(i64::new(1557, false), 7, i64::new(5, true), 1663680740), |
| 195 | + price::new(i64::new(1500, false), 3, i64::new(5, true), 1663680740), |
| 196 | + ), |
| 197 | + ), |
| 198 | + price_info::new( |
| 199 | + 1663680747, |
| 200 | + arrival_time, |
| 201 | + price_feed::new( |
| 202 | + price_identifier::from_byte_vec(x"3b9551a68d01d954d6387aff4df1529027ffb2fee413082e509feb29cc4904fe"), |
| 203 | + price::new(i64::new(1050, false), 3, i64::new(5, true), 1663680745), |
| 204 | + price::new(i64::new(1483, false), 3, i64::new(5, true), 1663680745), |
| 205 | + ), |
| 206 | + ), |
| 207 | + price_info::new( |
| 208 | + 1663680747, |
| 209 | + arrival_time, |
| 210 | + price_feed::new( |
| 211 | + price_identifier::from_byte_vec(x"33832fad6e36eb05a8972fe5f219b27b5b2bb2230a79ce79beb4c5c5e7ecc76d"), |
| 212 | + price::new(i64::new(1010, false), 2, i64::new(5, true), 1663680745), |
| 213 | + price::new(i64::new(1511, false), 3, i64::new(5, true), 1663680745), |
| 214 | + ), |
| 215 | + ), |
| 216 | + price_info::new( |
| 217 | + 1663680747, |
| 218 | + arrival_time, |
| 219 | + price_feed::new( |
| 220 | + price_identifier::from_byte_vec(x"21a28b4c6619968bd8c20e95b0aaed7df2187fd310275347e0376a2cd7427db8"), |
| 221 | + price::new(i64::new(1739, false), 1, i64::new(5, true), 1663680745), |
| 222 | + price::new(i64::new(1508, false), 3, i64::new(5, true), 1663680745), |
| 223 | + ), |
| 224 | + ), |
| 225 | + ], |
| 226 | + }; |
| 227 | + |
| 228 | + let deserialized = deserialize(bytes); |
| 229 | + |
| 230 | + assert!(&expected == &deserialized, 1); |
| 231 | + destroy(expected); |
| 232 | + destroy(deserialized); |
| 233 | + } |
| 234 | +} |
0 commit comments