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