From 2c4fe764a1a350763062c0b8d0855da815062030 Mon Sep 17 00:00:00 2001 From: Ali Behjati Date: Tue, 29 Jul 2025 13:50:12 +0200 Subject: [PATCH 1/4] feat(lazer): add sui example --- lazer/sui/Move.lock | 48 +++++ lazer/sui/Move.toml | 12 ++ lazer/sui/README.md | 31 +++ lazer/sui/sources/i16.move | 150 ++++++++++++++ lazer/sui/sources/i64.move | 137 +++++++++++++ lazer/sui/sources/lazer_example.move | 284 +++++++++++++++++++++++++++ 6 files changed, 662 insertions(+) create mode 100644 lazer/sui/Move.lock create mode 100644 lazer/sui/Move.toml create mode 100644 lazer/sui/README.md create mode 100644 lazer/sui/sources/i16.move create mode 100644 lazer/sui/sources/i64.move create mode 100644 lazer/sui/sources/lazer_example.move diff --git a/lazer/sui/Move.lock b/lazer/sui/Move.lock new file mode 100644 index 0000000..6bae3e0 --- /dev/null +++ b/lazer/sui/Move.lock @@ -0,0 +1,48 @@ +# @generated by Move, please check-in and do not edit manually. + +[move] +version = 3 +manifest_digest = "9A25A20E6E3BABDD1C296A4B0A45AE53C1ED219ECDBEDB8ABFE47CE82D7EB785" +deps_digest = "F9B494B64F0615AED0E98FC12A85B85ECD2BC5185C22D30E7F67786BB52E507C" +dependencies = [ + { id = "Bridge", name = "Bridge" }, + { id = "MoveStdlib", name = "MoveStdlib" }, + { id = "Sui", name = "Sui" }, + { id = "SuiSystem", name = "SuiSystem" }, +] + +[[move.package]] +id = "Bridge" +source = { git = "https://github.com/MystenLabs/sui.git", rev = "209f0da8e316", subdir = "crates/sui-framework/packages/bridge" } + +dependencies = [ + { id = "MoveStdlib", name = "MoveStdlib" }, + { id = "Sui", name = "Sui" }, + { id = "SuiSystem", name = "SuiSystem" }, +] + +[[move.package]] +id = "MoveStdlib" +source = { git = "https://github.com/MystenLabs/sui.git", rev = "209f0da8e316", subdir = "crates/sui-framework/packages/move-stdlib" } + +[[move.package]] +id = "Sui" +source = { git = "https://github.com/MystenLabs/sui.git", rev = "209f0da8e316", subdir = "crates/sui-framework/packages/sui-framework" } + +dependencies = [ + { id = "MoveStdlib", name = "MoveStdlib" }, +] + +[[move.package]] +id = "SuiSystem" +source = { git = "https://github.com/MystenLabs/sui.git", rev = "209f0da8e316", subdir = "crates/sui-framework/packages/sui-system" } + +dependencies = [ + { id = "MoveStdlib", name = "MoveStdlib" }, + { id = "Sui", name = "Sui" }, +] + +[move.toolchain-version] +compiler-version = "1.52.2" +edition = "2024.beta" +flavor = "sui" diff --git a/lazer/sui/Move.toml b/lazer/sui/Move.toml new file mode 100644 index 0000000..3e810d1 --- /dev/null +++ b/lazer/sui/Move.toml @@ -0,0 +1,12 @@ +[package] +name = "lazer_example" +edition = "2024.beta" + +[dependencies] + +[addresses] +lazer_example = "0x0" + +[dev-dependencies] + +[dev-addresses] diff --git a/lazer/sui/README.md b/lazer/sui/README.md new file mode 100644 index 0000000..b1d22df --- /dev/null +++ b/lazer/sui/README.md @@ -0,0 +1,31 @@ +# Pyth Lazer Sui Implementation + +**⚠️ DISCLAIMER: This is an example implementation for demonstration purposes only. It has not been audited and should be used at your own risk. Do not use this code in production without proper security review and testing.** + +A Sui Move implementation example for parsing and validating [Pyth Lazer](https://docs.pyth.network/lazer) price feed updates. This project demonstrates on-chain verification and parsing of cryptographically signed price feed data from the Pyth Network's high-frequency Lazer protocol. Look at the [`lazer_example` module](./sources/lazer_example.move) for the main implementation. + +## Prerequisites + +- [Sui CLI](https://docs.sui.io/guides/developer/getting-started/sui-install) installed +- Basic familiarity with Move programming language + +## Building and Testing the Project + +1. **Build the project**: + ```bash + sui move build + ``` + +2. **Run all tests**: +```bash +sui move test +``` + +**Run specific test**: +```bash +sui move test test_parse_and_validate_update +``` + +## Important Notes +- The `parse_and_validate_update` function uses a single hardcoded public key for signature verification. However, in a real-world scenario, the set of valid public keys may change over time, and multiple keys might be required. For production use, store the authorized public keys in the contract's configuration storage and reference them dynamically, rather than relying on a hardcoded value. +- There is no proper error handling in the `parse_and_validate_update` function and all the assertions use the same error code (0). diff --git a/lazer/sui/sources/i16.move b/lazer/sui/sources/i16.move new file mode 100644 index 0000000..df92c26 --- /dev/null +++ b/lazer/sui/sources/i16.move @@ -0,0 +1,150 @@ +/// Adopted from pyth::i64, adapted for i16 + +module lazer_example::i16; + +const MAX_POSITIVE_MAGNITUDE: u64 = (1 << 15) - 1; // 32767 +const MAX_NEGATIVE_MAGNITUDE: u64 = (1 << 15); // 32768 + +/// To consume these values, first call `get_is_negative()` to determine if the I16 +/// represents a negative or positive value. Then call `get_magnitude_if_positive()` or +/// `get_magnitude_if_negative()` to get the magnitude of the number in unsigned u64 format. +/// This API forces consumers to handle positive and negative numbers safely. +public struct I16 has copy, drop, store { + negative: bool, + magnitude: u64, +} + +public fun new(magnitude: u64, mut negative: bool): I16 { + let mut max_magnitude = MAX_POSITIVE_MAGNITUDE; + if (negative) { + max_magnitude = MAX_NEGATIVE_MAGNITUDE; + }; + assert!(magnitude <= max_magnitude, 0); //error::magnitude_too_large() + + // Ensure we have a single zero representation: (0, false). + // (0, true) is invalid. + if (magnitude == 0) { + negative = false; + }; + + I16 { + magnitude, + negative, + } +} + +public fun get_is_negative(i: &I16): bool { + i.negative +} + +public fun get_magnitude_if_positive(in: &I16): u64 { + assert!(!in.negative, 0); // error::negative_value() + in.magnitude +} + +public fun get_magnitude_if_negative(in: &I16): u64 { + assert!(in.negative, 0); //error::positive_value() + in.magnitude +} + +public fun from_u16(from: u16): I16 { + // Use the MSB to determine whether the number is negative or not. + let from_u64 = (from as u64); + let negative = (from_u64 >> 15) == 1; + let magnitude = parse_magnitude(from_u64, negative); + + new(magnitude, negative) +} + +fun parse_magnitude(from: u64, negative: bool): u64 { + // If positive, then return the input verbatim + if (!negative) { + return from + }; + + // Otherwise convert from two's complement by inverting and adding 1 + // For 16-bit numbers, we only invert the lower 16 bits + let inverted = from ^ 0xFFFF; + inverted + 1 +} + +#[test] +fun test_max_positive_magnitude() { + new(0x7FFF, false); // 32767 + assert!(&new((1<<15) - 1, false) == &from_u16(((1<<15) - 1) as u16), 1); +} + +#[test] +#[expected_failure] +fun test_magnitude_too_large_positive() { + new(0x8000, false); // 32768 +} + +#[test] +fun test_max_negative_magnitude() { + new(0x8000, true); // 32768 + assert!(&new(1<<15, true) == &from_u16((1<<15) as u16), 1); +} + +#[test] +#[expected_failure] +fun test_magnitude_too_large_negative() { + new(0x8001, true); // 32769 +} + +#[test] +fun test_from_u16_positive() { + assert!(from_u16(0x1234) == new(0x1234, false), 1); +} + +#[test] +fun test_from_u16_negative() { + assert!(from_u16(0xEDCC) == new(0x1234, true), 1); +} + +#[test] +fun test_get_is_negative() { + assert!(get_is_negative(&new(234, true)) == true, 1); + assert!(get_is_negative(&new(767, false)) == false, 1); +} + +#[test] +fun test_get_magnitude_if_positive_positive() { + assert!(get_magnitude_if_positive(&new(7686, false)) == 7686, 1); +} + +#[test] +#[expected_failure] +fun test_get_magnitude_if_positive_negative() { + assert!(get_magnitude_if_positive(&new(7686, true)) == 7686, 1); +} + +#[test] +fun test_get_magnitude_if_negative_negative() { + assert!(get_magnitude_if_negative(&new(7686, true)) == 7686, 1); +} + +#[test] +#[expected_failure] +fun test_get_magnitude_if_negative_positive() { + assert!(get_magnitude_if_negative(&new(7686, false)) == 7686, 1); +} + +#[test] +fun test_single_zero_representation() { + assert!(&new(0, true) == &new(0, false), 1); + assert!(&new(0, true) == &from_u16(0), 1); + assert!(&new(0, false) == &from_u16(0), 1); +} + +#[test] +fun test_boundary_values() { + // Test positive boundary + assert!(from_u16(0x7FFF) == new(32767, false), 1); + + // Test negative boundary + assert!(from_u16(0x8000) == new(32768, true), 1); + + // Test -1 + assert!(from_u16(0xFFFF) == new(1, true), 1); +} diff --git a/lazer/sui/sources/i64.move b/lazer/sui/sources/i64.move new file mode 100644 index 0000000..dd5eeaf --- /dev/null +++ b/lazer/sui/sources/i64.move @@ -0,0 +1,137 @@ +/// Adopted from pyth::i64 + +module lazer_example::i64; + +const MAX_POSITIVE_MAGNITUDE: u64 = (1 << 63) - 1; +const MAX_NEGATIVE_MAGNITUDE: u64 = (1 << 63); + +/// To consume these values, first call `get_is_negative()` to determine if the I64 +/// represents a negative or positive value. Then call `get_magnitude_if_positive()` or +/// `get_magnitude_if_negative()` to get the magnitude of the number in unsigned u64 format. +/// This API forces consumers to handle positive and negative numbers safely. +public struct I64 has copy, drop, store { + negative: bool, + magnitude: u64, +} + +public fun new(magnitude: u64, mut negative: bool): I64 { + let mut max_magnitude = MAX_POSITIVE_MAGNITUDE; + if (negative) { + max_magnitude = MAX_NEGATIVE_MAGNITUDE; + }; + assert!(magnitude <= max_magnitude, 0); //error::magnitude_too_large() + + + // Ensure we have a single zero representation: (0, false). + // (0, true) is invalid. + if (magnitude == 0) { + negative = false; + }; + + I64 { + magnitude, + negative, + } +} + +public fun get_is_negative(i: &I64): bool { + i.negative +} + +public fun get_magnitude_if_positive(in: &I64): u64 { + assert!(!in.negative, 0); // error::negative_value() + in.magnitude +} + +public fun get_magnitude_if_negative(in: &I64): u64 { + assert!(in.negative, 0); //error::positive_value() + in.magnitude +} + +public fun from_u64(from: u64): I64 { + // Use the MSB to determine whether the number is negative or not. + let negative = (from >> 63) == 1; + let magnitude = parse_magnitude(from, negative); + + new(magnitude, negative) +} + +fun parse_magnitude(from: u64, negative: bool): u64 { + // If positive, then return the input verbatamin + if (!negative) { + return from + }; + + // Otherwise convert from two's complement by inverting and adding 1 + let inverted = from ^ 0xFFFFFFFFFFFFFFFF; + inverted + 1 +} + +#[test] +fun test_max_positive_magnitude() { + new(0x7FFFFFFFFFFFFFFF, false); + assert!(&new(1<<63 - 1, false) == &from_u64(1<<63 - 1), 1); +} + +#[test] +#[expected_failure] +fun test_magnitude_too_large_positive() { + new(0x8000000000000000, false); +} + +#[test] +fun test_max_negative_magnitude() { + new(0x8000000000000000, true); + assert!(&new(1<<63, true) == &from_u64(1<<63), 1); +} + +#[test] +#[expected_failure] +fun test_magnitude_too_large_negative() { + new(0x8000000000000001, true); +} + +#[test] +fun test_from_u64_positive() { + assert!(from_u64(0x64673) == new(0x64673, false), 1); +} + +#[test] +fun test_from_u64_negative() { + assert!(from_u64(0xFFFFFFFFFFFEDC73) == new(0x1238D, true), 1); +} + +#[test] +fun test_get_is_negative() { + assert!(get_is_negative(&new(234, true)) == true, 1); + assert!(get_is_negative(&new(767, false)) == false, 1); +} + +#[test] +fun test_get_magnitude_if_positive_positive() { + assert!(get_magnitude_if_positive(&new(7686, false)) == 7686, 1); +} + +#[test] +#[expected_failure] +fun test_get_magnitude_if_positive_negative() { + assert!(get_magnitude_if_positive(&new(7686, true)) == 7686, 1); +} + +#[test] +fun test_get_magnitude_if_negative_negative() { + assert!(get_magnitude_if_negative(&new(7686, true)) == 7686, 1); +} + +#[test] +#[expected_failure] +fun test_get_magnitude_if_negative_positive() { + assert!(get_magnitude_if_negative(&new(7686, false)) == 7686, 1); +} + +#[test] +fun test_single_zero_representation() { + assert!(&new(0, true) == &new(0, false), 1); + assert!(&new(0, true) == &from_u64(0), 1); + assert!(&new(0, false) == &from_u64(0), 1); +} diff --git a/lazer/sui/sources/lazer_example.move b/lazer/sui/sources/lazer_example.move new file mode 100644 index 0000000..145ba44 --- /dev/null +++ b/lazer/sui/sources/lazer_example.move @@ -0,0 +1,284 @@ +module lazer_example::lazer_example; + +use lazer_example::i64::{Self, I64}; +use lazer_example::i16::{Self, I16}; +use sui::bcs; +use sui::ecdsa_k1::secp256k1_ecrecover; + +public enum Channel has copy, drop { + Invalid, + RealTime, + FixedRate50ms, + FixedRate200ms, +} + + + +public struct Update has drop { + timestamp: u64, + channel: Channel, + feeds: vector, +} + +/// The feed struct is based on the Lazer rust protocol definition defined here: +/// https://github.com/pyth-network/pyth-crosschain/blob/main/lazer/sdk/rust/protocol/src/types.rs#L10 +/// +/// Some fields in Lazer are optional, as in Lazer might return None for them due to some conditions (for example, +/// not having enough publishers to calculate the price) and that is why they are represented as Option>. +/// The first Option is for the existence of the field within the update data and the second Option is for the +/// value of the field. +public struct Feed has drop { + /// Unique identifier for the price feed (e.g., 1 for BTC/USD, 2 for ETH/USD) + feed_id: u32, + /// Current aggregate price from all publishers + price: Option>, + /// Best bid price available across all publishers + best_bid_price: Option>, + /// Best ask price available across all publishers + best_ask_price: Option>, + /// Number of publishers contributing to this price feed + publisher_count: Option, + /// Price exponent (typically negative, e.g., -8 means divide price by 10^8) + exponent: Option, + /// Confidence interval representing price uncertainty + confidence: Option>, + /// Funding rate for derivative products (e.g., perpetual futures) + funding_rate: Option>, + /// Timestamp when the funding rate was last updated + funding_timestamp: Option>, +} + +/// Parse the Lazer update message and validate the signature. +/// +/// The parsing logic is based on the Lazer rust protocol definition defined here: +/// https://github.com/pyth-network/pyth-crosschain/tree/main/lazer/sdk/rust/protocol +public fun parse_and_validate_update(update: vector): Update { + let mut cursor = bcs::new(update); + + let magic = cursor.peel_u32(); + assert!(magic == 1296547300, 0); + + let mut signature = vector::empty(); + + let mut sig_i = 0; + while (sig_i < 65) { + signature.push_back(cursor.peel_u8()); + sig_i = sig_i + 1; + }; + + let payload_len = cursor.peel_u16(); + + let payload = cursor.into_remainder_bytes(); + + assert!((payload_len as u64) == payload.length(), 0); + + // 0 stands for keccak256 hash + let pubkey = secp256k1_ecrecover(&signature, &payload, 0); + + // Lazer signer pubkey + assert!(pubkey == x"03a4380f01136eb2640f90c17e1e319e02bbafbeef2e6e67dc48af53f9827e155b", 0); + + let mut cursor = bcs::new(payload); + let payload_magic = cursor.peel_u32(); + assert!(payload_magic == 2479346549, 0); + + let timestamp = cursor.peel_u64(); + let channel_value = cursor.peel_u8(); + let channel = if (channel_value == 0) { + Channel::Invalid + } else if (channel_value == 1) { + Channel::RealTime + } else if (channel_value == 2) { + Channel::FixedRate50ms + } else if (channel_value == 3) { + Channel::FixedRate200ms + } else { + Channel::Invalid // Default to Invalid for unknown values + }; + + let mut feeds = vector::empty(); + let mut feed_i = 0; + + let feed_count = cursor.peel_u8(); + + while (feed_i < feed_count) { + let feed_id = cursor.peel_u32(); + let mut feed = Feed { + feed_id: feed_id, + price: option::none(), + best_bid_price: option::none(), + best_ask_price: option::none(), + publisher_count: option::none(), + exponent: option::none(), + confidence: option::none(), + funding_rate: option::none(), + funding_timestamp: option::none(), + }; + + let properties_count = cursor.peel_u8(); + let mut properties_i = 0; + + while (properties_i < properties_count) { + let property_id = cursor.peel_u8(); + + if (property_id == 0) { + let price = cursor.peel_u64(); + if (price != 0) { + feed.price = option::some(option::some(i64::from_u64(price))); + } else { + feed.price = option::some(option::none()); + } + } else if (property_id == 1) { + let best_bid_price = cursor.peel_u64(); + if (best_bid_price != 0) { + feed.best_bid_price = option::some(option::some(i64::from_u64(best_bid_price))); + } else { + feed.best_bid_price = option::some(option::none()); + } + } else if (property_id == 2) { + let best_ask_price = cursor.peel_u64(); + if (best_ask_price != 0) { + feed.best_ask_price = option::some(option::some(i64::from_u64(best_ask_price))); + } else { + feed.best_ask_price = option::some(option::none()); + } + } else if (property_id == 3) { + let publisher_count = cursor.peel_u16(); + feed.publisher_count = option::some(publisher_count); + } else if (property_id == 4) { + let exponent = cursor.peel_u16(); + feed.exponent = option::some(i16::from_u16(exponent)); + } else if (property_id == 5) { + let confidence = cursor.peel_u64(); + if (confidence != 0) { + feed.confidence = option::some(option::some(i64::from_u64(confidence))); + } else { + feed.confidence = option::some(option::none()); + } + } else if (property_id == 6) { + let exists = cursor.peel_u8(); + if (exists == 1) { + let funding_rate = cursor.peel_u64(); + feed.funding_rate = option::some(option::some(i64::from_u64(funding_rate))); + } else { + feed.funding_rate = option::some(option::none()); + } + } else if (property_id == 7) { + let exists = cursor.peel_u8(); + + if (exists == 1) { + let funding_timestamp = cursor.peel_u64(); + feed.funding_timestamp = option::some(option::some(funding_timestamp)); + } else { + feed.funding_timestamp = option::some(option::none()); + } + } else { + // When we have an unknown property, we do not know its length, and therefore + // we cannot ignore it and parse the next properties. + abort 0 + }; + + properties_i = properties_i + 1; + }; + + vector::push_back(&mut feeds, feed); + + feed_i = feed_i + 1; + }; + + let remaining_bytes = cursor.into_remainder_bytes(); + assert!(remaining_bytes.length() == 0, 0); + + Update { + timestamp: timestamp, + channel: channel, + feeds: feeds, + } +} + +#[test] +public fun test_parse_and_validate_update() { + /* + The test data is from the Lazer subscription: + > Request + {"subscriptionId": 1, "type": "subscribe", "priceFeedIds": [1, 2, 112], "properties": ["price", "bestBidPrice", "bestAskPrice", "exponent", "fundingRate", "fundingTimestamp"], "chains": ["leEcdsa"], "channel": "fixed_rate@200ms", "jsonBinaryEncoding": "hex"} + < Response + { + "type": "streamUpdated", + "subscriptionId": 1, + "parsed": { + "timestampUs": "1753787555800000", + "priceFeeds": [ + { + "priceFeedId": 1, + "price": "11838353875029", + "bestBidPrice": "11838047151903", + "bestAskPrice": "11839270720540", + "exponent": -8 + }, + { + "priceFeedId": 2, + "price": "382538699314", + "bestBidPrice": "382520831095", + "bestAskPrice": "382561500067", + "exponent": -8 + }, + { + "priceFeedId": 112, + "price": "118856300000000000", + "exponent": -12, + "fundingRate": 100000000, + "fundingTimestamp": 1753776000000000 + } + ] + }, + "leEcdsa": { + "encoding": "hex", + "data": "e4bd474daafa101a7cdc2f4af22f5735aa3278f7161ae15efa9eac3851ca437e322fde467c9475497e1297499344826fe1209f6de234dce35bdfab8bf6b073be12a07cb201930075d3c793c063467c0f3b0600030301000000060055a0e054c40a0000011f679842c40a0000021c94868bc40a000004f8ff0600070002000000060032521511590000000177ac04105900000002a33b71125900000004f8ff060007007000000006000038d1d42c43a60101000000000000000002000000000000000004f4ff060100e1f50500000000070100e07ecb0c3b0600" + } + } + */ + + let hex_message = x"e4bd474daafa101a7cdc2f4af22f5735aa3278f7161ae15efa9eac3851ca437e322fde467c9475497e1297499344826fe1209f6de234dce35bdfab8bf6b073be12a07cb201930075d3c793c063467c0f3b0600030301000000060055a0e054c40a0000011f679842c40a0000021c94868bc40a000004f8ff0600070002000000060032521511590000000177ac04105900000002a33b71125900000004f8ff060007007000000006000038d1d42c43a60101000000000000000002000000000000000004f4ff060100e1f50500000000070100e07ecb0c3b0600"; + + let Update { timestamp, channel, feeds } = parse_and_validate_update(hex_message); + + // If we reach this point, the function worked correctly + // (no assertion failures in parse_and_validate_update) + assert!(timestamp == 1753787555800000, 0); + assert!(channel == Channel::FixedRate200ms, 0); + assert!(vector::length(&feeds) == 3, 0); + + let feed_1 = vector::borrow(&feeds, 0); + assert!(feed_1.feed_id == 1, 0); + assert!(feed_1.price == option::some(option::some(i64::from_u64(11838353875029))), 0); + assert!(feed_1.best_bid_price == option::some(option::some(i64::from_u64(11838047151903))), 0); + assert!(feed_1.best_ask_price == option::some(option::some(i64::from_u64(11839270720540))), 0); + assert!(feed_1.exponent == option::some(i16::new(8, true)), 0); + assert!(feed_1.publisher_count == option::none(), 0); + assert!(feed_1.confidence == option::none(), 0); + assert!(feed_1.funding_rate == option::some(option::none()), 0); + assert!(feed_1.funding_timestamp == option::some(option::none()), 0); + + let feed_2 = vector::borrow(&feeds, 1); + assert!(feed_2.feed_id == 2, 0); + assert!(feed_2.price == option::some(option::some(i64::from_u64(382538699314))), 0); + assert!(feed_2.best_bid_price == option::some(option::some(i64::from_u64(382520831095))), 0); + assert!(feed_2.best_ask_price == option::some(option::some(i64::from_u64(382561500067))), 0); + assert!(feed_2.exponent == option::some(i16::new(8, true)), 0); + assert!(feed_2.publisher_count == option::none(), 0); + assert!(feed_2.confidence == option::none(), 0); + assert!(feed_2.funding_rate == option::some(option::none()), 0); + assert!(feed_2.funding_timestamp == option::some(option::none()), 0); + + let feed_3 = vector::borrow(&feeds, 2); + assert!(feed_3.feed_id == 112, 0); + assert!(feed_3.price == option::some(option::some(i64::from_u64(118856300000000000))), 0); + assert!(feed_3.best_bid_price == option::some(option::none()), 0); + assert!(feed_3.best_ask_price == option::some(option::none()), 0); + assert!(feed_3.exponent == option::some(i16::new(12, true)), 0); + assert!(feed_3.publisher_count == option::none(), 0); + assert!(feed_3.confidence == option::none(), 0); + assert!(feed_3.funding_rate == option::some(option::some(i64::from_u64(100000000))), 0); + assert!(feed_3.funding_timestamp == option::some(option::some(1753776000000000)), 0); +} From 4da4b684dc4ada8cff570eb71a35e73aacad4c3d Mon Sep 17 00:00:00 2001 From: Ali Behjati Date: Tue, 29 Jul 2025 14:59:12 +0200 Subject: [PATCH 2/4] chore: add ci --- .github/workflows/ci-lazer-sui-test.yml | 39 +++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 .github/workflows/ci-lazer-sui-test.yml diff --git a/.github/workflows/ci-lazer-sui-test.yml b/.github/workflows/ci-lazer-sui-test.yml new file mode 100644 index 0000000..c09494d --- /dev/null +++ b/.github/workflows/ci-lazer-sui-test.yml @@ -0,0 +1,39 @@ +name: Lazer Sui Move Build and Test + +on: + push: + branches: [ main ] + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + defaults: + run: + working-directory: lazer/sui + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install Sui CLI + run: | + LATEST_RELEASE=$(curl -s https://api.github.com/repos/MystenLabs/sui/releases/latest | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/') + echo "Installing Sui CLI version: $LATEST_RELEASE" + + wget -q "https://github.com/MystenLabs/sui/releases/download/$LATEST_RELEASE/sui-$LATEST_RELEASE-ubuntu-x86_64.tgz" + + tar -xzf "sui-$LATEST_RELEASE-ubuntu-x86_64.tgz" + chmod +x sui + sudo mv sui /usr/local/bin/ + + sui --version + + - name: Build Sui Move contract + run: sui move build + + - name: Run Sui Move tests + run: sui move test + + - name: Test with verbose output + run: sui move test --gas-limit 100000000 From a34e8ab75aca1ff969cf9fad0bad1f2da5ee416f Mon Sep 17 00:00:00 2001 From: Ali Behjati Date: Tue, 29 Jul 2025 15:21:00 +0200 Subject: [PATCH 3/4] chore: fix solana ci --- .../{ => fixtures}/pyth_lazer_solana_contract.so | Bin lazer/solana/tests/test1.rs | 10 +--------- 2 files changed, 1 insertion(+), 9 deletions(-) rename lazer/solana/tests/{ => fixtures}/pyth_lazer_solana_contract.so (100%) diff --git a/lazer/solana/tests/pyth_lazer_solana_contract.so b/lazer/solana/tests/fixtures/pyth_lazer_solana_contract.so similarity index 100% rename from lazer/solana/tests/pyth_lazer_solana_contract.so rename to lazer/solana/tests/fixtures/pyth_lazer_solana_contract.so diff --git a/lazer/solana/tests/test1.rs b/lazer/solana/tests/test1.rs index 29cea5d..025d44c 100644 --- a/lazer/solana/tests/test1.rs +++ b/lazer/solana/tests/test1.rs @@ -21,19 +21,11 @@ async fn test1() { env::set_var( "SBF_OUT_DIR", format!( - "{}/target/sbf-solana-solana/release", + "{}/target/sbpf-solana-solana/release", env::var("CARGO_MANIFEST_DIR").unwrap() ), ); } - std::fs::copy( - "tests/pyth_lazer_solana_contract.so", - format!( - "{}/target/sbf-solana-solana/release/pyth_lazer_solana_contract.so", - env::var("CARGO_MANIFEST_DIR").unwrap() - ), - ) - .unwrap(); println!("if add_program fails, run `cargo build-sbf` first."); let mut program_test = ProgramTest::new( "pyth_lazer_solana_example", From a74a512241f2da342369db18eec0a80159a4c28e Mon Sep 17 00:00:00 2001 From: Ali Behjati Date: Thu, 31 Jul 2025 11:13:14 +0200 Subject: [PATCH 4/4] fix: address comments --- lazer/sui/sources/lazer_example.move | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/lazer/sui/sources/lazer_example.move b/lazer/sui/sources/lazer_example.move index 145ba44..f5dd56e 100644 --- a/lazer/sui/sources/lazer_example.move +++ b/lazer/sui/sources/lazer_example.move @@ -5,6 +5,9 @@ use lazer_example::i16::{Self, I16}; use sui::bcs; use sui::ecdsa_k1::secp256k1_ecrecover; +const UPDATE_MESSAGE_MAGIC: u32 = 1296547300; +const PAYLOAD_MAGIC: u32 = 2479346549; + public enum Channel has copy, drop { Invalid, RealTime, @@ -12,8 +15,6 @@ public enum Channel has copy, drop { FixedRate200ms, } - - public struct Update has drop { timestamp: u64, channel: Channel, @@ -34,7 +35,7 @@ public struct Feed has drop { price: Option>, /// Best bid price available across all publishers best_bid_price: Option>, - /// Best ask price available across all publishers + /// Best ask price available across all publishers best_ask_price: Option>, /// Number of publishers contributing to this price feed publisher_count: Option, @@ -56,7 +57,7 @@ public fun parse_and_validate_update(update: vector): Update { let mut cursor = bcs::new(update); let magic = cursor.peel_u32(); - assert!(magic == 1296547300, 0); + assert!(magic == UPDATE_MESSAGE_MAGIC, 0); let mut signature = vector::empty(); @@ -80,7 +81,7 @@ public fun parse_and_validate_update(update: vector): Update { let mut cursor = bcs::new(payload); let payload_magic = cursor.peel_u32(); - assert!(payload_magic == 2479346549, 0); + assert!(payload_magic == PAYLOAD_MAGIC, 0); let timestamp = cursor.peel_u64(); let channel_value = cursor.peel_u8();