@pear-protocol/hyperliquid-sdk 0.1.3 → 0.1.4-pnl

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.js CHANGED
@@ -64,17 +64,28 @@ const useUserData = create((set) => ({
64
64
  notifications: null,
65
65
  userExtraAgents: null,
66
66
  spotState: null,
67
+ userAbstractionMode: null,
68
+ isReady: false,
69
+ setUserAbstractionMode: (value) => set({ userAbstractionMode: value }),
67
70
  setAccessToken: (token) => set({ accessToken: token }),
68
71
  setRefreshToken: (token) => set({ refreshToken: token }),
69
72
  setIsAuthenticated: (value) => set({ isAuthenticated: value }),
70
- setAddress: (address) => set(() => {
71
- if (typeof window !== 'undefined') {
72
- if (address) {
73
- window.localStorage.setItem('address', address);
74
- }
75
- else {
76
- window.localStorage.removeItem('address');
77
- }
73
+ setIsReady: (value) => set({ isReady: value }),
74
+ setAddress: (address) => set((state) => {
75
+ const addressChanged = state.address !== null && state.address !== address;
76
+ if (addressChanged) {
77
+ return {
78
+ address,
79
+ spotState: null,
80
+ tradeHistories: null,
81
+ rawOpenPositions: null,
82
+ openOrders: null,
83
+ accountSummary: null,
84
+ twapDetails: null,
85
+ notifications: null,
86
+ userExtraAgents: null,
87
+ userAbstractionMode: null,
88
+ };
78
89
  }
79
90
  return { address };
80
91
  }),
@@ -86,10 +97,11 @@ const useUserData = create((set) => ({
86
97
  setNotifications: (value) => set({ notifications: value }),
87
98
  setSpotState: (value) => set({ spotState: value }),
88
99
  clean: () => set({
89
- accessToken: null,
90
- refreshToken: null,
91
- isAuthenticated: false,
92
- address: null,
100
+ // accessToken: null,
101
+ // refreshToken: null,
102
+ // isAuthenticated: false,
103
+ // isReady: false,
104
+ // address: null,
93
105
  tradeHistories: null,
94
106
  rawOpenPositions: null,
95
107
  openOrders: null,
@@ -382,19 +394,19 @@ class TokenMetadataExtractor {
382
394
  if (!assetCtx) {
383
395
  return null;
384
396
  }
385
- // Get current price - prefer assetCtx.midPx as it's already index-matched,
386
- // fall back to allMids lookup if midPx is null
397
+ // Get current price - prefer allMids (real-time WebSocket data),
398
+ // fall back to assetCtx.midPx if not available
387
399
  const actualSymbol = foundAsset.name;
388
400
  let currentPrice = 0;
389
- // Fallback: assetCtx.midPx (already properly indexed)
390
- if (!currentPrice || isNaN(currentPrice)) {
391
- const currentPriceStr = allMids.mids[actualSymbol] || allMids.mids[symbol];
392
- currentPrice = currentPriceStr ? parseFloat(currentPriceStr) : 0;
393
- }
394
- // Primary source: allMids lookup
401
+ // Fallback: assetCtx.midPx (from REST API, less frequent)
395
402
  if (assetCtx.midPx) {
396
403
  currentPrice = parseFloat(assetCtx.midPx);
397
404
  }
405
+ // Primary source: allMids (real-time WebSocket data)
406
+ const currentPriceStr = allMids.mids[actualSymbol] || allMids.mids[symbol];
407
+ if (currentPriceStr) {
408
+ currentPrice = parseFloat(currentPriceStr);
409
+ }
398
410
  // Get previous day price
399
411
  const prevDayPrice = parseFloat(assetCtx.prevDayPx);
400
412
  // Calculate 24h price change
@@ -549,7 +561,7 @@ const useHyperliquidData = create((set) => ({
549
561
  tokenMetadata: refreshTokenMetadata(state, { allMids: value }),
550
562
  })),
551
563
  setActiveAssetData: (value) => set((state) => {
552
- const activeAssetData = typeof value === 'function' ? value(state.activeAssetData) : value;
564
+ const activeAssetData = typeof value === "function" ? value(state.activeAssetData) : value;
553
565
  return {
554
566
  activeAssetData,
555
567
  tokenMetadata: refreshTokenMetadata(state, { activeAssetData }),
@@ -589,7 +601,10 @@ const useHyperliquidData = create((set) => ({
589
601
  setCandleData: (value) => set({ candleData: value }),
590
602
  upsertActiveAssetData: (key, value) => set((state) => {
591
603
  var _a;
592
- const activeAssetData = { ...((_a = state.activeAssetData) !== null && _a !== void 0 ? _a : {}), [key]: value };
604
+ const activeAssetData = {
605
+ ...((_a = state.activeAssetData) !== null && _a !== void 0 ? _a : {}),
606
+ [key]: value,
607
+ };
593
608
  return {
594
609
  activeAssetData,
595
610
  tokenMetadata: refreshTokenMetadata(state, { activeAssetData }, { symbols: [key] }),
@@ -624,6 +639,29 @@ const useHyperliquidData = create((set) => ({
624
639
  perpMetasByDex: state.perpMetasByDex,
625
640
  }),
626
641
  })),
642
+ clearUserData: () => set((state) => {
643
+ const refreshedMetadata = refreshTokenMetadata(state, {
644
+ activeAssetData: null,
645
+ });
646
+ // Remove user-scoped fields even when upstream metadata inputs are not ready.
647
+ const tokenMetadata = Object.fromEntries(Object.entries(refreshedMetadata).map(([symbol, metadata]) => [
648
+ symbol,
649
+ metadata
650
+ ? {
651
+ ...metadata,
652
+ leverage: undefined,
653
+ maxTradeSzs: undefined,
654
+ availableToTrade: undefined,
655
+ }
656
+ : null,
657
+ ]));
658
+ return {
659
+ aggregatedClearingHouseState: null,
660
+ rawClearinghouseStates: null,
661
+ activeAssetData: null,
662
+ tokenMetadata,
663
+ };
664
+ }),
627
665
  }));
628
666
 
629
667
  /**
@@ -887,7 +925,7 @@ const useUserSelection$1 = create((set, get) => ({
887
925
 
888
926
  const useHyperliquidNativeWebSocket = ({ address, enabled = true, onUserFills, }) => {
889
927
  const { setAllMids, setActiveAssetData, upsertActiveAssetData, setCandleData, deleteCandleSymbol, deleteActiveAssetData, addCandleData, setFinalAtOICaps, setAggregatedClearingHouseState, setRawClearinghouseStates, setAssetContextsByDex, } = useHyperliquidData();
890
- const { setSpotState } = useUserData();
928
+ const { setSpotState, setUserAbstractionMode } = useUserData();
891
929
  const { candleInterval } = useUserSelection$1();
892
930
  const userSummary = useUserData((state) => state.accountSummary);
893
931
  const longTokens = useUserSelection$1((s) => s.longTokens);
@@ -899,18 +937,26 @@ const useHyperliquidNativeWebSocket = ({ address, enabled = true, onUserFills, }
899
937
  const [subscribedCandleTokens, setSubscribedCandleTokens] = useState([]);
900
938
  const [clearinghouseStateReceived, setClearinghouseStateReceived] = useState(false);
901
939
  const prevCandleIntervalRef = useRef(null);
940
+ const prevActiveAssetAddressRef = useRef(null);
902
941
  const pingIntervalRef = useRef(null);
903
942
  const wsRef = useRef(null);
904
943
  const reconnectAttemptsRef = useRef(0);
905
944
  const manualCloseRef = useRef(false);
945
+ const onUserFillsRef = useRef(onUserFills);
946
+ const reconnectTimeoutRef = useRef(null);
906
947
  const [readyState, setReadyState] = useState(ReadyState.CONNECTING);
948
+ // Keep the ref updated with the latest callback
949
+ useEffect(() => {
950
+ onUserFillsRef.current = onUserFills;
951
+ }, [onUserFills]);
907
952
  const handleMessage = useCallback((event) => {
953
+ var _a;
908
954
  try {
909
955
  const message = JSON.parse(event.data);
910
956
  // Handle subscription responses
911
- if ('success' in message || 'error' in message) {
957
+ if ("success" in message || "error" in message) {
912
958
  if (message.error) {
913
- console.error('[HyperLiquid WS] Subscription error:', message.error);
959
+ console.error("[HyperLiquid WS] Subscription error:", message.error);
914
960
  setLastError(message.error);
915
961
  }
916
962
  else {
@@ -919,43 +965,44 @@ const useHyperliquidNativeWebSocket = ({ address, enabled = true, onUserFills, }
919
965
  return;
920
966
  }
921
967
  // Handle channel data messages
922
- if ('channel' in message && 'data' in message) {
968
+ if ("channel" in message && "data" in message) {
923
969
  const response = message;
924
970
  switch (response.channel) {
925
- case 'userFills':
971
+ case "userFills":
926
972
  {
927
- const maybePromise = onUserFills === null || onUserFills === void 0 ? void 0 : onUserFills();
973
+ const maybePromise = (_a = onUserFillsRef.current) === null || _a === void 0 ? void 0 : _a.call(onUserFillsRef);
928
974
  if (maybePromise instanceof Promise) {
929
- maybePromise.catch((err) => console.error('[HyperLiquid WS] userFills callback error', err));
975
+ maybePromise.catch((err) => console.error("[HyperLiquid WS] userFills callback error", err));
930
976
  }
931
977
  }
932
978
  break;
933
- case 'webData3':
979
+ case "webData3":
934
980
  const webData3 = response.data;
935
981
  // finalAssetContexts now sourced from allDexsAssetCtxs channel
936
982
  const finalAtOICaps = webData3.perpDexStates.flatMap((dex) => dex.perpsAtOpenInterestCap);
937
983
  setFinalAtOICaps(finalAtOICaps);
984
+ setUserAbstractionMode(webData3.userState.abstraction || null);
938
985
  break;
939
- case 'allDexsAssetCtxs':
986
+ case "allDexsAssetCtxs":
940
987
  {
941
988
  const data = response.data;
942
989
  // Store by DEX name, mapping '' to 'HYPERLIQUID'
943
990
  const assetContextsByDex = new Map();
944
991
  data.ctxs.forEach(([dexKey, ctxs]) => {
945
- const dexName = dexKey === '' ? 'HYPERLIQUID' : dexKey;
992
+ const dexName = dexKey === "" ? "HYPERLIQUID" : dexKey;
946
993
  assetContextsByDex.set(dexName, ctxs || []);
947
994
  });
948
995
  setAssetContextsByDex(assetContextsByDex);
949
996
  }
950
997
  break;
951
- case 'allDexsClearinghouseState':
998
+ case "allDexsClearinghouseState":
952
999
  {
953
1000
  const data = response.data;
954
1001
  const states = (data.clearinghouseStates || [])
955
1002
  .map(([, s]) => s)
956
1003
  .filter(Boolean);
957
- const sum = (values) => values.reduce((acc, v) => acc + (parseFloat(v || '0') || 0), 0);
958
- const toStr = (n) => Number.isFinite(n) ? n.toString() : '0';
1004
+ const sum = (values) => values.reduce((acc, v) => acc + (parseFloat(v || "0") || 0), 0);
1005
+ const toStr = (n) => Number.isFinite(n) ? n.toString() : "0";
959
1006
  const assetPositions = states.flatMap((s) => s.assetPositions || []);
960
1007
  const crossMaintenanceMarginUsed = toStr(sum(states.map((s) => s.crossMaintenanceMarginUsed)));
961
1008
  const crossMarginSummary = {
@@ -985,26 +1032,26 @@ const useHyperliquidNativeWebSocket = ({ address, enabled = true, onUserFills, }
985
1032
  setClearinghouseStateReceived(true);
986
1033
  }
987
1034
  break;
988
- case 'allMids':
1035
+ case "allMids":
989
1036
  {
990
1037
  const data = response.data;
991
1038
  setAllMids(data);
992
1039
  }
993
1040
  break;
994
- case 'activeAssetData':
1041
+ case "activeAssetData":
995
1042
  {
996
1043
  const assetData = response.data;
997
1044
  upsertActiveAssetData(assetData.coin, assetData);
998
1045
  }
999
1046
  break;
1000
- case 'candle':
1047
+ case "candle":
1001
1048
  {
1002
1049
  const candleDataItem = response.data;
1003
- const symbol = candleDataItem.s || '';
1050
+ const symbol = candleDataItem.s || "";
1004
1051
  addCandleData(symbol, candleDataItem);
1005
1052
  }
1006
1053
  break;
1007
- case 'spotState':
1054
+ case "spotState":
1008
1055
  {
1009
1056
  const spotStateData = response.data;
1010
1057
  if (spotStateData === null || spotStateData === void 0 ? void 0 : spotStateData.spotState) {
@@ -1019,7 +1066,7 @@ const useHyperliquidNativeWebSocket = ({ address, enabled = true, onUserFills, }
1019
1066
  }
1020
1067
  catch (error) {
1021
1068
  const errorMessage = `Failed to parse message: ${error instanceof Error ? error.message : String(error)}`;
1022
- console.error('[HyperLiquid WS] Parse error:', errorMessage, 'Raw message:', event.data);
1069
+ console.error("[HyperLiquid WS] Parse error:", errorMessage, "Raw message:", event.data);
1023
1070
  setLastError(errorMessage);
1024
1071
  }
1025
1072
  }, [
@@ -1031,18 +1078,25 @@ const useHyperliquidNativeWebSocket = ({ address, enabled = true, onUserFills, }
1031
1078
  setRawClearinghouseStates,
1032
1079
  setAssetContextsByDex,
1033
1080
  setSpotState,
1034
- onUserFills,
1035
1081
  ]);
1036
1082
  const connect = useCallback(() => {
1083
+ console.log("[HyperLiquid WS] connect() called, enabled:", enabled);
1037
1084
  if (!enabled)
1038
1085
  return;
1086
+ // Clear any pending reconnect timeout
1087
+ if (reconnectTimeoutRef.current) {
1088
+ clearTimeout(reconnectTimeoutRef.current);
1089
+ reconnectTimeoutRef.current = null;
1090
+ }
1039
1091
  try {
1040
1092
  // Avoid opening multiple sockets if one is already active or connecting
1041
1093
  if (wsRef.current &&
1042
1094
  (wsRef.current.readyState === WebSocket.OPEN ||
1043
1095
  wsRef.current.readyState === WebSocket.CONNECTING)) {
1096
+ console.log('[HyperLiquid WS] connect() returning early - socket already exists, readyState:', wsRef.current.readyState);
1044
1097
  return;
1045
1098
  }
1099
+ console.log("[HyperLiquid WS] Creating new WebSocket connection");
1046
1100
  manualCloseRef.current = false;
1047
1101
  setReadyState(ReadyState.CONNECTING);
1048
1102
  const ws = new WebSocket("wss://api.hyperliquid.xyz/ws");
@@ -1059,12 +1113,17 @@ const useHyperliquidNativeWebSocket = ({ address, enabled = true, onUserFills, }
1059
1113
  };
1060
1114
  ws.onclose = () => {
1061
1115
  setReadyState(ReadyState.CLOSED);
1062
- if (!manualCloseRef.current && reconnectAttemptsRef.current < 5) {
1116
+ // Reset subscription state so effects will resubscribe on reconnect
1117
+ setSubscribedAddress(null);
1118
+ setSubscribedTokens([]);
1119
+ setSubscribedCandleTokens([]);
1120
+ setClearinghouseStateReceived(false);
1121
+ if (!manualCloseRef.current) {
1063
1122
  reconnectAttemptsRef.current += 1;
1064
- if (reconnectAttemptsRef.current === 5) {
1065
- console.error('[HyperLiquid WS] Reconnection stopped after 5 attempts');
1066
- }
1067
- setTimeout(() => connect(), 3000);
1123
+ // Exponential backoff: 1s, 2s, 4s, 8s, 16s, 30s (max), then stay at 30s
1124
+ const delay = Math.min(1000 * Math.pow(2, reconnectAttemptsRef.current - 1), 30000);
1125
+ console.log(`[HyperLiquid WS] Reconnecting in ${delay}ms (attempt ${reconnectAttemptsRef.current})`);
1126
+ reconnectTimeoutRef.current = setTimeout(() => connect(), delay);
1068
1127
  }
1069
1128
  };
1070
1129
  }
@@ -1073,9 +1132,55 @@ const useHyperliquidNativeWebSocket = ({ address, enabled = true, onUserFills, }
1073
1132
  }
1074
1133
  }, [handleMessage, enabled]);
1075
1134
  useEffect(() => {
1135
+ console.log('[HyperLiquid WS] Connection effect running - calling connect()');
1076
1136
  connect();
1137
+ // Handle online/offline events to reconnect when internet is restored
1138
+ const handleOnline = () => {
1139
+ console.log('[HyperLiquid WS] Browser went online, attempting reconnect');
1140
+ // Reset reconnect attempts when internet comes back
1141
+ reconnectAttemptsRef.current = 0;
1142
+ // Clear any pending reconnect timeout
1143
+ if (reconnectTimeoutRef.current) {
1144
+ clearTimeout(reconnectTimeoutRef.current);
1145
+ reconnectTimeoutRef.current = null;
1146
+ }
1147
+ // Reset subscription state so effects will resubscribe on reconnect
1148
+ setSubscribedAddress(null);
1149
+ setSubscribedTokens([]);
1150
+ setSubscribedCandleTokens([]);
1151
+ setClearinghouseStateReceived(false);
1152
+ // Close existing socket if in a bad state
1153
+ if (wsRef.current &&
1154
+ wsRef.current.readyState !== WebSocket.OPEN &&
1155
+ wsRef.current.readyState !== WebSocket.CONNECTING) {
1156
+ try {
1157
+ wsRef.current.close();
1158
+ }
1159
+ catch (_a) { }
1160
+ wsRef.current = null;
1161
+ }
1162
+ // Attempt to reconnect
1163
+ connect();
1164
+ };
1165
+ const handleOffline = () => {
1166
+ console.log('[HyperLiquid WS] Browser went offline');
1167
+ // Clear pending reconnect timeout since we're offline
1168
+ if (reconnectTimeoutRef.current) {
1169
+ clearTimeout(reconnectTimeoutRef.current);
1170
+ reconnectTimeoutRef.current = null;
1171
+ }
1172
+ };
1173
+ window.addEventListener('online', handleOnline);
1174
+ window.addEventListener('offline', handleOffline);
1077
1175
  return () => {
1176
+ console.log('[HyperLiquid WS] Connection effect cleanup - closing existing connection');
1177
+ window.removeEventListener('online', handleOnline);
1178
+ window.removeEventListener('offline', handleOffline);
1078
1179
  manualCloseRef.current = true;
1180
+ if (reconnectTimeoutRef.current) {
1181
+ clearTimeout(reconnectTimeoutRef.current);
1182
+ reconnectTimeoutRef.current = null;
1183
+ }
1079
1184
  if (wsRef.current) {
1080
1185
  try {
1081
1186
  wsRef.current.close();
@@ -1114,14 +1219,23 @@ const useHyperliquidNativeWebSocket = ({ address, enabled = true, onUserFills, }
1114
1219
  }, [isConnected, sendJsonMessage]);
1115
1220
  // Handle address subscription changes
1116
1221
  useEffect(() => {
1117
- if (!isConnected)
1118
- return;
1119
1222
  const DEFAULT_ADDRESS = "0x0000000000000000000000000000000000000000";
1120
- const userAddress = address || DEFAULT_ADDRESS;
1121
- if (subscribedAddress === userAddress)
1223
+ const userAddress = (address || DEFAULT_ADDRESS).toLowerCase();
1224
+ const normalizedSubscribedAddress = (subscribedAddress === null || subscribedAddress === void 0 ? void 0 : subscribedAddress.toLowerCase()) || null;
1225
+ console.log("[HyperLiquid WS] Address subscription effect running");
1226
+ console.log("[HyperLiquid WS] address:", address, "userAddress:", userAddress, "subscribedAddress:", subscribedAddress, "normalizedSubscribedAddress:", normalizedSubscribedAddress);
1227
+ console.log("[HyperLiquid WS] isConnected:", isConnected);
1228
+ if (normalizedSubscribedAddress === userAddress) {
1229
+ console.log("[HyperLiquid WS] Address unchanged, skipping subscription update");
1122
1230
  return;
1231
+ }
1232
+ if (!isConnected) {
1233
+ console.log("[HyperLiquid WS] Not connected, skipping subscription update");
1234
+ return;
1235
+ }
1123
1236
  // Unsubscribe from previous address if exists
1124
1237
  if (subscribedAddress) {
1238
+ console.log("[HyperLiquid WS] Unsubscribing from previous address:", subscribedAddress);
1125
1239
  const unsubscribeMessage = {
1126
1240
  method: "unsubscribe",
1127
1241
  subscription: {
@@ -1133,9 +1247,9 @@ const useHyperliquidNativeWebSocket = ({ address, enabled = true, onUserFills, }
1133
1247
  // Unsubscribe from spotState for previous address
1134
1248
  if (subscribedAddress !== DEFAULT_ADDRESS) {
1135
1249
  const unsubscribeSpotState = {
1136
- method: 'unsubscribe',
1250
+ method: "unsubscribe",
1137
1251
  subscription: {
1138
- type: 'spotState',
1252
+ type: "spotState",
1139
1253
  user: subscribedAddress,
1140
1254
  },
1141
1255
  };
@@ -1150,9 +1264,9 @@ const useHyperliquidNativeWebSocket = ({ address, enabled = true, onUserFills, }
1150
1264
  };
1151
1265
  sendJsonMessage(unsubscribeAllDexsClearinghouseState);
1152
1266
  const unsubscribeUserFills = {
1153
- method: 'unsubscribe',
1267
+ method: "unsubscribe",
1154
1268
  subscription: {
1155
- type: 'userFills',
1269
+ type: "userFills",
1156
1270
  user: subscribedAddress,
1157
1271
  },
1158
1272
  };
@@ -1165,14 +1279,6 @@ const useHyperliquidNativeWebSocket = ({ address, enabled = true, onUserFills, }
1165
1279
  user: userAddress,
1166
1280
  },
1167
1281
  };
1168
- // Subscribe to allDexsClearinghouseState with the same payload as webData3
1169
- const subscribeAllDexsClearinghouseState = {
1170
- method: "subscribe",
1171
- subscription: {
1172
- type: "allDexsClearinghouseState",
1173
- user: userAddress,
1174
- },
1175
- };
1176
1282
  // Subscribe to allMids
1177
1283
  const subscribeAllMids = {
1178
1284
  method: "subscribe",
@@ -1188,25 +1294,38 @@ const useHyperliquidNativeWebSocket = ({ address, enabled = true, onUserFills, }
1188
1294
  type: "allDexsAssetCtxs",
1189
1295
  },
1190
1296
  };
1297
+ console.log("[HyperLiquid WS] Subscribing to new address:", userAddress);
1191
1298
  sendJsonMessage(subscribeWebData3);
1192
- sendJsonMessage(subscribeAllDexsClearinghouseState);
1193
1299
  sendJsonMessage(subscribeAllMids);
1194
1300
  sendJsonMessage(subscribeAllDexsAssetCtxs);
1195
1301
  // Subscribe to spotState for real-time spot balances (USDH, USDC, etc.)
1196
1302
  // Only subscribe if we have a real user address (not the default)
1197
- if (userAddress !== DEFAULT_ADDRESS) {
1303
+ if (userAddress !== DEFAULT_ADDRESS.toLowerCase()) {
1198
1304
  const subscribeSpotState = {
1199
- method: 'subscribe',
1305
+ method: "subscribe",
1200
1306
  subscription: {
1201
- type: 'spotState',
1307
+ type: "spotState",
1202
1308
  user: userAddress,
1203
1309
  },
1204
1310
  };
1205
1311
  sendJsonMessage(subscribeSpotState);
1206
1312
  }
1313
+ // Subscribe to allDexsClearinghouseState with the same payload as webData3
1314
+ // Only subscribe if we have a real user address (not the default)
1315
+ if (userAddress !== DEFAULT_ADDRESS.toLowerCase()) {
1316
+ const subscribeAllDexsClearinghouseState = {
1317
+ method: "subscribe",
1318
+ subscription: {
1319
+ type: "allDexsClearinghouseState",
1320
+ user: userAddress,
1321
+ },
1322
+ };
1323
+ sendJsonMessage(subscribeAllDexsClearinghouseState);
1324
+ }
1207
1325
  setSubscribedAddress(userAddress);
1208
1326
  // Clear previous data when address changes
1209
- if (subscribedAddress && subscribedAddress !== userAddress) {
1327
+ if (normalizedSubscribedAddress &&
1328
+ normalizedSubscribedAddress !== userAddress) {
1210
1329
  // clear aggregatedClearingHouseState
1211
1330
  setAggregatedClearingHouseState(null);
1212
1331
  setRawClearinghouseStates(null);
@@ -1225,23 +1344,57 @@ const useHyperliquidNativeWebSocket = ({ address, enabled = true, onUserFills, }
1225
1344
  setSpotState,
1226
1345
  ]);
1227
1346
  useEffect(() => {
1228
- if (!isConnected || !subscribedAddress || !clearinghouseStateReceived || !userSummary)
1347
+ if (!isConnected ||
1348
+ !subscribedAddress ||
1349
+ !clearinghouseStateReceived ||
1350
+ !userSummary)
1229
1351
  return;
1230
1352
  const subscribeUserFills = {
1231
- method: 'subscribe',
1353
+ method: "subscribe",
1232
1354
  subscription: {
1233
- type: 'userFills',
1355
+ type: "userFills",
1234
1356
  user: subscribedAddress,
1235
1357
  },
1236
1358
  };
1237
1359
  sendJsonMessage(subscribeUserFills);
1238
- }, [isConnected, subscribedAddress, clearinghouseStateReceived, userSummary, sendJsonMessage]);
1360
+ }, [
1361
+ isConnected,
1362
+ subscribedAddress,
1363
+ clearinghouseStateReceived,
1364
+ userSummary,
1365
+ sendJsonMessage,
1366
+ ]);
1367
+ // activeAssetData is user-scoped; reset subscriptions/data whenever address changes.
1368
+ useEffect(() => {
1369
+ if (!isConnected)
1370
+ return;
1371
+ const previousAddress = prevActiveAssetAddressRef.current;
1372
+ if (previousAddress === address)
1373
+ return;
1374
+ if (previousAddress) {
1375
+ subscribedTokens.forEach((token) => {
1376
+ const unsubscribeMessage = {
1377
+ method: "unsubscribe",
1378
+ subscription: {
1379
+ type: "activeAssetData",
1380
+ user: previousAddress,
1381
+ coin: token,
1382
+ },
1383
+ };
1384
+ sendJsonMessage(unsubscribeMessage);
1385
+ });
1386
+ }
1387
+ // Clear user-derived leverage/available-to-trade data before re-subscribing.
1388
+ setActiveAssetData(null);
1389
+ setSubscribedTokens([]);
1390
+ prevActiveAssetAddressRef.current = address;
1391
+ }, [isConnected, address, subscribedTokens, sendJsonMessage, setActiveAssetData]);
1239
1392
  // Handle token subscriptions for activeAssetData
1240
1393
  useEffect(() => {
1241
1394
  if (!isConnected || !address)
1242
1395
  return;
1243
- const effectiveTokens = selectedTokenSymbols;
1244
- const tokensToSubscribe = effectiveTokens.filter((token) => token && !subscribedTokens.includes(token));
1396
+ const effectiveTokens = selectedTokenSymbols.filter((token) => token);
1397
+ const tokensToSubscribe = effectiveTokens.filter((token) => !subscribedTokens.includes(token));
1245
1398
  const tokensToUnsubscribe = subscribedTokens.filter((token) => !effectiveTokens.includes(token));
1246
1399
  // Unsubscribe from tokens no longer in the list
1247
1400
  tokensToUnsubscribe.forEach((token) => {
@@ -1268,7 +1421,7 @@ const useHyperliquidNativeWebSocket = ({ address, enabled = true, onUserFills, }
1268
1421
  sendJsonMessage(subscribeMessage);
1269
1422
  });
1270
1423
  if (tokensToSubscribe.length > 0 || tokensToUnsubscribe.length > 0) {
1271
- setSubscribedTokens(effectiveTokens.filter((token) => token));
1424
+ setSubscribedTokens(effectiveTokens);
1272
1425
  tokensToSubscribe.forEach((token) => deleteActiveAssetData(token));
1273
1426
  }
1274
1427
  }, [
@@ -1277,7 +1430,7 @@ const useHyperliquidNativeWebSocket = ({ address, enabled = true, onUserFills, }
1277
1430
  selectedTokenSymbols,
1278
1431
  subscribedTokens,
1279
1432
  sendJsonMessage,
1280
- setActiveAssetData,
1433
+ deleteActiveAssetData,
1281
1434
  ]);
1282
1435
  // Handle candle subscriptions for tokens and interval changes
1283
1436
  useEffect(() => {
@@ -1453,8 +1606,9 @@ const getAssetByName = (tokenMetadata, symbol) => {
1453
1606
  * Hook to access webData
1454
1607
  */
1455
1608
  const useMarket = () => {
1609
+ const supportedMarkets = ['USDT', 'USDT0', 'USDC', 'USDH'];
1456
1610
  const tokenMetadata = useHyperliquidData((state) => state.tokenMetadata);
1457
- const allTokenMetadata = useMemo(() => Object.values(tokenMetadata).filter((metadata) => Boolean(metadata)), [tokenMetadata]);
1611
+ const allTokenMetadata = useMemo(() => Object.values(tokenMetadata).filter((metadata) => Boolean(metadata)).filter((token) => { var _a; return supportedMarkets.includes((_a = token.collateralToken) !== null && _a !== void 0 ? _a : ''); }), [tokenMetadata]);
1458
1612
  const getAssetByName$1 = useCallback((symbol) => getAssetByName(tokenMetadata, symbol), [tokenMetadata]);
1459
1613
  return { allTokenMetadata, getAssetByName: getAssetByName$1 };
1460
1614
  };
@@ -1469,17 +1623,33 @@ const useTradeHistories = () => {
1469
1623
  const enrichedTradeHistories = useMemo(() => {
1470
1624
  if (!tradeHistories)
1471
1625
  return null;
1472
- return tradeHistories.map((history) => ({
1473
- ...history,
1474
- closedLongAssets: history.closedLongAssets.map((asset) => ({
1475
- ...asset,
1476
- metadata: getAssetByName(asset.coin),
1477
- })),
1478
- closedShortAssets: history.closedShortAssets.map((asset) => ({
1479
- ...asset,
1480
- metadata: getAssetByName(asset.coin),
1481
- })),
1482
- }));
1626
+ return tradeHistories.map((history) => {
1627
+ const totalClosePositionSize = history.closedLongAssets.reduce((acc, asset) => acc + asset.limitPrice * asset.size, 0) +
1628
+ history.closedShortAssets.reduce((acc, asset) => acc + asset.limitPrice * asset.size, 0);
1629
+ return {
1630
+ ...history,
1631
+ closedLongAssets: history.closedLongAssets.map((asset) => {
1632
+ const closeNotional = asset.limitPrice * asset.size;
1633
+ return {
1634
+ ...asset,
1635
+ closeWeight: totalClosePositionSize > 0
1636
+ ? closeNotional / totalClosePositionSize
1637
+ : 0,
1638
+ metadata: getAssetByName(asset.coin),
1639
+ };
1640
+ }),
1641
+ closedShortAssets: history.closedShortAssets.map((asset) => {
1642
+ const closeNotional = asset.limitPrice * asset.size;
1643
+ return {
1644
+ ...asset,
1645
+ closeWeight: totalClosePositionSize > 0
1646
+ ? closeNotional / totalClosePositionSize
1647
+ : 0,
1648
+ metadata: getAssetByName(asset.coin),
1649
+ };
1650
+ }),
1651
+ };
1652
+ });
1483
1653
  }, [tradeHistories, getAssetByName]);
1484
1654
  const isLoading = useMemo(() => {
1485
1655
  return tradeHistories === null && context.isConnected;
@@ -1560,7 +1730,7 @@ const useTokenSelectionMetadataStore = create((set) => ({
1560
1730
  maxLeverage: 0,
1561
1731
  minSize: {},
1562
1732
  leverageMatched: true,
1563
- recompute: ({ tokenMetadata, marketData, longTokens, shortTokens, }) => {
1733
+ recompute: ({ tokenMetadata, marketData, longTokens, shortTokens }) => {
1564
1734
  const isPriceDataReady = Object.keys(tokenMetadata).length > 0;
1565
1735
  const longSymbols = longTokens.map((t) => t.symbol);
1566
1736
  const shortSymbols = shortTokens.map((t) => t.symbol);
@@ -1691,10 +1861,9 @@ const useTokenSelectionMetadataStore = create((set) => ({
1691
1861
  return 0;
1692
1862
  let maxLev = 0;
1693
1863
  allSymbols.forEach((symbol) => {
1694
- var _a;
1695
1864
  const tokenDetail = tokenMetadata[symbol];
1696
- if ((_a = tokenDetail === null || tokenDetail === void 0 ? void 0 : tokenDetail.leverage) === null || _a === void 0 ? void 0 : _a.value) {
1697
- maxLev = Math.max(maxLev, tokenDetail.leverage.value);
1865
+ if (tokenDetail === null || tokenDetail === void 0 ? void 0 : tokenDetail.maxLeverage) {
1866
+ maxLev = Math.max(maxLev, tokenDetail.maxLeverage);
1698
1867
  }
1699
1868
  });
1700
1869
  return maxLev;
@@ -1818,6 +1987,22 @@ const getIntervalSeconds = (interval) => {
1818
1987
  default: return 60;
1819
1988
  }
1820
1989
  };
1990
+ /**
1991
+ * Merges overlapping or adjacent ranges to prevent unbounded growth
1992
+ */
1993
+ const mergeRanges = (ranges, newRange) => {
1994
+ const all = [...ranges, newRange].sort((a, b) => a.start - b.start);
1995
+ const merged = [];
1996
+ for (const r of all) {
1997
+ if (merged.length === 0 || r.start > merged[merged.length - 1].end) {
1998
+ merged.push({ start: r.start, end: r.end });
1999
+ }
2000
+ else {
2001
+ merged[merged.length - 1].end = Math.max(merged[merged.length - 1].end, r.end);
2002
+ }
2003
+ }
2004
+ return merged;
2005
+ };
1821
2006
  const useHistoricalPriceDataStore = create((set, get) => ({
1822
2007
  historicalPriceData: {},
1823
2008
  loadingTokens: new Set(),
@@ -1828,6 +2013,8 @@ const useHistoricalPriceDataStore = create((set, get) => ({
1828
2013
  if (!existing) {
1829
2014
  // Create new entry
1830
2015
  const sortedCandles = [...candles].sort((a, b) => a.t - b.t);
2016
+ // If first fetch returns empty, mark that there's no data before the requested end time
2017
+ const noDataBefore = sortedCandles.length === 0 ? range.end : null;
1831
2018
  return {
1832
2019
  historicalPriceData: {
1833
2020
  ...state.historicalPriceData,
@@ -1836,7 +2023,9 @@ const useHistoricalPriceDataStore = create((set, get) => ({
1836
2023
  interval,
1837
2024
  candles: sortedCandles,
1838
2025
  oldestTime: sortedCandles.length > 0 ? sortedCandles[0].t : null,
1839
- latestTime: sortedCandles.length > 0 ? sortedCandles[sortedCandles.length - 1].t : null
2026
+ latestTime: sortedCandles.length > 0 ? sortedCandles[sortedCandles.length - 1].t : null,
2027
+ requestedRanges: [range],
2028
+ noDataBefore
1840
2029
  }
1841
2030
  }
1842
2031
  };
@@ -1848,6 +2037,16 @@ const useHistoricalPriceDataStore = create((set, get) => ({
1848
2037
  // Update time pointers
1849
2038
  const oldestTime = mergedCandles.length > 0 ? mergedCandles[0].t : null;
1850
2039
  const latestTime = mergedCandles.length > 0 ? mergedCandles[mergedCandles.length - 1].t : null;
2040
+ // Merge requested ranges
2041
+ const mergedRanges = mergeRanges(existing.requestedRanges || [], range);
2042
+ // Update noDataBefore boundary:
2043
+ // If we fetched a range older than our current oldest data and got no new candles,
2044
+ // it means there's no data before our current oldest time
2045
+ let noDataBefore = existing.noDataBefore;
2046
+ if (candles.length === 0 && existing.oldestTime !== null && range.end <= existing.oldestTime) {
2047
+ // We tried to fetch older data and got nothing - set boundary to our oldest known data
2048
+ noDataBefore = existing.oldestTime;
2049
+ }
1851
2050
  return {
1852
2051
  historicalPriceData: {
1853
2052
  ...state.historicalPriceData,
@@ -1855,7 +2054,9 @@ const useHistoricalPriceDataStore = create((set, get) => ({
1855
2054
  ...existing,
1856
2055
  candles: mergedCandles,
1857
2056
  oldestTime,
1858
- latestTime
2057
+ latestTime,
2058
+ requestedRanges: mergedRanges,
2059
+ noDataBefore
1859
2060
  }
1860
2061
  }
1861
2062
  };
@@ -1865,8 +2066,24 @@ const useHistoricalPriceDataStore = create((set, get) => ({
1865
2066
  const { historicalPriceData } = get();
1866
2067
  const key = createKey(symbol, interval);
1867
2068
  const tokenData = historicalPriceData[key];
1868
- if (!tokenData || tokenData.oldestTime === null || tokenData.latestTime === null)
2069
+ if (!tokenData)
2070
+ return false;
2071
+ // Check if we've hit the "no data before" boundary
2072
+ // If the requested range ends before or at the boundary, we know there's no data
2073
+ if (tokenData.noDataBefore !== null && endTime <= tokenData.noDataBefore) {
2074
+ return true; // No point fetching - we know there's no data before this point
2075
+ }
2076
+ // Check if we've already attempted to fetch this range
2077
+ const requestedRanges = tokenData.requestedRanges || [];
2078
+ for (const range of requestedRanges) {
2079
+ if (range.start <= startTime && range.end >= endTime) {
2080
+ return true; // Already attempted this fetch
2081
+ }
2082
+ }
2083
+ // Check actual data coverage
2084
+ if (tokenData.oldestTime === null || tokenData.latestTime === null) {
1869
2085
  return false;
2086
+ }
1870
2087
  const intervalMilisecond = getIntervalSeconds(interval) * 1000;
1871
2088
  const hasStartCoverage = tokenData.oldestTime <= startTime;
1872
2089
  const hasEndCoverage = tokenData.latestTime >= endTime ||
@@ -1881,6 +2098,27 @@ const useHistoricalPriceDataStore = create((set, get) => ({
1881
2098
  return [];
1882
2099
  return tokenData.candles.filter(candle => candle.t >= startTime && candle.t < endTime);
1883
2100
  },
2101
+ getEffectiveDataBoundary: (symbols, interval) => {
2102
+ var _a;
2103
+ const { historicalPriceData } = get();
2104
+ if (symbols.length === 0)
2105
+ return null;
2106
+ let maxBoundary = null;
2107
+ for (const symbol of symbols) {
2108
+ const key = createKey(symbol, interval);
2109
+ const tokenData = historicalPriceData[key];
2110
+ if (!tokenData)
2111
+ continue;
2112
+ // Use noDataBefore if set, otherwise use oldestTime
2113
+ const boundary = (_a = tokenData.noDataBefore) !== null && _a !== void 0 ? _a : tokenData.oldestTime;
2114
+ if (boundary !== null) {
2115
+ if (maxBoundary === null || boundary > maxBoundary) {
2116
+ maxBoundary = boundary;
2117
+ }
2118
+ }
2119
+ }
2120
+ return maxBoundary;
2121
+ },
1884
2122
  setTokenLoading: (symbol, loading) => {
1885
2123
  set(state => {
1886
2124
  const newLoadingTokens = new Set(state.loadingTokens);
@@ -5761,10 +5999,10 @@ function toApiError(error) {
5761
5999
  var _a;
5762
6000
  const axiosError = error;
5763
6001
  const payload = (axiosError && axiosError.response ? axiosError.response.data : undefined);
5764
- const message = typeof payload === 'object' && payload && 'message' in payload
6002
+ const message = typeof payload === "object" && payload && "message" in payload
5765
6003
  ? String(payload.message)
5766
- : (axiosError === null || axiosError === void 0 ? void 0 : axiosError.message) || 'Request failed';
5767
- const errField = typeof payload === 'object' && payload && 'error' in payload
6004
+ : (axiosError === null || axiosError === void 0 ? void 0 : axiosError.message) || "Request failed";
6005
+ const errField = typeof payload === "object" && payload && "error" in payload
5768
6006
  ? String(payload.error)
5769
6007
  : undefined;
5770
6008
  return {
@@ -5774,8 +6012,8 @@ function toApiError(error) {
5774
6012
  };
5775
6013
  }
5776
6014
  function joinUrl(baseUrl, path) {
5777
- const cleanBase = baseUrl.replace(/\/$/, '');
5778
- const cleanPath = path.startsWith('/') ? path : `/${path}`;
6015
+ const cleanBase = baseUrl.replace(/\/$/, "");
6016
+ const cleanPath = path.startsWith("/") ? path : `/${path}`;
5779
6017
  return `${cleanBase}${cleanPath}`;
5780
6018
  }
5781
6019
  /**
@@ -5804,7 +6042,7 @@ function addAuthInterceptors(params) {
5804
6042
  pendingRequests = [];
5805
6043
  }
5806
6044
  const isOurApiUrl = (url) => Boolean(url && url.startsWith(apiBaseUrl));
5807
- const isRefreshUrl = (url) => Boolean(url && url.startsWith(joinUrl(apiBaseUrl, '/auth/refresh')));
6045
+ const isRefreshUrl = (url) => Boolean(url && url.startsWith(joinUrl(apiBaseUrl, "/auth/refresh")));
5808
6046
  const reqId = apiClient.interceptors.request.use((config) => {
5809
6047
  var _a;
5810
6048
  try {
@@ -5812,11 +6050,12 @@ function addAuthInterceptors(params) {
5812
6050
  const token = getAccessToken();
5813
6051
  if (token) {
5814
6052
  config.headers = (_a = config.headers) !== null && _a !== void 0 ? _a : {};
5815
- (config.headers)['Authorization'] = `Bearer ${token}`;
6053
+ config.headers["Authorization"] = `Bearer ${token}`;
5816
6054
  }
5817
6055
  }
5818
6056
  }
5819
- catch (_b) {
6057
+ catch (err) {
6058
+ console.error("[Auth Interceptor] Request interceptor error:", err);
5820
6059
  }
5821
6060
  return config;
5822
6061
  });
@@ -5827,22 +6066,36 @@ function addAuthInterceptors(params) {
5827
6066
  const url = originalRequest === null || originalRequest === void 0 ? void 0 : originalRequest.url;
5828
6067
  // If not our API or not 401, just reject
5829
6068
  if (!status || status !== 401 || !isOurApiUrl(url)) {
6069
+ if (status === 401) {
6070
+ console.warn("[Auth Interceptor] 401 received but URL check failed:", {
6071
+ url,
6072
+ apiBaseUrl,
6073
+ isOurApiUrl: isOurApiUrl(url),
6074
+ });
6075
+ }
5830
6076
  return Promise.reject(error);
5831
6077
  }
6078
+ console.log("[Auth Interceptor] 401 detected, attempting token refresh for URL:", url);
5832
6079
  // If the 401 is from refresh endpoint itself -> force logout
5833
6080
  if (isRefreshUrl(url)) {
6081
+ console.warn("[Auth Interceptor] Refresh endpoint returned 401, logging out");
5834
6082
  try {
5835
6083
  await logout();
5836
6084
  }
5837
- catch (_d) { }
6085
+ catch (err) {
6086
+ console.error("[Auth Interceptor] Logout failed:", err);
6087
+ }
5838
6088
  return Promise.reject(error);
5839
6089
  }
5840
6090
  // Prevent infinite loop
5841
6091
  if (originalRequest && originalRequest._retry) {
6092
+ console.warn("[Auth Interceptor] Request already retried, logging out");
5842
6093
  try {
5843
6094
  await logout();
5844
6095
  }
5845
- catch (_e) { }
6096
+ catch (err) {
6097
+ console.error("[Auth Interceptor] Logout failed:", err);
6098
+ }
5846
6099
  return Promise.reject(error);
5847
6100
  }
5848
6101
  // Mark so we don't retry twice
@@ -5856,31 +6109,45 @@ function addAuthInterceptors(params) {
5856
6109
  if (!newToken || !originalRequest)
5857
6110
  return reject(error);
5858
6111
  originalRequest.headers = (_a = originalRequest.headers) !== null && _a !== void 0 ? _a : {};
5859
- originalRequest.headers['Authorization'] = `Bearer ${newToken}`;
6112
+ originalRequest.headers["Authorization"] =
6113
+ `Bearer ${newToken}`;
5860
6114
  resolve(apiClient.request(originalRequest));
5861
6115
  });
5862
6116
  });
5863
6117
  }
5864
6118
  isRefreshing = true;
5865
6119
  try {
6120
+ console.log("[Auth Interceptor] Refreshing tokens...");
5866
6121
  const refreshed = await refreshTokens();
5867
- const newAccessToken = (_b = (refreshed && (refreshed.accessToken || ((_a = refreshed.data) === null || _a === void 0 ? void 0 : _a.accessToken)))) !== null && _b !== void 0 ? _b : null;
6122
+ const newAccessToken = (_b = (refreshed &&
6123
+ (refreshed.accessToken || ((_a = refreshed.data) === null || _a === void 0 ? void 0 : _a.accessToken)))) !== null && _b !== void 0 ? _b : null;
6124
+ if (!newAccessToken) {
6125
+ console.error("[Auth Interceptor] Token refresh succeeded but no access token in response:", refreshed);
6126
+ }
6127
+ else {
6128
+ console.log("[Auth Interceptor] Token refresh successful");
6129
+ }
5868
6130
  resolvePendingRequests(newAccessToken);
5869
6131
  if (originalRequest) {
5870
6132
  originalRequest.headers = (_c = originalRequest.headers) !== null && _c !== void 0 ? _c : {};
5871
6133
  if (newAccessToken)
5872
- (originalRequest.headers)['Authorization'] = `Bearer ${newAccessToken}`;
6134
+ originalRequest.headers["Authorization"] =
6135
+ `Bearer ${newAccessToken}`;
6136
+ console.log("[Auth Interceptor] Retrying original request with new token");
5873
6137
  const resp = await apiClient.request(originalRequest);
5874
6138
  return resp;
5875
6139
  }
5876
6140
  return Promise.reject(error);
5877
6141
  }
5878
6142
  catch (refreshErr) {
6143
+ console.error("[Auth Interceptor] Token refresh failed:", refreshErr);
5879
6144
  resolvePendingRequests(null);
5880
6145
  try {
5881
6146
  await logout();
5882
6147
  }
5883
- catch (_f) { }
6148
+ catch (err) {
6149
+ console.error("[Auth Interceptor] Logout failed:", err);
6150
+ }
5884
6151
  return Promise.reject(refreshErr);
5885
6152
  }
5886
6153
  finally {
@@ -5891,11 +6158,15 @@ function addAuthInterceptors(params) {
5891
6158
  try {
5892
6159
  apiClient.interceptors.request.eject(reqId);
5893
6160
  }
5894
- catch (_a) { }
6161
+ catch (err) {
6162
+ console.error("[Auth Interceptor] Failed to eject request interceptor:", err);
6163
+ }
5895
6164
  try {
5896
6165
  apiClient.interceptors.response.eject(resId);
5897
6166
  }
5898
- catch (_b) { }
6167
+ catch (err) {
6168
+ console.error("[Auth Interceptor] Failed to eject response interceptor:", err);
6169
+ }
5899
6170
  };
5900
6171
  }
5901
6172
 
@@ -6011,7 +6282,7 @@ const useHistoricalPriceData = () => {
6011
6282
  const shortTokens = useUserSelection$1((state) => state.shortTokens);
6012
6283
  const candleInterval = useUserSelection$1((state) => state.candleInterval);
6013
6284
  // Historical price data store
6014
- const { addHistoricalPriceData, hasHistoricalPriceData: storeHasData, getHistoricalPriceData: storeGetData, setTokenLoading, isTokenLoading, removeTokenPriceData, clearData, } = useHistoricalPriceDataStore();
6285
+ const { addHistoricalPriceData, hasHistoricalPriceData: storeHasData, getHistoricalPriceData: storeGetData, getEffectiveDataBoundary: storeGetBoundary, setTokenLoading, isTokenLoading, removeTokenPriceData, clearData, } = useHistoricalPriceDataStore();
6015
6286
  // Track previous tokens and interval to detect changes
6016
6287
  const prevTokensRef = useRef(new Set());
6017
6288
  const prevIntervalRef = useRef(null);
@@ -6064,6 +6335,11 @@ const useHistoricalPriceData = () => {
6064
6335
  const getAllHistoricalPriceData = useCallback(async () => {
6065
6336
  return useHistoricalPriceDataStore.getState().historicalPriceData;
6066
6337
  }, []);
6338
+ const getEffectiveDataBoundary = useCallback((interval) => {
6339
+ const allTokens = getAllTokens();
6340
+ const symbols = allTokens.map(t => t.symbol);
6341
+ return storeGetBoundary(symbols, interval);
6342
+ }, [getAllTokens, storeGetBoundary]);
6067
6343
  const fetchHistoricalPriceData = useCallback(async (startTime, endTime, interval, callback) => {
6068
6344
  const allTokens = getAllTokens();
6069
6345
  if (allTokens.length === 0) {
@@ -6121,6 +6397,7 @@ const useHistoricalPriceData = () => {
6121
6397
  hasHistoricalPriceData,
6122
6398
  getAllHistoricalPriceData,
6123
6399
  getHistoricalPriceData,
6400
+ getEffectiveDataBoundary,
6124
6401
  isLoading,
6125
6402
  clearCache,
6126
6403
  };
@@ -6317,7 +6594,7 @@ const useBasketCandles = () => {
6317
6594
  const longTokens = useUserSelection$1((state) => state.longTokens);
6318
6595
  const shortTokens = useUserSelection$1((state) => state.shortTokens);
6319
6596
  const candleData = useHyperliquidData((s) => s.candleData);
6320
- const { fetchHistoricalPriceData, isLoading: tokenLoading, getAllHistoricalPriceData } = useHistoricalPriceData();
6597
+ const { fetchHistoricalPriceData, isLoading: tokenLoading, getAllHistoricalPriceData, getEffectiveDataBoundary } = useHistoricalPriceData();
6321
6598
  const fetchBasketCandles = useCallback(async (startTime, endTime, interval) => {
6322
6599
  const tokenCandles = await fetchHistoricalPriceData(startTime, endTime, interval);
6323
6600
  const basket = computeBasketCandles(longTokens, shortTokens, tokenCandles);
@@ -6517,6 +6794,7 @@ const useBasketCandles = () => {
6517
6794
  fetchBasketCandles,
6518
6795
  fetchPerformanceCandles,
6519
6796
  fetchOverallPerformanceCandles,
6797
+ getEffectiveDataBoundary,
6520
6798
  isLoading,
6521
6799
  addRealtimeListener,
6522
6800
  removeRealtimeListener,
@@ -6792,11 +7070,13 @@ async function updateLeverage(baseUrl, positionId, payload) {
6792
7070
  }
6793
7071
  }
6794
7072
 
6795
- const calculatePositionAsset = (asset, currentPrice, totalInitialPositionSize, leverage, metadata, isLong = true) => {
7073
+ const calculatePositionAsset = (asset, currentPrice, totalInitialPositionSize, totalCurrentPositionSize, leverage, metadata, isLong = true) => {
6796
7074
  var _a;
6797
7075
  const entryNotional = asset.entryPrice * asset.size;
6798
7076
  const currentNotional = currentPrice * asset.size;
6799
- const marginUsed = currentNotional / (leverage || 1);
7077
+ const effectiveLeverage = leverage || 1;
7078
+ const marginUsed = currentNotional / effectiveLeverage;
7079
+ const entryMarginUsed = entryNotional / effectiveLeverage;
6800
7080
  const unrealizedPnl = isLong
6801
7081
  ? currentNotional - entryNotional
6802
7082
  : entryNotional - currentNotional;
@@ -6806,11 +7086,14 @@ const calculatePositionAsset = (asset, currentPrice, totalInitialPositionSize, l
6806
7086
  actualSize: asset.size,
6807
7087
  leverage: leverage,
6808
7088
  marginUsed: marginUsed,
7089
+ entryMarginUsed: entryMarginUsed,
6809
7090
  positionValue: currentNotional,
6810
7091
  unrealizedPnl: unrealizedPnl,
6811
7092
  entryPositionValue: entryNotional,
6812
7093
  initialWeight: totalInitialPositionSize > 0 ? entryNotional / totalInitialPositionSize : 0,
7094
+ currentWeight: totalCurrentPositionSize > 0 ? currentNotional / totalCurrentPositionSize : 0,
6813
7095
  fundingPaid: (_a = asset.fundingPaid) !== null && _a !== void 0 ? _a : 0,
7096
+ targetWeight: asset.targetWeight,
6814
7097
  metadata,
6815
7098
  };
6816
7099
  };
@@ -6831,20 +7114,32 @@ const buildPositionValue = (rawPositions, clearinghouseState, getAssetByName) =>
6831
7114
  takeProfit: position.takeProfit,
6832
7115
  stopLoss: position.stopLoss,
6833
7116
  };
7117
+ let entryMarginUsed = 0;
6834
7118
  const totalInitialPositionSize = position.longAssets.reduce((acc, asset) => acc + asset.entryPrice * asset.size, 0) +
6835
7119
  position.shortAssets.reduce((acc, asset) => acc + asset.entryPrice * asset.size, 0);
7120
+ const totalCurrentPositionSize = position.longAssets.reduce((acc, asset) => {
7121
+ var _a, _b;
7122
+ const currentPrice = (_b = (_a = getAssetByName(asset.coin)) === null || _a === void 0 ? void 0 : _a.currentPrice) !== null && _b !== void 0 ? _b : 0;
7123
+ return acc + currentPrice * asset.size;
7124
+ }, 0) +
7125
+ position.shortAssets.reduce((acc, asset) => {
7126
+ var _a, _b;
7127
+ const currentPrice = (_b = (_a = getAssetByName(asset.coin)) === null || _a === void 0 ? void 0 : _a.currentPrice) !== null && _b !== void 0 ? _b : 0;
7128
+ return acc + currentPrice * asset.size;
7129
+ }, 0);
6836
7130
  mappedPosition.longAssets = position.longAssets.map((longAsset) => {
6837
7131
  var _a, _b, _c, _d;
6838
7132
  const metadata = getAssetByName(longAsset.coin);
6839
7133
  const currentPrice = (_a = metadata === null || metadata === void 0 ? void 0 : metadata.currentPrice) !== null && _a !== void 0 ? _a : 0;
6840
7134
  const assetState = (_b = clearinghouseState.assetPositions.find((ap) => ap.position.coin === longAsset.coin)) === null || _b === void 0 ? void 0 : _b.position;
6841
7135
  const leverage = (_d = (_c = assetState === null || assetState === void 0 ? void 0 : assetState.leverage) === null || _c === void 0 ? void 0 : _c.value) !== null && _d !== void 0 ? _d : longAsset.leverage;
6842
- const mappedPositionAssets = calculatePositionAsset(longAsset, currentPrice, totalInitialPositionSize, leverage, metadata, true);
7136
+ const mappedPositionAssets = calculatePositionAsset(longAsset, currentPrice, totalInitialPositionSize, totalCurrentPositionSize, leverage, metadata, true);
6843
7137
  mappedPosition.entryPositionValue +=
6844
7138
  mappedPositionAssets.entryPositionValue;
6845
7139
  mappedPosition.unrealizedPnl += mappedPositionAssets.unrealizedPnl;
6846
7140
  mappedPosition.positionValue += mappedPositionAssets.positionValue;
6847
7141
  mappedPosition.marginUsed += mappedPositionAssets.marginUsed;
7142
+ entryMarginUsed += mappedPositionAssets.entryMarginUsed;
6848
7143
  mappedPosition.entryRatio *= Math.pow(longAsset.entryPrice, mappedPositionAssets.initialWeight);
6849
7144
  mappedPosition.markRatio *= Math.pow(currentPrice, mappedPositionAssets.initialWeight);
6850
7145
  return mappedPositionAssets;
@@ -6855,14 +7150,15 @@ const buildPositionValue = (rawPositions, clearinghouseState, getAssetByName) =>
6855
7150
  const currentPrice = (_a = metadata === null || metadata === void 0 ? void 0 : metadata.currentPrice) !== null && _a !== void 0 ? _a : 0;
6856
7151
  const assetState = (_b = clearinghouseState.assetPositions.find((ap) => ap.position.coin === shortAsset.coin)) === null || _b === void 0 ? void 0 : _b.position;
6857
7152
  const leverage = (_d = (_c = assetState === null || assetState === void 0 ? void 0 : assetState.leverage) === null || _c === void 0 ? void 0 : _c.value) !== null && _d !== void 0 ? _d : shortAsset.leverage;
6858
- const mappedPositionAssets = calculatePositionAsset(shortAsset, currentPrice, totalInitialPositionSize, leverage, metadata, false);
7153
+ const mappedPositionAssets = calculatePositionAsset(shortAsset, currentPrice, totalInitialPositionSize, totalCurrentPositionSize, leverage, metadata, false);
6859
7154
  mappedPosition.entryPositionValue +=
6860
7155
  mappedPositionAssets.entryPositionValue;
6861
7156
  mappedPosition.unrealizedPnl += mappedPositionAssets.unrealizedPnl;
6862
7157
  mappedPosition.positionValue += mappedPositionAssets.positionValue;
6863
7158
  mappedPosition.marginUsed += mappedPositionAssets.marginUsed;
7159
+ entryMarginUsed += mappedPositionAssets.entryMarginUsed;
6864
7160
  mappedPosition.entryRatio *= Math.pow(shortAsset.entryPrice, -mappedPositionAssets.initialWeight);
6865
- mappedPosition.markRatio *= Math.pow(currentPrice, -mappedPositionAssets.initialWeight);
7161
+ mappedPosition.markRatio *= Math.pow(currentPrice, -mappedPositionAssets.currentWeight);
6866
7162
  return mappedPositionAssets;
6867
7163
  });
6868
7164
  mappedPosition.positionValue =
@@ -6898,11 +7194,12 @@ const buildPositionValue = (rawPositions, clearinghouseState, getAssetByName) =>
6898
7194
  mappedPosition.markPriceRatio = shortMark;
6899
7195
  }
6900
7196
  mappedPosition.unrealizedPnlPercentage =
6901
- mappedPosition.unrealizedPnl / mappedPosition.marginUsed;
7197
+ entryMarginUsed > 0 ? mappedPosition.unrealizedPnl / entryMarginUsed : 0;
6902
7198
  return mappedPosition;
6903
7199
  });
6904
7200
  };
6905
7201
 
7202
+ const MIN_TRADE_SIZE_USD = 11;
6906
7203
  function usePosition() {
6907
7204
  const context = useContext(PearHyperliquidContext);
6908
7205
  if (!context) {
@@ -6945,6 +7242,85 @@ function usePosition() {
6945
7242
  return null;
6946
7243
  return buildPositionValue(userOpenPositions, aggregatedClearingHouseState, getAssetByName);
6947
7244
  }, [userOpenPositions, aggregatedClearingHouseState, tokenMetadata, getAssetByName]);
7245
+ const planRebalance = (positionId, targetWeights) => {
7246
+ var _a;
7247
+ if (!openPositions) {
7248
+ throw new Error('Open positions not loaded');
7249
+ }
7250
+ const position = openPositions.find((p) => p.positionId === positionId);
7251
+ if (!position) {
7252
+ throw new Error(`Position ${positionId} not found`);
7253
+ }
7254
+ const assets = [];
7255
+ const allAssets = [
7256
+ ...position.longAssets.map((a) => ({ asset: a, side: 'long' })),
7257
+ ...position.shortAssets.map((a) => ({ asset: a, side: 'short' })),
7258
+ ];
7259
+ const totalValue = allAssets.reduce((sum, { asset }) => sum + asset.positionValue, 0);
7260
+ for (const { asset, side } of allAssets) {
7261
+ const tw = (_a = targetWeights === null || targetWeights === void 0 ? void 0 : targetWeights[asset.coin]) !== null && _a !== void 0 ? _a : asset.targetWeight;
7262
+ if (tw === undefined) {
7263
+ assets.push({
7264
+ coin: asset.coin,
7265
+ side,
7266
+ currentWeight: totalValue > 0 ? (asset.positionValue / totalValue) * 100 : 0,
7267
+ targetWeight: 0,
7268
+ currentValue: asset.positionValue,
7269
+ targetValue: asset.positionValue,
7270
+ deltaValue: 0,
7271
+ currentSize: asset.actualSize,
7272
+ newSize: asset.actualSize,
7273
+ deltaSize: 0,
7274
+ skipped: true,
7275
+ skipReason: 'No target weight defined',
7276
+ });
7277
+ continue;
7278
+ }
7279
+ const currentWeight = totalValue > 0 ? (asset.positionValue / totalValue) * 100 : 0;
7280
+ const targetValue = totalValue * (tw / 100);
7281
+ const deltaValue = targetValue - asset.positionValue;
7282
+ const currentPrice = asset.actualSize > 0 ? asset.positionValue / asset.actualSize : 0;
7283
+ const deltaSize = currentPrice > 0 ? deltaValue / currentPrice : 0;
7284
+ const newSize = asset.actualSize + deltaSize;
7285
+ const belowMinimum = Math.abs(deltaValue) > 0 && Math.abs(deltaValue) < MIN_TRADE_SIZE_USD;
7286
+ assets.push({
7287
+ coin: asset.coin,
7288
+ side,
7289
+ currentWeight,
7290
+ targetWeight: tw,
7291
+ currentValue: asset.positionValue,
7292
+ targetValue,
7293
+ deltaValue,
7294
+ currentSize: asset.actualSize,
7295
+ newSize,
7296
+ deltaSize,
7297
+ skipped: belowMinimum,
7298
+ skipReason: belowMinimum
7299
+ ? `Trade size $${Math.abs(deltaValue).toFixed(2)} is below minimum $${MIN_TRADE_SIZE_USD}`
7300
+ : undefined,
7301
+ });
7302
+ }
7303
+ const canExecute = assets.some((a) => !a.skipped && Math.abs(a.deltaValue) > 0);
7304
+ return { positionId, assets, canExecute };
7305
+ };
7306
+ const executeRebalance = async (positionId, targetWeights) => {
7307
+ const plan = planRebalance(positionId, targetWeights);
7308
+ if (!plan.canExecute) {
7309
+ throw new Error('No executable rebalance changes — all below minimum trade size or no changes needed');
7310
+ }
7311
+ const executable = plan.assets.filter((a) => !a.skipped && Math.abs(a.deltaValue) > 0);
7312
+ const payload = [
7313
+ {
7314
+ longAssets: executable
7315
+ .filter((a) => a.side === 'long')
7316
+ .map((a) => ({ asset: a.coin, size: a.newSize })),
7317
+ shortAssets: executable
7318
+ .filter((a) => a.side === 'short')
7319
+ .map((a) => ({ asset: a.coin, size: a.newSize })),
7320
+ },
7321
+ ];
7322
+ return adjustAdvancePosition$1(positionId, payload);
7323
+ };
6948
7324
  return {
6949
7325
  createPosition: createPosition$1,
6950
7326
  updateRiskParameters: updateRiskParameters$1,
@@ -6955,6 +7331,8 @@ function usePosition() {
6955
7331
  updateLeverage: updateLeverage$1,
6956
7332
  openPositions,
6957
7333
  isLoading,
7334
+ planRebalance,
7335
+ executeRebalance,
6958
7336
  };
6959
7337
  }
6960
7338
 
@@ -7388,20 +7766,34 @@ function usePortfolio() {
7388
7766
  }
7389
7767
 
7390
7768
  async function getEIP712Message(baseUrl, address, clientId) {
7391
- const url = joinUrl(baseUrl, '/auth/eip712-message');
7769
+ const url = joinUrl(baseUrl, "/auth/eip712-message");
7392
7770
  try {
7393
- const resp = await axios$1.get(url, { params: { address, clientId }, timeout: 30000 });
7394
- return { data: resp.data, status: resp.status, headers: resp.headers };
7771
+ const resp = await apiClient.get(url, {
7772
+ params: { address, clientId },
7773
+ timeout: 30000,
7774
+ });
7775
+ return {
7776
+ data: resp.data,
7777
+ status: resp.status,
7778
+ headers: resp.headers,
7779
+ };
7395
7780
  }
7396
7781
  catch (error) {
7397
7782
  throw toApiError(error);
7398
7783
  }
7399
7784
  }
7400
7785
  async function authenticate(baseUrl, body) {
7401
- const url = joinUrl(baseUrl, '/auth/login');
7786
+ const url = joinUrl(baseUrl, "/auth/login");
7402
7787
  try {
7403
- const resp = await axios$1.post(url, body, { headers: { 'Content-Type': 'application/json' }, timeout: 30000 });
7404
- return { data: resp.data, status: resp.status, headers: resp.headers };
7788
+ const resp = await apiClient.post(url, body, {
7789
+ headers: { "Content-Type": "application/json" },
7790
+ timeout: 30000,
7791
+ });
7792
+ return {
7793
+ data: resp.data,
7794
+ status: resp.status,
7795
+ headers: resp.headers,
7796
+ };
7405
7797
  }
7406
7798
  catch (error) {
7407
7799
  throw toApiError(error);
@@ -7412,7 +7804,7 @@ async function authenticate(baseUrl, body) {
7412
7804
  */
7413
7805
  async function authenticateWithPrivy(baseUrl, params) {
7414
7806
  const body = {
7415
- method: 'privy_access_token',
7807
+ method: "privy_access_token",
7416
7808
  address: params.address,
7417
7809
  clientId: params.clientId,
7418
7810
  details: { appId: params.appId, accessToken: params.accessToken },
@@ -7420,61 +7812,124 @@ async function authenticateWithPrivy(baseUrl, params) {
7420
7812
  return authenticate(baseUrl, body);
7421
7813
  }
7422
7814
  async function refreshToken(baseUrl, refreshTokenVal) {
7423
- const url = joinUrl(baseUrl, '/auth/refresh');
7815
+ const url = joinUrl(baseUrl, "/auth/refresh");
7424
7816
  try {
7425
- const resp = await axios$1.post(url, { refreshToken: refreshTokenVal }, { headers: { 'Content-Type': 'application/json' }, timeout: 30000 });
7426
- return { data: resp.data, status: resp.status, headers: resp.headers };
7817
+ const resp = await apiClient.post(url, { refreshToken: refreshTokenVal }, { headers: { "Content-Type": "application/json" }, timeout: 30000 });
7818
+ return {
7819
+ data: resp.data,
7820
+ status: resp.status,
7821
+ headers: resp.headers,
7822
+ };
7427
7823
  }
7428
7824
  catch (error) {
7429
7825
  throw toApiError(error);
7430
7826
  }
7431
7827
  }
7432
7828
  async function logout(baseUrl, refreshTokenVal) {
7433
- const url = joinUrl(baseUrl, '/auth/logout');
7829
+ const url = joinUrl(baseUrl, "/auth/logout");
7434
7830
  try {
7435
- const resp = await axios$1.post(url, { refreshToken: refreshTokenVal }, { headers: { 'Content-Type': 'application/json' }, timeout: 30000 });
7436
- return { data: resp.data, status: resp.status, headers: resp.headers };
7831
+ const resp = await apiClient.post(url, { refreshToken: refreshTokenVal }, { headers: { "Content-Type": "application/json" }, timeout: 30000 });
7832
+ return {
7833
+ data: resp.data,
7834
+ status: resp.status,
7835
+ headers: resp.headers,
7836
+ };
7437
7837
  }
7438
7838
  catch (error) {
7439
7839
  throw toApiError(error);
7440
7840
  }
7441
7841
  }
7442
7842
 
7843
+ // Token expiration constants
7844
+ const ACCESS_TOKEN_BUFFER_MS = 5 * 60 * 1000; // Refresh 5 min before expiry
7845
+ const REFRESH_TOKEN_TTL_MS = 7 * 24 * 60 * 60 * 1000; // 7 days fallback
7846
+ function nowMs() {
7847
+ return Date.now();
7848
+ }
7849
+ function calcExpiresAt(expiresInSeconds) {
7850
+ return nowMs() + expiresInSeconds * 1000;
7851
+ }
7443
7852
  function useAuth() {
7444
7853
  const context = useContext(PearHyperliquidContext);
7445
7854
  if (!context) {
7446
- throw new Error("usePortfolio must be used within a PearHyperliquidProvider");
7855
+ throw new Error('useAuth must be used within a PearHyperliquidProvider');
7447
7856
  }
7448
7857
  const { apiBaseUrl, clientId } = context;
7449
- const [isReady, setIsReady] = useState(false);
7450
7858
  const accessToken = useUserData((s) => s.accessToken);
7451
7859
  const refreshToken$1 = useUserData((s) => s.refreshToken);
7860
+ const isReady = useUserData((s) => s.isReady);
7861
+ const isAuthenticated = useUserData((s) => s.isAuthenticated);
7452
7862
  const setAccessToken = useUserData((s) => s.setAccessToken);
7453
7863
  const setRefreshToken = useUserData((s) => s.setRefreshToken);
7454
- const isAuthenticated = useUserData((s) => s.isAuthenticated);
7864
+ const setIsReady = useUserData((s) => s.setIsReady);
7455
7865
  const setIsAuthenticated = useUserData((s) => s.setIsAuthenticated);
7866
+ const address = useUserData((s) => s.address);
7456
7867
  const setAddress = useUserData((s) => s.setAddress);
7868
+ // Ref to prevent concurrent refresh attempts
7869
+ const isRefreshingRef = useRef(false);
7457
7870
  useEffect(() => {
7458
- if (typeof window == "undefined") {
7871
+ if (typeof window == 'undefined') {
7459
7872
  return;
7460
7873
  }
7461
- const access = localStorage.getItem("accessToken");
7462
- const refresh = localStorage.getItem("refreshToken");
7463
- const addr = localStorage.getItem("address");
7464
- setAccessToken(access);
7465
- setRefreshToken(refresh);
7466
- setAddress(addr);
7467
- const authed = Boolean(access && addr);
7468
- setIsAuthenticated(authed);
7469
- setIsReady(true);
7470
- }, [setAccessToken, setRefreshToken, setIsAuthenticated, setAddress]);
7874
+ if (address) {
7875
+ const accessTokenKey = `${address}_accessToken`;
7876
+ const refreshTokenKey = `${address}_refreshToken`;
7877
+ const accessTokenExpiresAtKey = `${address}_accessTokenExpiresAt`;
7878
+ const refreshTokenExpiresAtKey = `${address}_refreshTokenExpiresAt`;
7879
+ const storedAccessToken = localStorage.getItem(accessTokenKey);
7880
+ const storedRefreshToken = localStorage.getItem(refreshTokenKey);
7881
+ const accessExpRaw = localStorage.getItem(accessTokenExpiresAtKey);
7882
+ const refreshExpRaw = localStorage.getItem(refreshTokenExpiresAtKey);
7883
+ const accessExp = accessExpRaw ? Number(accessExpRaw) : 0;
7884
+ const refreshExp = refreshExpRaw ? Number(refreshExpRaw) : 0;
7885
+ const now = nowMs();
7886
+ const accessValid = !!storedAccessToken && accessExp > now;
7887
+ const refreshValid = !!storedRefreshToken && refreshExp > now;
7888
+ if (accessValid && refreshValid) {
7889
+ // Both tokens are valid
7890
+ setAccessToken(storedAccessToken);
7891
+ setRefreshToken(storedRefreshToken);
7892
+ setIsAuthenticated(true);
7893
+ setIsReady(true);
7894
+ }
7895
+ else if (refreshValid) {
7896
+ // Access token expired but refresh still valid → refresh immediately
7897
+ setAccessToken(storedAccessToken);
7898
+ setRefreshToken(storedRefreshToken);
7899
+ (async () => {
7900
+ try {
7901
+ await refreshTokens();
7902
+ }
7903
+ catch (_a) {
7904
+ // Refresh failed → clear tokens
7905
+ setAccessToken(null);
7906
+ setRefreshToken(null);
7907
+ setIsAuthenticated(false);
7908
+ }
7909
+ setIsReady(true);
7910
+ })();
7911
+ return; // setIsReady will be called in the async block
7912
+ }
7913
+ else {
7914
+ // Refresh expired or no tokens → clear
7915
+ setAccessToken(null);
7916
+ setRefreshToken(null);
7917
+ setIsAuthenticated(false);
7918
+ setIsReady(true);
7919
+ }
7920
+ }
7921
+ else {
7922
+ setIsReady(true);
7923
+ }
7924
+ }, [address]);
7471
7925
  useEffect(() => {
7472
7926
  const cleanup = addAuthInterceptors({
7473
7927
  apiBaseUrl,
7474
7928
  getAccessToken: () => {
7475
- return typeof window !== "undefined"
7476
- ? window.localStorage.getItem("accessToken")
7477
- : null;
7929
+ if (typeof window === 'undefined')
7930
+ return null;
7931
+ // Read from Zustand state as single source of truth
7932
+ return useUserData.getState().accessToken;
7478
7933
  },
7479
7934
  refreshTokens: async () => {
7480
7935
  const data = await refreshTokens();
@@ -7488,6 +7943,55 @@ function useAuth() {
7488
7943
  cleanup();
7489
7944
  };
7490
7945
  }, [apiBaseUrl]);
7946
+ // Proactive refresh effect: refresh when app regains focus or timer fires
7947
+ useEffect(() => {
7948
+ if (typeof window === 'undefined' || !address || !refreshToken$1)
7949
+ return;
7950
+ const refreshIfNeeded = async () => {
7951
+ // Prevent concurrent refresh attempts
7952
+ if (isRefreshingRef.current)
7953
+ return;
7954
+ // Read fresh expiration values from localStorage (not stale closure)
7955
+ const accessExpRaw = localStorage.getItem(`${address}_accessTokenExpiresAt`);
7956
+ const refreshExpRaw = localStorage.getItem(`${address}_refreshTokenExpiresAt`);
7957
+ const accessExp = accessExpRaw ? Number(accessExpRaw) : 0;
7958
+ const refreshExp = refreshExpRaw ? Number(refreshExpRaw) : 0;
7959
+ const now = nowMs();
7960
+ // If refresh token is already expired, do nothing
7961
+ if (refreshExp <= now)
7962
+ return;
7963
+ // If access token is within buffer window, refresh
7964
+ if (accessExp - now <= ACCESS_TOKEN_BUFFER_MS) {
7965
+ isRefreshingRef.current = true;
7966
+ try {
7967
+ await refreshTokens();
7968
+ }
7969
+ catch (_a) {
7970
+ // Refresh failed, interceptor will handle logout on next API call
7971
+ }
7972
+ finally {
7973
+ isRefreshingRef.current = false;
7974
+ }
7975
+ }
7976
+ };
7977
+ const onVisibilityChange = () => {
7978
+ if (document.visibilityState === 'visible') {
7979
+ refreshIfNeeded();
7980
+ }
7981
+ };
7982
+ document.addEventListener('visibilitychange', onVisibilityChange);
7983
+ // Schedule timer for (accessExp - buffer)
7984
+ const accessExpRaw = localStorage.getItem(`${address}_accessTokenExpiresAt`);
7985
+ const accessExp = accessExpRaw ? Number(accessExpRaw) : 0;
7986
+ const delay = Math.max(0, accessExp - nowMs() - ACCESS_TOKEN_BUFFER_MS);
7987
+ const timer = window.setTimeout(() => {
7988
+ refreshIfNeeded();
7989
+ }, delay);
7990
+ return () => {
7991
+ document.removeEventListener('visibilitychange', onVisibilityChange);
7992
+ clearTimeout(timer);
7993
+ };
7994
+ }, [address, refreshToken$1]);
7491
7995
  async function getEip712(address) {
7492
7996
  const { data } = await getEIP712Message(apiBaseUrl, address, clientId);
7493
7997
  return data;
@@ -7495,17 +7999,21 @@ function useAuth() {
7495
7999
  async function loginWithSignedMessage(address, signature, timestamp) {
7496
8000
  try {
7497
8001
  const { data } = await authenticate(apiBaseUrl, {
7498
- method: "eip712",
8002
+ method: 'eip712',
7499
8003
  address,
7500
8004
  clientId,
7501
8005
  details: { signature, timestamp },
7502
8006
  });
7503
- window.localStorage.setItem("accessToken", data.accessToken);
7504
- window.localStorage.setItem("refreshToken", data.refreshToken);
7505
- window.localStorage.setItem("address", address);
8007
+ const accessTokenKey = `${address}_accessToken`;
8008
+ const refreshTokenKey = `${address}_refreshToken`;
8009
+ const accessTokenExpiresAtKey = `${address}_accessTokenExpiresAt`;
8010
+ const refreshTokenExpiresAtKey = `${address}_refreshTokenExpiresAt`;
8011
+ window.localStorage.setItem(accessTokenKey, data.accessToken);
8012
+ window.localStorage.setItem(refreshTokenKey, data.refreshToken);
8013
+ window.localStorage.setItem(accessTokenExpiresAtKey, String(calcExpiresAt(data.expiresIn)));
8014
+ window.localStorage.setItem(refreshTokenExpiresAtKey, String(nowMs() + REFRESH_TOKEN_TTL_MS));
7506
8015
  setAccessToken(data.accessToken);
7507
8016
  setRefreshToken(data.refreshToken);
7508
- setAddress(address);
7509
8017
  setIsAuthenticated(true);
7510
8018
  }
7511
8019
  catch (e) {
@@ -7520,12 +8028,16 @@ function useAuth() {
7520
8028
  appId,
7521
8029
  accessToken: privyAccessToken,
7522
8030
  });
7523
- window.localStorage.setItem("accessToken", data.accessToken);
7524
- window.localStorage.setItem("refreshToken", data.refreshToken);
7525
- window.localStorage.setItem("address", address);
8031
+ const accessTokenKey = `${address}_accessToken`;
8032
+ const refreshTokenKey = `${address}_refreshToken`;
8033
+ const accessTokenExpiresAtKey = `${address}_accessTokenExpiresAt`;
8034
+ const refreshTokenExpiresAtKey = `${address}_refreshTokenExpiresAt`;
8035
+ window.localStorage.setItem(accessTokenKey, data.accessToken);
8036
+ window.localStorage.setItem(refreshTokenKey, data.refreshToken);
8037
+ window.localStorage.setItem(accessTokenExpiresAtKey, String(calcExpiresAt(data.expiresIn)));
8038
+ window.localStorage.setItem(refreshTokenExpiresAtKey, String(nowMs() + REFRESH_TOKEN_TTL_MS));
7526
8039
  setAccessToken(data.accessToken);
7527
8040
  setRefreshToken(data.refreshToken);
7528
- setAddress(address);
7529
8041
  setIsAuthenticated(true);
7530
8042
  }
7531
8043
  catch (e) {
@@ -7533,38 +8045,60 @@ function useAuth() {
7533
8045
  }
7534
8046
  }
7535
8047
  async function refreshTokens() {
7536
- const refresh = window.localStorage.getItem("refreshToken");
7537
- if (!refresh)
7538
- throw new Error("No refresh token");
7539
- const { data } = await refreshToken(apiBaseUrl, refresh);
7540
- window.localStorage.setItem("accessToken", data.accessToken);
7541
- window.localStorage.setItem("refreshToken", data.refreshToken);
8048
+ const currentAddress = address;
8049
+ const currentRefresh = refreshToken$1;
8050
+ if (!currentRefresh || !currentAddress)
8051
+ throw new Error('No refresh token');
8052
+ const { data } = await refreshToken(apiBaseUrl, currentRefresh);
8053
+ // Update tokens in localStorage
8054
+ const accessTokenKey = `${currentAddress}_accessToken`;
8055
+ const refreshTokenKey = `${currentAddress}_refreshToken`;
8056
+ const accessTokenExpiresAtKey = `${currentAddress}_accessTokenExpiresAt`;
8057
+ const refreshTokenExpiresAtKey = `${currentAddress}_refreshTokenExpiresAt`;
8058
+ window.localStorage.setItem(accessTokenKey, data.accessToken);
8059
+ window.localStorage.setItem(refreshTokenKey, data.refreshToken);
8060
+ window.localStorage.setItem(accessTokenExpiresAtKey, String(calcExpiresAt(data.expiresIn)));
8061
+ window.localStorage.setItem(refreshTokenExpiresAtKey, String(nowMs() + REFRESH_TOKEN_TTL_MS));
7542
8062
  setAccessToken(data.accessToken);
7543
8063
  setRefreshToken(data.refreshToken);
7544
8064
  setIsAuthenticated(true);
7545
8065
  return data;
7546
8066
  }
7547
8067
  async function logout$1() {
7548
- const refresh = window.localStorage.getItem("refreshToken");
7549
- if (refresh) {
8068
+ const currentAddress = address;
8069
+ const currentRefresh = refreshToken$1;
8070
+ if (currentRefresh) {
7550
8071
  try {
7551
- await logout(apiBaseUrl, refresh);
8072
+ await logout(apiBaseUrl, currentRefresh);
7552
8073
  }
7553
8074
  catch (_a) {
7554
8075
  /* ignore */
7555
8076
  }
7556
8077
  }
7557
- window.localStorage.removeItem("accessToken");
7558
- window.localStorage.removeItem("refreshToken");
7559
- window.localStorage.removeItem("address");
8078
+ if (currentAddress) {
8079
+ const accessTokenKey = `${currentAddress}_accessToken`;
8080
+ const refreshTokenKey = `${currentAddress}_refreshToken`;
8081
+ const accessTokenExpiresAtKey = `${currentAddress}_accessTokenExpiresAt`;
8082
+ const refreshTokenExpiresAtKey = `${currentAddress}_refreshTokenExpiresAt`;
8083
+ window.localStorage.removeItem(accessTokenKey);
8084
+ window.localStorage.removeItem(refreshTokenKey);
8085
+ window.localStorage.removeItem(accessTokenExpiresAtKey);
8086
+ window.localStorage.removeItem(refreshTokenExpiresAtKey);
8087
+ }
7560
8088
  setAccessToken(null);
7561
8089
  setRefreshToken(null);
7562
8090
  setAddress(null);
7563
8091
  setIsAuthenticated(false);
7564
8092
  }
8093
+ function clearSession() {
8094
+ setAccessToken(null);
8095
+ setRefreshToken(null);
8096
+ setIsAuthenticated(false);
8097
+ }
7565
8098
  return {
7566
8099
  isReady,
7567
8100
  isAuthenticated,
8101
+ address,
7568
8102
  accessToken,
7569
8103
  refreshToken: refreshToken$1,
7570
8104
  getEip712,
@@ -7572,6 +8106,8 @@ function useAuth() {
7572
8106
  loginWithPrivyToken,
7573
8107
  refreshTokens,
7574
8108
  logout: logout$1,
8109
+ clearSession,
8110
+ setAddress,
7575
8111
  };
7576
8112
  }
7577
8113
 
@@ -7612,14 +8148,20 @@ const useAllUserBalances = () => {
7612
8148
  }
7613
8149
  }
7614
8150
  if (!availableToTrades['USDC']) {
7615
- availableToTrades['USDC'] = parseFloat((aggregatedClearingHouseState === null || aggregatedClearingHouseState === void 0 ? void 0 : aggregatedClearingHouseState.marginSummary.totalRawUsd) || '0');
8151
+ availableToTrades['USDC'] = Math.max(0, parseFloat((aggregatedClearingHouseState === null || aggregatedClearingHouseState === void 0 ? void 0 : aggregatedClearingHouseState.marginSummary.accountValue) || '0') -
8152
+ parseFloat((aggregatedClearingHouseState === null || aggregatedClearingHouseState === void 0 ? void 0 : aggregatedClearingHouseState.marginSummary.totalMarginUsed) || '0'));
7616
8153
  }
7617
8154
  return {
7618
8155
  spotBalances,
7619
8156
  availableToTrades,
7620
8157
  isLoading,
7621
8158
  };
7622
- }, [spotState, longTokensMetadata, shortTokensMetadata, aggregatedClearingHouseState]);
8159
+ }, [
8160
+ spotState,
8161
+ longTokensMetadata,
8162
+ shortTokensMetadata,
8163
+ aggregatedClearingHouseState,
8164
+ ]);
7623
8165
  /**
7624
8166
  * Calculate margin required for every collateral token based on asset leverages and size.
7625
8167
  * Returns margin required per collateral and whether there's sufficient margin.
@@ -7739,6 +8281,7 @@ const useAllUserBalances = () => {
7739
8281
  availableToTrades,
7740
8282
  ]);
7741
8283
  return {
8284
+ abstractionMode: useUserData((state) => state.userAbstractionMode),
7742
8285
  spotBalances,
7743
8286
  availableToTrades,
7744
8287
  isLoading,
@@ -7879,13 +8422,426 @@ function useHyperliquidUserFills(options) {
7879
8422
  };
7880
8423
  }
7881
8424
 
8425
+ async function getTradeHistory(baseUrl, params) {
8426
+ const url = joinUrl(baseUrl, '/trade-history');
8427
+ try {
8428
+ const resp = await apiClient.get(url, {
8429
+ params,
8430
+ timeout: 60000,
8431
+ });
8432
+ return { data: resp.data, status: resp.status, headers: resp.headers };
8433
+ }
8434
+ catch (error) {
8435
+ throw toApiError(error);
8436
+ }
8437
+ }
8438
+
8439
+ // ─── helpers ────────────────────────────────────────────────────
8440
+ const EMPTY_SUMMARY = {
8441
+ pnl: 0,
8442
+ volume: 0,
8443
+ winRate: 0,
8444
+ wins: 0,
8445
+ losses: 0,
8446
+ totalProfit: 0,
8447
+ totalLoss: 0,
8448
+ };
8449
+ const getTimeframeDays$1 = (tf) => {
8450
+ switch (tf) {
8451
+ case '2W':
8452
+ return 14;
8453
+ case '3W':
8454
+ return 21;
8455
+ case '2M':
8456
+ return 60;
8457
+ case '3M':
8458
+ return 90;
8459
+ }
8460
+ };
8461
+ const isWeekTimeframe = (tf) => tf === '2W' || tf === '3W';
8462
+ const toDateKey = (date) => {
8463
+ const y = date.getFullYear();
8464
+ const m = String(date.getMonth() + 1).padStart(2, '0');
8465
+ const d = String(date.getDate()).padStart(2, '0');
8466
+ return `${y}-${m}-${d}`;
8467
+ };
8468
+ const toMonthKey = (date) => {
8469
+ const y = date.getFullYear();
8470
+ const m = String(date.getMonth() + 1).padStart(2, '0');
8471
+ return `${y}-${m}`;
8472
+ };
8473
+ const formatMonthLabel = (date) => date.toLocaleDateString('en-US', { month: 'short', year: 'numeric' });
8474
+ const getMonday = (date) => {
8475
+ const d = new Date(date);
8476
+ const day = d.getDay(); // 0=Sun … 6=Sat
8477
+ const diff = day === 0 ? -6 : 1 - day;
8478
+ d.setDate(d.getDate() + diff);
8479
+ d.setHours(0, 0, 0, 0);
8480
+ return d;
8481
+ };
8482
+ const toLocalMidnight = (input) => {
8483
+ const d = typeof input === 'string' ? new Date(input + 'T00:00:00') : new Date(input);
8484
+ d.setHours(0, 0, 0, 0);
8485
+ return d;
8486
+ };
8487
+ const diffDays = (start, end) => {
8488
+ return Math.round((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24)) + 1;
8489
+ };
8490
+ const toISODateString$1 = (input) => {
8491
+ const d = typeof input === 'string' ? new Date(input + 'T00:00:00') : new Date(input);
8492
+ return d.toISOString();
8493
+ };
8494
+ const round2 = (n) => Math.round(n * 100) / 100;
8495
+ const buildSummary = (days) => {
8496
+ let pnl = 0;
8497
+ let volume = 0;
8498
+ let wins = 0;
8499
+ let losses = 0;
8500
+ let totalProfit = 0;
8501
+ let totalLoss = 0;
8502
+ for (const day of days) {
8503
+ pnl += day.totalPnl;
8504
+ volume += day.volume;
8505
+ if (day.positionsClosed === 0)
8506
+ continue;
8507
+ if (day.totalPnl > 0) {
8508
+ wins++;
8509
+ totalProfit += day.totalPnl;
8510
+ }
8511
+ else if (day.totalPnl < 0) {
8512
+ losses++;
8513
+ totalLoss += Math.abs(day.totalPnl);
8514
+ }
8515
+ }
8516
+ const total = wins + losses;
8517
+ const winRate = total > 0 ? Math.round((wins / total) * 100) : 0;
8518
+ return {
8519
+ pnl: round2(pnl),
8520
+ volume: round2(volume),
8521
+ winRate,
8522
+ wins,
8523
+ losses,
8524
+ totalProfit: round2(totalProfit),
8525
+ totalLoss: round2(totalLoss),
8526
+ };
8527
+ };
8528
+ const buildCalendarData = (tradeHistories, timeframe, rangeStart, rangeEnd, totalDays, useCustomDates, getAssetByName) => {
8529
+ const startKey = toDateKey(rangeStart);
8530
+ const endKey = toDateKey(rangeEnd);
8531
+ // Build day buckets for the full range
8532
+ const buckets = new Map();
8533
+ for (let i = 0; i < totalDays; i++) {
8534
+ const d = new Date(rangeStart);
8535
+ d.setDate(rangeStart.getDate() + i);
8536
+ buckets.set(toDateKey(d), {
8537
+ pnl: 0,
8538
+ volume: 0,
8539
+ positionsClosed: 0,
8540
+ trades: [],
8541
+ });
8542
+ }
8543
+ // Populate buckets from trade histories
8544
+ for (const trade of tradeHistories) {
8545
+ if (!trade.createdAt)
8546
+ continue;
8547
+ const date = new Date(trade.createdAt);
8548
+ if (isNaN(date.getTime()))
8549
+ continue;
8550
+ const dateKey = toDateKey(date);
8551
+ if (dateKey < startKey || dateKey > endKey)
8552
+ continue;
8553
+ const bucket = buckets.get(dateKey);
8554
+ if (!bucket)
8555
+ continue;
8556
+ const pnl = trade.realizedPnl;
8557
+ bucket.pnl += isFinite(pnl) ? pnl : 0;
8558
+ const vol = trade.totalValue;
8559
+ bucket.volume += isFinite(vol) ? vol : 0;
8560
+ bucket.positionsClosed += 1;
8561
+ const tradePnl = trade.realizedPnl;
8562
+ bucket.trades.push({
8563
+ tradeHistoryId: trade.tradeHistoryId,
8564
+ realizedPnl: tradePnl,
8565
+ result: tradePnl > 0 ? 'profit' : tradePnl < 0 ? 'loss' : 'breakeven',
8566
+ closedLongAssets: trade.closedLongAssets.map((a) => { var _a; return ({ coin: a.coin, metadata: (_a = getAssetByName(a.coin)) !== null && _a !== void 0 ? _a : null }); }),
8567
+ closedShortAssets: trade.closedShortAssets.map((a) => { var _a; return ({ coin: a.coin, metadata: (_a = getAssetByName(a.coin)) !== null && _a !== void 0 ? _a : null }); }),
8568
+ });
8569
+ }
8570
+ // Build day objects
8571
+ const allDays = [];
8572
+ const sortedKeys = Array.from(buckets.keys()).sort();
8573
+ for (const key of sortedKeys) {
8574
+ const bucket = buckets.get(key);
8575
+ const roundedPnl = round2(bucket.pnl);
8576
+ const result = roundedPnl > 0 ? 'profit' : roundedPnl < 0 ? 'loss' : 'breakeven';
8577
+ allDays.push({
8578
+ date: key,
8579
+ totalPnl: roundedPnl,
8580
+ volume: round2(bucket.volume),
8581
+ positionsClosed: bucket.positionsClosed,
8582
+ result,
8583
+ trades: bucket.trades,
8584
+ });
8585
+ }
8586
+ // Group into periods
8587
+ let weeks = [];
8588
+ let months = [];
8589
+ const useWeekGrouping = useCustomDates
8590
+ ? totalDays <= 28
8591
+ : isWeekTimeframe(timeframe);
8592
+ if (useWeekGrouping) {
8593
+ const weekMap = new Map();
8594
+ for (const day of allDays) {
8595
+ const date = new Date(day.date + 'T00:00:00');
8596
+ const monday = getMonday(date);
8597
+ const mondayKey = toDateKey(monday);
8598
+ if (!weekMap.has(mondayKey)) {
8599
+ weekMap.set(mondayKey, []);
8600
+ }
8601
+ weekMap.get(mondayKey).push(day);
8602
+ }
8603
+ const sortedWeekKeys = Array.from(weekMap.keys()).sort();
8604
+ weeks = sortedWeekKeys.map((mondayKey) => {
8605
+ const days = weekMap.get(mondayKey);
8606
+ const monday = new Date(mondayKey + 'T00:00:00');
8607
+ const sunday = new Date(monday);
8608
+ sunday.setDate(monday.getDate() + 6);
8609
+ return {
8610
+ weekStart: mondayKey,
8611
+ weekEnd: toDateKey(sunday),
8612
+ days,
8613
+ summary: buildSummary(days),
8614
+ };
8615
+ });
8616
+ }
8617
+ else {
8618
+ const monthMap = new Map();
8619
+ for (const day of allDays) {
8620
+ const date = new Date(day.date + 'T00:00:00');
8621
+ const mk = toMonthKey(date);
8622
+ if (!monthMap.has(mk)) {
8623
+ monthMap.set(mk, { days: [], label: formatMonthLabel(date) });
8624
+ }
8625
+ monthMap.get(mk).days.push(day);
8626
+ }
8627
+ const sortedMonthKeys = Array.from(monthMap.keys()).sort();
8628
+ months = sortedMonthKeys.map((mk) => {
8629
+ const { days, label } = monthMap.get(mk);
8630
+ return {
8631
+ month: mk,
8632
+ label,
8633
+ days,
8634
+ summary: buildSummary(days),
8635
+ };
8636
+ });
8637
+ }
8638
+ return {
8639
+ timeframe,
8640
+ weeks,
8641
+ months,
8642
+ overall: buildSummary(allDays),
8643
+ isLoading: false,
8644
+ };
8645
+ };
8646
+ // ─── hook ───────────────────────────────────────────────────────
8647
+ function usePnlCalendar(options) {
8648
+ var _a;
8649
+ const opts = typeof options === 'string'
8650
+ ? { timeframe: options }
8651
+ : options !== null && options !== void 0 ? options : {};
8652
+ const timeframe = (_a = opts.timeframe) !== null && _a !== void 0 ? _a : '2W';
8653
+ const customStart = opts.startDate;
8654
+ const customEnd = opts.endDate;
8655
+ const context = useContext(PearHyperliquidContext);
8656
+ if (!context) {
8657
+ throw new Error('usePnlCalendar must be used within a PearHyperliquidProvider');
8658
+ }
8659
+ const { apiBaseUrl } = context;
8660
+ const isAuthenticated = useUserData((state) => state.isAuthenticated);
8661
+ const { getAssetByName } = useMarket();
8662
+ const [trades, setTrades] = useState(null);
8663
+ const [isLoading, setIsLoading] = useState(false);
8664
+ const [error, setError] = useState(null);
8665
+ const mountedRef = useRef(true);
8666
+ useEffect(() => {
8667
+ mountedRef.current = true;
8668
+ return () => { mountedRef.current = false; };
8669
+ }, []);
8670
+ // Compute the date range
8671
+ const useCustomDates = !!(customStart && customEnd);
8672
+ let rangeStart;
8673
+ let rangeEnd;
8674
+ let totalDays;
8675
+ if (useCustomDates) {
8676
+ rangeStart = toLocalMidnight(customStart);
8677
+ rangeEnd = toLocalMidnight(customEnd);
8678
+ totalDays = diffDays(rangeStart, rangeEnd);
8679
+ }
8680
+ else {
8681
+ totalDays = getTimeframeDays$1(timeframe);
8682
+ rangeEnd = new Date();
8683
+ rangeEnd.setHours(0, 0, 0, 0);
8684
+ rangeStart = new Date(rangeEnd);
8685
+ rangeStart.setDate(rangeEnd.getDate() - totalDays + 1);
8686
+ }
8687
+ const startIso = toISODateString$1(rangeStart);
8688
+ const endIso = toISODateString$1(rangeEnd);
8689
+ const fetchData = useCallback(async () => {
8690
+ if (!isAuthenticated)
8691
+ return;
8692
+ setIsLoading(true);
8693
+ setError(null);
8694
+ try {
8695
+ const response = await getTradeHistory(apiBaseUrl, {
8696
+ startDate: startIso,
8697
+ endDate: endIso,
8698
+ limit: totalDays * 50,
8699
+ });
8700
+ if (!mountedRef.current)
8701
+ return;
8702
+ setTrades(response.data);
8703
+ }
8704
+ catch (err) {
8705
+ if (!mountedRef.current)
8706
+ return;
8707
+ setError(err instanceof Error ? err.message : 'Failed to fetch trade history');
8708
+ setTrades(null);
8709
+ }
8710
+ finally {
8711
+ if (mountedRef.current)
8712
+ setIsLoading(false);
8713
+ }
8714
+ }, [apiBaseUrl, isAuthenticated, startIso, endIso, totalDays]);
8715
+ useEffect(() => {
8716
+ fetchData();
8717
+ }, [fetchData]);
8718
+ const result = useMemo(() => {
8719
+ const empty = {
8720
+ timeframe,
8721
+ weeks: [],
8722
+ months: [],
8723
+ overall: EMPTY_SUMMARY,
8724
+ isLoading: true,
8725
+ };
8726
+ if (!trades)
8727
+ return empty;
8728
+ if (totalDays <= 0)
8729
+ return empty;
8730
+ return buildCalendarData(trades, timeframe, rangeStart, rangeEnd, totalDays, useCustomDates, getAssetByName);
8731
+ }, [trades, timeframe, startIso, endIso, getAssetByName]);
8732
+ return { ...result, isLoading, error, refetch: fetchData };
8733
+ }
8734
+
8735
+ const HEATMAP_LIMIT = 50;
8736
+ // ─── helpers ────────────────────────────────────────────────────
8737
+ const getTimeframeDays = (tf) => {
8738
+ switch (tf) {
8739
+ case '7D':
8740
+ return 7;
8741
+ case '30D':
8742
+ return 30;
8743
+ case '100D':
8744
+ return 100;
8745
+ case 'allTime':
8746
+ return null;
8747
+ }
8748
+ };
8749
+ const toISODateString = (date) => date.toISOString();
8750
+ const toCalendarTrade = (trade, getAssetByName) => {
8751
+ const pnl = trade.realizedPnl;
8752
+ return {
8753
+ tradeHistoryId: trade.tradeHistoryId,
8754
+ realizedPnl: pnl,
8755
+ result: pnl > 0 ? 'profit' : pnl < 0 ? 'loss' : 'breakeven',
8756
+ closedLongAssets: trade.closedLongAssets.map((a) => { var _a; return ({ coin: a.coin, metadata: (_a = getAssetByName(a.coin)) !== null && _a !== void 0 ? _a : null }); }),
8757
+ closedShortAssets: trade.closedShortAssets.map((a) => { var _a; return ({ coin: a.coin, metadata: (_a = getAssetByName(a.coin)) !== null && _a !== void 0 ? _a : null }); }),
8758
+ };
8759
+ };
8760
+ // ─── hook ───────────────────────────────────────────────────────
8761
+ function usePnlHeatmap(timeframe = 'allTime') {
8762
+ const context = useContext(PearHyperliquidContext);
8763
+ if (!context) {
8764
+ throw new Error('usePnlHeatmap must be used within a PearHyperliquidProvider');
8765
+ }
8766
+ const { apiBaseUrl } = context;
8767
+ const isAuthenticated = useUserData((state) => state.isAuthenticated);
8768
+ const { getAssetByName } = useMarket();
8769
+ const [trades, setTrades] = useState(null);
8770
+ const [isLoading, setIsLoading] = useState(false);
8771
+ const [error, setError] = useState(null);
8772
+ const mountedRef = useRef(true);
8773
+ useEffect(() => {
8774
+ mountedRef.current = true;
8775
+ return () => { mountedRef.current = false; };
8776
+ }, []);
8777
+ const days = getTimeframeDays(timeframe);
8778
+ let startIso;
8779
+ if (days !== null) {
8780
+ const start = new Date();
8781
+ start.setHours(0, 0, 0, 0);
8782
+ start.setDate(start.getDate() - days);
8783
+ startIso = toISODateString(start);
8784
+ }
8785
+ const fetchData = useCallback(async () => {
8786
+ if (!isAuthenticated)
8787
+ return;
8788
+ setIsLoading(true);
8789
+ setError(null);
8790
+ try {
8791
+ const response = await getTradeHistory(apiBaseUrl, {
8792
+ ...(startIso ? { startDate: startIso } : {}),
8793
+ limit: 5000,
8794
+ });
8795
+ if (!mountedRef.current)
8796
+ return;
8797
+ setTrades(response.data);
8798
+ }
8799
+ catch (err) {
8800
+ if (!mountedRef.current)
8801
+ return;
8802
+ setError(err instanceof Error ? err.message : 'Failed to fetch trade history');
8803
+ setTrades(null);
8804
+ }
8805
+ finally {
8806
+ if (mountedRef.current)
8807
+ setIsLoading(false);
8808
+ }
8809
+ }, [apiBaseUrl, isAuthenticated, startIso]);
8810
+ useEffect(() => {
8811
+ fetchData();
8812
+ }, [fetchData]);
8813
+ const result = useMemo(() => {
8814
+ if (!trades)
8815
+ return [];
8816
+ const top = trades
8817
+ .slice()
8818
+ .sort((a, b) => Math.abs(b.realizedPnl) - Math.abs(a.realizedPnl))
8819
+ .slice(0, HEATMAP_LIMIT);
8820
+ const totalAbsPnl = top.reduce((sum, t) => sum + Math.abs(t.realizedPnl), 0);
8821
+ return top.map((t) => ({
8822
+ ...toCalendarTrade(t, getAssetByName),
8823
+ percentage: totalAbsPnl > 0
8824
+ ? Math.round((Math.abs(t.realizedPnl) / totalAbsPnl) * 10000) / 100
8825
+ : 0,
8826
+ }));
8827
+ }, [trades, getAssetByName]);
8828
+ return { timeframe, trades: result, isLoading, error, refetch: fetchData };
8829
+ }
8830
+
7882
8831
  const PearHyperliquidContext = createContext(undefined);
7883
8832
  /**
7884
8833
  * React Provider for PearHyperliquidClient
7885
8834
  */
7886
- const PearHyperliquidProvider = ({ children, apiBaseUrl = 'https://hl-ui.pearprotocol.io', clientId = 'PEARPROTOCOLUI', wsUrl = 'wss://hl-ui.pearprotocol.io/ws', }) => {
8835
+ const PearHyperliquidProvider = ({ children, apiBaseUrl = "https://hl-ui.pearprotocol.io", clientId = "PEARPROTOCOLUI", wsUrl = "wss://hl-ui.pearprotocol.io/ws", }) => {
7887
8836
  const address = useUserData((s) => s.address);
7888
- const setAddress = useUserData((s) => s.setAddress);
8837
+ const clearHyperliquidUserData = useHyperliquidData((state) => state.clearUserData);
8838
+ const prevAddressRef = useRef(null);
8839
+ useEffect(() => {
8840
+ if (prevAddressRef.current !== null && prevAddressRef.current !== address) {
8841
+ clearHyperliquidUserData();
8842
+ }
8843
+ prevAddressRef.current = address;
8844
+ }, [address, clearHyperliquidUserData]);
7889
8845
  const perpMetasByDex = useHyperliquidData((state) => state.perpMetasByDex);
7890
8846
  const setPerpDexs = useHyperliquidData((state) => state.setPerpDexs);
7891
8847
  const setPerpMetasByDex = useHyperliquidData((state) => state.setPerpMetasByDex);
@@ -7916,20 +8872,20 @@ const PearHyperliquidProvider = ({ children, apiBaseUrl = 'https://hl-ui.pearpro
7916
8872
  perpMetas.forEach((item, perpIndex) => {
7917
8873
  var _a, _b;
7918
8874
  const dexName = perpIndex === 0
7919
- ? 'HYPERLIQUID'
8875
+ ? "HYPERLIQUID"
7920
8876
  : ((_b = (_a = perpDexs[perpIndex]) === null || _a === void 0 ? void 0 : _a.name) !== null && _b !== void 0 ? _b : `DEX_${perpIndex}`);
7921
8877
  var collateralToken;
7922
8878
  if (item.collateralToken === 360) {
7923
- collateralToken = 'USDH';
8879
+ collateralToken = "USDH";
7924
8880
  }
7925
8881
  if (item.collateralToken === 0) {
7926
- collateralToken = 'USDC';
8882
+ collateralToken = "USDC";
7927
8883
  }
7928
8884
  if (item.collateralToken === 235) {
7929
- collateralToken = 'USDE';
8885
+ collateralToken = "USDE";
7930
8886
  }
7931
8887
  if (item.collateralToken === 268) {
7932
- collateralToken = 'USDT0';
8888
+ collateralToken = "USDT0";
7933
8889
  }
7934
8890
  const universeAssets = item.universe.map((asset) => ({
7935
8891
  ...asset,
@@ -7957,8 +8913,6 @@ const PearHyperliquidProvider = ({ children, apiBaseUrl = 'https://hl-ui.pearpro
7957
8913
  }), [
7958
8914
  apiBaseUrl,
7959
8915
  wsUrl,
7960
- address,
7961
- setAddress,
7962
8916
  isConnected,
7963
8917
  lastError,
7964
8918
  nativeIsConnected,
@@ -7973,7 +8927,7 @@ const PearHyperliquidProvider = ({ children, apiBaseUrl = 'https://hl-ui.pearpro
7973
8927
  function usePearHyperliquid() {
7974
8928
  const ctx = useContext(PearHyperliquidContext);
7975
8929
  if (!ctx)
7976
- throw new Error('usePearHyperliquid must be used within a PearHyperliquidProvider');
8930
+ throw new Error("usePearHyperliquid must be used within a PearHyperliquidProvider");
7977
8931
  return ctx;
7978
8932
  }
7979
8933
 
@@ -8225,4 +9179,4 @@ function getOrderTrailingInfo(order) {
8225
9179
  return undefined;
8226
9180
  }
8227
9181
 
8228
- export { ConflictDetector, MAX_ASSETS_PER_LEG, MINIMUM_ASSET_USD_VALUE, MaxAssetsPerLegError, MinimumPositionSizeError, PearHyperliquidProvider, adjustAdvancePosition, adjustOrder, adjustPosition, calculateMinimumPositionValue, calculateWeightedRatio, cancelOrder, cancelTwap, cancelTwapOrder, closeAllPositions, closePosition, computeBasketCandles, createCandleLookups, createPosition, executeSpotOrder, getCompleteTimestamps, getKalshiMarkets, getOrderDirection, getOrderLadderConfig, getOrderLeverage, getOrderReduceOnly, getOrderTpSlTriggerType, getOrderTrailingInfo, getOrderTriggerType, getOrderTriggerValue, getOrderTwapDuration, getOrderUsdValue, getPortfolio, isBtcDomOrder, mapCandleIntervalToTradingViewInterval, mapTradingViewIntervalToCandleInterval, markNotificationReadById, markNotificationsRead, toggleWatchlist, updateLeverage, updateRiskParameters, useAccountSummary, useAgentWallet, useAllUserBalances, useAuth, useBasketCandles, useHistoricalPriceData, useHistoricalPriceDataStore, useHyperliquidUserFills, useMarket, useMarketData, useMarketDataHook, useNotifications, useOpenOrders, useOrders, usePearHyperliquid, usePerformanceOverlays, usePortfolio, usePosition, useSpotOrder, useTokenSelectionMetadata, useTradeHistories, useTwap, useUserSelection, useWatchlist, validateMaxAssetsPerLeg, validateMinimumAssetSize, validatePositionSize };
9182
+ export { ConflictDetector, MAX_ASSETS_PER_LEG, MINIMUM_ASSET_USD_VALUE, MaxAssetsPerLegError, MinimumPositionSizeError, PearHyperliquidProvider, adjustAdvancePosition, adjustOrder, adjustPosition, calculateMinimumPositionValue, calculateWeightedRatio, cancelOrder, cancelTwap, cancelTwapOrder, closeAllPositions, closePosition, computeBasketCandles, createCandleLookups, createPosition, executeSpotOrder, getCompleteTimestamps, getKalshiMarkets, getOrderDirection, getOrderLadderConfig, getOrderLeverage, getOrderReduceOnly, getOrderTpSlTriggerType, getOrderTrailingInfo, getOrderTriggerType, getOrderTriggerValue, getOrderTwapDuration, getOrderUsdValue, getPortfolio, isBtcDomOrder, mapCandleIntervalToTradingViewInterval, mapTradingViewIntervalToCandleInterval, markNotificationReadById, markNotificationsRead, toggleWatchlist, updateLeverage, updateRiskParameters, useAccountSummary, useAgentWallet, useAllUserBalances, useAuth, useBasketCandles, useHistoricalPriceData, useHistoricalPriceDataStore, useHyperliquidUserFills, useMarket, useMarketData, useMarketDataHook, useNotifications, useOpenOrders, useOrders, usePearHyperliquid, usePerformanceOverlays, usePnlCalendar, usePnlHeatmap, usePortfolio, usePosition, useSpotOrder, useTokenSelectionMetadata, useTradeHistories, useTwap, useUserSelection, useWatchlist, validateMaxAssetsPerLeg, validateMinimumAssetSize, validatePositionSize };