diff --git a/apps/developer-hub/content/docs/price-feeds/pro/integrate-as-consumer/sui.mdx b/apps/developer-hub/content/docs/price-feeds/pro/integrate-as-consumer/sui.mdx index 15b9f2afef..956da5621f 100644 --- a/apps/developer-hub/content/docs/price-feeds/pro/integrate-as-consumer/sui.mdx +++ b/apps/developer-hub/content/docs/price-feeds/pro/integrate-as-consumer/sui.mdx @@ -44,7 +44,7 @@ client.subscribe({ Pyth Pro provides a [Sui TypeScript SDK](https://github.com/pyth-network/pyth-crosschain/tree/main/lazer/contracts/sui/sdk/js) that handles the verification call for you. - Always call `parse_and_verify_le_ecdsa_update` in a Programmable Transaction Block (PTB), not inside your smart contract. + Always call `parse_and_verify_le_ecdsa_update_v2` in a Programmable Transaction Block (PTB), not inside your smart contract. This allows your contract to work with any newer version of the Pyth Lazer contract, since the `Update` type remains stable. @@ -122,7 +122,7 @@ pyth_lazer = { git = "https://github.com/pyth-network/pyth-crosschain.git", subd Import the necessary modules and write a function that accepts the `Update`: ```move copy -use pyth_lazer::update::Update; +use pyth_lazer::update_v2::Update; use pyth_lazer::feed::Feed; public fun consume_price(update: Update) { diff --git a/lazer/contracts/sui/README.md b/lazer/contracts/sui/README.md index d33a818544..e31c427ec9 100644 --- a/lazer/contracts/sui/README.md +++ b/lazer/contracts/sui/README.md @@ -18,7 +18,7 @@ Run tests: ```bash sui move test # run a specific test -sui move test test_parse_and_verify_le_ecdsa_update +sui move test test_parse_and_verify_le_ecdsa_update_v2 ``` Deploy: diff --git a/lazer/contracts/sui/sdk/js/README.md b/lazer/contracts/sui/sdk/js/README.md index b196c4d230..a8f202bf65 100644 --- a/lazer/contracts/sui/sdk/js/README.md +++ b/lazer/contracts/sui/sdk/js/README.md @@ -16,7 +16,7 @@ A runnable example is provided at `examples/FetchAndVerifyUpdate.ts`. It: - connects to Lazer via `@pythnetwork/pyth-lazer-sdk`, - fetches a single `leEcdsa` payload, -- composes a Sui transaction calling `parse_and_verify_le_ecdsa_update`. +- composes a Sui transaction calling `parse_and_verify_le_ecdsa_update_v2`. ### Run the example diff --git a/lazer/contracts/sui/sdk/js/src/index.ts b/lazer/contracts/sui/sdk/js/src/index.ts index 608d968ef0..b6b7f1013e 100644 --- a/lazer/contracts/sui/sdk/js/src/index.ts +++ b/lazer/contracts/sui/sdk/js/src/index.ts @@ -15,7 +15,7 @@ export async function addParseAndVerifyLeEcdsaUpdateCall(opts: { tx.object.clock(), tx.pure.vector("u8", update), ], - target: `${id}::pyth_lazer::parse_and_verify_le_ecdsa_update`, + target: `${id}::pyth_lazer::parse_and_verify_le_ecdsa_update_v2`, }); } diff --git a/lazer/contracts/sui/sources/channel.move b/lazer/contracts/sui/sources/channel.move index 76ed1a69e4..9afa8bed91 100644 --- a/lazer/contracts/sui/sources/channel.move +++ b/lazer/contracts/sui/sources/channel.move @@ -1,30 +1,34 @@ +#[deprecated(note = b"Use `pyth_lazer::channel_v2` instead.")] module pyth_lazer::channel; +use pyth_lazer::channel_v2; + #[error] const EInvalidChannel: vector = "Invalid channel value"; +#[deprecated(note = b"Use `channel_v2::Channel` instead.")] public enum Channel has copy, drop { RealTime, FixedRate50ms, FixedRate200ms, } -/// Create a new RealTime channel +#[deprecated(note = b"Use `channel_v2::Channel` instead.")] public fun new_real_time(): Channel { Channel::RealTime } -/// Create a new FixedRate50ms channel +#[deprecated(note = b"Use `channel_v2::Channel` instead.")] public fun new_fixed_rate_50ms(): Channel { Channel::FixedRate50ms } -/// Create a new FixedRate200ms channel +#[deprecated(note = b"Use `channel_v2::Channel` instead.")] public fun new_fixed_rate_200ms(): Channel { Channel::FixedRate200ms } -/// Parse channel from a channel value byte +#[deprecated(note = b"Use `channel_v2::Channel` instead.")] public fun from_u8(channel_value: u8): Channel { if (channel_value == 1) { new_real_time() @@ -37,7 +41,7 @@ public fun from_u8(channel_value: u8): Channel { } } -/// Check if the channel is RealTime +#[deprecated(note = b"Use `channel_v2::Channel` instead.")] public fun is_real_time(channel: &Channel): bool { match (channel) { Channel::RealTime => true, @@ -45,7 +49,7 @@ public fun is_real_time(channel: &Channel): bool { } } -/// Check if the channel is FixedRate50ms +#[deprecated(note = b"Use `channel_v2::Channel` instead.")] public fun is_fixed_rate_50ms(channel: &Channel): bool { match (channel) { Channel::FixedRate50ms => true, @@ -53,7 +57,7 @@ public fun is_fixed_rate_50ms(channel: &Channel): bool { } } -/// Check if the channel is FixedRate200ms +#[deprecated(note = b"Use `channel_v2::Channel` instead.")] public fun is_fixed_rate_200ms(channel: &Channel): bool { match (channel) { Channel::FixedRate200ms => true, @@ -61,7 +65,7 @@ public fun is_fixed_rate_200ms(channel: &Channel): bool { } } -/// Get the update interval in milliseconds for fixed rate channels, returns 0 for non-fixed rate channels +#[deprecated(note = b"Use `channel_v2::Channel` instead.")] public fun get_update_interval_ms(channel: &Channel): u64 { match (channel) { Channel::FixedRate50ms => 50, @@ -69,3 +73,16 @@ public fun get_update_interval_ms(channel: &Channel): u64 { _ => 0, } } + +#[allow(deprecated_usage)] +public(package) fun from_v2(channel: channel_v2::Channel): Channel { + if (channel.is_real_time()) { + Channel::RealTime + } else if (channel.is_fixed_rate_50ms()) { + Channel::FixedRate50ms + } else if (channel.is_fixed_rate_200ms()) { + Channel::FixedRate200ms + } else { + abort EInvalidChannel + } +} diff --git a/lazer/contracts/sui/sources/channel_v2.move b/lazer/contracts/sui/sources/channel_v2.move new file mode 100644 index 0000000000..a3dd0c8bd2 --- /dev/null +++ b/lazer/contracts/sui/sources/channel_v2.move @@ -0,0 +1,37 @@ +module pyth_lazer::channel_v2; + +#[error] +const EChannelOutOfRange: vector = "Channel value out of range"; + +/// Channel enum. Use `is_*` methods to check current value. +public struct Channel(u8) has copy, drop, store; + +public(package) fun from_u8(value: u8): Channel { + assert!(value >= 1 && value <= 4, EChannelOutOfRange); + Channel(value) +} + +public fun is_real_time(self: &Channel): bool { + self.0 == 1 +} + +public fun is_fixed_rate_50ms(self: &Channel): bool { + self.0 == 2 +} + +public fun is_fixed_rate_200ms(self: &Channel): bool { + self.0 == 3 +} + +public fun is_fixed_rate_1000ms(self: &Channel): bool { + self.0 == 4 +} + +/// Returns the update interval in milliseconds for fixed rate channels, 0 for RealTime. +public fun get_update_interval_ms(self: &Channel): u64 { + if (self.0 == 1) { 0 } + else if (self.0 == 2) { 50 } + else if (self.0 == 3) { 200 } + else if (self.0 == 4) { 1000 } + else { abort EChannelOutOfRange } +} diff --git a/lazer/contracts/sui/sources/pyth_lazer.move b/lazer/contracts/sui/sources/pyth_lazer.move index c44f8e5cbe..91f85db20e 100644 --- a/lazer/contracts/sui/sources/pyth_lazer.move +++ b/lazer/contracts/sui/sources/pyth_lazer.move @@ -6,10 +6,9 @@ use sui::{ ecdsa_k1::secp256k1_ecrecover, }; -use pyth_lazer::{ - state::State, - update::{Self, Update}, -}; +use pyth_lazer::state::State; +use pyth_lazer::update as update_v1; +use pyth_lazer::update_v2::{Self, Update}; const SECP256K1_SIG_LEN: u32 = 65; const UPDATE_MESSAGE_MAGIC: u32 = 1296547300; @@ -64,6 +63,12 @@ public(package) fun verify_le_ecdsa_message( assert!(clock.timestamp_ms() < expires_at_ms, ESignerExpired); } +#[deprecated(note = b"Use `pyth_lazer::parse_and_verify_le_ecdsa_update_v2` instead.")] +public fun parse_and_verify_le_ecdsa_update(s: &State, clock: &Clock, update: vector): update_v1::Update { + let update = parse_and_verify_le_ecdsa_update_v2(s, clock, update); + update_v1::from_v2(update) +} + /// Parse the Lazer update message and validate the signature within. /// The parsing logic is based on the Lazer rust protocol definition defined here: /// https://docs.rs/pyth-lazer-protocol/latest/pyth_lazer_protocol/payload/index.html @@ -79,7 +84,7 @@ public(package) fun verify_le_ecdsa_message( /// * `EInvalidPayloadLength` - Payload length doesn't match actual data /// * `ESignerNotTrusted` - The recovered public key is not in the trusted signers list /// * `ESignerExpired` - The signer's certificate has expired -public fun parse_and_verify_le_ecdsa_update(s: &State, clock: &Clock, update: vector): Update { +public fun parse_and_verify_le_ecdsa_update_v2(s: &State, clock: &Clock, update: vector): Update { let mut cursor = bcs::new(update); // Parse and validate message magic @@ -109,5 +114,5 @@ public fun parse_and_verify_le_ecdsa_update(s: &State, clock: &Clock, update: ve // Verify the signature against trusted signers verify_le_ecdsa_message(s, clock, &signature, &payload); - update::parse_from_cursor(payload_cursor) + update_v2::parse_from_cursor(payload_cursor) } diff --git a/lazer/contracts/sui/sources/update.move b/lazer/contracts/sui/sources/update.move index f46ddd0a9a..53078d6e34 100644 --- a/lazer/contracts/sui/sources/update.move +++ b/lazer/contracts/sui/sources/update.move @@ -1,66 +1,42 @@ +#[deprecated(note = b"Use `pyth_lazer::update_v2` instead.")] module pyth_lazer::update; use pyth_lazer::channel::{Self, Channel}; -use pyth_lazer::feed::{Self, Feed}; -use sui::bcs; - -// Error codes for update parsing -const EInvalidPayload: u64 = 3; +use pyth_lazer::feed::Feed; +use pyth_lazer::update_v2; +#[deprecated(note = b"Use `update_v2::Update` instead.")] public struct Update has copy, drop { timestamp: u64, channel: Channel, feeds: vector, } -public(package) fun new(timestamp: u64, channel: Channel, feeds: vector): Update { - Update { timestamp, channel, feeds } -} - -/// Get the timestamp of the update +#[deprecated(note = b"Use `update_v2::Update` instead.")] public fun timestamp(update: &Update): u64 { update.timestamp } -/// Get a reference to the channel of the update +#[deprecated(note = b"Use `update_v2::Update` instead.")] public fun channel(update: &Update): Channel { update.channel } -/// Get a copy of the feeds vector of the update +#[deprecated(note = b"Use `update_v2::Update` instead.")] public fun feeds(update: &Update): vector { update.feeds } -/// Get a reference to the feeds vector of the update +#[deprecated(note = b"Use `update_v2::Update` instead.")] public fun feeds_ref(update: &Update): &vector { &update.feeds } -/// Parse the update from a BCS cursor containing the payload data -/// This assumes the payload magic has already been validated and consumed -public(package) fun parse_from_cursor(mut cursor: bcs::BCS): Update { - // Parse timestamp - let timestamp = cursor.peel_u64(); - - // Parse channel - let channel_value = cursor.peel_u8(); - let channel = channel::from_u8(channel_value); - - // Parse feeds - let feed_count = cursor.peel_u8(); - let mut feeds = vector::empty(); - let mut feed_i = 0; - - while (feed_i < feed_count) { - let feed = feed::parse_from_cursor(&mut cursor); - vector::push_back(&mut feeds, feed); - feed_i = feed_i + 1; - }; - - // Verify no remaining bytes - let remaining_bytes = cursor.into_remainder_bytes(); - assert!(remaining_bytes.length() == 0, EInvalidPayload); - - Update { timestamp, channel, feeds } +#[allow(deprecated_usage)] +public(package) fun from_v2(update: update_v2::Update): Update { + Update { + timestamp: update.timestamp(), + channel: channel::from_v2(update.channel()), + feeds: update.feeds(), + } } diff --git a/lazer/contracts/sui/sources/update_v2.move b/lazer/contracts/sui/sources/update_v2.move new file mode 100644 index 0000000000..4ec5a04ad5 --- /dev/null +++ b/lazer/contracts/sui/sources/update_v2.move @@ -0,0 +1,62 @@ +module pyth_lazer::update_v2; + +use pyth_lazer::channel_v2::{Self, Channel}; +use pyth_lazer::feed::{Self, Feed}; +use sui::bcs; + +#[error] +const ETrailingPayloadData: vector = "Trailing Update payload data"; + +public struct Update has copy, drop { + timestamp: u64, + channel: Channel, + feeds: vector, +} + +public(package) fun new(timestamp: u64, channel: Channel, feeds: vector): Update { + Update { timestamp, channel, feeds } +} + +/// Get the timestamp of the update. +public fun timestamp(update: &Update): u64 { + update.timestamp +} + +/// Get a reference to the channel of the update. +public fun channel(update: &Update): Channel { + update.channel +} + +/// Get a copy of the feeds vector of the update. +public fun feeds(update: &Update): vector { + update.feeds +} + +/// Get a reference to the feeds vector of the update. +public fun feeds_ref(update: &Update): &vector { + &update.feeds +} + +/// Parse the update from a BCS cursor containing the payload data. +/// Assumes the payload magic has already been validated and consumed. +public(package) fun parse_from_cursor(mut cursor: bcs::BCS): Update { + let timestamp = cursor.peel_u64(); + + let channel_value = cursor.peel_u8(); + let channel = channel_v2::from_u8(channel_value); + + let feed_count = cursor.peel_u8(); + let mut feeds = vector::empty(); + let mut feed_i = 0; + + while (feed_i < feed_count) { + let feed = feed::parse_from_cursor(&mut cursor); + vector::push_back(&mut feeds, feed); + feed_i = feed_i + 1; + }; + + let remaining_bytes = cursor.into_remainder_bytes(); + assert!(remaining_bytes.length() == 0, ETrailingPayloadData); + + Update { timestamp, channel, feeds } +} diff --git a/lazer/contracts/sui/tests/pyth_lazer_tests.move b/lazer/contracts/sui/tests/pyth_lazer_tests.move index 264fadb015..906f7d7bb1 100644 --- a/lazer/contracts/sui/tests/pyth_lazer_tests.move +++ b/lazer/contracts/sui/tests/pyth_lazer_tests.move @@ -6,20 +6,20 @@ use std::unit_test::{assert_eq, destroy}; use sui::clock; use pyth_lazer::{ - channel::new_fixed_rate_200ms, feed::Feed, governance, i16, i64, pyth_lazer::{ - parse_and_verify_le_ecdsa_update, verify_le_ecdsa_message, - ESignerNotTrusted, ESignerExpired, EInvalidUpdateMagic, + parse_and_verify_le_ecdsa_update, parse_and_verify_le_ecdsa_update_v2, + verify_le_ecdsa_message, ESignerNotTrusted, ESignerExpired, EInvalidUpdateMagic, EInvalidPayloadMagic, EInvalidPayloadLength }, state, + channel::EInvalidChannel, }; -/* Test data from the Lazer subscription: +/* Test data from a Lazer subscription: > Request { "subscriptionId": 1, @@ -29,74 +29,126 @@ use pyth_lazer::{ "price", "bestBidPrice", "bestAskPrice", + "confidence", "exponent", + "publisherCount", "fundingRate", "fundingTimestamp", "fundingRateInterval", "marketSession", "emaPrice", "emaConfidence", - "feedUpdateTimestamp", + "feedUpdateTimestamp" ], "formats": ["leEcdsa"], - "channel": "fixed_rate@200ms", - "jsonBinaryEncoding": "hex", + "channel": "fixed_rate@1000ms", + "jsonBinaryEncoding": "hex" } < Response { - "type": "streamUpdated", - "subscriptionId": 1, "parsed": { - "timestampUs": "1771252161800000", + "timestampUs": "1778594100000000", "priceFeeds": [ { "priceFeedId": 1, - "price": "6828284601313", - "bestBidPrice": "6828243494234", - "bestAskPrice": "6828830067583", + "price": "8063953748222", + "bestBidPrice": "8063927547164", + "bestAskPrice": "8064518785063", + "publisherCount": 16, "exponent": -8, + "confidence": 1783407191, "marketSession": "regular", - "emaPrice": "6866807100000", - "emaConfidence": 6866706200000, - "feedUpdateTimestamp": 1771252161800000 + "emaPrice": "8071363900000", + "emaConfidence": 1946443420, + "feedUpdateTimestamp": 1778594100000000 }, { "priceFeedId": 2, - "price": "195892878231", - "bestBidPrice": "195881010500", - "bestAskPrice": "195897850776", + "price": "227676960645", + "bestBidPrice": "227672112231", + "bestAskPrice": "227695946107", + "publisherCount": 17, "exponent": -8, + "confidence": 69539855, "marketSession": "regular", - "emaPrice": "197455529000", - "emaConfidence": 197451517000, - "feedUpdateTimestamp": 1771252161800000 + "emaPrice": "228268947000", + "emaConfidence": 57689210, + "feedUpdateTimestamp": 1778594100000000 }, { "priceFeedId": 112, - "price": "68554377427540000", + "price": "80813667608700000", + "publisherCount": 3, "exponent": -12, - "fundingRate": -32770000, - "fundingTimestamp": 1771228800003000, + "fundingRate": 38770000, + "fundingTimestamp": 1778594099726525, "fundingRateInterval": 28800000000, "marketSession": "regular", - "feedUpdateTimestamp": 1771252161800000 + "feedUpdateTimestamp": 1778594100000000 } ] }, "leEcdsa": { "encoding": "hex", - "data": "e4bd474d73a7e70a8e2b8de236b55dcc6a771b4a8a1533fe492f424fae162369fa14103e04c1c93302cef8a052110a950da031f9dc5eade9e6099e95668aff2592ec1f7900fe0075d3c7934067e9c7f14a06000303010000000b00e1637ad535060000015a2507d335060000027f8bfdf53506000004f8ff0600070008000900000a601299cd3e0600000bc07595c73e0600000c014067e9c7f14a0600020000000b00971b209c2d0000000144056b9b2d0000000298fb6b9c2d00000004f8ff0600070008000900000a284444f92d0000000b480c07f92d0000000c014067e9c7f14a0600700000000b0020d85dd2d78df30001000000000000000002000000000000000004f4ff060130f80bfeffffffff0701b8ab7057ec4a0600080100209db4060000000900000a00000000000000000b00000000000000000c014067e9c7f14a0600" + "data": "e4bd474da72e1daf244e50635f473fcb0e595e3643302f976c95f7fabfc0660f5a612d2d55bdb94f5a2fe926932ce9d9da338d05969b5a8b8c94024c86399005cc547fd001220175d3c79300b587359f5106000403010000000d00fe40198955070000011c75898755070000022708c7aa550700000557a24c6a0000000004f8ff0310000600070008000900000a6032c742570700000b9c5e0474000000000c0100b587359f510600020000000d0085b79a02350000000167bc500235000000027b69bc0335000000050f1825040000000004f8ff0311000600070008000900000a38b6e325350000000b7a447003000000000c0100b587359f510600700000000d006024d03e9a1b1f0101000000000000000002000000000000000005000000000000000004f4ff030300060150954f02000000000701bd8883359f510600080100209db4060000000900000a00000000000000000b00000000000000000c0100b587359f510600" } } */ -const TEST_LAZER_UPDATE: vector = x"e4bd474d73a7e70a8e2b8de236b55dcc6a771b4a8a1533fe492f424fae162369fa14103e04c1c93302cef8a052110a950da031f9dc5eade9e6099e95668aff2592ec1f7900fe0075d3c7934067e9c7f14a06000303010000000b00e1637ad535060000015a2507d335060000027f8bfdf53506000004f8ff0600070008000900000a601299cd3e0600000bc07595c73e0600000c014067e9c7f14a0600020000000b00971b209c2d0000000144056b9b2d0000000298fb6b9c2d00000004f8ff0600070008000900000a284444f92d0000000b480c07f92d0000000c014067e9c7f14a0600700000000b0020d85dd2d78df30001000000000000000002000000000000000004f4ff060130f80bfeffffffff0701b8ab7057ec4a0600080100209db4060000000900000a00000000000000000b00000000000000000c014067e9c7f14a0600"; +const TEST_LAZER_UPDATE: vector = x"e4bd474da72e1daf244e50635f473fcb0e595e3643302f976c95f7fabfc0660f5a612d2d55bdb94f5a2fe926932ce9d9da338d05969b5a8b8c94024c86399005cc547fd001220175d3c79300b587359f5106000403010000000d00fe40198955070000011c75898755070000022708c7aa550700000557a24c6a0000000004f8ff0310000600070008000900000a6032c742570700000b9c5e0474000000000c0100b587359f510600020000000d0085b79a02350000000167bc500235000000027b69bc0335000000050f1825040000000004f8ff0311000600070008000900000a38b6e325350000000b7a447003000000000c0100b587359f510600700000000d006024d03e9a1b1f0101000000000000000002000000000000000005000000000000000004f4ff030300060150954f02000000000701bd8883359f510600080100209db4060000000900000a00000000000000000b00000000000000000c0100b587359f510600"; -const TEST_PAYLOAD: vector = x"75d3c793c0f4295fbb3c060003030100000007005986bacb520a00000162e937ca520a000002a5087bd4520a000004f8ff06000700080002000000070078625c456100000001aba11b456100000002ba8ac0456100000004f8ff060007000800700000000700d8c3e1445a1c940101000000000000000002000000000000000004f4ff0601f03ee30100000000070100e0c6f2b93c0600080100209db406000000"; -const TEST_SIGNATURE: vector = x"42e3c9c3477b30f2c5527ebe2fb2c8adadadacaddfa7d95243b80fb8f0d813b453e587f140cf40a1120d75f1ffee8ad4337267e4fcbd23eabb2a555804f85ec101"; +const TEST_PAYLOAD: vector = x"75d3c79300b587359f5106000403010000000d00fe40198955070000011c75898755070000022708c7aa550700000557a24c6a0000000004f8ff0310000600070008000900000a6032c742570700000b9c5e0474000000000c0100b587359f510600020000000d0085b79a02350000000167bc500235000000027b69bc0335000000050f1825040000000004f8ff0311000600070008000900000a38b6e325350000000b7a447003000000000c0100b587359f510600700000000d006024d03e9a1b1f0101000000000000000002000000000000000005000000000000000004f4ff030300060150954f02000000000701bd8883359f510600080100209db4060000000900000a00000000000000000b00000000000000000c0100b587359f510600"; +const TEST_SIGNATURE: vector = x"a72e1daf244e50635f473fcb0e595e3643302f976c95f7fabfc0660f5a612d2d55bdb94f5a2fe926932ce9d9da338d05969b5a8b8c94024c86399005cc547fd001"; const TEST_TRUSTED_SIGNER_PUBKEY: vector = x"03a4380f01136eb2640f90c17e1e319e02bbafbeef2e6e67dc48af53f9827e155b"; +#[allow(deprecated_usage)] +#[test] +public fun test_parse_and_verify_le_ecdsa_compatible_update() { + let mut ctx = tx_context::dummy(); + let mut state = state::new_for_test(&mut ctx, governance::dummy()); + let current_cap = state.current_cap(); + let clock = clock::create_for_testing(&mut ctx); + + // Add the trusted signer that matches the test data + let trusted_pubkey = TEST_TRUSTED_SIGNER_PUBKEY; + let expiry_time = 2_000_000_000_000; // Far in the future + state.update_trusted_signer(¤t_cap, trusted_pubkey, expiry_time); + + let update = parse_and_verify_le_ecdsa_update(&state, &clock, x"e4bd474dd87d2ef4d2d3ba283fb68ceeaea1fc2b1544061e8058036a38e56801481785aa0a92b4c1bd1fcecefec37919871b1254b4cdbcd3b70b22b2da0261d65d4dbfb000220175d3c793006f40a49f5106000303010000000d00c417492f5907000001ece9d31e5907000002a818b5675907000005fb6670680000000004f8ff0310000600070008000900000a403b5739560700000b60a7f476000000000c01006f40a49f510600020000000d001ef7c10e3500000001e72f860c35000000025edd430f350000000522f876020000000004f8ff0311000600070008000900000ac076720d350000000b5f047403000000000c01006f40a49f510600700000000d006024d03e9a1b1f0101000000000000000002000000000000000005000000000000000004f4ff030300060150954f020000000007016daf37a49f510600080100209db4060000000900000a00000000000000000b00000000000000000c01006f40a49f510600"); + + // If we reach this point, the function successfully verified & parsed the payload (no assertion failures) + // Validate that the fields have correct values + assert_eq!(update.timestamp(), 1778595957600000); + assert!(update.channel().is_fixed_rate_200ms()); + assert_eq!(update.feeds_ref().length(), 3); + + // Clean up + destroy(state); + clock.destroy_for_testing(); +} + +#[allow(deprecated_usage)] +#[test, expected_failure(abort_code = EInvalidChannel)] +public fun test_parse_and_verify_le_ecdsa_incompatible_update() { + let mut ctx = tx_context::dummy(); + let mut state = state::new_for_test(&mut ctx, governance::dummy()); + let current_cap = state.current_cap(); + let clock = clock::create_for_testing(&mut ctx); + + // Add the trusted signer that matches the test data + let trusted_pubkey = TEST_TRUSTED_SIGNER_PUBKEY; + let expiry_time = 2_000_000_000_000; // Far in the future + state.update_trusted_signer(¤t_cap, trusted_pubkey, expiry_time); + + // Should fail because of unsupported channel ID + parse_and_verify_le_ecdsa_update(&state, &clock, TEST_LAZER_UPDATE); + + // Clean up + destroy(state); + clock.destroy_for_testing(); +} + #[test] -public fun test_parse_and_verify_le_ecdsa_update() { +public fun test_parse_and_verify_le_ecdsa_update_v2() { let mut ctx = tx_context::dummy(); let mut state = state::new_for_test(&mut ctx, governance::dummy()); let current_cap = state.current_cap(); @@ -107,15 +159,14 @@ public fun test_parse_and_verify_le_ecdsa_update() { let expiry_time = 2_000_000_000_000; // Far in the future state.update_trusted_signer(¤t_cap, trusted_pubkey, expiry_time); - let update = parse_and_verify_le_ecdsa_update(&state, &clock, TEST_LAZER_UPDATE); + let update = parse_and_verify_le_ecdsa_update_v2(&state, &clock, TEST_LAZER_UPDATE); // If we reach this point, the function successfully verified & parsed the payload (no assertion failures) // Validate that the fields have correct values - assert_eq!(update.timestamp(), 1771252161800000); - assert_eq!(update.channel(), new_fixed_rate_200ms()); + assert_eq!(update.timestamp(), 1778594100000000); + assert!(update.channel().is_fixed_rate_1000ms()); assert_eq!(update.feeds_ref().length(), 3); - // Separated into another function to get past function size limit test_parse_and_verify_le_ecdsa_update__feeds(update.feeds_ref()); // Clean up @@ -129,58 +180,58 @@ fun test_parse_and_verify_le_ecdsa_update__feeds(feeds: &vector) { assert_eq!(feed_1.feed_id(), 1); assert_eq!( feed_1.price(), - option::some(option::some(i64::from_u64(6828284601313))) + option::some(option::some(i64::from_u64(8063953748222))) ); assert_eq!( feed_1.best_bid_price(), - option::some(option::some(i64::from_u64(6828243494234))), + option::some(option::some(i64::from_u64(8063927547164))), ); assert_eq!( feed_1.best_ask_price(), - option::some(option::some(i64::from_u64(6828830067583))), + option::some(option::some(i64::from_u64(8064518785063))), ); assert_eq!(feed_1.exponent(), option::some(i16::new(8, true))); - assert_eq!(feed_1.publisher_count(), option::none()); - assert_eq!(feed_1.confidence(), option::none()); + assert_eq!(feed_1.publisher_count(), option::some(16)); + assert_eq!(feed_1.confidence(), option::some(option::some(i64::from_u64(1783407191)))); assert_eq!(feed_1.funding_rate(), option::some(option::none())); assert_eq!(feed_1.funding_timestamp(), option::some(option::none())); assert_eq!(feed_1.funding_rate_interval(), option::some(option::none())); assert!(feed_1.market_session().is_some_and!(|s| s.is_regular())); - assert_eq!(feed_1.ema_price(), option::some(option::some(i64::from_u64(6866807100000)))); - assert_eq!(feed_1.ema_confidence(), option::some(option::some(6866706200000))); - assert_eq!(feed_1.feed_update_timestamp(), option::some(option::some(1771252161800000))); + assert_eq!(feed_1.ema_price(), option::some(option::some(i64::from_u64(8071363900000)))); + assert_eq!(feed_1.ema_confidence(), option::some(option::some(1946443420))); + assert_eq!(feed_1.feed_update_timestamp(), option::some(option::some(1778594100000000))); let feed_2 = &feeds[1]; assert_eq!(feed_2.feed_id(), 2); - assert_eq!(feed_2.price(), option::some(option::some(i64::from_u64(195892878231)))); - assert_eq!(feed_2.best_bid_price(), option::some(option::some(i64::from_u64(195881010500)))); - assert_eq!(feed_2.best_ask_price(), option::some(option::some(i64::from_u64(195897850776)))); + assert_eq!(feed_2.price(), option::some(option::some(i64::from_u64(227676960645)))); + assert_eq!(feed_2.best_bid_price(), option::some(option::some(i64::from_u64(227672112231)))); + assert_eq!(feed_2.best_ask_price(), option::some(option::some(i64::from_u64(227695946107)))); assert_eq!(feed_2.exponent(), option::some(i16::new(8, true))); - assert_eq!(feed_2.publisher_count(), option::none()); - assert_eq!(feed_2.confidence(), option::none()); + assert_eq!(feed_2.publisher_count(), option::some(17)); + assert_eq!(feed_2.confidence(), option::some(option::some(i64::from_u64(69539855)))); assert_eq!(feed_2.funding_rate(), option::some(option::none())); assert_eq!(feed_2.funding_timestamp(), option::some(option::none())); assert_eq!(feed_2.funding_rate_interval(), option::some(option::none())); assert!(feed_2.market_session().is_some_and!(|s| s.is_regular())); - assert_eq!(feed_2.ema_price(), option::some(option::some(i64::from_u64(197455529000)))); - assert_eq!(feed_2.ema_confidence(), option::some(option::some(197451517000))); - assert_eq!(feed_2.feed_update_timestamp(), option::some(option::some(1771252161800000))); + assert_eq!(feed_2.ema_price(), option::some(option::some(i64::from_u64(228268947000)))); + assert_eq!(feed_2.ema_confidence(), option::some(option::some(57689210))); + assert_eq!(feed_2.feed_update_timestamp(), option::some(option::some(1778594100000000))); let feed_3 = &feeds[2]; assert_eq!(feed_3.feed_id(), 112); - assert_eq!(feed_3.price(), option::some(option::some(i64::from_u64(68554377427540000)))); + assert_eq!(feed_3.price(), option::some(option::some(i64::from_u64(80813667608700000)))); assert_eq!(feed_3.best_bid_price(), option::some(option::none())); assert_eq!(feed_3.best_ask_price(), option::some(option::none())); assert_eq!(feed_3.exponent(), option::some(i16::new(12, true))); - assert_eq!(feed_3.publisher_count(), option::none()); - assert_eq!(feed_3.confidence(), option::none()); - assert_eq!(feed_3.funding_rate(), option::some(option::some(i64::new(32770000, true)))); - assert_eq!(feed_3.funding_timestamp(), option::some(option::some(1771228800003000))); + assert_eq!(feed_3.publisher_count(), option::some(3)); + assert_eq!(feed_3.confidence(), option::some(option::none())); + assert_eq!(feed_3.funding_rate(), option::some(option::some(i64::from_u64(38770000)))); + assert_eq!(feed_3.funding_timestamp(), option::some(option::some(1778594099726525))); assert_eq!(feed_3.funding_rate_interval(), option::some(option::some(28800000000))); assert!(feed_3.market_session().is_some_and!(|s| s.is_regular())); assert_eq!(feed_3.ema_price(), option::some(option::none())); assert_eq!(feed_3.ema_confidence(), option::some(option::none())); - assert_eq!(feed_3.feed_update_timestamp(), option::some(option::some(1771252161800000))); + assert_eq!(feed_3.feed_update_timestamp(), option::some(option::some(1778594100000000))); } #[test] @@ -342,7 +393,7 @@ public fun test_parse_invalid_update_magic() { *vector::borrow_mut(&mut invalid_update, 0) = 0xFF; // Corrupt the magic // This should fail with EInvalidUpdateMagic - parse_and_verify_le_ecdsa_update(&state, &clock, invalid_update); + parse_and_verify_le_ecdsa_update_v2(&state, &clock, invalid_update); // Clean up destroy(state); @@ -367,7 +418,7 @@ public fun test_parse_invalid_payload_magic() { *invalid_update.borrow_mut(71) = 0xFF; // Corrupt the payload magic // This corrupts the payload magic, so expect EInvalidMagic - parse_and_verify_le_ecdsa_update(&state, &clock, invalid_update); + parse_and_verify_le_ecdsa_update_v2(&state, &clock, invalid_update); // Clean up destroy(state); @@ -393,7 +444,7 @@ public fun test_parse_invalid_payload_length() { *invalid_update.borrow_mut(69) = 0xFF; // Set payload length too high // This should fail with EInvalidPayloadLength because payload length validation happens before signature verification - parse_and_verify_le_ecdsa_update(&state, &clock, invalid_update); + parse_and_verify_le_ecdsa_update_v2(&state, &clock, invalid_update); // Clean up destroy(state); @@ -416,7 +467,7 @@ public fun test_parse_truncated_data() { let truncated_update = TEST_LAZER_UPDATE.take(50); // This should fail with BCS EOutOfRange error when trying to read beyond available data - parse_and_verify_le_ecdsa_update(&state, &clock, truncated_update); + parse_and_verify_le_ecdsa_update_v2(&state, &clock, truncated_update); // Clean up destroy(state);