Jelajahi Sumber

feat(pulse-sdk-solidity): Create Pulse solidity SDK (#2667)

* feat: extracted public components out to SDK

* doc: improve readme

* fix: add remapping to abi gen

* fix: identifier issues

* doc: add signatures for the error types

* undo argus lockfile change

* fix: merge, format

* test: update references to sdk
Tejas Badadare 5 bulan lalu
induk
melakukan
9a1b059750
28 mengubah file dengan 1891 tambahan dan 492 penghapusan
  1. 19 0
      pnpm-lock.yaml
  2. 1 0
      pnpm-workspace.yaml
  3. 23 0
      target_chains/ethereum/abi_generator/src/index.js
  4. 0 140
      target_chains/ethereum/contracts/contracts/pulse/IScheduler.sol
  5. 168 122
      target_chains/ethereum/contracts/contracts/pulse/Scheduler.sol
  6. 0 39
      target_chains/ethereum/contracts/contracts/pulse/SchedulerErrors.sol
  7. 3 2
      target_chains/ethereum/contracts/contracts/pulse/SchedulerGovernance.sol
  8. 3 49
      target_chains/ethereum/contracts/contracts/pulse/SchedulerState.sol
  9. 3 2
      target_chains/ethereum/contracts/contracts/pulse/SchedulerUpgradeable.sol
  10. 190 120
      target_chains/ethereum/contracts/forge-test/PulseScheduler.t.sol
  11. 9 7
      target_chains/ethereum/contracts/forge-test/PulseSchedulerGasBenchmark.t.sol
  12. 7 3
      target_chains/ethereum/contracts/forge-test/PulseSchedulerGovernance.t.sol
  13. 8 7
      target_chains/ethereum/contracts/forge-test/utils/PulseSchedulerTestUtils.t.sol
  14. 1 0
      target_chains/ethereum/contracts/package.json
  15. 144 0
      target_chains/ethereum/pulse_sdk/solidity/IScheduler.sol
  16. 128 0
      target_chains/ethereum/pulse_sdk/solidity/README.md
  17. 30 0
      target_chains/ethereum/pulse_sdk/solidity/SchedulerConstants.sol
  18. 59 0
      target_chains/ethereum/pulse_sdk/solidity/SchedulerErrors.sol
  19. 1 1
      target_chains/ethereum/pulse_sdk/solidity/SchedulerEvents.sol
  20. 33 0
      target_chains/ethereum/pulse_sdk/solidity/SchedulerStructs.sol
  21. 674 0
      target_chains/ethereum/pulse_sdk/solidity/abis/IScheduler.json
  22. 80 0
      target_chains/ethereum/pulse_sdk/solidity/abis/SchedulerConstants.json
  23. 170 0
      target_chains/ethereum/pulse_sdk/solidity/abis/SchedulerErrors.json
  24. 79 0
      target_chains/ethereum/pulse_sdk/solidity/abis/SchedulerEvents.json
  25. 1 0
      target_chains/ethereum/pulse_sdk/solidity/abis/SchedulerStructs.json
  26. 40 0
      target_chains/ethereum/pulse_sdk/solidity/package.json
  27. 5 0
      target_chains/ethereum/pulse_sdk/solidity/prettier.config.js
  28. 12 0
      target_chains/ethereum/pulse_sdk/solidity/turbo.json

+ 19 - 0
pnpm-lock.yaml

@@ -2516,6 +2516,9 @@ importers:
       '@pythnetwork/entropy-sdk-solidity':
         specifier: workspace:*
         version: link:../entropy_sdk/solidity
+      '@pythnetwork/pulse-sdk-solidity':
+        specifier: workspace:*
+        version: link:../pulse_sdk/solidity
       '@pythnetwork/pyth-sdk-solidity':
         specifier: workspace:*
         version: link:../sdk/solidity
@@ -2620,6 +2623,22 @@ importers:
         specifier: 'catalog:'
         version: 1.4.2(prettier@3.5.3)
 
+  target_chains/ethereum/pulse_sdk/solidity:
+    dependencies:
+      '@pythnetwork/pyth-sdk-solidity':
+        specifier: workspace:*
+        version: link:../../sdk/solidity
+    devDependencies:
+      abi_generator:
+        specifier: workspace:*
+        version: link:../../abi_generator
+      prettier:
+        specifier: 'catalog:'
+        version: 3.5.3
+      prettier-plugin-solidity:
+        specifier: 'catalog:'
+        version: 1.4.2(prettier@3.5.3)
+
   target_chains/ethereum/sdk/js:
     dependencies:
       '@pythnetwork/price-service-client':

+ 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

+ 23 - 0
target_chains/ethereum/abi_generator/src/index.js

@@ -25,6 +25,10 @@ function generateAbi(contracts) {
     sources,
     settings: {
       outputSelection,
+      remappings: [
+        // Needed for @pythnetwork/pulse-sdk-solidity since it depends on @pythnetwork/pyth-sdk-solidity
+        "@pythnetwork/=./node_modules/@pythnetwork/",
+      ],
     },
   };
 
@@ -42,9 +46,28 @@ function generateAbi(contracts) {
     fs.mkdirSync("abis");
   }
 
+  // Report compilation failures
+  if (output.errors) {
+    // We can still generate ABIs with warnings, only throw for errors
+    const errors = output.errors.filter((e) => e.severity === "error");
+    if (errors.length > 0) {
+      console.error("Compilation errors:");
+      for (const error of errors) {
+        console.error(error.formattedMessage || error.message);
+      }
+      throw new Error("Compilation failed due to errors");
+    }
+  }
+
   for (let contract of contracts) {
     const contractFile = `${contract}.sol`;
 
+    if (!output.contracts[contractFile]) {
+      throw new Error(`Unable to produce ABI for ${contractFile}.`);
+    }
+    if (!output.contracts[contractFile][contract]) {
+      throw new Error(`Unable to produce ABI for ${contractFile}:${contract}.`);
+    }
     const abi = output.contracts[contractFile][contract].abi;
     fs.writeFileSync(
       `abis/${contract}.json`,

+ 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
-        );
-}

+ 168 - 122
target_chains/ethereum/contracts/contracts/pulse/Scheduler.sol

@@ -6,11 +6,13 @@ 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/SchedulerStructs.sol";
+import "@pythnetwork/pulse-sdk-solidity/IScheduler.sol";
+import "@pythnetwork/pulse-sdk-solidity/SchedulerErrors.sol";
+import "@pythnetwork/pulse-sdk-solidity/SchedulerConstants.sol";
 import "./SchedulerState.sol";
-import "./SchedulerErrors.sol";
 
-abstract contract Scheduler is IScheduler, SchedulerState {
+abstract contract Scheduler is IScheduler, SchedulerState, SchedulerConstants {
     function _initialize(
         address admin,
         address pythAddress,
@@ -28,7 +30,7 @@ abstract contract Scheduler is IScheduler, SchedulerState {
     }
 
     function createSubscription(
-        SubscriptionParams memory subscriptionParams
+        SchedulerStructs.SubscriptionParams memory subscriptionParams
     ) external payable override returns (uint256 subscriptionId) {
         _validateSubscriptionParams(subscriptionParams);
 
@@ -39,12 +41,12 @@ abstract contract Scheduler is IScheduler, SchedulerState {
 
         // Ensure enough funds were provided
         if (msg.value < minimumBalance) {
-            revert InsufficientBalance();
+            revert SchedulerErrors.InsufficientBalance();
         }
 
         // Check deposit limit for permanent subscriptions
         if (subscriptionParams.isPermanent && msg.value > MAX_DEPOSIT_LIMIT) {
-            revert MaxDepositLimitExceeded();
+            revert SchedulerErrors.MaxDepositLimitExceeded();
         }
 
         // Set subscription to active
@@ -56,9 +58,8 @@ abstract contract Scheduler is IScheduler, SchedulerState {
         _state.subscriptionParams[subscriptionId] = subscriptionParams;
 
         // Initialize subscription status
-        SubscriptionStatus storage status = _state.subscriptionStatuses[
-            subscriptionId
-        ];
+        SchedulerStructs.SubscriptionStatus storage status = _state
+            .subscriptionStatuses[subscriptionId];
         status.priceLastUpdatedAt = 0;
         status.balanceInWei = msg.value;
         status.totalUpdates = 0;
@@ -75,21 +76,19 @@ 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[
-            subscriptionId
-        ];
-        SubscriptionParams storage currentParams = _state.subscriptionParams[
-            subscriptionId
-        ];
+        SchedulerStructs.SubscriptionStatus storage currentStatus = _state
+            .subscriptionStatuses[subscriptionId];
+        SchedulerStructs.SubscriptionParams storage currentParams = _state
+            .subscriptionParams[subscriptionId];
 
         // Add incoming funds to balance
         currentStatus.balanceInWei += msg.value;
 
         // Updates to permanent subscriptions are not allowed
         if (currentParams.isPermanent) {
-            revert CannotUpdatePermanentSubscription();
+            revert SchedulerErrors.CannotUpdatePermanentSubscription();
         }
 
         // If subscription is inactive and will remain inactive, no need to validate parameters
@@ -109,7 +108,7 @@ abstract contract Scheduler is IScheduler, SchedulerState {
                 uint8(newParams.priceIds.length)
             );
             if (currentStatus.balanceInWei < minimumBalance) {
-                revert InsufficientBalance();
+                revert SchedulerErrors.InsufficientBalance();
             }
         }
 
@@ -122,7 +121,7 @@ abstract contract Scheduler is IScheduler, SchedulerState {
 
             // Check if balance meets minimum requirement
             if (currentStatus.balanceInWei < minimumBalance) {
-                revert InsufficientBalance();
+                revert SchedulerErrors.InsufficientBalance();
             }
 
             currentParams.isActive = true;
@@ -153,36 +152,37 @@ abstract contract Scheduler is IScheduler, SchedulerState {
         emit SubscriptionUpdated(subscriptionId);
     }
 
-    /**
-     * @notice Validates subscription parameters.
-     * @param params The subscription parameters to validate.
-     */
+    /// @notice Validates subscription parameters.
+    /// @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) {
-            revert EmptyPriceIds();
+            revert SchedulerErrors.EmptyPriceIds();
         }
 
         // Price ID limits and uniqueness
-        if (params.priceIds.length > MAX_PRICE_IDS_PER_SUBSCRIPTION) {
-            revert TooManyPriceIds(
+        if (
+            params.priceIds.length >
+            SchedulerConstants.MAX_PRICE_IDS_PER_SUBSCRIPTION
+        ) {
+            revert SchedulerErrors.TooManyPriceIds(
                 params.priceIds.length,
-                MAX_PRICE_IDS_PER_SUBSCRIPTION
+                SchedulerConstants.MAX_PRICE_IDS_PER_SUBSCRIPTION
             );
         }
         for (uint i = 0; i < params.priceIds.length; i++) {
             for (uint j = i + 1; j < params.priceIds.length; j++) {
                 if (params.priceIds[i] == params.priceIds[j]) {
-                    revert DuplicatePriceId(params.priceIds[i]);
+                    revert SchedulerErrors.DuplicatePriceId(params.priceIds[i]);
                 }
             }
         }
 
         // Whitelist size limit and uniqueness
         if (params.readerWhitelist.length > MAX_READER_WHITELIST_SIZE) {
-            revert TooManyWhitelistedReaders(
+            revert SchedulerErrors.TooManyWhitelistedReaders(
                 params.readerWhitelist.length,
                 MAX_READER_WHITELIST_SIZE
             );
@@ -190,7 +190,9 @@ abstract contract Scheduler is IScheduler, SchedulerState {
         for (uint i = 0; i < params.readerWhitelist.length; i++) {
             for (uint j = i + 1; j < params.readerWhitelist.length; j++) {
                 if (params.readerWhitelist[i] == params.readerWhitelist[j]) {
-                    revert DuplicateWhitelistAddress(params.readerWhitelist[i]);
+                    revert SchedulerErrors.DuplicateWhitelistAddress(
+                        params.readerWhitelist[i]
+                    );
                 }
             }
         }
@@ -200,19 +202,19 @@ abstract contract Scheduler is IScheduler, SchedulerState {
             !params.updateCriteria.updateOnHeartbeat &&
             !params.updateCriteria.updateOnDeviation
         ) {
-            revert InvalidUpdateCriteria();
+            revert SchedulerErrors.InvalidUpdateCriteria();
         }
         if (
             params.updateCriteria.updateOnHeartbeat &&
             params.updateCriteria.heartbeatSeconds == 0
         ) {
-            revert InvalidUpdateCriteria();
+            revert SchedulerErrors.InvalidUpdateCriteria();
         }
         if (
             params.updateCriteria.updateOnDeviation &&
             params.updateCriteria.deviationThresholdBps == 0
         ) {
-            revert InvalidUpdateCriteria();
+            revert SchedulerErrors.InvalidUpdateCriteria();
         }
     }
 
@@ -276,15 +278,13 @@ abstract contract Scheduler is IScheduler, SchedulerState {
     ) external override {
         uint256 startGas = gasleft();
 
-        SubscriptionStatus storage status = _state.subscriptionStatuses[
-            subscriptionId
-        ];
-        SubscriptionParams storage params = _state.subscriptionParams[
-            subscriptionId
-        ];
+        SchedulerStructs.SubscriptionStatus storage status = _state
+            .subscriptionStatuses[subscriptionId];
+        SchedulerStructs.SubscriptionParams storage params = _state
+            .subscriptionParams[subscriptionId];
 
         if (!params.isActive) {
-            revert InactiveSubscription();
+            revert SchedulerErrors.InactiveSubscription();
         }
 
         // Get the Pyth contract and parse price updates
@@ -293,7 +293,7 @@ abstract contract Scheduler is IScheduler, SchedulerState {
 
         // If we don't have enough balance, revert
         if (status.balanceInWei < pythFee) {
-            revert InsufficientBalance();
+            revert SchedulerErrors.InsufficientBalance();
         }
 
         // Parse the price feed updates with an acceptable timestamp range of [0, now+10s].
@@ -320,7 +320,7 @@ abstract contract Scheduler is IScheduler, SchedulerState {
         uint64 slot = slots[0];
         for (uint8 i = 1; i < slots.length; i++) {
             if (slots[i] != slot) {
-                revert PriceSlotMismatch();
+                revert SchedulerErrors.PriceSlotMismatch();
             }
         }
 
@@ -344,18 +344,16 @@ abstract contract Scheduler is IScheduler, SchedulerState {
         emit PricesUpdated(subscriptionId, latestPublishTime);
     }
 
-    /**
-     * @notice Validates whether the update trigger criteria is met for a subscription. Reverts if not met.
-     * @param subscriptionId The ID of the subscription (needed for reading previous prices).
-     * @param params The subscription's parameters struct.
-     * @param status The subscription's status struct.
-     * @param priceFeeds The array of price feeds to validate.
-     * @return The timestamp of the update if the trigger criteria is met, reverts if not met.
-     */
+    /// @notice Validates whether the update trigger criteria is met for a subscription. Reverts if not met.
+    /// @param subscriptionId The ID of the subscription (needed for reading previous prices).
+    /// @param params The subscription's parameters struct.
+    /// @param status The subscription's status struct.
+    /// @param priceFeeds The array of price feeds to validate.
+    /// @return The timestamp of the update if the trigger criteria is met, reverts if not met.
     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.
@@ -378,7 +376,10 @@ abstract contract Scheduler is IScheduler, SchedulerState {
 
         // Validate that the update timestamp is not too old
         if (updateTimestamp < minAllowedTimestamp) {
-            revert TimestampTooOld(updateTimestamp, block.timestamp);
+            revert SchedulerErrors.TimestampTooOld(
+                updateTimestamp,
+                block.timestamp
+            );
         }
 
         // Reject updates if they're older than the latest stored ones
@@ -386,7 +387,7 @@ abstract contract Scheduler is IScheduler, SchedulerState {
             status.priceLastUpdatedAt > 0 &&
             updateTimestamp <= status.priceLastUpdatedAt
         ) {
-            revert TimestampOlderThanLastUpdate(
+            revert SchedulerErrors.TimestampOlderThanLastUpdate(
                 updateTimestamp,
                 status.priceLastUpdatedAt
             );
@@ -446,28 +447,25 @@ abstract contract Scheduler is IScheduler, SchedulerState {
             }
         }
 
-        revert UpdateConditionsNotMet();
+        revert SchedulerErrors.UpdateConditionsNotMet();
     }
 
     /// FETCH PRICES
 
-    /**
-     * @notice Internal helper function to retrieve price feeds for a subscription.
-     * @param subscriptionId The ID of the subscription.
-     * @param priceIds The specific price IDs requested, or empty array to get all.
-     * @return priceFeeds An array of PriceFeed structs corresponding to the requested IDs.
-     */
+    /// @notice Internal helper function to retrieve price feeds for a subscription.
+    /// @param subscriptionId The ID of the subscription.
+    /// @param priceIds The specific price IDs requested, or empty array to get all.
+    /// @return priceFeeds An array of PriceFeed structs corresponding to the requested IDs.
     function _getPricesInternal(
         uint256 subscriptionId,
         bytes32[] calldata priceIds
     ) internal view returns (PythStructs.PriceFeed[] memory priceFeeds) {
         if (!_state.subscriptionParams[subscriptionId].isActive) {
-            revert InactiveSubscription();
+            revert SchedulerErrors.InactiveSubscription();
         }
 
-        SubscriptionParams storage params = _state.subscriptionParams[
-            subscriptionId
-        ];
+        SchedulerStructs.SubscriptionParams storage params = _state
+            .subscriptionParams[subscriptionId];
 
         // If no price IDs provided, return all price feeds for the subscription
         if (priceIds.length == 0) {
@@ -481,7 +479,10 @@ abstract contract Scheduler is IScheduler, SchedulerState {
                 ][params.priceIds[i]];
                 // Check if the price feed exists (price ID is valid and has been updated)
                 if (priceFeed.id == bytes32(0)) {
-                    revert InvalidPriceId(params.priceIds[i], bytes32(0));
+                    revert SchedulerErrors.InvalidPriceId(
+                        params.priceIds[i],
+                        bytes32(0)
+                    );
                 }
                 allFeeds[i] = priceFeed;
             }
@@ -500,7 +501,7 @@ abstract contract Scheduler is IScheduler, SchedulerState {
 
             // Check if the price feed exists (price ID is valid and has been updated)
             if (priceFeed.id == bytes32(0)) {
-                revert InvalidPriceId(priceIds[i], bytes32(0));
+                revert SchedulerErrors.InvalidPriceId(priceIds[i], bytes32(0));
             }
             requestedFeeds[i] = priceFeed;
         }
@@ -528,7 +529,29 @@ abstract contract Scheduler is IScheduler, SchedulerState {
         return prices;
     }
 
-    function getEmaPriceUnsafe(
+    function getPricesNoOlderThan(
+        uint256 subscriptionId,
+        bytes32[] calldata priceIds,
+        uint256 age_seconds
+    )
+        external
+        view
+        override
+        onlyWhitelistedReader(subscriptionId)
+        returns (PythStructs.Price[] memory prices)
+    {
+        SchedulerStructs.SubscriptionStatus memory status = _state
+            .subscriptionStatuses[subscriptionId];
+
+        // Use distance (absolute difference) since pythnet timestamps
+        // may be slightly ahead of this chain.
+        if (distance(block.timestamp, status.priceLastUpdatedAt) > age_seconds)
+            revert PythErrors.StalePrice();
+
+        prices = this.getPricesUnsafe(subscriptionId, priceIds);
+    }
+
+    function getEmaPricesUnsafe(
         uint256 subscriptionId,
         bytes32[] calldata priceIds
     )
@@ -549,23 +572,43 @@ abstract contract Scheduler is IScheduler, SchedulerState {
         return prices;
     }
 
+    function getEmaPricesNoOlderThan(
+        uint256 subscriptionId,
+        bytes32[] calldata priceIds,
+        uint256 age_seconds
+    )
+        external
+        view
+        override
+        onlyWhitelistedReader(subscriptionId)
+        returns (PythStructs.Price[] memory prices)
+    {
+        SchedulerStructs.SubscriptionStatus memory status = _state
+            .subscriptionStatuses[subscriptionId];
+
+        // Use distance (absolute difference) since pythnet timestamps
+        // may be slightly ahead of this chain.
+        if (distance(block.timestamp, status.priceLastUpdatedAt) > age_seconds)
+            revert PythErrors.StalePrice();
+
+        prices = this.getEmaPricesUnsafe(subscriptionId, priceIds);
+    }
+
     /// BALANCE MANAGEMENT
 
     function addFunds(uint256 subscriptionId) external payable override {
-        SubscriptionParams storage params = _state.subscriptionParams[
-            subscriptionId
-        ];
-        SubscriptionStatus storage status = _state.subscriptionStatuses[
-            subscriptionId
-        ];
+        SchedulerStructs.SubscriptionParams storage params = _state
+            .subscriptionParams[subscriptionId];
+        SchedulerStructs.SubscriptionStatus storage status = _state
+            .subscriptionStatuses[subscriptionId];
 
         if (!params.isActive) {
-            revert InactiveSubscription();
+            revert SchedulerErrors.InactiveSubscription();
         }
 
         // Check deposit limit for permanent subscriptions
         if (params.isPermanent && msg.value > MAX_DEPOSIT_LIMIT) {
-            revert MaxDepositLimitExceeded();
+            revert SchedulerErrors.MaxDepositLimitExceeded();
         }
 
         status.balanceInWei += msg.value;
@@ -576,7 +619,7 @@ abstract contract Scheduler is IScheduler, SchedulerState {
                 uint8(params.priceIds.length)
             );
             if (status.balanceInWei < minimumBalance) {
-                revert InsufficientBalance();
+                revert SchedulerErrors.InsufficientBalance();
             }
         }
     }
@@ -585,20 +628,18 @@ abstract contract Scheduler is IScheduler, SchedulerState {
         uint256 subscriptionId,
         uint256 amount
     ) external override onlyManager(subscriptionId) {
-        SubscriptionStatus storage status = _state.subscriptionStatuses[
-            subscriptionId
-        ];
-        SubscriptionParams storage params = _state.subscriptionParams[
-            subscriptionId
-        ];
+        SchedulerStructs.SubscriptionStatus storage status = _state
+            .subscriptionStatuses[subscriptionId];
+        SchedulerStructs.SubscriptionParams storage params = _state
+            .subscriptionParams[subscriptionId];
 
         // Prevent withdrawals from permanent subscriptions
         if (params.isPermanent) {
-            revert CannotUpdatePermanentSubscription();
+            revert SchedulerErrors.CannotUpdatePermanentSubscription();
         }
 
         if (status.balanceInWei < amount) {
-            revert InsufficientBalance();
+            revert SchedulerErrors.InsufficientBalance();
         }
 
         // If subscription is active, ensure minimum balance is maintained
@@ -607,7 +648,7 @@ abstract contract Scheduler is IScheduler, SchedulerState {
                 uint8(params.priceIds.length)
             );
             if (status.balanceInWei - amount < minimumBalance) {
-                revert InsufficientBalance();
+                revert SchedulerErrors.InsufficientBalance();
             }
         }
 
@@ -626,8 +667,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 (
@@ -646,7 +687,7 @@ abstract contract Scheduler is IScheduler, SchedulerState {
         override
         returns (
             uint256[] memory subscriptionIds,
-            SubscriptionParams[] memory subscriptionParams,
+            SchedulerStructs.SubscriptionParams[] memory subscriptionParams,
             uint256 totalCount
         )
     {
@@ -654,7 +695,11 @@ 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)
@@ -665,7 +710,9 @@ 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++) {
@@ -679,10 +726,8 @@ abstract contract Scheduler is IScheduler, SchedulerState {
         return (subscriptionIds, subscriptionParams, totalCount);
     }
 
-    /**
-     * @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.
-     */
+    /// @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 override returns (uint256 minimumBalanceInWei) {
@@ -694,7 +739,7 @@ abstract contract Scheduler is IScheduler, SchedulerState {
 
     modifier onlyManager(uint256 subscriptionId) {
         if (_state.subscriptionManager[subscriptionId] != msg.sender) {
-            revert Unauthorized();
+            revert SchedulerErrors.Unauthorized();
         }
         _;
     }
@@ -725,15 +770,13 @@ abstract contract Scheduler is IScheduler, SchedulerState {
         }
 
         if (!isWhitelisted) {
-            revert Unauthorized();
+            revert SchedulerErrors.Unauthorized();
         }
         _;
     }
 
-    /**
-     * @notice Adds a subscription to the active subscriptions list.
-     * @param subscriptionId The ID of the subscription to add.
-     */
+    /// @notice Adds a subscription to the active subscriptions list.
+    /// @param subscriptionId The ID of the subscription to add.
     function _addToActiveSubscriptions(uint256 subscriptionId) internal {
         // Only add if not already in the list
         if (_state.activeSubscriptionIndex[subscriptionId] == 0) {
@@ -746,10 +789,8 @@ abstract contract Scheduler is IScheduler, SchedulerState {
         }
     }
 
-    /**
-     * @notice Removes a subscription from the active subscriptions list.
-     * @param subscriptionId The ID of the subscription to remove.
-     */
+    /// @notice Removes a subscription from the active subscriptions list.
+    /// @param subscriptionId The ID of the subscription to remove.
     function _removeFromActiveSubscriptions(uint256 subscriptionId) internal {
         uint256 index = _state.activeSubscriptionIndex[subscriptionId];
 
@@ -773,11 +814,9 @@ abstract contract Scheduler is IScheduler, SchedulerState {
         }
     }
 
-    /**
-     * @notice Internal function to store the parsed price feeds.
-     * @param subscriptionId The ID of the subscription.
-     * @param priceFeeds The array of price feeds to store.
-     */
+    /// @notice Internal function to store the parsed price feeds.
+    /// @param subscriptionId The ID of the subscription.
+    /// @param priceFeeds The array of price feeds to store.
     function _storePriceUpdates(
         uint256 subscriptionId,
         PythStructs.PriceFeed[] memory priceFeeds
@@ -789,16 +828,14 @@ abstract contract Scheduler is IScheduler, SchedulerState {
         }
     }
 
-    /**
-     * @notice Internal function to calculate total fees, deduct from balance, and pay the keeper.
-     * @dev This function sends funds to `msg.sender`, so be sure that this is being called by a keeper.
-     * @dev Note that the Pyth fee is already paid in the parsePriceFeedUpdatesWithSlots call.
-     * @param status Storage reference to the subscription's status.
-     * @param startGas Gas remaining at the start of the parent function call.
-     * @param numPriceIds Number of price IDs being updated.
-     */
+    /// @notice Internal function to calculate total fees, deduct from balance, and pay the keeper.
+    /// @dev This function sends funds to `msg.sender`, so be sure that this is being called by a keeper.
+    /// @dev Note that the Pyth fee is already paid in the parsePriceFeedUpdatesWithSlots call.
+    /// @param status Storage reference to the subscription's status.
+    /// @param startGas Gas remaining at the start of the parent function call.
+    /// @param numPriceIds Number of price IDs being updated.
     function _processFeesAndPayKeeper(
-        SubscriptionStatus storage status,
+        SchedulerStructs.SubscriptionStatus storage status,
         uint256 startGas,
         uint256 numPriceIds
     ) internal {
@@ -810,7 +847,7 @@ abstract contract Scheduler is IScheduler, SchedulerState {
 
         // Check balance
         if (status.balanceInWei < totalKeeperFee) {
-            revert InsufficientBalance();
+            revert SchedulerErrors.InsufficientBalance();
         }
 
         status.balanceInWei -= totalKeeperFee;
@@ -819,7 +856,16 @@ abstract contract Scheduler is IScheduler, SchedulerState {
         // Pay keeper and update status
         (bool sent, ) = msg.sender.call{value: totalKeeperFee}("");
         if (!sent) {
-            revert KeeperPaymentFailed();
+            revert SchedulerErrors.KeeperPaymentFailed();
+        }
+    }
+
+    /// @notice Helper to calculate the distance (absolute difference) between two timestamps.
+    function distance(uint x, uint y) internal pure returns (uint) {
+        if (x > y) {
+            return x - y;
+        } else {
+            return y - x;
         }
     }
 }

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

@@ -1,39 +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();
-error MaxDepositLimitExceeded();

+ 3 - 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,8 @@ 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 - 49
target_chains/ethereum/contracts/contracts/pulse/SchedulerState.sol

@@ -3,32 +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 deposit limit for permanent subscriptions in wei
-    uint256 public constant MAX_DEPOSIT_LIMIT = 100 ether;
-
-    /// 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
         uint256 subscriptionNumber;
@@ -44,9 +21,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
@@ -60,29 +37,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.
      */

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

@@ -7,7 +7,8 @@ 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 +56,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 {

File diff ditekan karena terlalu besar
+ 190 - 120
target_chains/ethereum/contracts/forge-test/PulseScheduler.t.sol


+ 9 - 7
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,8 +161,10 @@ 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
-                    .getSubscription(subscriptionIds[i]);
+                (
+                    SchedulerStructs.SubscriptionParams memory params,
+
+                ) = scheduler.getSubscription(subscriptionIds[i]);
                 params.isActive = false;
                 scheduler.updateSubscription(subscriptionIds[i], params);
             }

+ 7 - 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,9 @@ contract PulseSchedulerGovernanceTest is Test {
     function testProposeAdminByUnauthorized() public {
         address unauthorized = address(5);
         vm.prank(unauthorized);
-        vm.expectRevert(Unauthorized.selector);
+        vm.expectRevert(
+            abi.encodeWithSelector(SchedulerErrors.Unauthorized.selector)
+        );
         scheduler.proposeAdmin(admin2);
     }
 
@@ -91,7 +93,9 @@ contract PulseSchedulerGovernanceTest is Test {
 
         address unauthorized = address(5);
         vm.prank(unauthorized);
-        vm.expectRevert(Unauthorized.selector);
+        vm.expectRevert(
+            abi.encodeWithSelector(SchedulerErrors.Unauthorized.selector)
+        );
         scheduler.acceptAdmin();
     }
 

+ 8 - 7
target_chains/ethereum/contracts/forge-test/utils/PulseSchedulerTestUtils.t.sol

@@ -6,6 +6,7 @@ import "forge-std/Test.sol";
 import "@openzeppelin/contracts/utils/math/SafeCast.sol";
 import "../../contracts/pulse/SchedulerUpgradeable.sol";
 import "../../contracts/pulse/SchedulerState.sol";
+import "@pythnetwork/pulse-sdk-solidity/SchedulerStructs.sol";
 import "./MockPriceFeedTestUtils.sol";
 
 abstract contract PulseSchedulerTestUtils is Test, MockPriceFeedTestUtils {
@@ -14,7 +15,7 @@ abstract contract PulseSchedulerTestUtils is Test, MockPriceFeedTestUtils {
         SchedulerUpgradeable scheduler,
         address whitelistedReader
     ) internal returns (uint256) {
-        SchedulerState.SubscriptionParams
+        SchedulerStructs.SubscriptionParams
             memory params = createDefaultSubscriptionParams(
                 2,
                 whitelistedReader
@@ -31,7 +32,7 @@ abstract contract PulseSchedulerTestUtils is Test, MockPriceFeedTestUtils {
         uint8 numFeeds,
         address whitelistedReader
     ) internal returns (uint256) {
-        SchedulerState.SubscriptionParams
+        SchedulerStructs.SubscriptionParams
             memory params = createDefaultSubscriptionParams(
                 numFeeds,
                 whitelistedReader
@@ -45,14 +46,14 @@ abstract contract PulseSchedulerTestUtils is Test, MockPriceFeedTestUtils {
     /// Helper function to add a test subscription with specific update criteria
     function addTestSubscriptionWithUpdateCriteria(
         SchedulerUpgradeable scheduler,
-        SchedulerState.UpdateCriteria memory updateCriteria,
+        SchedulerStructs.UpdateCriteria memory updateCriteria,
         address whitelistedReader
     ) internal returns (uint256) {
         bytes32[] memory priceIds = createPriceIds();
         address[] memory readerWhitelist = new address[](1);
         readerWhitelist[0] = whitelistedReader;
 
-        SchedulerState.SubscriptionParams memory params = SchedulerState
+        SchedulerStructs.SubscriptionParams memory params = SchedulerStructs
             .SubscriptionParams({
                 priceIds: priceIds,
                 readerWhitelist: readerWhitelist,
@@ -72,12 +73,12 @@ abstract contract PulseSchedulerTestUtils is Test, MockPriceFeedTestUtils {
     function createDefaultSubscriptionParams(
         uint8 numFeeds,
         address whitelistedReader
-    ) internal pure returns (SchedulerState.SubscriptionParams memory) {
+    ) internal pure returns (SchedulerStructs.SubscriptionParams memory) {
         bytes32[] memory priceIds = createPriceIds(numFeeds);
         address[] memory readerWhitelist = new address[](1);
         readerWhitelist[0] = whitelistedReader;
 
-        SchedulerState.UpdateCriteria memory updateCriteria = SchedulerState
+        SchedulerStructs.UpdateCriteria memory updateCriteria = SchedulerStructs
             .UpdateCriteria({
                 updateOnHeartbeat: true,
                 heartbeatSeconds: 60,
@@ -86,7 +87,7 @@ abstract contract PulseSchedulerTestUtils is Test, MockPriceFeedTestUtils {
             });
 
         return
-            SchedulerState.SubscriptionParams({
+            SchedulerStructs.SubscriptionParams({
                 priceIds: priceIds,
                 readerWhitelist: readerWhitelist,
                 whitelistEnabled: true,

+ 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",

+ 144 - 0
target_chains/ethereum/pulse_sdk/solidity/IScheduler.sol

@@ -0,0 +1,144 @@
+// 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 "./SchedulerStructs.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(
+        SchedulerStructs.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 (
+            SchedulerStructs.SubscriptionParams memory params,
+            SchedulerStructs.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,
+        SchedulerStructs.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 `getPricesNoOlderThan`.
+    /// @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 price that is no older than `age` seconds of the current time.
+    /// @dev This function is a sanity-checked version of `getPriceUnsafe` which is useful in
+    /// applications that require a sufficiently-recent price. Reverts if the price wasn't updated sufficiently
+    /// recently.
+    /// @return prices - please read the documentation of PythStructs.Price to understand how to use this safely.
+    function getPricesNoOlderThan(
+        uint256 subscriptionId,
+        bytes32[] calldata priceIds,
+        uint256 age
+    ) 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 prices - please read the documentation of PythStructs.Price to understand how to use this safely.
+    function getEmaPricesUnsafe(
+        uint256 subscriptionId,
+        bytes32[] calldata priceIds
+    ) external view returns (PythStructs.Price[] memory prices);
+
+    /// @notice Returns the exponentially-weighted moving average price that is no older than `age_seconds` seconds
+    /// of the current time.
+    /// @dev This function is a sanity-checked version of `getEmaPricesUnsafe` which is useful in
+    /// applications that require a sufficiently-recent price. Reverts if the price wasn't updated sufficiently
+    /// recently.
+    /// @return prices - please read the documentation of PythStructs.Price to understand how to use this safely.
+    function getEmaPricesNoOlderThan(
+        uint256 subscriptionId,
+        bytes32[] calldata priceIds,
+        uint256 age_seconds
+    ) external view returns (PythStructs.Price[] memory prices);
+
+    /// @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,
+            SchedulerStructs.SubscriptionParams[] memory subscriptionParams,
+            uint256 totalCount
+        );
+}

+ 128 - 0
target_chains/ethereum/pulse_sdk/solidity/README.md

@@ -0,0 +1,128 @@
+# Pyth Pulse Solidity SDK
+
+The Pyth Pulse Solidity SDK allows you to interact with the Pyth Pulse protocol, which automatically pushes Pyth price updates to on-chain contracts based on configurable conditions. This SDK provides the interfaces and data structures needed to integrate with the Pulse service.
+
+## Install
+
+### Truffle/Hardhat
+
+If you are using Truffle or Hardhat, simply install the NPM package:
+
+```bash
+npm install @pythnetwork/pulse-sdk-solidity
+```
+
+### Foundry
+
+If you are using Foundry, you will need to create an NPM project if you don't already have one.
+From the root directory of your project, run:
+
+```bash
+npm init -y
+npm install @pythnetwork/pulse-sdk-solidity
+```
+
+Then add the following line to your `remappings.txt` file:
+
+```text
+@pythnetwork/pulse-sdk-solidity/=node_modules/@pythnetwork/pulse-sdk-solidity
+```
+
+## Usage
+
+To use the SDK, you need the address of a Pulse contract on your blockchain.
+
+```solidity
+import "@pythnetwork/pulse-sdk-solidity/IScheduler.sol";
+import "@pythnetwork/pulse-sdk-solidity/SchedulerStructs.sol";
+
+IScheduler pulse = IScheduler(<address>);
+```
+
+## Key Data Structures
+
+### SubscriptionParams
+
+This struct defines the parameters for a Pulse subscription:
+
+```solidity
+struct SubscriptionParams {
+  bytes32[] priceIds; // Array of Pyth price feed IDs to subscribe to
+  address[] readerWhitelist; // Optional array of addresses allowed to read prices
+  bool whitelistEnabled; // Whether to enforce whitelist or allow anyone to read
+  bool isActive; // Whether the subscription is active
+  bool isPermanent; // Whether the subscription can be updated
+  UpdateCriteria updateCriteria; // When to update the price feeds
+}
+```
+
+### SubscriptionStatus
+
+This struct tracks the current status of a Pulse subscription:
+
+```solidity
+struct SubscriptionStatus {
+  uint256 priceLastUpdatedAt; // Timestamp of the last update. All feeds in the subscription are updated together.
+  uint256 balanceInWei; // Balance that will be used to fund the subscription's upkeep.
+  uint256 totalUpdates; // Tracks update count across all feeds in the subscription (increments by number of feeds per update)
+  uint256 totalSpent; // Counter of total fees paid for subscription upkeep in wei.
+}
+```
+
+### UpdateCriteria
+
+This struct defines when price feeds should be updated:
+
+```solidity
+struct UpdateCriteria {
+  bool updateOnHeartbeat; // Should update based on time elapsed
+  uint32 heartbeatSeconds; // Time interval for heartbeat updates
+  bool updateOnDeviation; // Should update on price deviation
+  uint32 deviationThresholdBps; // Price deviation threshold in basis points
+}
+```
+
+## Creating a Subscription
+
+```solidity
+SchedulerStructs.SubscriptionParams memory params = SchedulerStructs.SubscriptionParams({
+    priceIds: new bytes32[](1),
+    readerWhitelist: new address[](1),
+    whitelistEnabled: true,
+    isActive: true,
+    isPermanent: false,
+    updateCriteria: SchedulerStructs.UpdateCriteria({
+        updateOnHeartbeat: true,
+        heartbeatSeconds: 60,
+        updateOnDeviation: true,
+        deviationThresholdBps: 100
+    })
+});
+
+params.priceIds[0] = bytes32(...);  // Pyth price feed ID
+params.readerWhitelist[0] = address(...);  // Allowed reader
+
+uint256 minBalance = pulse.getMinimumBalance(uint8(params.priceIds.length));
+uint256 subscriptionId = pulse.createSubscription{value: minBalance}(params);
+```
+
+## Updating a Subscription
+
+You can update an existing subscription's parameters using the `updateSubscription` method. Only the subscription manager (the address that created it) can update a subscription, and permanent subscriptions cannot be updated afterwards.
+
+## Reading Price Feeds
+
+```solidity
+bytes32[] memory priceIds = new bytes32[](1);
+priceIds[0] = bytes32(...);  // Pyth price feed ID
+
+// Specify maximum age in seconds (e.g., 300 seconds = 5 minutes)
+uint256 maxAge = 300;
+PythStructs.Price[] memory prices = pulse.getPricesNoOlderThan(subscriptionId, priceIds, maxAge);
+
+// Access price data
+int64 price = prices[0].price;
+uint64 conf = prices[0].conf;
+int32 expo = prices[0].expo;
+uint publishTime = prices[0].publishTime;
+```

+ 30 - 0
target_chains/ethereum/pulse_sdk/solidity/SchedulerConstants.sol

@@ -0,0 +1,30 @@
+// SPDX-License-Identifier: Apache 2
+
+pragma solidity ^0.8.0;
+
+// This contract holds the Scheduler structs
+contract SchedulerConstants {
+    /// 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 deposit limit for permanent subscriptions in wei
+    uint256 public constant MAX_DEPOSIT_LIMIT = 100 ether;
+
+    /// 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;
+}

+ 59 - 0
target_chains/ethereum/pulse_sdk/solidity/SchedulerErrors.sol

@@ -0,0 +1,59 @@
+// SPDX-License-Identifier: Apache 2
+
+pragma solidity ^0.8.0;
+
+library SchedulerErrors {
+    // Authorization errors
+    /// 0x82b42900
+    error Unauthorized();
+
+    // Subscription state errors
+    /// 0xe7262b66
+    error InactiveSubscription();
+    /// 0xf4d678b8
+    error InsufficientBalance();
+    /// 0xf6181305
+    error CannotUpdatePermanentSubscription();
+
+    // Price feed errors
+    /// 0xae2eaaa9
+    error InvalidPriceId(bytes32 providedPriceId, bytes32 expectedPriceId);
+    /// 0xf14f93d1
+    error InvalidPriceIdsLength(uint256 providedLength, uint256 expectedLength);
+    /// 0x94ec8d9a
+    error EmptyPriceIds();
+    /// 0xb3d1acf6
+    error TooManyPriceIds(uint256 provided, uint256 maximum);
+    /// 0xe3509591
+    error DuplicatePriceId(bytes32 priceId);
+    /// 0xe56ccfaa
+    error PriceSlotMismatch();
+
+    // Update criteria errors
+    /// 0xa7bcd3ae
+    error InvalidUpdateCriteria();
+    /// 0x7e8b0263
+    error UpdateConditionsNotMet();
+    /// 0x38fdebae
+    error TimestampTooOld(
+        uint256 providedUpdateTimestamp,
+        uint256 currentTimestamp
+    );
+    /// 0x06daa54d
+    error TimestampOlderThanLastUpdate(
+        uint256 providedUpdateTimestamp,
+        uint256 lastUpdatedAt
+    );
+
+    // Whitelist errors
+    /// 0xbe4b60f7
+    error TooManyWhitelistedReaders(uint256 provided, uint256 maximum);
+    /// 0x9941ad5f
+    error DuplicateWhitelistAddress(address addr);
+
+    // Payment errors
+    /// 0xec58cd53
+    error KeeperPaymentFailed();
+    /// 0x82fcf1e2
+    error MaxDepositLimitExceeded();
+}

+ 1 - 1
target_chains/ethereum/contracts/contracts/pulse/SchedulerEvents.sol → target_chains/ethereum/pulse_sdk/solidity/SchedulerEvents.sol

@@ -1,7 +1,7 @@
 // SPDX-License-Identifier: Apache-2.0
 pragma solidity ^0.8.0;
 
-import "./SchedulerState.sol";
+import "./SchedulerStructs.sol";
 
 interface SchedulerEvents {
     event SubscriptionCreated(

+ 33 - 0
target_chains/ethereum/pulse_sdk/solidity/SchedulerStructs.sol

@@ -0,0 +1,33 @@
+// SPDX-License-Identifier: Apache 2
+
+pragma solidity ^0.8.0;
+
+/// @title SchedulerStructs
+/// @notice Contains data structures used by the Pyth Pulse protocol
+contract SchedulerStructs {
+    /// @notice Parameters defining a Pulse subscription
+    struct SubscriptionParams {
+        bytes32[] priceIds; // Array of Pyth price feed IDs to subscribe to
+        address[] readerWhitelist; // Optional array of addresses allowed to read prices
+        bool whitelistEnabled; // Whether to enforce whitelist or allow anyone to read
+        bool isActive; // Whether the subscription is active
+        bool isPermanent; // Whether the subscription can be updated
+        UpdateCriteria updateCriteria; // When to update the price feeds
+    }
+
+    /// @notice Status information for a Pulse subscription
+    struct SubscriptionStatus {
+        uint256 priceLastUpdatedAt; // Timestamp of the last update. All feeds in the subscription are updated together.
+        uint256 balanceInWei; // Balance that will be used to fund the subscription's upkeep.
+        uint256 totalUpdates; // Tracks update count across all feeds in the subscription (increments by number of feeds per update)
+        uint256 totalSpent; // Counter of total fees paid for subscription upkeep in wei.
+    }
+
+    /// @notice Criteria for when price feeds should be updated
+    struct UpdateCriteria {
+        bool updateOnHeartbeat; // Should update based on time elapsed
+        uint32 heartbeatSeconds; // Time interval for heartbeat updates
+        bool updateOnDeviation; // Should update based on price deviation
+        uint32 deviationThresholdBps; // Price deviation threshold in basis points
+    }
+}

+ 674 - 0
target_chains/ethereum/pulse_sdk/solidity/abis/IScheduler.json

@@ -0,0 +1,674 @@
+[
+  {
+    "anonymous": false,
+    "inputs": [
+      {
+        "indexed": true,
+        "internalType": "uint256",
+        "name": "subscriptionId",
+        "type": "uint256"
+      },
+      {
+        "indexed": false,
+        "internalType": "uint256",
+        "name": "timestamp",
+        "type": "uint256"
+      }
+    ],
+    "name": "PricesUpdated",
+    "type": "event"
+  },
+  {
+    "anonymous": false,
+    "inputs": [
+      {
+        "indexed": true,
+        "internalType": "uint256",
+        "name": "subscriptionId",
+        "type": "uint256"
+      }
+    ],
+    "name": "SubscriptionActivated",
+    "type": "event"
+  },
+  {
+    "anonymous": false,
+    "inputs": [
+      {
+        "indexed": true,
+        "internalType": "uint256",
+        "name": "subscriptionId",
+        "type": "uint256"
+      },
+      {
+        "indexed": true,
+        "internalType": "address",
+        "name": "manager",
+        "type": "address"
+      }
+    ],
+    "name": "SubscriptionCreated",
+    "type": "event"
+  },
+  {
+    "anonymous": false,
+    "inputs": [
+      {
+        "indexed": true,
+        "internalType": "uint256",
+        "name": "subscriptionId",
+        "type": "uint256"
+      }
+    ],
+    "name": "SubscriptionDeactivated",
+    "type": "event"
+  },
+  {
+    "anonymous": false,
+    "inputs": [
+      {
+        "indexed": true,
+        "internalType": "uint256",
+        "name": "subscriptionId",
+        "type": "uint256"
+      }
+    ],
+    "name": "SubscriptionUpdated",
+    "type": "event"
+  },
+  {
+    "inputs": [
+      {
+        "internalType": "uint256",
+        "name": "subscriptionId",
+        "type": "uint256"
+      }
+    ],
+    "name": "addFunds",
+    "outputs": [],
+    "stateMutability": "payable",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      {
+        "components": [
+          {
+            "internalType": "bytes32[]",
+            "name": "priceIds",
+            "type": "bytes32[]"
+          },
+          {
+            "internalType": "address[]",
+            "name": "readerWhitelist",
+            "type": "address[]"
+          },
+          {
+            "internalType": "bool",
+            "name": "whitelistEnabled",
+            "type": "bool"
+          },
+          {
+            "internalType": "bool",
+            "name": "isActive",
+            "type": "bool"
+          },
+          {
+            "internalType": "bool",
+            "name": "isPermanent",
+            "type": "bool"
+          },
+          {
+            "components": [
+              {
+                "internalType": "bool",
+                "name": "updateOnHeartbeat",
+                "type": "bool"
+              },
+              {
+                "internalType": "uint32",
+                "name": "heartbeatSeconds",
+                "type": "uint32"
+              },
+              {
+                "internalType": "bool",
+                "name": "updateOnDeviation",
+                "type": "bool"
+              },
+              {
+                "internalType": "uint32",
+                "name": "deviationThresholdBps",
+                "type": "uint32"
+              }
+            ],
+            "internalType": "struct SchedulerStructs.UpdateCriteria",
+            "name": "updateCriteria",
+            "type": "tuple"
+          }
+        ],
+        "internalType": "struct SchedulerStructs.SubscriptionParams",
+        "name": "subscriptionParams",
+        "type": "tuple"
+      }
+    ],
+    "name": "createSubscription",
+    "outputs": [
+      {
+        "internalType": "uint256",
+        "name": "subscriptionId",
+        "type": "uint256"
+      }
+    ],
+    "stateMutability": "payable",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      {
+        "internalType": "uint256",
+        "name": "startIndex",
+        "type": "uint256"
+      },
+      {
+        "internalType": "uint256",
+        "name": "maxResults",
+        "type": "uint256"
+      }
+    ],
+    "name": "getActiveSubscriptions",
+    "outputs": [
+      {
+        "internalType": "uint256[]",
+        "name": "subscriptionIds",
+        "type": "uint256[]"
+      },
+      {
+        "components": [
+          {
+            "internalType": "bytes32[]",
+            "name": "priceIds",
+            "type": "bytes32[]"
+          },
+          {
+            "internalType": "address[]",
+            "name": "readerWhitelist",
+            "type": "address[]"
+          },
+          {
+            "internalType": "bool",
+            "name": "whitelistEnabled",
+            "type": "bool"
+          },
+          {
+            "internalType": "bool",
+            "name": "isActive",
+            "type": "bool"
+          },
+          {
+            "internalType": "bool",
+            "name": "isPermanent",
+            "type": "bool"
+          },
+          {
+            "components": [
+              {
+                "internalType": "bool",
+                "name": "updateOnHeartbeat",
+                "type": "bool"
+              },
+              {
+                "internalType": "uint32",
+                "name": "heartbeatSeconds",
+                "type": "uint32"
+              },
+              {
+                "internalType": "bool",
+                "name": "updateOnDeviation",
+                "type": "bool"
+              },
+              {
+                "internalType": "uint32",
+                "name": "deviationThresholdBps",
+                "type": "uint32"
+              }
+            ],
+            "internalType": "struct SchedulerStructs.UpdateCriteria",
+            "name": "updateCriteria",
+            "type": "tuple"
+          }
+        ],
+        "internalType": "struct SchedulerStructs.SubscriptionParams[]",
+        "name": "subscriptionParams",
+        "type": "tuple[]"
+      },
+      {
+        "internalType": "uint256",
+        "name": "totalCount",
+        "type": "uint256"
+      }
+    ],
+    "stateMutability": "view",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      {
+        "internalType": "uint256",
+        "name": "subscriptionId",
+        "type": "uint256"
+      },
+      {
+        "internalType": "bytes32[]",
+        "name": "priceIds",
+        "type": "bytes32[]"
+      },
+      {
+        "internalType": "uint256",
+        "name": "age_seconds",
+        "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"
+  },
+  {
+    "inputs": [
+      {
+        "internalType": "uint256",
+        "name": "subscriptionId",
+        "type": "uint256"
+      },
+      {
+        "internalType": "bytes32[]",
+        "name": "priceIds",
+        "type": "bytes32[]"
+      }
+    ],
+    "name": "getEmaPricesUnsafe",
+    "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": "uint8",
+        "name": "numPriceFeeds",
+        "type": "uint8"
+      }
+    ],
+    "name": "getMinimumBalance",
+    "outputs": [
+      {
+        "internalType": "uint256",
+        "name": "minimumBalanceInWei",
+        "type": "uint256"
+      }
+    ],
+    "stateMutability": "view",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      {
+        "internalType": "uint256",
+        "name": "subscriptionId",
+        "type": "uint256"
+      },
+      {
+        "internalType": "bytes32[]",
+        "name": "priceIds",
+        "type": "bytes32[]"
+      },
+      {
+        "internalType": "uint256",
+        "name": "age",
+        "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": "priceIds",
+        "type": "bytes32[]"
+      }
+    ],
+    "name": "getPricesUnsafe",
+    "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"
+      }
+    ],
+    "name": "getSubscription",
+    "outputs": [
+      {
+        "components": [
+          {
+            "internalType": "bytes32[]",
+            "name": "priceIds",
+            "type": "bytes32[]"
+          },
+          {
+            "internalType": "address[]",
+            "name": "readerWhitelist",
+            "type": "address[]"
+          },
+          {
+            "internalType": "bool",
+            "name": "whitelistEnabled",
+            "type": "bool"
+          },
+          {
+            "internalType": "bool",
+            "name": "isActive",
+            "type": "bool"
+          },
+          {
+            "internalType": "bool",
+            "name": "isPermanent",
+            "type": "bool"
+          },
+          {
+            "components": [
+              {
+                "internalType": "bool",
+                "name": "updateOnHeartbeat",
+                "type": "bool"
+              },
+              {
+                "internalType": "uint32",
+                "name": "heartbeatSeconds",
+                "type": "uint32"
+              },
+              {
+                "internalType": "bool",
+                "name": "updateOnDeviation",
+                "type": "bool"
+              },
+              {
+                "internalType": "uint32",
+                "name": "deviationThresholdBps",
+                "type": "uint32"
+              }
+            ],
+            "internalType": "struct SchedulerStructs.UpdateCriteria",
+            "name": "updateCriteria",
+            "type": "tuple"
+          }
+        ],
+        "internalType": "struct SchedulerStructs.SubscriptionParams",
+        "name": "params",
+        "type": "tuple"
+      },
+      {
+        "components": [
+          {
+            "internalType": "uint256",
+            "name": "priceLastUpdatedAt",
+            "type": "uint256"
+          },
+          {
+            "internalType": "uint256",
+            "name": "balanceInWei",
+            "type": "uint256"
+          },
+          {
+            "internalType": "uint256",
+            "name": "totalUpdates",
+            "type": "uint256"
+          },
+          {
+            "internalType": "uint256",
+            "name": "totalSpent",
+            "type": "uint256"
+          }
+        ],
+        "internalType": "struct SchedulerStructs.SubscriptionStatus",
+        "name": "status",
+        "type": "tuple"
+      }
+    ],
+    "stateMutability": "view",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      {
+        "internalType": "uint256",
+        "name": "subscriptionId",
+        "type": "uint256"
+      },
+      {
+        "internalType": "bytes[]",
+        "name": "updateData",
+        "type": "bytes[]"
+      }
+    ],
+    "name": "updatePriceFeeds",
+    "outputs": [],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      {
+        "internalType": "uint256",
+        "name": "subscriptionId",
+        "type": "uint256"
+      },
+      {
+        "components": [
+          {
+            "internalType": "bytes32[]",
+            "name": "priceIds",
+            "type": "bytes32[]"
+          },
+          {
+            "internalType": "address[]",
+            "name": "readerWhitelist",
+            "type": "address[]"
+          },
+          {
+            "internalType": "bool",
+            "name": "whitelistEnabled",
+            "type": "bool"
+          },
+          {
+            "internalType": "bool",
+            "name": "isActive",
+            "type": "bool"
+          },
+          {
+            "internalType": "bool",
+            "name": "isPermanent",
+            "type": "bool"
+          },
+          {
+            "components": [
+              {
+                "internalType": "bool",
+                "name": "updateOnHeartbeat",
+                "type": "bool"
+              },
+              {
+                "internalType": "uint32",
+                "name": "heartbeatSeconds",
+                "type": "uint32"
+              },
+              {
+                "internalType": "bool",
+                "name": "updateOnDeviation",
+                "type": "bool"
+              },
+              {
+                "internalType": "uint32",
+                "name": "deviationThresholdBps",
+                "type": "uint32"
+              }
+            ],
+            "internalType": "struct SchedulerStructs.UpdateCriteria",
+            "name": "updateCriteria",
+            "type": "tuple"
+          }
+        ],
+        "internalType": "struct SchedulerStructs.SubscriptionParams",
+        "name": "newSubscriptionParams",
+        "type": "tuple"
+      }
+    ],
+    "name": "updateSubscription",
+    "outputs": [],
+    "stateMutability": "payable",
+    "type": "function"
+  },
+  {
+    "inputs": [
+      {
+        "internalType": "uint256",
+        "name": "subscriptionId",
+        "type": "uint256"
+      },
+      {
+        "internalType": "uint256",
+        "name": "amount",
+        "type": "uint256"
+      }
+    ],
+    "name": "withdrawFunds",
+    "outputs": [],
+    "stateMutability": "nonpayable",
+    "type": "function"
+  }
+]

+ 80 - 0
target_chains/ethereum/pulse_sdk/solidity/abis/SchedulerConstants.json

@@ -0,0 +1,80 @@
+[
+  {
+    "inputs": [],
+    "name": "FUTURE_TIMESTAMP_MAX_VALIDITY_PERIOD",
+    "outputs": [
+      {
+        "internalType": "uint64",
+        "name": "",
+        "type": "uint64"
+      }
+    ],
+    "stateMutability": "view",
+    "type": "function"
+  },
+  {
+    "inputs": [],
+    "name": "GAS_OVERHEAD",
+    "outputs": [
+      {
+        "internalType": "uint256",
+        "name": "",
+        "type": "uint256"
+      }
+    ],
+    "stateMutability": "view",
+    "type": "function"
+  },
+  {
+    "inputs": [],
+    "name": "MAX_DEPOSIT_LIMIT",
+    "outputs": [
+      {
+        "internalType": "uint256",
+        "name": "",
+        "type": "uint256"
+      }
+    ],
+    "stateMutability": "view",
+    "type": "function"
+  },
+  {
+    "inputs": [],
+    "name": "MAX_PRICE_IDS_PER_SUBSCRIPTION",
+    "outputs": [
+      {
+        "internalType": "uint8",
+        "name": "",
+        "type": "uint8"
+      }
+    ],
+    "stateMutability": "view",
+    "type": "function"
+  },
+  {
+    "inputs": [],
+    "name": "MAX_READER_WHITELIST_SIZE",
+    "outputs": [
+      {
+        "internalType": "uint8",
+        "name": "",
+        "type": "uint8"
+      }
+    ],
+    "stateMutability": "view",
+    "type": "function"
+  },
+  {
+    "inputs": [],
+    "name": "PAST_TIMESTAMP_MAX_VALIDITY_PERIOD",
+    "outputs": [
+      {
+        "internalType": "uint64",
+        "name": "",
+        "type": "uint64"
+      }
+    ],
+    "stateMutability": "view",
+    "type": "function"
+  }
+]

+ 170 - 0
target_chains/ethereum/pulse_sdk/solidity/abis/SchedulerErrors.json

@@ -0,0 +1,170 @@
+[
+  {
+    "inputs": [],
+    "name": "CannotUpdatePermanentSubscription",
+    "type": "error"
+  },
+  {
+    "inputs": [
+      {
+        "internalType": "bytes32",
+        "name": "priceId",
+        "type": "bytes32"
+      }
+    ],
+    "name": "DuplicatePriceId",
+    "type": "error"
+  },
+  {
+    "inputs": [
+      {
+        "internalType": "address",
+        "name": "addr",
+        "type": "address"
+      }
+    ],
+    "name": "DuplicateWhitelistAddress",
+    "type": "error"
+  },
+  {
+    "inputs": [],
+    "name": "EmptyPriceIds",
+    "type": "error"
+  },
+  {
+    "inputs": [],
+    "name": "InactiveSubscription",
+    "type": "error"
+  },
+  {
+    "inputs": [],
+    "name": "InsufficientBalance",
+    "type": "error"
+  },
+  {
+    "inputs": [
+      {
+        "internalType": "bytes32",
+        "name": "providedPriceId",
+        "type": "bytes32"
+      },
+      {
+        "internalType": "bytes32",
+        "name": "expectedPriceId",
+        "type": "bytes32"
+      }
+    ],
+    "name": "InvalidPriceId",
+    "type": "error"
+  },
+  {
+    "inputs": [
+      {
+        "internalType": "uint256",
+        "name": "providedLength",
+        "type": "uint256"
+      },
+      {
+        "internalType": "uint256",
+        "name": "expectedLength",
+        "type": "uint256"
+      }
+    ],
+    "name": "InvalidPriceIdsLength",
+    "type": "error"
+  },
+  {
+    "inputs": [],
+    "name": "InvalidUpdateCriteria",
+    "type": "error"
+  },
+  {
+    "inputs": [],
+    "name": "KeeperPaymentFailed",
+    "type": "error"
+  },
+  {
+    "inputs": [],
+    "name": "MaxDepositLimitExceeded",
+    "type": "error"
+  },
+  {
+    "inputs": [],
+    "name": "PriceSlotMismatch",
+    "type": "error"
+  },
+  {
+    "inputs": [
+      {
+        "internalType": "uint256",
+        "name": "providedUpdateTimestamp",
+        "type": "uint256"
+      },
+      {
+        "internalType": "uint256",
+        "name": "lastUpdatedAt",
+        "type": "uint256"
+      }
+    ],
+    "name": "TimestampOlderThanLastUpdate",
+    "type": "error"
+  },
+  {
+    "inputs": [
+      {
+        "internalType": "uint256",
+        "name": "providedUpdateTimestamp",
+        "type": "uint256"
+      },
+      {
+        "internalType": "uint256",
+        "name": "currentTimestamp",
+        "type": "uint256"
+      }
+    ],
+    "name": "TimestampTooOld",
+    "type": "error"
+  },
+  {
+    "inputs": [
+      {
+        "internalType": "uint256",
+        "name": "provided",
+        "type": "uint256"
+      },
+      {
+        "internalType": "uint256",
+        "name": "maximum",
+        "type": "uint256"
+      }
+    ],
+    "name": "TooManyPriceIds",
+    "type": "error"
+  },
+  {
+    "inputs": [
+      {
+        "internalType": "uint256",
+        "name": "provided",
+        "type": "uint256"
+      },
+      {
+        "internalType": "uint256",
+        "name": "maximum",
+        "type": "uint256"
+      }
+    ],
+    "name": "TooManyWhitelistedReaders",
+    "type": "error"
+  },
+  {
+    "inputs": [],
+    "name": "Unauthorized",
+    "type": "error"
+  },
+  {
+    "inputs": [],
+    "name": "UpdateConditionsNotMet",
+    "type": "error"
+  }
+]

+ 79 - 0
target_chains/ethereum/pulse_sdk/solidity/abis/SchedulerEvents.json

@@ -0,0 +1,79 @@
+[
+  {
+    "anonymous": false,
+    "inputs": [
+      {
+        "indexed": true,
+        "internalType": "uint256",
+        "name": "subscriptionId",
+        "type": "uint256"
+      },
+      {
+        "indexed": false,
+        "internalType": "uint256",
+        "name": "timestamp",
+        "type": "uint256"
+      }
+    ],
+    "name": "PricesUpdated",
+    "type": "event"
+  },
+  {
+    "anonymous": false,
+    "inputs": [
+      {
+        "indexed": true,
+        "internalType": "uint256",
+        "name": "subscriptionId",
+        "type": "uint256"
+      }
+    ],
+    "name": "SubscriptionActivated",
+    "type": "event"
+  },
+  {
+    "anonymous": false,
+    "inputs": [
+      {
+        "indexed": true,
+        "internalType": "uint256",
+        "name": "subscriptionId",
+        "type": "uint256"
+      },
+      {
+        "indexed": true,
+        "internalType": "address",
+        "name": "manager",
+        "type": "address"
+      }
+    ],
+    "name": "SubscriptionCreated",
+    "type": "event"
+  },
+  {
+    "anonymous": false,
+    "inputs": [
+      {
+        "indexed": true,
+        "internalType": "uint256",
+        "name": "subscriptionId",
+        "type": "uint256"
+      }
+    ],
+    "name": "SubscriptionDeactivated",
+    "type": "event"
+  },
+  {
+    "anonymous": false,
+    "inputs": [
+      {
+        "indexed": true,
+        "internalType": "uint256",
+        "name": "subscriptionId",
+        "type": "uint256"
+      }
+    ],
+    "name": "SubscriptionUpdated",
+    "type": "event"
+  }
+]

+ 1 - 0
target_chains/ethereum/pulse_sdk/solidity/abis/SchedulerStructs.json

@@ -0,0 +1 @@
+[]

+ 40 - 0
target_chains/ethereum/pulse_sdk/solidity/package.json

@@ -0,0 +1,40 @@
+{
+  "name": "@pythnetwork/pulse-sdk-solidity",
+  "version": "1.0.0",
+  "description": "Automatically update price feeds with Pyth Pulse",
+  "type": "module",
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/pyth-network/pyth-crosschain",
+    "directory": "target_chains/ethereum/pulse_sdk/solidity"
+  },
+  "publishConfig": {
+    "access": "public"
+  },
+  "scripts": {
+    "test:format": "prettier --check .",
+    "fix:format": "prettier --write .",
+    "build": "generate-abis IScheduler SchedulerConstants SchedulerErrors SchedulerEvents SchedulerStructs",
+    "test": "git diff --exit-code abis"
+  },
+  "keywords": [
+    "pyth",
+    "solidity",
+    "price feed",
+    "pulse"
+  ],
+  "author": "Douro Labs",
+  "license": "Apache-2.0",
+  "bugs": {
+    "url": "https://github.com/pyth-network/pyth-crosschain/issues"
+  },
+  "homepage": "https://github.com/pyth-network/pyth-crosschain/tree/main/target_chains/ethereum/pulse_sdk/solidity",
+  "devDependencies": {
+    "abi_generator": "workspace:*",
+    "prettier": "catalog:",
+    "prettier-plugin-solidity": "catalog:"
+  },
+  "dependencies": {
+    "@pythnetwork/pyth-sdk-solidity": "workspace:*"
+  }
+}

+ 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"]
+    }
+  }
+}

Beberapa file tidak ditampilkan karena terlalu banyak file yang berubah dalam diff ini