@oddmaki-protocol/sdk 1.10.0 → 1.11.0

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.d.mts CHANGED
@@ -9650,6 +9650,18 @@ declare class PriceMarketModule extends BaseModule {
9650
9650
  * Get the Pyth contract address configured on the Diamond
9651
9651
  */
9652
9652
  getPythContract(): Promise<Address>;
9653
+ /**
9654
+ * Mark a stuck price market as Invalid and refund holders 50/50 via the CTF.
9655
+ * Permissionless: anyone can call this once `closeTime + 7 days` has elapsed.
9656
+ *
9657
+ * Use this for markets that never resolve via Pyth — e.g. the feed was
9658
+ * deprecated by the publisher, or Hermes had no in-window VAA at closeTime.
9659
+ * Reports payouts `[1, 1]` to the CTF, so every YES and every NO token
9660
+ * redeems for half the underlying collateral.
9661
+ *
9662
+ * Reverts on chain with `GracePeriodNotElapsed` if called too early.
9663
+ */
9664
+ markInvalid(marketId: bigint): Promise<`0x${string}`>;
9653
9665
  /**
9654
9666
  * Set the Pyth oracle contract address. Diamond owner only.
9655
9667
  */
@@ -9705,11 +9717,43 @@ declare class PriceMarketModule extends BaseModule {
9705
9717
  */
9706
9718
  fetchProjectedOpenPrice(marketId: bigint): Promise<ProjectedOpenPrice | null>;
9707
9719
  /**
9708
- * Fetch historical Pyth price update data at a specific timestamp.
9709
- * Used internally by {@link resolvePyth} to fetch open-window and
9710
- * close-window VAAs.
9720
+ * Fetch historical Pyth price update data covering the window
9721
+ * `[publishTime, publishTime + windowSeconds]`.
9722
+ *
9723
+ * Hermes serves a VAA AT the exact requested second when one exists, and
9724
+ * 404s when no publish landed in that second. For low-volume feeds or
9725
+ * markets resolving deep in the past this means a single request at
9726
+ * `publishTime` is fragile. This helper walks a few sample points across
9727
+ * the window until it finds a VAA, transparently retrying with exponential
9728
+ * backoff on 429.
9729
+ *
9730
+ * The on-chain `pickEarliestInWindow` accepts any VAA whose `publishTime`
9731
+ * falls in the same window, so returning an offset VAA is correct as long
9732
+ * as it's still in range.
9711
9733
  */
9712
9734
  private fetchPythHistoricalData;
9735
+ /**
9736
+ * Hermes parsed+binary fetch shared by {@link fetchPythHistoricalData} and
9737
+ * {@link fetchProjectedOpenPrice}. Returns the first in-window VAA whose
9738
+ * Hermes response includes a `parsed[0].price` entry, walking a few sample
9739
+ * timestamps across the window before giving up. Returns `null` on
9740
+ * complete miss so the caller can decide how to surface the failure
9741
+ * (resolution throws; UI projection returns null gracefully).
9742
+ */
9743
+ private fetchPythHistoricalParsed;
9744
+ private fetchPythHistoricalRaw;
9745
+ /**
9746
+ * Fetch a Hermes URL with a short backoff on 429. Returns the response for
9747
+ * 2xx, `null` for 404 (treated as "no VAA at this timestamp, try another"),
9748
+ * and throws on persistent 429 or other non-OK statuses.
9749
+ *
9750
+ * Budget kept intentionally small (2 attempts) so callers that are already
9751
+ * iterating (e.g. multiple sample timestamps in `fetchPythHistoricalRaw`)
9752
+ * don't compound the rate-limit pressure. If Hermes is genuinely rate-
9753
+ * limiting us, retrying within the same request cycle won't help — better
9754
+ * to fail fast and let the caller back off until the next tick.
9755
+ */
9756
+ private hermesFetchWithBackoff;
9713
9757
  }
9714
9758
 
9715
9759
  declare class OddMakiClient {
package/dist/index.d.ts CHANGED
@@ -9650,6 +9650,18 @@ declare class PriceMarketModule extends BaseModule {
9650
9650
  * Get the Pyth contract address configured on the Diamond
9651
9651
  */
9652
9652
  getPythContract(): Promise<Address>;
9653
+ /**
9654
+ * Mark a stuck price market as Invalid and refund holders 50/50 via the CTF.
9655
+ * Permissionless: anyone can call this once `closeTime + 7 days` has elapsed.
9656
+ *
9657
+ * Use this for markets that never resolve via Pyth — e.g. the feed was
9658
+ * deprecated by the publisher, or Hermes had no in-window VAA at closeTime.
9659
+ * Reports payouts `[1, 1]` to the CTF, so every YES and every NO token
9660
+ * redeems for half the underlying collateral.
9661
+ *
9662
+ * Reverts on chain with `GracePeriodNotElapsed` if called too early.
9663
+ */
9664
+ markInvalid(marketId: bigint): Promise<`0x${string}`>;
9653
9665
  /**
9654
9666
  * Set the Pyth oracle contract address. Diamond owner only.
9655
9667
  */
@@ -9705,11 +9717,43 @@ declare class PriceMarketModule extends BaseModule {
9705
9717
  */
9706
9718
  fetchProjectedOpenPrice(marketId: bigint): Promise<ProjectedOpenPrice | null>;
9707
9719
  /**
9708
- * Fetch historical Pyth price update data at a specific timestamp.
9709
- * Used internally by {@link resolvePyth} to fetch open-window and
9710
- * close-window VAAs.
9720
+ * Fetch historical Pyth price update data covering the window
9721
+ * `[publishTime, publishTime + windowSeconds]`.
9722
+ *
9723
+ * Hermes serves a VAA AT the exact requested second when one exists, and
9724
+ * 404s when no publish landed in that second. For low-volume feeds or
9725
+ * markets resolving deep in the past this means a single request at
9726
+ * `publishTime` is fragile. This helper walks a few sample points across
9727
+ * the window until it finds a VAA, transparently retrying with exponential
9728
+ * backoff on 429.
9729
+ *
9730
+ * The on-chain `pickEarliestInWindow` accepts any VAA whose `publishTime`
9731
+ * falls in the same window, so returning an offset VAA is correct as long
9732
+ * as it's still in range.
9711
9733
  */
9712
9734
  private fetchPythHistoricalData;
9735
+ /**
9736
+ * Hermes parsed+binary fetch shared by {@link fetchPythHistoricalData} and
9737
+ * {@link fetchProjectedOpenPrice}. Returns the first in-window VAA whose
9738
+ * Hermes response includes a `parsed[0].price` entry, walking a few sample
9739
+ * timestamps across the window before giving up. Returns `null` on
9740
+ * complete miss so the caller can decide how to surface the failure
9741
+ * (resolution throws; UI projection returns null gracefully).
9742
+ */
9743
+ private fetchPythHistoricalParsed;
9744
+ private fetchPythHistoricalRaw;
9745
+ /**
9746
+ * Fetch a Hermes URL with a short backoff on 429. Returns the response for
9747
+ * 2xx, `null` for 404 (treated as "no VAA at this timestamp, try another"),
9748
+ * and throws on persistent 429 or other non-OK statuses.
9749
+ *
9750
+ * Budget kept intentionally small (2 attempts) so callers that are already
9751
+ * iterating (e.g. multiple sample timestamps in `fetchPythHistoricalRaw`)
9752
+ * don't compound the rate-limit pressure. If Hermes is genuinely rate-
9753
+ * limiting us, retrying within the same request cycle won't help — better
9754
+ * to fail fast and let the caller back off until the next tick.
9755
+ */
9756
+ private hermesFetchWithBackoff;
9713
9757
  }
9714
9758
 
9715
9759
  declare class OddMakiClient {
package/dist/index.js CHANGED
@@ -11575,15 +11575,18 @@ var PriceMarketModule = class extends BaseModule {
11575
11575
  );
11576
11576
  }
11577
11577
  const isDeferred = pm.strikePrice === BigInt(0);
11578
+ const windowSeconds = Number(pm.resolutionWindow);
11578
11579
  const closeVAA = await this.fetchPythHistoricalData(
11579
11580
  pm.feedId,
11580
- Number(pm.closeTime)
11581
+ Number(pm.closeTime),
11582
+ windowSeconds
11581
11583
  );
11582
11584
  let pythUpdateData;
11583
11585
  if (isDeferred) {
11584
11586
  const openVAA = await this.fetchPythHistoricalData(
11585
11587
  pm.feedId,
11586
- Number(pm.openTime)
11588
+ Number(pm.openTime),
11589
+ windowSeconds
11587
11590
  );
11588
11591
  pythUpdateData = [...openVAA, ...closeVAA];
11589
11592
  } else {
@@ -11662,6 +11665,29 @@ var PriceMarketModule = class extends BaseModule {
11662
11665
  functionName: "getPythContract"
11663
11666
  });
11664
11667
  }
11668
+ /**
11669
+ * Mark a stuck price market as Invalid and refund holders 50/50 via the CTF.
11670
+ * Permissionless: anyone can call this once `closeTime + 7 days` has elapsed.
11671
+ *
11672
+ * Use this for markets that never resolve via Pyth — e.g. the feed was
11673
+ * deprecated by the publisher, or Hermes had no in-window VAA at closeTime.
11674
+ * Reports payouts `[1, 1]` to the CTF, so every YES and every NO token
11675
+ * redeems for half the underlying collateral.
11676
+ *
11677
+ * Reverts on chain with `GracePeriodNotElapsed` if called too early.
11678
+ */
11679
+ async markInvalid(marketId) {
11680
+ const wallet = this.walletClient;
11681
+ const account = await this.getSignerAccount();
11682
+ const { request } = await this.publicClient.simulateContract({
11683
+ address: this.config.diamondAddress,
11684
+ abi: PythResolutionFacet_default,
11685
+ functionName: "markPriceMarketInvalid",
11686
+ args: [marketId],
11687
+ account
11688
+ });
11689
+ return wallet.writeContract(request);
11690
+ }
11665
11691
  /**
11666
11692
  * Set the Pyth oracle contract address. Diamond owner only.
11667
11693
  */
@@ -11801,18 +11827,12 @@ var PriceMarketModule = class extends BaseModule {
11801
11827
  if (pm.strikePrice !== BigInt(0)) return null;
11802
11828
  const now = BigInt(Math.floor(Date.now() / 1e3));
11803
11829
  if (now < pm.openTime) return null;
11804
- const url = `${PYTH_HERMES_BASE}/v2/updates/price/${pm.openTime}?ids[]=${pm.feedId}`;
11805
- const response = await fetch(url);
11806
- if (!response.ok) {
11807
- throw new Error(
11808
- `Pyth Hermes API error: ${response.status} ${response.statusText}`
11809
- );
11810
- }
11811
- const data = await response.json();
11812
- const parsed = data.parsed?.[0];
11813
- if (!parsed?.price) {
11814
- throw new Error("Pyth Hermes response missing parsed.price for open VAA");
11815
- }
11830
+ const parsed = await this.fetchPythHistoricalParsed(
11831
+ pm.feedId,
11832
+ Number(pm.openTime),
11833
+ Number(pm.resolutionWindow)
11834
+ );
11835
+ if (!parsed) return null;
11816
11836
  return {
11817
11837
  price: BigInt(parsed.price.price),
11818
11838
  publishTime: BigInt(parsed.price.publish_time),
@@ -11822,22 +11842,111 @@ var PriceMarketModule = class extends BaseModule {
11822
11842
  };
11823
11843
  }
11824
11844
  /**
11825
- * Fetch historical Pyth price update data at a specific timestamp.
11826
- * Used internally by {@link resolvePyth} to fetch open-window and
11827
- * close-window VAAs.
11845
+ * Fetch historical Pyth price update data covering the window
11846
+ * `[publishTime, publishTime + windowSeconds]`.
11847
+ *
11848
+ * Hermes serves a VAA AT the exact requested second when one exists, and
11849
+ * 404s when no publish landed in that second. For low-volume feeds or
11850
+ * markets resolving deep in the past this means a single request at
11851
+ * `publishTime` is fragile. This helper walks a few sample points across
11852
+ * the window until it finds a VAA, transparently retrying with exponential
11853
+ * backoff on 429.
11854
+ *
11855
+ * The on-chain `pickEarliestInWindow` accepts any VAA whose `publishTime`
11856
+ * falls in the same window, so returning an offset VAA is correct as long
11857
+ * as it's still in range.
11858
+ */
11859
+ async fetchPythHistoricalData(feedId, publishTime, windowSeconds) {
11860
+ const result = await this.fetchPythHistoricalRaw(
11861
+ feedId,
11862
+ publishTime,
11863
+ windowSeconds
11864
+ );
11865
+ if (!result) {
11866
+ throw new Error(
11867
+ `No Pyth VAA available in window [${publishTime}, ${publishTime + windowSeconds}] for feed ${feedId}`
11868
+ );
11869
+ }
11870
+ return result.updateData;
11871
+ }
11872
+ /**
11873
+ * Hermes parsed+binary fetch shared by {@link fetchPythHistoricalData} and
11874
+ * {@link fetchProjectedOpenPrice}. Returns the first in-window VAA whose
11875
+ * Hermes response includes a `parsed[0].price` entry, walking a few sample
11876
+ * timestamps across the window before giving up. Returns `null` on
11877
+ * complete miss so the caller can decide how to surface the failure
11878
+ * (resolution throws; UI projection returns null gracefully).
11828
11879
  */
11829
- async fetchPythHistoricalData(feedId, publishTime) {
11830
- const url = `${PYTH_HERMES_BASE}/v2/updates/price/${publishTime}?ids[]=${feedId}`;
11831
- const response = await fetch(url);
11832
- if (!response.ok) {
11833
- throw new Error(`Pyth Hermes API error: ${response.status} ${response.statusText}`);
11880
+ async fetchPythHistoricalParsed(feedId, publishTime, windowSeconds) {
11881
+ const result = await this.fetchPythHistoricalRaw(
11882
+ feedId,
11883
+ publishTime,
11884
+ windowSeconds
11885
+ );
11886
+ return result?.parsed ?? null;
11887
+ }
11888
+ async fetchPythHistoricalRaw(feedId, publishTime, windowSeconds) {
11889
+ const window = Math.max(0, Math.floor(windowSeconds));
11890
+ const offsets = window === 0 ? [0] : Array.from(/* @__PURE__ */ new Set([
11891
+ 0,
11892
+ Math.floor(window / 4),
11893
+ Math.floor(window / 2),
11894
+ Math.floor(3 * window / 4),
11895
+ window
11896
+ ])).sort((a, b) => a - b);
11897
+ for (const offset of offsets) {
11898
+ const t = publishTime + offset;
11899
+ const url = `${PYTH_HERMES_BASE}/v2/updates/price/${t}?ids[]=${feedId}`;
11900
+ const response = await this.hermesFetchWithBackoff(url);
11901
+ if (response === null) continue;
11902
+ const data = await response.json();
11903
+ const updateDataRaw = data.binary?.data;
11904
+ const parsed = data.parsed?.[0];
11905
+ if (!updateDataRaw || updateDataRaw.length === 0) continue;
11906
+ return {
11907
+ updateData: updateDataRaw.map(
11908
+ (d) => `0x${d}`
11909
+ ),
11910
+ parsed
11911
+ };
11834
11912
  }
11835
- const data = await response.json();
11836
- const updateData = data.binary?.data;
11837
- if (!updateData || updateData.length === 0) {
11838
- throw new Error("No historical price data returned from Pyth Hermes");
11913
+ return null;
11914
+ }
11915
+ /**
11916
+ * Fetch a Hermes URL with a short backoff on 429. Returns the response for
11917
+ * 2xx, `null` for 404 (treated as "no VAA at this timestamp, try another"),
11918
+ * and throws on persistent 429 or other non-OK statuses.
11919
+ *
11920
+ * Budget kept intentionally small (2 attempts) so callers that are already
11921
+ * iterating (e.g. multiple sample timestamps in `fetchPythHistoricalRaw`)
11922
+ * don't compound the rate-limit pressure. If Hermes is genuinely rate-
11923
+ * limiting us, retrying within the same request cycle won't help — better
11924
+ * to fail fast and let the caller back off until the next tick.
11925
+ */
11926
+ async hermesFetchWithBackoff(url, options = {}) {
11927
+ const maxAttempts = options.maxAttempts ?? 2;
11928
+ const initialDelayMs = options.initialDelayMs ?? 750;
11929
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
11930
+ const response = await fetch(url);
11931
+ if (response.status === 429) {
11932
+ if (attempt === maxAttempts - 1) {
11933
+ throw new Error(
11934
+ `Pyth Hermes API error: 429 Too Many Requests (gave up after ${maxAttempts} attempts)`
11935
+ );
11936
+ }
11937
+ const delay = initialDelayMs * Math.pow(2, attempt);
11938
+ await new Promise((r) => setTimeout(r, delay));
11939
+ continue;
11940
+ }
11941
+ if (response.status === 404) return null;
11942
+ if (!response.ok) {
11943
+ throw new Error(
11944
+ `Pyth Hermes API error: ${response.status} ${response.statusText}`
11945
+ );
11946
+ }
11947
+ return response;
11839
11948
  }
11840
- return updateData.map((d) => `0x${d}`);
11949
+ return null;
11841
11950
  }
11842
11951
  };
11843
11952