@pear-protocol/hyperliquid-sdk 0.1.12 → 0.1.13-beta.2

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.
@@ -1,4 +1,4 @@
1
- import type { ApiResponse, GetEIP712MessageResponse, AuthenticateRequest, AuthenticateResponse, RefreshTokenResponse, LogoutResponse } from '../types';
1
+ import type { ApiResponse, GetEIP712MessageResponse, AuthenticateRequest, AuthenticateResponse, RefreshTokenResponse, LogoutResponse } from "../types";
2
2
  export declare function getEIP712Message(baseUrl: string, address: string, clientId: string): Promise<ApiResponse<GetEIP712MessageResponse>>;
3
3
  export declare function authenticate(baseUrl: string, body: AuthenticateRequest): Promise<ApiResponse<AuthenticateResponse>>;
4
4
  /**
@@ -1,4 +1,4 @@
1
- import type { GetEIP712MessageResponse } from "../types";
1
+ import type { GetEIP712MessageResponse } from '../types';
2
2
  export declare function useAuth(): {
3
3
  readonly isReady: boolean;
4
4
  readonly isAuthenticated: boolean;
@@ -10,4 +10,6 @@ export declare function useAuth(): {
10
10
  readonly loginWithPrivyToken: (address: string, appId: string, privyAccessToken: string) => Promise<void>;
11
11
  readonly refreshTokens: () => Promise<import("../types").RefreshTokenResponse>;
12
12
  readonly logout: () => Promise<void>;
13
+ readonly setAddress: (address: string | null) => void;
14
+ readonly clearSession: () => void;
13
15
  };
package/dist/index.d.ts CHANGED
@@ -1429,6 +1429,8 @@ declare function useAuth(): {
1429
1429
  readonly loginWithPrivyToken: (address: string, appId: string, privyAccessToken: string) => Promise<void>;
1430
1430
  readonly refreshTokens: () => Promise<RefreshTokenResponse>;
1431
1431
  readonly logout: () => Promise<void>;
1432
+ readonly setAddress: (address: string | null) => void;
1433
+ readonly clearSession: () => void;
1432
1434
  };
1433
1435
 
1434
1436
  interface MarginRequiredPerCollateral {
package/dist/index.js CHANGED
@@ -65,18 +65,29 @@ const useUserData = create((set) => ({
65
65
  userExtraAgents: null,
66
66
  spotState: null,
67
67
  userAbstractionMode: null,
68
+ isReady: false,
68
69
  setUserAbstractionMode: (value) => set({ userAbstractionMode: value }),
69
70
  setAccessToken: (token) => set({ accessToken: token }),
70
71
  setRefreshToken: (token) => set({ refreshToken: token }),
71
72
  setIsAuthenticated: (value) => set({ isAuthenticated: value }),
72
- setAddress: (address) => set(() => {
73
- if (typeof window !== 'undefined') {
74
- if (address) {
75
- window.localStorage.setItem('address', address);
76
- }
77
- else {
78
- window.localStorage.removeItem('address');
79
- }
73
+ setIsReady: (value) => set({ isReady: value }),
74
+ setAddress: (address) => set((state) => {
75
+ // If address changed, clear user-specific data to prevent stale balances
76
+ const addressChanged = state.address !== null && state.address !== address;
77
+ if (addressChanged) {
78
+ return {
79
+ address,
80
+ // Clear user-specific data when address changes
81
+ spotState: null,
82
+ tradeHistories: null,
83
+ rawOpenPositions: null,
84
+ openOrders: null,
85
+ accountSummary: null,
86
+ twapDetails: null,
87
+ notifications: null,
88
+ userExtraAgents: null,
89
+ userAbstractionMode: null,
90
+ };
80
91
  }
81
92
  return { address };
82
93
  }),
@@ -88,10 +99,6 @@ const useUserData = create((set) => ({
88
99
  setNotifications: (value) => set({ notifications: value }),
89
100
  setSpotState: (value) => set({ spotState: value }),
90
101
  clean: () => set({
91
- accessToken: null,
92
- refreshToken: null,
93
- isAuthenticated: false,
94
- address: null,
95
102
  tradeHistories: null,
96
103
  rawOpenPositions: null,
97
104
  openOrders: null,
@@ -384,19 +391,19 @@ class TokenMetadataExtractor {
384
391
  if (!assetCtx) {
385
392
  return null;
386
393
  }
387
- // Get current price - prefer assetCtx.midPx as it's already index-matched,
388
- // fall back to allMids lookup if midPx is null
394
+ // Get current price - prefer allMids (real-time WebSocket data),
395
+ // fall back to assetCtx.midPx if not available
389
396
  const actualSymbol = foundAsset.name;
390
397
  let currentPrice = 0;
391
- // Fallback: assetCtx.midPx (already properly indexed)
392
- if (!currentPrice || isNaN(currentPrice)) {
393
- const currentPriceStr = allMids.mids[actualSymbol] || allMids.mids[symbol];
394
- currentPrice = currentPriceStr ? parseFloat(currentPriceStr) : 0;
395
- }
396
- // Primary source: allMids lookup
398
+ // Fallback: assetCtx.midPx (from REST API, less frequent)
397
399
  if (assetCtx.midPx) {
398
400
  currentPrice = parseFloat(assetCtx.midPx);
399
401
  }
402
+ // Primary source: allMids (real-time WebSocket data)
403
+ const currentPriceStr = allMids.mids[actualSymbol] || allMids.mids[symbol];
404
+ if (currentPriceStr) {
405
+ currentPrice = parseFloat(currentPriceStr);
406
+ }
400
407
  // Get previous day price
401
408
  const prevDayPrice = parseFloat(assetCtx.prevDayPx);
402
409
  // Calculate 24h price change
@@ -551,7 +558,7 @@ const useHyperliquidData = create((set) => ({
551
558
  tokenMetadata: refreshTokenMetadata(state, { allMids: value }),
552
559
  })),
553
560
  setActiveAssetData: (value) => set((state) => {
554
- const activeAssetData = typeof value === 'function' ? value(state.activeAssetData) : value;
561
+ const activeAssetData = typeof value === "function" ? value(state.activeAssetData) : value;
555
562
  return {
556
563
  activeAssetData,
557
564
  tokenMetadata: refreshTokenMetadata(state, { activeAssetData }),
@@ -591,7 +598,10 @@ const useHyperliquidData = create((set) => ({
591
598
  setCandleData: (value) => set({ candleData: value }),
592
599
  upsertActiveAssetData: (key, value) => set((state) => {
593
600
  var _a;
594
- const activeAssetData = { ...((_a = state.activeAssetData) !== null && _a !== void 0 ? _a : {}), [key]: value };
601
+ const activeAssetData = {
602
+ ...((_a = state.activeAssetData) !== null && _a !== void 0 ? _a : {}),
603
+ [key]: value,
604
+ };
595
605
  return {
596
606
  activeAssetData,
597
607
  tokenMetadata: refreshTokenMetadata(state, { activeAssetData }, { symbols: [key] }),
@@ -626,6 +636,11 @@ const useHyperliquidData = create((set) => ({
626
636
  perpMetasByDex: state.perpMetasByDex,
627
637
  }),
628
638
  })),
639
+ // Clear user-specific data (called when wallet address changes)
640
+ clearUserData: () => set({
641
+ aggregatedClearingHouseState: null,
642
+ rawClearinghouseStates: null,
643
+ }),
629
644
  }));
630
645
 
631
646
  /**
@@ -906,6 +921,7 @@ const useHyperliquidNativeWebSocket = ({ address, enabled = true, onUserFills, }
906
921
  const reconnectAttemptsRef = useRef(0);
907
922
  const manualCloseRef = useRef(false);
908
923
  const onUserFillsRef = useRef(onUserFills);
924
+ const reconnectTimeoutRef = useRef(null);
909
925
  const [readyState, setReadyState] = useState(ReadyState.CONNECTING);
910
926
  // Keep the ref updated with the latest callback
911
927
  useEffect(() => {
@@ -916,9 +932,9 @@ const useHyperliquidNativeWebSocket = ({ address, enabled = true, onUserFills, }
916
932
  try {
917
933
  const message = JSON.parse(event.data);
918
934
  // Handle subscription responses
919
- if ('success' in message || 'error' in message) {
935
+ if ("success" in message || "error" in message) {
920
936
  if (message.error) {
921
- console.error('[HyperLiquid WS] Subscription error:', message.error);
937
+ console.error("[HyperLiquid WS] Subscription error:", message.error);
922
938
  setLastError(message.error);
923
939
  }
924
940
  else {
@@ -927,44 +943,44 @@ const useHyperliquidNativeWebSocket = ({ address, enabled = true, onUserFills, }
927
943
  return;
928
944
  }
929
945
  // Handle channel data messages
930
- if ('channel' in message && 'data' in message) {
946
+ if ("channel" in message && "data" in message) {
931
947
  const response = message;
932
948
  switch (response.channel) {
933
- case 'userFills':
949
+ case "userFills":
934
950
  {
935
951
  const maybePromise = (_a = onUserFillsRef.current) === null || _a === void 0 ? void 0 : _a.call(onUserFillsRef);
936
952
  if (maybePromise instanceof Promise) {
937
- maybePromise.catch((err) => console.error('[HyperLiquid WS] userFills callback error', err));
953
+ maybePromise.catch((err) => console.error("[HyperLiquid WS] userFills callback error", err));
938
954
  }
939
955
  }
940
956
  break;
941
- case 'webData3':
957
+ case "webData3":
942
958
  const webData3 = response.data;
943
959
  // finalAssetContexts now sourced from allDexsAssetCtxs channel
944
960
  const finalAtOICaps = webData3.perpDexStates.flatMap((dex) => dex.perpsAtOpenInterestCap);
945
961
  setFinalAtOICaps(finalAtOICaps);
946
962
  setUserAbstractionMode(webData3.userState.abstraction || null);
947
963
  break;
948
- case 'allDexsAssetCtxs':
964
+ case "allDexsAssetCtxs":
949
965
  {
950
966
  const data = response.data;
951
967
  // Store by DEX name, mapping '' to 'HYPERLIQUID'
952
968
  const assetContextsByDex = new Map();
953
969
  data.ctxs.forEach(([dexKey, ctxs]) => {
954
- const dexName = dexKey === '' ? 'HYPERLIQUID' : dexKey;
970
+ const dexName = dexKey === "" ? "HYPERLIQUID" : dexKey;
955
971
  assetContextsByDex.set(dexName, ctxs || []);
956
972
  });
957
973
  setAssetContextsByDex(assetContextsByDex);
958
974
  }
959
975
  break;
960
- case 'allDexsClearinghouseState':
976
+ case "allDexsClearinghouseState":
961
977
  {
962
978
  const data = response.data;
963
979
  const states = (data.clearinghouseStates || [])
964
980
  .map(([, s]) => s)
965
981
  .filter(Boolean);
966
- const sum = (values) => values.reduce((acc, v) => acc + (parseFloat(v || '0') || 0), 0);
967
- const toStr = (n) => Number.isFinite(n) ? n.toString() : '0';
982
+ const sum = (values) => values.reduce((acc, v) => acc + (parseFloat(v || "0") || 0), 0);
983
+ const toStr = (n) => Number.isFinite(n) ? n.toString() : "0";
968
984
  const assetPositions = states.flatMap((s) => s.assetPositions || []);
969
985
  const crossMaintenanceMarginUsed = toStr(sum(states.map((s) => s.crossMaintenanceMarginUsed)));
970
986
  const crossMarginSummary = {
@@ -994,26 +1010,26 @@ const useHyperliquidNativeWebSocket = ({ address, enabled = true, onUserFills, }
994
1010
  setClearinghouseStateReceived(true);
995
1011
  }
996
1012
  break;
997
- case 'allMids':
1013
+ case "allMids":
998
1014
  {
999
1015
  const data = response.data;
1000
1016
  setAllMids(data);
1001
1017
  }
1002
1018
  break;
1003
- case 'activeAssetData':
1019
+ case "activeAssetData":
1004
1020
  {
1005
1021
  const assetData = response.data;
1006
1022
  upsertActiveAssetData(assetData.coin, assetData);
1007
1023
  }
1008
1024
  break;
1009
- case 'candle':
1025
+ case "candle":
1010
1026
  {
1011
1027
  const candleDataItem = response.data;
1012
- const symbol = candleDataItem.s || '';
1028
+ const symbol = candleDataItem.s || "";
1013
1029
  addCandleData(symbol, candleDataItem);
1014
1030
  }
1015
1031
  break;
1016
- case 'spotState':
1032
+ case "spotState":
1017
1033
  {
1018
1034
  const spotStateData = response.data;
1019
1035
  if (spotStateData === null || spotStateData === void 0 ? void 0 : spotStateData.spotState) {
@@ -1028,7 +1044,7 @@ const useHyperliquidNativeWebSocket = ({ address, enabled = true, onUserFills, }
1028
1044
  }
1029
1045
  catch (error) {
1030
1046
  const errorMessage = `Failed to parse message: ${error instanceof Error ? error.message : String(error)}`;
1031
- console.error('[HyperLiquid WS] Parse error:', errorMessage, 'Raw message:', event.data);
1047
+ console.error("[HyperLiquid WS] Parse error:", errorMessage, "Raw message:", event.data);
1032
1048
  setLastError(errorMessage);
1033
1049
  }
1034
1050
  }, [
@@ -1042,9 +1058,14 @@ const useHyperliquidNativeWebSocket = ({ address, enabled = true, onUserFills, }
1042
1058
  setSpotState,
1043
1059
  ]);
1044
1060
  const connect = useCallback(() => {
1045
- console.log('[HyperLiquid WS] connect() called, enabled:', enabled);
1061
+ console.log("[HyperLiquid WS] connect() called, enabled:", enabled);
1046
1062
  if (!enabled)
1047
1063
  return;
1064
+ // Clear any pending reconnect timeout
1065
+ if (reconnectTimeoutRef.current) {
1066
+ clearTimeout(reconnectTimeoutRef.current);
1067
+ reconnectTimeoutRef.current = null;
1068
+ }
1048
1069
  try {
1049
1070
  // Avoid opening multiple sockets if one is already active or connecting
1050
1071
  if (wsRef.current &&
@@ -1053,10 +1074,10 @@ const useHyperliquidNativeWebSocket = ({ address, enabled = true, onUserFills, }
1053
1074
  console.log('[HyperLiquid WS] connect() returning early - socket already exists, readyState:', wsRef.current.readyState);
1054
1075
  return;
1055
1076
  }
1056
- console.log('[HyperLiquid WS] Creating new WebSocket connection');
1077
+ console.log("[HyperLiquid WS] Creating new WebSocket connection");
1057
1078
  manualCloseRef.current = false;
1058
1079
  setReadyState(ReadyState.CONNECTING);
1059
- const ws = new WebSocket('wss://api.hyperliquid.xyz/ws');
1080
+ const ws = new WebSocket("wss://api.hyperliquid.xyz/ws");
1060
1081
  wsRef.current = ws;
1061
1082
  ws.onopen = () => {
1062
1083
  reconnectAttemptsRef.current = 0;
@@ -1065,17 +1086,22 @@ const useHyperliquidNativeWebSocket = ({ address, enabled = true, onUserFills, }
1065
1086
  };
1066
1087
  ws.onmessage = handleMessage;
1067
1088
  ws.onerror = (event) => {
1068
- console.error('[HyperLiquid WS] Connection error:', event);
1069
- setLastError('WebSocket error');
1089
+ console.error("[HyperLiquid WS] Connection error:", event);
1090
+ setLastError("WebSocket error");
1070
1091
  };
1071
1092
  ws.onclose = () => {
1072
1093
  setReadyState(ReadyState.CLOSED);
1073
- if (!manualCloseRef.current && reconnectAttemptsRef.current < 5) {
1094
+ // Reset subscription state so effects will resubscribe on reconnect
1095
+ setSubscribedAddress(null);
1096
+ setSubscribedTokens([]);
1097
+ setSubscribedCandleTokens([]);
1098
+ setClearinghouseStateReceived(false);
1099
+ if (!manualCloseRef.current) {
1074
1100
  reconnectAttemptsRef.current += 1;
1075
- if (reconnectAttemptsRef.current === 5) {
1076
- console.error('[HyperLiquid WS] Reconnection stopped after 5 attempts');
1077
- }
1078
- setTimeout(() => connect(), 3000);
1101
+ // Exponential backoff: 1s, 2s, 4s, 8s, 16s, 30s (max), then stay at 30s
1102
+ const delay = Math.min(1000 * Math.pow(2, reconnectAttemptsRef.current - 1), 30000);
1103
+ console.log(`[HyperLiquid WS] Reconnecting in ${delay}ms (attempt ${reconnectAttemptsRef.current})`);
1104
+ reconnectTimeoutRef.current = setTimeout(() => connect(), delay);
1079
1105
  }
1080
1106
  };
1081
1107
  }
@@ -1086,9 +1112,53 @@ const useHyperliquidNativeWebSocket = ({ address, enabled = true, onUserFills, }
1086
1112
  useEffect(() => {
1087
1113
  console.log('[HyperLiquid WS] Connection effect running - calling connect()');
1088
1114
  connect();
1115
+ // Handle online/offline events to reconnect when internet is restored
1116
+ const handleOnline = () => {
1117
+ console.log('[HyperLiquid WS] Browser went online, attempting reconnect');
1118
+ // Reset reconnect attempts when internet comes back
1119
+ reconnectAttemptsRef.current = 0;
1120
+ // Clear any pending reconnect timeout
1121
+ if (reconnectTimeoutRef.current) {
1122
+ clearTimeout(reconnectTimeoutRef.current);
1123
+ reconnectTimeoutRef.current = null;
1124
+ }
1125
+ // Reset subscription state so effects will resubscribe on reconnect
1126
+ setSubscribedAddress(null);
1127
+ setSubscribedTokens([]);
1128
+ setSubscribedCandleTokens([]);
1129
+ setClearinghouseStateReceived(false);
1130
+ // Close existing socket if in a bad state
1131
+ if (wsRef.current &&
1132
+ wsRef.current.readyState !== WebSocket.OPEN &&
1133
+ wsRef.current.readyState !== WebSocket.CONNECTING) {
1134
+ try {
1135
+ wsRef.current.close();
1136
+ }
1137
+ catch (_a) { }
1138
+ wsRef.current = null;
1139
+ }
1140
+ // Attempt to reconnect
1141
+ connect();
1142
+ };
1143
+ const handleOffline = () => {
1144
+ console.log('[HyperLiquid WS] Browser went offline');
1145
+ // Clear pending reconnect timeout since we're offline
1146
+ if (reconnectTimeoutRef.current) {
1147
+ clearTimeout(reconnectTimeoutRef.current);
1148
+ reconnectTimeoutRef.current = null;
1149
+ }
1150
+ };
1151
+ window.addEventListener('online', handleOnline);
1152
+ window.addEventListener('offline', handleOffline);
1089
1153
  return () => {
1090
1154
  console.log('[HyperLiquid WS] Connection effect cleanup - closing existing connection');
1155
+ window.removeEventListener('online', handleOnline);
1156
+ window.removeEventListener('offline', handleOffline);
1091
1157
  manualCloseRef.current = true;
1158
+ if (reconnectTimeoutRef.current) {
1159
+ clearTimeout(reconnectTimeoutRef.current);
1160
+ reconnectTimeoutRef.current = null;
1161
+ }
1092
1162
  if (wsRef.current) {
1093
1163
  try {
1094
1164
  wsRef.current.close();
@@ -1109,7 +1179,7 @@ const useHyperliquidNativeWebSocket = ({ address, enabled = true, onUserFills, }
1109
1179
  if (isConnected) {
1110
1180
  // Send ping every 30 seconds
1111
1181
  pingIntervalRef.current = setInterval(() => {
1112
- sendJsonMessage({ method: 'ping' });
1182
+ sendJsonMessage({ method: "ping" });
1113
1183
  }, 30000);
1114
1184
  }
1115
1185
  else {
@@ -1127,27 +1197,27 @@ const useHyperliquidNativeWebSocket = ({ address, enabled = true, onUserFills, }
1127
1197
  }, [isConnected, sendJsonMessage]);
1128
1198
  // Handle address subscription changes
1129
1199
  useEffect(() => {
1130
- const DEFAULT_ADDRESS = '0x0000000000000000000000000000000000000000';
1200
+ const DEFAULT_ADDRESS = "0x0000000000000000000000000000000000000000";
1131
1201
  const userAddress = (address || DEFAULT_ADDRESS).toLowerCase();
1132
1202
  const normalizedSubscribedAddress = (subscribedAddress === null || subscribedAddress === void 0 ? void 0 : subscribedAddress.toLowerCase()) || null;
1133
- console.log('[HyperLiquid WS] Address subscription effect running');
1134
- console.log('[HyperLiquid WS] address:', address, 'userAddress:', userAddress, 'subscribedAddress:', subscribedAddress, 'normalizedSubscribedAddress:', normalizedSubscribedAddress);
1135
- console.log('[HyperLiquid WS] isConnected:', isConnected);
1203
+ console.log("[HyperLiquid WS] Address subscription effect running");
1204
+ console.log("[HyperLiquid WS] address:", address, "userAddress:", userAddress, "subscribedAddress:", subscribedAddress, "normalizedSubscribedAddress:", normalizedSubscribedAddress);
1205
+ console.log("[HyperLiquid WS] isConnected:", isConnected);
1136
1206
  if (normalizedSubscribedAddress === userAddress) {
1137
- console.log('[HyperLiquid WS] Address unchanged, skipping subscription update');
1207
+ console.log("[HyperLiquid WS] Address unchanged, skipping subscription update");
1138
1208
  return;
1139
1209
  }
1140
1210
  if (!isConnected) {
1141
- console.log('[HyperLiquid WS] Not connected, skipping subscription update');
1211
+ console.log("[HyperLiquid WS] Not connected, skipping subscription update");
1142
1212
  return;
1143
1213
  }
1144
1214
  // Unsubscribe from previous address if exists
1145
1215
  if (subscribedAddress) {
1146
- console.log('[HyperLiquid WS] Unsubscribing from previous address:', subscribedAddress);
1216
+ console.log("[HyperLiquid WS] Unsubscribing from previous address:", subscribedAddress);
1147
1217
  const unsubscribeMessage = {
1148
- method: 'unsubscribe',
1218
+ method: "unsubscribe",
1149
1219
  subscription: {
1150
- type: 'webData3',
1220
+ type: "webData3",
1151
1221
  user: subscribedAddress,
1152
1222
  },
1153
1223
  };
@@ -1155,54 +1225,54 @@ const useHyperliquidNativeWebSocket = ({ address, enabled = true, onUserFills, }
1155
1225
  // Unsubscribe from spotState for previous address
1156
1226
  if (subscribedAddress !== DEFAULT_ADDRESS) {
1157
1227
  const unsubscribeSpotState = {
1158
- method: 'unsubscribe',
1228
+ method: "unsubscribe",
1159
1229
  subscription: {
1160
- type: 'spotState',
1230
+ type: "spotState",
1161
1231
  user: subscribedAddress,
1162
1232
  },
1163
1233
  };
1164
1234
  sendJsonMessage(unsubscribeSpotState);
1165
1235
  }
1166
1236
  const unsubscribeAllDexsClearinghouseState = {
1167
- method: 'unsubscribe',
1237
+ method: "unsubscribe",
1168
1238
  subscription: {
1169
- type: 'allDexsClearinghouseState',
1239
+ type: "allDexsClearinghouseState",
1170
1240
  user: subscribedAddress,
1171
1241
  },
1172
1242
  };
1173
1243
  sendJsonMessage(unsubscribeAllDexsClearinghouseState);
1174
1244
  const unsubscribeUserFills = {
1175
- method: 'unsubscribe',
1245
+ method: "unsubscribe",
1176
1246
  subscription: {
1177
- type: 'userFills',
1247
+ type: "userFills",
1178
1248
  user: subscribedAddress,
1179
1249
  },
1180
1250
  };
1181
1251
  sendJsonMessage(unsubscribeUserFills);
1182
1252
  }
1183
1253
  const subscribeWebData3 = {
1184
- method: 'subscribe',
1254
+ method: "subscribe",
1185
1255
  subscription: {
1186
- type: 'webData3',
1256
+ type: "webData3",
1187
1257
  user: userAddress,
1188
1258
  },
1189
1259
  };
1190
1260
  // Subscribe to allMids
1191
1261
  const subscribeAllMids = {
1192
- method: 'subscribe',
1262
+ method: "subscribe",
1193
1263
  subscription: {
1194
- type: 'allMids',
1195
- dex: 'ALL_DEXS',
1264
+ type: "allMids",
1265
+ dex: "ALL_DEXS",
1196
1266
  },
1197
1267
  };
1198
1268
  // Subscribe to allDexsAssetCtxs (no payload params, global feed)
1199
1269
  const subscribeAllDexsAssetCtxs = {
1200
- method: 'subscribe',
1270
+ method: "subscribe",
1201
1271
  subscription: {
1202
- type: 'allDexsAssetCtxs',
1272
+ type: "allDexsAssetCtxs",
1203
1273
  },
1204
1274
  };
1205
- console.log('[HyperLiquid WS] Subscribing to new address:', userAddress);
1275
+ console.log("[HyperLiquid WS] Subscribing to new address:", userAddress);
1206
1276
  sendJsonMessage(subscribeWebData3);
1207
1277
  sendJsonMessage(subscribeAllMids);
1208
1278
  sendJsonMessage(subscribeAllDexsAssetCtxs);
@@ -1210,9 +1280,9 @@ const useHyperliquidNativeWebSocket = ({ address, enabled = true, onUserFills, }
1210
1280
  // Only subscribe if we have a real user address (not the default)
1211
1281
  if (userAddress !== DEFAULT_ADDRESS.toLowerCase()) {
1212
1282
  const subscribeSpotState = {
1213
- method: 'subscribe',
1283
+ method: "subscribe",
1214
1284
  subscription: {
1215
- type: 'spotState',
1285
+ type: "spotState",
1216
1286
  user: userAddress,
1217
1287
  },
1218
1288
  };
@@ -1222,9 +1292,9 @@ const useHyperliquidNativeWebSocket = ({ address, enabled = true, onUserFills, }
1222
1292
  // Only subscribe if we have a real user address (not the default)
1223
1293
  if (userAddress !== DEFAULT_ADDRESS.toLowerCase()) {
1224
1294
  const subscribeAllDexsClearinghouseState = {
1225
- method: 'subscribe',
1295
+ method: "subscribe",
1226
1296
  subscription: {
1227
- type: 'allDexsClearinghouseState',
1297
+ type: "allDexsClearinghouseState",
1228
1298
  user: userAddress,
1229
1299
  },
1230
1300
  };
@@ -1258,9 +1328,9 @@ const useHyperliquidNativeWebSocket = ({ address, enabled = true, onUserFills, }
1258
1328
  !userSummary)
1259
1329
  return;
1260
1330
  const subscribeUserFills = {
1261
- method: 'subscribe',
1331
+ method: "subscribe",
1262
1332
  subscription: {
1263
- type: 'userFills',
1333
+ type: "userFills",
1264
1334
  user: subscribedAddress,
1265
1335
  },
1266
1336
  };
@@ -1282,9 +1352,9 @@ const useHyperliquidNativeWebSocket = ({ address, enabled = true, onUserFills, }
1282
1352
  // Unsubscribe from tokens no longer in the list
1283
1353
  tokensToUnsubscribe.forEach((token) => {
1284
1354
  const unsubscribeMessage = {
1285
- method: 'unsubscribe',
1355
+ method: "unsubscribe",
1286
1356
  subscription: {
1287
- type: 'activeAssetData',
1357
+ type: "activeAssetData",
1288
1358
  user: address,
1289
1359
  coin: token,
1290
1360
  },
@@ -1294,9 +1364,9 @@ const useHyperliquidNativeWebSocket = ({ address, enabled = true, onUserFills, }
1294
1364
  // Subscribe to new tokens
1295
1365
  tokensToSubscribe.forEach((token) => {
1296
1366
  const subscribeMessage = {
1297
- method: 'subscribe',
1367
+ method: "subscribe",
1298
1368
  subscription: {
1299
- type: 'activeAssetData',
1369
+ type: "activeAssetData",
1300
1370
  user: address,
1301
1371
  coin: token,
1302
1372
  },
@@ -1325,9 +1395,9 @@ const useHyperliquidNativeWebSocket = ({ address, enabled = true, onUserFills, }
1325
1395
  if (prevInterval && prevInterval !== candleInterval) {
1326
1396
  subscribedCandleTokens.forEach((token) => {
1327
1397
  const unsubscribeMessage = {
1328
- method: 'unsubscribe',
1398
+ method: "unsubscribe",
1329
1399
  subscription: {
1330
- type: 'candle',
1400
+ type: "candle",
1331
1401
  coin: token,
1332
1402
  interval: prevInterval,
1333
1403
  },
@@ -1342,9 +1412,9 @@ const useHyperliquidNativeWebSocket = ({ address, enabled = true, onUserFills, }
1342
1412
  // Unsubscribe from tokens no longer in the list
1343
1413
  tokensToUnsubscribe.forEach((token) => {
1344
1414
  const unsubscribeMessage = {
1345
- method: 'unsubscribe',
1415
+ method: "unsubscribe",
1346
1416
  subscription: {
1347
- type: 'candle',
1417
+ type: "candle",
1348
1418
  coin: token,
1349
1419
  interval: candleInterval,
1350
1420
  },
@@ -1354,9 +1424,9 @@ const useHyperliquidNativeWebSocket = ({ address, enabled = true, onUserFills, }
1354
1424
  // Subscribe to new tokens
1355
1425
  tokensToSubscribe.forEach((token) => {
1356
1426
  const subscribeMessage = {
1357
- method: 'subscribe',
1427
+ method: "subscribe",
1358
1428
  subscription: {
1359
- type: 'candle',
1429
+ type: "candle",
1360
1430
  coin: token,
1361
1431
  interval: candleInterval,
1362
1432
  },
@@ -5871,10 +5941,10 @@ function toApiError(error) {
5871
5941
  var _a;
5872
5942
  const axiosError = error;
5873
5943
  const payload = (axiosError && axiosError.response ? axiosError.response.data : undefined);
5874
- const message = typeof payload === 'object' && payload && 'message' in payload
5944
+ const message = typeof payload === "object" && payload && "message" in payload
5875
5945
  ? String(payload.message)
5876
- : (axiosError === null || axiosError === void 0 ? void 0 : axiosError.message) || 'Request failed';
5877
- const errField = typeof payload === 'object' && payload && 'error' in payload
5946
+ : (axiosError === null || axiosError === void 0 ? void 0 : axiosError.message) || "Request failed";
5947
+ const errField = typeof payload === "object" && payload && "error" in payload
5878
5948
  ? String(payload.error)
5879
5949
  : undefined;
5880
5950
  return {
@@ -5884,8 +5954,8 @@ function toApiError(error) {
5884
5954
  };
5885
5955
  }
5886
5956
  function joinUrl(baseUrl, path) {
5887
- const cleanBase = baseUrl.replace(/\/$/, '');
5888
- const cleanPath = path.startsWith('/') ? path : `/${path}`;
5957
+ const cleanBase = baseUrl.replace(/\/$/, "");
5958
+ const cleanPath = path.startsWith("/") ? path : `/${path}`;
5889
5959
  return `${cleanBase}${cleanPath}`;
5890
5960
  }
5891
5961
  /**
@@ -5914,7 +5984,7 @@ function addAuthInterceptors(params) {
5914
5984
  pendingRequests = [];
5915
5985
  }
5916
5986
  const isOurApiUrl = (url) => Boolean(url && url.startsWith(apiBaseUrl));
5917
- const isRefreshUrl = (url) => Boolean(url && url.startsWith(joinUrl(apiBaseUrl, '/auth/refresh')));
5987
+ const isRefreshUrl = (url) => Boolean(url && url.startsWith(joinUrl(apiBaseUrl, "/auth/refresh")));
5918
5988
  const reqId = apiClient.interceptors.request.use((config) => {
5919
5989
  var _a;
5920
5990
  try {
@@ -5922,11 +5992,12 @@ function addAuthInterceptors(params) {
5922
5992
  const token = getAccessToken();
5923
5993
  if (token) {
5924
5994
  config.headers = (_a = config.headers) !== null && _a !== void 0 ? _a : {};
5925
- (config.headers)['Authorization'] = `Bearer ${token}`;
5995
+ config.headers["Authorization"] = `Bearer ${token}`;
5926
5996
  }
5927
5997
  }
5928
5998
  }
5929
- catch (_b) {
5999
+ catch (err) {
6000
+ console.error("[Auth Interceptor] Request interceptor error:", err);
5930
6001
  }
5931
6002
  return config;
5932
6003
  });
@@ -5937,22 +6008,36 @@ function addAuthInterceptors(params) {
5937
6008
  const url = originalRequest === null || originalRequest === void 0 ? void 0 : originalRequest.url;
5938
6009
  // If not our API or not 401, just reject
5939
6010
  if (!status || status !== 401 || !isOurApiUrl(url)) {
6011
+ if (status === 401) {
6012
+ console.warn("[Auth Interceptor] 401 received but URL check failed:", {
6013
+ url,
6014
+ apiBaseUrl,
6015
+ isOurApiUrl: isOurApiUrl(url),
6016
+ });
6017
+ }
5940
6018
  return Promise.reject(error);
5941
6019
  }
6020
+ console.log("[Auth Interceptor] 401 detected, attempting token refresh for URL:", url);
5942
6021
  // If the 401 is from refresh endpoint itself -> force logout
5943
6022
  if (isRefreshUrl(url)) {
6023
+ console.warn("[Auth Interceptor] Refresh endpoint returned 401, logging out");
5944
6024
  try {
5945
6025
  await logout();
5946
6026
  }
5947
- catch (_d) { }
6027
+ catch (err) {
6028
+ console.error("[Auth Interceptor] Logout failed:", err);
6029
+ }
5948
6030
  return Promise.reject(error);
5949
6031
  }
5950
6032
  // Prevent infinite loop
5951
6033
  if (originalRequest && originalRequest._retry) {
6034
+ console.warn("[Auth Interceptor] Request already retried, logging out");
5952
6035
  try {
5953
6036
  await logout();
5954
6037
  }
5955
- catch (_e) { }
6038
+ catch (err) {
6039
+ console.error("[Auth Interceptor] Logout failed:", err);
6040
+ }
5956
6041
  return Promise.reject(error);
5957
6042
  }
5958
6043
  // Mark so we don't retry twice
@@ -5966,31 +6051,45 @@ function addAuthInterceptors(params) {
5966
6051
  if (!newToken || !originalRequest)
5967
6052
  return reject(error);
5968
6053
  originalRequest.headers = (_a = originalRequest.headers) !== null && _a !== void 0 ? _a : {};
5969
- originalRequest.headers['Authorization'] = `Bearer ${newToken}`;
6054
+ originalRequest.headers["Authorization"] =
6055
+ `Bearer ${newToken}`;
5970
6056
  resolve(apiClient.request(originalRequest));
5971
6057
  });
5972
6058
  });
5973
6059
  }
5974
6060
  isRefreshing = true;
5975
6061
  try {
6062
+ console.log("[Auth Interceptor] Refreshing tokens...");
5976
6063
  const refreshed = await refreshTokens();
5977
- const newAccessToken = (_b = (refreshed && (refreshed.accessToken || ((_a = refreshed.data) === null || _a === void 0 ? void 0 : _a.accessToken)))) !== null && _b !== void 0 ? _b : null;
6064
+ const newAccessToken = (_b = (refreshed &&
6065
+ (refreshed.accessToken || ((_a = refreshed.data) === null || _a === void 0 ? void 0 : _a.accessToken)))) !== null && _b !== void 0 ? _b : null;
6066
+ if (!newAccessToken) {
6067
+ console.error("[Auth Interceptor] Token refresh succeeded but no access token in response:", refreshed);
6068
+ }
6069
+ else {
6070
+ console.log("[Auth Interceptor] Token refresh successful");
6071
+ }
5978
6072
  resolvePendingRequests(newAccessToken);
5979
6073
  if (originalRequest) {
5980
6074
  originalRequest.headers = (_c = originalRequest.headers) !== null && _c !== void 0 ? _c : {};
5981
6075
  if (newAccessToken)
5982
- (originalRequest.headers)['Authorization'] = `Bearer ${newAccessToken}`;
6076
+ originalRequest.headers["Authorization"] =
6077
+ `Bearer ${newAccessToken}`;
6078
+ console.log("[Auth Interceptor] Retrying original request with new token");
5983
6079
  const resp = await apiClient.request(originalRequest);
5984
6080
  return resp;
5985
6081
  }
5986
6082
  return Promise.reject(error);
5987
6083
  }
5988
6084
  catch (refreshErr) {
6085
+ console.error("[Auth Interceptor] Token refresh failed:", refreshErr);
5989
6086
  resolvePendingRequests(null);
5990
6087
  try {
5991
6088
  await logout();
5992
6089
  }
5993
- catch (_f) { }
6090
+ catch (err) {
6091
+ console.error("[Auth Interceptor] Logout failed:", err);
6092
+ }
5994
6093
  return Promise.reject(refreshErr);
5995
6094
  }
5996
6095
  finally {
@@ -6001,11 +6100,15 @@ function addAuthInterceptors(params) {
6001
6100
  try {
6002
6101
  apiClient.interceptors.request.eject(reqId);
6003
6102
  }
6004
- catch (_a) { }
6103
+ catch (err) {
6104
+ console.error("[Auth Interceptor] Failed to eject request interceptor:", err);
6105
+ }
6005
6106
  try {
6006
6107
  apiClient.interceptors.response.eject(resId);
6007
6108
  }
6008
- catch (_b) { }
6109
+ catch (err) {
6110
+ console.error("[Auth Interceptor] Failed to eject response interceptor:", err);
6111
+ }
6009
6112
  };
6010
6113
  }
6011
6114
 
@@ -7511,20 +7614,34 @@ function usePortfolio() {
7511
7614
  }
7512
7615
 
7513
7616
  async function getEIP712Message(baseUrl, address, clientId) {
7514
- const url = joinUrl(baseUrl, '/auth/eip712-message');
7617
+ const url = joinUrl(baseUrl, "/auth/eip712-message");
7515
7618
  try {
7516
- const resp = await axios$1.get(url, { params: { address, clientId }, timeout: 30000 });
7517
- return { data: resp.data, status: resp.status, headers: resp.headers };
7619
+ const resp = await apiClient.get(url, {
7620
+ params: { address, clientId },
7621
+ timeout: 30000,
7622
+ });
7623
+ return {
7624
+ data: resp.data,
7625
+ status: resp.status,
7626
+ headers: resp.headers,
7627
+ };
7518
7628
  }
7519
7629
  catch (error) {
7520
7630
  throw toApiError(error);
7521
7631
  }
7522
7632
  }
7523
7633
  async function authenticate(baseUrl, body) {
7524
- const url = joinUrl(baseUrl, '/auth/login');
7634
+ const url = joinUrl(baseUrl, "/auth/login");
7525
7635
  try {
7526
- const resp = await axios$1.post(url, body, { headers: { 'Content-Type': 'application/json' }, timeout: 30000 });
7527
- return { data: resp.data, status: resp.status, headers: resp.headers };
7636
+ const resp = await apiClient.post(url, body, {
7637
+ headers: { "Content-Type": "application/json" },
7638
+ timeout: 30000,
7639
+ });
7640
+ return {
7641
+ data: resp.data,
7642
+ status: resp.status,
7643
+ headers: resp.headers,
7644
+ };
7528
7645
  }
7529
7646
  catch (error) {
7530
7647
  throw toApiError(error);
@@ -7535,7 +7652,7 @@ async function authenticate(baseUrl, body) {
7535
7652
  */
