| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167 |
- // SPDX-License-Identifier: UNLICENSED
- pragma solidity ^0.8.13;
- import {Test, console2} from "forge-std/Test.sol";
- import {PythLazer} from "../src/PythLazer.sol";
- import {PythLazerLib} from "../src/PythLazerLib.sol";
- import {PythLazerStructs} from "../src/PythLazerStructs.sol";
- import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
- /**
- * @title PythLazerApiTest
- * @notice Integration test that calls the real Pyth Lazer API to verify parsing
- * @dev Requires running with: forge test --match-test test_parseApiResponse --ffi -vv
- */
- contract PythLazerApiTest is Test {
- PythLazer public pythLazer;
- address owner;
- address trustedSigner = 0x26FB61A864c758AE9fBA027a96010480658385B9;
- uint256 trustedSignerExpiration = 3000000000000000;
- function setUp() public {
- owner = address(1);
- PythLazer pythLazerImpl = new PythLazer();
- TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy(
- address(pythLazerImpl),
- owner,
- abi.encodeWithSelector(PythLazer.initialize.selector, owner)
- );
- pythLazer = PythLazer(address(proxy));
- vm.prank(owner);
- pythLazer.updateTrustedSigner(trustedSigner, trustedSignerExpiration);
- assert(pythLazer.isValidSigner(trustedSigner));
- }
-
- /// @notice Test parsing real API response with two different feed types
- /// @dev Feed 3: Regular price feed (no funding rate properties)
- /// @dev Feed 112: Funding rate feed (no bid/ask properties)
- function test_parseApiResponse() public {
- // Call script to fetch full JSON response from API
- string[] memory inputs = new string[](2);
- inputs[0] = "bash";
- inputs[1] = "script/fetch_pyth_payload.sh";
- string memory jsonString = string(vm.ffi(inputs));
-
- // Extract Feed 3 reference values from API's parsed field (PYTH/USD)
- int64 apiRefFeed3Price = int64(uint64(vm.parseJsonUint(jsonString, ".parsed.priceFeeds[0].price")));
- int16 apiRefFeed3Exponent = int16(vm.parseJsonInt(jsonString, ".parsed.priceFeeds[0].exponent"));
- uint64 apiRefFeed3Confidence = uint64(vm.parseJsonUint(jsonString, ".parsed.priceFeeds[0].confidence"));
- uint16 apiRefFeed3PublisherCount = uint16(vm.parseJsonUint(jsonString, ".parsed.priceFeeds[0].publisherCount"));
- int64 apiRefFeed3BestBid = int64(uint64(vm.parseJsonUint(jsonString, ".parsed.priceFeeds[0].bestBidPrice")));
- int64 apiRefFeed3BestAsk = int64(uint64(vm.parseJsonUint(jsonString, ".parsed.priceFeeds[0].bestAskPrice")));
-
- // Extract Feed 112 reference values from API's parsed field
- int64 apiRefFeed112Price = int64(uint64(vm.parseJsonUint(jsonString, ".parsed.priceFeeds[1].price")));
- int16 apiRefFeed112Exponent = int16(vm.parseJsonInt(jsonString, ".parsed.priceFeeds[1].exponent"));
- uint16 apiRefFeed112PublisherCount = uint16(vm.parseJsonUint(jsonString, ".parsed.priceFeeds[1].publisherCount"));
- int64 apiRefFeed112FundingRate = int64(vm.parseJsonInt(jsonString, ".parsed.priceFeeds[1].fundingRate"));
- uint64 apiRefFeed112FundingTimestamp = uint64(vm.parseJsonUint(jsonString, ".parsed.priceFeeds[1].fundingTimestamp"));
- uint64 apiRefFeed112FundingRateInterval = uint64(vm.parseJsonUint(jsonString, ".parsed.priceFeeds[1].fundingRateInterval"));
- bytes memory encodedUpdate = hexStringToBytes(vm.parseJsonString(jsonString, ".evm.data"));
-
- // Verify and extract payload
- (bytes memory payload, address signer) = pythLazer.verifyUpdate{value: pythLazer.verification_fee()}(encodedUpdate);
- assertEq(signer, trustedSigner, "Signer mismatch");
-
- // Parse the verified payload
- PythLazerStructs.Update memory parsedUpdate = PythLazerLib.parseUpdateFromPayload(payload);
-
- // Verify we got 2 feeds
- assertEq(parsedUpdate.feeds.length, 2, "Should have 2 feeds");
-
- // Find feeds by ID (order may vary)
- PythLazerStructs.Feed memory feed3;
- PythLazerStructs.Feed memory feed112;
- bool found3 = false;
- bool found112 = false;
-
- for (uint256 i = 0; i < parsedUpdate.feeds.length; i++) {
- if (parsedUpdate.feeds[i].feedId == 3) {
- feed3 = parsedUpdate.feeds[i];
- found3 = true;
- } else if (parsedUpdate.feeds[i].feedId == 112) {
- feed112 = parsedUpdate.feeds[i];
- found112 = true;
- }
- }
-
- assertTrue(found3, "Feed 3 not found");
- assertTrue(found112, "Feed 112 not found");
-
- // Validate Feed 3 (Regular Price Feed) - Compare against API reference
- assertEq(feed3.feedId, 3, "Feed 3: feedId mismatch");
-
- // Verify parsed values match API reference values exactly
- assertEq(PythLazerLib.getPrice(feed3), apiRefFeed3Price, "Feed 3: price mismatch");
-
- assertEq(PythLazerLib.getExponent(feed3), apiRefFeed3Exponent, "Feed 3: exponent mismatch");
-
- assertEq(PythLazerLib.getConfidence(feed3), apiRefFeed3Confidence, "Feed 3: confidence mismatch");
-
- assertEq(PythLazerLib.getPublisherCount(feed3), apiRefFeed3PublisherCount, "Feed 3: publisher count mismatch");
-
- assertEq(PythLazerLib.getBestBidPrice(feed3), apiRefFeed3BestBid, "Feed 3: best bid price mismatch");
-
- assertEq(PythLazerLib.getBestAskPrice(feed3), apiRefFeed3BestAsk, "Feed 3: best ask price mismatch");
-
- // Feed 3 should NOT have funding rate properties
- assertFalse(PythLazerLib.hasFundingRate(feed3), "Feed 3: should NOT have funding rate");
- assertFalse(PythLazerLib.hasFundingTimestamp(feed3), "Feed 3: should NOT have funding timestamp");
- assertFalse(PythLazerLib.hasFundingRateInterval(feed3), "Feed 3: should NOT have funding rate interval");
-
- // Validate Feed 112 (Funding Rate Feed) - Compare against API reference
- assertEq(feed112.feedId, 112, "Feed 112: feedId mismatch");
-
- // Verify parsed values match API reference values exactly
- assertEq(PythLazerLib.getPrice(feed112), apiRefFeed112Price, "Feed 112: price mismatch");
-
- assertEq(PythLazerLib.getExponent(feed112), apiRefFeed112Exponent, "Feed 112: exponent mismatch");
-
- assertEq(PythLazerLib.getPublisherCount(feed112), apiRefFeed112PublisherCount, "Feed 112: publisher count mismatch");
-
- assertEq(PythLazerLib.getFundingRate(feed112), apiRefFeed112FundingRate, "Feed 112: funding rate mismatch");
-
- assertEq(PythLazerLib.getFundingTimestamp(feed112), apiRefFeed112FundingTimestamp, "Feed 112: funding timestamp mismatch");
-
- assertEq(PythLazerLib.getFundingRateInterval(feed112), apiRefFeed112FundingRateInterval, "Feed 112: funding rate interval mismatch");
-
- // Feed 112 should NOT have bid/ask prices
- assertFalse(PythLazerLib.hasBestBidPrice(feed112), "Feed 112: should NOT have best bid price");
- assertFalse(PythLazerLib.hasBestAskPrice(feed112), "Feed 112: should NOT have best ask price");
- }
-
- /// @notice Convert hex string to bytes (handles 0x prefix)
- function hexStringToBytes(string memory hexStr) internal pure returns (bytes memory) {
- bytes memory hexBytes = bytes(hexStr);
- uint256 startIndex = 0;
-
- uint256 length = hexBytes.length - startIndex;
-
- // Hex string should have even length
- require(length % 2 == 0, "Invalid hex string length");
-
- bytes memory result = new bytes(length / 2);
- for (uint256 i = 0; i < length / 2; i++) {
- result[i] = bytes1(
- (hexCharToUint8(hexBytes[startIndex + 2 * i]) << 4) |
- hexCharToUint8(hexBytes[startIndex + 2 * i + 1])
- );
- }
-
- return result;
- }
-
- /// @notice Convert hex character to uint8
- function hexCharToUint8(bytes1 char) internal pure returns (uint8) {
- uint8 byteValue = uint8(char);
- if (byteValue >= uint8(bytes1('0')) && byteValue <= uint8(bytes1('9'))) {
- return byteValue - uint8(bytes1('0'));
- } else if (byteValue >= uint8(bytes1('a')) && byteValue <= uint8(bytes1('f'))) {
- return 10 + byteValue - uint8(bytes1('a'));
- } else if (byteValue >= uint8(bytes1('A')) && byteValue <= uint8(bytes1('F'))) {
- return 10 + byteValue - uint8(bytes1('A'));
- }
- revert("Invalid hex character");
- }
- }
|