@oddmaki-protocol/sdk 1.10.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
@@ -11574,15 +11574,18 @@ var PriceMarketModule = class extends BaseModule {
11574
11574
  );
11575
11575
  }
11576
11576
  const isDeferred = pm.strikePrice === BigInt(0);
11577
+ const windowSeconds = Number(pm.resolutionWindow);
11577
11578
  const closeVAA = await this.fetchPythHistoricalData(
11578
11579
  pm.feedId,
11579
- Number(pm.closeTime)
11580
+ Number(pm.closeTime),
11581
+ windowSeconds
11580
11582
  );
11581
11583
  let pythUpdateData;
11582
11584
  if (isDeferred) {
11583
11585
  const openVAA = await this.fetchPythHistoricalData(
11584
11586
  pm.feedId,
11585
- Number(pm.openTime)
11587
+ Number(pm.openTime),
11588
+ windowSeconds
11586
11589
  );
11587
11590
  pythUpdateData = [...openVAA, ...closeVAA];
11588
11591
  } else {
@@ -11800,18 +11803,12 @@ var PriceMarketModule = class extends BaseModule {
11800
11803
  if (pm.strikePrice !== BigInt(0)) return null;
11801
11804
  const now = BigInt(Math.floor(Date.now() / 1e3));
11802
11805
  if (now < pm.openTime) return null;
11803
- const url = `${PYTH_HERMES_BASE}/v2/updates/price/${pm.openTime}?ids[]=${pm.feedId}`;
11804
- const response = await fetch(url);
11805
- if (!response.ok) {
11806
- throw new Error(
11807
- `Pyth Hermes API error: ${response.status} ${response.statusText}`
11808
- );
11809
- }
11810
- const data = await response.json();
11811
- const parsed = data.parsed?.[0];
11812
- if (!parsed?.price) {
11813
- throw new Error("Pyth Hermes response missing parsed.price for open VAA");
11814
- }
11806
+ const parsed = await this.fetchPythHistoricalParsed(
11807
+ pm.feedId,
11808
+ Number(pm.openTime),
11809
+ Number(pm.resolutionWindow)
11810
+ );
11811
+ if (!parsed) return null;
11815
11812
  return {
11816
11813
  price: BigInt(parsed.price.price),
11817
11814
  publishTime: BigInt(parsed.price.publish_time),
@@ -11821,22 +11818,105 @@ var PriceMarketModule = class extends BaseModule {
11821
11818
  };
11822
11819
  }
11823
11820
  /**
11824
- * Fetch historical Pyth price update data at a specific timestamp.
11825
- * Used internally by {@link resolvePyth} to fetch open-window and
11826
- * close-window VAAs.
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).
11827
11855
  */
11828
- async fetchPythHistoricalData(feedId, publishTime) {
11829
- const url = `${PYTH_HERMES_BASE}/v2/updates/price/${publishTime}?ids[]=${feedId}`;
11830
- const response = await fetch(url);
11831
- if (!response.ok) {
11832
- 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
+ };
11833
11888
  }
11834
- const data = await response.json();
11835
- const updateData = data.binary?.data;
11836
- if (!updateData || updateData.length === 0) {
11837
- 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;
11838
11918
  }
11839
- return updateData.map((d) => `0x${d}`);
11919
+ return null;
11840
11920
  }
11841
11921
  };
11842
11922