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