Skip to content
Open
Show file tree
Hide file tree
Changes from all 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,20 @@ 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 allBalances = spotStateData?.spotState?.balances || [];
const balances = allBalances.filter(
(balance): balance is ISpotBalance => 'token' in balance,
);
// Outcome-market balances have no token id or price feed, so they cannot
// be valued or rendered; the warn keeps the omission diagnosable.
const droppedPositiveCount = allBalances.filter(
(balance) => !('token' in balance) && new BigNumber(balance.total).gt(0),
).length;
if (droppedPositiveCount > 0) {
console.warn(
`[updateSpotBalances] excluded ${droppedPositiveCount} unsupported outcome balance(s)`,
);
}

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

Expand Down Expand Up @@ -3324,7 +3338,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 +3349,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 +3364,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 +3383,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 +3436,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,16 @@ export default class ServiceHyperliquidExchange extends ServiceBase {
m: params.minutes,
t: params.randomize,
};
// Plain TWAPs must keep the pre-0.33 wire shape production has validated.
const details =
triggerPrice || stopPrice
? {
t: triggerPrice
? { p: triggerPrice, a: params.triggerAbove as boolean }
: null,
s: stopPrice ?? null,
}
: undefined;
const client = await this.getExchangeClientForTrading();
const context = await this._buildLogContext();
const requestPayload = {
Expand All @@ -1412,13 +1490,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 ? { details } : {}),
}),
);
defaultLogger.perp.hyperliquid.twapOrder({
Expand Down Expand Up @@ -1706,11 +1787,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 @@ -64,6 +64,29 @@ describe('ServiceHyperliquidSubscription Fast L2 lifecycle', () => {
});
});

describe('ServiceHyperliquidSubscription ping measurement', () => {
it('keeps one ping in flight per client', async () => {
const service = createService();
const internals = service as unknown as {
_client: { ping: () => Promise<void> } | null;
_measurePing: () => Promise<void>;
};
let releasePing: (() => void) | undefined;
const pendingPing = new Promise<void>((resolve) => {
releasePing = resolve;
});
const ping = jest.fn(() => pendingPing);
internals._client = { ping };

const first = internals._measurePing();
const second = internals._measurePing();

expect(ping).toHaveBeenCalledTimes(1);
releasePing?.();
await Promise.all([first, second]);
});
});

describe('ServiceHyperliquidSubscription resume stream liveness', () => {
type IResumeInternals = {
_lastMessageAt: number | null;
Expand Down
Loading
Loading