Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<Callout type="warning">
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.
</Callout>

Expand Down Expand Up @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion lazer/contracts/sui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion lazer/contracts/sui/sdk/js/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion lazer/contracts/sui/sdk/js/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 SDK breaking change: return type changes from update::Update to update_v2::Update

The SDK function addParseAndVerifyLeEcdsaUpdateCall at lazer/contracts/sui/sdk/js/src/index.ts:18 now calls parse_and_verify_le_ecdsa_update_v2 instead of parse_and_verify_le_ecdsa_update. This changes the on-chain return type from pyth_lazer::update::Update to pyth_lazer::update_v2::Update. Any existing deployed consumer smart contracts that accept update::Update as a parameter will fail at transaction execution time when used with the updated SDK, because the types are incompatible on-chain. The package version at lazer/contracts/sui/sdk/js/package.json:59 remains 0.3.0 and was not bumped despite this being a breaking change. Per REVIEW.md guidelines, public packages with changes should have their version bumped — suggest a minor bump to 0.4.0 (since semver 0.x allows breaking changes in minor versions).

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

});
}

Expand Down
33 changes: 25 additions & 8 deletions lazer/contracts/sui/sources/channel.move
Original file line number Diff line number Diff line change
@@ -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<u8> = "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()
Expand All @@ -37,35 +41,48 @@ 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,
_ => false,
}
}

/// 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,
_ => false,
}
}

/// 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,
_ => false,
}
}

/// 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,
Channel::FixedRate200ms => 200,
_ => 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
}
}
37 changes: 37 additions & 0 deletions lazer/contracts/sui/sources/channel_v2.move
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
module pyth_lazer::channel_v2;

#[error]
const EChannelOutOfRange: vector<u8> = "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 }
}
17 changes: 11 additions & 6 deletions lazer/contracts/sui/sources/pyth_lazer.move
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<u8>): update_v1::Update {
let update = parse_and_verify_le_ecdsa_update_v2(s, clock, update);
update_v1::from_v2(update)
}
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.

/// 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
Expand All @@ -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<u8>): Update {
public fun parse_and_verify_le_ecdsa_update_v2(s: &State, clock: &Clock, update: vector<u8>): Update {
let mut cursor = bcs::new(update);

// Parse and validate message magic
Expand Down Expand Up @@ -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)
}
54 changes: 15 additions & 39 deletions lazer/contracts/sui/sources/update.move
Original file line number Diff line number Diff line change
@@ -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<Feed>,
}

public(package) fun new(timestamp: u64, channel: Channel, feeds: vector<Feed>): 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<Feed> {
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<Feed> {
&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<Feed>();
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(),
}
}
62 changes: 62 additions & 0 deletions lazer/contracts/sui/sources/update_v2.move
Original file line number Diff line number Diff line change
@@ -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<u8> = "Trailing Update payload data";

public struct Update has copy, drop {
timestamp: u64,
channel: Channel,
feeds: vector<Feed>,
}

public(package) fun new(timestamp: u64, channel: Channel, feeds: vector<Feed>): 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<Feed> {
update.feeds
}

/// Get a reference to the feeds vector of the update.
public fun feeds_ref(update: &Update): &vector<Feed> {
&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<Feed>();
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 }
}
Loading
Loading