@pear-protocol/hyperliquid-sdk 0.1.4 → 0.1.5-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");
1230
+ return;
1231
+ }
1232
+ if (!isConnected) {
1233
+ console.log("[HyperLiquid WS] Not connected, skipping subscription update");
1122
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(() => {
@@ -1470,17 +1623,33 @@ const useTradeHistories = () => {
1470
1623
  const enrichedTradeHistories = useMemo(() => {
1471
1624
  if (!tradeHistories)
1472
1625
  return null;
1473
- return tradeHistories.map((history) => ({
1474
- ...history,
1475
- closedLongAssets: history.closedLongAssets.map((asset) => ({
1476
- ...asset,
1477
- metadata: getAssetByName(asset.coin),
1478
- })),
1479
- closedShortAssets: history.closedShortAssets.map((asset) => ({
1480
- ...asset,
1481
- metadata: getAssetByName(asset.coin),
1482
- })),
1483
- }));
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
+ });
1484
1653
  }, [tradeHistories, getAssetByName]);
1485
1654
  const isLoading = useMemo(() => {
1486
1655
  return tradeHistories === null && context.isConnected;
@@ -1561,7 +1730,7 @@ const useTokenSelectionMetadataStore = create((set) => ({
1561
1730
  maxLeverage: 0,
1562
1731
  minSize: {},
1563
1732
  leverageMatched: true,
1564
- recompute: ({ tokenMetadata, marketData, longTokens, shortTokens, }) => {
1733
+ recompute: ({ tokenMetadata, marketData, longTokens, shortTokens }) => {
1565
1734
  const isPriceDataReady = Object.keys(tokenMetadata).length > 0;
1566
1735
  const longSymbols = longTokens.map((t) => t.symbol);
1567
1736
  const shortSymbols = shortTokens.map((t) => t.symbol);
@@ -1692,10 +1861,9 @@ const useTokenSelectionMetadataStore = create((set) => ({
1692
1861
  return 0;
1693
1862
  let maxLev = 0;
1694
1863
  allSymbols.forEach((symbol) => {
1695
- var _a;
1696
1864
  const tokenDetail = tokenMetadata[symbol];
1697
- if ((_a = tokenDetail === null || tokenDetail === void 0 ? void 0 : tokenDetail.leverage) === null || _a === void 0 ? void 0 : _a.value) {
1698
- 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);
1699
1867
  }
1700
1868
  });
1701
1869
  return maxLev;
@@ -1819,6 +1987,22 @@ const getIntervalSeconds = (interval) => {
1819
1987
  default: return 60;
1820
1988
  }
1821
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
+ };
1822
2006
  const useHistoricalPriceDataStore = create((set, get) => ({
1823
2007
  historicalPriceData: {},
1824
2008
  loadingTokens: new Set(),
@@ -1829,6 +2013,8 @@ const useHistoricalPriceDataStore = create((set, get) => ({
1829
2013
  if (!existing) {
1830
2014
  // Create new entry
1831
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;
1832
2018
  return {
1833
2019
  historicalPriceData: {
1834
2020
  ...state.historicalPriceData,
@@ -1837,7 +2023,9 @@ const useHistoricalPriceDataStore = create((set, get) => ({
1837
2023
  interval,
1838
2024
  candles: sortedCandles,
1839
2025
  oldestTime: sortedCandles.length > 0 ? sortedCandles[0].t : null,
1840
- 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
1841
2029
  }
1842
2030
  }
1843
2031
  };
@@ -1849,6 +2037,16 @@ const useHistoricalPriceDataStore = create((set, get) => ({
1849
2037
  // Update time pointers
1850
2038
  const oldestTime = mergedCandles.length > 0 ? mergedCandles[0].t : null;
1851
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
+ }
1852
2050
  return {
1853
2051
  historicalPriceData: {
1854
2052
  ...state.historicalPriceData,
@@ -1856,7 +2054,9 @@ const useHistoricalPriceDataStore = create((set, get) => ({
1856
2054
  ...existing,
1857
2055
  candles: mergedCandles,
1858
2056
  oldestTime,
1859
- latestTime
2057
+ latestTime,
2058
+ requestedRanges: mergedRanges,
2059
+ noDataBefore
1860
2060
  }
1861
2061
  }
1862
2062
  };
@@ -1866,8 +2066,24 @@ const useHistoricalPriceDataStore = create((set, get) => ({
1866
2066
  const { historicalPriceData } = get();
1867
2067
  const key = createKey(symbol, interval);
1868
2068
  const tokenData = historicalPriceData[key];
1869
- if (!tokenData || tokenData.oldestTime === null || tokenData.latestTime === null)
2069
+ if (!tokenData)
1870
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) {
2085
+ return false;
2086
+ }
1871
2087
  const intervalMilisecond = getIntervalSeconds(interval) * 1000;
1872
2088
  const hasStartCoverage = tokenData.oldestTime <= startTime;
1873
2089
  const hasEndCoverage = tokenData.latestTime >= endTime ||
@@ -1882,6 +2098,27 @@ const useHistoricalPriceDataStore = create((set, get) => ({
1882
2098
  return [];
1883
2099
  return tokenData.candles.filter(candle => candle.t >= startTime && candle.t < endTime);
1884
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
+ },
1885
2122
  setTokenLoading: (symbol, loading) => {
1886
2123
  set(state => {
1887
2124
  const newLoadingTokens = new Set(state.loadingTokens);
@@ -5762,10 +5999,10 @@ function toApiError(error) {
5762
5999
  var _a;
5763
6000
  const axiosError = error;
5764
6001
  const payload = (axiosError && axiosError.response ? axiosError.response.data : undefined);
5765
- const message = typeof payload === 'object' && payload && 'message' in payload
6002
+ const message = typeof payload === "object" && payload && "message" in payload
5766
6003
  ? String(payload.message)
5767
- : (axiosError === null || axiosError === void 0 ? void 0 : axiosError.message) || 'Request failed';
5768
- 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
5769
6006
  ? String(payload.error)
5770
6007
  : undefined;
5771
6008
  return {
@@ -5775,8 +6012,8 @@ function toApiError(error) {
5775
6012
  };
5776
6013
  }
5777
6014
  function joinUrl(baseUrl, path) {
5778
- const cleanBase = baseUrl.replace(/\/$/, '');
5779
- const cleanPath = path.startsWith('/') ? path : `/${path}`;
6015
+ const cleanBase = baseUrl.replace(/\/$/, "");
6016
+ const cleanPath = path.startsWith("/") ? path : `/${path}`;
5780
6017
  return `${cleanBase}${cleanPath}`;
5781
6018
  }
5782
6019
  /**
@@ -5805,7 +6042,7 @@ function addAuthInterceptors(params) {
5805
6042
  pendingRequests = [];
5806
6043
  }
5807
6044
  const isOurApiUrl = (url) => Boolean(url && url.startsWith(apiBaseUrl));
5808
- const isRefreshUrl = (url) => Boolean(url && url.startsWith(joinUrl(apiBaseUrl, '/auth/refresh')));
6045
+ const isRefreshUrl = (url) => Boolean(url && url.startsWith(joinUrl(apiBaseUrl, "/auth/refresh")));
5809
6046
  const reqId = apiClient.interceptors.request.use((config) => {
5810
6047
  var _a;
5811
6048
  try {
@@ -5813,11 +6050,12 @@ function addAuthInterceptors(params) {
5813
6050
  const token = getAccessToken();
5814
6051
  if (token) {
5815
6052
  config.headers = (_a = config.headers) !== null && _a !== void 0 ? _a : {};
5816
- (config.headers)['Authorization'] = `Bearer ${token}`;
6053
+ config.headers["Authorization"] = `Bearer ${token}`;
5817
6054
  }
5818
6055
  }
5819
6056
  }
5820
- catch (_b) {
6057
+ catch (err) {
6058
+ console.error("[Auth Interceptor] Request interceptor error:", err);
5821
6059
  }
5822
6060
  return config;
5823
6061
  });
@@ -5828,22 +6066,36 @@ function addAuthInterceptors(params) {
5828
6066
  const url = originalRequest === null || originalRequest === void 0 ? void 0 : originalRequest.url;
5829
6067
  // If not our API or not 401, just reject
5830
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
+ }
5831
6076
  return Promise.reject(error);
5832
6077
  }
6078
+ console.log("[Auth Interceptor] 401 detected, attempting token refresh for URL:", url);
5833
6079
  // If the 401 is from refresh endpoint itself -> force logout
5834
6080
  if (isRefreshUrl(url)) {
6081
+ console.warn("[Auth Interceptor] Refresh endpoint returned 401, logging out");
5835
6082
  try {
5836
6083
  await logout();
5837
6084
  }
5838
- catch (_d) { }
6085
+ catch (err) {
6086
+ console.error("[Auth Interceptor] Logout failed:", err);
6087
+ }
5839
6088
  return Promise.reject(error);
5840
6089
  }
5841
6090
  // Prevent infinite loop
5842
6091
  if (originalRequest && originalRequest._retry) {
6092
+ console.warn("[Auth Interceptor] Request already retried, logging out");
5843
6093
  try {
5844
6094
  await logout();
5845
6095
  }
5846
- catch (_e) { }
6096
+ catch (err) {
6097
+ console.error("[Auth Interceptor] Logout failed:", err);
6098
+ }
5847
6099
  return Promise.reject(error);
5848
6100
  }
5849
6101
  // Mark so we don't retry twice
@@ -5857,31 +6109,45 @@ function addAuthInterceptors(params) {
5857
6109
  if (!newToken || !originalRequest)
5858
6110
  return reject(error);
5859
6111
  originalRequest.headers = (_a = originalRequest.headers) !== null && _a !== void 0 ? _a : {};
5860
- originalRequest.headers['Authorization'] = `Bearer ${newToken}`;
6112
+ originalRequest.headers["Authorization"] =
6113
+ `Bearer ${newToken}`;
5861
6114
  resolve(apiClient.request(originalRequest));
5862
6115
  });
5863
6116
  });
5864
6117
  }
5865
6118
  isRefreshing = true;
5866
6119
  try {
6120
+ console.log("[Auth Interceptor] Refreshing tokens...");
5867
6121
  const refreshed = await refreshTokens();
5868
- 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
+ }
5869
6130
  resolvePendingRequests(newAccessToken);
5870
6131
  if (originalRequest) {
5871
6132
  originalRequest.headers = (_c = originalRequest.headers) !== null && _c !== void 0 ? _c : {};
5872
6133
  if (newAccessToken)
5873
- (originalRequest.headers)['Authorization'] = `Bearer ${newAccessToken}`;
6134
+ originalRequest.headers["Authorization"] =
6135
+ `Bearer ${newAccessToken}`;
6136
+ console.log("[Auth Interceptor] Retrying original request with new token");
5874
6137
  const resp = await apiClient.request(originalRequest);
5875
6138
  return resp;
5876
6139
  }
5877
6140
  return Promise.reject(error);
5878
6141
  }
5879
6142
  catch (refreshErr) {
6143
+ console.error("[Auth Interceptor] Token refresh failed:", refreshErr);
5880
6144
  resolvePendingRequests(null);
5881
6145
  try {
5882
6146
  await logout();
5883
6147
  }
5884
- catch (_f) { }
6148
+ catch (err) {
6149
+ console.error("[Auth Interceptor] Logout failed:", err);
6150
+ }
5885
6151
  return Promise.reject(refreshErr);
5886
6152
  }
5887
6153
  finally {
@@ -5892,11 +6158,15 @@ function addAuthInterceptors(params) {
5892
6158
  try {
5893
6159
  apiClient.interceptors.request.eject(reqId);
5894
6160
  }
5895
- catch (_a) { }
6161
+ catch (err) {
6162
+ console.error("[Auth Interceptor] Failed to eject request interceptor:", err);
6163
+ }
5896
6164
  try {
5897
6165
  apiClient.interceptors.response.eject(resId);
5898
6166
  }
5899
- catch (_b) { }
6167
+ catch (err) {
6168
+ console.error("[Auth Interceptor] Failed to eject response interceptor:", err);
6169
+ }
5900
6170
  };
5901
6171
  }
5902
6172
 
@@ -6012,7 +6282,7 @@ const useHistoricalPriceData = () => {
6012
6282
  const shortTokens = useUserSelection$1((state) => state.shortTokens);
6013
6283
  const candleInterval = useUserSelection$1((state) => state.candleInterval);
6014
6284
  // Historical price data store
6015
- 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();
6016
6286
  // Track previous tokens and interval to detect changes
6017
6287
  const prevTokensRef = useRef(new Set());
6018
6288
  const prevIntervalRef = useRef(null);
@@ -6065,6 +6335,11 @@ const useHistoricalPriceData = () => {
6065
6335
  const getAllHistoricalPriceData = useCallback(async () => {
6066
6336
  return useHistoricalPriceDataStore.getState().historicalPriceData;
6067
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]);
6068
6343
  const fetchHistoricalPriceData = useCallback(async (startTime, endTime, interval, callback) => {
6069
6344
  const allTokens = getAllTokens();
6070
6345
  if (allTokens.length === 0) {
@@ -6122,6 +6397,7 @@ const useHistoricalPriceData = () => {
6122
6397
  hasHistoricalPriceData,
6123
6398
  getAllHistoricalPriceData,
6124
6399
  getHistoricalPriceData,
6400
+ getEffectiveDataBoundary,
6125
6401
  isLoading,
6126
6402
  clearCache,
6127
6403
  };
@@ -6318,7 +6594,7 @@ const useBasketCandles = () => {
6318
6594
  const longTokens = useUserSelection$1((state) => state.longTokens);
6319
6595
  const shortTokens = useUserSelection$1((state) => state.shortTokens);
6320
6596
  const candleData = useHyperliquidData((s) => s.candleData);
6321
- const { fetchHistoricalPriceData, isLoading: tokenLoading, getAllHistoricalPriceData } = useHistoricalPriceData();
6597
+ const { fetchHistoricalPriceData, isLoading: tokenLoading, getAllHistoricalPriceData, getEffectiveDataBoundary } = useHistoricalPriceData();
6322
6598
  const fetchBasketCandles = useCallback(async (startTime, endTime, interval) => {
6323
6599
  const tokenCandles = await fetchHistoricalPriceData(startTime, endTime, interval);
6324
6600
  const basket = computeBasketCandles(longTokens, shortTokens, tokenCandles);
@@ -6518,6 +6794,7 @@ const useBasketCandles = () => {
6518
6794
  fetchBasketCandles,
6519
6795
  fetchPerformanceCandles,
6520
6796
  fetchOverallPerformanceCandles,
6797
+ getEffectiveDataBoundary,
6521
6798
  isLoading,
6522
6799
  addRealtimeListener,
6523
6800
  removeRealtimeListener,
@@ -6793,11 +7070,13 @@ async function updateLeverage(baseUrl, positionId, payload) {
6793
7070
  }
6794
7071
  }
6795
7072
 
6796
- const calculatePositionAsset = (asset, currentPrice, totalInitialPositionSize, leverage, metadata, isLong = true) => {
7073
+ const calculatePositionAsset = (asset, currentPrice, totalInitialPositionSize, totalCurrentPositionSize, leverage, metadata, isLong = true) => {
6797
7074
  var _a;
6798
7075
  const entryNotional = asset.entryPrice * asset.size;
6799
7076
  const currentNotional = currentPrice * asset.size;
6800
- const marginUsed = currentNotional / (leverage || 1);
7077
+ const effectiveLeverage = leverage || 1;
7078
+ const marginUsed = currentNotional / effectiveLeverage;
7079
+ const entryMarginUsed = entryNotional / effectiveLeverage;
6801
7080
  const unrealizedPnl = isLong
6802
7081
  ? currentNotional - entryNotional
6803
7082
  : entryNotional - currentNotional;
@@ -6807,11 +7086,14 @@ const calculatePositionAsset = (asset, currentPrice, totalInitialPositionSize, l
6807
7086
  actualSize: asset.size,
6808
7087
  leverage: leverage,
6809
7088
  marginUsed: marginUsed,
7089
+ entryMarginUsed: entryMarginUsed,
6810
7090
  positionValue: currentNotional,
6811
7091
  unrealizedPnl: unrealizedPnl,
6812
7092
  entryPositionValue: entryNotional,
6813
7093
  initialWeight: totalInitialPositionSize > 0 ? entryNotional / totalInitialPositionSize : 0,
7094
+ currentWeight: totalCurrentPositionSize > 0 ? currentNotional / totalCurrentPositionSize : 0,
6814
7095
  fundingPaid: (_a = asset.fundingPaid) !== null && _a !== void 0 ? _a : 0,
7096
+ targetWeight: asset.targetWeight,
6815
7097
  metadata,
6816
7098
  };
6817
7099
  };
@@ -6832,20 +7114,32 @@ const buildPositionValue = (rawPositions, clearinghouseState, getAssetByName) =>
6832
7114
  takeProfit: position.takeProfit,
6833
7115
  stopLoss: position.stopLoss,
6834
7116
  };
7117
+ let entryMarginUsed = 0;
6835
7118
  const totalInitialPositionSize = position.longAssets.reduce((acc, asset) => acc + asset.entryPrice * asset.size, 0) +
6836
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);
6837
7130
  mappedPosition.longAssets = position.longAssets.map((longAsset) => {
6838
7131
  var _a, _b, _c, _d;
6839
7132
  const metadata = getAssetByName(longAsset.coin);
6840
7133
  const currentPrice = (_a = metadata === null || metadata === void 0 ? void 0 : metadata.currentPrice) !== null && _a !== void 0 ? _a : 0;
6841
7134
  const assetState = (_b = clearinghouseState.assetPositions.find((ap) => ap.position.coin === longAsset.coin)) === null || _b === void 0 ? void 0 : _b.position;
6842
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;
6843
- const mappedPositionAssets = calculatePositionAsset(longAsset, currentPrice, totalInitialPositionSize, leverage, metadata, true);
7136
+ const mappedPositionAssets = calculatePositionAsset(longAsset, currentPrice, totalInitialPositionSize, totalCurrentPositionSize, leverage, metadata, true);
6844
7137
  mappedPosition.entryPositionValue +=
6845
7138
  mappedPositionAssets.entryPositionValue;
6846
7139
  mappedPosition.unrealizedPnl += mappedPositionAssets.unrealizedPnl;
6847
7140
  mappedPosition.positionValue += mappedPositionAssets.positionValue;
6848
7141
  mappedPosition.marginUsed += mappedPositionAssets.marginUsed;
7142
+ entryMarginUsed += mappedPositionAssets.entryMarginUsed;
6849
7143
  mappedPosition.entryRatio *= Math.pow(longAsset.entryPrice, mappedPositionAssets.initialWeight);
6850
7144
  mappedPosition.markRatio *= Math.pow(currentPrice, mappedPositionAssets.initialWeight);
6851
7145
  return mappedPositionAssets;
@@ -6856,14 +7150,15 @@ const buildPositionValue = (rawPositions, clearinghouseState, getAssetByName) =>
6856
7150
  const currentPrice = (_a = metadata === null || metadata === void 0 ? void 0 : metadata.currentPrice) !== null && _a !== void 0 ? _a : 0;
6857
7151
  const assetState = (_b = clearinghouseState.assetPositions.find((ap) => ap.position.coin === shortAsset.coin)) === null || _b === void 0 ? void 0 : _b.position;
6858
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;
6859
- const mappedPositionAssets = calculatePositionAsset(shortAsset, currentPrice, totalInitialPositionSize, leverage, metadata, false);
7153
+ const mappedPositionAssets = calculatePositionAsset(shortAsset, currentPrice, totalInitialPositionSize, totalCurrentPositionSize, leverage, metadata, false);
6860
7154
  mappedPosition.entryPositionValue +=
6861
7155
  mappedPositionAssets.entryPositionValue;
6862
7156
  mappedPosition.unrealizedPnl += mappedPositionAssets.unrealizedPnl;
6863
7157
  mappedPosition.positionValue += mappedPositionAssets.positionValue;
6864
7158
  mappedPosition.marginUsed += mappedPositionAssets.marginUsed;
7159
+ entryMarginUsed += mappedPositionAssets.entryMarginUsed;
6865
7160
  mappedPosition.entryRatio *= Math.pow(shortAsset.entryPrice, -mappedPositionAssets.initialWeight);
6866
- mappedPosition.markRatio *= Math.pow(currentPrice, -mappedPositionAssets.initialWeight);
7161
+ mappedPosition.markRatio *= Math.pow(currentPrice, -mappedPositionAssets.currentWeight);
6867
7162
  return mappedPositionAssets;
6868
7163
  });
6869
7164
  mappedPosition.positionValue =
@@ -6899,11 +7194,12 @@ const buildPositionValue = (rawPositions, clearinghouseState, getAssetByName) =>
6899
7194
  mappedPosition.markPriceRatio = shortMark;
6900
7195
  }
6901
7196
  mappedPosition.unrealizedPnlPercentage =
6902
- mappedPosition.unrealizedPnl / mappedPosition.marginUsed;
7197
+ entryMarginUsed > 0 ? mappedPosition.unrealizedPnl / entryMarginUsed : 0;
6903
7198
  return mappedPosition;
6904
7199
  });
6905
7200
  };
6906
7201
 
7202
+ const MIN_TRADE_SIZE_USD = 11;
6907
7203
  function usePosition() {
6908
7204
  const context = useContext(PearHyperliquidContext);
6909
7205
  if (!context) {
@@ -6946,6 +7242,85 @@ function usePosition() {
6946
7242
  return null;
6947
7243
  return buildPositionValue(userOpenPositions, aggregatedClearingHouseState, getAssetByName);
6948
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
+ };
6949
7324
  return {
6950
7325
  createPosition: createPosition$1,
6951
7326
  updateRiskParameters: updateRiskParameters$1,
@@ -6956,6 +7331,8 @@ function usePosition() {
6956
7331
  updateLeverage: updateLeverage$1,
6957
7332
  openPositions,
6958
7333
  isLoading,
7334
+ planRebalance,
7335
+ executeRebalance,
6959
7336
  };
6960
7337
  }
6961
7338
 
@@ -7389,20 +7766,34 @@ function usePortfolio() {
7389
7766
  }
7390
7767
 
7391
7768
  async function getEIP712Message(baseUrl, address, clientId) {
7392
- const url = joinUrl(baseUrl, '/auth/eip712-message');
7769
+ const url = joinUrl(baseUrl, "/auth/eip712-message");
7393
7770
  try {
7394
- const resp = await axios$1.get(url, { params: { address, clientId }, timeout: 30000 });
7395
- 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
+ };
7396
7780
  }
7397
7781
  catch (error) {
7398
7782
  throw toApiError(error);
7399
7783
  }
7400
7784
  }
7401
7785
  async function authenticate(baseUrl, body) {
7402
- const url = joinUrl(baseUrl, '/auth/login');
7786
+ const url = joinUrl(baseUrl, "/auth/login");
7403
7787
  try {
7404
- const resp = await axios$1.post(url, body, { headers: { 'Content-Type': 'application/json' }, timeout: 30000 });
7405
- 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
+ };
7406
7797
  }
7407
7798
  catch (error) {
7408
7799
  throw toApiError(error);
@@ -7413,7 +7804,7 @@ async function authenticate(baseUrl, body) {
7413
7804
  */
7414
7805
  async function authenticateWithPrivy(baseUrl, params) {
7415
7806
  const body = {
7416
- method: 'privy_access_token',
7807
+ method: "privy_access_token",
7417
7808
  address: params.address,
7418
7809
  clientId: params.clientId,
7419
7810
  details: { appId: params.appId, accessToken: params.accessToken },
@@ -7421,61 +7812,124 @@ async function authenticateWithPrivy(baseUrl, params) {
7421
7812
  return authenticate(baseUrl, body);
7422
7813
  }
7423
7814
  async function refreshToken(baseUrl, refreshTokenVal) {
7424
- const url = joinUrl(baseUrl, '/auth/refresh');
7815
+ const url = joinUrl(baseUrl, "/auth/refresh");
7425
7816
  try {
7426
- const resp = await axios$1.post(url, { refreshToken: refreshTokenVal }, { headers: { 'Content-Type': 'application/json' }, timeout: 30000 });
7427
- 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
+ };
7428
7823
  }
7429
7824
  catch (error) {
7430
7825
  throw toApiError(error);
7431
7826
  }
7432
7827
  }
7433
7828
  async function logout(baseUrl, refreshTokenVal) {
7434
- const url = joinUrl(baseUrl, '/auth/logout');
7829
+ const url = joinUrl(baseUrl, "/auth/logout");
7435
7830
  try {
7436
- const resp = await axios$1.post(url, { refreshToken: refreshTokenVal }, { headers: { 'Content-Type': 'application/json' }, timeout: 30000 });
7437
- 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
+ };
7438
7837
  }
7439
7838
  catch (error) {
7440
7839
  throw toApiError(error);
7441
7840
  }
7442
7841
  }
7443
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
+ }
7444
7852
  function useAuth() {
7445
7853
  const context = useContext(PearHyperliquidContext);
7446
7854
  if (!context) {
7447
- throw new Error("usePortfolio must be used within a PearHyperliquidProvider");
7855
+ throw new Error('useAuth must be used within a PearHyperliquidProvider');
7448
7856
  }
7449
7857
  const { apiBaseUrl, clientId } = context;
7450
- const [isReady, setIsReady] = useState(false);
7451
7858
  const accessToken = useUserData((s) => s.accessToken);
7452
7859
  const refreshToken$1 = useUserData((s) => s.refreshToken);
7860
+ const isReady = useUserData((s) => s.isReady);
7861
+ const isAuthenticated = useUserData((s) => s.isAuthenticated);
7453
7862
  const setAccessToken = useUserData((s) => s.setAccessToken);
7454
7863
  const setRefreshToken = useUserData((s) => s.setRefreshToken);
7455
- const isAuthenticated = useUserData((s) => s.isAuthenticated);
7864
+ const setIsReady = useUserData((s) => s.setIsReady);
7456
7865
  const setIsAuthenticated = useUserData((s) => s.setIsAuthenticated);
7866
+ const address = useUserData((s) => s.address);
7457
7867
  const setAddress = useUserData((s) => s.setAddress);
7868
+ // Ref to prevent concurrent refresh attempts
7869
+ const isRefreshingRef = useRef(false);
7458
7870
  useEffect(() => {
7459
- if (typeof window == "undefined") {
7871
+ if (typeof window == 'undefined') {
7460
7872
  return;
7461
7873
  }
7462
- const access = localStorage.getItem("accessToken");
7463
- const refresh = localStorage.getItem("refreshToken");
7464
- const addr = localStorage.getItem("address");
7465
- setAccessToken(access);
7466
- setRefreshToken(refresh);
7467
- setAddress(addr);
7468
- const authed = Boolean(access && addr);
7469
- setIsAuthenticated(authed);
7470
- setIsReady(true);
7471
- }, [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]);
7472
7925
  useEffect(() => {
7473
7926
  const cleanup = addAuthInterceptors({
7474
7927
  apiBaseUrl,
7475
7928
  getAccessToken: () => {
7476
- return typeof window !== "undefined"
7477
- ? window.localStorage.getItem("accessToken")
7478
- : null;
7929
+ if (typeof window === 'undefined')
7930
+ return null;
7931
+ // Read from Zustand state as single source of truth
7932
+ return useUserData.getState().accessToken;
7479
7933
  },
7480
7934
  refreshTokens: async () => {
7481
7935
  const data = await refreshTokens();
@@ -7489,6 +7943,55 @@ function useAuth() {
7489
7943
  cleanup();
7490
7944
  };
7491
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]);
7492
7995
  async function getEip712(address) {
7493
7996
  const { data } = await getEIP712Message(apiBaseUrl, address, clientId);
7494
7997
  return data;
@@ -7496,17 +7999,21 @@ function useAuth() {
7496
7999
  async function loginWithSignedMessage(address, signature, timestamp) {
7497
8000
  try {
7498
8001
  const { data } = await authenticate(apiBaseUrl, {
7499
- method: "eip712",
8002
+ method: 'eip712',
7500
8003
  address,
7501
8004
  clientId,
7502
8005
  details: { signature, timestamp },
7503
8006
  });
7504
- window.localStorage.setItem("accessToken", data.accessToken);
7505
- window.localStorage.setItem("refreshToken", data.refreshToken);
7506
- 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));
7507
8015
  setAccessToken(data.accessToken);
7508
8016
  setRefreshToken(data.refreshToken);
7509
- setAddress(address);
7510
8017
  setIsAuthenticated(true);
7511
8018
  }
7512
8019
  catch (e) {
@@ -7521,12 +8028,16 @@ function useAuth() {
7521
8028
  appId,
7522
8029
  accessToken: privyAccessToken,
7523
8030
  });
7524
- window.localStorage.setItem("accessToken", data.accessToken);
7525
- window.localStorage.setItem("refreshToken", data.refreshToken);
7526
- 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));
7527
8039
  setAccessToken(data.accessToken);
7528
8040
  setRefreshToken(data.refreshToken);
7529
- setAddress(address);
7530
8041
  setIsAuthenticated(true);
7531
8042
  }
7532
8043
  catch (e) {
@@ -7534,38 +8045,60 @@ function useAuth() {
7534
8045
  }
7535
8046
  }
7536
8047
  async function refreshTokens() {
7537
- const refresh = window.localStorage.getItem("refreshToken");
7538
- if (!refresh)
7539
- throw new Error("No refresh token");
7540
- const { data } = await refreshToken(apiBaseUrl, refresh);
7541
- window.localStorage.setItem("accessToken", data.accessToken);
7542
- 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));
7543
8062
  setAccessToken(data.accessToken);
7544
8063
  setRefreshToken(data.refreshToken);
7545
8064
  setIsAuthenticated(true);
7546
8065
  return data;
7547
8066
  }
7548
8067
  async function logout$1() {
7549
- const refresh = window.localStorage.getItem("refreshToken");
7550
- if (refresh) {
8068
+ const currentAddress = address;
8069
+ const currentRefresh = refreshToken$1;
8070
+ if (currentRefresh) {
7551
8071
  try {
7552
- await logout(apiBaseUrl, refresh);
8072
+ await logout(apiBaseUrl, currentRefresh);
7553
8073
  }
7554
8074
  catch (_a) {
7555
8075
  /* ignore */
7556
8076
  }
7557
8077
  }
7558
- window.localStorage.removeItem("accessToken");
7559
- window.localStorage.removeItem("refreshToken");
7560
- 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
+ }
7561
8088
  setAccessToken(null);
7562
8089
  setRefreshToken(null);
7563
8090
  setAddress(null);
7564
8091
  setIsAuthenticated(false);
7565
8092
  }
8093
+ function clearSession() {
8094
+ setAccessToken(null);
8095
+ setRefreshToken(null);
8096
+ setIsAuthenticated(false);
8097
+ }
7566
8098
  return {
7567
8099
  isReady,
7568
8100
  isAuthenticated,
8101
+ address,
7569
8102
  accessToken,
7570
8103
  refreshToken: refreshToken$1,
7571
8104
  getEip712,
@@ -7573,6 +8106,8 @@ function useAuth() {
7573
8106
  loginWithPrivyToken,
7574
8107
  refreshTokens,
7575
8108
  logout: logout$1,
8109
+ clearSession,
8110
+ setAddress,
7576
8111
  };
7577
8112
  }
7578
8113
 
@@ -7613,14 +8148,20 @@ const useAllUserBalances = () => {
7613
8148
  }
7614
8149
  }
7615
8150
  if (!availableToTrades['USDC']) {
7616
- 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'));
7617
8153
  }
7618
8154
  return {
7619
8155
  spotBalances,
7620
8156
  availableToTrades,
7621
8157
  isLoading,
7622
8158
  };
7623
- }, [spotState, longTokensMetadata, shortTokensMetadata, aggregatedClearingHouseState]);
8159
+ }, [
8160
+ spotState,
8161
+ longTokensMetadata,
8162
+ shortTokensMetadata,
8163
+ aggregatedClearingHouseState,
8164
+ ]);
7624
8165
  /**
7625
8166
  * Calculate margin required for every collateral token based on asset leverages and size.
7626
8167
  * Returns margin required per collateral and whether there's sufficient margin.
@@ -7740,6 +8281,7 @@ const useAllUserBalances = () => {
7740
8281
  availableToTrades,
7741
8282
  ]);
7742
8283
  return {
8284
+ abstractionMode: useUserData((state) => state.userAbstractionMode),
7743
8285
  spotBalances,
7744
8286
  availableToTrades,
7745
8287
  isLoading,
@@ -7880,13 +8422,470 @@ function useHyperliquidUserFills(options) {
7880
8422
  };
7881
8423
  }
7882
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 mapAsset$1 = (asset, getAssetByName) => {
8496
+ var _a, _b, _c, _d;
8497
+ const metadata = getAssetByName(asset.coin);
8498
+ const marketInfo = getMarketInfoFromSymbol(asset.coin);
8499
+ return {
8500
+ coin: asset.coin,
8501
+ symbol: (_a = metadata === null || metadata === void 0 ? void 0 : metadata.symbolName) !== null && _a !== void 0 ? _a : marketInfo.symbolName,
8502
+ assetName: (_b = metadata === null || metadata === void 0 ? void 0 : metadata.assetName) !== null && _b !== void 0 ? _b : asset.coin,
8503
+ marketPrefix: (_c = metadata === null || metadata === void 0 ? void 0 : metadata.marketName) !== null && _c !== void 0 ? _c : marketInfo.marketName,
8504
+ percentage: asset.closeWeight * 100,
8505
+ collateralToken: (_d = metadata === null || metadata === void 0 ? void 0 : metadata.collateralToken) !== null && _d !== void 0 ? _d : 'USDC',
8506
+ };
8507
+ };
8508
+ const getCollateralTypes$1 = (assets) => {
8509
+ const set = new Set();
8510
+ for (const a of assets)
8511
+ set.add(a.collateralToken);
8512
+ return set.size > 0 ? Array.from(set) : ['USDC'];
8513
+ };
8514
+ const buildSummary = (days) => {
8515
+ let pnl = 0;
8516
+ let volume = 0;
8517
+ let wins = 0;
8518
+ let losses = 0;
8519
+ let totalProfit = 0;
8520
+ let totalLoss = 0;
8521
+ for (const day of days) {
8522
+ pnl += day.totalPnl;
8523
+ volume += day.volume;
8524
+ if (day.positionsClosed === 0)
8525
+ continue;
8526
+ if (day.totalPnl > 0) {
8527
+ wins++;
8528
+ totalProfit += day.totalPnl;
8529
+ }
8530
+ else if (day.totalPnl < 0) {
8531
+ losses++;
8532
+ totalLoss += Math.abs(day.totalPnl);
8533
+ }
8534
+ }
8535
+ const total = wins + losses;
8536
+ const winRate = total > 0 ? Math.round((wins / total) * 100) : 0;
8537
+ return {
8538
+ pnl: round2(pnl),
8539
+ volume: round2(volume),
8540
+ winRate,
8541
+ wins,
8542
+ losses,
8543
+ totalProfit: round2(totalProfit),
8544
+ totalLoss: round2(totalLoss),
8545
+ };
8546
+ };
8547
+ const buildCalendarData = (tradeHistories, timeframe, rangeStart, rangeEnd, totalDays, useCustomDates, getAssetByName) => {
8548
+ const startKey = toDateKey(rangeStart);
8549
+ const endKey = toDateKey(rangeEnd);
8550
+ // Build day buckets for the full range
8551
+ const buckets = new Map();
8552
+ for (let i = 0; i < totalDays; i++) {
8553
+ const d = new Date(rangeStart);
8554
+ d.setDate(rangeStart.getDate() + i);
8555
+ buckets.set(toDateKey(d), {
8556
+ pnl: 0,
8557
+ volume: 0,
8558
+ positionsClosed: 0,
8559
+ trades: [],
8560
+ });
8561
+ }
8562
+ // Populate buckets from trade histories
8563
+ for (const trade of tradeHistories) {
8564
+ if (!trade.createdAt)
8565
+ continue;
8566
+ const date = new Date(trade.createdAt);
8567
+ if (isNaN(date.getTime()))
8568
+ continue;
8569
+ const dateKey = toDateKey(date);
8570
+ if (dateKey < startKey || dateKey > endKey)
8571
+ continue;
8572
+ const bucket = buckets.get(dateKey);
8573
+ if (!bucket)
8574
+ continue;
8575
+ const pnl = trade.realizedPnl;
8576
+ bucket.pnl += isFinite(pnl) ? pnl : 0;
8577
+ const vol = trade.totalValue;
8578
+ bucket.volume += isFinite(vol) ? vol : 0;
8579
+ bucket.positionsClosed += 1;
8580
+ const tradePnl = trade.realizedPnl;
8581
+ const longAssets = trade.closedLongAssets.map((a) => mapAsset$1(a, getAssetByName));
8582
+ const shortAssets = trade.closedShortAssets.map((a) => mapAsset$1(a, getAssetByName));
8583
+ bucket.trades.push({
8584
+ tradeHistoryId: trade.tradeHistoryId,
8585
+ realizedPnl: tradePnl,
8586
+ result: tradePnl > 0 ? 'profit' : tradePnl < 0 ? 'loss' : 'breakeven',
8587
+ collateralTypes: getCollateralTypes$1([...longAssets, ...shortAssets]),
8588
+ closedLongAssets: longAssets,
8589
+ closedShortAssets: shortAssets,
8590
+ });
8591
+ }
8592
+ // Build day objects
8593
+ const allDays = [];
8594
+ const sortedKeys = Array.from(buckets.keys()).sort();
8595
+ for (const key of sortedKeys) {
8596
+ const bucket = buckets.get(key);
8597
+ const roundedPnl = round2(bucket.pnl);
8598
+ const result = roundedPnl > 0 ? 'profit' : roundedPnl < 0 ? 'loss' : 'breakeven';
8599
+ allDays.push({
8600
+ date: key,
8601
+ totalPnl: roundedPnl,
8602
+ volume: round2(bucket.volume),
8603
+ positionsClosed: bucket.positionsClosed,
8604
+ result,
8605
+ trades: bucket.trades,
8606
+ });
8607
+ }
8608
+ // Group into periods
8609
+ let weeks = [];
8610
+ let months = [];
8611
+ const useWeekGrouping = useCustomDates
8612
+ ? totalDays <= 28
8613
+ : isWeekTimeframe(timeframe);
8614
+ if (useWeekGrouping) {
8615
+ const weekMap = new Map();
8616
+ for (const day of allDays) {
8617
+ const date = new Date(day.date + 'T00:00:00');
8618
+ const monday = getMonday(date);
8619
+ const mondayKey = toDateKey(monday);
8620
+ if (!weekMap.has(mondayKey)) {
8621
+ weekMap.set(mondayKey, []);
8622
+ }
8623
+ weekMap.get(mondayKey).push(day);
8624
+ }
8625
+ const sortedWeekKeys = Array.from(weekMap.keys()).sort();
8626
+ weeks = sortedWeekKeys.map((mondayKey) => {
8627
+ const days = weekMap.get(mondayKey);
8628
+ const monday = new Date(mondayKey + 'T00:00:00');
8629
+ const sunday = new Date(monday);
8630
+ sunday.setDate(monday.getDate() + 6);
8631
+ return {
8632
+ weekStart: mondayKey,
8633
+ weekEnd: toDateKey(sunday),
8634
+ days,
8635
+ summary: buildSummary(days),
8636
+ };
8637
+ });
8638
+ }
8639
+ else {
8640
+ const monthMap = new Map();
8641
+ for (const day of allDays) {
8642
+ const date = new Date(day.date + 'T00:00:00');
8643
+ const mk = toMonthKey(date);
8644
+ if (!monthMap.has(mk)) {
8645
+ monthMap.set(mk, { days: [], label: formatMonthLabel(date) });
8646
+ }
8647
+ monthMap.get(mk).days.push(day);
8648
+ }
8649
+ const sortedMonthKeys = Array.from(monthMap.keys()).sort();
8650
+ months = sortedMonthKeys.map((mk) => {
8651
+ const { days, label } = monthMap.get(mk);
8652
+ return {
8653
+ month: mk,
8654
+ label,
8655
+ days,
8656
+ summary: buildSummary(days),
8657
+ };
8658
+ });
8659
+ }
8660
+ return {
8661
+ timeframe,
8662
+ weeks,
8663
+ months,
8664
+ overall: buildSummary(allDays),
8665
+ isLoading: false,
8666
+ };
8667
+ };
8668
+ // ─── hook ───────────────────────────────────────────────────────
8669
+ function usePnlCalendar(options) {
8670
+ var _a;
8671
+ const opts = typeof options === 'string'
8672
+ ? { timeframe: options }
8673
+ : options !== null && options !== void 0 ? options : {};
8674
+ const timeframe = (_a = opts.timeframe) !== null && _a !== void 0 ? _a : '2W';
8675
+ const customStart = opts.startDate;
8676
+ const customEnd = opts.endDate;
8677
+ const context = useContext(PearHyperliquidContext);
8678
+ if (!context) {
8679
+ throw new Error('usePnlCalendar must be used within a PearHyperliquidProvider');
8680
+ }
8681
+ const { apiBaseUrl } = context;
8682
+ const isAuthenticated = useUserData((state) => state.isAuthenticated);
8683
+ const { getAssetByName } = useMarket();
8684
+ const [trades, setTrades] = useState(null);
8685
+ const [isLoading, setIsLoading] = useState(false);
8686
+ const [error, setError] = useState(null);
8687
+ const mountedRef = useRef(true);
8688
+ useEffect(() => {
8689
+ mountedRef.current = true;
8690
+ return () => { mountedRef.current = false; };
8691
+ }, []);
8692
+ // Compute the date range
8693
+ const useCustomDates = !!(customStart && customEnd);
8694
+ let rangeStart;
8695
+ let rangeEnd;
8696
+ let totalDays;
8697
+ if (useCustomDates) {
8698
+ rangeStart = toLocalMidnight(customStart);
8699
+ rangeEnd = toLocalMidnight(customEnd);
8700
+ totalDays = diffDays(rangeStart, rangeEnd);
8701
+ }
8702
+ else {
8703
+ totalDays = getTimeframeDays$1(timeframe);
8704
+ rangeEnd = new Date();
8705
+ rangeEnd.setHours(0, 0, 0, 0);
8706
+ rangeStart = new Date(rangeEnd);
8707
+ rangeStart.setDate(rangeEnd.getDate() - totalDays + 1);
8708
+ }
8709
+ const startIso = toISODateString$1(rangeStart);
8710
+ const endIso = toISODateString$1(rangeEnd);
8711
+ const fetchData = useCallback(async () => {
8712
+ if (!isAuthenticated)
8713
+ return;
8714
+ setIsLoading(true);
8715
+ setError(null);
8716
+ try {
8717
+ const response = await getTradeHistory(apiBaseUrl, {
8718
+ startDate: startIso,
8719
+ endDate: endIso,
8720
+ limit: totalDays * 50,
8721
+ });
8722
+ if (!mountedRef.current)
8723
+ return;
8724
+ setTrades(response.data);
8725
+ }
8726
+ catch (err) {
8727
+ if (!mountedRef.current)
8728
+ return;
8729
+ setError(err instanceof Error ? err.message : 'Failed to fetch trade history');
8730
+ setTrades(null);
8731
+ }
8732
+ finally {
8733
+ if (mountedRef.current)
8734
+ setIsLoading(false);
8735
+ }
8736
+ }, [apiBaseUrl, isAuthenticated, startIso, endIso, totalDays]);
8737
+ useEffect(() => {
8738
+ fetchData();
8739
+ }, [fetchData]);
8740
+ const result = useMemo(() => {
8741
+ const empty = {
8742
+ timeframe,
8743
+ weeks: [],
8744
+ months: [],
8745
+ overall: EMPTY_SUMMARY,
8746
+ isLoading: true,
8747
+ };
8748
+ if (!trades)
8749
+ return empty;
8750
+ if (totalDays <= 0)
8751
+ return empty;
8752
+ return buildCalendarData(trades, timeframe, rangeStart, rangeEnd, totalDays, useCustomDates, getAssetByName);
8753
+ }, [trades, timeframe, startIso, endIso, getAssetByName]);
8754
+ return { ...result, isLoading, error, refetch: fetchData };
8755
+ }
8756
+
8757
+ const HEATMAP_LIMIT = 50;
8758
+ // ─── helpers ────────────────────────────────────────────────────
8759
+ const getTimeframeDays = (tf) => {
8760
+ switch (tf) {
8761
+ case '7D':
8762
+ return 7;
8763
+ case '30D':
8764
+ return 30;
8765
+ case '100D':
8766
+ return 100;
8767
+ case 'allTime':
8768
+ return null;
8769
+ }
8770
+ };
8771
+ const toISODateString = (date) => date.toISOString();
8772
+ const mapAsset = (asset, getAssetByName) => {
8773
+ var _a, _b, _c, _d;
8774
+ const metadata = getAssetByName(asset.coin);
8775
+ const marketInfo = getMarketInfoFromSymbol(asset.coin);
8776
+ return {
8777
+ coin: asset.coin,
8778
+ symbol: (_a = metadata === null || metadata === void 0 ? void 0 : metadata.symbolName) !== null && _a !== void 0 ? _a : marketInfo.symbolName,
8779
+ assetName: (_b = metadata === null || metadata === void 0 ? void 0 : metadata.assetName) !== null && _b !== void 0 ? _b : asset.coin,
8780
+ marketPrefix: (_c = metadata === null || metadata === void 0 ? void 0 : metadata.marketName) !== null && _c !== void 0 ? _c : marketInfo.marketName,
8781
+ percentage: asset.closeWeight * 100,
8782
+ collateralToken: (_d = metadata === null || metadata === void 0 ? void 0 : metadata.collateralToken) !== null && _d !== void 0 ? _d : 'USDC',
8783
+ };
8784
+ };
8785
+ const getCollateralTypes = (assets) => {
8786
+ const set = new Set();
8787
+ for (const a of assets)
8788
+ set.add(a.collateralToken);
8789
+ return set.size > 0 ? Array.from(set) : ['USDC'];
8790
+ };
8791
+ const toCalendarTrade = (trade, getAssetByName) => {
8792
+ const pnl = trade.realizedPnl;
8793
+ const longAssets = trade.closedLongAssets.map((a) => mapAsset(a, getAssetByName));
8794
+ const shortAssets = trade.closedShortAssets.map((a) => mapAsset(a, getAssetByName));
8795
+ return {
8796
+ tradeHistoryId: trade.tradeHistoryId,
8797
+ realizedPnl: pnl,
8798
+ result: pnl > 0 ? 'profit' : pnl < 0 ? 'loss' : 'breakeven',
8799
+ collateralTypes: getCollateralTypes([...longAssets, ...shortAssets]),
8800
+ closedLongAssets: longAssets,
8801
+ closedShortAssets: shortAssets,
8802
+ };
8803
+ };
8804
+ // ─── hook ───────────────────────────────────────────────────────
8805
+ function usePnlHeatmap(timeframe = 'allTime') {
8806
+ const context = useContext(PearHyperliquidContext);
8807
+ if (!context) {
8808
+ throw new Error('usePnlHeatmap must be used within a PearHyperliquidProvider');
8809
+ }
8810
+ const { apiBaseUrl } = context;
8811
+ const isAuthenticated = useUserData((state) => state.isAuthenticated);
8812
+ const { getAssetByName } = useMarket();
8813
+ const [trades, setTrades] = useState(null);
8814
+ const [isLoading, setIsLoading] = useState(false);
8815
+ const [error, setError] = useState(null);
8816
+ const mountedRef = useRef(true);
8817
+ useEffect(() => {
8818
+ mountedRef.current = true;
8819
+ return () => { mountedRef.current = false; };
8820
+ }, []);
8821
+ const days = getTimeframeDays(timeframe);
8822
+ let startIso;
8823
+ if (days !== null) {
8824
+ const start = new Date();
8825
+ start.setHours(0, 0, 0, 0);
8826
+ start.setDate(start.getDate() - days);
8827
+ startIso = toISODateString(start);
8828
+ }
8829
+ const fetchData = useCallback(async () => {
8830
+ if (!isAuthenticated)
8831
+ return;
8832
+ setIsLoading(true);
8833
+ setError(null);
8834
+ try {
8835
+ const response = await getTradeHistory(apiBaseUrl, {
8836
+ ...(startIso ? { startDate: startIso } : {}),
8837
+ limit: 5000,
8838
+ });
8839
+ if (!mountedRef.current)
8840
+ return;
8841
+ setTrades(response.data);
8842
+ }
8843
+ catch (err) {
8844
+ if (!mountedRef.current)
8845
+ return;
8846
+ setError(err instanceof Error ? err.message : 'Failed to fetch trade history');
8847
+ setTrades(null);
8848
+ }
8849
+ finally {
8850
+ if (mountedRef.current)
8851
+ setIsLoading(false);
8852
+ }
8853
+ }, [apiBaseUrl, isAuthenticated, startIso]);
8854
+ useEffect(() => {
8855
+ fetchData();
8856
+ }, [fetchData]);
8857
+ const result = useMemo(() => {
8858
+ if (!trades)
8859
+ return [];
8860
+ const top = trades
8861
+ .slice()
8862
+ .sort((a, b) => Math.abs(b.realizedPnl) - Math.abs(a.realizedPnl))
8863
+ .slice(0, HEATMAP_LIMIT);
8864
+ const totalAbsPnl = top.reduce((sum, t) => sum + Math.abs(t.realizedPnl), 0);
8865
+ return top.map((t) => ({
8866
+ ...toCalendarTrade(t, getAssetByName),
8867
+ percentage: totalAbsPnl > 0
8868
+ ? Math.round((Math.abs(t.realizedPnl) / totalAbsPnl) * 10000) / 100
8869
+ : 0,
8870
+ }));
8871
+ }, [trades, getAssetByName]);
8872
+ return { timeframe, trades: result, isLoading, error, refetch: fetchData };
8873
+ }
8874
+
7883
8875
  const PearHyperliquidContext = createContext(undefined);
7884
8876
  /**
7885
8877
  * React Provider for PearHyperliquidClient
7886
8878
  */
7887
- const PearHyperliquidProvider = ({ children, apiBaseUrl = 'https://hl-ui.pearprotocol.io', clientId = 'PEARPROTOCOLUI', wsUrl = 'wss://hl-ui.pearprotocol.io/ws', }) => {
8879
+ const PearHyperliquidProvider = ({ children, apiBaseUrl = "https://hl-ui.pearprotocol.io", clientId = "PEARPROTOCOLUI", wsUrl = "wss://hl-ui.pearprotocol.io/ws", }) => {
7888
8880
  const address = useUserData((s) => s.address);
7889
- const setAddress = useUserData((s) => s.setAddress);
8881
+ const clearHyperliquidUserData = useHyperliquidData((state) => state.clearUserData);
8882
+ const prevAddressRef = useRef(null);
8883
+ useEffect(() => {
8884
+ if (prevAddressRef.current !== null && prevAddressRef.current !== address) {
8885
+ clearHyperliquidUserData();
8886
+ }
8887
+ prevAddressRef.current = address;
8888
+ }, [address, clearHyperliquidUserData]);
7890
8889
  const perpMetasByDex = useHyperliquidData((state) => state.perpMetasByDex);
7891
8890
  const setPerpDexs = useHyperliquidData((state) => state.setPerpDexs);
7892
8891
  const setPerpMetasByDex = useHyperliquidData((state) => state.setPerpMetasByDex);
@@ -7917,20 +8916,20 @@ const PearHyperliquidProvider = ({ children, apiBaseUrl = 'https://hl-ui.pearpro
7917
8916
  perpMetas.forEach((item, perpIndex) => {
7918
8917
  var _a, _b;
7919
8918
  const dexName = perpIndex === 0
7920
- ? 'HYPERLIQUID'
8919
+ ? "HYPERLIQUID"
7921
8920
  : ((_b = (_a = perpDexs[perpIndex]) === null || _a === void 0 ? void 0 : _a.name) !== null && _b !== void 0 ? _b : `DEX_${perpIndex}`);
7922
8921
  var collateralToken;
7923
8922
  if (item.collateralToken === 360) {
7924
- collateralToken = 'USDH';
8923
+ collateralToken = "USDH";
7925
8924
  }
7926
8925
  if (item.collateralToken === 0) {
7927
- collateralToken = 'USDC';
8926
+ collateralToken = "USDC";
7928
8927
  }
7929
8928
  if (item.collateralToken === 235) {
7930
- collateralToken = 'USDE';
8929
+ collateralToken = "USDE";
7931
8930
  }
7932
8931
  if (item.collateralToken === 268) {
7933
- collateralToken = 'USDT0';
8932
+ collateralToken = "USDT0";
7934
8933
  }
7935
8934
  const universeAssets = item.universe.map((asset) => ({
7936
8935
  ...asset,
@@ -7958,8 +8957,6 @@ const PearHyperliquidProvider = ({ children, apiBaseUrl = 'https://hl-ui.pearpro
7958
8957
  }), [
7959
8958
  apiBaseUrl,
7960
8959
  wsUrl,
7961
- address,
7962
- setAddress,
7963
8960
  isConnected,
7964
8961
  lastError,
7965
8962
  nativeIsConnected,
@@ -7974,7 +8971,7 @@ const PearHyperliquidProvider = ({ children, apiBaseUrl = 'https://hl-ui.pearpro
7974
8971
  function usePearHyperliquid() {
7975
8972
  const ctx = useContext(PearHyperliquidContext);
7976
8973
  if (!ctx)
7977
- throw new Error('usePearHyperliquid must be used within a PearHyperliquidProvider');
8974
+ throw new Error("usePearHyperliquid must be used within a PearHyperliquidProvider");
7978
8975
  return ctx;
7979
8976
  }
7980
8977
 
@@ -8226,4 +9223,4 @@ function getOrderTrailingInfo(order) {
8226
9223
  return undefined;
8227
9224
  }
8228
9225
 
8229
- 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 };
9226
+ 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 };