Browse Source

feat: update Pulse scheduler contracts to depend on SDK interfaces

Co-Authored-By: Tejas Badadare <tejas@dourolabs.xyz>
Devin AI 6 tháng trước cách đây
mục cha
commit
b8da302c37

+ 1 - 0
pnpm-workspace.yaml

@@ -26,6 +26,7 @@ packages:
   - target_chains/ethereum/contracts
   - target_chains/ethereum/abi_generator
   - target_chains/ethereum/entropy_sdk/solidity
+  - target_chains/ethereum/pulse_sdk/solidity
   - target_chains/ethereum/sdk/js
   - target_chains/ethereum/sdk/solidity
   - target_chains/ethereum/sdk/stylus/pyth-mock-solidity

+ 26 - 3
target_chains/ethereum/abi_generator/src/index.js

@@ -29,9 +29,32 @@ function generateAbi(contracts) {
   };
 
   function findImports(path) {
-    return {
-      contents: fs.readFileSync(path).toString(),
-    };
+    try {
+      if (path.startsWith('@pythnetwork/pyth-sdk-solidity/')) {
+        const sdkPath = path.replace('@pythnetwork/pyth-sdk-solidity/', '../../../sdk/solidity/');
+        return {
+          contents: fs.readFileSync(sdkPath).toString(),
+        };
+      } else if (path.startsWith('@pythnetwork/')) {
+        const nodeModulesPath = path.replace('@pythnetwork/', '../../../node_modules/@pythnetwork/');
+        try {
+          return {
+            contents: fs.readFileSync(nodeModulesPath).toString(),
+          };
+        } catch (innerError) {
+          const localPath = path.replace('@pythnetwork/', '../');
+          return {
+            contents: fs.readFileSync(localPath).toString(),
+          };
+        }
+      }
+      return {
+        contents: fs.readFileSync(path).toString(),
+      };
+    } catch (error) {
+      console.error(`Error importing ${path}: ${error.message}`);
+      return { error: `Error importing ${path}: ${error.message}` };
+    }
   }
 
   const output = JSON.parse(

+ 0 - 140
target_chains/ethereum/contracts/contracts/pulse/IScheduler.sol

@@ -1,140 +0,0 @@
-// SPDX-License-Identifier: Apache 2
-
-pragma solidity ^0.8.0;
-
-import "@pythnetwork/pyth-sdk-solidity/IPyth.sol";
-import "@pythnetwork/pyth-sdk-solidity/PythStructs.sol";
-import "./SchedulerEvents.sol";
-import "./SchedulerState.sol";
-
-interface IScheduler is SchedulerEvents {
-    /**
-     * @notice Creates a new subscription
-     * @dev Requires msg.value to be at least the minimum balance for the subscription (calculated by getMinimumBalance()).
-     * @param subscriptionParams The parameters for the subscription
-     * @return subscriptionId The ID of the newly created subscription
-     */
-    function createSubscription(
-        SchedulerState.SubscriptionParams calldata subscriptionParams
-    ) external payable returns (uint256 subscriptionId);
-
-    /**
-     * @notice Gets a subscription's parameters and status
-     * @param subscriptionId The ID of the subscription
-     * @return params The subscription parameters
-     * @return status The subscription status
-     */
-    function getSubscription(
-        uint256 subscriptionId
-    )
-        external
-        view
-        returns (
-            SchedulerState.SubscriptionParams memory params,
-            SchedulerState.SubscriptionStatus memory status
-        );
-
-    /**
-     * @notice Updates an existing subscription
-     * @dev You can activate or deactivate a subscription by setting isActive to true or false. Reactivating a subscription
-     *      requires the subscription to hold at least the minimum balance (calculated by getMinimumBalance()).
-     * @dev Any Ether sent with this call (`msg.value`) will be added to the subscription's balance before processing the update.
-     * @param subscriptionId The ID of the subscription to update
-     * @param newSubscriptionParams The new parameters for the subscription
-     */
-    function updateSubscription(
-        uint256 subscriptionId,
-        SchedulerState.SubscriptionParams calldata newSubscriptionParams
-    ) external payable;
-
-    /**
-     * @notice Updates price feeds for a subscription.
-     * @dev The updateData must contain all price feeds for the subscription, not a subset or superset.
-     * @dev Internally, the updateData is verified using the Pyth contract and validates update conditions.
-     *      The call will only succeed if the update conditions for the subscription are met.
-     * @param subscriptionId The ID of the subscription
-     * @param updateData The price update data from Pyth
-     */
-    function updatePriceFeeds(
-        uint256 subscriptionId,
-        bytes[] calldata updateData
-    ) external;
-
-    /** @notice Returns the price of a price feed without any sanity checks.
-     * @dev This function returns the most recent price update in this contract without any recency checks.
-     * This function is unsafe as the returned price update may be arbitrarily far in the past.
-     *
-     * Users of this function should check the `publishTime` in the price to ensure that the returned price is
-     * sufficiently recent for their application. If you are considering using this function, it may be
-     * safer / easier to use `getPriceNoOlderThan`.
-     * @return prices - please read the documentation of PythStructs.Price to understand how to use this safely.
-     */
-    function getPricesUnsafe(
-        uint256 subscriptionId,
-        bytes32[] calldata priceIds
-    ) external view returns (PythStructs.Price[] memory prices);
-
-    /**
-     * @notice Returns the exponentially-weighted moving average price of a price feed without any sanity checks.
-     * @dev This function returns the same price as `getEmaPrice` in the case where the price is available.
-     * However, if the price is not recent this function returns the latest available price.
-     *
-     * The returned price can be from arbitrarily far in the past; this function makes no guarantees that
-     * the returned price is recent or useful for any particular application.
-     *
-     * Users of this function should check the `publishTime` in the price to ensure that the returned price is
-     * sufficiently recent for their application. If you are considering using this function, it may be
-     * safer / easier to use either `getEmaPrice` or `getEmaPriceNoOlderThan`.
-     * @return price - please read the documentation of PythStructs.Price to understand how to use this safely.
-     */
-    function getEmaPriceUnsafe(
-        uint256 subscriptionId,
-        bytes32[] calldata priceIds
-    ) external view returns (PythStructs.Price[] memory price);
-
-    /**
-     * @notice Adds funds to a subscription's balance
-     * @param subscriptionId The ID of the subscription
-     */
-    function addFunds(uint256 subscriptionId) external payable;
-
-    /**
-     * @notice Withdraws funds from a subscription's balance.
-     * @dev A minimum balance must be maintained for active subscriptions. To withdraw past
-     * the minimum balance limit, deactivate the subscription first.
-     * @param subscriptionId The ID of the subscription
-     * @param amount The amount to withdraw
-     */
-    function withdrawFunds(uint256 subscriptionId, uint256 amount) external;
-
-    /**
-     * @notice Returns the minimum balance an active subscription of a given size needs to hold.
-     * @param numPriceFeeds The number of price feeds in the subscription.
-     */
-    function getMinimumBalance(
-        uint8 numPriceFeeds
-    ) external view returns (uint256 minimumBalanceInWei);
-
-    /**
-     * @notice Gets all active subscriptions with their parameters, paginated.
-     * @dev This function has no access control to allow keepers to discover active subscriptions.
-     * @dev Note that the order of subscription IDs returned may not be sequential and can change
-     *      when subscriptions are deactivated or reactivated.
-     * @param startIndex The starting index within the list of active subscriptions (NOT the subscription ID).
-     * @param maxResults The maximum number of results to return starting from startIndex.
-     * @return subscriptionIds Array of active subscription IDs
-     * @return subscriptionParams Array of subscription parameters for each active subscription
-     * @return totalCount Total number of active subscriptions
-     */
-    function getActiveSubscriptions(
-        uint256 startIndex,
-        uint256 maxResults
-    )
-        external
-        view
-        returns (
-            uint256[] memory subscriptionIds,
-            SchedulerState.SubscriptionParams[] memory subscriptionParams,
-            uint256 totalCount
-        );
-}

+ 30 - 29
target_chains/ethereum/contracts/contracts/pulse/Scheduler.sol

@@ -6,9 +6,10 @@ import "@openzeppelin/contracts/utils/math/SignedMath.sol";
 import "@openzeppelin/contracts/utils/math/Math.sol";
 import "@pythnetwork/pyth-sdk-solidity/IPyth.sol";
 import "@pythnetwork/pyth-sdk-solidity/PythErrors.sol";
-import "./IScheduler.sol";
+import "@pythnetwork/pulse-sdk-solidity/IScheduler.sol";
+import "@pythnetwork/pulse-sdk-solidity/SchedulerStructs.sol";
 import "./SchedulerState.sol";
-import "./SchedulerErrors.sol";
+import "@pythnetwork/pulse-sdk-solidity/SchedulerErrors.sol";
 
 abstract contract Scheduler is IScheduler, SchedulerState {
     function _initialize(
@@ -28,7 +29,7 @@ abstract contract Scheduler is IScheduler, SchedulerState {
     }
 
     function createSubscription(
-        SubscriptionParams memory subscriptionParams
+        SchedulerStructs.SubscriptionParams memory subscriptionParams
     ) external payable override returns (uint256 subscriptionId) {
         _validateSubscriptionParams(subscriptionParams);
 
@@ -51,7 +52,7 @@ abstract contract Scheduler is IScheduler, SchedulerState {
         _state.subscriptionParams[subscriptionId] = subscriptionParams;
 
         // Initialize subscription status
-        SubscriptionStatus storage status = _state.subscriptionStatuses[
+        SchedulerStructs.SubscriptionStatus storage status = _state.subscriptionStatuses[
             subscriptionId
         ];
         status.priceLastUpdatedAt = 0;
@@ -70,12 +71,12 @@ abstract contract Scheduler is IScheduler, SchedulerState {
 
     function updateSubscription(
         uint256 subscriptionId,
-        SubscriptionParams memory newParams
+        SchedulerStructs.SubscriptionParams memory newParams
     ) external payable override onlyManager(subscriptionId) {
-        SubscriptionStatus storage currentStatus = _state.subscriptionStatuses[
+        SchedulerStructs.SubscriptionStatus storage currentStatus = _state.subscriptionStatuses[
             subscriptionId
         ];
-        SubscriptionParams storage currentParams = _state.subscriptionParams[
+        SchedulerStructs.SubscriptionParams storage currentParams = _state.subscriptionParams[
             subscriptionId
         ];
 
@@ -148,7 +149,7 @@ abstract contract Scheduler is IScheduler, SchedulerState {
      * @param params The subscription parameters to validate.
      */
     function _validateSubscriptionParams(
-        SubscriptionParams memory params
+        SchedulerStructs.SubscriptionParams memory params
     ) internal pure {
         // No zero‐feed subscriptions
         if (params.priceIds.length == 0) {
@@ -156,10 +157,10 @@ abstract contract Scheduler is IScheduler, SchedulerState {
         }
 
         // Price ID limits and uniqueness
-        if (params.priceIds.length > MAX_PRICE_IDS_PER_SUBSCRIPTION) {
+        if (params.priceIds.length > SchedulerStructs.MAX_PRICE_IDS_PER_SUBSCRIPTION) {
             revert TooManyPriceIds(
                 params.priceIds.length,
-                MAX_PRICE_IDS_PER_SUBSCRIPTION
+                SchedulerStructs.MAX_PRICE_IDS_PER_SUBSCRIPTION
             );
         }
         for (uint i = 0; i < params.priceIds.length; i++) {
@@ -171,10 +172,10 @@ abstract contract Scheduler is IScheduler, SchedulerState {
         }
 
         // Whitelist size limit and uniqueness
-        if (params.readerWhitelist.length > MAX_READER_WHITELIST_SIZE) {
+        if (params.readerWhitelist.length > SchedulerStructs.MAX_READER_WHITELIST_SIZE) {
             revert TooManyWhitelistedReaders(
                 params.readerWhitelist.length,
-                MAX_READER_WHITELIST_SIZE
+                SchedulerStructs.MAX_READER_WHITELIST_SIZE
             );
         }
         for (uint i = 0; i < params.readerWhitelist.length; i++) {
@@ -243,10 +244,10 @@ abstract contract Scheduler is IScheduler, SchedulerState {
     ) external override {
         uint256 startGas = gasleft();
 
-        SubscriptionStatus storage status = _state.subscriptionStatuses[
+        SchedulerStructs.SubscriptionStatus storage status = _state.subscriptionStatuses[
             subscriptionId
         ];
-        SubscriptionParams storage params = _state.subscriptionParams[
+        SchedulerStructs.SubscriptionParams storage params = _state.subscriptionParams[
             subscriptionId
         ];
 
@@ -277,7 +278,7 @@ abstract contract Scheduler is IScheduler, SchedulerState {
                 updateData,
                 params.priceIds,
                 0, // We enforce the past max validity ourselves in _validateShouldUpdatePrices
-                curTime + FUTURE_TIMESTAMP_MAX_VALIDITY_PERIOD
+                curTime + SchedulerStructs.FUTURE_TIMESTAMP_MAX_VALIDITY_PERIOD
             );
         status.balanceInWei -= pythFee;
         status.totalSpent += pythFee;
@@ -321,8 +322,8 @@ abstract contract Scheduler is IScheduler, SchedulerState {
      */
     function _validateShouldUpdatePrices(
         uint256 subscriptionId,
-        SubscriptionParams storage params,
-        SubscriptionStatus storage status,
+        SchedulerStructs.SubscriptionParams storage params,
+        SchedulerStructs.SubscriptionStatus storage status,
         PythStructs.PriceFeed[] memory priceFeeds
     ) internal view returns (uint256) {
         // Use the most recent timestamp, as some asset markets may be closed.
@@ -339,8 +340,8 @@ abstract contract Scheduler is IScheduler, SchedulerState {
         // Calculate the minimum acceptable timestamp (clamped at 0)
         // The maximum acceptable timestamp is enforced by the parsePriceFeedUpdatesWithSlots call
         uint256 minAllowedTimestamp = (block.timestamp >
-            PAST_TIMESTAMP_MAX_VALIDITY_PERIOD)
-            ? (block.timestamp - PAST_TIMESTAMP_MAX_VALIDITY_PERIOD)
+            SchedulerStructs.PAST_TIMESTAMP_MAX_VALIDITY_PERIOD)
+            ? (block.timestamp - SchedulerStructs.PAST_TIMESTAMP_MAX_VALIDITY_PERIOD)
             : 0;
 
         // Validate that the update timestamp is not too old
@@ -432,7 +433,7 @@ abstract contract Scheduler is IScheduler, SchedulerState {
             revert InactiveSubscription();
         }
 
-        SubscriptionParams storage params = _state.subscriptionParams[
+        SchedulerStructs.SubscriptionParams storage params = _state.subscriptionParams[
             subscriptionId
         ];
 
@@ -530,10 +531,10 @@ abstract contract Scheduler is IScheduler, SchedulerState {
         uint256 subscriptionId,
         uint256 amount
     ) external override onlyManager(subscriptionId) {
-        SubscriptionStatus storage status = _state.subscriptionStatuses[
+        SchedulerStructs.SubscriptionStatus storage status = _state.subscriptionStatuses[
             subscriptionId
         ];
-        SubscriptionParams storage params = _state.subscriptionParams[
+        SchedulerStructs.SubscriptionParams storage params = _state.subscriptionParams[
             subscriptionId
         ];
 
@@ -571,8 +572,8 @@ abstract contract Scheduler is IScheduler, SchedulerState {
         view
         override
         returns (
-            SubscriptionParams memory params,
-            SubscriptionStatus memory status
+            SchedulerStructs.SubscriptionParams memory params,
+            SchedulerStructs.SubscriptionStatus memory status
         )
     {
         return (
@@ -591,7 +592,7 @@ abstract contract Scheduler is IScheduler, SchedulerState {
         override
         returns (
             uint256[] memory subscriptionIds,
-            SubscriptionParams[] memory subscriptionParams,
+            SchedulerStructs.SubscriptionParams[] memory subscriptionParams,
             uint256 totalCount
         )
     {
@@ -599,7 +600,7 @@ abstract contract Scheduler is IScheduler, SchedulerState {
 
         // If startIndex is beyond the total count, return empty arrays
         if (startIndex >= totalCount) {
-            return (new uint256[](0), new SubscriptionParams[](0), totalCount);
+            return (new uint256[](0), new SchedulerStructs.SubscriptionParams[](0), totalCount);
         }
 
         // Calculate how many results to return (bounded by maxResults and remaining items)
@@ -610,7 +611,7 @@ abstract contract Scheduler is IScheduler, SchedulerState {
 
         // Create arrays for subscription IDs and parameters
         subscriptionIds = new uint256[](resultCount);
-        subscriptionParams = new SubscriptionParams[](resultCount);
+        subscriptionParams = new SchedulerStructs.SubscriptionParams[](resultCount);
 
         // Populate the arrays with the requested page of active subscriptions
         for (uint256 i = 0; i < resultCount; i++) {
@@ -743,12 +744,12 @@ abstract contract Scheduler is IScheduler, SchedulerState {
      * @param numPriceIds Number of price IDs being updated.
      */
     function _processFeesAndPayKeeper(
-        SubscriptionStatus storage status,
+        SchedulerStructs.SubscriptionStatus storage status,
         uint256 startGas,
         uint256 numPriceIds
     ) internal {
         // Calculate fee components
-        uint256 gasCost = (startGas - gasleft() + GAS_OVERHEAD) * tx.gasprice;
+        uint256 gasCost = (startGas - gasleft() + SchedulerStructs.GAS_OVERHEAD) * tx.gasprice;
         uint256 keeperSpecificFee = uint256(_state.singleUpdateKeeperFeeInWei) *
             numPriceIds;
         uint256 totalKeeperFee = gasCost + keeperSpecificFee;

+ 0 - 38
target_chains/ethereum/contracts/contracts/pulse/SchedulerErrors.sol

@@ -1,38 +0,0 @@
-// SPDX-License-Identifier: Apache 2
-
-pragma solidity ^0.8.0;
-
-// Authorization errors
-error Unauthorized();
-
-// Subscription state errors
-error InactiveSubscription();
-error InsufficientBalance();
-error CannotUpdatePermanentSubscription();
-
-// Price feed errors
-error InvalidPriceId(bytes32 providedPriceId, bytes32 expectedPriceId);
-error InvalidPriceIdsLength(uint256 providedLength, uint256 expectedLength);
-error EmptyPriceIds();
-error TooManyPriceIds(uint256 provided, uint256 maximum);
-error DuplicatePriceId(bytes32 priceId);
-error PriceSlotMismatch();
-
-// Update criteria errors
-error InvalidUpdateCriteria();
-error UpdateConditionsNotMet();
-error TimestampTooOld(
-    uint256 providedUpdateTimestamp,
-    uint256 currentTimestamp
-);
-error TimestampOlderThanLastUpdate(
-    uint256 providedUpdateTimestamp,
-    uint256 lastUpdatedAt
-);
-
-// Whitelist errors
-error TooManyWhitelistedReaders(uint256 provided, uint256 maximum);
-error DuplicateWhitelistAddress(address addr);
-
-// Payment errors
-error KeeperPaymentFailed();

+ 0 - 15
target_chains/ethereum/contracts/contracts/pulse/SchedulerEvents.sol

@@ -1,15 +0,0 @@
-// SPDX-License-Identifier: Apache-2.0
-pragma solidity ^0.8.0;
-
-import "./SchedulerState.sol";
-
-interface SchedulerEvents {
-    event SubscriptionCreated(
-        uint256 indexed subscriptionId,
-        address indexed manager
-    );
-    event SubscriptionUpdated(uint256 indexed subscriptionId);
-    event SubscriptionDeactivated(uint256 indexed subscriptionId);
-    event SubscriptionActivated(uint256 indexed subscriptionId);
-    event PricesUpdated(uint256 indexed subscriptionId, uint256 timestamp);
-}

+ 2 - 2
target_chains/ethereum/contracts/contracts/pulse/SchedulerGovernance.sol

@@ -2,7 +2,7 @@
 pragma solidity ^0.8.0;
 
 import "./SchedulerState.sol";
-import "./SchedulerErrors.sol";
+import "@pythnetwork/pulse-sdk-solidity/SchedulerErrors.sol";
 
 /**
  * @dev `SchedulerGovernance` defines governance capabilities for the Pulse contract.
@@ -44,7 +44,7 @@ abstract contract SchedulerGovernance is SchedulerState {
      * @dev The proposed admin accepts the admin transfer.
      */
     function acceptAdmin() external {
-        if (msg.sender != _state.proposedAdmin) revert Unauthorized();
+        if (msg.sender != _state.proposedAdmin) revert SchedulerErrors.Unauthorized();
 
         address oldAdmin = _state.admin;
         _state.admin = msg.sender;

+ 3 - 46
target_chains/ethereum/contracts/contracts/pulse/SchedulerState.sol

@@ -3,29 +3,9 @@
 pragma solidity ^0.8.0;
 
 import "@pythnetwork/pyth-sdk-solidity/PythStructs.sol";
+import "@pythnetwork/pulse-sdk-solidity/SchedulerStructs.sol";
 
 contract SchedulerState {
-    /// Maximum number of price feeds per subscription
-    uint8 public constant MAX_PRICE_IDS_PER_SUBSCRIPTION = 255;
-    /// Maximum number of addresses in the reader whitelist
-    uint8 public constant MAX_READER_WHITELIST_SIZE = 255;
-
-    /// Maximum time in the past (relative to current block timestamp)
-    /// for which a price update timestamp is considered valid
-    /// when validating the update conditions.
-    /// @dev Note: We don't use this when parsing update data from the Pyth contract
-    /// because don't want to reject update data if it contains a price from a market
-    /// that closed a few days ago, since it will contain a timestamp from the last
-    /// trading period. We enforce this value ourselves against the maximum
-    /// timestamp in the provided update data.
-    uint64 public constant PAST_TIMESTAMP_MAX_VALIDITY_PERIOD = 1 hours;
-
-    /// Maximum time in the future (relative to current block timestamp)
-    /// for which a price update timestamp is considered valid
-    uint64 public constant FUTURE_TIMESTAMP_MAX_VALIDITY_PERIOD = 10 seconds;
-    /// Fixed gas overhead component used in keeper fee calculation.
-    /// This is a rough estimate of the tx overhead for a keeper to call updatePriceFeeds.
-    uint256 public constant GAS_OVERHEAD = 30000;
 
     struct State {
         /// Monotonically increasing counter for subscription IDs
@@ -42,9 +22,9 @@ contract SchedulerState {
         /// Minimum balance required per price feed in a subscription
         uint128 minimumBalancePerFeed;
         /// Sub ID -> subscription parameters (which price feeds, when to update, etc)
-        mapping(uint256 => SubscriptionParams) subscriptionParams;
+        mapping(uint256 => SchedulerStructs.SubscriptionParams) subscriptionParams;
         /// Sub ID -> subscription status (metadata about their sub)
-        mapping(uint256 => SubscriptionStatus) subscriptionStatuses;
+        mapping(uint256 => SchedulerStructs.SubscriptionStatus) subscriptionStatuses;
         /// Sub ID -> price ID -> latest parsed price update for the subscribed feed
         mapping(uint256 => mapping(bytes32 => PythStructs.PriceFeed)) priceUpdates;
         /// Sub ID -> manager address
@@ -58,29 +38,6 @@ contract SchedulerState {
     }
     State internal _state;
 
-    struct SubscriptionParams {
-        bytes32[] priceIds;
-        address[] readerWhitelist;
-        bool whitelistEnabled;
-        bool isActive;
-        bool isPermanent;
-        UpdateCriteria updateCriteria;
-    }
-
-    struct SubscriptionStatus {
-        uint256 priceLastUpdatedAt;
-        uint256 balanceInWei;
-        uint256 totalUpdates;
-        uint256 totalSpent;
-    }
-
-    struct UpdateCriteria {
-        bool updateOnHeartbeat;
-        uint32 heartbeatSeconds;
-        bool updateOnDeviation;
-        uint32 deviationThresholdBps;
-    }
-
     /**
      * @dev Returns the minimum balance required per feed in a subscription.
      */

+ 2 - 2
target_chains/ethereum/contracts/contracts/pulse/SchedulerUpgradeable.sol

@@ -7,7 +7,7 @@ import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
 import "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol";
 import "./Scheduler.sol";
 import "./SchedulerGovernance.sol";
-import "./SchedulerErrors.sol";
+import "@pythnetwork/pulse-sdk-solidity/SchedulerErrors.sol";
 contract SchedulerUpgradeable is
     Initializable,
     Ownable2StepUpgradeable,
@@ -55,7 +55,7 @@ contract SchedulerUpgradeable is
     /// Authorize actions that both admin and owner can perform
     function _authorizeAdminAction() internal view override {
         if (msg.sender != owner() && msg.sender != _state.admin)
-            revert Unauthorized();
+            revert SchedulerErrors.Unauthorized();
     }
 
     function upgradeTo(address newImplementation) external override onlyProxy {

+ 77 - 77
target_chains/ethereum/contracts/forge-test/PulseScheduler.t.sol

@@ -6,10 +6,10 @@ import "forge-std/Test.sol";
 import "forge-std/console.sol";
 import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
 import "../contracts/pulse/SchedulerUpgradeable.sol";
-import "../contracts/pulse/IScheduler.sol";
-import "../contracts/pulse/SchedulerState.sol";
-import "../contracts/pulse/SchedulerEvents.sol";
-import "../contracts/pulse/SchedulerErrors.sol";
+import "@pythnetwork/pulse-sdk-solidity/IScheduler.sol";
+import "@pythnetwork/pulse-sdk-solidity/SchedulerStructs.sol";
+import "@pythnetwork/pulse-sdk-solidity/SchedulerEvents.sol";
+import "@pythnetwork/pulse-sdk-solidity/SchedulerErrors.sol";
 import "./utils/PulseSchedulerTestUtils.t.sol";
 import "@pythnetwork/pyth-sdk-solidity/IPyth.sol";
 
@@ -105,7 +105,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
     }
 
     function testCreateSubscription() public {
-        SchedulerState.SubscriptionParams
+        SchedulerStructs.SubscriptionParams
             memory params = createDefaultSubscriptionParams(2, address(reader));
         bytes32[] memory priceIds = params.priceIds; // Get the generated price IDs
 
@@ -125,8 +125,8 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
 
         // Verify subscription was added correctly
         (
-            SchedulerState.SubscriptionParams memory storedParams,
-            SchedulerState.SubscriptionStatus memory status
+            SchedulerStructs.SubscriptionParams memory storedParams,
+            SchedulerStructs.SubscriptionStatus memory status
         ) = scheduler.getSubscription(subscriptionId);
 
         assertEq(
@@ -184,7 +184,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
                 deviationThresholdBps: 200 // Changed from 100
             });
 
-        SchedulerState.SubscriptionParams memory newParams = SchedulerState
+        SchedulerStructs.SubscriptionParams memory newParams = SchedulerState
             .SubscriptionParams({
                 priceIds: newPriceIds,
                 readerWhitelist: newReaderWhitelist,
@@ -206,7 +206,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
         scheduler.updateSubscription(subscriptionId, newParams);
 
         // Verify subscription was updated correctly
-        (SchedulerState.SubscriptionParams memory storedParams, ) = scheduler
+        (SchedulerStructs.SubscriptionParams memory storedParams, ) = scheduler
             .getSubscription(subscriptionId);
 
         assertEq(
@@ -279,9 +279,9 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
         }
         bytes32 removedPriceId = initialPriceIds[numInitialFeeds - 1]; // The ID we removed
 
-        (SchedulerState.SubscriptionParams memory currentParams, ) = scheduler
+        (SchedulerStructs.SubscriptionParams memory currentParams, ) = scheduler
             .getSubscription(subscriptionId);
-        SchedulerState.SubscriptionParams memory newParams = currentParams; // Copy existing params
+        SchedulerStructs.SubscriptionParams memory newParams = currentParams; // Copy existing params
         newParams.priceIds = newPriceIds; // Update price IDs
 
         vm.expectEmit(); // Expect SubscriptionUpdated
@@ -294,7 +294,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
         removedIdArray[0] = removedPriceId;
         vm.expectRevert(
             abi.encodeWithSelector(
-                InvalidPriceId.selector,
+                SchedulerErrors.InvalidPriceId.selector,
                 removedPriceId,
                 bytes32(0)
             )
@@ -330,7 +330,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
 
     function testcreateSubscriptionWithInsufficientFundsReverts() public {
         uint8 numFeeds = 2;
-        SchedulerState.SubscriptionParams
+        SchedulerStructs.SubscriptionParams
             memory params = createDefaultSubscriptionParams(
                 numFeeds,
                 address(reader)
@@ -342,7 +342,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
         );
 
         // Try to add subscription with insufficient funds
-        vm.expectRevert(abi.encodeWithSelector(InsufficientBalance.selector));
+        vm.expectRevert(abi.encodeWithSelector(SchedulerErrors.InsufficientBalance.selector));
         scheduler.createSubscription{value: minimumBalance - 1 wei}(params);
     }
 
@@ -362,7 +362,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
         assertEq(activeIds[2], subId3, "Initial: ID 3 should be active");
 
         // --- Deactivate the middle subscription (ID 2) ---
-        (SchedulerState.SubscriptionParams memory params2, ) = scheduler
+        (SchedulerStructs.SubscriptionParams memory params2, ) = scheduler
             .getSubscription(subId2);
         params2.isActive = false;
         vm.expectEmit();
@@ -387,7 +387,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
         ); // ID 3 takes the place of ID 2
 
         // --- Deactivate the last subscription (ID 3, now at index 1) ---
-        (SchedulerState.SubscriptionParams memory params3, ) = scheduler
+        (SchedulerStructs.SubscriptionParams memory params3, ) = scheduler
             .getSubscription(subId3);
         params3.isActive = false;
         vm.expectEmit();
@@ -447,7 +447,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
 
         // --- Deactivate all remaining subscriptions ---
         // Deactivate ID 1 (first element)
-        (SchedulerState.SubscriptionParams memory params1, ) = scheduler
+        (SchedulerStructs.SubscriptionParams memory params1, ) = scheduler
             .getSubscription(subId1);
         params1.isActive = false;
         vm.expectEmit();
@@ -523,7 +523,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
         );
 
         // Get initial balance (which includes minimum balance)
-        (, SchedulerState.SubscriptionStatus memory initialStatus) = scheduler
+        (, SchedulerStructs.SubscriptionStatus memory initialStatus) = scheduler
             .getSubscription(subscriptionId);
         uint256 initialBalance = initialStatus.balanceInWei;
 
@@ -532,7 +532,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
         scheduler.addFunds{value: fundAmount}(subscriptionId);
 
         // Verify funds were added
-        (, SchedulerState.SubscriptionStatus memory status) = scheduler
+        (, SchedulerStructs.SubscriptionStatus memory status) = scheduler
             .getSubscription(subscriptionId);
 
         assertEq(
@@ -548,7 +548,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
             scheduler,
             address(reader)
         );
-        (SchedulerState.SubscriptionParams memory params, ) = scheduler
+        (SchedulerStructs.SubscriptionParams memory params, ) = scheduler
             .getSubscription(subscriptionId);
         uint256 minimumBalance = scheduler.getMinimumBalance(
             uint8(params.priceIds.length)
@@ -565,7 +565,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
         scheduler.withdrawFunds(subscriptionId, extraFunds);
 
         // Verify funds were withdrawn
-        (, SchedulerState.SubscriptionStatus memory status) = scheduler
+        (, SchedulerStructs.SubscriptionStatus memory status) = scheduler
             .getSubscription(subscriptionId);
 
         assertEq(
@@ -580,7 +580,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
         );
 
         // Try to withdraw below minimum balance
-        vm.expectRevert(abi.encodeWithSelector(InsufficientBalance.selector));
+        vm.expectRevert(abi.encodeWithSelector(SchedulerErrors.InsufficientBalance.selector));
         scheduler.withdrawFunds(subscriptionId, 1 wei);
 
         // Deactivate subscription
@@ -607,7 +607,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
         );
 
         // Verify subscription was created as non-permanent initially
-        (SchedulerState.SubscriptionParams memory params, ) = scheduler
+        (SchedulerStructs.SubscriptionParams memory params, ) = scheduler
             .getSubscription(subscriptionId);
         assertFalse(params.isPermanent, "Should not be permanent initially");
 
@@ -616,7 +616,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
         scheduler.updateSubscription(subscriptionId, params);
 
         // Verify subscription is now permanent
-        (SchedulerState.SubscriptionParams memory storedParams, ) = scheduler
+        (SchedulerStructs.SubscriptionParams memory storedParams, ) = scheduler
             .getSubscription(subscriptionId);
         assertTrue(
             storedParams.isPermanent,
@@ -624,11 +624,11 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
         );
 
         // Test 1: Cannot disable isPermanent flag
-        SchedulerState.SubscriptionParams memory updatedParams = storedParams;
+        SchedulerStructs.SubscriptionParams memory updatedParams = storedParams;
         updatedParams.isPermanent = false;
 
         vm.expectRevert(
-            abi.encodeWithSelector(CannotUpdatePermanentSubscription.selector)
+            abi.encodeWithSelector(SchedulerErrors.CannotUpdatePermanentSubscription.selector)
         );
         scheduler.updateSubscription(subscriptionId, updatedParams);
 
@@ -643,7 +643,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
         updatedParams.priceIds = reducedPriceIds;
 
         vm.expectRevert(
-            abi.encodeWithSelector(CannotUpdatePermanentSubscription.selector)
+            abi.encodeWithSelector(SchedulerErrors.CannotUpdatePermanentSubscription.selector)
         );
         scheduler.updateSubscription(subscriptionId, updatedParams);
 
@@ -656,7 +656,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
         scheduler.addFunds{value: extraFunds}(subscriptionId);
 
         vm.expectRevert(
-            abi.encodeWithSelector(CannotUpdatePermanentSubscription.selector)
+            abi.encodeWithSelector(SchedulerErrors.CannotUpdatePermanentSubscription.selector)
         );
         scheduler.withdrawFunds(subscriptionId, 0.1 ether);
 
@@ -674,7 +674,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
         updatedParams.priceIds = expandedPriceIds;
 
         vm.expectRevert(
-            abi.encodeWithSelector(CannotUpdatePermanentSubscription.selector)
+            abi.encodeWithSelector(SchedulerErrors.CannotUpdatePermanentSubscription.selector)
         );
         scheduler.updateSubscription(subscriptionId, updatedParams);
 
@@ -692,7 +692,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
             storedParams.updateCriteria.heartbeatSeconds +
             60;
         vm.expectRevert(
-            abi.encodeWithSelector(CannotUpdatePermanentSubscription.selector)
+            abi.encodeWithSelector(SchedulerErrors.CannotUpdatePermanentSubscription.selector)
         );
         scheduler.updateSubscription(subscriptionId, updatedParams);
 
@@ -700,7 +700,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
         updatedParams = storedParams;
         updatedParams.whitelistEnabled = !storedParams.whitelistEnabled;
         vm.expectRevert(
-            abi.encodeWithSelector(CannotUpdatePermanentSubscription.selector)
+            abi.encodeWithSelector(SchedulerErrors.CannotUpdatePermanentSubscription.selector)
         );
         scheduler.updateSubscription(subscriptionId, updatedParams);
 
@@ -715,7 +715,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
         expandedWhitelist[storedParams.readerWhitelist.length] = address(0x456);
         updatedParams.readerWhitelist = expandedWhitelist;
         vm.expectRevert(
-            abi.encodeWithSelector(CannotUpdatePermanentSubscription.selector)
+            abi.encodeWithSelector(SchedulerErrors.CannotUpdatePermanentSubscription.selector)
         );
         scheduler.updateSubscription(subscriptionId, updatedParams);
 
@@ -742,7 +742,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
         updatedParams = storedParams;
         updatedParams.isActive = false;
         vm.expectRevert(
-            abi.encodeWithSelector(CannotUpdatePermanentSubscription.selector)
+            abi.encodeWithSelector(SchedulerErrors.CannotUpdatePermanentSubscription.selector)
         );
         scheduler.updateSubscription(subscriptionId, updatedParams);
     }
@@ -755,7 +755,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
         );
 
         // Verify it's not permanent
-        (SchedulerState.SubscriptionParams memory params, ) = scheduler
+        (SchedulerStructs.SubscriptionParams memory params, ) = scheduler
             .getSubscription(subscriptionId);
 
         assertFalse(
@@ -774,7 +774,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
         // Verify we can't make it non-permanent again
         params.isPermanent = false;
         vm.expectRevert(
-            abi.encodeWithSelector(CannotUpdatePermanentSubscription.selector)
+            abi.encodeWithSelector(SchedulerErrors.CannotUpdatePermanentSubscription.selector)
         );
         scheduler.updateSubscription(subscriptionId, params);
     }
@@ -787,7 +787,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
         );
 
         // Get initial balance
-        (, SchedulerState.SubscriptionStatus memory initialStatus) = scheduler
+        (, SchedulerStructs.SubscriptionStatus memory initialStatus) = scheduler
             .getSubscription(subscriptionId);
         uint256 initialBalance = initialStatus.balanceInWei;
 
@@ -800,7 +800,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
         scheduler.addFunds{value: fundAmount}(subscriptionId);
 
         // Verify funds were added
-        (, SchedulerState.SubscriptionStatus memory status) = scheduler
+        (, SchedulerStructs.SubscriptionStatus memory status) = scheduler
             .getSubscription(subscriptionId);
 
         assertEq(
@@ -841,7 +841,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
         scheduler.updatePriceFeeds(subscriptionId, updateData1);
 
         // Verify first update
-        (, SchedulerState.SubscriptionStatus memory status1) = scheduler
+        (, SchedulerStructs.SubscriptionStatus memory status1) = scheduler
             .getSubscription(subscriptionId);
         assertEq(
             status1.priceLastUpdatedAt,
@@ -892,7 +892,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
         scheduler.updatePriceFeeds(subscriptionId, updateData2);
 
         // Verify second update
-        (, SchedulerState.SubscriptionStatus memory status2) = scheduler
+        (, SchedulerStructs.SubscriptionStatus memory status2) = scheduler
             .getSubscription(subscriptionId);
         assertEq(
             status2.priceLastUpdatedAt,
@@ -931,7 +931,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
         );
 
         // Prepare update data
-        (SchedulerState.SubscriptionParams memory params, ) = scheduler
+        (SchedulerStructs.SubscriptionParams memory params, ) = scheduler
             .getSubscription(subscriptionId);
         (
             PythStructs.PriceFeed[] memory priceFeeds,
@@ -947,7 +947,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
 
         // Get state before
         uint256 pusherBalanceBefore = pusher.balance;
-        (, SchedulerState.SubscriptionStatus memory statusBefore) = scheduler
+        (, SchedulerStructs.SubscriptionStatus memory statusBefore) = scheduler
             .getSubscription(subscriptionId);
         console.log(
             "Subscription balance before update:",
@@ -959,7 +959,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
         scheduler.updatePriceFeeds(subscriptionId, updateData);
 
         // Get state after
-        (, SchedulerState.SubscriptionStatus memory statusAfter) = scheduler
+        (, SchedulerStructs.SubscriptionStatus memory statusAfter) = scheduler
             .getSubscription(subscriptionId);
 
         // Calculate total fee deducted from subscription
@@ -1042,7 +1042,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
         scheduler.addFunds{value: fundAmount}(subscriptionId);
 
         // Get and print the subscription balance before attempting the update
-        (, SchedulerState.SubscriptionStatus memory status) = scheduler
+        (, SchedulerStructs.SubscriptionStatus memory status) = scheduler
             .getSubscription(subscriptionId);
         console.log(
             "Subscription balance before update:",
@@ -1056,7 +1056,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
         );
 
         // Expect revert due to insufficient balance for total fee
-        vm.expectRevert(abi.encodeWithSelector(InsufficientBalance.selector));
+        vm.expectRevert(abi.encodeWithSelector(SchedulerErrors.InsufficientBalance.selector));
         vm.prank(pusher);
         scheduler.updatePriceFeeds(subscriptionId, updateData);
     }
@@ -1239,7 +1239,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
         bytes[] memory updateData = createMockUpdateData(priceFeeds);
 
         // Expect revert with PriceSlotMismatch error
-        vm.expectRevert(abi.encodeWithSelector(PriceSlotMismatch.selector));
+        vm.expectRevert(abi.encodeWithSelector(SchedulerErrors.PriceSlotMismatch.selector));
 
         // Attempt to update price feeds
         vm.prank(pusher);
@@ -1255,8 +1255,8 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
             address(reader)
         );
         (
-            SchedulerState.SubscriptionParams memory currentParams,
-            SchedulerState.SubscriptionStatus memory initialStatus
+            SchedulerStructs.SubscriptionParams memory currentParams,
+            SchedulerStructs.SubscriptionStatus memory initialStatus
         ) = scheduler.getSubscription(subscriptionId);
         uint256 initialMinimumBalance = scheduler.getMinimumBalance(
             initialNumFeeds
@@ -1269,12 +1269,12 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
 
         // Prepare new params with more feeds (4)
         uint8 newNumFeeds = 4;
-        SchedulerState.SubscriptionParams memory newParams = currentParams;
+        SchedulerStructs.SubscriptionParams memory newParams = currentParams;
         newParams.priceIds = createPriceIds(newNumFeeds); // Increase feeds
         newParams.isActive = true; // Keep it active
 
         // Action 1: Try to update with insufficient funds
-        vm.expectRevert(abi.encodeWithSelector(InsufficientBalance.selector));
+        vm.expectRevert(abi.encodeWithSelector(SchedulerErrors.InsufficientBalance.selector));
         scheduler.updateSubscription(subscriptionId, newParams);
 
         // Action 2: Supply enough funds to the updateSubscription call to meet the new minimum balance
@@ -1287,7 +1287,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
         );
 
         // Verification 2: Update should now succeed
-        (SchedulerState.SubscriptionParams memory updatedParams, ) = scheduler
+        (SchedulerStructs.SubscriptionParams memory updatedParams, ) = scheduler
             .getSubscription(subscriptionId);
         assertEq(
             updatedParams.priceIds.length,
@@ -1307,10 +1307,10 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
         // Prepare params to add feeds (4) but also deactivate
         uint8 newNumFeeds_deact = 4;
         (
-            SchedulerState.SubscriptionParams memory currentParams_deact,
+            SchedulerStructs.SubscriptionParams memory currentParams_deact,
 
         ) = scheduler.getSubscription(subId_deact);
-        SchedulerState.SubscriptionParams
+        SchedulerStructs.SubscriptionParams
             memory newParams_deact = currentParams_deact;
         newParams_deact.priceIds = createPriceIds(newNumFeeds_deact);
         newParams_deact.isActive = false; // Deactivate
@@ -1320,7 +1320,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
 
         // Verification 3: Subscription should be inactive and have 4 feeds
         (
-            SchedulerState.SubscriptionParams memory updatedParams_deact,
+            SchedulerStructs.SubscriptionParams memory updatedParams_deact,
 
         ) = scheduler.getSubscription(subId_deact);
         assertFalse(
@@ -1363,7 +1363,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
         }
 
         // Check that balance is now below minimum for 1 feed
-        (, SchedulerState.SubscriptionStatus memory status_reduce) = scheduler
+        (, SchedulerStructs.SubscriptionStatus memory status_reduce) = scheduler
             .getSubscription(subId_reduce);
         uint256 minBalanceForOneFeed = scheduler.getMinimumBalance(1);
         assertTrue(
@@ -1373,16 +1373,16 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
 
         // Prepare params to reduce feeds from 2 to 1
         (
-            SchedulerState.SubscriptionParams memory currentParams_reduce,
+            SchedulerStructs.SubscriptionParams memory currentParams_reduce,
 
         ) = scheduler.getSubscription(subId_reduce);
-        SchedulerState.SubscriptionParams
+        SchedulerStructs.SubscriptionParams
             memory newParams_reduce = currentParams_reduce;
         newParams_reduce.priceIds = new bytes32[](1);
         newParams_reduce.priceIds[0] = currentParams_reduce.priceIds[0];
 
         // Action 4: Update should fail due to insufficient balance
-        vm.expectRevert(abi.encodeWithSelector(InsufficientBalance.selector));
+        vm.expectRevert(abi.encodeWithSelector(SchedulerErrors.InsufficientBalance.selector));
         scheduler.updateSubscription(subId_reduce, newParams_reduce);
 
         // Add funds to cover minimum balance for 1 feed
@@ -1398,7 +1398,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
 
         // Verify the subscription now has 1 feed
         (
-            SchedulerState.SubscriptionParams memory updatedParams_reduce,
+            SchedulerStructs.SubscriptionParams memory updatedParams_reduce,
 
         ) = scheduler.getSubscription(subId_reduce);
         assertEq(
@@ -1505,7 +1505,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
         );
 
         // Get params and modify them
-        (SchedulerState.SubscriptionParams memory params, ) = scheduler
+        (SchedulerStructs.SubscriptionParams memory params, ) = scheduler
             .getSubscription(subscriptionId);
         params.whitelistEnabled = false;
         params.readerWhitelist = new address[](0);
@@ -1551,7 +1551,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
         scheduler.addFunds{value: 1 ether}(subscriptionId);
 
         // Get the price IDs from the created subscription
-        (SchedulerState.SubscriptionParams memory params, ) = scheduler
+        (SchedulerStructs.SubscriptionParams memory params, ) = scheduler
             .getSubscription(subscriptionId);
         bytes32[] memory priceIds = params.priceIds;
 
@@ -1572,7 +1572,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
         // Try to access from a non-whitelisted address (should fail)
         vm.startPrank(address(0xdead));
         bytes32[] memory emptyPriceIds = new bytes32[](0);
-        vm.expectRevert(abi.encodeWithSelector(Unauthorized.selector));
+        vm.expectRevert(abi.encodeWithSelector(SchedulerErrors.Unauthorized.selector));
         scheduler.getPricesUnsafe(subscriptionId, emptyPriceIds);
         vm.stopPrank();
 
@@ -1683,7 +1683,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
                 deviationThresholdBps: 100
             });
 
-        SchedulerState.SubscriptionParams memory pusherParams = SchedulerState
+        SchedulerStructs.SubscriptionParams memory pusherParams = SchedulerState
             .SubscriptionParams({
                 priceIds: priceIds,
                 readerWhitelist: emptyWhitelist,
@@ -1702,7 +1702,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
 
         // Get active subscriptions directly - should work without any special permissions
         uint256[] memory activeIds;
-        SchedulerState.SubscriptionParams[] memory activeParams;
+        SchedulerStructs.SubscriptionParams[] memory activeParams;
         uint256 totalCount;
 
         (activeIds, activeParams, totalCount) = scheduler
@@ -1720,7 +1720,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
         // Verify subscription params
         for (uint i = 0; i < activeIds.length; i++) {
             (
-                SchedulerState.SubscriptionParams memory storedParams,
+                SchedulerStructs.SubscriptionParams memory storedParams,
 
             ) = scheduler.getSubscription(activeIds[i]);
 
@@ -1778,22 +1778,22 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
         uint256 initialSubId = 0; // For update tests
 
         // === Empty Price IDs ===
-        SchedulerState.SubscriptionParams
+        SchedulerStructs.SubscriptionParams
             memory emptyPriceIdsParams = createDefaultSubscriptionParams(
                 1,
                 address(reader)
             );
         emptyPriceIdsParams.priceIds = new bytes32[](0);
 
-        vm.expectRevert(abi.encodeWithSelector(EmptyPriceIds.selector));
+        vm.expectRevert(abi.encodeWithSelector(SchedulerErrors.EmptyPriceIds.selector));
         scheduler.createSubscription{value: 1 ether}(emptyPriceIdsParams);
 
         initialSubId = addTestSubscription(scheduler, address(reader)); // Create a valid one for update test
-        vm.expectRevert(abi.encodeWithSelector(EmptyPriceIds.selector));
+        vm.expectRevert(abi.encodeWithSelector(SchedulerErrors.EmptyPriceIds.selector));
         scheduler.updateSubscription(initialSubId, emptyPriceIdsParams);
 
         // === Duplicate Price IDs ===
-        SchedulerState.SubscriptionParams
+        SchedulerStructs.SubscriptionParams
             memory duplicatePriceIdsParams = createDefaultSubscriptionParams(
                 2,
                 address(reader)
@@ -1813,7 +1813,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
         scheduler.updateSubscription(initialSubId, duplicatePriceIdsParams);
 
         // === Too Many Whitelist Readers ===
-        SchedulerState.SubscriptionParams
+        SchedulerStructs.SubscriptionParams
             memory largeWhitelistParams = createDefaultSubscriptionParams(
                 1,
                 address(reader)
@@ -1845,7 +1845,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
         scheduler.updateSubscription(initialSubId, largeWhitelistParams);
 
         // === Duplicate Whitelist Address ===
-        SchedulerState.SubscriptionParams
+        SchedulerStructs.SubscriptionParams
             memory duplicateWhitelistParams = createDefaultSubscriptionParams(
                 1,
                 address(reader)
@@ -1873,7 +1873,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
         scheduler.updateSubscription(initialSubId, duplicateWhitelistParams);
 
         // === Invalid Heartbeat (Zero Seconds) ===
-        SchedulerState.SubscriptionParams
+        SchedulerStructs.SubscriptionParams
             memory invalidHeartbeatParams = createDefaultSubscriptionParams(
                 1,
                 address(reader)
@@ -1881,15 +1881,15 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
         invalidHeartbeatParams.updateCriteria.updateOnHeartbeat = true;
         invalidHeartbeatParams.updateCriteria.heartbeatSeconds = 0; // Invalid
 
-        vm.expectRevert(abi.encodeWithSelector(InvalidUpdateCriteria.selector));
+        vm.expectRevert(abi.encodeWithSelector(SchedulerErrors.InvalidUpdateCriteria.selector));
         scheduler.createSubscription{value: 1 ether}(invalidHeartbeatParams);
 
         initialSubId = addTestSubscription(scheduler, address(reader));
-        vm.expectRevert(abi.encodeWithSelector(InvalidUpdateCriteria.selector));
+        vm.expectRevert(abi.encodeWithSelector(SchedulerErrors.InvalidUpdateCriteria.selector));
         scheduler.updateSubscription(initialSubId, invalidHeartbeatParams);
 
         // === Invalid Deviation (Zero Bps) ===
-        SchedulerState.SubscriptionParams
+        SchedulerStructs.SubscriptionParams
             memory invalidDeviationParams = createDefaultSubscriptionParams(
                 1,
                 address(reader)
@@ -1897,11 +1897,11 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
         invalidDeviationParams.updateCriteria.updateOnDeviation = true;
         invalidDeviationParams.updateCriteria.deviationThresholdBps = 0; // Invalid
 
-        vm.expectRevert(abi.encodeWithSelector(InvalidUpdateCriteria.selector));
+        vm.expectRevert(abi.encodeWithSelector(SchedulerErrors.InvalidUpdateCriteria.selector));
         scheduler.createSubscription{value: 1 ether}(invalidDeviationParams);
 
         initialSubId = addTestSubscription(scheduler, address(reader));
-        vm.expectRevert(abi.encodeWithSelector(InvalidUpdateCriteria.selector));
+        vm.expectRevert(abi.encodeWithSelector(SchedulerErrors.InvalidUpdateCriteria.selector));
         scheduler.updateSubscription(initialSubId, invalidDeviationParams);
     }
 
@@ -1947,7 +1947,7 @@ contract SchedulerTest is Test, SchedulerEvents, PulseSchedulerTestUtils {
         scheduler.updatePriceFeeds(subscriptionId, updateData);
 
         // Verify last updated timestamp
-        (, SchedulerState.SubscriptionStatus memory status) = scheduler
+        (, SchedulerStructs.SubscriptionStatus memory status) = scheduler
             .getSubscription(subscriptionId);
         assertEq(
             status.priceLastUpdatedAt,

+ 6 - 6
target_chains/ethereum/contracts/forge-test/PulseSchedulerGasBenchmark.t.sol

@@ -7,10 +7,10 @@ import "forge-std/console.sol";
 import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
 import "@openzeppelin/contracts/utils/math/SafeCast.sol";
 import "../contracts/pulse/SchedulerUpgradeable.sol";
-import "../contracts/pulse/IScheduler.sol";
-import "../contracts/pulse/SchedulerState.sol";
-import "../contracts/pulse/SchedulerEvents.sol";
-import "../contracts/pulse/SchedulerErrors.sol";
+import "@pythnetwork/pulse-sdk-solidity/IScheduler.sol";
+import "@pythnetwork/pulse-sdk-solidity/SchedulerStructs.sol";
+import "@pythnetwork/pulse-sdk-solidity/SchedulerEvents.sol";
+import "@pythnetwork/pulse-sdk-solidity/SchedulerErrors.sol";
 import "./utils/PulseSchedulerTestUtils.t.sol";
 
 contract PulseSchedulerGasBenchmark is Test, PulseSchedulerTestUtils {
@@ -54,7 +54,7 @@ contract PulseSchedulerGasBenchmark is Test, PulseSchedulerTestUtils {
         // Setup: Create subscription and perform initial update
         vm.prank(manager);
         uint256 subscriptionId = _setupSubscriptionWithInitialUpdate(numFeeds);
-        (SchedulerState.SubscriptionParams memory params, ) = scheduler
+        (SchedulerStructs.SubscriptionParams memory params, ) = scheduler
             .getSubscription(subscriptionId);
 
         // Advance time to meet heartbeat criteria
@@ -161,7 +161,7 @@ contract PulseSchedulerGasBenchmark is Test, PulseSchedulerTestUtils {
         // Deactivate every other subscription
         for (uint256 i = 0; i < numSubscriptions; i++) {
             if (i % 2 == 1) {
-                (SchedulerState.SubscriptionParams memory params, ) = scheduler
+                (SchedulerStructs.SubscriptionParams memory params, ) = scheduler
                     .getSubscription(subscriptionIds[i]);
                 params.isActive = false;
                 scheduler.updateSubscription(subscriptionIds[i], params);

+ 3 - 3
target_chains/ethereum/contracts/forge-test/PulseSchedulerGovernance.t.sol

@@ -6,7 +6,7 @@ import "forge-std/Test.sol";
 import "forge-std/console.sol";
 import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
 import "../contracts/pulse/SchedulerUpgradeable.sol";
-import "../contracts/pulse/SchedulerErrors.sol";
+import "@pythnetwork/pulse-sdk-solidity/SchedulerErrors.sol";
 contract SchedulerInvalidMagic is SchedulerUpgradeable {
     function schedulerUpgradableMagic() public pure override returns (uint32) {
         return 0x12345678; // Incorrect magic
@@ -70,7 +70,7 @@ contract PulseSchedulerGovernanceTest is Test {
     function testProposeAdminByUnauthorized() public {
         address unauthorized = address(5);
         vm.prank(unauthorized);
-        vm.expectRevert(Unauthorized.selector);
+        vm.expectRevert(SchedulerErrors.Unauthorized.selector);
         scheduler.proposeAdmin(admin2);
     }
 
@@ -91,7 +91,7 @@ contract PulseSchedulerGovernanceTest is Test {
 
         address unauthorized = address(5);
         vm.prank(unauthorized);
-        vm.expectRevert(Unauthorized.selector);
+        vm.expectRevert(SchedulerErrors.Unauthorized.selector);
         scheduler.acceptAdmin();
     }
 

+ 1 - 0
target_chains/ethereum/contracts/package.json

@@ -42,6 +42,7 @@
     "@nomad-xyz/excessively-safe-call": "^0.0.1-rc.1",
     "@pythnetwork/contract-manager": "workspace:*",
     "@pythnetwork/entropy-sdk-solidity": "workspace:*",
+    "@pythnetwork/pulse-sdk-solidity": "workspace:*",
     "@pythnetwork/pyth-sdk-solidity": "workspace:*",
     "@pythnetwork/xc-admin-common": "workspace:*",
     "dotenv": "^10.0.0",

+ 94 - 0
target_chains/ethereum/pulse_sdk/solidity/AbstractPulse.sol

@@ -0,0 +1,94 @@
+// SPDX-License-Identifier: Apache-2.0
+pragma solidity ^0.8.0;
+
+import "./SchedulerStructs.sol";
+import "./IScheduler.sol";
+import "@pythnetwork/pyth-sdk-solidity/IPyth.sol";
+import "@pythnetwork/pyth-sdk-solidity/PythStructs.sol";
+import "@pythnetwork/pyth-sdk-solidity/PythErrors.sol";
+
+abstract contract AbstractPulse is IScheduler {
+    /**
+     * @notice Get a price that is no older than the specified age
+     * @param subscriptionId The ID of the subscription
+     * @param priceId The price ID to get the price for
+     * @param maxAge The maximum acceptable age of the price in seconds
+     * @return price The price
+     */
+    function getPriceNoOlderThan(
+        uint256 subscriptionId,
+        bytes32 priceId,
+        uint256 maxAge
+    ) public view virtual returns (PythStructs.Price memory price) {
+        bytes32[] memory priceIds = new bytes32[](1);
+        priceIds[0] = priceId;
+        
+        PythStructs.Price[] memory prices = getPricesNoOlderThan(subscriptionId, priceIds, maxAge);
+        return prices[0];
+    }
+
+    /**
+     * @notice Get prices that are no older than the specified age
+     * @param subscriptionId The ID of the subscription
+     * @param priceIds Array of price IDs to get prices for
+     * @param maxAge The maximum acceptable age of the prices in seconds
+     * @return prices Array of prices
+     */
+    function getPricesNoOlderThan(
+        uint256 subscriptionId,
+        bytes32[] memory priceIds,
+        uint256 maxAge
+    ) public view virtual returns (PythStructs.Price[] memory prices) {
+        prices = getPricesUnsafe(subscriptionId, priceIds);
+        
+        for (uint i = 0; i < prices.length; i++) {
+            if (block.timestamp - prices[i].publishTime > maxAge) {
+                revert PythErrors.StalePrice();
+            }
+        }
+        
+        return prices;
+    }
+
+    /**
+     * @notice Get an EMA price that is no older than the specified age
+     * @param subscriptionId The ID of the subscription
+     * @param priceId The price ID to get the EMA price for
+     * @param maxAge The maximum acceptable age of the price in seconds
+     * @return price The EMA price
+     */
+    function getEmaPriceNoOlderThan(
+        uint256 subscriptionId,
+        bytes32 priceId,
+        uint256 maxAge
+    ) public view virtual returns (PythStructs.Price memory price) {
+        bytes32[] memory priceIds = new bytes32[](1);
+        priceIds[0] = priceId;
+        
+        PythStructs.Price[] memory prices = getEmaPricesNoOlderThan(subscriptionId, priceIds, maxAge);
+        return prices[0];
+    }
+
+    /**
+     * @notice Get EMA prices that are no older than the specified age
+     * @param subscriptionId The ID of the subscription
+     * @param priceIds Array of price IDs to get EMA prices for
+     * @param maxAge The maximum acceptable age of the prices in seconds
+     * @return prices Array of EMA prices
+     */
+    function getEmaPricesNoOlderThan(
+        uint256 subscriptionId,
+        bytes32[] memory priceIds,
+        uint256 maxAge
+    ) public view virtual returns (PythStructs.Price[] memory prices) {
+        prices = getEmaPriceUnsafe(subscriptionId, priceIds);
+        
+        for (uint i = 0; i < prices.length; i++) {
+            if (block.timestamp - prices[i].publishTime > maxAge) {
+                revert PythErrors.StalePrice();
+            }
+        }
+        
+        return prices;
+    }
+}

+ 206 - 0
target_chains/ethereum/pulse_sdk/solidity/abis/AbstractPulse.json

@@ -0,0 +1,206 @@
+[
+  {
+    "inputs": [
+      {
+        "internalType": "uint256",
+        "name": "subscriptionId",
+        "type": "uint256"
+      },
+      {
+        "internalType": "bytes32",
+        "name": "priceId",
+        "type": "bytes32"
+      },
+      {
+        "internalType": "uint256",
+        "name": "maxAge",
+        "type": "uint256"
+      }
+    ],
+    "name": "getPriceNoOlderThan",
+    "outputs": [
+      {
+        "components": [
+          {
+            "internalType": "int64",
+            "name": "price",
+            "type": "int64"
+          },
+          {
+            "internalType": "uint64",
+            "name": "conf",
+            "type": "uint64"
+          },
+          {
+            "internalType": "int32",
+            "name": "expo",
+            "type": "int32"
+          },
+          {
+            "internalType": "uint256",
+            "name": "publishTime",
+            "type": "uint256"
+          }
+        ],
+        "internalType": "struct PythStructs.Price",
+        "name": "price",
+        "type": "tuple"
+      }
+    ],
+    "stateMutability": "view",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      {
+        "internalType": "uint256",
+        "name": "subscriptionId",
+        "type": "uint256"
+      },
+      {
+        "internalType": "bytes32[]",
+        "name": "priceIds",
+        "type": "bytes32[]"
+      },
+      {
+        "internalType": "uint256",
+        "name": "maxAge",
+        "type": "uint256"
+      }
+    ],
+    "name": "getPricesNoOlderThan",
+    "outputs": [
+      {
+        "components": [
+          {
+            "internalType": "int64",
+            "name": "price",
+            "type": "int64"
+          },
+          {
+            "internalType": "uint64",
+            "name": "conf",
+            "type": "uint64"
+          },
+          {
+            "internalType": "int32",
+            "name": "expo",
+            "type": "int32"
+          },
+          {
+            "internalType": "uint256",
+            "name": "publishTime",
+            "type": "uint256"
+          }
+        ],
+        "internalType": "struct PythStructs.Price[]",
+        "name": "prices",
+        "type": "tuple[]"
+      }
+    ],
+    "stateMutability": "view",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      {
+        "internalType": "uint256",
+        "name": "subscriptionId",
+        "type": "uint256"
+      },
+      {
+        "internalType": "bytes32",
+        "name": "priceId",
+        "type": "bytes32"
+      },
+      {
+        "internalType": "uint256",
+        "name": "maxAge",
+        "type": "uint256"
+      }
+    ],
+    "name": "getEmaPriceNoOlderThan",
+    "outputs": [
+      {
+        "components": [
+          {
+            "internalType": "int64",
+            "name": "price",
+            "type": "int64"
+          },
+          {
+            "internalType": "uint64",
+            "name": "conf",
+            "type": "uint64"
+          },
+          {
+            "internalType": "int32",
+            "name": "expo",
+            "type": "int32"
+          },
+          {
+            "internalType": "uint256",
+            "name": "publishTime",
+            "type": "uint256"
+          }
+        ],
+        "internalType": "struct PythStructs.Price",
+        "name": "price",
+        "type": "tuple"
+      }
+    ],
+    "stateMutability": "view",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      {
+        "internalType": "uint256",
+        "name": "subscriptionId",
+        "type": "uint256"
+      },
+      {
+        "internalType": "bytes32[]",
+        "name": "priceIds",
+        "type": "bytes32[]"
+      },
+      {
+        "internalType": "uint256",
+        "name": "maxAge",
+        "type": "uint256"
+      }
+    ],
+    "name": "getEmaPricesNoOlderThan",
+    "outputs": [
+      {
+        "components": [
+          {
+            "internalType": "int64",
+            "name": "price",
+            "type": "int64"
+          },
+          {
+            "internalType": "uint64",
+            "name": "conf",
+            "type": "uint64"
+          },
+          {
+            "internalType": "int32",
+            "name": "expo",
+            "type": "int32"
+          },
+          {
+            "internalType": "uint256",
+            "name": "publishTime",
+            "type": "uint256"
+          }
+        ],
+        "internalType": "struct PythStructs.Price[]",
+        "name": "prices",
+        "type": "tuple[]"
+      }
+    ],
+    "stateMutability": "view",
+    "type": "function"
+  }
+]

+ 1 - 1
target_chains/ethereum/pulse_sdk/solidity/package.json

@@ -14,7 +14,7 @@
   "scripts": {
     "test:format": "prettier --check .",
     "fix:format": "prettier --write .",
-    "build": "generate-abis IScheduler SchedulerEvents SchedulerErrors SchedulerStructs",
+    "build": "generate-abis IScheduler SchedulerEvents SchedulerErrors SchedulerStructs AbstractPulse",
     "test": "git diff --exit-code abis"
   },
   "keywords": [

+ 5 - 0
target_chains/ethereum/pulse_sdk/solidity/prettier.config.js

@@ -0,0 +1,5 @@
+import solidity from "prettier-plugin-solidity";
+
+export default {
+  plugins: [solidity],
+};

+ 12 - 0
target_chains/ethereum/pulse_sdk/solidity/turbo.json

@@ -0,0 +1,12 @@
+{
+  "$schema": "https://turbo.build/schema.json",
+  "extends": ["//"],
+  "tasks": {
+    "build": {
+      "outputs": ["abis/**"]
+    },
+    "test": {
+      "dependsOn": ["build"]
+    }
+  }
+}