@pear-protocol/hyperliquid-sdk 0.1.4 → 0.1.5-pnl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/clients/auth.d.ts +1 -1
- package/dist/clients/tradeHistory.d.ts +7 -0
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks/useAllUserBalances.d.ts +2 -1
- package/dist/hooks/useAuth.d.ts +4 -1
- package/dist/hooks/useBasketCandles.d.ts +1 -0
- package/dist/hooks/useHistoricalPriceData.d.ts +1 -0
- package/dist/hooks/usePnlCalendar.d.ts +61 -0
- package/dist/hooks/usePnlHeatmap.d.ts +13 -0
- package/dist/hooks/usePosition.d.ts +21 -0
- package/dist/index.d.ts +113 -2
- package/dist/index.js +1179 -182
- package/dist/provider.d.ts +1 -1
- package/dist/store/historicalPriceDataStore.d.ts +3 -0
- package/dist/store/hyperliquidDataStore.d.ts +3 -2
- package/dist/store/userDataStore.d.ts +5 -1
- package/dist/types.d.ts +6 -0
- package/dist/utils/http.d.ts +2 -2
- package/package.json +1 -1
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
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
|
|
386
|
-
// fall back to
|
|
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 (
|
|
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 ===
|
|
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 = {
|
|
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 (
|
|
957
|
+
if ("success" in message || "error" in message) {
|
|
912
958
|
if (message.error) {
|
|
913
|
-
console.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 (
|
|
968
|
+
if ("channel" in message && "data" in message) {
|
|
923
969
|
const response = message;
|
|
924
970
|
switch (response.channel) {
|
|
925
|
-
case
|
|
971
|
+
case "userFills":
|
|
926
972
|
{
|
|
927
|
-
const maybePromise =
|
|
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(
|
|
975
|
+
maybePromise.catch((err) => console.error("[HyperLiquid WS] userFills callback error", err));
|
|
930
976
|
}
|
|
931
977
|
}
|
|
932
978
|
break;
|
|
933
|
-
case
|
|
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
|
|
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 ===
|
|
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
|
|
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 ||
|
|
958
|
-
const toStr = (n) => Number.isFinite(n) ? n.toString() :
|
|
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
|
|
1035
|
+
case "allMids":
|
|
989
1036
|
{
|
|
990
1037
|
const data = response.data;
|
|
991
1038
|
setAllMids(data);
|
|
992
1039
|
}
|
|
993
1040
|
break;
|
|
994
|
-
case
|
|
1041
|
+
case "activeAssetData":
|
|
995
1042
|
{
|
|
996
1043
|
const assetData = response.data;
|
|
997
1044
|
upsertActiveAssetData(assetData.coin, assetData);
|
|
998
1045
|
}
|
|
999
1046
|
break;
|
|
1000
|
-
case
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
1065
|
-
|
|
1066
|
-
}
|
|
1067
|
-
setTimeout(() => connect(),
|
|
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
|
-
|
|
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:
|
|
1250
|
+
method: "unsubscribe",
|
|
1137
1251
|
subscription: {
|
|
1138
|
-
type:
|
|
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:
|
|
1267
|
+
method: "unsubscribe",
|
|
1154
1268
|
subscription: {
|
|
1155
|
-
type:
|
|
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:
|
|
1305
|
+
method: "subscribe",
|
|
1200
1306
|
subscription: {
|
|
1201
|
-
type:
|
|
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 (
|
|
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 ||
|
|
1347
|
+
if (!isConnected ||
|
|
1348
|
+
!subscribedAddress ||
|
|
1349
|
+
!clearinghouseStateReceived ||
|
|
1350
|
+
!userSummary)
|
|
1229
1351
|
return;
|
|
1230
1352
|
const subscribeUserFills = {
|
|
1231
|
-
method:
|
|
1353
|
+
method: "subscribe",
|
|
1232
1354
|
subscription: {
|
|
1233
|
-
type:
|
|
1355
|
+
type: "userFills",
|
|
1234
1356
|
user: subscribedAddress,
|
|
1235
1357
|
},
|
|
1236
1358
|
};
|
|
1237
1359
|
sendJsonMessage(subscribeUserFills);
|
|
1238
|
-
}, [
|
|
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) =>
|
|
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
|
|
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
|
-
|
|
1433
|
+
deleteActiveAssetData,
|
|
1281
1434
|
]);
|
|
1282
1435
|
// Handle candle subscriptions for tokens and interval changes
|
|
1283
1436
|
useEffect(() => {
|
|
@@ -1470,17 +1623,33 @@ const useTradeHistories = () => {
|
|
|
1470
1623
|
const enrichedTradeHistories = useMemo(() => {
|
|
1471
1624
|
if (!tradeHistories)
|
|
1472
1625
|
return null;
|
|
1473
|
-
return tradeHistories.map((history) =>
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1626
|
+
return tradeHistories.map((history) => {
|
|
1627
|
+
const totalClosePositionSize = history.closedLongAssets.reduce((acc, asset) => acc + asset.limitPrice * asset.size, 0) +
|
|
1628
|
+
history.closedShortAssets.reduce((acc, asset) => acc + asset.limitPrice * asset.size, 0);
|
|
1629
|
+
return {
|
|
1630
|
+
...history,
|
|
1631
|
+
closedLongAssets: history.closedLongAssets.map((asset) => {
|
|
1632
|
+
const closeNotional = asset.limitPrice * asset.size;
|
|
1633
|
+
return {
|
|
1634
|
+
...asset,
|
|
1635
|
+
closeWeight: totalClosePositionSize > 0
|
|
1636
|
+
? closeNotional / totalClosePositionSize
|
|
1637
|
+
: 0,
|
|
1638
|
+
metadata: getAssetByName(asset.coin),
|
|
1639
|
+
};
|
|
1640
|
+
}),
|
|
1641
|
+
closedShortAssets: history.closedShortAssets.map((asset) => {
|
|
1642
|
+
const closeNotional = asset.limitPrice * asset.size;
|
|
1643
|
+
return {
|
|
1644
|
+
...asset,
|
|
1645
|
+
closeWeight: totalClosePositionSize > 0
|
|
1646
|
+
? closeNotional / totalClosePositionSize
|
|
1647
|
+
: 0,
|
|
1648
|
+
metadata: getAssetByName(asset.coin),
|
|
1649
|
+
};
|
|
1650
|
+
}),
|
|
1651
|
+
};
|
|
1652
|
+
});
|
|
1484
1653
|
}, [tradeHistories, getAssetByName]);
|
|
1485
1654
|
const isLoading = useMemo(() => {
|
|
1486
1655
|
return tradeHistories === null && context.isConnected;
|
|
@@ -1561,7 +1730,7 @@ const useTokenSelectionMetadataStore = create((set) => ({
|
|
|
1561
1730
|
maxLeverage: 0,
|
|
1562
1731
|
minSize: {},
|
|
1563
1732
|
leverageMatched: true,
|
|
1564
|
-
recompute: ({ tokenMetadata, marketData, longTokens, shortTokens
|
|
1733
|
+
recompute: ({ tokenMetadata, marketData, longTokens, shortTokens }) => {
|
|
1565
1734
|
const isPriceDataReady = Object.keys(tokenMetadata).length > 0;
|
|
1566
1735
|
const longSymbols = longTokens.map((t) => t.symbol);
|
|
1567
1736
|
const shortSymbols = shortTokens.map((t) => t.symbol);
|
|
@@ -1692,10 +1861,9 @@ const useTokenSelectionMetadataStore = create((set) => ({
|
|
|
1692
1861
|
return 0;
|
|
1693
1862
|
let maxLev = 0;
|
|
1694
1863
|
allSymbols.forEach((symbol) => {
|
|
1695
|
-
var _a;
|
|
1696
1864
|
const tokenDetail = tokenMetadata[symbol];
|
|
1697
|
-
if (
|
|
1698
|
-
maxLev = Math.max(maxLev, tokenDetail.
|
|
1865
|
+
if (tokenDetail === null || tokenDetail === void 0 ? void 0 : tokenDetail.maxLeverage) {
|
|
1866
|
+
maxLev = Math.max(maxLev, tokenDetail.maxLeverage);
|
|
1699
1867
|
}
|
|
1700
1868
|
});
|
|
1701
1869
|
return maxLev;
|
|
@@ -1819,6 +1987,22 @@ const getIntervalSeconds = (interval) => {
|
|
|
1819
1987
|
default: return 60;
|
|
1820
1988
|
}
|
|
1821
1989
|
};
|
|
1990
|
+
/**
|
|
1991
|
+
* Merges overlapping or adjacent ranges to prevent unbounded growth
|
|
1992
|
+
*/
|
|
1993
|
+
const mergeRanges = (ranges, newRange) => {
|
|
1994
|
+
const all = [...ranges, newRange].sort((a, b) => a.start - b.start);
|
|
1995
|
+
const merged = [];
|
|
1996
|
+
for (const r of all) {
|
|
1997
|
+
if (merged.length === 0 || r.start > merged[merged.length - 1].end) {
|
|
1998
|
+
merged.push({ start: r.start, end: r.end });
|
|
1999
|
+
}
|
|
2000
|
+
else {
|
|
2001
|
+
merged[merged.length - 1].end = Math.max(merged[merged.length - 1].end, r.end);
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
return merged;
|
|
2005
|
+
};
|
|
1822
2006
|
const useHistoricalPriceDataStore = create((set, get) => ({
|
|
1823
2007
|
historicalPriceData: {},
|
|
1824
2008
|
loadingTokens: new Set(),
|
|
@@ -1829,6 +2013,8 @@ const useHistoricalPriceDataStore = create((set, get) => ({
|
|
|
1829
2013
|
if (!existing) {
|
|
1830
2014
|
// Create new entry
|
|
1831
2015
|
const sortedCandles = [...candles].sort((a, b) => a.t - b.t);
|
|
2016
|
+
// If first fetch returns empty, mark that there's no data before the requested end time
|
|
2017
|
+
const noDataBefore = sortedCandles.length === 0 ? range.end : null;
|
|
1832
2018
|
return {
|
|
1833
2019
|
historicalPriceData: {
|
|
1834
2020
|
...state.historicalPriceData,
|
|
@@ -1837,7 +2023,9 @@ const useHistoricalPriceDataStore = create((set, get) => ({
|
|
|
1837
2023
|
interval,
|
|
1838
2024
|
candles: sortedCandles,
|
|
1839
2025
|
oldestTime: sortedCandles.length > 0 ? sortedCandles[0].t : null,
|
|
1840
|
-
latestTime: sortedCandles.length > 0 ? sortedCandles[sortedCandles.length - 1].t : null
|
|
2026
|
+
latestTime: sortedCandles.length > 0 ? sortedCandles[sortedCandles.length - 1].t : null,
|
|
2027
|
+
requestedRanges: [range],
|
|
2028
|
+
noDataBefore
|
|
1841
2029
|
}
|
|
1842
2030
|
}
|
|
1843
2031
|
};
|
|
@@ -1849,6 +2037,16 @@ const useHistoricalPriceDataStore = create((set, get) => ({
|
|
|
1849
2037
|
// Update time pointers
|
|
1850
2038
|
const oldestTime = mergedCandles.length > 0 ? mergedCandles[0].t : null;
|
|
1851
2039
|
const latestTime = mergedCandles.length > 0 ? mergedCandles[mergedCandles.length - 1].t : null;
|
|
2040
|
+
// Merge requested ranges
|
|
2041
|
+
const mergedRanges = mergeRanges(existing.requestedRanges || [], range);
|
|
2042
|
+
// Update noDataBefore boundary:
|
|
2043
|
+
// If we fetched a range older than our current oldest data and got no new candles,
|
|
2044
|
+
// it means there's no data before our current oldest time
|
|
2045
|
+
let noDataBefore = existing.noDataBefore;
|
|
2046
|
+
if (candles.length === 0 && existing.oldestTime !== null && range.end <= existing.oldestTime) {
|
|
2047
|
+
// We tried to fetch older data and got nothing - set boundary to our oldest known data
|
|
2048
|
+
noDataBefore = existing.oldestTime;
|
|
2049
|
+
}
|
|
1852
2050
|
return {
|
|
1853
2051
|
historicalPriceData: {
|
|
1854
2052
|
...state.historicalPriceData,
|
|
@@ -1856,7 +2054,9 @@ const useHistoricalPriceDataStore = create((set, get) => ({
|
|
|
1856
2054
|
...existing,
|
|
1857
2055
|
candles: mergedCandles,
|
|
1858
2056
|
oldestTime,
|
|
1859
|
-
latestTime
|
|
2057
|
+
latestTime,
|
|
2058
|
+
requestedRanges: mergedRanges,
|
|
2059
|
+
noDataBefore
|
|
1860
2060
|
}
|
|
1861
2061
|
}
|
|
1862
2062
|
};
|
|
@@ -1866,8 +2066,24 @@ const useHistoricalPriceDataStore = create((set, get) => ({
|
|
|
1866
2066
|
const { historicalPriceData } = get();
|
|
1867
2067
|
const key = createKey(symbol, interval);
|
|
1868
2068
|
const tokenData = historicalPriceData[key];
|
|
1869
|
-
if (!tokenData
|
|
2069
|
+
if (!tokenData)
|
|
1870
2070
|
return false;
|
|
2071
|
+
// Check if we've hit the "no data before" boundary
|
|
2072
|
+
// If the requested range ends before or at the boundary, we know there's no data
|
|
2073
|
+
if (tokenData.noDataBefore !== null && endTime <= tokenData.noDataBefore) {
|
|
2074
|
+
return true; // No point fetching - we know there's no data before this point
|
|
2075
|
+
}
|
|
2076
|
+
// Check if we've already attempted to fetch this range
|
|
2077
|
+
const requestedRanges = tokenData.requestedRanges || [];
|
|
2078
|
+
for (const range of requestedRanges) {
|
|
2079
|
+
if (range.start <= startTime && range.end >= endTime) {
|
|
2080
|
+
return true; // Already attempted this fetch
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
// Check actual data coverage
|
|
2084
|
+
if (tokenData.oldestTime === null || tokenData.latestTime === null) {
|
|
2085
|
+
return false;
|
|
2086
|
+
}
|
|
1871
2087
|
const intervalMilisecond = getIntervalSeconds(interval) * 1000;
|
|
1872
2088
|
const hasStartCoverage = tokenData.oldestTime <= startTime;
|
|
1873
2089
|
const hasEndCoverage = tokenData.latestTime >= endTime ||
|
|
@@ -1882,6 +2098,27 @@ const useHistoricalPriceDataStore = create((set, get) => ({
|
|
|
1882
2098
|
return [];
|
|
1883
2099
|
return tokenData.candles.filter(candle => candle.t >= startTime && candle.t < endTime);
|
|
1884
2100
|
},
|
|
2101
|
+
getEffectiveDataBoundary: (symbols, interval) => {
|
|
2102
|
+
var _a;
|
|
2103
|
+
const { historicalPriceData } = get();
|
|
2104
|
+
if (symbols.length === 0)
|
|
2105
|
+
return null;
|
|
2106
|
+
let maxBoundary = null;
|
|
2107
|
+
for (const symbol of symbols) {
|
|
2108
|
+
const key = createKey(symbol, interval);
|
|
2109
|
+
const tokenData = historicalPriceData[key];
|
|
2110
|
+
if (!tokenData)
|
|
2111
|
+
continue;
|
|
2112
|
+
// Use noDataBefore if set, otherwise use oldestTime
|
|
2113
|
+
const boundary = (_a = tokenData.noDataBefore) !== null && _a !== void 0 ? _a : tokenData.oldestTime;
|
|
2114
|
+
if (boundary !== null) {
|
|
2115
|
+
if (maxBoundary === null || boundary > maxBoundary) {
|
|
2116
|
+
maxBoundary = boundary;
|
|
2117
|
+
}
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
return maxBoundary;
|
|
2121
|
+
},
|
|
1885
2122
|
setTokenLoading: (symbol, loading) => {
|
|
1886
2123
|
set(state => {
|
|
1887
2124
|
const newLoadingTokens = new Set(state.loadingTokens);
|
|
@@ -5762,10 +5999,10 @@ function toApiError(error) {
|
|
|
5762
5999
|
var _a;
|
|
5763
6000
|
const axiosError = error;
|
|
5764
6001
|
const payload = (axiosError && axiosError.response ? axiosError.response.data : undefined);
|
|
5765
|
-
const message = typeof payload ===
|
|
6002
|
+
const message = typeof payload === "object" && payload && "message" in payload
|
|
5766
6003
|
? String(payload.message)
|
|
5767
|
-
: (axiosError === null || axiosError === void 0 ? void 0 : axiosError.message) ||
|
|
5768
|
-
const errField = typeof payload ===
|
|
6004
|
+
: (axiosError === null || axiosError === void 0 ? void 0 : axiosError.message) || "Request failed";
|
|
6005
|
+
const errField = typeof payload === "object" && payload && "error" in payload
|
|
5769
6006
|
? String(payload.error)
|
|
5770
6007
|
: undefined;
|
|
5771
6008
|
return {
|
|
@@ -5775,8 +6012,8 @@ function toApiError(error) {
|
|
|
5775
6012
|
};
|
|
5776
6013
|
}
|
|
5777
6014
|
function joinUrl(baseUrl, path) {
|
|
5778
|
-
const cleanBase = baseUrl.replace(/\/$/,
|
|
5779
|
-
const cleanPath = path.startsWith(
|
|
6015
|
+
const cleanBase = baseUrl.replace(/\/$/, "");
|
|
6016
|
+
const cleanPath = path.startsWith("/") ? path : `/${path}`;
|
|
5780
6017
|
return `${cleanBase}${cleanPath}`;
|
|
5781
6018
|
}
|
|
5782
6019
|
/**
|
|
@@ -5805,7 +6042,7 @@ function addAuthInterceptors(params) {
|
|
|
5805
6042
|
pendingRequests = [];
|
|
5806
6043
|
}
|
|
5807
6044
|
const isOurApiUrl = (url) => Boolean(url && url.startsWith(apiBaseUrl));
|
|
5808
|
-
const isRefreshUrl = (url) => Boolean(url && url.startsWith(joinUrl(apiBaseUrl,
|
|
6045
|
+
const isRefreshUrl = (url) => Boolean(url && url.startsWith(joinUrl(apiBaseUrl, "/auth/refresh")));
|
|
5809
6046
|
const reqId = apiClient.interceptors.request.use((config) => {
|
|
5810
6047
|
var _a;
|
|
5811
6048
|
try {
|
|
@@ -5813,11 +6050,12 @@ function addAuthInterceptors(params) {
|
|
|
5813
6050
|
const token = getAccessToken();
|
|
5814
6051
|
if (token) {
|
|
5815
6052
|
config.headers = (_a = config.headers) !== null && _a !== void 0 ? _a : {};
|
|
5816
|
-
|
|
6053
|
+
config.headers["Authorization"] = `Bearer ${token}`;
|
|
5817
6054
|
}
|
|
5818
6055
|
}
|
|
5819
6056
|
}
|
|
5820
|
-
catch (
|
|
6057
|
+
catch (err) {
|
|
6058
|
+
console.error("[Auth Interceptor] Request interceptor error:", err);
|
|
5821
6059
|
}
|
|
5822
6060
|
return config;
|
|
5823
6061
|
});
|
|
@@ -5828,22 +6066,36 @@ function addAuthInterceptors(params) {
|
|
|
5828
6066
|
const url = originalRequest === null || originalRequest === void 0 ? void 0 : originalRequest.url;
|
|
5829
6067
|
// If not our API or not 401, just reject
|
|
5830
6068
|
if (!status || status !== 401 || !isOurApiUrl(url)) {
|
|
6069
|
+
if (status === 401) {
|
|
6070
|
+
console.warn("[Auth Interceptor] 401 received but URL check failed:", {
|
|
6071
|
+
url,
|
|
6072
|
+
apiBaseUrl,
|
|
6073
|
+
isOurApiUrl: isOurApiUrl(url),
|
|
6074
|
+
});
|
|
6075
|
+
}
|
|
5831
6076
|
return Promise.reject(error);
|
|
5832
6077
|
}
|
|
6078
|
+
console.log("[Auth Interceptor] 401 detected, attempting token refresh for URL:", url);
|
|
5833
6079
|
// If the 401 is from refresh endpoint itself -> force logout
|
|
5834
6080
|
if (isRefreshUrl(url)) {
|
|
6081
|
+
console.warn("[Auth Interceptor] Refresh endpoint returned 401, logging out");
|
|
5835
6082
|
try {
|
|
5836
6083
|
await logout();
|
|
5837
6084
|
}
|
|
5838
|
-
catch (
|
|
6085
|
+
catch (err) {
|
|
6086
|
+
console.error("[Auth Interceptor] Logout failed:", err);
|
|
6087
|
+
}
|
|
5839
6088
|
return Promise.reject(error);
|
|
5840
6089
|
}
|
|
5841
6090
|
// Prevent infinite loop
|
|
5842
6091
|
if (originalRequest && originalRequest._retry) {
|
|
6092
|
+
console.warn("[Auth Interceptor] Request already retried, logging out");
|
|
5843
6093
|
try {
|
|
5844
6094
|
await logout();
|
|
5845
6095
|
}
|
|
5846
|
-
catch (
|
|
6096
|
+
catch (err) {
|
|
6097
|
+
console.error("[Auth Interceptor] Logout failed:", err);
|
|
6098
|
+
}
|
|
5847
6099
|
return Promise.reject(error);
|
|
5848
6100
|
}
|
|
5849
6101
|
// Mark so we don't retry twice
|
|
@@ -5857,31 +6109,45 @@ function addAuthInterceptors(params) {
|
|
|
5857
6109
|
if (!newToken || !originalRequest)
|
|
5858
6110
|
return reject(error);
|
|
5859
6111
|
originalRequest.headers = (_a = originalRequest.headers) !== null && _a !== void 0 ? _a : {};
|
|
5860
|
-
originalRequest.headers[
|
|
6112
|
+
originalRequest.headers["Authorization"] =
|
|
6113
|
+
`Bearer ${newToken}`;
|
|
5861
6114
|
resolve(apiClient.request(originalRequest));
|
|
5862
6115
|
});
|
|
5863
6116
|
});
|
|
5864
6117
|
}
|
|
5865
6118
|
isRefreshing = true;
|
|
5866
6119
|
try {
|
|
6120
|
+
console.log("[Auth Interceptor] Refreshing tokens...");
|
|
5867
6121
|
const refreshed = await refreshTokens();
|
|
5868
|
-
const newAccessToken = (_b = (refreshed &&
|
|
6122
|
+
const newAccessToken = (_b = (refreshed &&
|
|
6123
|
+
(refreshed.accessToken || ((_a = refreshed.data) === null || _a === void 0 ? void 0 : _a.accessToken)))) !== null && _b !== void 0 ? _b : null;
|
|
6124
|
+
if (!newAccessToken) {
|
|
6125
|
+
console.error("[Auth Interceptor] Token refresh succeeded but no access token in response:", refreshed);
|
|
6126
|
+
}
|
|
6127
|
+
else {
|
|
6128
|
+
console.log("[Auth Interceptor] Token refresh successful");
|
|
6129
|
+
}
|
|
5869
6130
|
resolvePendingRequests(newAccessToken);
|
|
5870
6131
|
if (originalRequest) {
|
|
5871
6132
|
originalRequest.headers = (_c = originalRequest.headers) !== null && _c !== void 0 ? _c : {};
|
|
5872
6133
|
if (newAccessToken)
|
|
5873
|
-
|
|
6134
|
+
originalRequest.headers["Authorization"] =
|
|
6135
|
+
`Bearer ${newAccessToken}`;
|
|
6136
|
+
console.log("[Auth Interceptor] Retrying original request with new token");
|
|
5874
6137
|
const resp = await apiClient.request(originalRequest);
|
|
5875
6138
|
return resp;
|
|
5876
6139
|
}
|
|
5877
6140
|
return Promise.reject(error);
|
|
5878
6141
|
}
|
|
5879
6142
|
catch (refreshErr) {
|
|
6143
|
+
console.error("[Auth Interceptor] Token refresh failed:", refreshErr);
|
|
5880
6144
|
resolvePendingRequests(null);
|
|
5881
6145
|
try {
|
|
5882
6146
|
await logout();
|
|
5883
6147
|
}
|
|
5884
|
-
catch (
|
|
6148
|
+
catch (err) {
|
|
6149
|
+
console.error("[Auth Interceptor] Logout failed:", err);
|
|
6150
|
+
}
|
|
5885
6151
|
return Promise.reject(refreshErr);
|
|
5886
6152
|
}
|
|
5887
6153
|
finally {
|
|
@@ -5892,11 +6158,15 @@ function addAuthInterceptors(params) {
|
|
|
5892
6158
|
try {
|
|
5893
6159
|
apiClient.interceptors.request.eject(reqId);
|
|
5894
6160
|
}
|
|
5895
|
-
catch (
|
|
6161
|
+
catch (err) {
|
|
6162
|
+
console.error("[Auth Interceptor] Failed to eject request interceptor:", err);
|
|
6163
|
+
}
|
|
5896
6164
|
try {
|
|
5897
6165
|
apiClient.interceptors.response.eject(resId);
|
|
5898
6166
|
}
|
|
5899
|
-
catch (
|
|
6167
|
+
catch (err) {
|
|
6168
|
+
console.error("[Auth Interceptor] Failed to eject response interceptor:", err);
|
|
6169
|
+
}
|
|
5900
6170
|
};
|
|
5901
6171
|
}
|
|
5902
6172
|
|
|
@@ -6012,7 +6282,7 @@ const useHistoricalPriceData = () => {
|
|
|
6012
6282
|
const shortTokens = useUserSelection$1((state) => state.shortTokens);
|
|
6013
6283
|
const candleInterval = useUserSelection$1((state) => state.candleInterval);
|
|
6014
6284
|
// Historical price data store
|
|
6015
|
-
const { addHistoricalPriceData, hasHistoricalPriceData: storeHasData, getHistoricalPriceData: storeGetData, setTokenLoading, isTokenLoading, removeTokenPriceData, clearData, } = useHistoricalPriceDataStore();
|
|
6285
|
+
const { addHistoricalPriceData, hasHistoricalPriceData: storeHasData, getHistoricalPriceData: storeGetData, getEffectiveDataBoundary: storeGetBoundary, setTokenLoading, isTokenLoading, removeTokenPriceData, clearData, } = useHistoricalPriceDataStore();
|
|
6016
6286
|
// Track previous tokens and interval to detect changes
|
|
6017
6287
|
const prevTokensRef = useRef(new Set());
|
|
6018
6288
|
const prevIntervalRef = useRef(null);
|
|
@@ -6065,6 +6335,11 @@ const useHistoricalPriceData = () => {
|
|
|
6065
6335
|
const getAllHistoricalPriceData = useCallback(async () => {
|
|
6066
6336
|
return useHistoricalPriceDataStore.getState().historicalPriceData;
|
|
6067
6337
|
}, []);
|
|
6338
|
+
const getEffectiveDataBoundary = useCallback((interval) => {
|
|
6339
|
+
const allTokens = getAllTokens();
|
|
6340
|
+
const symbols = allTokens.map(t => t.symbol);
|
|
6341
|
+
return storeGetBoundary(symbols, interval);
|
|
6342
|
+
}, [getAllTokens, storeGetBoundary]);
|
|
6068
6343
|
const fetchHistoricalPriceData = useCallback(async (startTime, endTime, interval, callback) => {
|
|
6069
6344
|
const allTokens = getAllTokens();
|
|
6070
6345
|
if (allTokens.length === 0) {
|
|
@@ -6122,6 +6397,7 @@ const useHistoricalPriceData = () => {
|
|
|
6122
6397
|
hasHistoricalPriceData,
|
|
6123
6398
|
getAllHistoricalPriceData,
|
|
6124
6399
|
getHistoricalPriceData,
|
|
6400
|
+
getEffectiveDataBoundary,
|
|
6125
6401
|
isLoading,
|
|
6126
6402
|
clearCache,
|
|
6127
6403
|
};
|
|
@@ -6318,7 +6594,7 @@ const useBasketCandles = () => {
|
|
|
6318
6594
|
const longTokens = useUserSelection$1((state) => state.longTokens);
|
|
6319
6595
|
const shortTokens = useUserSelection$1((state) => state.shortTokens);
|
|
6320
6596
|
const candleData = useHyperliquidData((s) => s.candleData);
|
|
6321
|
-
const { fetchHistoricalPriceData, isLoading: tokenLoading, getAllHistoricalPriceData } = useHistoricalPriceData();
|
|
6597
|
+
const { fetchHistoricalPriceData, isLoading: tokenLoading, getAllHistoricalPriceData, getEffectiveDataBoundary } = useHistoricalPriceData();
|
|
6322
6598
|
const fetchBasketCandles = useCallback(async (startTime, endTime, interval) => {
|
|
6323
6599
|
const tokenCandles = await fetchHistoricalPriceData(startTime, endTime, interval);
|
|
6324
6600
|
const basket = computeBasketCandles(longTokens, shortTokens, tokenCandles);
|
|
@@ -6518,6 +6794,7 @@ const useBasketCandles = () => {
|
|
|
6518
6794
|
fetchBasketCandles,
|
|
6519
6795
|
fetchPerformanceCandles,
|
|
6520
6796
|
fetchOverallPerformanceCandles,
|
|
6797
|
+
getEffectiveDataBoundary,
|
|
6521
6798
|
isLoading,
|
|
6522
6799
|
addRealtimeListener,
|
|
6523
6800
|
removeRealtimeListener,
|
|
@@ -6793,11 +7070,13 @@ async function updateLeverage(baseUrl, positionId, payload) {
|
|
|
6793
7070
|
}
|
|
6794
7071
|
}
|
|
6795
7072
|
|
|
6796
|
-
const calculatePositionAsset = (asset, currentPrice, totalInitialPositionSize, leverage, metadata, isLong = true) => {
|
|
7073
|
+
const calculatePositionAsset = (asset, currentPrice, totalInitialPositionSize, totalCurrentPositionSize, leverage, metadata, isLong = true) => {
|
|
6797
7074
|
var _a;
|
|
6798
7075
|
const entryNotional = asset.entryPrice * asset.size;
|
|
6799
7076
|
const currentNotional = currentPrice * asset.size;
|
|
6800
|
-
const
|
|
7077
|
+
const effectiveLeverage = leverage || 1;
|
|
7078
|
+
const marginUsed = currentNotional / effectiveLeverage;
|
|
7079
|
+
const entryMarginUsed = entryNotional / effectiveLeverage;
|
|
6801
7080
|
const unrealizedPnl = isLong
|
|
6802
7081
|
? currentNotional - entryNotional
|
|
6803
7082
|
: entryNotional - currentNotional;
|
|
@@ -6807,11 +7086,14 @@ const calculatePositionAsset = (asset, currentPrice, totalInitialPositionSize, l
|
|
|
6807
7086
|
actualSize: asset.size,
|
|
6808
7087
|
leverage: leverage,
|
|
6809
7088
|
marginUsed: marginUsed,
|
|
7089
|
+
entryMarginUsed: entryMarginUsed,
|
|
6810
7090
|
positionValue: currentNotional,
|
|
6811
7091
|
unrealizedPnl: unrealizedPnl,
|
|
6812
7092
|
entryPositionValue: entryNotional,
|
|
6813
7093
|
initialWeight: totalInitialPositionSize > 0 ? entryNotional / totalInitialPositionSize : 0,
|
|
7094
|
+
currentWeight: totalCurrentPositionSize > 0 ? currentNotional / totalCurrentPositionSize : 0,
|
|
6814
7095
|
fundingPaid: (_a = asset.fundingPaid) !== null && _a !== void 0 ? _a : 0,
|
|
7096
|
+
targetWeight: asset.targetWeight,
|
|
6815
7097
|
metadata,
|
|
6816
7098
|
};
|
|
6817
7099
|
};
|
|
@@ -6832,20 +7114,32 @@ const buildPositionValue = (rawPositions, clearinghouseState, getAssetByName) =>
|
|
|
6832
7114
|
takeProfit: position.takeProfit,
|
|
6833
7115
|
stopLoss: position.stopLoss,
|
|
6834
7116
|
};
|
|
7117
|
+
let entryMarginUsed = 0;
|
|
6835
7118
|
const totalInitialPositionSize = position.longAssets.reduce((acc, asset) => acc + asset.entryPrice * asset.size, 0) +
|
|
6836
7119
|
position.shortAssets.reduce((acc, asset) => acc + asset.entryPrice * asset.size, 0);
|
|
7120
|
+
const totalCurrentPositionSize = position.longAssets.reduce((acc, asset) => {
|
|
7121
|
+
var _a, _b;
|
|
7122
|
+
const currentPrice = (_b = (_a = getAssetByName(asset.coin)) === null || _a === void 0 ? void 0 : _a.currentPrice) !== null && _b !== void 0 ? _b : 0;
|
|
7123
|
+
return acc + currentPrice * asset.size;
|
|
7124
|
+
}, 0) +
|
|
7125
|
+
position.shortAssets.reduce((acc, asset) => {
|
|
7126
|
+
var _a, _b;
|
|
7127
|
+
const currentPrice = (_b = (_a = getAssetByName(asset.coin)) === null || _a === void 0 ? void 0 : _a.currentPrice) !== null && _b !== void 0 ? _b : 0;
|
|
7128
|
+
return acc + currentPrice * asset.size;
|
|
7129
|
+
}, 0);
|
|
6837
7130
|
mappedPosition.longAssets = position.longAssets.map((longAsset) => {
|
|
6838
7131
|
var _a, _b, _c, _d;
|
|
6839
7132
|
const metadata = getAssetByName(longAsset.coin);
|
|
6840
7133
|
const currentPrice = (_a = metadata === null || metadata === void 0 ? void 0 : metadata.currentPrice) !== null && _a !== void 0 ? _a : 0;
|
|
6841
7134
|
const assetState = (_b = clearinghouseState.assetPositions.find((ap) => ap.position.coin === longAsset.coin)) === null || _b === void 0 ? void 0 : _b.position;
|
|
6842
7135
|
const leverage = (_d = (_c = assetState === null || assetState === void 0 ? void 0 : assetState.leverage) === null || _c === void 0 ? void 0 : _c.value) !== null && _d !== void 0 ? _d : longAsset.leverage;
|
|
6843
|
-
const mappedPositionAssets = calculatePositionAsset(longAsset, currentPrice, totalInitialPositionSize, leverage, metadata, true);
|
|
7136
|
+
const mappedPositionAssets = calculatePositionAsset(longAsset, currentPrice, totalInitialPositionSize, totalCurrentPositionSize, leverage, metadata, true);
|
|
6844
7137
|
mappedPosition.entryPositionValue +=
|
|
6845
7138
|
mappedPositionAssets.entryPositionValue;
|
|
6846
7139
|
mappedPosition.unrealizedPnl += mappedPositionAssets.unrealizedPnl;
|
|
6847
7140
|
mappedPosition.positionValue += mappedPositionAssets.positionValue;
|
|
6848
7141
|
mappedPosition.marginUsed += mappedPositionAssets.marginUsed;
|
|
7142
|
+
entryMarginUsed += mappedPositionAssets.entryMarginUsed;
|
|
6849
7143
|
mappedPosition.entryRatio *= Math.pow(longAsset.entryPrice, mappedPositionAssets.initialWeight);
|
|
6850
7144
|
mappedPosition.markRatio *= Math.pow(currentPrice, mappedPositionAssets.initialWeight);
|
|
6851
7145
|
return mappedPositionAssets;
|
|
@@ -6856,14 +7150,15 @@ const buildPositionValue = (rawPositions, clearinghouseState, getAssetByName) =>
|
|
|
6856
7150
|
const currentPrice = (_a = metadata === null || metadata === void 0 ? void 0 : metadata.currentPrice) !== null && _a !== void 0 ? _a : 0;
|
|
6857
7151
|
const assetState = (_b = clearinghouseState.assetPositions.find((ap) => ap.position.coin === shortAsset.coin)) === null || _b === void 0 ? void 0 : _b.position;
|
|
6858
7152
|
const leverage = (_d = (_c = assetState === null || assetState === void 0 ? void 0 : assetState.leverage) === null || _c === void 0 ? void 0 : _c.value) !== null && _d !== void 0 ? _d : shortAsset.leverage;
|
|
6859
|
-
const mappedPositionAssets = calculatePositionAsset(shortAsset, currentPrice, totalInitialPositionSize, leverage, metadata, false);
|
|
7153
|
+
const mappedPositionAssets = calculatePositionAsset(shortAsset, currentPrice, totalInitialPositionSize, totalCurrentPositionSize, leverage, metadata, false);
|
|
6860
7154
|
mappedPosition.entryPositionValue +=
|
|
6861
7155
|
mappedPositionAssets.entryPositionValue;
|
|
6862
7156
|
mappedPosition.unrealizedPnl += mappedPositionAssets.unrealizedPnl;
|
|
6863
7157
|
mappedPosition.positionValue += mappedPositionAssets.positionValue;
|
|
6864
7158
|
mappedPosition.marginUsed += mappedPositionAssets.marginUsed;
|
|
7159
|
+
entryMarginUsed += mappedPositionAssets.entryMarginUsed;
|
|
6865
7160
|
mappedPosition.entryRatio *= Math.pow(shortAsset.entryPrice, -mappedPositionAssets.initialWeight);
|
|
6866
|
-
mappedPosition.markRatio *= Math.pow(currentPrice, -mappedPositionAssets.
|
|
7161
|
+
mappedPosition.markRatio *= Math.pow(currentPrice, -mappedPositionAssets.currentWeight);
|
|
6867
7162
|
return mappedPositionAssets;
|
|
6868
7163
|
});
|
|
6869
7164
|
mappedPosition.positionValue =
|
|
@@ -6899,11 +7194,12 @@ const buildPositionValue = (rawPositions, clearinghouseState, getAssetByName) =>
|
|
|
6899
7194
|
mappedPosition.markPriceRatio = shortMark;
|
|
6900
7195
|
}
|
|
6901
7196
|
mappedPosition.unrealizedPnlPercentage =
|
|
6902
|
-
mappedPosition.unrealizedPnl /
|
|
7197
|
+
entryMarginUsed > 0 ? mappedPosition.unrealizedPnl / entryMarginUsed : 0;
|
|
6903
7198
|
return mappedPosition;
|
|
6904
7199
|
});
|
|
6905
7200
|
};
|
|
6906
7201
|
|
|
7202
|
+
const MIN_TRADE_SIZE_USD = 11;
|
|
6907
7203
|
function usePosition() {
|
|
6908
7204
|
const context = useContext(PearHyperliquidContext);
|
|
6909
7205
|
if (!context) {
|
|
@@ -6946,6 +7242,85 @@ function usePosition() {
|
|
|
6946
7242
|
return null;
|
|
6947
7243
|
return buildPositionValue(userOpenPositions, aggregatedClearingHouseState, getAssetByName);
|
|
6948
7244
|
}, [userOpenPositions, aggregatedClearingHouseState, tokenMetadata, getAssetByName]);
|
|
7245
|
+
const planRebalance = (positionId, targetWeights) => {
|
|
7246
|
+
var _a;
|
|
7247
|
+
if (!openPositions) {
|
|
7248
|
+
throw new Error('Open positions not loaded');
|
|
7249
|
+
}
|
|
7250
|
+
const position = openPositions.find((p) => p.positionId === positionId);
|
|
7251
|
+
if (!position) {
|
|
7252
|
+
throw new Error(`Position ${positionId} not found`);
|
|
7253
|
+
}
|
|
7254
|
+
const assets = [];
|
|
7255
|
+
const allAssets = [
|
|
7256
|
+
...position.longAssets.map((a) => ({ asset: a, side: 'long' })),
|
|
7257
|
+
...position.shortAssets.map((a) => ({ asset: a, side: 'short' })),
|
|
7258
|
+
];
|
|
7259
|
+
const totalValue = allAssets.reduce((sum, { asset }) => sum + asset.positionValue, 0);
|
|
7260
|
+
for (const { asset, side } of allAssets) {
|
|
7261
|
+
const tw = (_a = targetWeights === null || targetWeights === void 0 ? void 0 : targetWeights[asset.coin]) !== null && _a !== void 0 ? _a : asset.targetWeight;
|
|
7262
|
+
if (tw === undefined) {
|
|
7263
|
+
assets.push({
|
|
7264
|
+
coin: asset.coin,
|
|
7265
|
+
side,
|
|
7266
|
+
currentWeight: totalValue > 0 ? (asset.positionValue / totalValue) * 100 : 0,
|
|
7267
|
+
targetWeight: 0,
|
|
7268
|
+
currentValue: asset.positionValue,
|
|
7269
|
+
targetValue: asset.positionValue,
|
|
7270
|
+
deltaValue: 0,
|
|
7271
|
+
currentSize: asset.actualSize,
|
|
7272
|
+
newSize: asset.actualSize,
|
|
7273
|
+
deltaSize: 0,
|
|
7274
|
+
skipped: true,
|
|
7275
|
+
skipReason: 'No target weight defined',
|
|
7276
|
+
});
|
|
7277
|
+
continue;
|
|
7278
|
+
}
|
|
7279
|
+
const currentWeight = totalValue > 0 ? (asset.positionValue / totalValue) * 100 : 0;
|
|
7280
|
+
const targetValue = totalValue * (tw / 100);
|
|
7281
|
+
const deltaValue = targetValue - asset.positionValue;
|
|
7282
|
+
const currentPrice = asset.actualSize > 0 ? asset.positionValue / asset.actualSize : 0;
|
|
7283
|
+
const deltaSize = currentPrice > 0 ? deltaValue / currentPrice : 0;
|
|
7284
|
+
const newSize = asset.actualSize + deltaSize;
|
|
7285
|
+
const belowMinimum = Math.abs(deltaValue) > 0 && Math.abs(deltaValue) < MIN_TRADE_SIZE_USD;
|
|
7286
|
+
assets.push({
|
|
7287
|
+
coin: asset.coin,
|
|
7288
|
+
side,
|
|
7289
|
+
currentWeight,
|
|
7290
|
+
targetWeight: tw,
|
|
7291
|
+
currentValue: asset.positionValue,
|
|
7292
|
+
targetValue,
|
|
7293
|
+
deltaValue,
|
|
7294
|
+
currentSize: asset.actualSize,
|
|
7295
|
+
newSize,
|
|
7296
|
+
deltaSize,
|
|
7297
|
+
skipped: belowMinimum,
|
|
7298
|
+
skipReason: belowMinimum
|
|
7299
|
+
? `Trade size $${Math.abs(deltaValue).toFixed(2)} is below minimum $${MIN_TRADE_SIZE_USD}`
|
|
7300
|
+
: undefined,
|
|
7301
|
+
});
|
|
7302
|
+
}
|
|
7303
|
+
const canExecute = assets.some((a) => !a.skipped && Math.abs(a.deltaValue) > 0);
|
|
7304
|
+
return { positionId, assets, canExecute };
|
|
7305
|
+
};
|
|
7306
|
+
const executeRebalance = async (positionId, targetWeights) => {
|
|
7307
|
+
const plan = planRebalance(positionId, targetWeights);
|
|
7308
|
+
if (!plan.canExecute) {
|
|
7309
|
+
throw new Error('No executable rebalance changes — all below minimum trade size or no changes needed');
|
|
7310
|
+
}
|
|
7311
|
+
const executable = plan.assets.filter((a) => !a.skipped && Math.abs(a.deltaValue) > 0);
|
|
7312
|
+
const payload = [
|
|
7313
|
+
{
|
|
7314
|
+
longAssets: executable
|
|
7315
|
+
.filter((a) => a.side === 'long')
|
|
7316
|
+
.map((a) => ({ asset: a.coin, size: a.newSize })),
|
|
7317
|
+
shortAssets: executable
|
|
7318
|
+
.filter((a) => a.side === 'short')
|
|
7319
|
+
.map((a) => ({ asset: a.coin, size: a.newSize })),
|
|
7320
|
+
},
|
|
7321
|
+
];
|
|
7322
|
+
return adjustAdvancePosition$1(positionId, payload);
|
|
7323
|
+
};
|
|
6949
7324
|
return {
|
|
6950
7325
|
createPosition: createPosition$1,
|
|
6951
7326
|
updateRiskParameters: updateRiskParameters$1,
|
|
@@ -6956,6 +7331,8 @@ function usePosition() {
|
|
|
6956
7331
|
updateLeverage: updateLeverage$1,
|
|
6957
7332
|
openPositions,
|
|
6958
7333
|
isLoading,
|
|
7334
|
+
planRebalance,
|
|
7335
|
+
executeRebalance,
|
|
6959
7336
|
};
|
|
6960
7337
|
}
|
|
6961
7338
|
|
|
@@ -7389,20 +7766,34 @@ function usePortfolio() {
|
|
|
7389
7766
|
}
|
|
7390
7767
|
|
|
7391
7768
|
async function getEIP712Message(baseUrl, address, clientId) {
|
|
7392
|
-
const url = joinUrl(baseUrl,
|
|
7769
|
+
const url = joinUrl(baseUrl, "/auth/eip712-message");
|
|
7393
7770
|
try {
|
|
7394
|
-
const resp = await
|
|
7395
|
-
|
|
7771
|
+
const resp = await apiClient.get(url, {
|
|
7772
|
+
params: { address, clientId },
|
|
7773
|
+
timeout: 30000,
|
|
7774
|
+
});
|
|
7775
|
+
return {
|
|
7776
|
+
data: resp.data,
|
|
7777
|
+
status: resp.status,
|
|
7778
|
+
headers: resp.headers,
|
|
7779
|
+
};
|
|
7396
7780
|
}
|
|
7397
7781
|
catch (error) {
|
|
7398
7782
|
throw toApiError(error);
|
|
7399
7783
|
}
|
|
7400
7784
|
}
|
|
7401
7785
|
async function authenticate(baseUrl, body) {
|
|
7402
|
-
const url = joinUrl(baseUrl,
|
|
7786
|
+
const url = joinUrl(baseUrl, "/auth/login");
|
|
7403
7787
|
try {
|
|
7404
|
-
const resp = await
|
|
7405
|
-
|
|
7788
|
+
const resp = await apiClient.post(url, body, {
|
|
7789
|
+
headers: { "Content-Type": "application/json" },
|
|
7790
|
+
timeout: 30000,
|
|
7791
|
+
});
|
|
7792
|
+
return {
|
|
7793
|
+
data: resp.data,
|
|
7794
|
+
status: resp.status,
|
|
7795
|
+
headers: resp.headers,
|
|
7796
|
+
};
|
|
7406
7797
|
}
|
|
7407
7798
|
catch (error) {
|
|
7408
7799
|
throw toApiError(error);
|
|
@@ -7413,7 +7804,7 @@ async function authenticate(baseUrl, body) {
|
|
|
7413
7804
|
*/
|
|
7414
7805
|
async function authenticateWithPrivy(baseUrl, params) {
|
|
7415
7806
|
const body = {
|
|
7416
|
-
method:
|
|
7807
|
+
method: "privy_access_token",
|
|
7417
7808
|
address: params.address,
|
|
7418
7809
|
clientId: params.clientId,
|
|
7419
7810
|
details: { appId: params.appId, accessToken: params.accessToken },
|
|
@@ -7421,61 +7812,124 @@ async function authenticateWithPrivy(baseUrl, params) {
|
|
|
7421
7812
|
return authenticate(baseUrl, body);
|
|
7422
7813
|
}
|
|
7423
7814
|
async function refreshToken(baseUrl, refreshTokenVal) {
|
|
7424
|
-
const url = joinUrl(baseUrl,
|
|
7815
|
+
const url = joinUrl(baseUrl, "/auth/refresh");
|
|
7425
7816
|
try {
|
|
7426
|
-
const resp = await
|
|
7427
|
-
return {
|
|
7817
|
+
const resp = await apiClient.post(url, { refreshToken: refreshTokenVal }, { headers: { "Content-Type": "application/json" }, timeout: 30000 });
|
|
7818
|
+
return {
|
|
7819
|
+
data: resp.data,
|
|
7820
|
+
status: resp.status,
|
|
7821
|
+
headers: resp.headers,
|
|
7822
|
+
};
|
|
7428
7823
|
}
|
|
7429
7824
|
catch (error) {
|
|
7430
7825
|
throw toApiError(error);
|
|
7431
7826
|
}
|
|
7432
7827
|
}
|
|
7433
7828
|
async function logout(baseUrl, refreshTokenVal) {
|
|
7434
|
-
const url = joinUrl(baseUrl,
|
|
7829
|
+
const url = joinUrl(baseUrl, "/auth/logout");
|
|
7435
7830
|
try {
|
|
7436
|
-
const resp = await
|
|
7437
|
-
return {
|
|
7831
|
+
const resp = await apiClient.post(url, { refreshToken: refreshTokenVal }, { headers: { "Content-Type": "application/json" }, timeout: 30000 });
|
|
7832
|
+
return {
|
|
7833
|
+
data: resp.data,
|
|
7834
|
+
status: resp.status,
|
|
7835
|
+
headers: resp.headers,
|
|
7836
|
+
};
|
|
7438
7837
|
}
|
|
7439
7838
|
catch (error) {
|
|
7440
7839
|
throw toApiError(error);
|
|
7441
7840
|
}
|
|
7442
7841
|
}
|
|
7443
7842
|
|
|
7843
|
+
// Token expiration constants
|
|
7844
|
+
const ACCESS_TOKEN_BUFFER_MS = 5 * 60 * 1000; // Refresh 5 min before expiry
|
|
7845
|
+
const REFRESH_TOKEN_TTL_MS = 7 * 24 * 60 * 60 * 1000; // 7 days fallback
|
|
7846
|
+
function nowMs() {
|
|
7847
|
+
return Date.now();
|
|
7848
|
+
}
|
|
7849
|
+
function calcExpiresAt(expiresInSeconds) {
|
|
7850
|
+
return nowMs() + expiresInSeconds * 1000;
|
|
7851
|
+
}
|
|
7444
7852
|
function useAuth() {
|
|
7445
7853
|
const context = useContext(PearHyperliquidContext);
|
|
7446
7854
|
if (!context) {
|
|
7447
|
-
throw new Error(
|
|
7855
|
+
throw new Error('useAuth must be used within a PearHyperliquidProvider');
|
|
7448
7856
|
}
|
|
7449
7857
|
const { apiBaseUrl, clientId } = context;
|
|
7450
|
-
const [isReady, setIsReady] = useState(false);
|
|
7451
7858
|
const accessToken = useUserData((s) => s.accessToken);
|
|
7452
7859
|
const refreshToken$1 = useUserData((s) => s.refreshToken);
|
|
7860
|
+
const isReady = useUserData((s) => s.isReady);
|
|
7861
|
+
const isAuthenticated = useUserData((s) => s.isAuthenticated);
|
|
7453
7862
|
const setAccessToken = useUserData((s) => s.setAccessToken);
|
|
7454
7863
|
const setRefreshToken = useUserData((s) => s.setRefreshToken);
|
|
7455
|
-
const
|
|
7864
|
+
const setIsReady = useUserData((s) => s.setIsReady);
|
|
7456
7865
|
const setIsAuthenticated = useUserData((s) => s.setIsAuthenticated);
|
|
7866
|
+
const address = useUserData((s) => s.address);
|
|
7457
7867
|
const setAddress = useUserData((s) => s.setAddress);
|
|
7868
|
+
// Ref to prevent concurrent refresh attempts
|
|
7869
|
+
const isRefreshingRef = useRef(false);
|
|
7458
7870
|
useEffect(() => {
|
|
7459
|
-
if (typeof window ==
|
|
7871
|
+
if (typeof window == 'undefined') {
|
|
7460
7872
|
return;
|
|
7461
7873
|
}
|
|
7462
|
-
|
|
7463
|
-
|
|
7464
|
-
|
|
7465
|
-
|
|
7466
|
-
|
|
7467
|
-
|
|
7468
|
-
|
|
7469
|
-
|
|
7470
|
-
|
|
7471
|
-
|
|
7874
|
+
if (address) {
|
|
7875
|
+
const accessTokenKey = `${address}_accessToken`;
|
|
7876
|
+
const refreshTokenKey = `${address}_refreshToken`;
|
|
7877
|
+
const accessTokenExpiresAtKey = `${address}_accessTokenExpiresAt`;
|
|
7878
|
+
const refreshTokenExpiresAtKey = `${address}_refreshTokenExpiresAt`;
|
|
7879
|
+
const storedAccessToken = localStorage.getItem(accessTokenKey);
|
|
7880
|
+
const storedRefreshToken = localStorage.getItem(refreshTokenKey);
|
|
7881
|
+
const accessExpRaw = localStorage.getItem(accessTokenExpiresAtKey);
|
|
7882
|
+
const refreshExpRaw = localStorage.getItem(refreshTokenExpiresAtKey);
|
|
7883
|
+
const accessExp = accessExpRaw ? Number(accessExpRaw) : 0;
|
|
7884
|
+
const refreshExp = refreshExpRaw ? Number(refreshExpRaw) : 0;
|
|
7885
|
+
const now = nowMs();
|
|
7886
|
+
const accessValid = !!storedAccessToken && accessExp > now;
|
|
7887
|
+
const refreshValid = !!storedRefreshToken && refreshExp > now;
|
|
7888
|
+
if (accessValid && refreshValid) {
|
|
7889
|
+
// Both tokens are valid
|
|
7890
|
+
setAccessToken(storedAccessToken);
|
|
7891
|
+
setRefreshToken(storedRefreshToken);
|
|
7892
|
+
setIsAuthenticated(true);
|
|
7893
|
+
setIsReady(true);
|
|
7894
|
+
}
|
|
7895
|
+
else if (refreshValid) {
|
|
7896
|
+
// Access token expired but refresh still valid → refresh immediately
|
|
7897
|
+
setAccessToken(storedAccessToken);
|
|
7898
|
+
setRefreshToken(storedRefreshToken);
|
|
7899
|
+
(async () => {
|
|
7900
|
+
try {
|
|
7901
|
+
await refreshTokens();
|
|
7902
|
+
}
|
|
7903
|
+
catch (_a) {
|
|
7904
|
+
// Refresh failed → clear tokens
|
|
7905
|
+
setAccessToken(null);
|
|
7906
|
+
setRefreshToken(null);
|
|
7907
|
+
setIsAuthenticated(false);
|
|
7908
|
+
}
|
|
7909
|
+
setIsReady(true);
|
|
7910
|
+
})();
|
|
7911
|
+
return; // setIsReady will be called in the async block
|
|
7912
|
+
}
|
|
7913
|
+
else {
|
|
7914
|
+
// Refresh expired or no tokens → clear
|
|
7915
|
+
setAccessToken(null);
|
|
7916
|
+
setRefreshToken(null);
|
|
7917
|
+
setIsAuthenticated(false);
|
|
7918
|
+
setIsReady(true);
|
|
7919
|
+
}
|
|
7920
|
+
}
|
|
7921
|
+
else {
|
|
7922
|
+
setIsReady(true);
|
|
7923
|
+
}
|
|
7924
|
+
}, [address]);
|
|
7472
7925
|
useEffect(() => {
|
|
7473
7926
|
const cleanup = addAuthInterceptors({
|
|
7474
7927
|
apiBaseUrl,
|
|
7475
7928
|
getAccessToken: () => {
|
|
7476
|
-
|
|
7477
|
-
|
|
7478
|
-
|
|
7929
|
+
if (typeof window === 'undefined')
|
|
7930
|
+
return null;
|
|
7931
|
+
// Read from Zustand state as single source of truth
|
|
7932
|
+
return useUserData.getState().accessToken;
|
|
7479
7933
|
},
|
|
7480
7934
|
refreshTokens: async () => {
|
|
7481
7935
|
const data = await refreshTokens();
|
|
@@ -7489,6 +7943,55 @@ function useAuth() {
|
|
|
7489
7943
|
cleanup();
|
|
7490
7944
|
};
|
|
7491
7945
|
}, [apiBaseUrl]);
|
|
7946
|
+
// Proactive refresh effect: refresh when app regains focus or timer fires
|
|
7947
|
+
useEffect(() => {
|
|
7948
|
+
if (typeof window === 'undefined' || !address || !refreshToken$1)
|
|
7949
|
+
return;
|
|
7950
|
+
const refreshIfNeeded = async () => {
|
|
7951
|
+
// Prevent concurrent refresh attempts
|
|
7952
|
+
if (isRefreshingRef.current)
|
|
7953
|
+
return;
|
|
7954
|
+
// Read fresh expiration values from localStorage (not stale closure)
|
|
7955
|
+
const accessExpRaw = localStorage.getItem(`${address}_accessTokenExpiresAt`);
|
|
7956
|
+
const refreshExpRaw = localStorage.getItem(`${address}_refreshTokenExpiresAt`);
|
|
7957
|
+
const accessExp = accessExpRaw ? Number(accessExpRaw) : 0;
|
|
7958
|
+
const refreshExp = refreshExpRaw ? Number(refreshExpRaw) : 0;
|
|
7959
|
+
const now = nowMs();
|
|
7960
|
+
// If refresh token is already expired, do nothing
|
|
7961
|
+
if (refreshExp <= now)
|
|
7962
|
+
return;
|
|
7963
|
+
// If access token is within buffer window, refresh
|
|
7964
|
+
if (accessExp - now <= ACCESS_TOKEN_BUFFER_MS) {
|
|
7965
|
+
isRefreshingRef.current = true;
|
|
7966
|
+
try {
|
|
7967
|
+
await refreshTokens();
|
|
7968
|
+
}
|
|
7969
|
+
catch (_a) {
|
|
7970
|
+
// Refresh failed, interceptor will handle logout on next API call
|
|
7971
|
+
}
|
|
7972
|
+
finally {
|
|
7973
|
+
isRefreshingRef.current = false;
|
|
7974
|
+
}
|
|
7975
|
+
}
|
|
7976
|
+
};
|
|
7977
|
+
const onVisibilityChange = () => {
|
|
7978
|
+
if (document.visibilityState === 'visible') {
|
|
7979
|
+
refreshIfNeeded();
|
|
7980
|
+
}
|
|
7981
|
+
};
|
|
7982
|
+
document.addEventListener('visibilitychange', onVisibilityChange);
|
|
7983
|
+
// Schedule timer for (accessExp - buffer)
|
|
7984
|
+
const accessExpRaw = localStorage.getItem(`${address}_accessTokenExpiresAt`);
|
|
7985
|
+
const accessExp = accessExpRaw ? Number(accessExpRaw) : 0;
|
|
7986
|
+
const delay = Math.max(0, accessExp - nowMs() - ACCESS_TOKEN_BUFFER_MS);
|
|
7987
|
+
const timer = window.setTimeout(() => {
|
|
7988
|
+
refreshIfNeeded();
|
|
7989
|
+
}, delay);
|
|
7990
|
+
return () => {
|
|
7991
|
+
document.removeEventListener('visibilitychange', onVisibilityChange);
|
|
7992
|
+
clearTimeout(timer);
|
|
7993
|
+
};
|
|
7994
|
+
}, [address, refreshToken$1]);
|
|
7492
7995
|
async function getEip712(address) {
|
|
7493
7996
|
const { data } = await getEIP712Message(apiBaseUrl, address, clientId);
|
|
7494
7997
|
return data;
|
|
@@ -7496,17 +7999,21 @@ function useAuth() {
|
|
|
7496
7999
|
async function loginWithSignedMessage(address, signature, timestamp) {
|
|
7497
8000
|
try {
|
|
7498
8001
|
const { data } = await authenticate(apiBaseUrl, {
|
|
7499
|
-
method:
|
|
8002
|
+
method: 'eip712',
|
|
7500
8003
|
address,
|
|
7501
8004
|
clientId,
|
|
7502
8005
|
details: { signature, timestamp },
|
|
7503
8006
|
});
|
|
7504
|
-
|
|
7505
|
-
|
|
7506
|
-
|
|
8007
|
+
const accessTokenKey = `${address}_accessToken`;
|
|
8008
|
+
const refreshTokenKey = `${address}_refreshToken`;
|
|
8009
|
+
const accessTokenExpiresAtKey = `${address}_accessTokenExpiresAt`;
|
|
8010
|
+
const refreshTokenExpiresAtKey = `${address}_refreshTokenExpiresAt`;
|
|
8011
|
+
window.localStorage.setItem(accessTokenKey, data.accessToken);
|
|
8012
|
+
window.localStorage.setItem(refreshTokenKey, data.refreshToken);
|
|
8013
|
+
window.localStorage.setItem(accessTokenExpiresAtKey, String(calcExpiresAt(data.expiresIn)));
|
|
8014
|
+
window.localStorage.setItem(refreshTokenExpiresAtKey, String(nowMs() + REFRESH_TOKEN_TTL_MS));
|
|
7507
8015
|
setAccessToken(data.accessToken);
|
|
7508
8016
|
setRefreshToken(data.refreshToken);
|
|
7509
|
-
setAddress(address);
|
|
7510
8017
|
setIsAuthenticated(true);
|
|
7511
8018
|
}
|
|
7512
8019
|
catch (e) {
|
|
@@ -7521,12 +8028,16 @@ function useAuth() {
|
|
|
7521
8028
|
appId,
|
|
7522
8029
|
accessToken: privyAccessToken,
|
|
7523
8030
|
});
|
|
7524
|
-
|
|
7525
|
-
|
|
7526
|
-
|
|
8031
|
+
const accessTokenKey = `${address}_accessToken`;
|
|
8032
|
+
const refreshTokenKey = `${address}_refreshToken`;
|
|
8033
|
+
const accessTokenExpiresAtKey = `${address}_accessTokenExpiresAt`;
|
|
8034
|
+
const refreshTokenExpiresAtKey = `${address}_refreshTokenExpiresAt`;
|
|
8035
|
+
window.localStorage.setItem(accessTokenKey, data.accessToken);
|
|
8036
|
+
window.localStorage.setItem(refreshTokenKey, data.refreshToken);
|
|
8037
|
+
window.localStorage.setItem(accessTokenExpiresAtKey, String(calcExpiresAt(data.expiresIn)));
|
|
8038
|
+
window.localStorage.setItem(refreshTokenExpiresAtKey, String(nowMs() + REFRESH_TOKEN_TTL_MS));
|
|
7527
8039
|
setAccessToken(data.accessToken);
|
|
7528
8040
|
setRefreshToken(data.refreshToken);
|
|
7529
|
-
setAddress(address);
|
|
7530
8041
|
setIsAuthenticated(true);
|
|
7531
8042
|
}
|
|
7532
8043
|
catch (e) {
|
|
@@ -7534,38 +8045,60 @@ function useAuth() {
|
|
|
7534
8045
|
}
|
|
7535
8046
|
}
|
|
7536
8047
|
async function refreshTokens() {
|
|
7537
|
-
const
|
|
7538
|
-
|
|
7539
|
-
|
|
7540
|
-
|
|
7541
|
-
|
|
7542
|
-
|
|
8048
|
+
const currentAddress = address;
|
|
8049
|
+
const currentRefresh = refreshToken$1;
|
|
8050
|
+
if (!currentRefresh || !currentAddress)
|
|
8051
|
+
throw new Error('No refresh token');
|
|
8052
|
+
const { data } = await refreshToken(apiBaseUrl, currentRefresh);
|
|
8053
|
+
// Update tokens in localStorage
|
|
8054
|
+
const accessTokenKey = `${currentAddress}_accessToken`;
|
|
8055
|
+
const refreshTokenKey = `${currentAddress}_refreshToken`;
|
|
8056
|
+
const accessTokenExpiresAtKey = `${currentAddress}_accessTokenExpiresAt`;
|
|
8057
|
+
const refreshTokenExpiresAtKey = `${currentAddress}_refreshTokenExpiresAt`;
|
|
8058
|
+
window.localStorage.setItem(accessTokenKey, data.accessToken);
|
|
8059
|
+
window.localStorage.setItem(refreshTokenKey, data.refreshToken);
|
|
8060
|
+
window.localStorage.setItem(accessTokenExpiresAtKey, String(calcExpiresAt(data.expiresIn)));
|
|
8061
|
+
window.localStorage.setItem(refreshTokenExpiresAtKey, String(nowMs() + REFRESH_TOKEN_TTL_MS));
|
|
7543
8062
|
setAccessToken(data.accessToken);
|
|
7544
8063
|
setRefreshToken(data.refreshToken);
|
|
7545
8064
|
setIsAuthenticated(true);
|
|
7546
8065
|
return data;
|
|
7547
8066
|
}
|
|
7548
8067
|
async function logout$1() {
|
|
7549
|
-
const
|
|
7550
|
-
|
|
8068
|
+
const currentAddress = address;
|
|
8069
|
+
const currentRefresh = refreshToken$1;
|
|
8070
|
+
if (currentRefresh) {
|
|
7551
8071
|
try {
|
|
7552
|
-
await logout(apiBaseUrl,
|
|
8072
|
+
await logout(apiBaseUrl, currentRefresh);
|
|
7553
8073
|
}
|
|
7554
8074
|
catch (_a) {
|
|
7555
8075
|
/* ignore */
|
|
7556
8076
|
}
|
|
7557
8077
|
}
|
|
7558
|
-
|
|
7559
|
-
|
|
7560
|
-
|
|
8078
|
+
if (currentAddress) {
|
|
8079
|
+
const accessTokenKey = `${currentAddress}_accessToken`;
|
|
8080
|
+
const refreshTokenKey = `${currentAddress}_refreshToken`;
|
|
8081
|
+
const accessTokenExpiresAtKey = `${currentAddress}_accessTokenExpiresAt`;
|
|
8082
|
+
const refreshTokenExpiresAtKey = `${currentAddress}_refreshTokenExpiresAt`;
|
|
8083
|
+
window.localStorage.removeItem(accessTokenKey);
|
|
8084
|
+
window.localStorage.removeItem(refreshTokenKey);
|
|
8085
|
+
window.localStorage.removeItem(accessTokenExpiresAtKey);
|
|
8086
|
+
window.localStorage.removeItem(refreshTokenExpiresAtKey);
|
|
8087
|
+
}
|
|
7561
8088
|
setAccessToken(null);
|
|
7562
8089
|
setRefreshToken(null);
|
|
7563
8090
|
setAddress(null);
|
|
7564
8091
|
setIsAuthenticated(false);
|
|
7565
8092
|
}
|
|
8093
|
+
function clearSession() {
|
|
8094
|
+
setAccessToken(null);
|
|
8095
|
+
setRefreshToken(null);
|
|
8096
|
+
setIsAuthenticated(false);
|
|
8097
|
+
}
|
|
7566
8098
|
return {
|
|
7567
8099
|
isReady,
|
|
7568
8100
|
isAuthenticated,
|
|
8101
|
+
address,
|
|
7569
8102
|
accessToken,
|
|
7570
8103
|
refreshToken: refreshToken$1,
|
|
7571
8104
|
getEip712,
|
|
@@ -7573,6 +8106,8 @@ function useAuth() {
|
|
|
7573
8106
|
loginWithPrivyToken,
|
|
7574
8107
|
refreshTokens,
|
|
7575
8108
|
logout: logout$1,
|
|
8109
|
+
clearSession,
|
|
8110
|
+
setAddress,
|
|
7576
8111
|
};
|
|
7577
8112
|
}
|
|
7578
8113
|
|
|
@@ -7613,14 +8148,20 @@ const useAllUserBalances = () => {
|
|
|
7613
8148
|
}
|
|
7614
8149
|
}
|
|
7615
8150
|
if (!availableToTrades['USDC']) {
|
|
7616
|
-
availableToTrades['USDC'] = parseFloat((aggregatedClearingHouseState === null || aggregatedClearingHouseState === void 0 ? void 0 : aggregatedClearingHouseState.marginSummary.
|
|
8151
|
+
availableToTrades['USDC'] = Math.max(0, parseFloat((aggregatedClearingHouseState === null || aggregatedClearingHouseState === void 0 ? void 0 : aggregatedClearingHouseState.marginSummary.accountValue) || '0') -
|
|
8152
|
+
parseFloat((aggregatedClearingHouseState === null || aggregatedClearingHouseState === void 0 ? void 0 : aggregatedClearingHouseState.marginSummary.totalMarginUsed) || '0'));
|
|
7617
8153
|
}
|
|
7618
8154
|
return {
|
|
7619
8155
|
spotBalances,
|
|
7620
8156
|
availableToTrades,
|
|
7621
8157
|
isLoading,
|
|
7622
8158
|
};
|
|
7623
|
-
}, [
|
|
8159
|
+
}, [
|
|
8160
|
+
spotState,
|
|
8161
|
+
longTokensMetadata,
|
|
8162
|
+
shortTokensMetadata,
|
|
8163
|
+
aggregatedClearingHouseState,
|
|
8164
|
+
]);
|
|
7624
8165
|
/**
|
|
7625
8166
|
* Calculate margin required for every collateral token based on asset leverages and size.
|
|
7626
8167
|
* Returns margin required per collateral and whether there's sufficient margin.
|
|
@@ -7740,6 +8281,7 @@ const useAllUserBalances = () => {
|
|
|
7740
8281
|
availableToTrades,
|
|
7741
8282
|
]);
|
|
7742
8283
|
return {
|
|
8284
|
+
abstractionMode: useUserData((state) => state.userAbstractionMode),
|
|
7743
8285
|
spotBalances,
|
|
7744
8286
|
availableToTrades,
|
|
7745
8287
|
isLoading,
|
|
@@ -7880,13 +8422,470 @@ function useHyperliquidUserFills(options) {
|
|
|
7880
8422
|
};
|
|
7881
8423
|
}
|
|
7882
8424
|
|
|
8425
|
+
async function getTradeHistory(baseUrl, params) {
|
|
8426
|
+
const url = joinUrl(baseUrl, '/trade-history');
|
|
8427
|
+
try {
|
|
8428
|
+
const resp = await apiClient.get(url, {
|
|
8429
|
+
params,
|
|
8430
|
+
timeout: 60000,
|
|
8431
|
+
});
|
|
8432
|
+
return { data: resp.data, status: resp.status, headers: resp.headers };
|
|
8433
|
+
}
|
|
8434
|
+
catch (error) {
|
|
8435
|
+
throw toApiError(error);
|
|
8436
|
+
}
|
|
8437
|
+
}
|
|
8438
|
+
|
|
8439
|
+
// ─── helpers ────────────────────────────────────────────────────
|
|
8440
|
+
const EMPTY_SUMMARY = {
|
|
8441
|
+
pnl: 0,
|
|
8442
|
+
volume: 0,
|
|
8443
|
+
winRate: 0,
|
|
8444
|
+
wins: 0,
|
|
8445
|
+
losses: 0,
|
|
8446
|
+
totalProfit: 0,
|
|
8447
|
+
totalLoss: 0,
|
|
8448
|
+
};
|
|
8449
|
+
const getTimeframeDays$1 = (tf) => {
|
|
8450
|
+
switch (tf) {
|
|
8451
|
+
case '2W':
|
|
8452
|
+
return 14;
|
|
8453
|
+
case '3W':
|
|
8454
|
+
return 21;
|
|
8455
|
+
case '2M':
|
|
8456
|
+
return 60;
|
|
8457
|
+
case '3M':
|
|
8458
|
+
return 90;
|
|
8459
|
+
}
|
|
8460
|
+
};
|
|
8461
|
+
const isWeekTimeframe = (tf) => tf === '2W' || tf === '3W';
|
|
8462
|
+
const toDateKey = (date) => {
|
|
8463
|
+
const y = date.getFullYear();
|
|
8464
|
+
const m = String(date.getMonth() + 1).padStart(2, '0');
|
|
8465
|
+
const d = String(date.getDate()).padStart(2, '0');
|
|
8466
|
+
return `${y}-${m}-${d}`;
|
|
8467
|
+
};
|
|
8468
|
+
const toMonthKey = (date) => {
|
|
8469
|
+
const y = date.getFullYear();
|
|
8470
|
+
const m = String(date.getMonth() + 1).padStart(2, '0');
|
|
8471
|
+
return `${y}-${m}`;
|
|
8472
|
+
};
|
|
8473
|
+
const formatMonthLabel = (date) => date.toLocaleDateString('en-US', { month: 'short', year: 'numeric' });
|
|
8474
|
+
const getMonday = (date) => {
|
|
8475
|
+
const d = new Date(date);
|
|
8476
|
+
const day = d.getDay(); // 0=Sun … 6=Sat
|
|
8477
|
+
const diff = day === 0 ? -6 : 1 - day;
|
|
8478
|
+
d.setDate(d.getDate() + diff);
|
|
8479
|
+
d.setHours(0, 0, 0, 0);
|
|
8480
|
+
return d;
|
|
8481
|
+
};
|
|
8482
|
+
const toLocalMidnight = (input) => {
|
|
8483
|
+
const d = typeof input === 'string' ? new Date(input + 'T00:00:00') : new Date(input);
|
|
8484
|
+
d.setHours(0, 0, 0, 0);
|
|
8485
|
+
return d;
|
|
8486
|
+
};
|
|
8487
|
+
const diffDays = (start, end) => {
|
|
8488
|
+
return Math.round((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24)) + 1;
|
|
8489
|
+
};
|
|
8490
|
+
const toISODateString$1 = (input) => {
|
|
8491
|
+
const d = typeof input === 'string' ? new Date(input + 'T00:00:00') : new Date(input);
|
|
8492
|
+
return d.toISOString();
|
|
8493
|
+
};
|
|
8494
|
+
const round2 = (n) => Math.round(n * 100) / 100;
|
|
8495
|
+
const mapAsset$1 = (asset, getAssetByName) => {
|
|
8496
|
+
var _a, _b, _c, _d;
|
|
8497
|
+
const metadata = getAssetByName(asset.coin);
|
|
8498
|
+
const marketInfo = getMarketInfoFromSymbol(asset.coin);
|
|
8499
|
+
return {
|
|
8500
|
+
coin: asset.coin,
|
|
8501
|
+
symbol: (_a = metadata === null || metadata === void 0 ? void 0 : metadata.symbolName) !== null && _a !== void 0 ? _a : marketInfo.symbolName,
|
|
8502
|
+
assetName: (_b = metadata === null || metadata === void 0 ? void 0 : metadata.assetName) !== null && _b !== void 0 ? _b : asset.coin,
|
|
8503
|
+
marketPrefix: (_c = metadata === null || metadata === void 0 ? void 0 : metadata.marketName) !== null && _c !== void 0 ? _c : marketInfo.marketName,
|
|
8504
|
+
percentage: asset.closeWeight * 100,
|
|
8505
|
+
collateralToken: (_d = metadata === null || metadata === void 0 ? void 0 : metadata.collateralToken) !== null && _d !== void 0 ? _d : 'USDC',
|
|
8506
|
+
};
|
|
8507
|
+
};
|
|
8508
|
+
const getCollateralTypes$1 = (assets) => {
|
|
8509
|
+
const set = new Set();
|
|
8510
|
+
for (const a of assets)
|
|
8511
|
+
set.add(a.collateralToken);
|
|
8512
|
+
return set.size > 0 ? Array.from(set) : ['USDC'];
|
|
8513
|
+
};
|
|
8514
|
+
const buildSummary = (days) => {
|
|
8515
|
+
let pnl = 0;
|
|
8516
|
+
let volume = 0;
|
|
8517
|
+
let wins = 0;
|
|
8518
|
+
let losses = 0;
|
|
8519
|
+
let totalProfit = 0;
|
|
8520
|
+
let totalLoss = 0;
|
|
8521
|
+
for (const day of days) {
|
|
8522
|
+
pnl += day.totalPnl;
|
|
8523
|
+
volume += day.volume;
|
|
8524
|
+
if (day.positionsClosed === 0)
|
|
8525
|
+
continue;
|
|
8526
|
+
if (day.totalPnl > 0) {
|
|
8527
|
+
wins++;
|
|
8528
|
+
totalProfit += day.totalPnl;
|
|
8529
|
+
}
|
|
8530
|
+
else if (day.totalPnl < 0) {
|
|
8531
|
+
losses++;
|
|
8532
|
+
totalLoss += Math.abs(day.totalPnl);
|
|
8533
|
+
}
|
|
8534
|
+
}
|
|
8535
|
+
const total = wins + losses;
|
|
8536
|
+
const winRate = total > 0 ? Math.round((wins / total) * 100) : 0;
|
|
8537
|
+
return {
|
|
8538
|
+
pnl: round2(pnl),
|
|
8539
|
+
volume: round2(volume),
|
|
8540
|
+
winRate,
|
|
8541
|
+
wins,
|
|
8542
|
+
losses,
|
|
8543
|
+
totalProfit: round2(totalProfit),
|
|
8544
|
+
totalLoss: round2(totalLoss),
|
|
8545
|
+
};
|
|
8546
|
+
};
|
|
8547
|
+
const buildCalendarData = (tradeHistories, timeframe, rangeStart, rangeEnd, totalDays, useCustomDates, getAssetByName) => {
|
|
8548
|
+
const startKey = toDateKey(rangeStart);
|
|
8549
|
+
const endKey = toDateKey(rangeEnd);
|
|
8550
|
+
// Build day buckets for the full range
|
|
8551
|
+
const buckets = new Map();
|
|
8552
|
+
for (let i = 0; i < totalDays; i++) {
|
|
8553
|
+
const d = new Date(rangeStart);
|
|
8554
|
+
d.setDate(rangeStart.getDate() + i);
|
|
8555
|
+
buckets.set(toDateKey(d), {
|
|
8556
|
+
pnl: 0,
|
|
8557
|
+
volume: 0,
|
|
8558
|
+
positionsClosed: 0,
|
|
8559
|
+
trades: [],
|
|
8560
|
+
});
|
|
8561
|
+
}
|
|
8562
|
+
// Populate buckets from trade histories
|
|
8563
|
+
for (const trade of tradeHistories) {
|
|
8564
|
+
if (!trade.createdAt)
|
|
8565
|
+
continue;
|
|
8566
|
+
const date = new Date(trade.createdAt);
|
|
8567
|
+
if (isNaN(date.getTime()))
|
|
8568
|
+
continue;
|
|
8569
|
+
const dateKey = toDateKey(date);
|
|
8570
|
+
if (dateKey < startKey || dateKey > endKey)
|
|
8571
|
+
continue;
|
|
8572
|
+
const bucket = buckets.get(dateKey);
|
|
8573
|
+
if (!bucket)
|
|
8574
|
+
continue;
|
|
8575
|
+
const pnl = trade.realizedPnl;
|
|
8576
|
+
bucket.pnl += isFinite(pnl) ? pnl : 0;
|
|
8577
|
+
const vol = trade.totalValue;
|
|
8578
|
+
bucket.volume += isFinite(vol) ? vol : 0;
|
|
8579
|
+
bucket.positionsClosed += 1;
|
|
8580
|
+
const tradePnl = trade.realizedPnl;
|
|
8581
|
+
const longAssets = trade.closedLongAssets.map((a) => mapAsset$1(a, getAssetByName));
|
|
8582
|
+
const shortAssets = trade.closedShortAssets.map((a) => mapAsset$1(a, getAssetByName));
|
|
8583
|
+
bucket.trades.push({
|
|
8584
|
+
tradeHistoryId: trade.tradeHistoryId,
|
|
8585
|
+
realizedPnl: tradePnl,
|
|
8586
|
+
result: tradePnl > 0 ? 'profit' : tradePnl < 0 ? 'loss' : 'breakeven',
|
|
8587
|
+
collateralTypes: getCollateralTypes$1([...longAssets, ...shortAssets]),
|
|
8588
|
+
closedLongAssets: longAssets,
|
|
8589
|
+
closedShortAssets: shortAssets,
|
|
8590
|
+
});
|
|
8591
|
+
}
|
|
8592
|
+
// Build day objects
|
|
8593
|
+
const allDays = [];
|
|
8594
|
+
const sortedKeys = Array.from(buckets.keys()).sort();
|
|
8595
|
+
for (const key of sortedKeys) {
|
|
8596
|
+
const bucket = buckets.get(key);
|
|
8597
|
+
const roundedPnl = round2(bucket.pnl);
|
|
8598
|
+
const result = roundedPnl > 0 ? 'profit' : roundedPnl < 0 ? 'loss' : 'breakeven';
|
|
8599
|
+
allDays.push({
|
|
8600
|
+
date: key,
|
|
8601
|
+
totalPnl: roundedPnl,
|
|
8602
|
+
volume: round2(bucket.volume),
|
|
8603
|
+
positionsClosed: bucket.positionsClosed,
|
|
8604
|
+
result,
|
|
8605
|
+
trades: bucket.trades,
|
|
8606
|
+
});
|
|
8607
|
+
}
|
|
8608
|
+
// Group into periods
|
|
8609
|
+
let weeks = [];
|
|
8610
|
+
let months = [];
|
|
8611
|
+
const useWeekGrouping = useCustomDates
|
|
8612
|
+
? totalDays <= 28
|
|
8613
|
+
: isWeekTimeframe(timeframe);
|
|
8614
|
+
if (useWeekGrouping) {
|
|
8615
|
+
const weekMap = new Map();
|
|
8616
|
+
for (const day of allDays) {
|
|
8617
|
+
const date = new Date(day.date + 'T00:00:00');
|
|
8618
|
+
const monday = getMonday(date);
|
|
8619
|
+
const mondayKey = toDateKey(monday);
|
|
8620
|
+
if (!weekMap.has(mondayKey)) {
|
|
8621
|
+
weekMap.set(mondayKey, []);
|
|
8622
|
+
}
|
|
8623
|
+
weekMap.get(mondayKey).push(day);
|
|
8624
|
+
}
|
|
8625
|
+
const sortedWeekKeys = Array.from(weekMap.keys()).sort();
|
|
8626
|
+
weeks = sortedWeekKeys.map((mondayKey) => {
|
|
8627
|
+
const days = weekMap.get(mondayKey);
|
|
8628
|
+
const monday = new Date(mondayKey + 'T00:00:00');
|
|
8629
|
+
const sunday = new Date(monday);
|
|
8630
|
+
sunday.setDate(monday.getDate() + 6);
|
|
8631
|
+
return {
|
|
8632
|
+
weekStart: mondayKey,
|
|
8633
|
+
weekEnd: toDateKey(sunday),
|
|
8634
|
+
days,
|
|
8635
|
+
summary: buildSummary(days),
|
|
8636
|
+
};
|
|
8637
|
+
});
|
|
8638
|
+
}
|
|
8639
|
+
else {
|
|
8640
|
+
const monthMap = new Map();
|
|
8641
|
+
for (const day of allDays) {
|
|
8642
|
+
const date = new Date(day.date + 'T00:00:00');
|
|
8643
|
+
const mk = toMonthKey(date);
|
|
8644
|
+
if (!monthMap.has(mk)) {
|
|
8645
|
+
monthMap.set(mk, { days: [], label: formatMonthLabel(date) });
|
|
8646
|
+
}
|
|
8647
|
+
monthMap.get(mk).days.push(day);
|
|
8648
|
+
}
|
|
8649
|
+
const sortedMonthKeys = Array.from(monthMap.keys()).sort();
|
|
8650
|
+
months = sortedMonthKeys.map((mk) => {
|
|
8651
|
+
const { days, label } = monthMap.get(mk);
|
|
8652
|
+
return {
|
|
8653
|
+
month: mk,
|
|
8654
|
+
label,
|
|
8655
|
+
days,
|
|
8656
|
+
summary: buildSummary(days),
|
|
8657
|
+
};
|
|
8658
|
+
});
|
|
8659
|
+
}
|
|
8660
|
+
return {
|
|
8661
|
+
timeframe,
|
|
8662
|
+
weeks,
|
|
8663
|
+
months,
|
|
8664
|
+
overall: buildSummary(allDays),
|
|
8665
|
+
isLoading: false,
|
|
8666
|
+
};
|
|
8667
|
+
};
|
|
8668
|
+
// ─── hook ───────────────────────────────────────────────────────
|
|
8669
|
+
function usePnlCalendar(options) {
|
|
8670
|
+
var _a;
|
|
8671
|
+
const opts = typeof options === 'string'
|
|
8672
|
+
? { timeframe: options }
|
|
8673
|
+
: options !== null && options !== void 0 ? options : {};
|
|
8674
|
+
const timeframe = (_a = opts.timeframe) !== null && _a !== void 0 ? _a : '2W';
|
|
8675
|
+
const customStart = opts.startDate;
|
|
8676
|
+
const customEnd = opts.endDate;
|
|
8677
|
+
const context = useContext(PearHyperliquidContext);
|
|
8678
|
+
if (!context) {
|
|
8679
|
+
throw new Error('usePnlCalendar must be used within a PearHyperliquidProvider');
|
|
8680
|
+
}
|
|
8681
|
+
const { apiBaseUrl } = context;
|
|
8682
|
+
const isAuthenticated = useUserData((state) => state.isAuthenticated);
|
|
8683
|
+
const { getAssetByName } = useMarket();
|
|
8684
|
+
const [trades, setTrades] = useState(null);
|
|
8685
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
8686
|
+
const [error, setError] = useState(null);
|
|
8687
|
+
const mountedRef = useRef(true);
|
|
8688
|
+
useEffect(() => {
|
|
8689
|
+
mountedRef.current = true;
|
|
8690
|
+
return () => { mountedRef.current = false; };
|
|
8691
|
+
}, []);
|
|
8692
|
+
// Compute the date range
|
|
8693
|
+
const useCustomDates = !!(customStart && customEnd);
|
|
8694
|
+
let rangeStart;
|
|
8695
|
+
let rangeEnd;
|
|
8696
|
+
let totalDays;
|
|
8697
|
+
if (useCustomDates) {
|
|
8698
|
+
rangeStart = toLocalMidnight(customStart);
|
|
8699
|
+
rangeEnd = toLocalMidnight(customEnd);
|
|
8700
|
+
totalDays = diffDays(rangeStart, rangeEnd);
|
|
8701
|
+
}
|
|
8702
|
+
else {
|
|
8703
|
+
totalDays = getTimeframeDays$1(timeframe);
|
|
8704
|
+
rangeEnd = new Date();
|
|
8705
|
+
rangeEnd.setHours(0, 0, 0, 0);
|
|
8706
|
+
rangeStart = new Date(rangeEnd);
|
|
8707
|
+
rangeStart.setDate(rangeEnd.getDate() - totalDays + 1);
|
|
8708
|
+
}
|
|
8709
|
+
const startIso = toISODateString$1(rangeStart);
|
|
8710
|
+
const endIso = toISODateString$1(rangeEnd);
|
|
8711
|
+
const fetchData = useCallback(async () => {
|
|
8712
|
+
if (!isAuthenticated)
|
|
8713
|
+
return;
|
|
8714
|
+
setIsLoading(true);
|
|
8715
|
+
setError(null);
|
|
8716
|
+
try {
|
|
8717
|
+
const response = await getTradeHistory(apiBaseUrl, {
|
|
8718
|
+
startDate: startIso,
|
|
8719
|
+
endDate: endIso,
|
|
8720
|
+
limit: totalDays * 50,
|
|
8721
|
+
});
|
|
8722
|
+
if (!mountedRef.current)
|
|
8723
|
+
return;
|
|
8724
|
+
setTrades(response.data);
|
|
8725
|
+
}
|
|
8726
|
+
catch (err) {
|
|
8727
|
+
if (!mountedRef.current)
|
|
8728
|
+
return;
|
|
8729
|
+
setError(err instanceof Error ? err.message : 'Failed to fetch trade history');
|
|
8730
|
+
setTrades(null);
|
|
8731
|
+
}
|
|
8732
|
+
finally {
|
|
8733
|
+
if (mountedRef.current)
|
|
8734
|
+
setIsLoading(false);
|
|
8735
|
+
}
|
|
8736
|
+
}, [apiBaseUrl, isAuthenticated, startIso, endIso, totalDays]);
|
|
8737
|
+
useEffect(() => {
|
|
8738
|
+
fetchData();
|
|
8739
|
+
}, [fetchData]);
|
|
8740
|
+
const result = useMemo(() => {
|
|
8741
|
+
const empty = {
|
|
8742
|
+
timeframe,
|
|
8743
|
+
weeks: [],
|
|
8744
|
+
months: [],
|
|
8745
|
+
overall: EMPTY_SUMMARY,
|
|
8746
|
+
isLoading: true,
|
|
8747
|
+
};
|
|
8748
|
+
if (!trades)
|
|
8749
|
+
return empty;
|
|
8750
|
+
if (totalDays <= 0)
|
|
8751
|
+
return empty;
|
|
8752
|
+
return buildCalendarData(trades, timeframe, rangeStart, rangeEnd, totalDays, useCustomDates, getAssetByName);
|
|
8753
|
+
}, [trades, timeframe, startIso, endIso, getAssetByName]);
|
|
8754
|
+
return { ...result, isLoading, error, refetch: fetchData };
|
|
8755
|
+
}
|
|
8756
|
+
|
|
8757
|
+
const HEATMAP_LIMIT = 50;
|
|
8758
|
+
// ─── helpers ────────────────────────────────────────────────────
|
|
8759
|
+
const getTimeframeDays = (tf) => {
|
|
8760
|
+
switch (tf) {
|
|
8761
|
+
case '7D':
|
|
8762
|
+
return 7;
|
|
8763
|
+
case '30D':
|
|
8764
|
+
return 30;
|
|
8765
|
+
case '100D':
|
|
8766
|
+
return 100;
|
|
8767
|
+
case 'allTime':
|
|
8768
|
+
return null;
|
|
8769
|
+
}
|
|
8770
|
+
};
|
|
8771
|
+
const toISODateString = (date) => date.toISOString();
|
|
8772
|
+
const mapAsset = (asset, getAssetByName) => {
|
|
8773
|
+
var _a, _b, _c, _d;
|
|
8774
|
+
const metadata = getAssetByName(asset.coin);
|
|
8775
|
+
const marketInfo = getMarketInfoFromSymbol(asset.coin);
|
|
8776
|
+
return {
|
|
8777
|
+
coin: asset.coin,
|
|
8778
|
+
symbol: (_a = metadata === null || metadata === void 0 ? void 0 : metadata.symbolName) !== null && _a !== void 0 ? _a : marketInfo.symbolName,
|
|
8779
|
+
assetName: (_b = metadata === null || metadata === void 0 ? void 0 : metadata.assetName) !== null && _b !== void 0 ? _b : asset.coin,
|
|
8780
|
+
marketPrefix: (_c = metadata === null || metadata === void 0 ? void 0 : metadata.marketName) !== null && _c !== void 0 ? _c : marketInfo.marketName,
|
|
8781
|
+
percentage: asset.closeWeight * 100,
|
|
8782
|
+
collateralToken: (_d = metadata === null || metadata === void 0 ? void 0 : metadata.collateralToken) !== null && _d !== void 0 ? _d : 'USDC',
|
|
8783
|
+
};
|
|
8784
|
+
};
|
|
8785
|
+
const getCollateralTypes = (assets) => {
|
|
8786
|
+
const set = new Set();
|
|
8787
|
+
for (const a of assets)
|
|
8788
|
+
set.add(a.collateralToken);
|
|
8789
|
+
return set.size > 0 ? Array.from(set) : ['USDC'];
|
|
8790
|
+
};
|
|
8791
|
+
const toCalendarTrade = (trade, getAssetByName) => {
|
|
8792
|
+
const pnl = trade.realizedPnl;
|
|
8793
|
+
const longAssets = trade.closedLongAssets.map((a) => mapAsset(a, getAssetByName));
|
|
8794
|
+
const shortAssets = trade.closedShortAssets.map((a) => mapAsset(a, getAssetByName));
|
|
8795
|
+
return {
|
|
8796
|
+
tradeHistoryId: trade.tradeHistoryId,
|
|
8797
|
+
realizedPnl: pnl,
|
|
8798
|
+
result: pnl > 0 ? 'profit' : pnl < 0 ? 'loss' : 'breakeven',
|
|
8799
|
+
collateralTypes: getCollateralTypes([...longAssets, ...shortAssets]),
|
|
8800
|
+
closedLongAssets: longAssets,
|
|
8801
|
+
closedShortAssets: shortAssets,
|
|
8802
|
+
};
|
|
8803
|
+
};
|
|
8804
|
+
// ─── hook ───────────────────────────────────────────────────────
|
|
8805
|
+
function usePnlHeatmap(timeframe = 'allTime') {
|
|
8806
|
+
const context = useContext(PearHyperliquidContext);
|
|
8807
|
+
if (!context) {
|
|
8808
|
+
throw new Error('usePnlHeatmap must be used within a PearHyperliquidProvider');
|
|
8809
|
+
}
|
|
8810
|
+
const { apiBaseUrl } = context;
|
|
8811
|
+
const isAuthenticated = useUserData((state) => state.isAuthenticated);
|
|
8812
|
+
const { getAssetByName } = useMarket();
|
|
8813
|
+
const [trades, setTrades] = useState(null);
|
|
8814
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
8815
|
+
const [error, setError] = useState(null);
|
|
8816
|
+
const mountedRef = useRef(true);
|
|
8817
|
+
useEffect(() => {
|
|
8818
|
+
mountedRef.current = true;
|
|
8819
|
+
return () => { mountedRef.current = false; };
|
|
8820
|
+
}, []);
|
|
8821
|
+
const days = getTimeframeDays(timeframe);
|
|
8822
|
+
let startIso;
|
|
8823
|
+
if (days !== null) {
|
|
8824
|
+
const start = new Date();
|
|
8825
|
+
start.setHours(0, 0, 0, 0);
|
|
8826
|
+
start.setDate(start.getDate() - days);
|
|
8827
|
+
startIso = toISODateString(start);
|
|
8828
|
+
}
|
|
8829
|
+
const fetchData = useCallback(async () => {
|
|
8830
|
+
if (!isAuthenticated)
|
|
8831
|
+
return;
|
|
8832
|
+
setIsLoading(true);
|
|
8833
|
+
setError(null);
|
|
8834
|
+
try {
|
|
8835
|
+
const response = await getTradeHistory(apiBaseUrl, {
|
|
8836
|
+
...(startIso ? { startDate: startIso } : {}),
|
|
8837
|
+
limit: 5000,
|
|
8838
|
+
});
|
|
8839
|
+
if (!mountedRef.current)
|
|
8840
|
+
return;
|
|
8841
|
+
setTrades(response.data);
|
|
8842
|
+
}
|
|
8843
|
+
catch (err) {
|
|
8844
|
+
if (!mountedRef.current)
|
|
8845
|
+
return;
|
|
8846
|
+
setError(err instanceof Error ? err.message : 'Failed to fetch trade history');
|
|
8847
|
+
setTrades(null);
|
|
8848
|
+
}
|
|
8849
|
+
finally {
|
|
8850
|
+
if (mountedRef.current)
|
|
8851
|
+
setIsLoading(false);
|
|
8852
|
+
}
|
|
8853
|
+
}, [apiBaseUrl, isAuthenticated, startIso]);
|
|
8854
|
+
useEffect(() => {
|
|
8855
|
+
fetchData();
|
|
8856
|
+
}, [fetchData]);
|
|
8857
|
+
const result = useMemo(() => {
|
|
8858
|
+
if (!trades)
|
|
8859
|
+
return [];
|
|
8860
|
+
const top = trades
|
|
8861
|
+
.slice()
|
|
8862
|
+
.sort((a, b) => Math.abs(b.realizedPnl) - Math.abs(a.realizedPnl))
|
|
8863
|
+
.slice(0, HEATMAP_LIMIT);
|
|
8864
|
+
const totalAbsPnl = top.reduce((sum, t) => sum + Math.abs(t.realizedPnl), 0);
|
|
8865
|
+
return top.map((t) => ({
|
|
8866
|
+
...toCalendarTrade(t, getAssetByName),
|
|
8867
|
+
percentage: totalAbsPnl > 0
|
|
8868
|
+
? Math.round((Math.abs(t.realizedPnl) / totalAbsPnl) * 10000) / 100
|
|
8869
|
+
: 0,
|
|
8870
|
+
}));
|
|
8871
|
+
}, [trades, getAssetByName]);
|
|
8872
|
+
return { timeframe, trades: result, isLoading, error, refetch: fetchData };
|
|
8873
|
+
}
|
|
8874
|
+
|
|
7883
8875
|
const PearHyperliquidContext = createContext(undefined);
|
|
7884
8876
|
/**
|
|
7885
8877
|
* React Provider for PearHyperliquidClient
|
|
7886
8878
|
*/
|
|
7887
|
-
const PearHyperliquidProvider = ({ children, apiBaseUrl =
|
|
8879
|
+
const PearHyperliquidProvider = ({ children, apiBaseUrl = "https://hl-ui.pearprotocol.io", clientId = "PEARPROTOCOLUI", wsUrl = "wss://hl-ui.pearprotocol.io/ws", }) => {
|
|
7888
8880
|
const address = useUserData((s) => s.address);
|
|
7889
|
-
const
|
|
8881
|
+
const clearHyperliquidUserData = useHyperliquidData((state) => state.clearUserData);
|
|
8882
|
+
const prevAddressRef = useRef(null);
|
|
8883
|
+
useEffect(() => {
|
|
8884
|
+
if (prevAddressRef.current !== null && prevAddressRef.current !== address) {
|
|
8885
|
+
clearHyperliquidUserData();
|
|
8886
|
+
}
|
|
8887
|
+
prevAddressRef.current = address;
|
|
8888
|
+
}, [address, clearHyperliquidUserData]);
|
|
7890
8889
|
const perpMetasByDex = useHyperliquidData((state) => state.perpMetasByDex);
|
|
7891
8890
|
const setPerpDexs = useHyperliquidData((state) => state.setPerpDexs);
|
|
7892
8891
|
const setPerpMetasByDex = useHyperliquidData((state) => state.setPerpMetasByDex);
|
|
@@ -7917,20 +8916,20 @@ const PearHyperliquidProvider = ({ children, apiBaseUrl = 'https://hl-ui.pearpro
|
|
|
7917
8916
|
perpMetas.forEach((item, perpIndex) => {
|
|
7918
8917
|
var _a, _b;
|
|
7919
8918
|
const dexName = perpIndex === 0
|
|
7920
|
-
?
|
|
8919
|
+
? "HYPERLIQUID"
|
|
7921
8920
|
: ((_b = (_a = perpDexs[perpIndex]) === null || _a === void 0 ? void 0 : _a.name) !== null && _b !== void 0 ? _b : `DEX_${perpIndex}`);
|
|
7922
8921
|
var collateralToken;
|
|
7923
8922
|
if (item.collateralToken === 360) {
|
|
7924
|
-
collateralToken =
|
|
8923
|
+
collateralToken = "USDH";
|
|
7925
8924
|
}
|
|
7926
8925
|
if (item.collateralToken === 0) {
|
|
7927
|
-
collateralToken =
|
|
8926
|
+
collateralToken = "USDC";
|
|
7928
8927
|
}
|
|
7929
8928
|
if (item.collateralToken === 235) {
|
|
7930
|
-
collateralToken =
|
|
8929
|
+
collateralToken = "USDE";
|
|
7931
8930
|
}
|
|
7932
8931
|
if (item.collateralToken === 268) {
|
|
7933
|
-
collateralToken =
|
|
8932
|
+
collateralToken = "USDT0";
|
|
7934
8933
|
}
|
|
7935
8934
|
const universeAssets = item.universe.map((asset) => ({
|
|
7936
8935
|
...asset,
|
|
@@ -7958,8 +8957,6 @@ const PearHyperliquidProvider = ({ children, apiBaseUrl = 'https://hl-ui.pearpro
|
|
|
7958
8957
|
}), [
|
|
7959
8958
|
apiBaseUrl,
|
|
7960
8959
|
wsUrl,
|
|
7961
|
-
address,
|
|
7962
|
-
setAddress,
|
|
7963
8960
|
isConnected,
|
|
7964
8961
|
lastError,
|
|
7965
8962
|
nativeIsConnected,
|
|
@@ -7974,7 +8971,7 @@ const PearHyperliquidProvider = ({ children, apiBaseUrl = 'https://hl-ui.pearpro
|
|
|
7974
8971
|
function usePearHyperliquid() {
|
|
7975
8972
|
const ctx = useContext(PearHyperliquidContext);
|
|
7976
8973
|
if (!ctx)
|
|
7977
|
-
throw new Error(
|
|
8974
|
+
throw new Error("usePearHyperliquid must be used within a PearHyperliquidProvider");
|
|
7978
8975
|
return ctx;
|
|
7979
8976
|
}
|
|
7980
8977
|
|
|
@@ -8226,4 +9223,4 @@ function getOrderTrailingInfo(order) {
|
|
|
8226
9223
|
return undefined;
|
|
8227
9224
|
}
|
|
8228
9225
|
|
|
8229
|
-
export { ConflictDetector, MAX_ASSETS_PER_LEG, MINIMUM_ASSET_USD_VALUE, MaxAssetsPerLegError, MinimumPositionSizeError, PearHyperliquidProvider, adjustAdvancePosition, adjustOrder, adjustPosition, calculateMinimumPositionValue, calculateWeightedRatio, cancelOrder, cancelTwap, cancelTwapOrder, closeAllPositions, closePosition, computeBasketCandles, createCandleLookups, createPosition, executeSpotOrder, getCompleteTimestamps, getKalshiMarkets, getOrderDirection, getOrderLadderConfig, getOrderLeverage, getOrderReduceOnly, getOrderTpSlTriggerType, getOrderTrailingInfo, getOrderTriggerType, getOrderTriggerValue, getOrderTwapDuration, getOrderUsdValue, getPortfolio, isBtcDomOrder, mapCandleIntervalToTradingViewInterval, mapTradingViewIntervalToCandleInterval, markNotificationReadById, markNotificationsRead, toggleWatchlist, updateLeverage, updateRiskParameters, useAccountSummary, useAgentWallet, useAllUserBalances, useAuth, useBasketCandles, useHistoricalPriceData, useHistoricalPriceDataStore, useHyperliquidUserFills, useMarket, useMarketData, useMarketDataHook, useNotifications, useOpenOrders, useOrders, usePearHyperliquid, usePerformanceOverlays, usePortfolio, usePosition, useSpotOrder, useTokenSelectionMetadata, useTradeHistories, useTwap, useUserSelection, useWatchlist, validateMaxAssetsPerLeg, validateMinimumAssetSize, validatePositionSize };
|
|
9226
|
+
export { ConflictDetector, MAX_ASSETS_PER_LEG, MINIMUM_ASSET_USD_VALUE, MaxAssetsPerLegError, MinimumPositionSizeError, PearHyperliquidProvider, adjustAdvancePosition, adjustOrder, adjustPosition, calculateMinimumPositionValue, calculateWeightedRatio, cancelOrder, cancelTwap, cancelTwapOrder, closeAllPositions, closePosition, computeBasketCandles, createCandleLookups, createPosition, executeSpotOrder, getCompleteTimestamps, getKalshiMarkets, getOrderDirection, getOrderLadderConfig, getOrderLeverage, getOrderReduceOnly, getOrderTpSlTriggerType, getOrderTrailingInfo, getOrderTriggerType, getOrderTriggerValue, getOrderTwapDuration, getOrderUsdValue, getPortfolio, isBtcDomOrder, mapCandleIntervalToTradingViewInterval, mapTradingViewIntervalToCandleInterval, markNotificationReadById, markNotificationsRead, toggleWatchlist, updateLeverage, updateRiskParameters, useAccountSummary, useAgentWallet, useAllUserBalances, useAuth, useBasketCandles, useHistoricalPriceData, useHistoricalPriceDataStore, useHyperliquidUserFills, useMarket, useMarketData, useMarketDataHook, useNotifications, useOpenOrders, useOrders, usePearHyperliquid, usePerformanceOverlays, usePnlCalendar, usePnlHeatmap, usePortfolio, usePosition, useSpotOrder, useTokenSelectionMetadata, useTradeHistories, useTwap, useUserSelection, useWatchlist, validateMaxAssetsPerLeg, validateMinimumAssetSize, validatePositionSize };
|