7536
7653
  async function authenticateWithPrivy(baseUrl, params) {
7537
7654
  const body = {
7538
- method: 'privy_access_token',
7655
+ method: "privy_access_token",
7539
7656
  address: params.address,
7540
7657
  clientId: params.clientId,
7541
7658
  details: { appId: params.appId, accessToken: params.accessToken },
@@ -7543,62 +7660,124 @@ async function authenticateWithPrivy(baseUrl, params) {
7543
7660
  return authenticate(baseUrl, body);
7544
7661
  }
7545
7662
  async function refreshToken(baseUrl, refreshTokenVal) {
7546
- const url = joinUrl(baseUrl, '/auth/refresh');
7663
+ const url = joinUrl(baseUrl, "/auth/refresh");
7547
7664
  try {
7548
- const resp = await axios$1.post(url, { refreshToken: refreshTokenVal }, { headers: { 'Content-Type': 'application/json' }, timeout: 30000 });
7549
- return { data: resp.data, status: resp.status, headers: resp.headers };
7665
+ const resp = await apiClient.post(url, { refreshToken: refreshTokenVal }, { headers: { "Content-Type": "application/json" }, timeout: 30000 });
7666
+ return {
7667
+ data: resp.data,
7668
+ status: resp.status,
7669
+ headers: resp.headers,
7670
+ };
7550
7671
  }
7551
7672
  catch (error) {
7552
7673
  throw toApiError(error);
7553
7674
  }
7554
7675
  }
7555
7676
  async function logout(baseUrl, refreshTokenVal) {
7556
- const url = joinUrl(baseUrl, '/auth/logout');
7677
+ const url = joinUrl(baseUrl, "/auth/logout");
7557
7678
  try {
7558
- const resp = await axios$1.post(url, { refreshToken: refreshTokenVal }, { headers: { 'Content-Type': 'application/json' }, timeout: 30000 });
7559
- return { data: resp.data, status: resp.status, headers: resp.headers };
7679
+ const resp = await apiClient.post(url, { refreshToken: refreshTokenVal }, { headers: { "Content-Type": "application/json" }, timeout: 30000 });
7680
+ return {
7681
+ data: resp.data,
7682
+ status: resp.status,
7683
+ headers: resp.headers,
7684
+ };
7560
7685
  }
7561
7686
  catch (error) {
7562
7687
  throw toApiError(error);
7563
7688
  }
7564
7689
  }
7565
7690
 
7691
+ // Token expiration constants
7692
+ const ACCESS_TOKEN_BUFFER_MS = 5 * 60 * 1000; // Refresh 5 min before expiry
7693
+ const REFRESH_TOKEN_TTL_MS = 7 * 24 * 60 * 60 * 1000; // 7 days fallback
7694
+ function nowMs() {
7695
+ return Date.now();
7696
+ }
7697
+ function calcExpiresAt(expiresInSeconds) {
7698
+ return nowMs() + expiresInSeconds * 1000;
7699
+ }
7566
7700
  function useAuth() {
7567
7701
  const context = useContext(PearHyperliquidContext);
7568
7702
  if (!context) {
7569
- throw new Error("usePortfolio must be used within a PearHyperliquidProvider");
7703
+ throw new Error('useAuth must be used within a PearHyperliquidProvider');
7570
7704
  }
7571
7705
  const { apiBaseUrl, clientId } = context;
7572
- const [isReady, setIsReady] = useState(false);
7573
7706
  const accessToken = useUserData((s) => s.accessToken);
7574
7707
  const refreshToken$1 = useUserData((s) => s.refreshToken);
7708
+ const isReady = useUserData((s) => s.isReady);
7709
+ const isAuthenticated = useUserData((s) => s.isAuthenticated);
7575
7710
  const setAccessToken = useUserData((s) => s.setAccessToken);
7576
7711
  const setRefreshToken = useUserData((s) => s.setRefreshToken);
7577
- const isAuthenticated = useUserData((s) => s.isAuthenticated);
7712
+ const setIsReady = useUserData((s) => s.setIsReady);
7578
7713
  const setIsAuthenticated = useUserData((s) => s.setIsAuthenticated);
7579
7714
  const address = useUserData((s) => s.address);
7580
7715
  const setAddress = useUserData((s) => s.setAddress);
7716
+ // Ref to prevent concurrent refresh attempts
7717
+ const isRefreshingRef = useRef(false);
7581
7718
  useEffect(() => {
7582
- if (typeof window == "undefined") {
7719
+ if (typeof window == 'undefined') {
7583
7720
  return;
7584
7721
  }
7585
- const access = localStorage.getItem("accessToken");
7586
- const refresh = localStorage.getItem("refreshToken");
7587
- const addr = localStorage.getItem("address");
7588
- setAccessToken(access);
7589
- setRefreshToken(refresh);
7590
- setAddress(addr);
7591
- const authed = Boolean(access && addr);
7592
- setIsAuthenticated(authed);
7593
- setIsReady(true);
7594
- }, [setAccessToken, setRefreshToken, setIsAuthenticated, setAddress]);
7722
+ if (address) {
7723
+ const accessTokenKey = `${address}_accessToken`;
7724
+ const refreshTokenKey = `${address}_refreshToken`;
7725
+ const accessTokenExpiresAtKey = `${address}_accessTokenExpiresAt`;
7726
+ const refreshTokenExpiresAtKey = `${address}_refreshTokenExpiresAt`;
7727
+ const storedAccessToken = localStorage.getItem(accessTokenKey);
7728
+ const storedRefreshToken = localStorage.getItem(refreshTokenKey);
7729
+ const accessExpRaw = localStorage.getItem(accessTokenExpiresAtKey);
7730
+ const refreshExpRaw = localStorage.getItem(refreshTokenExpiresAtKey);
7731
+ const accessExp = accessExpRaw ? Number(accessExpRaw) : 0;
7732
+ const refreshExp = refreshExpRaw ? Number(refreshExpRaw) : 0;
7733
+ const now = nowMs();
7734
+ const accessValid = !!storedAccessToken && accessExp > now;
7735
+ const refreshValid = !!storedRefreshToken && refreshExp > now;
7736
+ if (accessValid && refreshValid) {
7737
+ // Both tokens are valid
7738
+ setAccessToken(storedAccessToken);
7739
+ setRefreshToken(storedRefreshToken);
7740
+ setIsAuthenticated(true);
7741
+ setIsReady(true);
7742
+ }
7743
+ else if (refreshValid) {
7744
+ // Access token expired but refresh still valid → refresh immediately
7745
+ setAccessToken(storedAccessToken);
7746
+ setRefreshToken(storedRefreshToken);
7747
+ (async () => {
7748
+ try {
7749
+ await refreshTokens();
7750
+ }
7751
+ catch (_a) {
7752
+ // Refresh failed → clear tokens
7753
+ setAccessToken(null);
7754
+ setRefreshToken(null);
7755
+ setIsAuthenticated(false);
7756
+ }
7757
+ setIsReady(true);
7758
+ })();
7759
+ return; // setIsReady will be called in the async block
7760
+ }
7761
+ else {
7762
+ // Refresh expired or no tokens → clear
7763
+ setAccessToken(null);
7764
+ setRefreshToken(null);
7765
+ setIsAuthenticated(false);
7766
+ setIsReady(true);
7767
+ }
7768
+ }
7769
+ else {
7770
+ setIsReady(true);
7771
+ }
7772
+ }, [address]);
7595
7773
  useEffect(() => {
7596
7774
  const cleanup = addAuthInterceptors({
7597
7775
  apiBaseUrl,
7598
7776
  getAccessToken: () => {
7599
- return typeof window !== "undefined"
7600
- ? window.localStorage.getItem("accessToken")
7601
- : null;
7777
+ if (typeof window === 'undefined')
7778
+ return null;
7779
+ // Read from Zustand state as single source of truth
7780
+ return useUserData.getState().accessToken;
7602
7781
  },
7603
7782
  refreshTokens: async () => {
7604
7783
  const data = await refreshTokens();
@@ -7612,6 +7791,55 @@ function useAuth() {
7612
7791
  cleanup();
7613
7792
  };
7614
7793
  }, [apiBaseUrl]);
7794
+ // Proactive refresh effect: refresh when app regains focus or timer fires
7795
+ useEffect(() => {
7796
+ if (typeof window === 'undefined' || !address || !refreshToken$1)
7797
+ return;
7798
+ const refreshIfNeeded = async () => {
7799
+ // Prevent concurrent refresh attempts
7800
+ if (isRefreshingRef.current)
7801
+ return;
7802
+ // Read fresh expiration values from localStorage (not stale closure)
7803
+ const accessExpRaw = localStorage.getItem(`${address}_accessTokenExpiresAt`);
7804
+ const refreshExpRaw = localStorage.getItem(`${address}_refreshTokenExpiresAt`);
7805
+ const accessExp = accessExpRaw ? Number(accessExpRaw) : 0;
7806
+ const refreshExp = refreshExpRaw ? Number(refreshExpRaw) : 0;
7807
+ const now = nowMs();
7808
+ // If refresh token is already expired, do nothing
7809
+ if (refreshExp <= now)
7810
+ return;
7811
+ // If access token is within buffer window, refresh
7812
+ if (accessExp - now <= ACCESS_TOKEN_BUFFER_MS) {
7813
+ isRefreshingRef.current = true;
7814
+ try {
7815
+ await refreshTokens();
7816
+ }
7817
+ catch (_a) {
7818
+ // Refresh failed, interceptor will handle logout on next API call
7819
+ }
7820
+ finally {
7821
+ isRefreshingRef.current = false;
7822
+ }
7823
+ }
7824
+ };
7825
+ const onVisibilityChange = () => {
7826
+ if (document.visibilityState === 'visible') {
7827
+ refreshIfNeeded();
7828
+ }
7829
+ };
7830
+ document.addEventListener('visibilitychange', onVisibilityChange);
7831
+ // Schedule timer for (accessExp - buffer)
7832
+ const accessExpRaw = localStorage.getItem(`${address}_accessTokenExpiresAt`);
7833
+ const accessExp = accessExpRaw ? Number(accessExpRaw) : 0;
7834
+ const delay = Math.max(0, accessExp - nowMs() - ACCESS_TOKEN_BUFFER_MS);
7835
+ const timer = window.setTimeout(() => {
7836
+ refreshIfNeeded();
7837
+ }, delay);
7838
+ return () => {
7839
+ document.removeEventListener('visibilitychange', onVisibilityChange);
7840
+ clearTimeout(timer);
7841
+ };
7842
+ }, [address, refreshToken$1]);
7615
7843
  async function getEip712(address) {
7616
7844
  const { data } = await getEIP712Message(apiBaseUrl, address, clientId);
7617
7845
  return data;
@@ -7619,17 +7847,21 @@ function useAuth() {
7619
7847
  async function loginWithSignedMessage(address, signature, timestamp) {
7620
7848
  try {
7621
7849
  const { data } = await authenticate(apiBaseUrl, {
7622
- method: "eip712",
7850
+ method: 'eip712',
7623
7851
  address,
7624
7852
  clientId,
7625
7853
  details: { signature, timestamp },
7626
7854
  });
7627
- window.localStorage.setItem("accessToken", data.accessToken);
7628
- window.localStorage.setItem("refreshToken", data.refreshToken);
7629
- window.localStorage.setItem("address", address);
7855
+ const accessTokenKey = `${address}_accessToken`;
7856
+ const refreshTokenKey = `${address}_refreshToken`;
7857
+ const accessTokenExpiresAtKey = `${address}_accessTokenExpiresAt`;
7858
+ const refreshTokenExpiresAtKey = `${address}_refreshTokenExpiresAt`;
7859
+ window.localStorage.setItem(accessTokenKey, data.accessToken);
7860
+ window.localStorage.setItem(refreshTokenKey, data.refreshToken);
7861
+ window.localStorage.setItem(accessTokenExpiresAtKey, String(calcExpiresAt(data.expiresIn)));
7862
+ window.localStorage.setItem(refreshTokenExpiresAtKey, String(nowMs() + REFRESH_TOKEN_TTL_MS));
7630
7863
  setAccessToken(data.accessToken);
7631
7864
  setRefreshToken(data.refreshToken);
7632
- setAddress(address);
7633
7865
  setIsAuthenticated(true);
7634
7866
  }
7635
7867
  catch (e) {
@@ -7644,12 +7876,16 @@ function useAuth() {
7644
7876
  appId,
7645
7877
  accessToken: privyAccessToken,
7646
7878
  });
7647
- window.localStorage.setItem("accessToken", data.accessToken);
7648
- window.localStorage.setItem("refreshToken", data.refreshToken);
7649
- window.localStorage.setItem("address", address);
7879
+ const accessTokenKey = `${address}_accessToken`;
7880
+ const refreshTokenKey = `${address}_refreshToken`;
7881
+ const accessTokenExpiresAtKey = `${address}_accessTokenExpiresAt`;
7882
+ const refreshTokenExpiresAtKey = `${address}_refreshTokenExpiresAt`;
7883
+ window.localStorage.setItem(accessTokenKey, data.accessToken);
7884
+ window.localStorage.setItem(refreshTokenKey, data.refreshToken);
7885
+ window.localStorage.setItem(accessTokenExpiresAtKey, String(calcExpiresAt(data.expiresIn)));
7886
+ window.localStorage.setItem(refreshTokenExpiresAtKey, String(nowMs() + REFRESH_TOKEN_TTL_MS));
7650
7887
  setAccessToken(data.accessToken);
7651
7888
  setRefreshToken(data.refreshToken);
7652
- setAddress(address);
7653
7889
  setIsAuthenticated(true);
7654
7890
  }
7655
7891
  catch (e) {
@@ -7657,35 +7893,61 @@ function useAuth() {
7657
7893
  }
7658
7894
  }
7659
7895
  async function refreshTokens() {
7660
- const refresh = window.localStorage.getItem("refreshToken");
7661
- if (!refresh)
7662
- throw new Error("No refresh token");
7663
- const { data } = await refreshToken(apiBaseUrl, refresh);
7664
- window.localStorage.setItem("accessToken", data.accessToken);
7665
- window.localStorage.setItem("refreshToken", data.refreshToken);
7896
+ const currentAddress = address;
7897
+ const currentRefresh = refreshToken$1;
7898
+ if (!currentRefresh || !currentAddress)
7899
+ throw new Error('No refresh token');
7900
+ const { data } = await refreshToken(apiBaseUrl, currentRefresh);
7901
+ // Update tokens in localStorage
7902
+ const accessTokenKey = `${currentAddress}_accessToken`;
7903
+ const refreshTokenKey = `${currentAddress}_refreshToken`;
7904
+ const accessTokenExpiresAtKey = `${currentAddress}_accessTokenExpiresAt`;
7905
+ const refreshTokenExpiresAtKey = `${currentAddress}_refreshTokenExpiresAt`;
7906
+ window.localStorage.setItem(accessTokenKey, data.accessToken);
7907
+ window.localStorage.setItem(refreshTokenKey, data.refreshToken);
7908
+ window.localStorage.setItem(accessTokenExpiresAtKey, String(calcExpiresAt(data.expiresIn)));
7909
+ window.localStorage.setItem(refreshTokenExpiresAtKey, String(nowMs() + REFRESH_TOKEN_TTL_MS));
7666
7910
  setAccessToken(data.accessToken);
7667
7911
  setRefreshToken(data.refreshToken);
7668
7912
  setIsAuthenticated(true);
7669
7913
  return data;
7670
7914
  }
7671
7915
  async function logout$1() {
7672
- const refresh = window.localStorage.getItem("refreshToken");
7673
- if (refresh) {
7916
+ const currentAddress = address;
7917
+ const currentRefresh = refreshToken$1;
7918
+ if (currentRefresh) {
7674
7919
  try {
7675
- await logout(apiBaseUrl, refresh);
7920
+ await logout(apiBaseUrl, currentRefresh);
7676
7921
  }
7677
7922
  catch (_a) {
7678
7923
  /* ignore */
7679
7924
  }
7680
7925
  }
7681
- window.localStorage.removeItem("accessToken");
7682
- window.localStorage.removeItem("refreshToken");
7683
- window.localStorage.removeItem("address");
7926
+ if (currentAddress) {
7927
+ const accessTokenKey = `${currentAddress}_accessToken`;
7928
+ const refreshTokenKey = `${currentAddress}_refreshToken`;
7929
+ const accessTokenExpiresAtKey = `${currentAddress}_accessTokenExpiresAt`;
7930
+ const refreshTokenExpiresAtKey = `${currentAddress}_refreshTokenExpiresAt`;
7931
+ window.localStorage.removeItem(accessTokenKey);
7932
+ window.localStorage.removeItem(refreshTokenKey);
7933
+ window.localStorage.removeItem(accessTokenExpiresAtKey);
7934
+ window.localStorage.removeItem(refreshTokenExpiresAtKey);
7935
+ }
7684
7936
  setAccessToken(null);
7685
7937
  setRefreshToken(null);
7686
7938
  setAddress(null);
7687
7939
  setIsAuthenticated(false);
7688
7940
  }
7941
+ /**
7942
+ * Clear the current session without logging out from the API.
7943
+ * Useful when switching wallets to clear stale in-memory auth state.
7944
+ * Note: setAddress will clear user-specific data (spotState, etc.) when called
7945
+ */
7946
+ function clearSession() {
7947
+ setAccessToken(null);
7948
+ setRefreshToken(null);
7949
+ setIsAuthenticated(false);
7950
+ }
7689
7951
  return {
7690
7952
  isReady,
7691
7953
  isAuthenticated,
@@ -7697,6 +7959,8 @@ function useAuth() {
7697
7959
  loginWithPrivyToken,
7698
7960
  refreshTokens,
7699
7961
  logout: logout$1,
7962
+ setAddress,
7963
+ clearSession,
7700
7964
  };
7701
7965
  }
7702
7966
 
@@ -8009,9 +8273,17 @@ const PearHyperliquidContext = createContext(undefined);
8009
8273
  /**
8010
8274
  * React Provider for PearHyperliquidClient
8011
8275
  */
8012
- const PearHyperliquidProvider = ({ children, apiBaseUrl = 'https://hl-ui.pearprotocol.io', clientId = 'PEARPROTOCOLUI', wsUrl = 'wss://hl-ui.pearprotocol.io/ws', }) => {
8276
+ const PearHyperliquidProvider = ({ children, apiBaseUrl = "https://hl-ui.pearprotocol.io", clientId = "PEARPROTOCOLUI", wsUrl = "wss://hl-ui.pearprotocol.io/ws", }) => {
8013
8277
  const address = useUserData((s) => s.address);
8014
- const setAddress = useUserData((s) => s.setAddress);
8278
+ const clearHyperliquidUserData = useHyperliquidData((state) => state.clearUserData);
8279
+ const prevAddressRef = useRef(null);
8280
+ // Clear user-specific data when address changes
8281
+ useEffect(() => {
8282
+ if (prevAddressRef.current !== null && prevAddressRef.current !== address) {
8283
+ clearHyperliquidUserData();
8284
+ }
8285
+ prevAddressRef.current = address;
8286
+ }, [address, clearHyperliquidUserData]);
8015
8287
  const perpMetasByDex = useHyperliquidData((state) => state.perpMetasByDex);
8016
8288
  const setPerpDexs = useHyperliquidData((state) => state.setPerpDexs);
8017
8289
  const setPerpMetasByDex = useHyperliquidData((state) => state.setPerpMetasByDex);
@@ -8042,20 +8314,20 @@ const PearHyperliquidProvider = ({ children, apiBaseUrl = 'https://hl-ui.pearpro
8042
8314
  perpMetas.forEach((item, perpIndex) => {
8043
8315
  var _a, _b;
8044
8316
  const dexName = perpIndex === 0
8045
- ? 'HYPERLIQUID'
8317
+ ? "HYPERLIQUID"
8046
8318
  : ((_b = (_a = perpDexs[perpIndex]) === null || _a === void 0 ? void 0 : _a.name) !== null && _b !== void 0 ? _b : `DEX_${perpIndex}`);
8047
8319
  var collateralToken;
8048
8320
  if (item.collateralToken === 360) {
8049
- collateralToken = 'USDH';
8321
+ collateralToken = "USDH";
8050
8322
  }
8051
8323
  if (item.collateralToken === 0) {
8052
- collateralToken = 'USDC';
8324
+ collateralToken = "USDC";
8053
8325
  }
8054
8326
  if (item.collateralToken === 235) {
8055
- collateralToken = 'USDE';
8327
+ collateralToken = "USDE";
8056
8328
  }
8057
8329
  if (item.collateralToken === 268) {
8058
- collateralToken = 'USDT0';
8330
+ collateralToken = "USDT0";
8059
8331
  }
8060
8332
  const universeAssets = item.universe.map((asset) => ({
8061
8333
  ...asset,
@@ -8083,8 +8355,6 @@ const PearHyperliquidProvider = ({ children, apiBaseUrl = 'https://hl-ui.pearpro
8083
8355
  }), [
8084
8356
  apiBaseUrl,
8085
8357
  wsUrl,
8086
- address,
8087
- setAddress,
8088
8358
  isConnected,
8089
8359
  lastError,
8090
8360
  nativeIsConnected,
@@ -8099,7 +8369,7 @@ const PearHyperliquidProvider = ({ children, apiBaseUrl = 'https://hl-ui.pearpro
8099
8369
  function usePearHyperliquid() {
8100
8370
  const ctx = useContext(PearHyperliquidContext);
8101
8371
  if (!ctx)
8102
- throw new Error('usePearHyperliquid must be used within a PearHyperliquidProvider');
8372
+ throw new Error("usePearHyperliquid must be used within a PearHyperliquidProvider");
8103
8373
  return ctx;
8104
8374
  }
8105
8375
 
@@ -1,4 +1,4 @@
1
- import React, { ReactNode } from 'react';
1
+ import React, { ReactNode } from "react";
2
2
  export interface PearHyperliquidContextType {
3
3
  clientId: string;
4
4
  apiBaseUrl: string;
@@ -1,5 +1,5 @@
1
- import { ActiveAssetData, CandleChartData, CandleData, ClearinghouseState, UniverseAsset, WebData3AssetCtx, WsAllMidsData, PerpDexsResponse } from '../types';
2
- import { TokenMetadataBySymbol } from '../utils/token-metadata-extractor';
1
+ import { ActiveAssetData, CandleChartData, CandleData, ClearinghouseState, UniverseAsset, WebData3AssetCtx, WsAllMidsData, PerpDexsResponse } from "../types";
2
+ import { TokenMetadataBySymbol } from "../utils/token-metadata-extractor";
3
3
  interface HyperliquidDataState {
4
4
  allMids: WsAllMidsData | null;
5
5
  activeAssetData: Record<string, ActiveAssetData> | null;
@@ -28,6 +28,7 @@ interface HyperliquidDataState {
28
28
  setPerpDexs: (value: PerpDexsResponse | null) => void;
29
29
  setPerpMetasByDex: (value: Map<string, UniverseAsset[]> | null) => void;
30
30
  setAssetContextsByDex: (value: Map<string, WebData3AssetCtx[]> | null) => void;
31
+ clearUserData: () => void;
31
32
  }
32
33
  export declare const useHyperliquidData: import("zustand").UseBoundStore<import("zustand").StoreApi<HyperliquidDataState>>;
33
34
  export {};
@@ -3,6 +3,7 @@ interface UserDataState {
3
3
  accessToken: string | null;
4
4
  refreshToken: string | null;
5
5
  isAuthenticated: boolean;
6
+ isReady: boolean;
6
7
  address: string | null;
7
8
  tradeHistories: TradeHistoryDataDto[] | null;
8
9
  rawOpenPositions: RawPositionDto[] | null;
@@ -26,6 +27,7 @@ interface UserDataState {
26
27
  setNotifications: (value: NotificationDto[] | null) => void;
27
28
  setSpotState: (value: SpotState | null) => void;
28
29
  setUserAbstractionMode: (value: UserAbstraction | null) => void;
30
+ setIsReady: (value: boolean) => void;
29
31
  clean: () => void;
30
32
  }
31
33
  export declare const useUserData: import("zustand").UseBoundStore<import("zustand").StoreApi<UserDataState>>;
@@ -1,5 +1,5 @@
1
- import type { AxiosInstance } from 'axios';
2
- import { ApiErrorResponse } from '../types';
1
+ import type { AxiosInstance } from "axios";
2
+ import { ApiErrorResponse } from "../types";
3
3
  export declare function toApiError(error: unknown): ApiErrorResponse;
4
4
  export declare function joinUrl(baseUrl: string, path: string): string;
5
5
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pear-protocol/hyperliquid-sdk",
3
- "version": "0.1.12",
3
+ "version": "0.1.13-beta.2",
4
4
  "description": "React SDK for Pear Protocol Hyperliquid API integration",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",