|
|
@@ -0,0 +1,167 @@
|
|
|
+// 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");
|
|
|
+ }
|
|
|
+}
|