From 8c18375f425671671843326d7f3a1a6fa6fb8ce6 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 16 Dec 2025 16:00:11 +0000 Subject: [PATCH 1/3] feat(lazer/evm): update example to use new PythLazerLib struct-based parsing - Update ExampleReceiver.sol to use parseUpdateFromPayload() for high-level struct-based parsing - Add helper function pattern for memory-to-calldata conversion - Use safe getter functions (hasPrice, getPrice, etc.) for property extraction - Add utility functions: getCurrentPrice(), getSpread(), isPriceFresh(), setTargetFeedId() - Update tests to use TransparentUpgradeableProxy for PythLazer initialization - Add comprehensive test cases for struct-based parsing, fee validation, and helper functions - Update README with detailed documentation on contract architecture, tri-state property system, and integration guide - Update pyth-crosschain submodule to include PythLazerStructs.sol Co-Authored-By: aditya@dourolabs.xyz --- lazer/evm/README.md | 206 ++++++++++++++++++--------- lazer/evm/lib/pyth-crosschain | 2 +- lazer/evm/src/ExampleReceiver.sol | 190 +++++++++++++++++------- lazer/evm/test/ExampleReceiver.t.sol | 124 +++++++++++++--- 4 files changed, 378 insertions(+), 144 deletions(-) diff --git a/lazer/evm/README.md b/lazer/evm/README.md index 5c3271c5..869f386d 100644 --- a/lazer/evm/README.md +++ b/lazer/evm/README.md @@ -4,7 +4,9 @@ This directory contains Solidity smart contract examples demonstrating how to in ## What is Pyth Lazer? -Pyth Lazer is a high-performance, low-latency price feed service that provides real-time financial market data to blockchain applications. It supports multiple blockchain networks and offers both JSON and binary message formats for optimal performance. +Pyth Lazer is a high-performance, low-latency price feed protocol that provides real-time financial market data to blockchain applications. Unlike traditional pull-based oracles, Pyth Lazer uses ECDSA signatures for fast verification and delivers sub-second price updates via WebSocket streams. + +Key features of Pyth Lazer include support for multiple blockchain networks, a tri-state property system that distinguishes between present values, applicable but missing values, and not applicable properties, and support for various price feed properties including price, confidence, bid/ask prices, funding rates, and market session information. ## Prerequisites @@ -30,43 +32,65 @@ Before running these examples, make sure you have the following installed: forge build ``` +## Contract Architecture + +The example uses three main components from the Pyth Lazer SDK: + +**PythLazer.sol** is the main contract that verifies ECDSA signatures from trusted signers. It manages trusted signer keys with expiration times and collects verification fees for each update. + +**PythLazerLib.sol** is a library that provides parsing functions for Lazer payloads. It includes both low-level parsing functions like `parsePayloadHeader()` and `parseFeedHeader()`, as well as a high-level `parseUpdateFromPayload()` function that returns a structured `Update` object. + +**PythLazerStructs.sol** defines the data structures used by the library, including the `Update` struct containing timestamp, channel, and feeds array, the `Feed` struct with all price properties and a tri-state map, and enums for `Channel`, `PriceFeedProperty`, `PropertyState`, and `MarketSession`. + ## Examples -### 1. ExampleReceiver Contract (`src/ExampleReceiver.sol`) -Demonstrates how to receive and process Pyth Lazer price updates in a smart contract. +### ExampleReceiver Contract (`src/ExampleReceiver.sol`) + +This contract demonstrates the recommended approach for receiving and processing Pyth Lazer price updates using the high-level struct-based parsing. -**What it does:** -- Verifies Pyth Lazer signatures on-chain -- Parses price feed payloads to extract price data +**Key Features:** +- Verifies Pyth Lazer signatures on-chain via the PythLazer contract +- Uses `parseUpdateFromPayload()` for clean, structured parsing +- Extracts all available price feed properties using safe getter functions - Handles verification fees and refunds excess payments -- Extracts multiple price feed properties (price, timestamps, exponents, etc.) -- Filters updates by feed ID and timestamp +- Filters updates by target feed ID and timestamp freshness +- Emits events for price updates -**Key Functions:** +**Main Function:** ```solidity function updatePrice(bytes calldata update) public payable ``` -**How to run the test:** -```bash -forge test -v -``` +This function performs the following steps: +1. Pays the verification fee to PythLazer and verifies the signature +2. Parses the payload into a structured `Update` object +3. Iterates through feeds to find the target feed +4. Extracts available properties using safe getter functions like `hasPrice()` and `getPrice()` +5. Updates contract state and emits a `PriceUpdated` event -### 2. Test Suite (`test/ExampleReceiver.t.sol`) -Comprehensive test demonstrating the contract functionality with real price data. +**Helper Functions:** +- `getCurrentPrice()` - Returns the current price and exponent +- `getSpread()` - Returns the bid-ask spread +- `isPriceFresh(maxAge)` - Checks if the price is within the specified age +- `setTargetFeedId(feedId)` - Updates the target feed ID -**What it does:** -- Sets up a PythLazer contract with trusted signer -- Creates and funds test accounts -- Submits a price update with verification fee -- Validates parsed price data and fee handling +### Test Suite (`test/ExampleReceiver.t.sol`) + +Comprehensive tests demonstrating the contract functionality with real signed price data. + +**Test Cases:** +- `test_updatePrice_structBased()` - Tests the main price update flow +- `test_revert_insufficientFee()` - Verifies fee requirement +- `test_nonTargetFeed_noUpdate()` - Ensures non-target feeds don't update state +- `test_setTargetFeedId()` - Tests feed ID configuration +- `test_helperFunctions()` - Tests utility functions **How to run:** ```bash -forge test -v +forge test -vv ``` -**Expected output:** +**Expected output for the struct-based test:** - **Timestamp**: `1738270008001000` (microseconds since Unix epoch) - **Price**: `100000000` (raw price value) - **Exponent**: `-8` (price = 100000000 × 10^-8 = $1.00) @@ -74,87 +98,129 @@ forge test -v ## Understanding Price Data -Pyth Lazer prices use a fixed-point representation: -``` -actual_price = price × 10^exponent -``` +Pyth Lazer prices use a fixed-point representation where the actual price equals the raw price multiplied by 10 raised to the power of the exponent. **Example from the test:** - Raw price: `100000000` - Exponent: `-8` - Actual price: `100000000 × 10^-8 = $1.00` -### Feed Properties +### Available Feed Properties + +The `Feed` struct can contain the following properties, each with a tri-state indicating whether it's present, applicable but missing, or not applicable: -The contract can extract multiple price feed properties: +| Property | Type | Description | +|----------|------|-------------| +| Price | int64 | Main price value | +| BestBidPrice | int64 | Highest bid price in the market | +| BestAskPrice | int64 | Lowest ask price in the market | +| Exponent | int16 | Decimal exponent for price normalization | +| PublisherCount | uint16 | Number of publishers contributing to this price | +| Confidence | uint64 | Confidence interval (1 standard deviation) | +| FundingRate | int64 | Perpetual funding rate (optional) | +| FundingTimestamp | uint64 | Timestamp of funding rate (optional) | +| FundingRateInterval | uint64 | Funding rate interval in seconds (optional) | +| MarketSession | enum | Market session status (Regular, PreMarket, PostMarket, OverNight, Closed) | -- **Price**: Main price value -- **BestBidPrice**: Highest bid price in the market -- **BestAskPrice**: Lowest ask price in the market -- **Exponent**: Decimal exponent for price normalization -- **PublisherCount**: Number of publishers contributing to this price +### Tri-State Property System + +Each property in a feed has a tri-state that indicates its availability: + +- **Present**: The property has a valid value for this timestamp +- **ApplicableButMissing**: The property was requested but no value is available +- **NotApplicable**: The property was not included in this update + +Use the `has*()` functions (e.g., `hasPrice()`, `hasExponent()`) to check if a property is present before accessing it with the `get*()` functions. ## Integration Guide To integrate Pyth Lazer into your own contract: -1. **Import the required libraries:** - ```solidity - import {PythLazer} from "pyth-lazer/PythLazer.sol"; - import {PythLazerLib} from "pyth-lazer/PythLazerLib.sol"; - ``` +### Step 1: Import the required contracts -2. **Set up the PythLazer contract:** - ```solidity - PythLazer pythLazer; - constructor(address pythLazerAddress) { - pythLazer = PythLazer(pythLazerAddress); - } - ``` +```solidity +import {PythLazer} from "pyth-lazer/PythLazer.sol"; +import {PythLazerLib} from "pyth-lazer/PythLazerLib.sol"; +import {PythLazerStructs} from "pyth-lazer/PythLazerStructs.sol"; +``` -3. **Handle verification fees:** - ```solidity - uint256 verification_fee = pythLazer.verification_fee(); - require(msg.value >= verification_fee, "Insufficient fee"); - ``` +### Step 2: Store the PythLazer contract reference -4. **Verify and parse updates:** - ```solidity - (bytes memory payload,) = pythLazer.verifyUpdate{value: verification_fee}(update); - // Parse payload using PythLazerLib functions - ``` +```solidity +PythLazer public pythLazer; -## Configuration +constructor(address pythLazerAddress) { + pythLazer = PythLazer(pythLazerAddress); +} +``` -### Feed IDs +### Step 3: Verify updates and parse the payload -The example filters for feed ID `6`. To use different feeds: +```solidity +function updatePrice(bytes calldata update) public payable { + // Pay fee and verify signature + uint256 fee = pythLazer.verification_fee(); + require(msg.value >= fee, "Insufficient fee"); + (bytes memory payload, ) = pythLazer.verifyUpdate{value: fee}(update); + + // Parse using helper (converts memory to calldata) + PythLazerStructs.Update memory parsedUpdate = this.parsePayloadExternal(payload); + + // Process feeds... +} + +// Helper to convert memory bytes to calldata for the library +function parsePayloadExternal(bytes calldata payload) + external view returns (PythLazerStructs.Update memory) { + return PythLazerLib.parseUpdateFromPayload(payload); +} +``` -1. Update the feed ID check in `updatePrice()`: - ```solidity - if (feedId == YOUR_FEED_ID && _timestamp > timestamp) { - // Update logic - } - ``` +### Step 4: Extract price data using safe getters + +```solidity +for (uint256 i = 0; i < parsedUpdate.feeds.length; i++) { + PythLazerStructs.Feed memory feed = parsedUpdate.feeds[i]; + uint32 feedId = PythLazerLib.getFeedId(feed); + + if (feedId == targetFeedId) { + if (PythLazerLib.hasPrice(feed)) { + int64 price = PythLazerLib.getPrice(feed); + } + if (PythLazerLib.hasExponent(feed)) { + int16 exponent = PythLazerLib.getExponent(feed); + } + // ... extract other properties as needed + } +} +``` -2. Obtain feed IDs from the Pyth Lazer documentation or API +## Deployed Contract Addresses + +For production deployments, use the official PythLazer contract addresses. You can find the latest addresses in the [Pyth Network documentation](https://docs.pyth.network/price-feeds/contract-addresses). ## Troubleshooting ### Common Issues -1. **Build Errors**: Make sure all dependencies are installed with `forge install` +**Build Errors**: Make sure all dependencies are installed with `forge install`. If you see missing file errors, try updating the pyth-crosschain submodule: +```bash +cd lib/pyth-crosschain && git fetch origin && git checkout origin/main +``` + +**InvalidInitialization Error in Tests**: The PythLazer contract uses OpenZeppelin's upgradeable pattern. Deploy it via a TransparentUpgradeableProxy as shown in the test file. -2. **Test Failures**: Ensure you're using a compatible Foundry version and all submodules are properly initialized +**Memory to Calldata Conversion**: The `parseUpdateFromPayload()` function expects calldata bytes, but `verifyUpdate()` returns memory bytes. Use the external helper pattern shown in the example to convert between them. -3. **Gas Issues**: The contract includes gas optimization for parsing multiple feed properties +**Gas Optimization**: For gas-sensitive applications, consider using the low-level parsing functions (`parsePayloadHeader`, `parseFeedHeader`, `parseFeedProperty`) to parse only the properties you need. ## Resources - [Pyth Network Documentation](https://docs.pyth.network/) +- [Pyth Lazer Documentation](https://docs.pyth.network/lazer) - [Foundry Book](https://book.getfoundry.sh/) -- [Pyth Lazer SDK](https://github.com/pyth-network/pyth-crosschain) +- [Pyth Crosschain Repository](https://github.com/pyth-network/pyth-crosschain) ## License -This project is licensed under the Apache-2.0 license. \ No newline at end of file +This project is licensed under the Apache-2.0 license. diff --git a/lazer/evm/lib/pyth-crosschain b/lazer/evm/lib/pyth-crosschain index 5ef46e49..e1b61f1e 160000 --- a/lazer/evm/lib/pyth-crosschain +++ b/lazer/evm/lib/pyth-crosschain @@ -1 +1 @@ -Subproject commit 5ef46e4986f22faaddf45b662401178292bb2e57 +Subproject commit e1b61f1e8022e286fc688b3bcd40d8fb20cbb1f6 diff --git a/lazer/evm/src/ExampleReceiver.sol b/lazer/evm/src/ExampleReceiver.sol index 13aa6e35..ee0303d5 100644 --- a/lazer/evm/src/ExampleReceiver.sol +++ b/lazer/evm/src/ExampleReceiver.sol @@ -4,73 +4,155 @@ pragma solidity ^0.8.13; import {console} from "forge-std/console.sol"; import {PythLazer} from "pyth-lazer/PythLazer.sol"; import {PythLazerLib} from "pyth-lazer/PythLazerLib.sol"; +import {PythLazerStructs} from "pyth-lazer/PythLazerStructs.sol"; +/// @title ExampleReceiver +/// @notice Example contract demonstrating how to receive and process Pyth Lazer price updates +/// @dev This contract shows how to use PythLazerLib to parse price feed data from Pyth Lazer. +/// The recommended approach is to use the high-level parseUpdateFromPayload() function +/// which returns a structured Update object with all feeds and their properties. contract ExampleReceiver { - PythLazer pythLazer; - uint64 public price; + PythLazer public pythLazer; + + // Stored price data for a specific feed + int64 public price; uint64 public timestamp; int16 public exponent; - uint16 public publisher_count; + uint16 public publisherCount; + uint64 public confidence; + int64 public bestBidPrice; + int64 public bestAskPrice; + + // The feed ID we're tracking + uint32 public targetFeedId; + + // Events for price updates + event PriceUpdated(uint32 indexed feedId, int64 price, int16 exponent, uint64 timestamp, uint16 publisherCount); - constructor(address pythLazerAddress) { + constructor(address pythLazerAddress, uint32 _targetFeedId) { pythLazer = PythLazer(pythLazerAddress); + targetFeedId = _targetFeedId; } + /// @notice Update price from a Pyth Lazer update + /// @dev This function verifies the update signature and parses the payload using the + /// high-level struct-based approach. It extracts all available properties for the + /// target feed and stores them in contract state. + /// @param update The raw update bytes from Pyth Lazer (includes signature and payload) function updatePrice(bytes calldata update) public payable { - uint256 verification_fee = pythLazer.verification_fee(); - require(msg.value >= verification_fee, "Insufficient fee provided"); - (bytes memory payload,) = pythLazer.verifyUpdate{value: verification_fee}(update); - if (msg.value > verification_fee) { - payable(msg.sender).transfer(msg.value - verification_fee); - } + // Step 1: Pay the verification fee and verify the update signature + uint256 verificationFee = pythLazer.verification_fee(); + require(msg.value >= verificationFee, "Insufficient fee provided"); + + (bytes memory payload,) = pythLazer.verifyUpdate{value: verificationFee}(update); - (uint64 _timestamp, PythLazerLib.Channel channel, uint8 feedsLen, uint16 pos) = - PythLazerLib.parsePayloadHeader(payload); - console.log("timestamp %d", _timestamp); - console.log("channel %d", uint8(channel)); - if (channel != PythLazerLib.Channel.RealTime) { - revert("expected update from RealTime channel"); + // Refund excess payment + if (msg.value > verificationFee) { + payable(msg.sender).transfer(msg.value - verificationFee); } - console.log("feedsLen %d", feedsLen); - for (uint8 i = 0; i < feedsLen; i++) { - uint32 feedId; - uint8 num_properties; - (feedId, num_properties, pos) = PythLazerLib.parseFeedHeader(payload, pos); - console.log("feedId %d", feedId); - console.log("num_properties %d", num_properties); - for (uint8 j = 0; j < num_properties; j++) { - PythLazerLib.PriceFeedProperty property; - (property, pos) = PythLazerLib.parseFeedProperty(payload, pos); - if (property == PythLazerLib.PriceFeedProperty.Price) { - uint64 _price; - (_price, pos) = PythLazerLib.parseFeedValueUint64(payload, pos); - console.log("price %d", _price); - if (feedId == 6 && _timestamp > timestamp) { - price = _price; - timestamp = _timestamp; - } - } else if (property == PythLazerLib.PriceFeedProperty.BestBidPrice) { - uint64 _price; - (_price, pos) = PythLazerLib.parseFeedValueUint64(payload, pos); - console.log("best bid price %d", _price); - } else if (property == PythLazerLib.PriceFeedProperty.BestAskPrice) { - uint64 _price; - (_price, pos) = PythLazerLib.parseFeedValueUint64(payload, pos); - console.log("best ask price %d", _price); - } else if (property == PythLazerLib.PriceFeedProperty.Exponent) { - int16 _exponent; - (_exponent, pos) = PythLazerLib.parseFeedValueInt16(payload, pos); - console.log("exponent %d", _exponent); - exponent = _exponent; - } else if (property == PythLazerLib.PriceFeedProperty.PublisherCount) { - uint16 _publisher_count; - (_publisher_count, pos) = PythLazerLib.parseFeedValueUint16(payload, pos); - console.log("publisher count %d", _publisher_count); - publisher_count = _publisher_count; - } else { - revert("unknown property"); + + // Step 2: Parse the payload using the helper function (converts memory to calldata) + PythLazerStructs.Update memory parsedUpdate = _parsePayload(payload); + + console.log("Timestamp: %d", parsedUpdate.timestamp); + console.log("Channel: %d", uint8(parsedUpdate.channel)); + console.log("Number of feeds: %d", parsedUpdate.feeds.length); + + // Step 3: Iterate through feeds and find our target feed + for (uint256 i = 0; i < parsedUpdate.feeds.length; i++) { + PythLazerStructs.Feed memory feed = parsedUpdate.feeds[i]; + uint32 feedId = PythLazerLib.getFeedId(feed); + + console.log("Feed ID: %d", feedId); + + // Check if this is our target feed and if the timestamp is newer + if (feedId == targetFeedId && parsedUpdate.timestamp > timestamp) { + // Step 4: Use the safe getter functions to extract values + // These functions check if the property exists before returning the value + + // Price is required for most feeds + if (PythLazerLib.hasPrice(feed)) { + price = PythLazerLib.getPrice(feed); + console.log("Price: %d", uint64(price > 0 ? price : -price)); } + + // Exponent tells us how to interpret the price (e.g., -8 means divide by 10^8) + if (PythLazerLib.hasExponent(feed)) { + exponent = PythLazerLib.getExponent(feed); + console.log("Exponent: %d", uint16(exponent > 0 ? exponent : -exponent)); + } + + // Publisher count indicates data quality + if (PythLazerLib.hasPublisherCount(feed)) { + publisherCount = PythLazerLib.getPublisherCount(feed); + console.log("Publisher count: %d", publisherCount); + } + + // Confidence interval (optional) + if (PythLazerLib.hasConfidence(feed)) { + confidence = PythLazerLib.getConfidence(feed); + console.log("Confidence: %d", confidence); + } + + // Best bid/ask prices (optional, useful for spread calculation) + if (PythLazerLib.hasBestBidPrice(feed)) { + bestBidPrice = PythLazerLib.getBestBidPrice(feed); + console.log("Best bid price: %d", uint64(bestBidPrice > 0 ? bestBidPrice : -bestBidPrice)); + } + + if (PythLazerLib.hasBestAskPrice(feed)) { + bestAskPrice = PythLazerLib.getBestAskPrice(feed); + console.log("Best ask price: %d", uint64(bestAskPrice > 0 ? bestAskPrice : -bestAskPrice)); + } + + // Update timestamp + timestamp = parsedUpdate.timestamp; + + emit PriceUpdated(feedId, price, exponent, timestamp, publisherCount); } } } + + /// @notice Helper function to parse payload (converts memory bytes to calldata for library) + /// @dev This is needed because PythLazerLib.parseUpdateFromPayload expects calldata bytes, + /// but verifyUpdate returns memory bytes. This external call converts memory to calldata. + /// @param payload The payload bytes to parse + /// @return update The parsed Update struct + function _parsePayload(bytes memory payload) internal view returns (PythLazerStructs.Update memory) { + return this.parsePayloadExternal(payload); + } + + /// @notice External wrapper for parseUpdateFromPayload (enables memory to calldata conversion) + /// @dev This function is called via this.parsePayloadExternal() to convert memory to calldata + /// @param payload The payload bytes to parse + /// @return The parsed Update struct + function parsePayloadExternal(bytes calldata payload) external view returns (PythLazerStructs.Update memory) { + return PythLazerLib.parseUpdateFromPayload(payload); + } + + /// @notice Get the current price with proper decimal adjustment + /// @return The price as a signed integer + /// @return The exponent (negative means divide by 10^|exponent|) + function getCurrentPrice() external view returns (int64, int16) { + return (price, exponent); + } + + /// @notice Get the bid-ask spread + /// @return spread The spread between best ask and best bid prices + function getSpread() external view returns (int64 spread) { + return bestAskPrice - bestBidPrice; + } + + /// @notice Check if the price data is fresh (within maxAge microseconds) + /// @param maxAge Maximum age in microseconds + /// @return True if the price is fresh + function isPriceFresh(uint64 maxAge) external view returns (bool) { + return block.timestamp * 1_000_000 - timestamp <= maxAge; + } + + /// @notice Update the target feed ID + /// @param _targetFeedId The new feed ID to track + function setTargetFeedId(uint32 _targetFeedId) external { + targetFeedId = _targetFeedId; + } } diff --git a/lazer/evm/test/ExampleReceiver.t.sol b/lazer/evm/test/ExampleReceiver.t.sol index 8acc0142..1b912228 100644 --- a/lazer/evm/test/ExampleReceiver.t.sol +++ b/lazer/evm/test/ExampleReceiver.t.sol @@ -4,40 +4,126 @@ pragma solidity ^0.8.13; import {Test, console} from "forge-std/Test.sol"; import {ExampleReceiver} from "../src/ExampleReceiver.sol"; import {PythLazer} from "pyth-lazer/PythLazer.sol"; +import {PythLazerStructs} from "pyth-lazer/PythLazerStructs.sol"; +import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; contract ExampleReceiverTest is Test { - function setUp() public {} + PythLazer public pythLazer; + ExampleReceiver public receiver; + address public trustedSigner; + address public owner; + address public consumer; + uint256 public fee; - function test_1() public { - address trustedSigner = 0xb8d50f0bAE75BF6E03c104903d7C3aFc4a6596Da; - console.log("trustedSigner %s", trustedSigner); + function setUp() public { + trustedSigner = 0xb8d50f0bAE75BF6E03c104903d7C3aFc4a6596Da; + owner = makeAddr("owner"); + consumer = makeAddr("consumer"); - address lazer = makeAddr("lazer"); - PythLazer pythLazer = new PythLazer(); - pythLazer.initialize(lazer); + // Deploy PythLazer implementation and proxy + // PythLazer uses OpenZeppelin's upgradeable pattern, so we need to deploy via proxy + PythLazer pythLazerImpl = new PythLazer(); + TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy( + address(pythLazerImpl), owner, abi.encodeWithSelector(PythLazer.initialize.selector, owner) + ); + pythLazer = PythLazer(address(proxy)); - vm.prank(lazer); + // Add trusted signer + vm.prank(owner); pythLazer.updateTrustedSigner(trustedSigner, 3000000000000000); - uint256 fee = pythLazer.verification_fee(); - address consumer = makeAddr("consumer"); - vm.deal(consumer, 10 wei); + fee = pythLazer.verification_fee(); - ExampleReceiver receiver = new ExampleReceiver(address(pythLazer)); + // Fund the consumer + vm.deal(consumer, 1 ether); + } + + /// @notice Test the high-level struct-based parsing approach (updatePrice) + function test_updatePrice_structBased() public { + // Deploy receiver with target feed ID 6 + receiver = new ExampleReceiver(address(pythLazer), 6); + + // This is a real signed update for feed ID 6 with: + // - timestamp: 1738270008001000 + // - price: 100000000 (1.00 with exponent -8) + // - exponent: -8 + // - publisher_count: 1 bytes memory update = hex"2a22999a9ee4e2a3df5affd0ad8c7c46c96d3b5ef197dd653bedd8f44a4b6b69b767fbc66341e80b80acb09ead98c60d169b9a99657ebada101f447378f227bffbc69d3d01003493c7d37500062cf28659c1e801010000000605000000000005f5e10002000000000000000001000000000000000003000104fff8"; + + console.log("Testing struct-based parsing (updatePrice)"); console.logBytes(update); vm.prank(consumer); receiver.updatePrice{value: 5 * fee}(update); - assertEq(receiver.timestamp(), 1738270008001000); - assertEq(receiver.price(), 100000000); - assertEq(receiver.exponent(), -8); - assertEq(receiver.publisher_count(), 1); + // Verify the parsed values + assertEq(receiver.timestamp(), 1738270008001000, "Timestamp mismatch"); + assertEq(receiver.price(), 100000000, "Price mismatch"); + assertEq(receiver.exponent(), -8, "Exponent mismatch"); + assertEq(receiver.publisherCount(), 1, "Publisher count mismatch"); + assertEq(receiver.targetFeedId(), 6, "Target feed ID mismatch"); + + // Verify fee handling + assertEq(address(pythLazer).balance, fee, "PythLazer should have received the fee"); + assertEq(address(receiver).balance, 0, "Receiver should have no balance"); + assertEq(consumer.balance, 1 ether - fee, "Consumer should have been refunded excess"); + } + + /// @notice Test that insufficient fee reverts + function test_revert_insufficientFee() public { + receiver = new ExampleReceiver(address(pythLazer), 6); + + bytes memory update = + hex"2a22999a9ee4e2a3df5affd0ad8c7c46c96d3b5ef197dd653bedd8f44a4b6b69b767fbc66341e80b80acb09ead98c60d169b9a99657ebada101f447378f227bffbc69d3d01003493c7d37500062cf28659c1e801010000000605000000000005f5e10002000000000000000001000000000000000003000104fff8"; + + vm.prank(consumer); + vm.expectRevert("Insufficient fee provided"); + receiver.updatePrice{value: 0}(update); + } + + /// @notice Test that updates for non-target feeds don't update state + function test_nonTargetFeed_noUpdate() public { + // Deploy receiver with target feed ID 999 (not in the update) + receiver = new ExampleReceiver(address(pythLazer), 999); + + bytes memory update = + hex"2a22999a9ee4e2a3df5affd0ad8c7c46c96d3b5ef197dd653bedd8f44a4b6b69b767fbc66341e80b80acb09ead98c60d169b9a99657ebada101f447378f227bffbc69d3d01003493c7d37500062cf28659c1e801010000000605000000000005f5e10002000000000000000001000000000000000003000104fff8"; + + vm.prank(consumer); + receiver.updatePrice{value: fee}(update); + + // State should remain at default values since feed ID 6 != 999 + assertEq(receiver.timestamp(), 0, "Timestamp should be 0"); + assertEq(receiver.price(), 0, "Price should be 0"); + } + + /// @notice Test changing the target feed ID + function test_setTargetFeedId() public { + receiver = new ExampleReceiver(address(pythLazer), 6); + assertEq(receiver.targetFeedId(), 6); + + receiver.setTargetFeedId(100); + assertEq(receiver.targetFeedId(), 100); + } + + /// @notice Test helper functions + function test_helperFunctions() public { + receiver = new ExampleReceiver(address(pythLazer), 6); + + bytes memory update = + hex"2a22999a9ee4e2a3df5affd0ad8c7c46c96d3b5ef197dd653bedd8f44a4b6b69b767fbc66341e80b80acb09ead98c60d169b9a99657ebada101f447378f227bffbc69d3d01003493c7d37500062cf28659c1e801010000000605000000000005f5e10002000000000000000001000000000000000003000104fff8"; + + vm.prank(consumer); + receiver.updatePrice{value: fee}(update); + + // Test getCurrentPrice + (int64 currentPrice, int16 currentExponent) = receiver.getCurrentPrice(); + assertEq(currentPrice, 100000000); + assertEq(currentExponent, -8); - assertEq(address(pythLazer).balance, fee); - assertEq(address(receiver).balance, 0); - assertEq(consumer.balance, 10 wei - fee); + // Test getSpread (will be 0 since bid/ask not in this update) + int64 spread = receiver.getSpread(); + assertEq(spread, 0); } } From 34d9208625100ab01f3cb3001463847dfe8e3b0c Mon Sep 17 00:00:00 2001 From: Aditya Arora Date: Thu, 18 Dec 2025 11:38:38 -0500 Subject: [PATCH 2/3] update --- lazer/evm/src/ExampleReceiver.sol | 165 +++++++++------------------ lazer/evm/test/ExampleReceiver.t.sol | 66 +---------- 2 files changed, 60 insertions(+), 171 deletions(-) diff --git a/lazer/evm/src/ExampleReceiver.sol b/lazer/evm/src/ExampleReceiver.sol index ee0303d5..e3d6cf4b 100644 --- a/lazer/evm/src/ExampleReceiver.sol +++ b/lazer/evm/src/ExampleReceiver.sol @@ -7,37 +7,18 @@ import {PythLazerLib} from "pyth-lazer/PythLazerLib.sol"; import {PythLazerStructs} from "pyth-lazer/PythLazerStructs.sol"; /// @title ExampleReceiver -/// @notice Example contract demonstrating how to receive and process Pyth Lazer price updates -/// @dev This contract shows how to use PythLazerLib to parse price feed data from Pyth Lazer. -/// The recommended approach is to use the high-level parseUpdateFromPayload() function -/// which returns a structured Update object with all feeds and their properties. +/// @notice Example contract demonstrating how to parse and log Pyth Lazer price updates +/// @dev This contract shows how to use PythLazerLib helper methods (hasX/getX pattern) +/// to safely extract price feed properties from Pyth Lazer updates. contract ExampleReceiver { PythLazer public pythLazer; - // Stored price data for a specific feed - int64 public price; - uint64 public timestamp; - int16 public exponent; - uint16 public publisherCount; - uint64 public confidence; - int64 public bestBidPrice; - int64 public bestAskPrice; - - // The feed ID we're tracking - uint32 public targetFeedId; - - // Events for price updates - event PriceUpdated(uint32 indexed feedId, int64 price, int16 exponent, uint64 timestamp, uint16 publisherCount); - - constructor(address pythLazerAddress, uint32 _targetFeedId) { + constructor(address pythLazerAddress) { pythLazer = PythLazer(pythLazerAddress); - targetFeedId = _targetFeedId; } - /// @notice Update price from a Pyth Lazer update - /// @dev This function verifies the update signature and parses the payload using the - /// high-level struct-based approach. It extracts all available properties for the - /// target feed and stores them in contract state. + /// @notice Parse and log price data from a Pyth Lazer update + /// @dev Demonstrates the use of PythLazerLib helper methods to safely extract feed properties /// @param update The raw update bytes from Pyth Lazer (includes signature and payload) function updatePrice(bytes calldata update) public payable { // Step 1: Pay the verification fee and verify the update signature @@ -48,111 +29,73 @@ contract ExampleReceiver { // Refund excess payment if (msg.value > verificationFee) { - payable(msg.sender).transfer(msg.value - verificationFee); + (bool success, ) = payable(msg.sender).call{value: msg.value - verificationFee}(""); + require(success, "Refund failed"); } // Step 2: Parse the payload using the helper function (converts memory to calldata) - PythLazerStructs.Update memory parsedUpdate = _parsePayload(payload); + PythLazerStructs.Update memory parsedUpdate = this.parsePayload(payload); console.log("Timestamp: %d", parsedUpdate.timestamp); console.log("Channel: %d", uint8(parsedUpdate.channel)); console.log("Number of feeds: %d", parsedUpdate.feeds.length); - // Step 3: Iterate through feeds and find our target feed + // Step 3: Iterate through all feeds and log their properties for (uint256 i = 0; i < parsedUpdate.feeds.length; i++) { PythLazerStructs.Feed memory feed = parsedUpdate.feeds[i]; + + // Get the feed ID uint32 feedId = PythLazerLib.getFeedId(feed); + console.log("--- Feed ID: %d ---", feedId); - console.log("Feed ID: %d", feedId); - - // Check if this is our target feed and if the timestamp is newer - if (feedId == targetFeedId && parsedUpdate.timestamp > timestamp) { - // Step 4: Use the safe getter functions to extract values - // These functions check if the property exists before returning the value - - // Price is required for most feeds - if (PythLazerLib.hasPrice(feed)) { - price = PythLazerLib.getPrice(feed); - console.log("Price: %d", uint64(price > 0 ? price : -price)); - } - - // Exponent tells us how to interpret the price (e.g., -8 means divide by 10^8) - if (PythLazerLib.hasExponent(feed)) { - exponent = PythLazerLib.getExponent(feed); - console.log("Exponent: %d", uint16(exponent > 0 ? exponent : -exponent)); - } - - // Publisher count indicates data quality - if (PythLazerLib.hasPublisherCount(feed)) { - publisherCount = PythLazerLib.getPublisherCount(feed); - console.log("Publisher count: %d", publisherCount); - } - - // Confidence interval (optional) - if (PythLazerLib.hasConfidence(feed)) { - confidence = PythLazerLib.getConfidence(feed); - console.log("Confidence: %d", confidence); - } - - // Best bid/ask prices (optional, useful for spread calculation) - if (PythLazerLib.hasBestBidPrice(feed)) { - bestBidPrice = PythLazerLib.getBestBidPrice(feed); - console.log("Best bid price: %d", uint64(bestBidPrice > 0 ? bestBidPrice : -bestBidPrice)); - } - - if (PythLazerLib.hasBestAskPrice(feed)) { - bestAskPrice = PythLazerLib.getBestAskPrice(feed); - console.log("Best ask price: %d", uint64(bestAskPrice > 0 ? bestAskPrice : -bestAskPrice)); - } - - // Update timestamp - timestamp = parsedUpdate.timestamp; - - emit PriceUpdated(feedId, price, exponent, timestamp, publisherCount); + // Use hasPrice/getPrice pattern to safely extract price + if (PythLazerLib.hasPrice(feed)) { + int64 price = PythLazerLib.getPrice(feed); + console.log("Price:"); + console.logInt(price); } - } - } - /// @notice Helper function to parse payload (converts memory bytes to calldata for library) - /// @dev This is needed because PythLazerLib.parseUpdateFromPayload expects calldata bytes, - /// but verifyUpdate returns memory bytes. This external call converts memory to calldata. - /// @param payload The payload bytes to parse - /// @return update The parsed Update struct - function _parsePayload(bytes memory payload) internal view returns (PythLazerStructs.Update memory) { - return this.parsePayloadExternal(payload); - } + // Use hasExponent/getExponent pattern to get decimal places + if (PythLazerLib.hasExponent(feed)) { + int16 exponent = PythLazerLib.getExponent(feed); + console.log("Exponent:"); + console.logInt(exponent); + } - /// @notice External wrapper for parseUpdateFromPayload (enables memory to calldata conversion) - /// @dev This function is called via this.parsePayloadExternal() to convert memory to calldata - /// @param payload The payload bytes to parse - /// @return The parsed Update struct - function parsePayloadExternal(bytes calldata payload) external view returns (PythLazerStructs.Update memory) { - return PythLazerLib.parseUpdateFromPayload(payload); - } + // Use hasPublisherCount/getPublisherCount pattern for data quality + if (PythLazerLib.hasPublisherCount(feed)) { + uint16 publisherCount = PythLazerLib.getPublisherCount(feed); + console.log("Publisher count: %d", publisherCount); + } - /// @notice Get the current price with proper decimal adjustment - /// @return The price as a signed integer - /// @return The exponent (negative means divide by 10^|exponent|) - function getCurrentPrice() external view returns (int64, int16) { - return (price, exponent); - } + // Use hasConfidence/getConfidence pattern for confidence interval + if (PythLazerLib.hasConfidence(feed)) { + uint64 confidence = PythLazerLib.getConfidence(feed); + console.log("Confidence: %d", confidence); + } - /// @notice Get the bid-ask spread - /// @return spread The spread between best ask and best bid prices - function getSpread() external view returns (int64 spread) { - return bestAskPrice - bestBidPrice; - } + // Use hasBestBidPrice/getBestBidPrice pattern for bid price + if (PythLazerLib.hasBestBidPrice(feed)) { + int64 bestBidPrice = PythLazerLib.getBestBidPrice(feed); + console.log("Best bid price:"); + console.logInt(bestBidPrice); + } - /// @notice Check if the price data is fresh (within maxAge microseconds) - /// @param maxAge Maximum age in microseconds - /// @return True if the price is fresh - function isPriceFresh(uint64 maxAge) external view returns (bool) { - return block.timestamp * 1_000_000 - timestamp <= maxAge; + // Use hasBestAskPrice/getBestAskPrice pattern for ask price + if (PythLazerLib.hasBestAskPrice(feed)) { + int64 bestAskPrice = PythLazerLib.getBestAskPrice(feed); + console.log("Best ask price:"); + console.logInt(bestAskPrice); + } + } } - /// @notice Update the target feed ID - /// @param _targetFeedId The new feed ID to track - function setTargetFeedId(uint32 _targetFeedId) external { - targetFeedId = _targetFeedId; + /// @notice Parse payload (converts memory bytes to calldata for library) + /// @dev Called via this.parsePayload() to convert memory to calldata, since + /// PythLazerLib.parseUpdateFromPayload expects calldata bytes. + /// @param payload The payload bytes to parse + /// @return The parsed Update struct + function parsePayload(bytes calldata payload) external pure returns (PythLazerStructs.Update memory) { + return PythLazerLib.parseUpdateFromPayload(payload); } } diff --git a/lazer/evm/test/ExampleReceiver.t.sol b/lazer/evm/test/ExampleReceiver.t.sol index 1b912228..fd6b00da 100644 --- a/lazer/evm/test/ExampleReceiver.t.sol +++ b/lazer/evm/test/ExampleReceiver.t.sol @@ -36,13 +36,13 @@ contract ExampleReceiverTest is Test { // Fund the consumer vm.deal(consumer, 1 ether); - } - /// @notice Test the high-level struct-based parsing approach (updatePrice) - function test_updatePrice_structBased() public { - // Deploy receiver with target feed ID 6 - receiver = new ExampleReceiver(address(pythLazer), 6); + // Deploy receiver + receiver = new ExampleReceiver(address(pythLazer)); + } + /// @notice Test parsing and logging price data using PythLazerLib helper methods + function test_updatePrice_parseAndLog() public { // This is a real signed update for feed ID 6 with: // - timestamp: 1738270008001000 // - price: 100000000 (1.00 with exponent -8) @@ -51,19 +51,12 @@ contract ExampleReceiverTest is Test { bytes memory update = hex"2a22999a9ee4e2a3df5affd0ad8c7c46c96d3b5ef197dd653bedd8f44a4b6b69b767fbc66341e80b80acb09ead98c60d169b9a99657ebada101f447378f227bffbc69d3d01003493c7d37500062cf28659c1e801010000000605000000000005f5e10002000000000000000001000000000000000003000104fff8"; - console.log("Testing struct-based parsing (updatePrice)"); + console.log("Testing parse and log with PythLazerLib helper methods"); console.logBytes(update); vm.prank(consumer); receiver.updatePrice{value: 5 * fee}(update); - // Verify the parsed values - assertEq(receiver.timestamp(), 1738270008001000, "Timestamp mismatch"); - assertEq(receiver.price(), 100000000, "Price mismatch"); - assertEq(receiver.exponent(), -8, "Exponent mismatch"); - assertEq(receiver.publisherCount(), 1, "Publisher count mismatch"); - assertEq(receiver.targetFeedId(), 6, "Target feed ID mismatch"); - // Verify fee handling assertEq(address(pythLazer).balance, fee, "PythLazer should have received the fee"); assertEq(address(receiver).balance, 0, "Receiver should have no balance"); @@ -72,8 +65,6 @@ contract ExampleReceiverTest is Test { /// @notice Test that insufficient fee reverts function test_revert_insufficientFee() public { - receiver = new ExampleReceiver(address(pythLazer), 6); - bytes memory update = hex"2a22999a9ee4e2a3df5affd0ad8c7c46c96d3b5ef197dd653bedd8f44a4b6b69b767fbc66341e80b80acb09ead98c60d169b9a99657ebada101f447378f227bffbc69d3d01003493c7d37500062cf28659c1e801010000000605000000000005f5e10002000000000000000001000000000000000003000104fff8"; @@ -81,49 +72,4 @@ contract ExampleReceiverTest is Test { vm.expectRevert("Insufficient fee provided"); receiver.updatePrice{value: 0}(update); } - - /// @notice Test that updates for non-target feeds don't update state - function test_nonTargetFeed_noUpdate() public { - // Deploy receiver with target feed ID 999 (not in the update) - receiver = new ExampleReceiver(address(pythLazer), 999); - - bytes memory update = - hex"2a22999a9ee4e2a3df5affd0ad8c7c46c96d3b5ef197dd653bedd8f44a4b6b69b767fbc66341e80b80acb09ead98c60d169b9a99657ebada101f447378f227bffbc69d3d01003493c7d37500062cf28659c1e801010000000605000000000005f5e10002000000000000000001000000000000000003000104fff8"; - - vm.prank(consumer); - receiver.updatePrice{value: fee}(update); - - // State should remain at default values since feed ID 6 != 999 - assertEq(receiver.timestamp(), 0, "Timestamp should be 0"); - assertEq(receiver.price(), 0, "Price should be 0"); - } - - /// @notice Test changing the target feed ID - function test_setTargetFeedId() public { - receiver = new ExampleReceiver(address(pythLazer), 6); - assertEq(receiver.targetFeedId(), 6); - - receiver.setTargetFeedId(100); - assertEq(receiver.targetFeedId(), 100); - } - - /// @notice Test helper functions - function test_helperFunctions() public { - receiver = new ExampleReceiver(address(pythLazer), 6); - - bytes memory update = - hex"2a22999a9ee4e2a3df5affd0ad8c7c46c96d3b5ef197dd653bedd8f44a4b6b69b767fbc66341e80b80acb09ead98c60d169b9a99657ebada101f447378f227bffbc69d3d01003493c7d37500062cf28659c1e801010000000605000000000005f5e10002000000000000000001000000000000000003000104fff8"; - - vm.prank(consumer); - receiver.updatePrice{value: fee}(update); - - // Test getCurrentPrice - (int64 currentPrice, int16 currentExponent) = receiver.getCurrentPrice(); - assertEq(currentPrice, 100000000); - assertEq(currentExponent, -8); - - // Test getSpread (will be 0 since bid/ask not in this update) - int64 spread = receiver.getSpread(); - assertEq(spread, 0); - } } From 755048113af3ba03fc64ec9f0a98c9afba359c92 Mon Sep 17 00:00:00 2001 From: Aditya Arora Date: Thu, 18 Dec 2025 11:43:03 -0500 Subject: [PATCH 3/3] readme update --- lazer/evm/README.md | 98 +++++++++++++++++++++++++++++---------------- 1 file changed, 64 insertions(+), 34 deletions(-) diff --git a/lazer/evm/README.md b/lazer/evm/README.md index 869f386d..99fd6f00 100644 --- a/lazer/evm/README.md +++ b/lazer/evm/README.md @@ -46,15 +46,14 @@ The example uses three main components from the Pyth Lazer SDK: ### ExampleReceiver Contract (`src/ExampleReceiver.sol`) -This contract demonstrates the recommended approach for receiving and processing Pyth Lazer price updates using the high-level struct-based parsing. +This contract demonstrates how to receive, parse, and log Pyth Lazer price updates using the high-level struct-based parsing and the `PythLazerLib` helper methods. **Key Features:** - Verifies Pyth Lazer signatures on-chain via the PythLazer contract - Uses `parseUpdateFromPayload()` for clean, structured parsing -- Extracts all available price feed properties using safe getter functions +- Demonstrates the `hasX()`/`getX()` pattern to safely extract price feed properties - Handles verification fees and refunds excess payments -- Filters updates by target feed ID and timestamp freshness -- Emits events for price updates +- Logs all parsed price data for demonstration **Main Function:** ```solidity @@ -64,37 +63,70 @@ function updatePrice(bytes calldata update) public payable This function performs the following steps: 1. Pays the verification fee to PythLazer and verifies the signature 2. Parses the payload into a structured `Update` object -3. Iterates through feeds to find the target feed -4. Extracts available properties using safe getter functions like `hasPrice()` and `getPrice()` -5. Updates contract state and emits a `PriceUpdated` event +3. Iterates through all feeds and logs their properties +4. Uses safe getter functions like `hasPrice()` and `getPrice()` to extract values -**Helper Functions:** -- `getCurrentPrice()` - Returns the current price and exponent -- `getSpread()` - Returns the bid-ask spread -- `isPriceFresh(maxAge)` - Checks if the price is within the specified age -- `setTargetFeedId(feedId)` - Updates the target feed ID +**Using PythLazerLib Helper Methods:** + +The contract demonstrates the recommended pattern for safely extracting feed properties: + +```solidity +// Use hasPrice/getPrice pattern to safely extract price +if (PythLazerLib.hasPrice(feed)) { + int64 price = PythLazerLib.getPrice(feed); +} + +// Use hasExponent/getExponent pattern to get decimal places +if (PythLazerLib.hasExponent(feed)) { + int16 exponent = PythLazerLib.getExponent(feed); +} + +// Use hasPublisherCount/getPublisherCount pattern for data quality +if (PythLazerLib.hasPublisherCount(feed)) { + uint16 publisherCount = PythLazerLib.getPublisherCount(feed); +} + +// Use hasConfidence/getConfidence pattern for confidence interval +if (PythLazerLib.hasConfidence(feed)) { + uint64 confidence = PythLazerLib.getConfidence(feed); +} + +// Use hasBestBidPrice/getBestBidPrice pattern for bid price +if (PythLazerLib.hasBestBidPrice(feed)) { + int64 bestBidPrice = PythLazerLib.getBestBidPrice(feed); +} + +// Use hasBestAskPrice/getBestAskPrice pattern for ask price +if (PythLazerLib.hasBestAskPrice(feed)) { + int64 bestAskPrice = PythLazerLib.getBestAskPrice(feed); +} +``` ### Test Suite (`test/ExampleReceiver.t.sol`) -Comprehensive tests demonstrating the contract functionality with real signed price data. +Tests demonstrating the contract functionality with real signed price data. **Test Cases:** -- `test_updatePrice_structBased()` - Tests the main price update flow +- `test_updatePrice_parseAndLog()` - Tests parsing and logging price data - `test_revert_insufficientFee()` - Verifies fee requirement -- `test_nonTargetFeed_noUpdate()` - Ensures non-target feeds don't update state -- `test_setTargetFeedId()` - Tests feed ID configuration -- `test_helperFunctions()` - Tests utility functions **How to run:** ```bash -forge test -vv +forge test -vvv ``` -**Expected output for the struct-based test:** -- **Timestamp**: `1738270008001000` (microseconds since Unix epoch) -- **Price**: `100000000` (raw price value) -- **Exponent**: `-8` (price = 100000000 × 10^-8 = $1.00) -- **Publisher Count**: `1` +**Expected output:** +``` +Timestamp: 1738270008001000 +Channel: 1 +Number of feeds: 1 +--- Feed ID: 6 --- +Price: +100000000 +Exponent: +-8 +Publisher count: 1 +``` ## Understanding Price Data @@ -164,14 +196,14 @@ function updatePrice(bytes calldata update) public payable { (bytes memory payload, ) = pythLazer.verifyUpdate{value: fee}(update); // Parse using helper (converts memory to calldata) - PythLazerStructs.Update memory parsedUpdate = this.parsePayloadExternal(payload); + PythLazerStructs.Update memory parsedUpdate = this.parsePayload(payload); // Process feeds... } // Helper to convert memory bytes to calldata for the library -function parsePayloadExternal(bytes calldata payload) - external view returns (PythLazerStructs.Update memory) { +function parsePayload(bytes calldata payload) + external pure returns (PythLazerStructs.Update memory) { return PythLazerLib.parseUpdateFromPayload(payload); } ``` @@ -183,15 +215,13 @@ for (uint256 i = 0; i < parsedUpdate.feeds.length; i++) { PythLazerStructs.Feed memory feed = parsedUpdate.feeds[i]; uint32 feedId = PythLazerLib.getFeedId(feed); - if (feedId == targetFeedId) { - if (PythLazerLib.hasPrice(feed)) { - int64 price = PythLazerLib.getPrice(feed); - } - if (PythLazerLib.hasExponent(feed)) { - int16 exponent = PythLazerLib.getExponent(feed); - } - // ... extract other properties as needed + if (PythLazerLib.hasPrice(feed)) { + int64 price = PythLazerLib.getPrice(feed); + } + if (PythLazerLib.hasExponent(feed)) { + int16 exponent = PythLazerLib.getExponent(feed); } + // ... extract other properties as needed } ```