PythLazerApi.t.sol 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. // SPDX-License-Identifier: UNLICENSED
  2. pragma solidity ^0.8.13;
  3. import {Test, console2} from "forge-std/Test.sol";
  4. import {PythLazer} from "../src/PythLazer.sol";
  5. import {PythLazerLib} from "../src/PythLazerLib.sol";
  6. import {PythLazerStructs} from "../src/PythLazerStructs.sol";
  7. import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
  8. /**
  9. * @title PythLazerApiTest
  10. * @notice Integration test that calls the real Pyth Lazer API to verify parsing
  11. * @dev Requires running with: forge test --match-test test_parseApiResponse --ffi -vv
  12. */
  13. contract PythLazerApiTest is Test {
  14. PythLazer public pythLazer;
  15. address owner;
  16. address trustedSigner = 0x26FB61A864c758AE9fBA027a96010480658385B9;
  17. uint256 trustedSignerExpiration = 3000000000000000;
  18. function setUp() public {
  19. owner = address(1);
  20. PythLazer pythLazerImpl = new PythLazer();
  21. TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy(
  22. address(pythLazerImpl),
  23. owner,
  24. abi.encodeWithSelector(PythLazer.initialize.selector, owner)
  25. );
  26. pythLazer = PythLazer(address(proxy));
  27. vm.prank(owner);
  28. pythLazer.updateTrustedSigner(trustedSigner, trustedSignerExpiration);
  29. assert(pythLazer.isValidSigner(trustedSigner));
  30. }
  31. /// @notice Test parsing real API response with two different feed types
  32. /// @dev Feed 3: Regular price feed (no funding rate properties)
  33. /// @dev Feed 112: Funding rate feed (no bid/ask properties)
  34. function test_parseApiResponse() public {
  35. // Call script to fetch full JSON response from API
  36. string[] memory inputs = new string[](2);
  37. inputs[0] = "bash";
  38. inputs[1] = "script/fetch_pyth_payload.sh";
  39. string memory jsonString = string(vm.ffi(inputs));
  40. // Extract Feed 3 reference values from API's parsed field (PYTH/USD)
  41. int64 apiRefFeed3Price = int64(uint64(vm.parseJsonUint(jsonString, ".parsed.priceFeeds[0].price")));
  42. int16 apiRefFeed3Exponent = int16(vm.parseJsonInt(jsonString, ".parsed.priceFeeds[0].exponent"));
  43. uint64 apiRefFeed3Confidence = uint64(vm.parseJsonUint(jsonString, ".parsed.priceFeeds[0].confidence"));
  44. uint16 apiRefFeed3PublisherCount = uint16(vm.parseJsonUint(jsonString, ".parsed.priceFeeds[0].publisherCount"));
  45. int64 apiRefFeed3BestBid = int64(uint64(vm.parseJsonUint(jsonString, ".parsed.priceFeeds[0].bestBidPrice")));
  46. int64 apiRefFeed3BestAsk = int64(uint64(vm.parseJsonUint(jsonString, ".parsed.priceFeeds[0].bestAskPrice")));
  47. // Extract Feed 112 reference values from API's parsed field
  48. int64 apiRefFeed112Price = int64(uint64(vm.parseJsonUint(jsonString, ".parsed.priceFeeds[1].price")));
  49. int16 apiRefFeed112Exponent = int16(vm.parseJsonInt(jsonString, ".parsed.priceFeeds[1].exponent"));
  50. uint16 apiRefFeed112PublisherCount = uint16(vm.parseJsonUint(jsonString, ".parsed.priceFeeds[1].publisherCount"));
  51. int64 apiRefFeed112FundingRate = int64(vm.parseJsonInt(jsonString, ".parsed.priceFeeds[1].fundingRate"));
  52. uint64 apiRefFeed112FundingTimestamp = uint64(vm.parseJsonUint(jsonString, ".parsed.priceFeeds[1].fundingTimestamp"));
  53. uint64 apiRefFeed112FundingRateInterval = uint64(vm.parseJsonUint(jsonString, ".parsed.priceFeeds[1].fundingRateInterval"));
  54. bytes memory encodedUpdate = hexStringToBytes(vm.parseJsonString(jsonString, ".evm.data"));
  55. // Verify and extract payload
  56. (bytes memory payload, address signer) = pythLazer.verifyUpdate{value: pythLazer.verification_fee()}(encodedUpdate);
  57. assertEq(signer, trustedSigner, "Signer mismatch");
  58. // Parse the verified payload
  59. PythLazerStructs.Update memory parsedUpdate = PythLazerLib.parseUpdateFromPayload(payload);
  60. // Verify we got 2 feeds
  61. assertEq(parsedUpdate.feeds.length, 2, "Should have 2 feeds");
  62. // Find feeds by ID (order may vary)
  63. PythLazerStructs.Feed memory feed3;
  64. PythLazerStructs.Feed memory feed112;
  65. bool found3 = false;
  66. bool found112 = false;
  67. for (uint256 i = 0; i < parsedUpdate.feeds.length; i++) {
  68. if (parsedUpdate.feeds[i].feedId == 3) {
  69. feed3 = parsedUpdate.feeds[i];
  70. found3 = true;
  71. } else if (parsedUpdate.feeds[i].feedId == 112) {
  72. feed112 = parsedUpdate.feeds[i];
  73. found112 = true;
  74. }
  75. }
  76. assertTrue(found3, "Feed 3 not found");
  77. assertTrue(found112, "Feed 112 not found");
  78. // Validate Feed 3 (Regular Price Feed) - Compare against API reference
  79. assertEq(feed3.feedId, 3, "Feed 3: feedId mismatch");
  80. // Verify parsed values match API reference values exactly
  81. assertEq(PythLazerLib.getPrice(feed3), apiRefFeed3Price, "Feed 3: price mismatch");
  82. assertEq(PythLazerLib.getExponent(feed3), apiRefFeed3Exponent, "Feed 3: exponent mismatch");
  83. assertEq(PythLazerLib.getConfidence(feed3), apiRefFeed3Confidence, "Feed 3: confidence mismatch");
  84. assertEq(PythLazerLib.getPublisherCount(feed3), apiRefFeed3PublisherCount, "Feed 3: publisher count mismatch");
  85. assertEq(PythLazerLib.getBestBidPrice(feed3), apiRefFeed3BestBid, "Feed 3: best bid price mismatch");
  86. assertEq(PythLazerLib.getBestAskPrice(feed3), apiRefFeed3BestAsk, "Feed 3: best ask price mismatch");
  87. // Feed 3 should NOT have funding rate properties
  88. assertFalse(PythLazerLib.hasFundingRate(feed3), "Feed 3: should NOT have funding rate");
  89. assertFalse(PythLazerLib.hasFundingTimestamp(feed3), "Feed 3: should NOT have funding timestamp");
  90. assertFalse(PythLazerLib.hasFundingRateInterval(feed3), "Feed 3: should NOT have funding rate interval");
  91. // Validate Feed 112 (Funding Rate Feed) - Compare against API reference
  92. assertEq(feed112.feedId, 112, "Feed 112: feedId mismatch");
  93. // Verify parsed values match API reference values exactly
  94. assertEq(PythLazerLib.getPrice(feed112), apiRefFeed112Price, "Feed 112: price mismatch");
  95. assertEq(PythLazerLib.getExponent(feed112), apiRefFeed112Exponent, "Feed 112: exponent mismatch");
  96. assertEq(PythLazerLib.getPublisherCount(feed112), apiRefFeed112PublisherCount, "Feed 112: publisher count mismatch");
  97. assertEq(PythLazerLib.getFundingRate(feed112), apiRefFeed112FundingRate, "Feed 112: funding rate mismatch");
  98. assertEq(PythLazerLib.getFundingTimestamp(feed112), apiRefFeed112FundingTimestamp, "Feed 112: funding timestamp mismatch");
  99. assertEq(PythLazerLib.getFundingRateInterval(feed112), apiRefFeed112FundingRateInterval, "Feed 112: funding rate interval mismatch");
  100. // Feed 112 should NOT have bid/ask prices
  101. assertFalse(PythLazerLib.hasBestBidPrice(feed112), "Feed 112: should NOT have best bid price");
  102. assertFalse(PythLazerLib.hasBestAskPrice(feed112), "Feed 112: should NOT have best ask price");
  103. }
  104. /// @notice Convert hex string to bytes (handles 0x prefix)
  105. function hexStringToBytes(string memory hexStr) internal pure returns (bytes memory) {
  106. bytes memory hexBytes = bytes(hexStr);
  107. uint256 startIndex = 0;
  108. uint256 length = hexBytes.length - startIndex;
  109. // Hex string should have even length
  110. require(length % 2 == 0, "Invalid hex string length");
  111. bytes memory result = new bytes(length / 2);
  112. for (uint256 i = 0; i < length / 2; i++) {
  113. result[i] = bytes1(
  114. (hexCharToUint8(hexBytes[startIndex + 2 * i]) << 4) |
  115. hexCharToUint8(hexBytes[startIndex + 2 * i + 1])
  116. );
  117. }
  118. return result;
  119. }
  120. /// @notice Convert hex character to uint8
  121. function hexCharToUint8(bytes1 char) internal pure returns (uint8) {
  122. uint8 byteValue = uint8(char);
  123. if (byteValue >= uint8(bytes1('0')) && byteValue <= uint8(bytes1('9'))) {
  124. return byteValue - uint8(bytes1('0'));
  125. } else if (byteValue >= uint8(bytes1('a')) && byteValue <= uint8(bytes1('f'))) {
  126. return 10 + byteValue - uint8(bytes1('a'));
  127. } else if (byteValue >= uint8(bytes1('A')) && byteValue <= uint8(bytes1('F'))) {
  128. return 10 + byteValue - uint8(bytes1('A'));
  129. }
  130. revert("Invalid hex character");
  131. }
  132. }