@oddmaki-protocol/sdk 1.9.0 → 1.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -9,7 +9,7 @@ var CONTRACT_ADDRESSES = {
9
9
  [chains.baseSepolia.id]: {
10
10
  diamond: "0x31a4126aec35b36d46dd371eb0f0d5b71e1c2292",
11
11
  conditionalTokens: "0x7364747372Ac4a175B5326f5B2C9CB1C271d32e8",
12
- usdc: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
12
+ usdc: "0x036cbd53842c5426634e7929541ec2318f3dcf7e",
13
13
  subgraph: "https://api.studio.thegraph.com/query/1716020/oddmaki-base-sepolia/version/latest"
14
14
  },
15
15
  [chains.base.id]: {
@@ -5214,6 +5214,11 @@ var PythResolutionFacet_default = [
5214
5214
  type: "int64",
5215
5215
  internalType: "int64"
5216
5216
  },
5217
+ {
5218
+ name: "openTime",
5219
+ type: "uint256",
5220
+ internalType: "uint256"
5221
+ },
5217
5222
  {
5218
5223
  name: "closeTime",
5219
5224
  type: "uint256",
@@ -5253,11 +5258,6 @@ var PythResolutionFacet_default = [
5253
5258
  name: "resolutionWindow",
5254
5259
  type: "uint256",
5255
5260
  internalType: "uint256"
5256
- },
5257
- {
5258
- name: "pythUpdateData",
5259
- type: "bytes[]",
5260
- internalType: "bytes[]"
5261
5261
  }
5262
5262
  ],
5263
5263
  outputs: [
@@ -5267,20 +5267,7 @@ var PythResolutionFacet_default = [
5267
5267
  internalType: "uint256"
5268
5268
  }
5269
5269
  ],
5270
- stateMutability: "payable"
5271
- },
5272
- {
5273
- type: "function",
5274
- name: "getOpenMaxStaleness",
5275
- inputs: [],
5276
- outputs: [
5277
- {
5278
- name: "",
5279
- type: "uint256",
5280
- internalType: "uint256"
5281
- }
5282
- ],
5283
- stateMutability: "view"
5270
+ stateMutability: "nonpayable"
5284
5271
  },
5285
5272
  {
5286
5273
  type: "function",
@@ -5297,34 +5284,34 @@ var PythResolutionFacet_default = [
5297
5284
  },
5298
5285
  {
5299
5286
  type: "function",
5300
- name: "resolvePriceMarketPyth",
5287
+ name: "markPriceMarketInvalid",
5301
5288
  inputs: [
5302
5289
  {
5303
5290
  name: "marketId",
5304
5291
  type: "uint256",
5305
5292
  internalType: "uint256"
5306
- },
5307
- {
5308
- name: "pythUpdateData",
5309
- type: "bytes[]",
5310
- internalType: "bytes[]"
5311
5293
  }
5312
5294
  ],
5313
5295
  outputs: [],
5314
- stateMutability: "payable"
5296
+ stateMutability: "nonpayable"
5315
5297
  },
5316
5298
  {
5317
5299
  type: "function",
5318
- name: "setOpenMaxStaleness",
5300
+ name: "resolvePriceMarketPyth",
5319
5301
  inputs: [
5320
5302
  {
5321
- name: "openMaxStaleness",
5303
+ name: "marketId",
5322
5304
  type: "uint256",
5323
5305
  internalType: "uint256"
5306
+ },
5307
+ {
5308
+ name: "pythUpdateData",
5309
+ type: "bytes[]",
5310
+ internalType: "bytes[]"
5324
5311
  }
5325
5312
  ],
5326
5313
  outputs: [],
5327
- stateMutability: "nonpayable"
5314
+ stateMutability: "payable"
5328
5315
  },
5329
5316
  {
5330
5317
  type: "function",
@@ -5462,19 +5449,6 @@ var PythResolutionFacet_default = [
5462
5449
  ],
5463
5450
  anonymous: false
5464
5451
  },
5465
- {
5466
- type: "event",
5467
- name: "OpenMaxStalenessUpdated",
5468
- inputs: [
5469
- {
5470
- name: "openMaxStaleness",
5471
- type: "uint256",
5472
- indexed: false,
5473
- internalType: "uint256"
5474
- }
5475
- ],
5476
- anonymous: false
5477
- },
5478
5452
  {
5479
5453
  type: "event",
5480
5454
  name: "PriceMarketCreatedPyth",
@@ -5530,6 +5504,25 @@ var PythResolutionFacet_default = [
5530
5504
  ],
5531
5505
  anonymous: false
5532
5506
  },
5507
+ {
5508
+ type: "event",
5509
+ name: "PriceMarketInvalidated",
5510
+ inputs: [
5511
+ {
5512
+ name: "marketId",
5513
+ type: "uint256",
5514
+ indexed: true,
5515
+ internalType: "uint256"
5516
+ },
5517
+ {
5518
+ name: "caller",
5519
+ type: "address",
5520
+ indexed: true,
5521
+ internalType: "address"
5522
+ }
5523
+ ],
5524
+ anonymous: false
5525
+ },
5533
5526
  {
5534
5527
  type: "event",
5535
5528
  name: "PriceMarketResolvedPyth",
@@ -5552,6 +5545,12 @@ var PythResolutionFacet_default = [
5552
5545
  indexed: false,
5553
5546
  internalType: "int64"
5554
5547
  },
5548
+ {
5549
+ name: "openPriceTime",
5550
+ type: "uint256",
5551
+ indexed: false,
5552
+ internalType: "uint256"
5553
+ },
5555
5554
  {
5556
5555
  name: "outcome",
5557
5556
  type: "string",
@@ -5595,17 +5594,17 @@ var PythResolutionFacet_default = [
5595
5594
  },
5596
5595
  {
5597
5596
  type: "error",
5598
- name: "CloseTimeNotReached",
5597
+ name: "AssertionInProgress",
5599
5598
  inputs: []
5600
5599
  },
5601
5600
  {
5602
5601
  type: "error",
5603
- name: "CloseTimeTooFar",
5602
+ name: "CloseTimeNotAfterOpenTime",
5604
5603
  inputs: []
5605
5604
  },
5606
5605
  {
5607
5606
  type: "error",
5608
- name: "CloseTimeTooSoon",
5607
+ name: "CloseTimeNotReached",
5609
5608
  inputs: []
5610
5609
  },
5611
5610
  {
@@ -5639,6 +5638,11 @@ var PythResolutionFacet_default = [
5639
5638
  }
5640
5639
  ]
5641
5640
  },
5641
+ {
5642
+ type: "error",
5643
+ name: "GracePeriodNotElapsed",
5644
+ inputs: []
5645
+ },
5642
5646
  {
5643
5647
  type: "error",
5644
5648
  name: "InsufficientPythFee",
@@ -5664,6 +5668,11 @@ var PythResolutionFacet_default = [
5664
5668
  name: "InvalidMarketId",
5665
5669
  inputs: []
5666
5670
  },
5671
+ {
5672
+ type: "error",
5673
+ name: "InvalidOpenTime",
5674
+ inputs: []
5675
+ },
5667
5676
  {
5668
5677
  type: "error",
5669
5678
  name: "InvalidOutcomesLength",
@@ -5681,7 +5690,12 @@ var PythResolutionFacet_default = [
5681
5690
  },
5682
5691
  {
5683
5692
  type: "error",
5684
- name: "NoValidPriceUpdate",
5693
+ name: "NoClosePriceInWindow",
5694
+ inputs: []
5695
+ },
5696
+ {
5697
+ type: "error",
5698
+ name: "NoOpenPriceInWindow",
5685
5699
  inputs: []
5686
5700
  },
5687
5701
  {
@@ -6708,6 +6722,24 @@ var ERC20_default = [
6708
6722
  ],
6709
6723
  stateMutability: "view"
6710
6724
  },
6725
+ {
6726
+ type: "function",
6727
+ name: "mint",
6728
+ inputs: [
6729
+ {
6730
+ name: "to",
6731
+ type: "address",
6732
+ internalType: "address"
6733
+ },
6734
+ {
6735
+ name: "amount",
6736
+ type: "uint256",
6737
+ internalType: "uint256"
6738
+ }
6739
+ ],
6740
+ outputs: [],
6741
+ stateMutability: "nonpayable"
6742
+ },
6711
6743
  {
6712
6744
  type: "function",
6713
6745
  name: "name",
@@ -6926,6 +6958,24 @@ var UmaOracle_default = [
6926
6958
  ],
6927
6959
  stateMutability: "view"
6928
6960
  },
6961
+ {
6962
+ type: "function",
6963
+ name: "disputeAssertion",
6964
+ inputs: [
6965
+ {
6966
+ name: "assertionId",
6967
+ type: "bytes32",
6968
+ internalType: "bytes32"
6969
+ },
6970
+ {
6971
+ name: "disputer",
6972
+ type: "address",
6973
+ internalType: "address"
6974
+ }
6975
+ ],
6976
+ outputs: [],
6977
+ stateMutability: "nonpayable"
6978
+ },
6929
6979
  {
6930
6980
  type: "function",
6931
6981
  name: "getAssertion",
@@ -7103,24 +7153,6 @@ var UmaOracle_default = [
7103
7153
  ],
7104
7154
  outputs: [],
7105
7155
  stateMutability: "nonpayable"
7106
- },
7107
- {
7108
- type: "function",
7109
- name: "disputeAssertion",
7110
- inputs: [
7111
- {
7112
- name: "assertionId",
7113
- type: "bytes32",
7114
- internalType: "bytes32"
7115
- },
7116
- {
7117
- name: "disputer",
7118
- type: "address",
7119
- internalType: "address"
7120
- }
7121
- ],
7122
- outputs: [],
7123
- stateMutability: "nonpayable"
7124
7156
  }
7125
7157
  ];
7126
7158
 
@@ -11458,41 +11490,45 @@ var DEFAULT_FRESH_MAX_AGE_SECONDS = 120;
11458
11490
  var DEFAULT_FRESH_MAX_ATTEMPTS = 3;
11459
11491
  var PriceMarketModule = class extends BaseModule {
11460
11492
  /**
11461
- * Create a Pyth-powered price market
11493
+ * Create a Pyth-powered price market. Three shapes via one entry point:
11494
+ *
11495
+ * - **Immediate Up/Down** (`strikePrice = 0`, `openTime = 0` or omitted):
11496
+ * `openTime` is set to `block.timestamp` at creation; open price is
11497
+ * captured at resolution from the earliest Hermes VAA in
11498
+ * `[openTime, openTime + resolutionWindow]`.
11462
11499
  *
11463
- * When strikePrice > 0, creates a strike market resolved against
11464
- * the explicit target price. No Pyth update data or ETH is needed.
11465
- * When strikePrice is 0 or omitted, captures the current Pyth price
11466
- * and uses it as the reference (standard Up/Down market).
11500
+ * - **Scheduled Up/Down** (`strikePrice = 0`, `openTime > now`):
11501
+ * Market exists from creation and is tradable. At `openTime` the open
11502
+ * price window begins; capture happens at resolution as above.
11503
+ *
11504
+ * - **Explicit strike** (`strikePrice > 0`): Above/Below market resolved
11505
+ * against the caller-supplied target. `openTime` is ignored and stored
11506
+ * as `block.timestamp`; no open-price capture occurs at resolution.
11507
+ *
11508
+ * Creation never touches Pyth — no VAA submission, no Pyth fee. The
11509
+ * resolver pays Pyth fees later when calling {@link resolvePyth}.
11467
11510
  *
11468
11511
  * @param params.venueId - The venue to create the market in
11469
11512
  * @param params.pythFeedId - Pyth price feed ID (e.g., ETH/USD)
11470
- * @param params.strikePrice - Target price in feed's exponent scale (0 = use current price)
11471
- * @param params.closeTime - Absolute close timestamp (must be 300–86400s from now)
11513
+ * @param params.strikePrice - Target price (0 = capture open at resolution)
11514
+ * @param params.openTime - When the market opens (0 = immediate / now)
11515
+ * @param params.closeTime - Absolute close timestamp (must be > effective openTime)
11472
11516
  * @param params.outcomes - Market outcome labels (e.g., ["Up", "Down"] or ["Above", "Below"])
11473
- * @param params.tickSize - Price increment for the orderbook
11517
+ * @param params.tickSize - Price increment for the orderbook (1e15 or 1e16)
11474
11518
  * @param params.collateralToken - ERC20 collateral token address
11475
- * @param params.question - Market question for UMA ancillary data (fallback)
11519
+ * @param params.question - Market question for ancillary data
11476
11520
  * @param params.liveness - UMA challenge period in seconds (0 = default)
11477
11521
  * @param params.tags - Optional tags for the market
11478
- * @param params.resolutionWindow - Pyth timestamp tolerance in seconds (0 = default 60s)
11522
+ * @param params.resolutionWindow - Pyth timestamp tolerance in seconds (0 = default 60s, max 300s)
11479
11523
  */
11480
11524
  async createPyth(params) {
11481
11525
  const wallet = this.walletClient;
11482
11526
  const account = await this.getSignerAccount();
11483
11527
  const outcomes = params.outcomes ?? ["Up", "Down"];
11484
- const isStrikeMarket = params.strikePrice && params.strikePrice > BigInt(0);
11485
11528
  const { encodedTags, ancillaryData } = await this._prepareCreationCommon(params, await this.getSignerAddress());
11486
11529
  if (!isValidTickSize(params.tickSize)) {
11487
11530
  throw new Error("Invalid tickSize: must be 1e15 (0.1%) or 1e16 (1%)");
11488
11531
  }
11489
- let pythUpdateData = [];
11490
- let valueSent = BigInt(0);
11491
- if (!isStrikeMarket) {
11492
- const pythResult = await this._preparePythUpdate(params.pythFeedId);
11493
- pythUpdateData = pythResult.pythUpdateData;
11494
- valueSent = pythResult.valueSent;
11495
- }
11496
11532
  const { request } = await this.publicClient.simulateContract({
11497
11533
  address: this.config.diamondAddress,
11498
11534
  abi: PythResolutionFacet_default,
@@ -11501,6 +11537,7 @@ var PriceMarketModule = class extends BaseModule {
11501
11537
  params.venueId,
11502
11538
  params.pythFeedId,
11503
11539
  params.strikePrice ?? BigInt(0),
11540
+ params.openTime ?? BigInt(0),
11504
11541
  params.closeTime,
11505
11542
  outcomes,
11506
11543
  params.tickSize,
@@ -11508,20 +11545,21 @@ var PriceMarketModule = class extends BaseModule {
11508
11545
  ancillaryData,
11509
11546
  params.liveness ?? BigInt(0),
11510
11547
  encodedTags,
11511
- params.resolutionWindow ?? BigInt(0),
11512
- pythUpdateData
11548
+ params.resolutionWindow ?? BigInt(0)
11513
11549
  ],
11514
- account,
11515
- value: valueSent
11550
+ account
11516
11551
  });
11517
11552
  return wallet.writeContract(request);
11518
11553
  }
11519
11554
  /**
11520
- * Resolve a price market using Pyth closing price
11555
+ * Resolve a price market using Pyth.
11521
11556
  *
11522
- * Anyone can call this after the market's closeTime has passed.
11523
- * Fetches the historical Pyth price at closeTime and submits it.
11524
- * Works for both standard price markets and strike markets.
11557
+ * Anyone can call this after `closeTime`. For deferred Up/Down markets
11558
+ * (`strikePrice == 0`), the SDK fetches **two** Hermes VAAs one for the
11559
+ * open window, one for the close window and submits them together so the
11560
+ * on-chain facet can capture the open price and compare against the close
11561
+ * in a single transaction. For explicit-strike markets only the close VAA
11562
+ * is fetched.
11525
11563
  */
11526
11564
  async resolvePyth(marketId) {
11527
11565
  const wallet = this.walletClient;
@@ -11536,10 +11574,24 @@ var PriceMarketModule = class extends BaseModule {
11536
11574
  `Close time not reached. Current: ${now.toString()}, CloseTime: ${pm.closeTime.toString()}`
11537
11575
  );
11538
11576
  }
11539
- const pythUpdateData = await this.fetchPythHistoricalData(
11577
+ const isDeferred = pm.strikePrice === BigInt(0);
11578
+ const windowSeconds = Number(pm.resolutionWindow);
11579
+ const closeVAA = await this.fetchPythHistoricalData(
11540
11580
  pm.feedId,
11541
- Number(pm.closeTime)
11581
+ Number(pm.closeTime),
11582
+ windowSeconds
11542
11583
  );
11584
+ let pythUpdateData;
11585
+ if (isDeferred) {
11586
+ const openVAA = await this.fetchPythHistoricalData(
11587
+ pm.feedId,
11588
+ Number(pm.openTime),
11589
+ windowSeconds
11590
+ );
11591
+ pythUpdateData = [...openVAA, ...closeVAA];
11592
+ } else {
11593
+ pythUpdateData = closeVAA;
11594
+ }
11543
11595
  const pythAddress = await this.getPythContract();
11544
11596
  const pythFee = await this.publicClient.readContract({
11545
11597
  address: pythAddress,
@@ -11570,7 +11622,7 @@ var PriceMarketModule = class extends BaseModule {
11570
11622
  });
11571
11623
  }
11572
11624
  /**
11573
- * Check if a price market can be resolved
11625
+ * Check if a price market can be resolved (closeTime reached and not yet resolved).
11574
11626
  */
11575
11627
  async canResolve(marketId) {
11576
11628
  return await this.publicClient.readContract({
@@ -11628,36 +11680,6 @@ var PriceMarketModule = class extends BaseModule {
11628
11680
  });
11629
11681
  return wallet.writeContract(request);
11630
11682
  }
11631
- /**
11632
- * Get the effective opening-price staleness window in seconds.
11633
- *
11634
- * A submitted VAA's `publishTime` must fall within
11635
- * `[block.timestamp - openMaxStaleness, block.timestamp + OPEN_FUTURE_SKEW]`
11636
- * at `createPriceMarketPyth` time. Defaults to 300s when unset on-chain.
11637
- */
11638
- async getOpenMaxStaleness() {
11639
- return await this.publicClient.readContract({
11640
- address: this.config.diamondAddress,
11641
- abi: PythResolutionFacet_default,
11642
- functionName: "getOpenMaxStaleness"
11643
- });
11644
- }
11645
- /**
11646
- * Set the opening-price staleness window (seconds). Diamond owner only.
11647
- * Pass 0 to fall back to the built-in default.
11648
- */
11649
- async setOpenMaxStaleness(openMaxStaleness) {
11650
- const wallet = this.walletClient;
11651
- const account = await this.getSignerAccount();
11652
- const { request } = await this.publicClient.simulateContract({
11653
- address: this.config.diamondAddress,
11654
- abi: PythResolutionFacet_default,
11655
- functionName: "setOpenMaxStaleness",
11656
- args: [openMaxStaleness],
11657
- account
11658
- });
11659
- return wallet.writeContract(request);
11660
- }
11661
11683
  // ---- Private helpers ----
11662
11684
  /**
11663
11685
  * Shared pre-flight checks: creation fee allowance, ancillary data, tags
@@ -11689,33 +11711,18 @@ var PriceMarketModule = class extends BaseModule {
11689
11711
  );
11690
11712
  return { encodedTags, ancillaryData };
11691
11713
  }
11692
- /**
11693
- * Fetch Pyth update data and compute the required ETH value (for Up/Down markets)
11694
- */
11695
- async _preparePythUpdate(feedId) {
11696
- const pythUpdateData = await this.fetchPythUpdateData(feedId);
11697
- const pythAddress = await this.getPythContract();
11698
- const pythFee = await this.publicClient.readContract({
11699
- address: pythAddress,
11700
- abi: PYTH_GET_UPDATE_FEE_ABI,
11701
- functionName: "getUpdateFee",
11702
- args: [pythUpdateData]
11703
- });
11704
- const valueSent = pythFee > BigInt(0) ? pythFee : BigInt(1e9);
11705
- return { pythUpdateData, valueSent };
11706
- }
11707
11714
  formatAncillaryData(question) {
11708
11715
  let data = `q:title:${question.title}`;
11709
11716
  data += `,description:${question.description}`;
11710
11717
  return viem.stringToHex(data);
11711
11718
  }
11712
11719
  /**
11713
- * Fetch latest Pyth price update from Hermes — bytes plus the VAA's publishTime.
11720
+ * Fetch the latest Pyth price update from Hermes — the bytes plus the
11721
+ * VAA's publishTime.
11714
11722
  *
11715
- * Use this (or {@link fetchFreshPythUpdate}) instead of re-rolling Hermes
11716
- * calls when building `createPriceMarketPyth` transactions. The on-chain
11717
- * staleness window defaults to 300s — if the user takes longer than that
11718
- * to sign, the VAA will be rejected.
11723
+ * Useful for UI projected-strike display: callers can render the current
11724
+ * Pyth price (and its publishTime) before a deferred market's open window
11725
+ * closes, then switch to the on-chain strike once resolution happens.
11719
11726
  */
11720
11727
  async fetchPythLatestUpdate(feedId) {
11721
11728
  const url = `${PYTH_HERMES_BASE}/v2/updates/price/latest?ids[]=${feedId}`;
@@ -11742,14 +11749,14 @@ var PriceMarketModule = class extends BaseModule {
11742
11749
  };
11743
11750
  }
11744
11751
  /**
11745
- * Return a Pyth update that is fresh enough to pass the on-chain staleness check.
11752
+ * Return a Pyth update that is fresh enough for client-side projected-strike
11753
+ * display.
11746
11754
  *
11747
- * If `cached` is provided and still within `maxAgeSeconds`, it is returned as-is.
11748
- * Otherwise Hermes is re-queried; if the newly fetched VAA is also older than
11749
- * `maxAgeSeconds` (e.g. feed is quiet), it retries up to `maxAttempts`.
11755
+ * If `cached` is provided and still within `maxAgeSeconds`, returns it
11756
+ * unchanged. Otherwise re-queries Hermes; if the new VAA is also stale
11757
+ * (quiet feed), retries up to `maxAttempts`.
11750
11758
  *
11751
- * Defaults: `maxAgeSeconds = 120` (leaves ~180s headroom under the 300s default
11752
- * on-chain window), `maxAttempts = 3`.
11759
+ * Defaults: `maxAgeSeconds = 120`, `maxAttempts = 3`.
11753
11760
  */
11754
11761
  async fetchFreshPythUpdate(feedId, options = {}) {
11755
11762
  const maxAgeSeconds = options.maxAgeSeconds ?? DEFAULT_FRESH_MAX_AGE_SECONDS;
@@ -11772,27 +11779,145 @@ var PriceMarketModule = class extends BaseModule {
11772
11779
  return latest;
11773
11780
  }
11774
11781
  /**
11775
- * Fetch latest Pyth price update data from Hermes API (raw bytes only).
11782
+ * Resolve the projected open price for a deferred Up/Down market what the
11783
+ * UI should display as the "strike" between `openTime` and resolution.
11784
+ *
11785
+ * For deferred markets, the on-chain `strikePrice` is 0 until the resolver
11786
+ * fires {@link resolvePyth}. This helper replicates the same rule the
11787
+ * contract will use ("earliest in-window Hermes VAA in
11788
+ * `[openTime, openTime + resolutionWindow]`") by querying Hermes at
11789
+ * `openTime`, so the value rendered in the UI matches what the on-chain
11790
+ * strike will be once resolution lands.
11791
+ *
11792
+ * Returns `null` when the projection isn't applicable:
11793
+ * - The market is already resolved — caller should read `pm.strikePrice` directly.
11794
+ * - The market is explicit-strike (`pm.strikePrice > 0`) — same, read from chain.
11795
+ * - The market hasn't reached `openTime` yet — no VAA exists; UI should show
11796
+ * "strike set at HH:MM" or similar pending state.
11797
+ *
11798
+ * `result.canonical === true` means the open window has fully elapsed and the
11799
+ * value will not change. Before that, render the price with a "pending" hint.
11776
11800
  */
11777
- async fetchPythUpdateData(feedId) {
11778
- const { updateData } = await this.fetchPythLatestUpdate(feedId);
11779
- return updateData;
11801
+ async fetchProjectedOpenPrice(marketId) {
11802
+ const pm = await this.get(marketId);
11803
+ if (pm.resolved) return null;
11804
+ if (pm.strikePrice !== BigInt(0)) return null;
11805
+ const now = BigInt(Math.floor(Date.now() / 1e3));
11806
+ if (now < pm.openTime) return null;
11807
+ const parsed = await this.fetchPythHistoricalParsed(
11808
+ pm.feedId,
11809
+ Number(pm.openTime),
11810
+ Number(pm.resolutionWindow)
11811
+ );
11812
+ if (!parsed) return null;
11813
+ return {
11814
+ price: BigInt(parsed.price.price),
11815
+ publishTime: BigInt(parsed.price.publish_time),
11816
+ expo: Number(parsed.price.expo),
11817
+ canonical: now >= pm.openTime + pm.resolutionWindow,
11818
+ openTime: pm.openTime
11819
+ };
11780
11820
  }
11781
11821
  /**
11782
- * Fetch historical Pyth price update data at a specific timestamp
11822
+ * Fetch historical Pyth price update data covering the window
11823
+ * `[publishTime, publishTime + windowSeconds]`.
11824
+ *
11825
+ * Hermes serves a VAA AT the exact requested second when one exists, and
11826
+ * 404s when no publish landed in that second. For low-volume feeds or
11827
+ * markets resolving deep in the past this means a single request at
11828
+ * `publishTime` is fragile. This helper walks a few sample points across
11829
+ * the window until it finds a VAA, transparently retrying with exponential
11830
+ * backoff on 429.
11831
+ *
11832
+ * The on-chain `pickEarliestInWindow` accepts any VAA whose `publishTime`
11833
+ * falls in the same window, so returning an offset VAA is correct as long
11834
+ * as it's still in range.
11835
+ */
11836
+ async fetchPythHistoricalData(feedId, publishTime, windowSeconds) {
11837
+ const result = await this.fetchPythHistoricalRaw(
11838
+ feedId,
11839
+ publishTime,
11840
+ windowSeconds
11841
+ );
11842
+ if (!result) {
11843
+ throw new Error(
11844
+ `No Pyth VAA available in window [${publishTime}, ${publishTime + windowSeconds}] for feed ${feedId}`
11845
+ );
11846
+ }
11847
+ return result.updateData;
11848
+ }
11849
+ /**
11850
+ * Hermes parsed+binary fetch shared by {@link fetchPythHistoricalData} and
11851
+ * {@link fetchProjectedOpenPrice}. Returns the first in-window VAA whose
11852
+ * Hermes response includes a `parsed[0].price` entry, walking a few sample
11853
+ * timestamps across the window before giving up. Returns `null` on
11854
+ * complete miss so the caller can decide how to surface the failure
11855
+ * (resolution throws; UI projection returns null gracefully).
11783
11856
  */
11784
- async fetchPythHistoricalData(feedId, publishTime) {
11785
- const url = `${PYTH_HERMES_BASE}/v2/updates/price/${publishTime}?ids[]=${feedId}`;
11786
- const response = await fetch(url);
11787
- if (!response.ok) {
11788
- throw new Error(`Pyth Hermes API error: ${response.status} ${response.statusText}`);
11857
+ async fetchPythHistoricalParsed(feedId, publishTime, windowSeconds) {
11858
+ const result = await this.fetchPythHistoricalRaw(
11859
+ feedId,
11860
+ publishTime,
11861
+ windowSeconds
11862
+ );
11863
+ return result?.parsed ?? null;
11864
+ }
11865
+ async fetchPythHistoricalRaw(feedId, publishTime, windowSeconds) {
11866
+ const window = Math.max(0, Math.floor(windowSeconds));
11867
+ const offsets = window === 0 ? [0] : Array.from(/* @__PURE__ */ new Set([
11868
+ 0,
11869
+ Math.floor(window / 4),
11870
+ Math.floor(window / 2),
11871
+ Math.floor(3 * window / 4),
11872
+ window
11873
+ ])).sort((a, b) => a - b);
11874
+ for (const offset of offsets) {
11875
+ const t = publishTime + offset;
11876
+ const url = `${PYTH_HERMES_BASE}/v2/updates/price/${t}?ids[]=${feedId}`;
11877
+ const response = await this.hermesFetchWithBackoff(url);
11878
+ if (response === null) continue;
11879
+ const data = await response.json();
11880
+ const updateDataRaw = data.binary?.data;
11881
+ const parsed = data.parsed?.[0];
11882
+ if (!updateDataRaw || updateDataRaw.length === 0) continue;
11883
+ return {
11884
+ updateData: updateDataRaw.map(
11885
+ (d) => `0x${d}`
11886
+ ),
11887
+ parsed
11888
+ };
11789
11889
  }
11790
- const data = await response.json();
11791
- const updateData = data.binary?.data;
11792
- if (!updateData || updateData.length === 0) {
11793
- throw new Error("No historical price data returned from Pyth Hermes");
11890
+ return null;
11891
+ }
11892
+ /**
11893
+ * Fetch a Hermes URL with exponential backoff on 429. Returns the response
11894
+ * for 2xx, `null` for 404 (treated as "no VAA at this timestamp, try
11895
+ * another"), and throws for other errors.
11896
+ */
11897
+ async hermesFetchWithBackoff(url, options = {}) {
11898
+ const maxAttempts = options.maxAttempts ?? 4;
11899
+ const initialDelayMs = options.initialDelayMs ?? 500;
11900
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
11901
+ const response = await fetch(url);
11902
+ if (response.status === 429) {
11903
+ if (attempt === maxAttempts - 1) {
11904
+ throw new Error(
11905
+ `Pyth Hermes API error: 429 Too Many Requests (gave up after ${maxAttempts} attempts)`
11906
+ );
11907
+ }
11908
+ const delay = initialDelayMs * Math.pow(2, attempt);
11909
+ await new Promise((r) => setTimeout(r, delay));
11910
+ continue;
11911
+ }
11912
+ if (response.status === 404) return null;
11913
+ if (!response.ok) {
11914
+ throw new Error(
11915
+ `Pyth Hermes API error: ${response.status} ${response.statusText}`
11916
+ );
11917
+ }
11918
+ return response;
11794
11919
  }
11795
- return updateData.map((d) => `0x${d}`);
11920
+ return null;
11796
11921
  }
11797
11922
  };
11798
11923