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