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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -64,17 +64,28 @@ const useUserData = create((set) => ({
64
64
  notifications: null,
65
65
  userExtraAgents: null,
66
66
  spotState: null,
67
+ userAbstractionMode: null,
68
+ isReady: false,
69
+ setUserAbstractionMode: (value) => set({ userAbstractionMode: value }),
67
70
  setAccessToken: (token) => set({ accessToken: token }),
68
71
  setRefreshToken: (token) => set({ refreshToken: token }),
69
72
  setIsAuthenticated: (value) => set({ isAuthenticated: value }),
70
- setAddress: (address) => set(() => {
71
- if (typeof window !== 'undefined') {
72
- if (address) {
73
- window.localStorage.setItem('address', address);
74
- }
75
- else {
76
- window.localStorage.removeItem('address');
77
- }
73
+ setIsReady: (value) => set({ isReady: value }),
74
+ setAddress: (address) => set((state) => {
75
+ const addressChanged = state.address !== null && state.address !== address;
76
+ if (addressChanged) {
77
+ return {
78
+ address,
79
+ spotState: null,
80
+ tradeHistories: null,
81
+ rawOpenPositions: null,
82
+ openOrders: null,
83
+ accountSummary: null,
84
+ twapDetails: null,
85
+ notifications: null,
86
+ userExtraAgents: null,
87
+ userAbstractionMode: null,
88
+ };
78
89
  }
79
90
  return { address };
80
91
  }),
@@ -86,10 +97,11 @@ const useUserData = create((set) => ({
86
97
  setNotifications: (value) => set({ notifications: value }),
87
98
  setSpotState: (value) => set({ spotState: value }),
88
99
  clean: () => set({
89
- accessToken: null,
90
- refreshToken: null,
91
- isAuthenticated: false,
92
- address: null,
100
+ // accessToken: null,
101
+ // refreshToken: null,
102
+ // isAuthenticated: false,
103
+ // isReady: false,
104
+ // address: null,
93
105
  tradeHistories: null,
94
106
  rawOpenPositions: null,
95
107
  openOrders: null,
@@ -382,19 +394,19 @@ class TokenMetadataExtractor {
382
394
  if (!assetCtx) {
383
395
  return null;
384
396
  }
385
- // Get current price - prefer assetCtx.midPx as it's already index-matched,
386
- // fall back to allMids lookup if midPx is null
397
+ // Get current price - prefer allMids (real-time WebSocket data),
398
+ // fall back to assetCtx.midPx if not available
387
399
  const actualSymbol = foundAsset.name;
388
400
  let currentPrice = 0;
389
- // Fallback: assetCtx.midPx (already properly indexed)
390
- if (!currentPrice || isNaN(currentPrice)) {
391
- const currentPriceStr = allMids.mids[actualSymbol] || allMids.mids[symbol];
392
- currentPrice = currentPriceStr ? parseFloat(currentPriceStr) : 0;
393
- }
394
- // Primary source: allMids lookup
401
+ // Fallback: assetCtx.midPx (from REST API, less frequent)
395
402
  if (assetCtx.midPx) {
396
403
  currentPrice = parseFloat(assetCtx.midPx);
397
404
  }
405
+ // Primary source: allMids (real-time WebSocket data)
406
+ const currentPriceStr = allMids.mids[actualSymbol] || allMids.mids[symbol];
407
+ if (currentPriceStr) {
408
+ currentPrice = parseFloat(currentPriceStr);
409
+ }
398
410
  // Get previous day price
399
411
  const prevDayPrice = parseFloat(assetCtx.prevDayPx);
400
412
  // Calculate 24h price change
@@ -549,7 +561,7 @@ const useHyperliquidData = create((set) => ({
549
561
  tokenMetadata: refreshTokenMetadata(state, { allMids: value }),
550
562
  })),
551
563
  setActiveAssetData: (value) => set((state) => {
552
- const activeAssetData = typeof value === 'function' ? value(state.activeAssetData) : value;
564
+ const activeAssetData = typeof value === "function" ? value(state.activeAssetData) : value;
553
565
  return {
554
566
  activeAssetData,
555
567
  tokenMetadata: refreshTokenMetadata(state, { activeAssetData }),
@@ -589,7 +601,10 @@ const useHyperliquidData = create((set) => ({
589
601
  setCandleData: (value) => set({ candleData: value }),
590
602
  upsertActiveAssetData: (key, value) => set((state) => {
591
603
  var _a;
592
- const activeAssetData = { ...((_a = state.activeAssetData) !== null && _a !== void 0 ? _a : {}), [key]: value };
604
+ const activeAssetData = {
605
+ ...((_a = state.activeAssetData) !== null && _a !== void 0 ? _a : {}),
606
+ [key]: value,
607
+ };
593
608
  return {
594
609
  activeAssetData,
595
610
  tokenMetadata: refreshTokenMetadata(state, { activeAssetData }, { symbols: [key] }),
@@ -624,6 +639,29 @@ const useHyperliquidData = create((set) => ({
624
639
  perpMetasByDex: state.perpMetasByDex,
625
640
  }),
626
641
  })),
642
+ clearUserData: () => set((state) => {
643
+ const refreshedMetadata = refreshTokenMetadata(state, {
644
+ activeAssetData: null,
645
+ });
646
+ // Remove user-scoped fields even when upstream metadata inputs are not ready.
647
+ const tokenMetadata = Object.fromEntries(Object.entries(refreshedMetadata).map(([symbol, metadata]) => [
648
+ symbol,
649
+ metadata
650
+ ? {
651
+ ...metadata,
652
+ leverage: undefined,
653
+ maxTradeSzs: undefined,
654
+ availableToTrade: undefined,
655
+ }
656
+ : null,
657
+ ]));
658
+ return {
659
+ aggregatedClearingHouseState: null,
660
+ rawClearinghouseStates: null,
661
+ activeAssetData: null,
662
+ tokenMetadata,
663
+ };
664
+ }),
627
665
  }));
628
666
 
629
667
  /**
@@ -887,7 +925,7 @@ const useUserSelection$1 = create((set, get) => ({
887
925
 
888
926
  const useHyperliquidNativeWebSocket = ({ address, enabled = true, onUserFills, }) => {
889
927
  const { setAllMids, setActiveAssetData, upsertActiveAssetData, setCandleData, deleteCandleSymbol, deleteActiveAssetData, addCandleData, setFinalAtOICaps, setAggregatedClearingHouseState, setRawClearinghouseStates, setAssetContextsByDex, } = useHyperliquidData();
890
- const { setSpotState } = useUserData();
928
+ const { setSpotState, setUserAbstractionMode } = useUserData();
891
929
  const { candleInterval } = useUserSelection$1();
892
930
  const userSummary = useUserData((state) => state.accountSummary);
893
931
  const longTokens = useUserSelection$1((s) => s.longTokens);
@@ -899,18 +937,26 @@ const useHyperliquidNativeWebSocket = ({ address, enabled = true, onUserFills, }
899
937
  const [subscribedCandleTokens, setSubscribedCandleTokens] = useState([]);
900
938
  const [clearinghouseStateReceived, setClearinghouseStateReceived] = useState(false);
901
939
  const prevCandleIntervalRef = useRef(null);
940
+ const prevActiveAssetAddressRef = useRef(null);
902
941
  const pingIntervalRef = useRef(null);
903
942
  const wsRef = useRef(null);
904
943
  const reconnectAttemptsRef = useRef(0);
905
944
  const manualCloseRef = useRef(false);
945
+ const onUserFillsRef = useRef(onUserFills);
946
+ const reconnectTimeoutRef = useRef(null);
906
947
  const [readyState, setReadyState] = useState(ReadyState.CONNECTING);
948
+ // Keep the ref updated with the latest callback
949
+ useEffect(() => {
950
+ onUserFillsRef.current = onUserFills;
951
+ }, [onUserFills]);
907
952
  const handleMessage = useCallback((event) => {
953
+ var _a;
908
954
  try {
909
955
  const message = JSON.parse(event.data);
910
956
  // Handle subscription responses
911
- if ('success' in message || 'error' in message) {
957
+ if ("success" in message || "error" in message) {
912
958
  if (message.error) {
913
- console.error('[HyperLiquid WS] Subscription error:', message.error);
959
+ console.error("[HyperLiquid WS] Subscription error:", message.error);
914
960
  setLastError(message.error);
915
961
  }
916
962
  else {
@@ -919,43 +965,44 @@ const useHyperliquidNativeWebSocket = ({ address, enabled = true, onUserFills, }
919
965
  return;
920
966
  }
921
967
  // Handle channel data messages
922
- if ('channel' in message && 'data' in message) {
968
+ if ("channel" in message && "data" in message) {
923
969
  const response = message;
924
970
  switch (response.channel) {
925
- case 'userFills':
971
+ case "userFills":
926
972
  {
927
- const maybePromise = onUserFills === null || onUserFills === void 0 ? void 0 : onUserFills();
973
+ const maybePromise = (_a = onUserFillsRef.current) === null || _a === void 0 ? void 0 : _a.call(onUserFillsRef);
928
974
  if (maybePromise instanceof Promise) {
929
- maybePromise.catch((err) => console.error('[HyperLiquid WS] userFills callback error', err));
975
+ maybePromise.catch((err) => console.error("[HyperLiquid WS] userFills callback error", err));
930
976
  }
931
977
  }
932
978
  break;
933
- case 'webData3':
979
+ case "webData3":
934
980
  const webData3 = response.data;
935
981
  // finalAssetContexts now sourced from allDexsAssetCtxs channel
936
982
  const finalAtOICaps = webData3.perpDexStates.flatMap((dex) => dex.perpsAtOpenInterestCap);
937
983
  setFinalAtOICaps(finalAtOICaps);
984
+ setUserAbstractionMode(webData3.userState.abstraction || null);
938
985
  break;
939
- case 'allDexsAssetCtxs':
986
+ case "allDexsAssetCtxs":
940
987
  {
941
988
  const data = response.data;
942
989
  // Store by DEX name, mapping '' to 'HYPERLIQUID'
943
990
  const assetContextsByDex = new Map();
944
991
  data.ctxs.forEach(([dexKey, ctxs]) => {
945
- const dexName = dexKey === '' ? 'HYPERLIQUID' : dexKey;
992
+ const dexName = dexKey === "" ? "HYPERLIQUID" : dexKey;
946
993
  assetContextsByDex.set(dexName, ctxs || []);
947
994
  });
948
995
  setAssetContextsByDex(assetContextsByDex);
949
996
  }
950
997
  break;
951
- case 'allDexsClearinghouseState':
998
+ case "allDexsClearinghouseState":
952
999
  {
953
1000
  const data = response.data;
954
1001
  const states = (data.clearinghouseStates || [])
955
1002
  .map(([, s]) => s)
956
1003
  .filter(Boolean);
957
- const sum = (values) => values.reduce((acc, v) => acc + (parseFloat(v || '0') || 0), 0);
958
- const toStr = (n) => Number.isFinite(n) ? n.toString() : '0';
1004
+ const sum = (values) => values.reduce((acc, v) => acc + (parseFloat(v || "0") || 0), 0);
1005
+ const toStr = (n) => Number.isFinite(n) ? n.toString() : "0";
959
1006
  const assetPositions = states.flatMap((s) => s.assetPositions || []);
960
1007
  const crossMaintenanceMarginUsed = toStr(sum(states.map((s) => s.crossMaintenanceMarginUsed)));
961
1008
  const crossMarginSummary = {
@@ -985,26 +1032,26 @@ const useHyperliquidNativeWebSocket = ({ address, enabled = true, onUserFills, }
985
1032
  setClearinghouseStateReceived(true);
986
1033
  }
987
1034
  break;
988
- case 'allMids':
1035
+ case "allMids":
989
1036
  {
990
1037
  const data = response.data;
991
1038
  setAllMids(data);
992
1039
  }
993
1040
  break;
994
- case 'activeAssetData':
1041
+ case "activeAssetData":
995
1042
  {
996
1043
  const assetData = response.data;
997
1044
  upsertActiveAssetData(assetData.coin, assetData);
998
1045
  }
999
1046
  break;
1000
- case 'candle':
1047
+ case "candle":
1001
1048
  {
1002
1049
  const candleDataItem = response.data;
1003
- const symbol = candleDataItem.s || '';
1050
+ const symbol = candleDataItem.s || "";
1004
1051
  addCandleData(symbol, candleDataItem);
1005
1052
  }
1006
1053
  break;
1007
- case 'spotState':
1054
+ case "spotState":
1008
1055
  {
1009
1056
  const spotStateData = response.data;
1010
1057
  if (spotStateData === null || spotStateData === void 0 ? void 0 : spotStateData.spotState) {
@@ -1019,7 +1066,7 @@ const useHyperliquidNativeWebSocket = ({ address, enabled = true, onUserFills, }
1019
1066
  }
1020
1067
  catch (error) {
1021
1068
  const errorMessage = `Failed to parse message: ${error instanceof Error ? error.message : String(error)}`;
1022
- console.error('[HyperLiquid WS] Parse error:', errorMessage, 'Raw message:', event.data);
1069
+ console.error("[HyperLiquid WS] Parse error:", errorMessage, "Raw message:", event.data);
1023
1070
  setLastError(errorMessage);
1024
1071
  }
1025
1072
  }, [
@@ -1031,18 +1078,25 @@ const useHyperliquidNativeWebSocket = ({ address, enabled = true, onUserFills, }
1031
1078
  setRawClearinghouseStates,
1032
1079
  setAssetContextsByDex,
1033
1080
  setSpotState,
1034
- onUserFills,
1035
1081
  ]);
1036
1082
  const connect = useCallback(() => {
1083
+ console.log("[HyperLiquid WS] connect() called, enabled:", enabled);
1037
1084
  if (!enabled)
1038
1085
  return;
1086
+ // Clear any pending reconnect timeout
1087
+ if (reconnectTimeoutRef.current) {
1088
+ clearTimeout(reconnectTimeoutRef.current);
1089
+ reconnectTimeoutRef.current = null;
1090
+ }
1039
1091
  try {
1040
1092
  // Avoid opening multiple sockets if one is already active or connecting
1041
1093
  if (wsRef.current &&
1042
1094
  (wsRef.current.readyState === WebSocket.OPEN ||
1043
1095
  wsRef.current.readyState === WebSocket.CONNECTING)) {
1096
+ console.log('[HyperLiquid WS] connect() returning early - socket already exists, readyState:', wsRef.current.readyState);
1044
1097
  return;
1045
1098
  }
1099
+ console.log("[HyperLiquid WS] Creating new WebSocket connection");
1046
1100
  manualCloseRef.current = false;
1047
1101
  setReadyState(ReadyState.CONNECTING);
1048
1102
  const ws = new WebSocket("wss://api.hyperliquid.xyz/ws");
@@ -1059,12 +1113,17 @@ const useHyperliquidNativeWebSocket = ({ address, enabled = true, onUserFills, }
1059
1113
  };
1060
1114
  ws.onclose = () => {
1061
1115
  setReadyState(ReadyState.CLOSED);
1062
- if (!manualCloseRef.current && reconnectAttemptsRef.current < 5) {
1116
+ // Reset subscription state so effects will resubscribe on reconnect
1117
+ setSubscribedAddress(null);
1118
+ setSubscribedTokens([]);
1119
+ setSubscribedCandleTokens([]);
1120
+ setClearinghouseStateReceived(false);
1121
+ if (!manualCloseRef.current) {
1063
1122
  reconnectAttemptsRef.current += 1;
1064
- if (reconnectAttemptsRef.current === 5) {
1065
- console.error('[HyperLiquid WS] Reconnection stopped after 5 attempts');
1066
- }
1067
- setTimeout(() => connect(), 3000);
1123
+ // Exponential backoff: 1s, 2s, 4s, 8s, 16s, 30s (max), then stay at 30s
1124
+ const delay = Math.min(1000 * Math.pow(2, reconnectAttemptsRef.current - 1), 30000);
1125
+ console.log(`[HyperLiquid WS] Reconnecting in ${delay}ms (attempt ${reconnectAttemptsRef.current})`);
1126
+ reconnectTimeoutRef.current = setTimeout(() => connect(), delay);
1068
1127
  }
1069
1128
  };
1070
1129
  }
@@ -1073,9 +1132,55 @@ const useHyperliquidNativeWebSocket = ({ address, enabled = true, onUserFills, }
1073
1132
  }
1074
1133
  }, [handleMessage, enabled]);
1075
1134
  useEffect(() => {
1135
+ console.log('[HyperLiquid WS] Connection effect running - calling connect()');
1076
1136
  connect();
1137
+ // Handle online/offline events to reconnect when internet is restored
1138
+ const handleOnline = () => {
1139
+ console.log('[HyperLiquid WS] Browser went online, attempting reconnect');
1140
+ // Reset reconnect attempts when internet comes back
1141
+ reconnectAttemptsRef.current = 0;
1142
+ // Clear any pending reconnect timeout
1143
+ if (reconnectTimeoutRef.current) {
1144
+ clearTimeout(reconnectTimeoutRef.current);
1145
+ reconnectTimeoutRef.current = null;
1146
+ }
1147
+ // Reset subscription state so effects will resubscribe on reconnect
1148
+ setSubscribedAddress(null);
1149
+ setSubscribedTokens([]);
1150
+ setSubscribedCandleTokens([]);
1151
+ setClearinghouseStateReceived(false);
1152
+ // Close existing socket if in a bad state
1153
+ if (wsRef.current &&
1154
+ wsRef.current.readyState !== WebSocket.OPEN &&
1155
+ wsRef.current.readyState !== WebSocket.CONNECTING) {
1156
+ try {
1157
+ wsRef.current.close();
1158
+ }
1159
+ catch (_a) { }
1160
+ wsRef.current = null;
1161
+ }
1162
+ // Attempt to reconnect
1163
+ connect();
1164
+ };
1165
+ const handleOffline = () => {
1166
+ console.log('[HyperLiquid WS] Browser went offline');
1167
+ // Clear pending reconnect timeout since we're offline
1168
+ if (reconnectTimeoutRef.current) {
1169
+ clearTimeout(reconnectTimeoutRef.current);
1170
+ reconnectTimeoutRef.current = null;
1171
+ }
1172
+ };
1173
+ window.addEventListener('online', handleOnline);
1174
+ window.addEventListener('offline', handleOffline);
1077
1175
  return () => {
1176
+ console.log('[HyperLiquid WS] Connection effect cleanup - closing existing connection');
1177
+ window.removeEventListener('online', handleOnline);
1178
+ window.removeEventListener('offline', handleOffline);
1078
1179
  manualCloseRef.current = true;
1180
+ if (reconnectTimeoutRef.current) {
1181
+ clearTimeout(reconnectTimeoutRef.current);
1182
+ reconnectTimeoutRef.current = null;
1183
+ }
1079
1184
  if (wsRef.current) {
1080
1185
  try {
1081
1186
  wsRef.current.close();
@@ -1114,14 +1219,23 @@ const useHyperliquidNativeWebSocket = ({ address, enabled = true, onUserFills, }
1114
1219
  }, [isConnected, sendJsonMessage]);
1115
1220
  // Handle address subscription changes
1116
1221
  useEffect(() => {
1117
- if (!isConnected)
1118
- return;
1119
1222
  const DEFAULT_ADDRESS = "0x0000000000000000000000000000000000000000";
1120
- const userAddress = address || DEFAULT_ADDRESS;
1121
- if (subscribedAddress === userAddress)
1223
+ const userAddress = (address || DEFAULT_ADDRESS).toLowerCase();
1224
+ const normalizedSubscribedAddress = (subscribedAddress === null || subscribedAddress === void 0 ? void 0 : subscribedAddress.toLowerCase()) || null;
1225
+ console.log("[HyperLiquid WS] Address subscription effect running");
1226
+ console.log("[HyperLiquid WS] address:", address, "userAddress:", userAddress, "subscribedAddress:", subscribedAddress, "normalizedSubscribedAddress:", normalizedSubscribedAddress);
1227
+ console.log("[HyperLiquid WS] isConnected:", isConnected);
1228
+ if (normalizedSubscribedAddress === userAddress) {
1229
+ console.log("[HyperLiquid WS] Address unchanged, skipping subscription update");
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(() => {
@@ -1453,8 +1606,9 @@ const getAssetByName = (tokenMetadata, symbol) => {
1453
1606
  * Hook to access webData
1454
1607
  */
1455
1608
  const useMarket = () => {
1609
+ const supportedMarkets = ['USDT', 'USDT0', 'USDC', 'USDH'];
1456
1610
  const tokenMetadata = useHyperliquidData((state) => state.tokenMetadata);
1457
- const allTokenMetadata = useMemo(() => Object.values(tokenMetadata).filter((metadata) => Boolean(metadata)), [tokenMetadata]);
1611
+ const allTokenMetadata = useMemo(() => Object.values(tokenMetadata).filter((metadata) => Boolean(metadata)).filter((token) => { var _a; return supportedMarkets.includes((_a = token.collateralToken) !== null && _a !== void 0 ? _a : ''); }), [tokenMetadata]);
1458
1612
  const getAssetByName$1 = useCallback((symbol) => getAssetByName(tokenMetadata, symbol), [tokenMetadata]);
1459
1613
  return { allTokenMetadata, getAssetByName: getAssetByName$1 };
1460
1614
  };
@@ -1469,17 +1623,33 @@ const useTradeHistories = () => {
1469
1623
  const enrichedTradeHistories = useMemo(() => {
1470
1624
  if (!tradeHistories)
1471
1625
  return null;
1472
- return tradeHistories.map((history) => ({
1473
- ...history,
1474
- closedLongAssets: history.closedLongAssets.map((asset) => ({
1475
- ...asset,
1476
- metadata: getAssetByName(asset.coin),
1477
- })),
1478
- closedShortAssets: history.closedShortAssets.map((asset) => ({
1479
- ...asset,
1480
- metadata: getAssetByName(asset.coin),
1481
- })),
1482
- }));
1626
+ return tradeHistories.map((history) => {
1627
+ const totalClosePositionSize = history.closedLongAssets.reduce((acc, asset) => acc + asset.limitPrice * asset.size, 0) +
1628
+ history.closedShortAssets.reduce((acc, asset) => acc + asset.limitPrice * asset.size, 0);
1629
+ return {
1630
+ ...history,
1631
+ closedLongAssets: history.closedLongAssets.map((asset) => {
1632
+ const closeNotional = asset.limitPrice * asset.size;
1633
+ return {
1634
+ ...asset,
1635
+ closeWeight: totalClosePositionSize > 0
1636
+ ? closeNotional / totalClosePositionSize
1637
+ : 0,
1638
+ metadata: getAssetByName(asset.coin),
1639
+ };
1640
+ }),
1641
+ closedShortAssets: history.closedShortAssets.map((asset) => {
1642
+ const closeNotional = asset.limitPrice * asset.size;
1643
+ return {
1644
+ ...asset,
1645
+ closeWeight: totalClosePositionSize > 0
1646
+ ? closeNotional / totalClosePositionSize
1647
+ : 0,
1648
+ metadata: getAssetByName(asset.coin),
1649
+ };
1650
+ }),
1651
+ };
1652
+ });
1483
1653
  }, [tradeHistories, getAssetByName]);
1484
1654
  const isLoading = useMemo(() => {
1485
1655
  return tradeHistories === null && context.isConnected;
@@ -1513,7 +1683,36 @@ const useOpenOrders = () => {
1513
1683
  };
1514
1684
 
1515
1685
  const useUserSelection = () => {
1516
- return useUserSelection$1();
1686
+ const longTokens = useUserSelection$1((s) => s.longTokens);
1687
+ const shortTokens = useUserSelection$1((s) => s.shortTokens);
1688
+ const candleInterval = useUserSelection$1((s) => s.candleInterval);
1689
+ const openTokenSelector = useUserSelection$1((s) => s.openTokenSelector);
1690
+ const selectorConfig = useUserSelection$1((s) => s.selectorConfig);
1691
+ const openConflictModal = useUserSelection$1((s) => s.openConflictModal);
1692
+ const setLongTokens = useUserSelection$1((s) => s.setLongTokens);
1693
+ const setShortTokens = useUserSelection$1((s) => s.setShortTokens);
1694
+ const setCandleInterval = useUserSelection$1((s) => s.setCandleInterval);
1695
+ const setTokenSelections = useUserSelection$1((s) => s.setTokenSelections);
1696
+ const setOpenTokenSelector = useUserSelection$1((s) => s.setOpenTokenSelector);
1697
+ const setSelectorConfig = useUserSelection$1((s) => s.setSelectorConfig);
1698
+ const setOpenConflictModal = useUserSelection$1((s) => s.setOpenConflictModal);
1699
+ const addToken = useUserSelection$1((s) => s.addToken);
1700
+ return {
1701
+ longTokens,
1702
+ shortTokens,
1703
+ candleInterval,
1704
+ openTokenSelector,
1705
+ selectorConfig,
1706
+ openConflictModal,
1707
+ setLongTokens,
1708
+ setShortTokens,
1709
+ setCandleInterval,
1710
+ setTokenSelections,
1711
+ setOpenTokenSelector,
1712
+ setSelectorConfig,
1713
+ setOpenConflictModal,
1714
+ addToken,
1715
+ };
1517
1716
  };
1518
1717
 
1519
1718
  const useTokenSelectionMetadataStore = create((set) => ({
@@ -1529,11 +1728,10 @@ const useTokenSelectionMetadataStore = create((set) => ({
1529
1728
  volume: "0",
1530
1729
  sumNetFunding: 0,
1531
1730
  maxLeverage: 0,
1532
- minMargin: 0,
1731
+ minSize: {},
1533
1732
  leverageMatched: true,
1534
- recompute: ({ perpMetaAssets, tokenMetadata, marketData, longTokens, shortTokens, }) => {
1733
+ recompute: ({ tokenMetadata, marketData, longTokens, shortTokens }) => {
1535
1734
  const isPriceDataReady = Object.keys(tokenMetadata).length > 0;
1536
- // Get token symbols for lookups
1537
1735
  const longSymbols = longTokens.map((t) => t.symbol);
1538
1736
  const shortSymbols = shortTokens.map((t) => t.symbol);
1539
1737
  // Get metadata
@@ -1656,24 +1854,36 @@ const useTokenSelectionMetadataStore = create((set) => ({
1656
1854
  })();
1657
1855
  // Max leverage (maximum across all tokens)
1658
1856
  const maxLeverage = (() => {
1659
- if (!perpMetaAssets)
1857
+ if (!tokenMetadata)
1660
1858
  return 0;
1661
1859
  const allSymbols = [...longSymbols, ...shortSymbols];
1662
1860
  if (allSymbols.length === 0)
1663
1861
  return 0;
1664
1862
  let maxLev = 0;
1665
1863
  allSymbols.forEach((symbol) => {
1666
- const tokenUniverse = perpMetaAssets.find((u) => u.name === symbol);
1667
- if (tokenUniverse === null || tokenUniverse === void 0 ? void 0 : tokenUniverse.maxLeverage) {
1668
- maxLev = Math.max(maxLev, tokenUniverse.maxLeverage);
1864
+ const tokenDetail = tokenMetadata[symbol];
1865
+ if (tokenDetail === null || tokenDetail === void 0 ? void 0 : tokenDetail.maxLeverage) {
1866
+ maxLev = Math.max(maxLev, tokenDetail.maxLeverage);
1669
1867
  }
1670
1868
  });
1671
1869
  return maxLev;
1672
1870
  })();
1673
- // Min margin (10 * total number of tokens)
1674
- const minMargin = (() => {
1675
- const totalTokenCount = longTokens.length + shortTokens.length;
1676
- return 10 * totalTokenCount;
1871
+ const minSize = (() => {
1872
+ const allSymbols = [...longSymbols, ...shortSymbols];
1873
+ const collateralCounts = {};
1874
+ allSymbols.forEach((symbol) => {
1875
+ var _a;
1876
+ const collateralToken = (_a = tokenMetadata[symbol]) === null || _a === void 0 ? void 0 : _a.collateralToken;
1877
+ if (collateralToken) {
1878
+ collateralCounts[collateralToken] =
1879
+ (collateralCounts[collateralToken] || 0) + 1;
1880
+ }
1881
+ });
1882
+ const minSizeMap = {};
1883
+ Object.entries(collateralCounts).forEach(([collateral, count]) => {
1884
+ minSizeMap[collateral] = 11 * count;
1885
+ });
1886
+ return minSizeMap;
1677
1887
  })();
1678
1888
  // Whether all tokens have matching leverage
1679
1889
  const leverageMatched = (() => {
@@ -1708,7 +1918,7 @@ const useTokenSelectionMetadataStore = create((set) => ({
1708
1918
  volume,
1709
1919
  sumNetFunding,
1710
1920
  maxLeverage,
1711
- minMargin,
1921
+ minSize,
1712
1922
  leverageMatched,
1713
1923
  });
1714
1924
  },
@@ -1723,11 +1933,10 @@ const useTokenSelectionMetadata = () => {
1723
1933
  const tokenMetadata = useHyperliquidData((state) => state.tokenMetadata);
1724
1934
  const marketData = useMarketData((state) => state.marketData);
1725
1935
  const { longTokens, shortTokens } = useUserSelection$1();
1726
- const { isLoading, isPriceDataReady, longTokensMetadata, shortTokensMetadata, weightedRatio, weightedRatio24h, priceRatio, priceRatio24h, openInterest, volume, sumNetFunding, maxLeverage, minMargin, leverageMatched, recompute, } = useTokenSelectionMetadataStore();
1936
+ const { isLoading, isPriceDataReady, longTokensMetadata, shortTokensMetadata, weightedRatio, weightedRatio24h, priceRatio, priceRatio24h, openInterest, volume, sumNetFunding, maxLeverage, minSize, leverageMatched, recompute, } = useTokenSelectionMetadataStore();
1727
1937
  // Recompute derived metadata when inputs change
1728
1938
  useEffect(() => {
1729
1939
  recompute({
1730
- perpMetaAssets,
1731
1940
  tokenMetadata,
1732
1941
  marketData: marketData || null,
1733
1942
  longTokens,
@@ -1751,8 +1960,8 @@ const useTokenSelectionMetadata = () => {
1751
1960
  volume,
1752
1961
  sumNetFunding,
1753
1962
  maxLeverage,
1754
- minMargin,
1755
1963
  leverageMatched,
1964
+ minSize,
1756
1965
  };
1757
1966
  };
1758
1967
 
@@ -1778,6 +1987,22 @@ const getIntervalSeconds = (interval) => {
1778
1987
  default: return 60;
1779
1988
  }
1780
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
+ };
1781
2006
  const useHistoricalPriceDataStore = create((set, get) => ({
1782
2007
  historicalPriceData: {},
1783
2008
  loadingTokens: new Set(),
@@ -1788,6 +2013,8 @@ const useHistoricalPriceDataStore = create((set, get) => ({
1788
2013
  if (!existing) {
1789
2014
  // Create new entry
1790
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;
1791
2018
  return {
1792
2019
  historicalPriceData: {
1793
2020
  ...state.historicalPriceData,
@@ -1796,7 +2023,9 @@ const useHistoricalPriceDataStore = create((set, get) => ({
1796
2023
  interval,
1797
2024
  candles: sortedCandles,
1798
2025
  oldestTime: sortedCandles.length > 0 ? sortedCandles[0].t : null,
1799
- 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
1800
2029
  }
1801
2030
  }
1802
2031
  };
@@ -1808,6 +2037,16 @@ const useHistoricalPriceDataStore = create((set, get) => ({
1808
2037
  // Update time pointers
1809
2038
  const oldestTime = mergedCandles.length > 0 ? mergedCandles[0].t : null;
1810
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
+ }
1811
2050
  return {
1812
2051
  historicalPriceData: {
1813
2052
  ...state.historicalPriceData,
@@ -1815,7 +2054,9 @@ const useHistoricalPriceDataStore = create((set, get) => ({
1815
2054
  ...existing,
1816
2055
  candles: mergedCandles,
1817
2056
  oldestTime,
1818
- latestTime
2057
+ latestTime,
2058
+ requestedRanges: mergedRanges,
2059
+ noDataBefore
1819
2060
  }
1820
2061
  }
1821
2062
  };
@@ -1825,8 +2066,24 @@ const useHistoricalPriceDataStore = create((set, get) => ({
1825
2066
  const { historicalPriceData } = get();
1826
2067
  const key = createKey(symbol, interval);
1827
2068
  const tokenData = historicalPriceData[key];
1828
- if (!tokenData || tokenData.oldestTime === null || tokenData.latestTime === null)
2069
+ if (!tokenData)
1829
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
+ }
1830
2087
  const intervalMilisecond = getIntervalSeconds(interval) * 1000;
1831
2088
  const hasStartCoverage = tokenData.oldestTime <= startTime;
1832
2089
  const hasEndCoverage = tokenData.latestTime >= endTime ||
@@ -1841,6 +2098,27 @@ const useHistoricalPriceDataStore = create((set, get) => ({
1841
2098
  return [];
1842
2099
  return tokenData.candles.filter(candle => candle.t >= startTime && candle.t < endTime);
1843
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
+ },
1844
2122
  setTokenLoading: (symbol, loading) => {
1845
2123
  set(state => {
1846
2124
  const newLoadingTokens = new Set(state.loadingTokens);
@@ -5721,10 +5999,10 @@ function toApiError(error) {
5721
5999
  var _a;
5722
6000
  const axiosError = error;
5723
6001
  const payload = (axiosError && axiosError.response ? axiosError.response.data : undefined);
5724
- const message = typeof payload === 'object' && payload && 'message' in payload
6002
+ const message = typeof payload === "object" && payload && "message" in payload
5725
6003
  ? String(payload.message)
5726
- : (axiosError === null || axiosError === void 0 ? void 0 : axiosError.message) || 'Request failed';
5727
- 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
5728
6006
  ? String(payload.error)
5729
6007
  : undefined;
5730
6008
  return {
@@ -5734,8 +6012,8 @@ function toApiError(error) {
5734
6012
  };
5735
6013
  }
5736
6014
  function joinUrl(baseUrl, path) {
5737
- const cleanBase = baseUrl.replace(/\/$/, '');
5738
- const cleanPath = path.startsWith('/') ? path : `/${path}`;
6015
+ const cleanBase = baseUrl.replace(/\/$/, "");
6016
+ const cleanPath = path.startsWith("/") ? path : `/${path}`;
5739
6017
  return `${cleanBase}${cleanPath}`;
5740
6018
  }
5741
6019
  /**
@@ -5764,7 +6042,7 @@ function addAuthInterceptors(params) {
5764
6042
  pendingRequests = [];
5765
6043
  }
5766
6044
  const isOurApiUrl = (url) => Boolean(url && url.startsWith(apiBaseUrl));
5767
- const isRefreshUrl = (url) => Boolean(url && url.startsWith(joinUrl(apiBaseUrl, '/auth/refresh')));
6045
+ const isRefreshUrl = (url) => Boolean(url && url.startsWith(joinUrl(apiBaseUrl, "/auth/refresh")));
5768
6046
  const reqId = apiClient.interceptors.request.use((config) => {
5769
6047
  var _a;
5770
6048
  try {
@@ -5772,11 +6050,12 @@ function addAuthInterceptors(params) {
5772
6050
  const token = getAccessToken();
5773
6051
  if (token) {
5774
6052
  config.headers = (_a = config.headers) !== null && _a !== void 0 ? _a : {};
5775
- (config.headers)['Authorization'] = `Bearer ${token}`;
6053
+ config.headers["Authorization"] = `Bearer ${token}`;
5776
6054
  }
5777
6055
  }
5778
6056
  }
5779
- catch (_b) {
6057
+ catch (err) {
6058
+ console.error("[Auth Interceptor] Request interceptor error:", err);
5780
6059
  }
5781
6060
  return config;
5782
6061
  });
@@ -5787,22 +6066,36 @@ function addAuthInterceptors(params) {
5787
6066
  const url = originalRequest === null || originalRequest === void 0 ? void 0 : originalRequest.url;
5788
6067
  // If not our API or not 401, just reject
5789
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
+ }
5790
6076
  return Promise.reject(error);
5791
6077
  }
6078
+ console.log("[Auth Interceptor] 401 detected, attempting token refresh for URL:", url);
5792
6079
  // If the 401 is from refresh endpoint itself -> force logout
5793
6080
  if (isRefreshUrl(url)) {
6081
+ console.warn("[Auth Interceptor] Refresh endpoint returned 401, logging out");
5794
6082
  try {
5795
6083
  await logout();
5796
6084
  }
5797
- catch (_d) { }
6085
+ catch (err) {
6086
+ console.error("[Auth Interceptor] Logout failed:", err);
6087
+ }
5798
6088
  return Promise.reject(error);
5799
6089
  }
5800
6090
  // Prevent infinite loop
5801
6091
  if (originalRequest && originalRequest._retry) {
6092
+ console.warn("[Auth Interceptor] Request already retried, logging out");
5802
6093
  try {
5803
6094
  await logout();
5804
6095
  }
5805
- catch (_e) { }
6096
+ catch (err) {
6097
+ console.error("[Auth Interceptor] Logout failed:", err);
6098
+ }
5806
6099
  return Promise.reject(error);
5807
6100
  }
5808
6101
  // Mark so we don't retry twice
@@ -5816,31 +6109,45 @@ function addAuthInterceptors(params) {
5816
6109
  if (!newToken || !originalRequest)
5817
6110
  return reject(error);
5818
6111
  originalRequest.headers = (_a = originalRequest.headers) !== null && _a !== void 0 ? _a : {};
5819
- originalRequest.headers['Authorization'] = `Bearer ${newToken}`;
6112
+ originalRequest.headers["Authorization"] =
6113
+ `Bearer ${newToken}`;
5820
6114
  resolve(apiClient.request(originalRequest));
5821
6115
  });
5822
6116
  });
5823
6117
  }
5824
6118
  isRefreshing = true;
5825
6119
  try {
6120
+ console.log("[Auth Interceptor] Refreshing tokens...");
5826
6121
  const refreshed = await refreshTokens();
5827
- 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
+ }
5828
6130
  resolvePendingRequests(newAccessToken);
5829
6131
  if (originalRequest) {
5830
6132
  originalRequest.headers = (_c = originalRequest.headers) !== null && _c !== void 0 ? _c : {};
5831
6133
  if (newAccessToken)
5832
- (originalRequest.headers)['Authorization'] = `Bearer ${newAccessToken}`;
6134
+ originalRequest.headers["Authorization"] =
6135
+ `Bearer ${newAccessToken}`;
6136
+ console.log("[Auth Interceptor] Retrying original request with new token");
5833
6137
  const resp = await apiClient.request(originalRequest);
5834
6138
  return resp;
5835
6139
  }
5836
6140
  return Promise.reject(error);
5837
6141
  }
5838
6142
  catch (refreshErr) {
6143
+ console.error("[Auth Interceptor] Token refresh failed:", refreshErr);
5839
6144
  resolvePendingRequests(null);
5840
6145
  try {
5841
6146
  await logout();
5842
6147
  }
5843
- catch (_f) { }
6148
+ catch (err) {
6149
+ console.error("[Auth Interceptor] Logout failed:", err);
6150
+ }
5844
6151
  return Promise.reject(refreshErr);
5845
6152
  }
5846
6153
  finally {
@@ -5851,11 +6158,15 @@ function addAuthInterceptors(params) {
5851
6158
  try {
5852
6159
  apiClient.interceptors.request.eject(reqId);
5853
6160
  }
5854
- catch (_a) { }
6161
+ catch (err) {
6162
+ console.error("[Auth Interceptor] Failed to eject request interceptor:", err);
6163
+ }
5855
6164
  try {
5856
6165
  apiClient.interceptors.response.eject(resId);
5857
6166
  }
5858
- catch (_b) { }
6167
+ catch (err) {
6168
+ console.error("[Auth Interceptor] Failed to eject response interceptor:", err);
6169
+ }
5859
6170
  };
5860
6171
  }
5861
6172
 
@@ -5971,7 +6282,7 @@ const useHistoricalPriceData = () => {
5971
6282
  const shortTokens = useUserSelection$1((state) => state.shortTokens);
5972
6283
  const candleInterval = useUserSelection$1((state) => state.candleInterval);
5973
6284
  // Historical price data store
5974
- 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();
5975
6286
  // Track previous tokens and interval to detect changes
5976
6287
  const prevTokensRef = useRef(new Set());
5977
6288
  const prevIntervalRef = useRef(null);
@@ -6024,6 +6335,11 @@ const useHistoricalPriceData = () => {
6024
6335
  const getAllHistoricalPriceData = useCallback(async () => {
6025
6336
  return useHistoricalPriceDataStore.getState().historicalPriceData;
6026
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]);
6027
6343
  const fetchHistoricalPriceData = useCallback(async (startTime, endTime, interval, callback) => {
6028
6344
  const allTokens = getAllTokens();
6029
6345
  if (allTokens.length === 0) {
@@ -6081,6 +6397,7 @@ const useHistoricalPriceData = () => {
6081
6397
  hasHistoricalPriceData,
6082
6398
  getAllHistoricalPriceData,
6083
6399
  getHistoricalPriceData,
6400
+ getEffectiveDataBoundary,
6084
6401
  isLoading,
6085
6402
  clearCache,
6086
6403
  };
@@ -6277,7 +6594,7 @@ const useBasketCandles = () => {
6277
6594
  const longTokens = useUserSelection$1((state) => state.longTokens);
6278
6595
  const shortTokens = useUserSelection$1((state) => state.shortTokens);
6279
6596
  const candleData = useHyperliquidData((s) => s.candleData);
6280
- const { fetchHistoricalPriceData, isLoading: tokenLoading, getAllHistoricalPriceData } = useHistoricalPriceData();
6597
+ const { fetchHistoricalPriceData, isLoading: tokenLoading, getAllHistoricalPriceData, getEffectiveDataBoundary } = useHistoricalPriceData();
6281
6598
  const fetchBasketCandles = useCallback(async (startTime, endTime, interval) => {
6282
6599
  const tokenCandles = await fetchHistoricalPriceData(startTime, endTime, interval);
6283
6600
  const basket = computeBasketCandles(longTokens, shortTokens, tokenCandles);
@@ -6477,6 +6794,7 @@ const useBasketCandles = () => {
6477
6794
  fetchBasketCandles,
6478
6795
  fetchPerformanceCandles,
6479
6796
  fetchOverallPerformanceCandles,
6797
+ getEffectiveDataBoundary,
6480
6798
  isLoading,
6481
6799
  addRealtimeListener,
6482
6800
  removeRealtimeListener,
@@ -6752,11 +7070,13 @@ async function updateLeverage(baseUrl, positionId, payload) {
6752
7070
  }
6753
7071
  }
6754
7072
 
6755
- const calculatePositionAsset = (asset, currentPrice, totalInitialPositionSize, leverage, metadata, isLong = true) => {
7073
+ const calculatePositionAsset = (asset, currentPrice, totalInitialPositionSize, totalCurrentPositionSize, leverage, metadata, isLong = true) => {
6756
7074
  var _a;
6757
7075
  const entryNotional = asset.entryPrice * asset.size;
6758
7076
  const currentNotional = currentPrice * asset.size;
6759
- const marginUsed = currentNotional / (leverage || 1);
7077
+ const effectiveLeverage = leverage || 1;
7078
+ const marginUsed = currentNotional / effectiveLeverage;
7079
+ const entryMarginUsed = entryNotional / effectiveLeverage;
6760
7080
  const unrealizedPnl = isLong
6761
7081
  ? currentNotional - entryNotional
6762
7082
  : entryNotional - currentNotional;
@@ -6766,11 +7086,14 @@ const calculatePositionAsset = (asset, currentPrice, totalInitialPositionSize, l
6766
7086
  actualSize: asset.size,
6767
7087
  leverage: leverage,
6768
7088
  marginUsed: marginUsed,
7089
+ entryMarginUsed: entryMarginUsed,
6769
7090
  positionValue: currentNotional,
6770
7091
  unrealizedPnl: unrealizedPnl,
6771
7092
  entryPositionValue: entryNotional,
6772
7093
  initialWeight: totalInitialPositionSize > 0 ? entryNotional / totalInitialPositionSize : 0,
7094
+ currentWeight: totalCurrentPositionSize > 0 ? currentNotional / totalCurrentPositionSize : 0,
6773
7095
  fundingPaid: (_a = asset.fundingPaid) !== null && _a !== void 0 ? _a : 0,
7096
+ targetWeight: asset.targetWeight,
6774
7097
  metadata,
6775
7098
  };
6776
7099
  };
@@ -6791,20 +7114,32 @@ const buildPositionValue = (rawPositions, clearinghouseState, getAssetByName) =>
6791
7114
  takeProfit: position.takeProfit,
6792
7115
  stopLoss: position.stopLoss,
6793
7116
  };
7117
+ let entryMarginUsed = 0;
6794
7118
  const totalInitialPositionSize = position.longAssets.reduce((acc, asset) => acc + asset.entryPrice * asset.size, 0) +
6795
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);
6796
7130
  mappedPosition.longAssets = position.longAssets.map((longAsset) => {
6797
7131
  var _a, _b, _c, _d;
6798
7132
  const metadata = getAssetByName(longAsset.coin);
6799
7133
  const currentPrice = (_a = metadata === null || metadata === void 0 ? void 0 : metadata.currentPrice) !== null && _a !== void 0 ? _a : 0;
6800
7134
  const assetState = (_b = clearinghouseState.assetPositions.find((ap) => ap.position.coin === longAsset.coin)) === null || _b === void 0 ? void 0 : _b.position;
6801
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;
6802
- const mappedPositionAssets = calculatePositionAsset(longAsset, currentPrice, totalInitialPositionSize, leverage, metadata, true);
7136
+ const mappedPositionAssets = calculatePositionAsset(longAsset, currentPrice, totalInitialPositionSize, totalCurrentPositionSize, leverage, metadata, true);
6803
7137
  mappedPosition.entryPositionValue +=
6804
7138
  mappedPositionAssets.entryPositionValue;
6805
7139
  mappedPosition.unrealizedPnl += mappedPositionAssets.unrealizedPnl;
6806
7140
  mappedPosition.positionValue += mappedPositionAssets.positionValue;
6807
7141
  mappedPosition.marginUsed += mappedPositionAssets.marginUsed;
7142
+ entryMarginUsed += mappedPositionAssets.entryMarginUsed;
6808
7143
  mappedPosition.entryRatio *= Math.pow(longAsset.entryPrice, mappedPositionAssets.initialWeight);
6809
7144
  mappedPosition.markRatio *= Math.pow(currentPrice, mappedPositionAssets.initialWeight);
6810
7145
  return mappedPositionAssets;
@@ -6815,14 +7150,15 @@ const buildPositionValue = (rawPositions, clearinghouseState, getAssetByName) =>
6815
7150
  const currentPrice = (_a = metadata === null || metadata === void 0 ? void 0 : metadata.currentPrice) !== null && _a !== void 0 ? _a : 0;
6816
7151
  const assetState = (_b = clearinghouseState.assetPositions.find((ap) => ap.position.coin === shortAsset.coin)) === null || _b === void 0 ? void 0 : _b.position;
6817
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;
6818
- const mappedPositionAssets = calculatePositionAsset(shortAsset, currentPrice, totalInitialPositionSize, leverage, metadata, false);
7153
+ const mappedPositionAssets = calculatePositionAsset(shortAsset, currentPrice, totalInitialPositionSize, totalCurrentPositionSize, leverage, metadata, false);
6819
7154
  mappedPosition.entryPositionValue +=
6820
7155
  mappedPositionAssets.entryPositionValue;
6821
7156
  mappedPosition.unrealizedPnl += mappedPositionAssets.unrealizedPnl;
6822
7157
  mappedPosition.positionValue += mappedPositionAssets.positionValue;
6823
7158
  mappedPosition.marginUsed += mappedPositionAssets.marginUsed;
7159
+ entryMarginUsed += mappedPositionAssets.entryMarginUsed;
6824
7160
  mappedPosition.entryRatio *= Math.pow(shortAsset.entryPrice, -mappedPositionAssets.initialWeight);
6825
- mappedPosition.markRatio *= Math.pow(currentPrice, -mappedPositionAssets.initialWeight);
7161
+ mappedPosition.markRatio *= Math.pow(currentPrice, -mappedPositionAssets.currentWeight);
6826
7162
  return mappedPositionAssets;
6827
7163
  });
6828
7164
  mappedPosition.positionValue =
@@ -6858,11 +7194,12 @@ const buildPositionValue = (rawPositions, clearinghouseState, getAssetByName) =>
6858
7194
  mappedPosition.markPriceRatio = shortMark;
6859
7195
  }
6860
7196
  mappedPosition.unrealizedPnlPercentage =
6861
- mappedPosition.unrealizedPnl / mappedPosition.marginUsed;
7197
+ entryMarginUsed > 0 ? mappedPosition.unrealizedPnl / entryMarginUsed : 0;
6862
7198
  return mappedPosition;
6863
7199
  });
6864
7200
  };
6865
7201
 
7202
+ const MIN_TRADE_SIZE_USD = 11;
6866
7203
  function usePosition() {
6867
7204
  const context = useContext(PearHyperliquidContext);
6868
7205
  if (!context) {
@@ -6905,6 +7242,85 @@ function usePosition() {
6905
7242
  return null;
6906
7243
  return buildPositionValue(userOpenPositions, aggregatedClearingHouseState, getAssetByName);
6907
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
+ };
6908
7324
  return {
6909
7325
  createPosition: createPosition$1,
6910
7326
  updateRiskParameters: updateRiskParameters$1,
@@ -6915,6 +7331,8 @@ function usePosition() {
6915
7331
  updateLeverage: updateLeverage$1,
6916
7332
  openPositions,
6917
7333
  isLoading,
7334
+ planRebalance,
7335
+ executeRebalance,
6918
7336
  };
6919
7337
  }
6920
7338
 
@@ -7197,122 +7615,62 @@ function useNotifications() {
7197
7615
  };
7198
7616
  }
7199
7617
 
7200
- // Convert a basket item to the expected type
7201
- function enrichBasketItem(item) {
7202
- const enrichedLongs = item.longAssets.map((a) => ({
7203
- ...a,
7204
- }));
7205
- const enrichedShorts = item.shortAssets.map((a) => ({
7206
- ...a,
7207
- }));
7618
+ function enrichBasketWithMetadata(basket, tokenMetadata) {
7208
7619
  return {
7209
- ...item,
7210
- longAssets: enrichedLongs,
7211
- shortAssets: enrichedShorts,
7212
- collateralType: 'USDC', //TODO: change
7620
+ ...basket,
7621
+ longAssets: basket.longAssets.map((asset) => {
7622
+ var _a;
7623
+ return ({
7624
+ ...asset,
7625
+ metadata: (_a = tokenMetadata[asset.asset]) !== null && _a !== void 0 ? _a : null,
7626
+ });
7627
+ }),
7628
+ shortAssets: basket.shortAssets.map((asset) => {
7629
+ var _a;
7630
+ return ({
7631
+ ...asset,
7632
+ metadata: (_a = tokenMetadata[asset.asset]) !== null && _a !== void 0 ? _a : null,
7633
+ });
7634
+ }),
7213
7635
  };
7214
7636
  }
7215
- /**
7216
- * Filter baskets by collateral type
7217
- * - 'USDC': Only baskets where ALL assets use USDC (collateralType === 'USDC')
7218
- * - 'USDH': Only baskets where ALL assets use USDH (collateralType === 'USDH')
7219
- * - 'ALL' or undefined: No filtering, returns all baskets
7220
- */
7221
- function filterByCollateral(baskets, filter) {
7222
- if (!filter || filter === 'ALL') {
7223
- return baskets;
7224
- }
7225
- return baskets.filter((basket) => {
7226
- if (filter === 'USDC') {
7227
- // Include baskets that are purely USDC or have USDC assets
7228
- return (basket.collateralType === 'USDC' || basket.collateralType === 'MIXED');
7229
- }
7230
- if (filter === 'USDH') {
7231
- // Include baskets that are purely USDH or have USDH assets
7232
- return (basket.collateralType === 'USDH' || basket.collateralType === 'MIXED');
7233
- }
7234
- return true;
7235
- });
7637
+ function enrichBasketsWithMetadata(baskets, tokenMetadata) {
7638
+ return baskets.map((basket) => enrichBasketWithMetadata(basket, tokenMetadata));
7236
7639
  }
7237
- // Base selector for the full market-data payload (raw from WS)
7238
- const useMarketDataPayload = () => {
7239
- return useMarketData((s) => s.marketData);
7240
- };
7241
- // Active baskets
7242
- const useActiveBaskets = (collateralFilter) => {
7243
- const data = useMarketDataPayload();
7244
- return useMemo(() => {
7245
- if (!(data === null || data === void 0 ? void 0 : data.active))
7246
- return [];
7247
- const enriched = data.active.map((item) => enrichBasketItem(item));
7248
- return filterByCollateral(enriched, collateralFilter);
7249
- }, [data, collateralFilter]);
7250
- };
7251
- // Top gainers
7252
- const useTopGainers = (limit, collateralFilter) => {
7253
- const data = useMarketDataPayload();
7254
- return useMemo(() => {
7255
- var _a;
7256
- const list = (_a = data === null || data === void 0 ? void 0 : data.topGainers) !== null && _a !== void 0 ? _a : [];
7257
- const limited = typeof limit === 'number' ? list.slice(0, Math.max(0, limit)) : list;
7258
- const enriched = limited.map((item) => enrichBasketItem(item));
7259
- return filterByCollateral(enriched, collateralFilter);
7260
- }, [data, limit, collateralFilter]);
7261
- };
7262
- // Top losers
7263
- const useTopLosers = (limit, collateralFilter) => {
7264
- const data = useMarketDataPayload();
7265
- return useMemo(() => {
7266
- var _a;
7267
- const list = (_a = data === null || data === void 0 ? void 0 : data.topLosers) !== null && _a !== void 0 ? _a : [];
7268
- const limited = typeof limit === 'number' ? list.slice(0, Math.max(0, limit)) : list;
7269
- const enriched = limited.map((item) => enrichBasketItem(item));
7270
- return filterByCollateral(enriched, collateralFilter);
7271
- }, [data, limit, collateralFilter]);
7272
- };
7273
- // Highlighted baskets
7274
- const useHighlightedBaskets = (collateralFilter) => {
7275
- const data = useMarketDataPayload();
7276
- return useMemo(() => {
7277
- if (!(data === null || data === void 0 ? void 0 : data.highlighted))
7278
- return [];
7279
- const enriched = data.highlighted.map((item) => enrichBasketItem(item));
7280
- return filterByCollateral(enriched, collateralFilter);
7281
- }, [data, collateralFilter]);
7282
- };
7283
- // Watchlist baskets
7284
- const useWatchlistBaskets = (collateralFilter) => {
7285
- const data = useMarketDataPayload();
7640
+ const useMarketDataHook = (options = {}) => {
7641
+ const { topGainersLimit, topLosersLimit } = options;
7642
+ const data = useMarketData((s) => s.marketData);
7643
+ const tokenMetadata = useHyperliquidData((s) => s.tokenMetadata);
7286
7644
  return useMemo(() => {
7287
- if (!(data === null || data === void 0 ? void 0 : data.watchlist))
7288
- return [];
7289
- const enriched = data.watchlist.map((item) => enrichBasketItem(item));
7290
- return filterByCollateral(enriched, collateralFilter);
7291
- }, [data, collateralFilter]);
7292
- };
7293
- // Find a basket by its exact asset composition (order-insensitive)
7294
- const useFindBasket = (longs, shorts) => {
7295
- const data = useMarketDataPayload();
7296
- return useMemo(() => {
7297
- if (!data)
7298
- return undefined;
7299
- const normalize = (arr) => Array.isArray(arr)
7300
- ? arr
7301
- .map((v) => (typeof v === 'string' ? v : v === null || v === void 0 ? void 0 : v.asset))
7302
- .filter(Boolean)
7303
- .map((s) => s.toUpperCase())
7304
- .sort()
7305
- .join(',')
7306
- : '';
7307
- const lKey = normalize(longs);
7308
- const sKey = normalize(shorts);
7309
- const match = (item) => normalize(item.longAssets) === lKey &&
7310
- normalize(item.shortAssets) === sKey;
7311
- const found = data.active.find(match) || data.highlighted.find(match);
7312
- return found
7313
- ? enrichBasketItem(found)
7314
- : undefined;
7315
- }, [data, longs, shorts]);
7645
+ var _a, _b, _c;
7646
+ const activeBaskets = enrichBasketsWithMetadata((_a = data === null || data === void 0 ? void 0 : data.active) !== null && _a !== void 0 ? _a : [], tokenMetadata);
7647
+ const topGainers = (() => {
7648
+ var _a;
7649
+ const list = (_a = data === null || data === void 0 ? void 0 : data.topGainers) !== null && _a !== void 0 ? _a : [];
7650
+ const limited = typeof topGainersLimit === 'number'
7651
+ ? list.slice(0, Math.max(0, topGainersLimit))
7652
+ : list;
7653
+ return enrichBasketsWithMetadata(limited, tokenMetadata);
7654
+ })();
7655
+ const topLosers = (() => {
7656
+ var _a;
7657
+ const list = (_a = data === null || data === void 0 ? void 0 : data.topLosers) !== null && _a !== void 0 ? _a : [];
7658
+ const limited = typeof topLosersLimit === 'number'
7659
+ ? list.slice(0, Math.max(0, topLosersLimit))
7660
+ : list;
7661
+ return enrichBasketsWithMetadata(limited, tokenMetadata);
7662
+ })();
7663
+ const highlightedBaskets = enrichBasketsWithMetadata((_b = data === null || data === void 0 ? void 0 : data.highlighted) !== null && _b !== void 0 ? _b : [], tokenMetadata);
7664
+ const watchlistBaskets = enrichBasketsWithMetadata((_c = data === null || data === void 0 ? void 0 : data.watchlist) !== null && _c !== void 0 ? _c : [], tokenMetadata);
7665
+ return {
7666
+ marketData: data,
7667
+ activeBaskets,
7668
+ topGainers,
7669
+ topLosers,
7670
+ highlightedBaskets,
7671
+ watchlistBaskets,
7672
+ };
7673
+ }, [data, tokenMetadata, topGainersLimit, topLosersLimit]);
7316
7674
  };
7317
7675
 
7318
7676
  async function toggleWatchlist(baseUrl, longAssets, shortAssets) {
@@ -7338,7 +7696,7 @@ function useWatchlist() {
7338
7696
  if (!context)
7339
7697
  throw new Error('useWatchlist must be used within a PearHyperliquidProvider');
7340
7698
  const { apiBaseUrl, isConnected } = context;
7341
- const marketData = useMarketDataPayload();
7699
+ const marketData = useMarketData(s => s.marketData);
7342
7700
  const isLoading = useMemo(() => !marketData && isConnected, [marketData, isConnected]);
7343
7701
  const toggle = async (longAssets, shortAssets) => {
7344
7702
  const resp = await toggleWatchlist(apiBaseUrl, longAssets, shortAssets);
@@ -7408,20 +7766,34 @@ function usePortfolio() {
7408
7766
  }
7409
7767
 
7410
7768
  async function getEIP712Message(baseUrl, address, clientId) {
7411
- const url = joinUrl(baseUrl, '/auth/eip712-message');
7769
+ const url = joinUrl(baseUrl, "/auth/eip712-message");
7412
7770
  try {
7413
- const resp = await axios$1.get(url, { params: { address, clientId }, timeout: 30000 });
7414
- 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
+ };
7415
7780
  }
7416
7781
  catch (error) {
7417
7782
  throw toApiError(error);
7418
7783
  }
7419
7784
  }
7420
7785
  async function authenticate(baseUrl, body) {
7421
- const url = joinUrl(baseUrl, '/auth/login');
7786
+ const url = joinUrl(baseUrl, "/auth/login");
7422
7787
  try {
7423
- const resp = await axios$1.post(url, body, { headers: { 'Content-Type': 'application/json' }, timeout: 30000 });
7424
- 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
+ };
7425
7797
  }
7426
7798
  catch (error) {
7427
7799
  throw toApiError(error);
@@ -7432,7 +7804,7 @@ async function authenticate(baseUrl, body) {
7432
7804
  */
7433
7805
  async function authenticateWithPrivy(baseUrl, params) {
7434
7806
  const body = {
7435
- method: 'privy_access_token',
7807
+ method: "privy_access_token",
7436
7808
  address: params.address,
7437
7809
  clientId: params.clientId,
7438
7810
  details: { appId: params.appId, accessToken: params.accessToken },
@@ -7440,61 +7812,124 @@ async function authenticateWithPrivy(baseUrl, params) {
7440
7812
  return authenticate(baseUrl, body);
7441
7813
  }
7442
7814
  async function refreshToken(baseUrl, refreshTokenVal) {
7443
- const url = joinUrl(baseUrl, '/auth/refresh');
7815
+ const url = joinUrl(baseUrl, "/auth/refresh");
7444
7816
  try {
7445
- const resp = await axios$1.post(url, { refreshToken: refreshTokenVal }, { headers: { 'Content-Type': 'application/json' }, timeout: 30000 });
7446
- 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
+ };
7447
7823
  }
7448
7824
  catch (error) {
7449
7825
  throw toApiError(error);
7450
7826
  }
7451
7827
  }
7452
7828
  async function logout(baseUrl, refreshTokenVal) {
7453
- const url = joinUrl(baseUrl, '/auth/logout');
7829
+ const url = joinUrl(baseUrl, "/auth/logout");
7454
7830
  try {
7455
- const resp = await axios$1.post(url, { refreshToken: refreshTokenVal }, { headers: { 'Content-Type': 'application/json' }, timeout: 30000 });
7456
- 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
+ };
7457
7837
  }
7458
7838
  catch (error) {
7459
7839
  throw toApiError(error);
7460
7840
  }
7461
7841
  }
7462
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
+ }
7463
7852
  function useAuth() {
7464
7853
  const context = useContext(PearHyperliquidContext);
7465
7854
  if (!context) {
7466
- throw new Error("usePortfolio must be used within a PearHyperliquidProvider");
7855
+ throw new Error('useAuth must be used within a PearHyperliquidProvider');
7467
7856
  }
7468
7857
  const { apiBaseUrl, clientId } = context;
7469
- const [isReady, setIsReady] = useState(false);
7470
7858
  const accessToken = useUserData((s) => s.accessToken);
7471
7859
  const refreshToken$1 = useUserData((s) => s.refreshToken);
7860
+ const isReady = useUserData((s) => s.isReady);
7861
+ const isAuthenticated = useUserData((s) => s.isAuthenticated);
7472
7862
  const setAccessToken = useUserData((s) => s.setAccessToken);
7473
7863
  const setRefreshToken = useUserData((s) => s.setRefreshToken);
7474
- const isAuthenticated = useUserData((s) => s.isAuthenticated);
7864
+ const setIsReady = useUserData((s) => s.setIsReady);
7475
7865
  const setIsAuthenticated = useUserData((s) => s.setIsAuthenticated);
7866
+ const address = useUserData((s) => s.address);
7476
7867
  const setAddress = useUserData((s) => s.setAddress);
7868
+ // Ref to prevent concurrent refresh attempts
7869
+ const isRefreshingRef = useRef(false);
7477
7870
  useEffect(() => {
7478
- if (typeof window == "undefined") {
7871
+ if (typeof window == 'undefined') {
7479
7872
  return;
7480
7873
  }
7481
- const access = localStorage.getItem("accessToken");
7482
- const refresh = localStorage.getItem("refreshToken");
7483
- const addr = localStorage.getItem("address");
7484
- setAccessToken(access);
7485
- setRefreshToken(refresh);
7486
- setAddress(addr);
7487
- const authed = Boolean(access && addr);
7488
- setIsAuthenticated(authed);
7489
- setIsReady(true);
7490
- }, [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]);
7491
7925
  useEffect(() => {
7492
7926
  const cleanup = addAuthInterceptors({
7493
7927
  apiBaseUrl,
7494
7928
  getAccessToken: () => {
7495
- return typeof window !== "undefined"
7496
- ? window.localStorage.getItem("accessToken")
7497
- : null;
7929
+ if (typeof window === 'undefined')
7930
+ return null;
7931
+ // Read from Zustand state as single source of truth
7932
+ return useUserData.getState().accessToken;
7498
7933
  },
7499
7934
  refreshTokens: async () => {
7500
7935
  const data = await refreshTokens();
@@ -7508,6 +7943,55 @@ function useAuth() {
7508
7943
  cleanup();
7509
7944
  };
7510
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]);
7511
7995
  async function getEip712(address) {
7512
7996
  const { data } = await getEIP712Message(apiBaseUrl, address, clientId);
7513
7997
  return data;
@@ -7515,17 +7999,21 @@ function useAuth() {
7515
7999
  async function loginWithSignedMessage(address, signature, timestamp) {
7516
8000
  try {
7517
8001
  const { data } = await authenticate(apiBaseUrl, {
7518
- method: "eip712",
8002
+ method: 'eip712',
7519
8003
  address,
7520
8004
  clientId,
7521
8005
  details: { signature, timestamp },
7522
8006
  });
7523
- window.localStorage.setItem("accessToken", data.accessToken);
7524
- window.localStorage.setItem("refreshToken", data.refreshToken);
7525
- 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));
7526
8015
  setAccessToken(data.accessToken);
7527
8016
  setRefreshToken(data.refreshToken);
7528
- setAddress(address);
7529
8017
  setIsAuthenticated(true);
7530
8018
  }
7531
8019
  catch (e) {
@@ -7540,12 +8028,16 @@ function useAuth() {
7540
8028
  appId,
7541
8029
  accessToken: privyAccessToken,
7542
8030
  });
7543
- window.localStorage.setItem("accessToken", data.accessToken);
7544
- window.localStorage.setItem("refreshToken", data.refreshToken);
7545
- 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));
7546
8039
  setAccessToken(data.accessToken);
7547
8040
  setRefreshToken(data.refreshToken);
7548
- setAddress(address);
7549
8041
  setIsAuthenticated(true);
7550
8042
  }
7551
8043
  catch (e) {
@@ -7553,38 +8045,60 @@ function useAuth() {
7553
8045
  }
7554
8046
  }
7555
8047
  async function refreshTokens() {
7556
- const refresh = window.localStorage.getItem("refreshToken");
7557
- if (!refresh)
7558
- throw new Error("No refresh token");
7559
- const { data } = await refreshToken(apiBaseUrl, refresh);
7560
- window.localStorage.setItem("accessToken", data.accessToken);
7561
- 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));
7562
8062
  setAccessToken(data.accessToken);
7563
8063
  setRefreshToken(data.refreshToken);
7564
8064
  setIsAuthenticated(true);
7565
8065
  return data;
7566
8066
  }
7567
8067
  async function logout$1() {
7568
- const refresh = window.localStorage.getItem("refreshToken");
7569
- if (refresh) {
8068
+ const currentAddress = address;
8069
+ const currentRefresh = refreshToken$1;
8070
+ if (currentRefresh) {
7570
8071
  try {
7571
- await logout(apiBaseUrl, refresh);
8072
+ await logout(apiBaseUrl, currentRefresh);
7572
8073
  }
7573
8074
  catch (_a) {
7574
8075
  /* ignore */
7575
8076
  }
7576
8077
  }
7577
- window.localStorage.removeItem("accessToken");
7578
- window.localStorage.removeItem("refreshToken");
7579
- 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
+ }
7580
8088
  setAccessToken(null);
7581
8089
  setRefreshToken(null);
7582
8090
  setAddress(null);
7583
8091
  setIsAuthenticated(false);
7584
8092
  }
8093
+ function clearSession() {
8094
+ setAccessToken(null);
8095
+ setRefreshToken(null);
8096
+ setIsAuthenticated(false);
8097
+ }
7585
8098
  return {
7586
8099
  isReady,
7587
8100
  isAuthenticated,
8101
+ address,
7588
8102
  accessToken,
7589
8103
  refreshToken: refreshToken$1,
7590
8104
  getEip712,
@@ -7592,109 +8106,188 @@ function useAuth() {
7592
8106
  loginWithPrivyToken,
7593
8107
  refreshTokens,
7594
8108
  logout: logout$1,
8109
+ clearSession,
8110
+ setAddress,
7595
8111
  };
7596
8112
  }
7597
8113
 
7598
8114
  const useAllUserBalances = () => {
7599
8115
  const spotState = useUserData((state) => state.spotState);
7600
- const aggregatedClearingHouseState = useHyperliquidData((state) => state.aggregatedClearingHouseState);
7601
- const rawClearinghouseStates = useHyperliquidData((state) => state.rawClearinghouseStates);
7602
8116
  const { longTokensMetadata, shortTokensMetadata } = useTokenSelectionMetadata();
7603
- return useMemo(() => {
7604
- const isLoading = !spotState || !aggregatedClearingHouseState;
7605
- // Helper function to truncate to 2 decimal places without rounding
7606
- const truncateToTwoDecimals = (value) => {
7607
- return Math.floor(value * 100) / 100;
7608
- };
7609
- // Get spot balances from spotState
7610
- let spotUsdcBal = undefined;
7611
- let spotUsdhBal = undefined;
7612
- if (spotState) {
7613
- const balances = spotState.balances || [];
7614
- for (const balance of balances) {
8117
+ const { longTokens, shortTokens } = useUserSelection$1();
8118
+ const aggregatedClearingHouseState = useHyperliquidData((state) => state.aggregatedClearingHouseState);
8119
+ const { spotBalances, availableToTrades, isLoading } = useMemo(() => {
8120
+ const isLoading = !spotState;
8121
+ const spotBalances = {};
8122
+ const availableToTrades = {};
8123
+ if (spotState === null || spotState === void 0 ? void 0 : spotState.balances) {
8124
+ for (const balance of spotState.balances) {
7615
8125
  const total = parseFloat(balance.total || '0');
7616
- if (balance.coin === 'USDC') {
7617
- spotUsdcBal = truncateToTwoDecimals(total);
7618
- }
7619
- if (balance.coin === 'USDH') {
7620
- spotUsdhBal = truncateToTwoDecimals(total);
7621
- }
8126
+ spotBalances[balance.coin] = total;
7622
8127
  }
7623
8128
  }
7624
- // Get available to trade from tokenMetadata for both USDC and USDH markets
7625
- let availableToTradeUsdcFromAsset = 0;
7626
- let availableToTradeUsdhFromAsset = 0;
7627
- // Token metadata only contains availableToTrade for SELECTED tokens (user's long and short Tokens)
7628
- // It does NOT contain data for all tokens, so we cannot reliably use it for available to trade as used on hl trade page
7629
- // so intead, we rely on rawClearinghouseStates which provides market-specific data
7630
- const selectedMetadataEntries = [
7631
- ...Object.entries(longTokensMetadata),
7632
- ...Object.entries(shortTokensMetadata),
8129
+ const allMetadataWithSource = [
8130
+ ...Object.values(longTokensMetadata).map((m) => ({
8131
+ metadata: m,
8132
+ isLong: true,
8133
+ })),
8134
+ ...Object.values(shortTokensMetadata).map((m) => ({
8135
+ metadata: m,
8136
+ isLong: false,
8137
+ })),
7633
8138
  ];
7634
- selectedMetadataEntries.forEach(([symbol, metadata]) => {
7635
- var _a;
7636
- const availableStr = (_a = metadata === null || metadata === void 0 ? void 0 : metadata.availableToTrade) === null || _a === void 0 ? void 0 : _a[0];
7637
- if (!availableStr)
7638
- return;
7639
- const availableValue = truncateToTwoDecimals(parseFloat(availableStr || '0'));
7640
- if ((metadata === null || metadata === void 0 ? void 0 : metadata.collateralToken) === 'USDH') {
7641
- availableToTradeUsdhFromAsset = Math.max(availableToTradeUsdhFromAsset, availableValue);
7642
- return;
7643
- }
7644
- if ((metadata === null || metadata === void 0 ? void 0 : metadata.collateralToken) === 'USDC') {
7645
- availableToTradeUsdcFromAsset = Math.max(availableToTradeUsdcFromAsset, availableValue);
7646
- return;
7647
- }
7648
- if (symbol.includes(':')) {
7649
- const prefix = symbol.split(':')[0];
7650
- if (prefix === 'xyz') {
7651
- availableToTradeUsdcFromAsset = Math.max(availableToTradeUsdcFromAsset, availableValue);
7652
- }
7653
- else {
7654
- availableToTradeUsdhFromAsset = Math.max(availableToTradeUsdhFromAsset, availableValue);
7655
- }
7656
- return;
7657
- }
7658
- availableToTradeUsdcFromAsset = Math.max(availableToTradeUsdcFromAsset, availableValue);
7659
- });
7660
- // Calculate USDC available to trade
7661
- // Priority 1: Use value from activeAssetData if available (> 0)
7662
- // Priority 2: Calculate from USDC-specific clearinghouseState (empty prefix)
7663
- let availableToTradeUsdcValue = undefined;
7664
- if (availableToTradeUsdcFromAsset > 0) {
7665
- availableToTradeUsdcValue = availableToTradeUsdcFromAsset;
7666
- }
7667
- else if (rawClearinghouseStates) {
7668
- // Find USDC market (empty prefix)
7669
- const usdcMarket = rawClearinghouseStates.find(([prefix]) => prefix === '');
7670
- const usdcState = usdcMarket === null || usdcMarket === void 0 ? void 0 : usdcMarket[1];
7671
- if (usdcState === null || usdcState === void 0 ? void 0 : usdcState.marginSummary) {
7672
- const accountValue = parseFloat(usdcState.marginSummary.accountValue || '0');
7673
- const totalMarginUsed = parseFloat(usdcState.marginSummary.totalMarginUsed || '0');
7674
- const calculatedValue = Math.max(0, accountValue - totalMarginUsed);
7675
- availableToTradeUsdcValue = truncateToTwoDecimals(calculatedValue);
8139
+ for (const { metadata, isLong } of allMetadataWithSource) {
8140
+ if (!(metadata === null || metadata === void 0 ? void 0 : metadata.collateralToken) || !(metadata === null || metadata === void 0 ? void 0 : metadata.availableToTrade))
8141
+ continue;
8142
+ const collateralCoin = metadata.collateralToken;
8143
+ const availableToTrade = metadata.availableToTrade;
8144
+ let value = parseFloat(availableToTrade[isLong ? 0 : 1] || '0');
8145
+ if (!(collateralCoin in availableToTrades) ||
8146
+ value < availableToTrades[collateralCoin]) {
8147
+ availableToTrades[collateralCoin] = value;
7676
8148
  }
7677
8149
  }
7678
- // Calculate USDH available to trade
7679
- // Priority 1: Use value from activeAssetData if available (> 0)
7680
- // Priority 2: Use spot USDH balance
7681
- const availableToTradeUsdhValue = availableToTradeUsdhFromAsset > 0
7682
- ? availableToTradeUsdhFromAsset
7683
- : spotUsdhBal;
8150
+ if (!availableToTrades['USDC']) {
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'));
8153
+ }
7684
8154
  return {
7685
- spotUsdcBalance: spotUsdcBal,
7686
- availableToTradeUsdc: availableToTradeUsdcValue,
7687
- spotUsdhBalance: spotUsdhBal,
7688
- availableToTradeUsdh: availableToTradeUsdhValue,
8155
+ spotBalances,
8156
+ availableToTrades,
7689
8157
  isLoading,
7690
8158
  };
7691
8159
  }, [
7692
8160
  spotState,
8161
+ longTokensMetadata,
8162
+ shortTokensMetadata,
7693
8163
  aggregatedClearingHouseState,
7694
- rawClearinghouseStates,
8164
+ ]);
8165
+ /**
8166
+ * Calculate margin required for every collateral token based on asset leverages and size.
8167
+ * Returns margin required per collateral and whether there's sufficient margin.
8168
+ */
8169
+ const getMarginRequired = useCallback((assetsLeverage, size) => {
8170
+ const sizeValue = parseFloat(String(size)) || 0;
8171
+ // Group tokens by collateral type and calculate margin required
8172
+ const marginByCollateral = {};
8173
+ // Process all tokens (long and short)
8174
+ const allTokensWithMetadata = [
8175
+ ...longTokens.map((t) => ({
8176
+ ...t,
8177
+ metadata: longTokensMetadata[t.symbol],
8178
+ })),
8179
+ ...shortTokens.map((t) => ({
8180
+ ...t,
8181
+ metadata: shortTokensMetadata[t.symbol],
8182
+ })),
8183
+ ];
8184
+ let totalMarginRequired = 0;
8185
+ allTokensWithMetadata.forEach((token) => {
8186
+ var _a;
8187
+ const weight = token.weight || 0;
8188
+ const assetSize = (sizeValue * weight) / 100;
8189
+ const assetLeverage = assetsLeverage[token.symbol] || 1;
8190
+ const assetMargin = assetLeverage > 0 ? assetSize / assetLeverage : 0;
8191
+ // Get collateral type from metadata, default to USDC
8192
+ const collateralToken = ((_a = token.metadata) === null || _a === void 0 ? void 0 : _a.collateralToken) || 'USDC';
8193
+ if (!marginByCollateral[collateralToken]) {
8194
+ marginByCollateral[collateralToken] = 0;
8195
+ }
8196
+ marginByCollateral[collateralToken] += assetMargin;
8197
+ totalMarginRequired += assetMargin;
8198
+ });
8199
+ const perCollateral = Object.entries(marginByCollateral).map(([collateral, marginRequired]) => {
8200
+ var _a;
8201
+ const collateralToken = collateral;
8202
+ const availableBalance = (_a = availableToTrades[collateralToken]) !== null && _a !== void 0 ? _a : 0;
8203
+ const hasEnough = marginRequired <= availableBalance;
8204
+ const shortfall = Math.max(0, marginRequired - availableBalance);
8205
+ return {
8206
+ collateral: collateralToken,
8207
+ marginRequired: Number(marginRequired.toFixed(2)),
8208
+ availableBalance: Number(availableBalance.toFixed(2)),
8209
+ hasEnough,
8210
+ shortfall: Number(shortfall.toFixed(2)),
8211
+ };
8212
+ });
8213
+ const hasEnoughTotal = perCollateral.every((c) => c.hasEnough);
8214
+ return {
8215
+ totalMarginRequired: Number(totalMarginRequired.toFixed(2)),
8216
+ orderValue: sizeValue,
8217
+ perCollateral,
8218
+ hasEnoughTotal,
8219
+ };
8220
+ }, [
8221
+ longTokens,
8222
+ shortTokens,
7695
8223
  longTokensMetadata,
7696
8224
  shortTokensMetadata,
8225
+ spotBalances,
8226
+ availableToTrades,
7697
8227
  ]);
8228
+ /**
8229
+ * Calculate the maximum order size ($) based on available balances and asset leverages.
8230
+ * Returns the overall maximum order size constrained by all collateral types.
8231
+ */
8232
+ const getMaxSize = useCallback((assetsLeverage) => {
8233
+ const marginFactorByCollateral = {};
8234
+ const allTokensWithMetadata = [
8235
+ ...longTokens.map((t) => ({
8236
+ ...t,
8237
+ metadata: longTokensMetadata[t.symbol],
8238
+ })),
8239
+ ...shortTokens.map((t) => ({
8240
+ ...t,
8241
+ metadata: shortTokensMetadata[t.symbol],
8242
+ })),
8243
+ ];
8244
+ // Calculate the margin factor for each collateral type
8245
+ // marginFactor = sum of (weight / leverage) for all assets using that collateral
8246
+ allTokensWithMetadata.forEach((token) => {
8247
+ var _a;
8248
+ const weight = token.weight || 0;
8249
+ const assetLeverage = assetsLeverage[token.symbol] || 1;
8250
+ const collateralToken = ((_a = token.metadata) === null || _a === void 0 ? void 0 : _a.collateralToken) || 'USDC';
8251
+ // marginFactor represents how much margin is needed per $1 of order size
8252
+ const marginFactor = assetLeverage > 0 ? weight / (100 * assetLeverage) : 0;
8253
+ if (!marginFactorByCollateral[collateralToken]) {
8254
+ marginFactorByCollateral[collateralToken] = 0;
8255
+ }
8256
+ marginFactorByCollateral[collateralToken] += marginFactor;
8257
+ });
8258
+ // Calculate max size for each collateral type
8259
+ // maxSize = availableBalance / marginFactor
8260
+ // Each maxSize represents the total order size that collateral can support
8261
+ const maxSizePerCollateral = [];
8262
+ Object.entries(marginFactorByCollateral).forEach(([collateral, marginFactor]) => {
8263
+ var _a;
8264
+ const collateralToken = collateral;
8265
+ const availableBalance = (_a = availableToTrades[collateralToken]) !== null && _a !== void 0 ? _a : 0;
8266
+ if (marginFactor > 0) {
8267
+ const maxSize = availableBalance / marginFactor;
8268
+ maxSizePerCollateral.push(maxSize);
8269
+ }
8270
+ });
8271
+ if (maxSizePerCollateral.length === 0) {
8272
+ return 0;
8273
+ }
8274
+ const overallMaxSize = Math.min(...maxSizePerCollateral);
8275
+ return Number(overallMaxSize.toFixed(2));
8276
+ }, [
8277
+ longTokens,
8278
+ shortTokens,
8279
+ longTokensMetadata,
8280
+ shortTokensMetadata,
8281
+ availableToTrades,
8282
+ ]);
8283
+ return {
8284
+ abstractionMode: useUserData((state) => state.userAbstractionMode),
8285
+ spotBalances,
8286
+ availableToTrades,
8287
+ isLoading,
8288
+ getMarginRequired,
8289
+ getMaxSize,
8290
+ };
7698
8291
  };
7699
8292
 
7700
8293
  /**
@@ -7829,13 +8422,426 @@ function useHyperliquidUserFills(options) {
7829
8422
  };
7830
8423
  }
7831
8424
 
8425
+ async function getTradeHistory(baseUrl, params) {
8426
+ const url = joinUrl(baseUrl, '/trade-history');
8427
+ try {
8428
+ const resp = await apiClient.get(url, {
8429
+ params,
8430
+ timeout: 60000,
8431
+ });
8432
+ return { data: resp.data, status: resp.status, headers: resp.headers };
8433
+ }
8434
+ catch (error) {
8435
+ throw toApiError(error);
8436
+ }
8437
+ }
8438
+
8439
+ // ─── helpers ────────────────────────────────────────────────────
8440
+ const EMPTY_SUMMARY = {
8441
+ pnl: 0,
8442
+ volume: 0,
8443
+ winRate: 0,
8444
+ wins: 0,
8445
+ losses: 0,
8446
+ totalProfit: 0,
8447
+ totalLoss: 0,
8448
+ };
8449
+ const getTimeframeDays$1 = (tf) => {
8450
+ switch (tf) {
8451
+ case '2W':
8452
+ return 14;
8453
+ case '3W':
8454
+ return 21;
8455
+ case '2M':
8456
+ return 60;
8457
+ case '3M':
8458
+ return 90;
8459
+ }
8460
+ };
8461
+ const isWeekTimeframe = (tf) => tf === '2W' || tf === '3W';
8462
+ const toDateKey = (date) => {
8463
+ const y = date.getFullYear();
8464
+ const m = String(date.getMonth() + 1).padStart(2, '0');
8465
+ const d = String(date.getDate()).padStart(2, '0');
8466
+ return `${y}-${m}-${d}`;
8467
+ };
8468
+ const toMonthKey = (date) => {
8469
+ const y = date.getFullYear();
8470
+ const m = String(date.getMonth() + 1).padStart(2, '0');
8471
+ return `${y}-${m}`;
8472
+ };
8473
+ const formatMonthLabel = (date) => date.toLocaleDateString('en-US', { month: 'short', year: 'numeric' });
8474
+ const getMonday = (date) => {
8475
+ const d = new Date(date);
8476
+ const day = d.getDay(); // 0=Sun … 6=Sat
8477
+ const diff = day === 0 ? -6 : 1 - day;
8478
+ d.setDate(d.getDate() + diff);
8479
+ d.setHours(0, 0, 0, 0);
8480
+ return d;
8481
+ };
8482
+ const toLocalMidnight = (input) => {
8483
+ const d = typeof input === 'string' ? new Date(input + 'T00:00:00') : new Date(input);
8484
+ d.setHours(0, 0, 0, 0);
8485
+ return d;
8486
+ };
8487
+ const diffDays = (start, end) => {
8488
+ return Math.round((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24)) + 1;
8489
+ };
8490
+ const toISODateString$1 = (input) => {
8491
+ const d = typeof input === 'string' ? new Date(input + 'T00:00:00') : new Date(input);
8492
+ return d.toISOString();
8493
+ };
8494
+ const round2 = (n) => Math.round(n * 100) / 100;
8495
+ const buildSummary = (days) => {
8496
+ let pnl = 0;
8497
+ let volume = 0;
8498
+ let wins = 0;
8499
+ let losses = 0;
8500
+ let totalProfit = 0;
8501
+ let totalLoss = 0;
8502
+ for (const day of days) {
8503
+ pnl += day.totalPnl;
8504
+ volume += day.volume;
8505
+ if (day.positionsClosed === 0)
8506
+ continue;
8507
+ if (day.totalPnl > 0) {
8508
+ wins++;
8509
+ totalProfit += day.totalPnl;
8510
+ }
8511
+ else if (day.totalPnl < 0) {
8512
+ losses++;
8513
+ totalLoss += Math.abs(day.totalPnl);
8514
+ }
8515
+ }
8516
+ const total = wins + losses;
8517
+ const winRate = total > 0 ? Math.round((wins / total) * 100) : 0;
8518
+ return {
8519
+ pnl: round2(pnl),
8520
+ volume: round2(volume),
8521
+ winRate,
8522
+ wins,
8523
+ losses,
8524
+ totalProfit: round2(totalProfit),
8525
+ totalLoss: round2(totalLoss),
8526
+ };
8527
+ };
8528
+ const buildCalendarData = (tradeHistories, timeframe, rangeStart, rangeEnd, totalDays, useCustomDates, getAssetByName) => {
8529
+ const startKey = toDateKey(rangeStart);
8530
+ const endKey = toDateKey(rangeEnd);
8531
+ // Build day buckets for the full range
8532
+ const buckets = new Map();
8533
+ for (let i = 0; i < totalDays; i++) {
8534
+ const d = new Date(rangeStart);
8535
+ d.setDate(rangeStart.getDate() + i);
8536
+ buckets.set(toDateKey(d), {
8537
+ pnl: 0,
8538
+ volume: 0,
8539
+ positionsClosed: 0,
8540
+ trades: [],
8541
+ });
8542
+ }
8543
+ // Populate buckets from trade histories
8544
+ for (const trade of tradeHistories) {
8545
+ if (!trade.createdAt)
8546
+ continue;
8547
+ const date = new Date(trade.createdAt);
8548
+ if (isNaN(date.getTime()))
8549
+ continue;
8550
+ const dateKey = toDateKey(date);
8551
+ if (dateKey < startKey || dateKey > endKey)
8552
+ continue;
8553
+ const bucket = buckets.get(dateKey);
8554
+ if (!bucket)
8555
+ continue;
8556
+ const pnl = trade.realizedPnl;
8557
+ bucket.pnl += isFinite(pnl) ? pnl : 0;
8558
+ const vol = trade.totalValue;
8559
+ bucket.volume += isFinite(vol) ? vol : 0;
8560
+ bucket.positionsClosed += 1;
8561
+ const tradePnl = trade.realizedPnl;
8562
+ bucket.trades.push({
8563
+ tradeHistoryId: trade.tradeHistoryId,
8564
+ realizedPnl: tradePnl,
8565
+ result: tradePnl > 0 ? 'profit' : tradePnl < 0 ? 'loss' : 'breakeven',
8566
+ closedLongAssets: trade.closedLongAssets.map((a) => { var _a; return ({ coin: a.coin, metadata: (_a = getAssetByName(a.coin)) !== null && _a !== void 0 ? _a : null }); }),
8567
+ closedShortAssets: trade.closedShortAssets.map((a) => { var _a; return ({ coin: a.coin, metadata: (_a = getAssetByName(a.coin)) !== null && _a !== void 0 ? _a : null }); }),
8568
+ });
8569
+ }
8570
+ // Build day objects
8571
+ const allDays = [];
8572
+ const sortedKeys = Array.from(buckets.keys()).sort();
8573
+ for (const key of sortedKeys) {
8574
+ const bucket = buckets.get(key);
8575
+ const roundedPnl = round2(bucket.pnl);
8576
+ const result = roundedPnl > 0 ? 'profit' : roundedPnl < 0 ? 'loss' : 'breakeven';
8577
+ allDays.push({
8578
+ date: key,
8579
+ totalPnl: roundedPnl,
8580
+ volume: round2(bucket.volume),
8581
+ positionsClosed: bucket.positionsClosed,
8582
+ result,
8583
+ trades: bucket.trades,
8584
+ });
8585
+ }
8586
+ // Group into periods
8587
+ let weeks = [];
8588
+ let months = [];
8589
+ const useWeekGrouping = useCustomDates
8590
+ ? totalDays <= 28
8591
+ : isWeekTimeframe(timeframe);
8592
+ if (useWeekGrouping) {
8593
+ const weekMap = new Map();
8594
+ for (const day of allDays) {
8595
+ const date = new Date(day.date + 'T00:00:00');
8596
+ const monday = getMonday(date);
8597
+ const mondayKey = toDateKey(monday);
8598
+ if (!weekMap.has(mondayKey)) {
8599
+ weekMap.set(mondayKey, []);
8600
+ }
8601
+ weekMap.get(mondayKey).push(day);
8602
+ }
8603
+ const sortedWeekKeys = Array.from(weekMap.keys()).sort();
8604
+ weeks = sortedWeekKeys.map((mondayKey) => {
8605
+ const days = weekMap.get(mondayKey);
8606
+ const monday = new Date(mondayKey + 'T00:00:00');
8607
+ const sunday = new Date(monday);
8608
+ sunday.setDate(monday.getDate() + 6);
8609
+ return {
8610
+ weekStart: mondayKey,
8611
+ weekEnd: toDateKey(sunday),
8612
+ days,
8613
+ summary: buildSummary(days),
8614
+ };
8615
+ });
8616
+ }
8617
+ else {
8618
+ const monthMap = new Map();
8619
+ for (const day of allDays) {
8620
+ const date = new Date(day.date + 'T00:00:00');
8621
+ const mk = toMonthKey(date);
8622
+ if (!monthMap.has(mk)) {
8623
+ monthMap.set(mk, { days: [], label: formatMonthLabel(date) });
8624
+ }
8625
+ monthMap.get(mk).days.push(day);
8626
+ }
8627
+ const sortedMonthKeys = Array.from(monthMap.keys()).sort();
8628
+ months = sortedMonthKeys.map((mk) => {
8629
+ const { days, label } = monthMap.get(mk);
8630
+ return {
8631
+ month: mk,
8632
+ label,
8633
+ days,
8634
+ summary: buildSummary(days),
8635
+ };
8636
+ });
8637
+ }
8638
+ return {
8639
+ timeframe,
8640
+ weeks,
8641
+ months,
8642
+ overall: buildSummary(allDays),
8643
+ isLoading: false,
8644
+ };
8645
+ };
8646
+ // ─── hook ───────────────────────────────────────────────────────
8647
+ function usePnlCalendar(options) {
8648
+ var _a;
8649
+ const opts = typeof options === 'string'
8650
+ ? { timeframe: options }
8651
+ : options !== null && options !== void 0 ? options : {};
8652
+ const timeframe = (_a = opts.timeframe) !== null && _a !== void 0 ? _a : '2W';
8653
+ const customStart = opts.startDate;
8654
+ const customEnd = opts.endDate;
8655
+ const context = useContext(PearHyperliquidContext);
8656
+ if (!context) {
8657
+ throw new Error('usePnlCalendar must be used within a PearHyperliquidProvider');
8658
+ }
8659
+ const { apiBaseUrl } = context;
8660
+ const isAuthenticated = useUserData((state) => state.isAuthenticated);
8661
+ const { getAssetByName } = useMarket();
8662
+ const [trades, setTrades] = useState(null);
8663
+ const [isLoading, setIsLoading] = useState(false);
8664
+ const [error, setError] = useState(null);
8665
+ const mountedRef = useRef(true);
8666
+ useEffect(() => {
8667
+ mountedRef.current = true;
8668
+ return () => { mountedRef.current = false; };
8669
+ }, []);
8670
+ // Compute the date range
8671
+ const useCustomDates = !!(customStart && customEnd);
8672
+ let rangeStart;
8673
+ let rangeEnd;
8674
+ let totalDays;
8675
+ if (useCustomDates) {
8676
+ rangeStart = toLocalMidnight(customStart);
8677
+ rangeEnd = toLocalMidnight(customEnd);
8678
+ totalDays = diffDays(rangeStart, rangeEnd);
8679
+ }
8680
+ else {
8681
+ totalDays = getTimeframeDays$1(timeframe);
8682
+ rangeEnd = new Date();
8683
+ rangeEnd.setHours(0, 0, 0, 0);
8684
+ rangeStart = new Date(rangeEnd);
8685
+ rangeStart.setDate(rangeEnd.getDate() - totalDays + 1);
8686
+ }
8687
+ const startIso = toISODateString$1(rangeStart);
8688
+ const endIso = toISODateString$1(rangeEnd);
8689
+ const fetchData = useCallback(async () => {
8690
+ if (!isAuthenticated)
8691
+ return;
8692
+ setIsLoading(true);
8693
+ setError(null);
8694
+ try {
8695
+ const response = await getTradeHistory(apiBaseUrl, {
8696
+ startDate: startIso,
8697
+ endDate: endIso,
8698
+ limit: totalDays * 50,
8699
+ });
8700
+ if (!mountedRef.current)
8701
+ return;
8702
+ setTrades(response.data);
8703
+ }
8704
+ catch (err) {
8705
+ if (!mountedRef.current)
8706
+ return;
8707
+ setError(err instanceof Error ? err.message : 'Failed to fetch trade history');
8708
+ setTrades(null);
8709
+ }
8710
+ finally {
8711
+ if (mountedRef.current)
8712
+ setIsLoading(false);
8713
+ }
8714
+ }, [apiBaseUrl, isAuthenticated, startIso, endIso, totalDays]);
8715
+ useEffect(() => {
8716
+ fetchData();
8717
+ }, [fetchData]);
8718
+ const result = useMemo(() => {
8719
+ const empty = {
8720
+ timeframe,
8721
+ weeks: [],
8722
+ months: [],
8723
+ overall: EMPTY_SUMMARY,
8724
+ isLoading: true,
8725
+ };
8726
+ if (!trades)
8727
+ return empty;
8728
+ if (totalDays <= 0)
8729
+ return empty;
8730
+ return buildCalendarData(trades, timeframe, rangeStart, rangeEnd, totalDays, useCustomDates, getAssetByName);
8731
+ }, [trades, timeframe, startIso, endIso, getAssetByName]);
8732
+ return { ...result, isLoading, error, refetch: fetchData };
8733
+ }
8734
+
8735
+ const HEATMAP_LIMIT = 50;
8736
+ // ─── helpers ────────────────────────────────────────────────────
8737
+ const getTimeframeDays = (tf) => {
8738
+ switch (tf) {
8739
+ case '7D':
8740
+ return 7;
8741
+ case '30D':
8742
+ return 30;
8743
+ case '100D':
8744
+ return 100;
8745
+ case 'allTime':
8746
+ return null;
8747
+ }
8748
+ };
8749
+ const toISODateString = (date) => date.toISOString();
8750
+ const toCalendarTrade = (trade, getAssetByName) => {
8751
+ const pnl = trade.realizedPnl;
8752
+ return {
8753
+ tradeHistoryId: trade.tradeHistoryId,
8754
+ realizedPnl: pnl,
8755
+ result: pnl > 0 ? 'profit' : pnl < 0 ? 'loss' : 'breakeven',
8756
+ closedLongAssets: trade.closedLongAssets.map((a) => { var _a; return ({ coin: a.coin, metadata: (_a = getAssetByName(a.coin)) !== null && _a !== void 0 ? _a : null }); }),
8757
+ closedShortAssets: trade.closedShortAssets.map((a) => { var _a; return ({ coin: a.coin, metadata: (_a = getAssetByName(a.coin)) !== null && _a !== void 0 ? _a : null }); }),
8758
+ };
8759
+ };
8760
+ // ─── hook ───────────────────────────────────────────────────────
8761
+ function usePnlHeatmap(timeframe = 'allTime') {
8762
+ const context = useContext(PearHyperliquidContext);
8763
+ if (!context) {
8764
+ throw new Error('usePnlHeatmap must be used within a PearHyperliquidProvider');
8765
+ }
8766
+ const { apiBaseUrl } = context;
8767
+ const isAuthenticated = useUserData((state) => state.isAuthenticated);
8768
+ const { getAssetByName } = useMarket();
8769
+ const [trades, setTrades] = useState(null);
8770
+ const [isLoading, setIsLoading] = useState(false);
8771
+ const [error, setError] = useState(null);
8772
+ const mountedRef = useRef(true);
8773
+ useEffect(() => {
8774
+ mountedRef.current = true;
8775
+ return () => { mountedRef.current = false; };
8776
+ }, []);
8777
+ const days = getTimeframeDays(timeframe);
8778
+ let startIso;
8779
+ if (days !== null) {
8780
+ const start = new Date();
8781
+ start.setHours(0, 0, 0, 0);
8782
+ start.setDate(start.getDate() - days);
8783
+ startIso = toISODateString(start);
8784
+ }
8785
+ const fetchData = useCallback(async () => {
8786
+ if (!isAuthenticated)
8787
+ return;
8788
+ setIsLoading(true);
8789
+ setError(null);
8790
+ try {
8791
+ const response = await getTradeHistory(apiBaseUrl, {
8792
+ ...(startIso ? { startDate: startIso } : {}),
8793
+ limit: 5000,
8794
+ });
8795
+ if (!mountedRef.current)
8796
+ return;
8797
+ setTrades(response.data);
8798
+ }
8799
+ catch (err) {
8800
+ if (!mountedRef.current)
8801
+ return;
8802
+ setError(err instanceof Error ? err.message : 'Failed to fetch trade history');
8803
+ setTrades(null);
8804
+ }
8805
+ finally {
8806
+ if (mountedRef.current)
8807
+ setIsLoading(false);
8808
+ }
8809
+ }, [apiBaseUrl, isAuthenticated, startIso]);
8810
+ useEffect(() => {
8811
+ fetchData();
8812
+ }, [fetchData]);
8813
+ const result = useMemo(() => {
8814
+ if (!trades)
8815
+ return [];
8816
+ const top = trades
8817
+ .slice()
8818
+ .sort((a, b) => Math.abs(b.realizedPnl) - Math.abs(a.realizedPnl))
8819
+ .slice(0, HEATMAP_LIMIT);
8820
+ const totalAbsPnl = top.reduce((sum, t) => sum + Math.abs(t.realizedPnl), 0);
8821
+ return top.map((t) => ({
8822
+ ...toCalendarTrade(t, getAssetByName),
8823
+ percentage: totalAbsPnl > 0
8824
+ ? Math.round((Math.abs(t.realizedPnl) / totalAbsPnl) * 10000) / 100
8825
+ : 0,
8826
+ }));
8827
+ }, [trades, getAssetByName]);
8828
+ return { timeframe, trades: result, isLoading, error, refetch: fetchData };
8829
+ }
8830
+
7832
8831
  const PearHyperliquidContext = createContext(undefined);
7833
8832
  /**
7834
8833
  * React Provider for PearHyperliquidClient
7835
8834
  */
7836
- const PearHyperliquidProvider = ({ children, apiBaseUrl = 'https://hl-ui.pearprotocol.io', clientId = 'PEARPROTOCOLUI', wsUrl = 'wss://hl-ui.pearprotocol.io/ws', }) => {
8835
+ const PearHyperliquidProvider = ({ children, apiBaseUrl = "https://hl-ui.pearprotocol.io", clientId = "PEARPROTOCOLUI", wsUrl = "wss://hl-ui.pearprotocol.io/ws", }) => {
7837
8836
  const address = useUserData((s) => s.address);
7838
- const setAddress = useUserData((s) => s.setAddress);
8837
+ const clearHyperliquidUserData = useHyperliquidData((state) => state.clearUserData);
8838
+ const prevAddressRef = useRef(null);
8839
+ useEffect(() => {
8840
+ if (prevAddressRef.current !== null && prevAddressRef.current !== address) {
8841
+ clearHyperliquidUserData();
8842
+ }
8843
+ prevAddressRef.current = address;
8844
+ }, [address, clearHyperliquidUserData]);
7839
8845
  const perpMetasByDex = useHyperliquidData((state) => state.perpMetasByDex);
7840
8846
  const setPerpDexs = useHyperliquidData((state) => state.setPerpDexs);
7841
8847
  const setPerpMetasByDex = useHyperliquidData((state) => state.setPerpMetasByDex);
@@ -7866,20 +8872,20 @@ const PearHyperliquidProvider = ({ children, apiBaseUrl = 'https://hl-ui.pearpro
7866
8872
  perpMetas.forEach((item, perpIndex) => {
7867
8873
  var _a, _b;
7868
8874
  const dexName = perpIndex === 0
7869
- ? 'HYPERLIQUID'
8875
+ ? "HYPERLIQUID"
7870
8876
  : ((_b = (_a = perpDexs[perpIndex]) === null || _a === void 0 ? void 0 : _a.name) !== null && _b !== void 0 ? _b : `DEX_${perpIndex}`);
7871
8877
  var collateralToken;
7872
8878
  if (item.collateralToken === 360) {
7873
- collateralToken = 'USDH';
8879
+ collateralToken = "USDH";
7874
8880
  }
7875
8881
  if (item.collateralToken === 0) {
7876
- collateralToken = 'USDC';
8882
+ collateralToken = "USDC";
7877
8883
  }
7878
8884
  if (item.collateralToken === 235) {
7879
- collateralToken = 'USDE';
8885
+ collateralToken = "USDE";
7880
8886
  }
7881
8887
  if (item.collateralToken === 268) {
7882
- collateralToken = 'USDT';
8888
+ collateralToken = "USDT0";
7883
8889
  }
7884
8890
  const universeAssets = item.universe.map((asset) => ({
7885
8891
  ...asset,
@@ -7907,8 +8913,6 @@ const PearHyperliquidProvider = ({ children, apiBaseUrl = 'https://hl-ui.pearpro
7907
8913
  }), [
7908
8914
  apiBaseUrl,
7909
8915
  wsUrl,
7910
- address,
7911
- setAddress,
7912
8916
  isConnected,
7913
8917
  lastError,
7914
8918
  nativeIsConnected,
@@ -7923,7 +8927,7 @@ const PearHyperliquidProvider = ({ children, apiBaseUrl = 'https://hl-ui.pearpro
7923
8927
  function usePearHyperliquid() {
7924
8928
  const ctx = useContext(PearHyperliquidContext);
7925
8929
  if (!ctx)
7926
- throw new Error('usePearHyperliquid must be used within a PearHyperliquidProvider');
8930
+ throw new Error("usePearHyperliquid must be used within a PearHyperliquidProvider");
7927
8931
  return ctx;
7928
8932
  }
7929
8933
 
@@ -8175,4 +9179,4 @@ function getOrderTrailingInfo(order) {
8175
9179
  return undefined;
8176
9180
  }
8177
9181
 
8178
- export { AccountSummaryCalculator, ConflictDetector, MAX_ASSETS_PER_LEG, MINIMUM_ASSET_USD_VALUE, MaxAssetsPerLegError, MinimumPositionSizeError, PearHyperliquidProvider, TokenMetadataExtractor, adjustAdvancePosition, adjustOrder, adjustPosition, calculateMinimumPositionValue, calculateWeightedRatio, cancelOrder, cancelTwap, cancelTwapOrder, closeAllPositions, closePosition, computeBasketCandles, createCandleLookups, createPosition, executeSpotOrder, getAssetByName, getCompleteTimestamps, getKalshiMarkets, getOrderDirection, getOrderLadderConfig, getOrderLeverage, getOrderReduceOnly, getOrderTpSlTriggerType, getOrderTrailingInfo, getOrderTriggerType, getOrderTriggerValue, getOrderTwapDuration, getOrderUsdValue, getPortfolio, isBtcDomOrder, mapCandleIntervalToTradingViewInterval, mapTradingViewIntervalToCandleInterval, markNotificationReadById, markNotificationsRead, selectTokenMetadataBySymbols, toggleWatchlist, updateLeverage, updateRiskParameters, useAccountSummary, useActiveBaskets, useAgentWallet, useAllUserBalances, useAuth, useBasketCandles, useFindBasket, useHighlightedBaskets, useHistoricalPriceData, useHistoricalPriceDataStore, useHyperliquidNativeWebSocket, useHyperliquidUserFills, useHyperliquidWebSocket, useMarket, useMarketData, useMarketDataPayload, useNotifications, useOpenOrders, useOrders, usePearHyperliquid, usePerformanceOverlays, usePortfolio, usePosition, useSpotOrder, useTokenSelectionMetadata, useTopGainers, useTopLosers, useTradeHistories, useTwap, useUserSelection, useWatchlist, useWatchlistBaskets, validateMaxAssetsPerLeg, validateMinimumAssetSize, validatePositionSize };
9182
+ export { ConflictDetector, MAX_ASSETS_PER_LEG, MINIMUM_ASSET_USD_VALUE, MaxAssetsPerLegError, MinimumPositionSizeError, PearHyperliquidProvider, adjustAdvancePosition, adjustOrder, adjustPosition, calculateMinimumPositionValue, calculateWeightedRatio, cancelOrder, cancelTwap, cancelTwapOrder, closeAllPositions, closePosition, computeBasketCandles, createCandleLookups, createPosition, executeSpotOrder, getCompleteTimestamps, getKalshiMarkets, getOrderDirection, getOrderLadderConfig, getOrderLeverage, getOrderReduceOnly, getOrderTpSlTriggerType, getOrderTrailingInfo, getOrderTriggerType, getOrderTriggerValue, getOrderTwapDuration, getOrderUsdValue, getPortfolio, isBtcDomOrder, mapCandleIntervalToTradingViewInterval, mapTradingViewIntervalToCandleInterval, markNotificationReadById, markNotificationsRead, toggleWatchlist, updateLeverage, updateRiskParameters, useAccountSummary, useAgentWallet, useAllUserBalances, useAuth, useBasketCandles, useHistoricalPriceData, useHistoricalPriceDataStore, useHyperliquidUserFills, useMarket, useMarketData, useMarketDataHook, useNotifications, useOpenOrders, useOrders, usePearHyperliquid, usePerformanceOverlays, usePnlCalendar, usePnlHeatmap, usePortfolio, usePosition, useSpotOrder, useTokenSelectionMetadata, useTradeHistories, useTwap, useUserSelection, useWatchlist, validateMaxAssetsPerLeg, validateMinimumAssetSize, validatePositionSize };