@pear-protocol/hyperliquid-sdk 0.1.28 → 0.1.29

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.ts CHANGED
@@ -888,6 +888,7 @@ type CandleInterval = '1m' | '3m' | '5m' | '15m' | '30m' | '1h' | '2h' | '4h' |
888
888
  */
889
889
  interface CandleData {
890
890
  s?: string;
891
+ i?: CandleInterval;
891
892
  t: number;
892
893
  T: number;
893
894
  o: number;
package/dist/index.js CHANGED
@@ -972,6 +972,20 @@ const useUserSelection$1 = create((set, get) => ({
972
972
  resetToDefaults: () => set((prev) => ({ ...prev, ...DEFAULT_STATE })),
973
973
  }));
974
974
 
975
+ const CANDLE_STALE_WARNING_MS = 90000;
976
+ const getCandleSubscriptionKey = (coin, interval) => `${coin}:${interval}`;
977
+ const debugHyperliquidWs = (message, payload) => {
978
+ if (typeof window === "undefined")
979
+ return;
980
+ if (window.localStorage.getItem("pear:debug:hl-ws") !== "1")
981
+ return;
982
+ if (payload === undefined) {
983
+ console.debug(`[HyperLiquid WS] ${message}`);
984
+ }
985
+ else {
986
+ console.debug(`[HyperLiquid WS] ${message}`, payload);
987
+ }
988
+ };
975
989
  const useHyperliquidNativeWebSocket = ({ address, enabled = true, onUserFills, }) => {
976
990
  const { setAllMids, setActiveAssetData, upsertActiveAssetData, setCandleData, deleteCandleSymbol, deleteActiveAssetData, addCandleData, setFinalAtOICaps, setAggregatedClearingHouseState, setRawClearinghouseStates, setAssetContextsByDex, } = useHyperliquidData();
977
991
  const { setSpotState, setUserAbstractionMode } = useUserData();
@@ -986,7 +1000,10 @@ const useHyperliquidNativeWebSocket = ({ address, enabled = true, onUserFills, }
986
1000
  const [subscribedCandleTokens, setSubscribedCandleTokens] = useState([]);
987
1001
  const [clearinghouseStateReceived, setClearinghouseStateReceived] = useState(false);
988
1002
  const prevCandleIntervalRef = useRef(null);
1003
+ const currentCandleIntervalRef = useRef(candleInterval);
989
1004
  const prevActiveAssetAddressRef = useRef(null);
1005
+ const lastCandleMessageAtRef = useRef(null);
1006
+ const lastCandleBySubscriptionRef = useRef(new Map());
990
1007
  const pingIntervalRef = useRef(null);
991
1008
  const wsRef = useRef(null);
992
1009
  const reconnectAttemptsRef = useRef(0);
@@ -998,8 +1015,11 @@ const useHyperliquidNativeWebSocket = ({ address, enabled = true, onUserFills, }
998
1015
  useEffect(() => {
999
1016
  onUserFillsRef.current = onUserFills;
1000
1017
  }, [onUserFills]);
1018
+ useEffect(() => {
1019
+ currentCandleIntervalRef.current = candleInterval;
1020
+ }, [candleInterval]);
1001
1021
  const handleMessage = useCallback((event) => {
1002
- var _a;
1022
+ var _a, _b;
1003
1023
  try {
1004
1024
  const message = JSON.parse(event.data);
1005
1025
  // Handle subscription responses
@@ -1097,7 +1117,27 @@ const useHyperliquidNativeWebSocket = ({ address, enabled = true, onUserFills, }
1097
1117
  {
1098
1118
  const candleDataItem = response.data;
1099
1119
  const symbol = candleDataItem.s || "";
1120
+ const messageInterval = (_b = candleDataItem.i) !== null && _b !== void 0 ? _b : currentCandleIntervalRef.current;
1121
+ if (messageInterval !== currentCandleIntervalRef.current) {
1122
+ debugHyperliquidWs("ignoring stale candle interval", {
1123
+ symbol,
1124
+ messageInterval,
1125
+ currentInterval: currentCandleIntervalRef.current,
1126
+ t: candleDataItem.t,
1127
+ T: candleDataItem.T,
1128
+ });
1129
+ break;
1130
+ }
1100
1131
  addCandleData(symbol, candleDataItem);
1132
+ const now = Date.now();
1133
+ lastCandleMessageAtRef.current = now;
1134
+ lastCandleBySubscriptionRef.current.set(getCandleSubscriptionKey(symbol, messageInterval), now);
1135
+ debugHyperliquidWs("candle received", {
1136
+ symbol,
1137
+ interval: messageInterval,
1138
+ t: candleDataItem.t,
1139
+ T: candleDataItem.T,
1140
+ });
1101
1141
  }
1102
1142
  break;
1103
1143
  case "spotState":
@@ -1166,6 +1206,7 @@ const useHyperliquidNativeWebSocket = ({ address, enabled = true, onUserFills, }
1166
1206
  setSubscribedAddress(null);
1167
1207
  setSubscribedTokens([]);
1168
1208
  setSubscribedCandleTokens([]);
1209
+ lastCandleBySubscriptionRef.current.clear();
1169
1210
  setClearinghouseStateReceived(false);
1170
1211
  if (!manualCloseRef.current) {
1171
1212
  reconnectAttemptsRef.current += 1;
@@ -1197,6 +1238,7 @@ const useHyperliquidNativeWebSocket = ({ address, enabled = true, onUserFills, }
1197
1238
  setSubscribedAddress(null);
1198
1239
  setSubscribedTokens([]);
1199
1240
  setSubscribedCandleTokens([]);
1241
+ lastCandleBySubscriptionRef.current.clear();
1200
1242
  setClearinghouseStateReceived(false);
1201
1243
  // Close existing socket if in a bad state
1202
1244
  if (wsRef.current &&
@@ -1486,9 +1528,13 @@ const useHyperliquidNativeWebSocket = ({ address, enabled = true, onUserFills, }
1486
1528
  if (!isConnected)
1487
1529
  return;
1488
1530
  const effectiveTokens = selectedTokenSymbols;
1489
- // Unsubscribe from previous candle subscriptions if interval changed
1490
1531
  const prevInterval = prevCandleIntervalRef.current;
1491
- if (prevInterval && prevInterval !== candleInterval) {
1532
+ const intervalChanged = prevInterval !== null && prevInterval !== candleInterval;
1533
+ const activeSubscribedCandleTokens = intervalChanged
1534
+ ? []
1535
+ : subscribedCandleTokens;
1536
+ // Unsubscribe from previous candle subscriptions if interval changed
1537
+ if (intervalChanged && prevInterval !== null) {
1492
1538
  subscribedCandleTokens.forEach((token) => {
1493
1539
  const unsubscribeMessage = {
1494
1540
  method: "unsubscribe",
@@ -1499,12 +1545,17 @@ const useHyperliquidNativeWebSocket = ({ address, enabled = true, onUserFills, }
1499
1545
  },
1500
1546
  };
1501
1547
  sendJsonMessage(unsubscribeMessage);
1548
+ lastCandleBySubscriptionRef.current.delete(getCandleSubscriptionKey(token, prevInterval));
1549
+ debugHyperliquidWs("candle unsubscribe", {
1550
+ token,
1551
+ interval: prevInterval,
1552
+ reason: "interval-change",
1553
+ });
1502
1554
  });
1503
1555
  setCandleData(new Map());
1504
- setSubscribedCandleTokens([]);
1505
1556
  }
1506
- const tokensToSubscribe = effectiveTokens.filter((token) => token && !subscribedCandleTokens.includes(token));
1507
- const tokensToUnsubscribe = subscribedCandleTokens.filter((token) => !effectiveTokens.includes(token));
1557
+ const tokensToSubscribe = effectiveTokens.filter((token) => token && !activeSubscribedCandleTokens.includes(token));
1558
+ const tokensToUnsubscribe = activeSubscribedCandleTokens.filter((token) => !effectiveTokens.includes(token));
1508
1559
  // Unsubscribe from tokens no longer in the list
1509
1560
  tokensToUnsubscribe.forEach((token) => {
1510
1561
  const unsubscribeMessage = {
@@ -1516,6 +1567,12 @@ const useHyperliquidNativeWebSocket = ({ address, enabled = true, onUserFills, }
1516
1567
  },
1517
1568
  };
1518
1569
  sendJsonMessage(unsubscribeMessage);
1570
+ lastCandleBySubscriptionRef.current.delete(getCandleSubscriptionKey(token, candleInterval));
1571
+ debugHyperliquidWs("candle unsubscribe", {
1572
+ token,
1573
+ interval: candleInterval,
1574
+ reason: "token-removed",
1575
+ });
1519
1576
  });
1520
1577
  // Subscribe to new tokens
1521
1578
  tokensToSubscribe.forEach((token) => {
@@ -1528,6 +1585,11 @@ const useHyperliquidNativeWebSocket = ({ address, enabled = true, onUserFills, }
1528
1585
  },
1529
1586
  };
1530
1587
  sendJsonMessage(subscribeMessage);
1588
+ lastCandleBySubscriptionRef.current.delete(getCandleSubscriptionKey(token, candleInterval));
1589
+ debugHyperliquidWs("candle subscribe", {
1590
+ token,
1591
+ interval: candleInterval,
1592
+ });
1531
1593
  });
1532
1594
  // Update subscribed state
1533
1595
  if (tokensToSubscribe.length > 0 ||
@@ -1544,7 +1606,48 @@ const useHyperliquidNativeWebSocket = ({ address, enabled = true, onUserFills, }
1544
1606
  subscribedCandleTokens,
1545
1607
  sendJsonMessage,
1546
1608
  setCandleData,
1609
+ deleteCandleSymbol,
1547
1610
  ]);
1611
+ useEffect(() => {
1612
+ if (!isConnected)
1613
+ return;
1614
+ const checkCandleSubscriptionHealth = () => {
1615
+ const effectiveTokens = selectedTokenSymbols.filter((token) => token);
1616
+ if (effectiveTokens.length === 0)
1617
+ return;
1618
+ const now = Date.now();
1619
+ const missingSubscriptionState = effectiveTokens.filter((token) => !subscribedCandleTokens.includes(token));
1620
+ const missingCandles = effectiveTokens.filter((token) => {
1621
+ const key = getCandleSubscriptionKey(token, candleInterval);
1622
+ return !lastCandleBySubscriptionRef.current.has(key);
1623
+ });
1624
+ const staleCandles = effectiveTokens.filter((token) => {
1625
+ const key = getCandleSubscriptionKey(token, candleInterval);
1626
+ const lastSeenAt = lastCandleBySubscriptionRef.current.get(key);
1627
+ return (lastSeenAt !== undefined &&
1628
+ now - lastSeenAt > CANDLE_STALE_WARNING_MS);
1629
+ });
1630
+ if (missingSubscriptionState.length === 0 &&
1631
+ missingCandles.length === 0 &&
1632
+ staleCandles.length === 0) {
1633
+ return;
1634
+ }
1635
+ debugHyperliquidWs("candle subscription health", {
1636
+ interval: candleInterval,
1637
+ selectedTokens: effectiveTokens,
1638
+ subscribedTokens: subscribedCandleTokens,
1639
+ missingSubscriptionState,
1640
+ missingCandles,
1641
+ staleCandles,
1642
+ lastAnyCandleAgeMs: lastCandleMessageAtRef.current === null
1643
+ ? null
1644
+ : now - lastCandleMessageAtRef.current,
1645
+ });
1646
+ };
1647
+ checkCandleSubscriptionHealth();
1648
+ const healthInterval = setInterval(checkCandleSubscriptionHealth, 30000);
1649
+ return () => clearInterval(healthInterval);
1650
+ }, [isConnected, selectedTokenSymbols, candleInterval, subscribedCandleTokens]);
1548
1651
  return {
1549
1652
  isConnected,
1550
1653
  lastError,
@@ -6633,6 +6736,22 @@ const computeBasketCandles = (longTokens, shortTokens, tokenCandles) => {
6633
6736
  return basketCandles;
6634
6737
  };
6635
6738
 
6739
+ const CANDLE_INTERVAL_MS = {
6740
+ '1m': 60 * 1000,
6741
+ '3m': 3 * 60 * 1000,
6742
+ '5m': 5 * 60 * 1000,
6743
+ '15m': 15 * 60 * 1000,
6744
+ '30m': 30 * 60 * 1000,
6745
+ '1h': 60 * 60 * 1000,
6746
+ '2h': 2 * 60 * 60 * 1000,
6747
+ '4h': 4 * 60 * 60 * 1000,
6748
+ '8h': 8 * 60 * 60 * 1000,
6749
+ '12h': 12 * 60 * 60 * 1000,
6750
+ '1d': 24 * 60 * 60 * 1000,
6751
+ '3d': 3 * 24 * 60 * 60 * 1000,
6752
+ '1w': 7 * 24 * 60 * 60 * 1000,
6753
+ '1M': 30 * 24 * 60 * 60 * 1000,
6754
+ };
6636
6755
  /**
6637
6756
  * Composes historical price fetching with basket candle computation.
6638
6757
  * - Listens to `longTokens` and `shortTokens` from user selection.
@@ -6642,6 +6761,7 @@ const computeBasketCandles = (longTokens, shortTokens, tokenCandles) => {
6642
6761
  const useBasketCandles = () => {
6643
6762
  const longTokens = useUserSelection$1((state) => state.longTokens);
6644
6763
  const shortTokens = useUserSelection$1((state) => state.shortTokens);
6764
+ const candleInterval = useUserSelection$1((state) => state.candleInterval);
6645
6765
  const candleData = useHyperliquidData((s) => s.candleData);
6646
6766
  const { fetchHistoricalPriceData, isLoading: tokenLoading, getAllHistoricalPriceData, getEffectiveDataBoundary } = useHistoricalPriceData();
6647
6767
  const fetchBasketCandles = useCallback(async (startTime, endTime, interval) => {
@@ -6747,40 +6867,56 @@ const useBasketCandles = () => {
6747
6867
  const removeRealtimeListener = useCallback((id) => {
6748
6868
  listenersRef.current.delete(id);
6749
6869
  }, []);
6750
- // Helper: compute weighted bar from latest snapshot if all tokens aligned
6870
+ // Helper: compute weighted bar from latest snapshot, carrying forward lagging
6871
+ // symbols so one sparse feed does not suppress every realtime chart update.
6751
6872
  const computeRealtimeBar = useCallback(() => {
6752
6873
  if (!candleData)
6753
6874
  return null;
6754
6875
  const allTokens = [...longTokens, ...shortTokens];
6755
6876
  if (allTokens.length === 0)
6756
6877
  return null;
6757
- // Collect candles, ensure presence and alignment
6758
6878
  const symbolSet = new Set(allTokens.map((t) => t.symbol));
6759
6879
  const snapshot = {};
6880
+ let t = null;
6881
+ let T = null;
6760
6882
  for (const symbol of symbolSet) {
6761
6883
  const c = candleData.get(symbol);
6762
6884
  if (!c)
6763
6885
  return null; // missing latest candle for symbol
6764
6886
  snapshot[symbol] = c;
6765
- }
6766
- // Verify same interval window (t and T match across symbols)
6767
- let t = null;
6768
- let T = null;
6769
- for (const symbol of symbolSet) {
6770
- const c = snapshot[symbol];
6771
- if (t === null) {
6887
+ if (t === null || c.t > t) {
6772
6888
  t = c.t;
6773
6889
  T = c.T;
6774
6890
  }
6775
- else if (c.t !== t || c.T !== T) {
6776
- return null; // not aligned yet
6777
- }
6778
6891
  }
6892
+ if (t === null || T === null)
6893
+ return null;
6894
+ const intervalMs = CANDLE_INTERVAL_MS[candleInterval];
6895
+ const maxCarryForwardMs = intervalMs * 2;
6896
+ const getCandleForTargetWindow = (symbol) => {
6897
+ const c = snapshot[symbol];
6898
+ if (c.t === t)
6899
+ return c;
6900
+ if (c.t > t || t - c.t > maxCarryForwardMs)
6901
+ return null;
6902
+ const price = c.c;
6903
+ if (!(price > 0))
6904
+ return null;
6905
+ return {
6906
+ s: c.s,
6907
+ t,
6908
+ T,
6909
+ o: price,
6910
+ h: price,
6911
+ l: price,
6912
+ c: price,
6913
+ };
6914
+ };
6779
6915
  // Compute weighted OHLC similar to computeBasketCandles
6780
6916
  let longOpen = 1, longHigh = 1, longLow = 1, longClose = 1;
6781
6917
  let shortOpen = 1, shortHigh = 1, shortLow = 1, shortClose = 1;
6782
6918
  for (const token of longTokens) {
6783
- const c = snapshot[token.symbol];
6919
+ const c = getCandleForTargetWindow(token.symbol);
6784
6920
  if (!c)
6785
6921
  return null;
6786
6922
  const w = token.weight / 100;
@@ -6793,7 +6929,7 @@ const useBasketCandles = () => {
6793
6929
  longClose *= Math.pow(cl, w);
6794
6930
  }
6795
6931
  for (const token of shortTokens) {
6796
- const c = snapshot[token.symbol];
6932
+ const c = getCandleForTargetWindow(token.symbol);
6797
6933
  if (!c)
6798
6934
  return null;
6799
6935
  const w = -(token.weight / 100);
@@ -6805,8 +6941,6 @@ const useBasketCandles = () => {
6805
6941
  shortLow *= Math.pow(l, w);
6806
6942
  shortClose *= Math.pow(cl, w);
6807
6943
  }
6808
- if (t === null || T === null)
6809
- return null;
6810
6944
  const weighted = {
6811
6945
  t,
6812
6946
  T,
@@ -6816,8 +6950,8 @@ const useBasketCandles = () => {
6816
6950
  c: longClose * shortClose,
6817
6951
  };
6818
6952
  return weighted;
6819
- }, [candleData, longTokens, shortTokens]);
6820
- // Emit realtime bars when aligned snapshot updates
6953
+ }, [candleData, candleInterval, longTokens, shortTokens]);
6954
+ // Emit realtime bars when the latest snapshot can produce a valid bar.
6821
6955
  useEffect(() => {
6822
6956
  if (listenersRef.current.size === 0)
6823
6957
  return;
package/dist/types.d.ts CHANGED
@@ -860,6 +860,7 @@ export type CandleInterval = '1m' | '3m' | '5m' | '15m' | '30m' | '1h' | '2h' |
860
860
  */
861
861
  export interface CandleData {
862
862
  s?: string;
863
+ i?: CandleInterval;
863
864
  t: number;
864
865
  T: number;
865
866
  o: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pear-protocol/hyperliquid-sdk",
3
- "version": "0.1.28",
3
+ "version": "0.1.29",
4
4
  "description": "React SDK for Pear Protocol Hyperliquid API integration",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",