Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"packages/*"
],
"engines": {
"node": ">=22"
"node": ">=22.12.0"
},
"scripts": {
"setup:env": "node -e \"const fs=require('fs');if(!fs.existsSync('.env')){if(fs.existsSync('.env.example')){fs.copyFileSync('.env.example','.env')}else{fs.writeFileSync('.env','')}}\"",
Expand Down Expand Up @@ -152,7 +152,7 @@
"@metamask/eth-sig-util": "5.1.0",
"@mysten/sui": "2.17.0",
"@ngraveio/bc-ur": "^1.1.13",
"@nktkas/hyperliquid": "0.32.2",
"@nktkas/hyperliquid": "0.33.3",
"@onekeyfe/cross-inpage-provider-core": "2.2.73",
"@onekeyfe/cross-inpage-provider-errors": "2.2.73",
"@onekeyfe/cross-inpage-provider-injected": "2.2.73",
Expand Down Expand Up @@ -401,6 +401,7 @@
"@alephium/walletconnect-provider": "3.0.4",
"@alephium/web3-wallet": "3.0.4",
"@noble/curves": "1.9.7",
"@noble/hashes@^2.2.0": "2.2.0",
"@ledgerhq/devices": "8.14.0",
"@ledgerhq/hw-transport": "6.35.0",
"@ledgerhq/device-signer-kit-solana": "1.7.1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ import type {
IPerpsActiveAssetDataRaw,
IPerpsUniverse,
IRecentTrade,
ISpotBalance,
ISpotMetaAndAssetCtxsResponse,
ISpotUniverse,
ITwapHistoryParameters,
Expand Down Expand Up @@ -2089,7 +2090,9 @@ export default class ServiceHyperliquid extends ServiceBase {
// Active-account alignment: only process data for current account
if (!activeAddress || activeAddress !== dataUser) return;

const balances = spotStateData?.spotState?.balances || [];
const balances = (spotStateData?.spotState?.balances || []).filter(
(balance): balance is ISpotBalance => 'token' in balance,
);

await spotBalancesAtom.set({ balances, isLoaded: true });

Expand Down Expand Up @@ -3324,7 +3327,8 @@ export default class ServiceHyperliquid extends ServiceBase {
});
return null;
}
if (agent.validUntil <= validThreshold) {
const validUntil = agent.validUntil ?? Number.MAX_SAFE_INTEGER;
if (validUntil <= validThreshold) {
defaultLogger.perp.agentLifeCycle.trackReason({
reason: 'agent_near_expiry',
accountAddress,
Expand All @@ -3334,7 +3338,7 @@ export default class ServiceHyperliquid extends ServiceBase {
...statusDetails,
agentName: agent.name,
agentAddress: agent.address,
validUntil: agent.validUntil,
validUntil: agent.validUntil ?? undefined,
},
});
return null;
Expand All @@ -3349,7 +3353,7 @@ export default class ServiceHyperliquid extends ServiceBase {
...statusDetails,
agentName: agent.name,
agentAddress: agent.address,
validUntil: agent.validUntil,
validUntil: agent.validUntil ?? undefined,
},
});
return null;
Expand All @@ -3368,12 +3372,12 @@ export default class ServiceHyperliquid extends ServiceBase {
agentName: agent.name,
chainAgentAddress: agent.address,
localAgentAddress: credential.agentAddress,
validUntil: agent.validUntil,
validUntil: agent.validUntil ?? undefined,
},
});
return null;
}
credential.validUntil = agent.validUntil;
credential.validUntil = validUntil;
return credential;
}),
)
Expand Down Expand Up @@ -3421,7 +3425,11 @@ export default class ServiceHyperliquid extends ServiceBase {
);
const agentToRemove = (
nonOneKeyAgents.length ? nonOneKeyAgents : extraAgents
).toSorted((a, b) => a.validUntil - b.validUntil)?.[0];
).toSorted(
(a, b) =>
(a.validUntil ?? Number.MAX_SAFE_INTEGER) -
(b.validUntil ?? Number.MAX_SAFE_INTEGER),
)?.[0];
const agentNameToRemove = agentToRemove?.name as
| EHyperLiquidAgentName
| undefined;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ import {
assertValidScaleOrderLegs,
buildScaleOrderLegs,
} from '@onekeyhq/shared/src/utils/hyperliquidScaleOrderUtils';
import {
formatTwapPriceForOrder,
isTwapStopPriceValid,
} from '@onekeyhq/shared/src/utils/hyperliquidTwapUtils';
import { normalizeDexCoin } from '@onekeyhq/shared/src/utils/perpsDexUtils';
import {
MAX_DECIMALS_PERP,
Expand Down Expand Up @@ -1388,12 +1392,76 @@ export default class ServiceHyperliquidExchange extends ServiceBase {
const szDecimals = params.szDecimals ?? precision?.szDecimals ?? 2;
const size = formatHlSize(params.size, szDecimals);
if (!size) {
throw new OneKeyLocalError('TWAP size is too small for HL lot size');
throw new OneKeyLocalError(
appLocale.intl.formatMessage({
id: ETranslations.perp_scale_order_size_too_small__msg,
}),
);
}

const assetType = precision?.type;
const reduceOnly =
assetType === 'spot' ? false : Boolean(params.reduceOnly);
const formatOptionalTwapPrice = (
price: string | undefined,
fieldName: 'trigger' | 'stop',
) => {
if (!price) {
return undefined;
}
const formattedPrice = formatTwapPriceForOrder({
price,
szDecimals,
assetType: assetType ?? 'perp',
});
if (!formattedPrice) {
let messageId = params.isBuy
? ETranslations.perp_scale_upper_price_placeholder__desc
: ETranslations.perp_scale_lower_price_placeholder__desc;
if (fieldName === 'trigger') {
messageId = ETranslations.perps_input_trigger_price;
}
throw new OneKeyLocalError(
appLocale.intl.formatMessage({
id: messageId,
}),
);
}
return formattedPrice;
};
const triggerPrice = formatOptionalTwapPrice(
params.triggerPrice,
'trigger',
);
const stopPrice = formatOptionalTwapPrice(params.stopPrice, 'stop');
Comment thread
Minnzen marked this conversation as resolved.
if (triggerPrice && typeof params.triggerAbove !== 'boolean') {
throw new OneKeyLocalError(
appLocale.intl.formatMessage({
id: ETranslations.perps_input_trigger_price,
}),
);
}
if (
stopPrice &&
!isTwapStopPriceValid({
isBuy: params.isBuy,
stopPrice,
referencePrice: params.referencePrice,
triggerPrice,
})
) {
throw new OneKeyLocalError(
`${appLocale.intl.formatMessage({
id: params.isBuy
? ETranslations.perp_scale_upper_price_label__title
: ETranslations.perp_scale_lower_price_label__title,
})} ${params.isBuy ? '>' : '<'} ${appLocale.intl.formatMessage({
id: ETranslations.perp_market_price,
})} / ${appLocale.intl.formatMessage({
id: ETranslations.dexmarket_pro_trigger_price,
})}`,
);
}
const twap = {
a: params.assetId,
b: params.isBuy,
Expand All @@ -1402,6 +1470,12 @@ export default class ServiceHyperliquidExchange extends ServiceBase {
m: params.minutes,
t: params.randomize,
};
const details = {
t: triggerPrice
? { p: triggerPrice, a: params.triggerAbove as boolean }
: null,
s: stopPrice ?? null,
};
const client = await this.getExchangeClientForTrading();
const context = await this._buildLogContext();
const requestPayload = {
Expand All @@ -1412,13 +1486,16 @@ export default class ServiceHyperliquidExchange extends ServiceBase {
reduceOnly,
minutes: params.minutes,
randomize: params.randomize,
referencePrice: params.referencePrice,
},
details,
};

try {
const response = await convertHyperLiquidResponse(() =>
client.twapOrder({
twap,
details,
}),
);
defaultLogger.perp.hyperliquid.twapOrder({
Expand Down Expand Up @@ -1706,11 +1783,7 @@ export default class ServiceHyperliquidExchange extends ServiceBase {
async setAbstractionWithUserWallet(params: {
userAccountId: string;
userAddress: string;
abstraction:
| 'disabled'
| 'unifiedAccount'
| 'portfolioMargin'
| 'dexAbstraction';
abstraction: 'disabled' | 'unifiedAccount' | 'portfolioMargin';
}): Promise<void> {
await this.checkAccountCanTrade();
const wallet =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,9 +121,7 @@ type IHyperliquidWsClient = {
transport: WebSocketTransport;
dispose: () => Promise<void>;
hlEventTarget: IHyperliquidEventTarget;
wsRequester: {
request: (method: string, payload: any) => Promise<void>;
};
ping: () => Promise<void>;
subscribe: <T extends ESubscriptionType>(
type: T,
params: IPerpsSubscriptionParams[T],
Expand Down Expand Up @@ -1636,9 +1634,18 @@ export default class ServiceHyperliquidSubscription extends ServiceBase {
transport.socket.addEventListener('open', this.socketOpenHandler);
// transport.socket.addEventListener('message', this.socketMessageHandler);
const innerClient = new SubscriptionClient({ transport });
const innerTransport = transport;
// @ts-ignore
const hlEventTarget = innerTransport._hlEvents;
// OneKey reconciles subscriptions itself, so it needs the SDK's parsed
// events and raw dispatcher without delegating ownership to the client.
const { _hlEvents: hlEventTarget, _dispatcher: subscriptionDispatcher } =
transport as unknown as {
_hlEvents: IHyperliquidEventTarget;
_dispatcher: {
request: (
method: 'subscribe' | 'unsubscribe',
payload: unknown,
) => Promise<void>;
};
};

const registerSubscriptionHandler = (type: ESubscriptionType) => {
if (!this.subscriptionHandlerByType[type]) {
Expand Down Expand Up @@ -1707,15 +1714,11 @@ export default class ServiceHyperliquidSubscription extends ServiceBase {
registerSubscriptionHandler(type);
});

// @ts-ignore
const wsRequester = innerTransport._postRequest as {
request: (method: string, payload: any) => Promise<void>;
};
const subscribe = async <T extends ESubscriptionType>(
type: T,
params: IPerpsSubscriptionParams[T],
) => {
return wsRequester.request('subscribe', {
return subscriptionDispatcher.request('subscribe', {
type,
...params,
});
Expand All @@ -1724,35 +1727,50 @@ export default class ServiceHyperliquidSubscription extends ServiceBase {
type: T,
params: IPerpsSubscriptionParams[T],
) => {
return wsRequester.request('unsubscribe', {
return subscriptionDispatcher.request('unsubscribe', {
type,
...params,
});
};
const ping = () =>
new Promise<void>((resolve, reject) => {
const listenerController = new AbortController();
const timeout = setTimeout(() => {
listenerController.abort();
reject(new Error('Hyperliquid WebSocket ping timed out'));
}, transport.timeout ?? 10_000);
hlEventTarget.addEventListener(
'pong',
() => {
clearTimeout(timeout);
listenerController.abort();
resolve();
},
{ once: true, signal: listenerController.signal },
);
try {
transport.socket.send('{"method":"ping"}');
} catch (error) {
clearTimeout(timeout);
listenerController.abort();
reject(error);
}
});
this._client = {
clientId,
transport,
hlEventTarget,
wsRequester,
ping,
subscribe,
unsubscribe,
dispose: async () => {
// OneKey: dispose order matters for orphan-timer cleanup. We must
// close the underlying socket BEFORE removing OUR listeners — the
// close() triggers rews's internal `cleanup` listener (registered
// with { once: true } on close/error/open) which calls clearTimeout
// on its connection-timeout timer. If we removed listeners first,
// any in-flight close event might be dropped before rews can clean
// up its 5s setTimeout, leaving an orphan timer that could fire
// after dispose and re-trigger the dispatchEvent path (now caught
// defensively by the rews patch, but harmless cleanup is preferred).
// Closing first lets the socket release reconnect timers before our
// listeners are detached.
defaultLogger.perp.hyperliquid.subscriptionTransportDispose({
clientId,
});
try {
// Close socket first so rews's internal close listener fires and
// clears its connection-timeout setTimeout.
transport.socket.close();
transport.close();
Comment thread
Minnzen marked this conversation as resolved.
} catch (error) {
console.error('dispose__transport.socket.close__error', error);
}
Expand Down Expand Up @@ -2527,7 +2545,7 @@ export default class ServiceHyperliquidSubscription extends ServiceBase {
}
try {
const start = Date.now();
await client.wsRequester.request('ping', undefined);
await client.ping();
// Guard: client may have been replaced/closed during await
if (this._client !== client) return;
const pingMs = Date.now() - start;
Expand Down
Loading
Loading