Skip to content

feat(lazer): add sui example #59

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jul 31, 2025
Merged
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
39 changes: 39 additions & 0 deletions .github/workflows/ci-lazer-sui-test.yml
Original file line number Diff line number Diff line change
@@ -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
10 changes: 1 addition & 9 deletions lazer/solana/tests/test1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
48 changes: 48 additions & 0 deletions lazer/sui/Move.lock
Original file line number Diff line number Diff line change
@@ -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"
12 changes: 12 additions & 0 deletions lazer/sui/Move.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[package]
name = "lazer_example"
edition = "2024.beta"

[dependencies]

[addresses]
lazer_example = "0x0"

[dev-dependencies]

[dev-addresses]
31 changes: 31 additions & 0 deletions lazer/sui/README.md
Original file line number Diff line number Diff line change
@@ -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).
150 changes: 150 additions & 0 deletions lazer/sui/sources/i16.move
Original file line number Diff line number Diff line change
@@ -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);
}
Loading