Skip to content

Commit d2a2fc0

Browse files
committed
chore: better prechecks
1 parent b51c6e5 commit d2a2fc0

File tree

12 files changed

+579
-316
lines changed

12 files changed

+579
-316
lines changed

packages/apps/ability-hyperliquid/src/generated/lit-action.js

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
"ipfsCid": "QmUnh3PKQPpUQ3yiiC5EkNCjR5TpPRWkxDWrrop36wDj3Z"
2+
"ipfsCid": "QmRXDcNv2pfpPqSDjJM86u75povvZwRaLr2Azx9F1hnsWF"
33
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import * as hyperliquid from '@nktkas/hyperliquid';
2+
3+
export type CancelOrderPrechecksResult =
4+
| CancelOrderPrechecksResultSuccess
5+
| CancelOrderPrechecksResultFailure;
6+
7+
export interface CancelOrderPrechecksResultSuccess {
8+
success: true;
9+
}
10+
11+
export interface CancelOrderPrechecksResultFailure {
12+
success: false;
13+
reason: string;
14+
}
15+
16+
export interface CancelOrderParams {
17+
orderId: number;
18+
}
19+
20+
export type CancelAllOrdersForSymbolPrechecksResult =
21+
| CancelAllOrdersForSymbolPrechecksResultSuccess
22+
| CancelAllOrdersForSymbolPrechecksResultFailure;
23+
24+
export interface CancelAllOrdersForSymbolPrechecksResultSuccess {
25+
success: true;
26+
orderCount: number;
27+
}
28+
29+
export interface CancelAllOrdersForSymbolPrechecksResultFailure {
30+
success: false;
31+
reason: string;
32+
}
33+
34+
export interface CancelAllOrdersForSymbolParams {
35+
symbol: string;
36+
}
37+
38+
/**
39+
* Check if a specific order can be cancelled
40+
*/
41+
export async function cancelOrderPrechecks({
42+
infoClient,
43+
ethAddress,
44+
params,
45+
}: {
46+
infoClient: hyperliquid.InfoClient;
47+
ethAddress: string;
48+
params: CancelOrderParams;
49+
}): Promise<CancelOrderPrechecksResult> {
50+
try {
51+
const openOrders = await infoClient.openOrders({
52+
user: ethAddress as `0x${string}`,
53+
});
54+
55+
const orderExists = openOrders.some((order) => order.oid === params.orderId);
56+
57+
if (!orderExists) {
58+
return {
59+
success: false,
60+
reason: `Order ${params.orderId} not found or already filled/cancelled`,
61+
};
62+
}
63+
64+
return {
65+
success: true,
66+
};
67+
} catch (error) {
68+
return {
69+
success: false,
70+
reason: error instanceof Error ? error.message : String(error),
71+
};
72+
}
73+
}
74+
75+
/**
76+
* Check if there are open orders for a symbol that can be cancelled
77+
*/
78+
export async function cancelAllOrdersForSymbolPrechecks({
79+
infoClient,
80+
ethAddress,
81+
params,
82+
}: {
83+
infoClient: hyperliquid.InfoClient;
84+
ethAddress: string;
85+
params: CancelAllOrdersForSymbolParams;
86+
}): Promise<CancelAllOrdersForSymbolPrechecksResult> {
87+
try {
88+
const openOrders = await infoClient.openOrders({
89+
user: ethAddress as `0x${string}`,
90+
});
91+
92+
const ordersForSymbol = openOrders.filter((order) => order.coin === params.symbol);
93+
94+
if (ordersForSymbol.length === 0) {
95+
return {
96+
success: false,
97+
reason: `No open orders found for ${params.symbol}`,
98+
};
99+
}
100+
101+
return {
102+
success: true,
103+
orderCount: ordersForSymbol.length,
104+
};
105+
} catch (error) {
106+
return {
107+
success: false,
108+
reason: error instanceof Error ? error.message : String(error),
109+
};
110+
}
111+
}

packages/apps/ability-hyperliquid/src/lib/ability-checks/deposit.ts

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,18 @@ export type DepositPrechecksResult = DepositPrechecksResultSuccess | DepositPrec
77

88
export interface DepositPrechecksResultSuccess {
99
success: true;
10-
balance: string;
10+
availableBalance: string;
1111
}
1212

1313
export interface DepositPrechecksResultFailure {
1414
success: false;
1515
reason: string;
16-
balance: string;
16+
availableBalance?: string;
1717
}
1818

19+
// https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/bridge2#deposit
20+
const MIN_USDC_DEPOSIT_AMOUNT = ethers.BigNumber.from('5000000');
21+
1922
export const depositPrechecks = async ({
2023
provider,
2124
agentWalletPkpEthAddress,
@@ -30,18 +33,34 @@ export const depositPrechecks = async ({
3033
const usdcAddress = useTestnet ? ARBITRUM_USDC_ADDRESS_TESTNET : ARBITRUM_USDC_ADDRESS_MAINNET;
3134
const usdcContract = new ethers.Contract(usdcAddress, ERC20_ABI, provider);
3235

33-
const balance = await usdcContract.balanceOf(agentWalletPkpEthAddress);
36+
const ethBalance = await provider.getBalance(agentWalletPkpEthAddress);
37+
if (ethBalance.eq(0n)) {
38+
return {
39+
success: false,
40+
reason: `Agent Wallet PKP has no ETH balance. Please fund the Agent Wallet PKP with ETH`,
41+
};
42+
}
43+
44+
const usdcBalance = await usdcContract.balanceOf(agentWalletPkpEthAddress);
45+
const _depositAmountInMicroUsdc = ethers.BigNumber.from(depositAmountInMicroUsdc);
46+
if (_depositAmountInMicroUsdc.lt(MIN_USDC_DEPOSIT_AMOUNT)) {
47+
return {
48+
success: false,
49+
reason: `Deposit amount is less than the minimum deposit amount. Minimum deposit amount required: ${ethers.utils.formatUnits(MIN_USDC_DEPOSIT_AMOUNT, 6)} USDC`,
50+
availableBalance: ethers.utils.formatUnits(usdcBalance, 6),
51+
};
52+
}
3453

35-
if (balance.lt(depositAmountInMicroUsdc)) {
54+
if (usdcBalance.lt(_depositAmountInMicroUsdc)) {
3655
return {
3756
success: false,
38-
reason: `Insufficient USDC balance. Required: ${depositAmountInMicroUsdc} USDC, Available: ${ethers.utils.formatUnits(balance, 6)} USDC`,
39-
balance: ethers.utils.formatUnits(balance, 6),
57+
reason: `Insufficient USDC balance. Attempted deposit amount: ${ethers.utils.formatUnits(depositAmountInMicroUsdc, 6)} USDC, Available balance: ${ethers.utils.formatUnits(usdcBalance, 6)} USDC`,
58+
availableBalance: ethers.utils.formatUnits(usdcBalance, 6),
4059
};
4160
}
4261

4362
return {
4463
success: true,
45-
balance: ethers.utils.formatUnits(balance, 6),
64+
availableBalance: ethers.utils.formatUnits(usdcBalance, 6),
4665
};
4766
};
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import type { InfoClient } from '@nktkas/hyperliquid';
2+
3+
export const hyperliquidAccountExists = async ({
4+
infoClient,
5+
ethAddress,
6+
}: {
7+
infoClient: InfoClient;
8+
ethAddress: string;
9+
}) => {
10+
try {
11+
await infoClient.clearinghouseState({ user: ethAddress });
12+
return true;
13+
} catch (error) {
14+
console.error(
15+
'[@lit-protocol/vincent-ability-hyperliquid precheck] Error checking clearinghouse state',
16+
error,
17+
);
18+
19+
const errorMessage = error instanceof Error ? error.message : String(error);
20+
if (errorMessage.includes('does not exist')) {
21+
return false;
22+
} else {
23+
// Unknown error occurred - not a "does not exist" error
24+
throw error;
25+
}
26+
}
27+
};
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
export { hyperliquidAccountExists } from './hyperliquid-account-exists';
12
export { depositPrechecks } from './deposit';
3+
export { transferPrechecks } from './transfer';
24
export { spotTradePrechecks } from './spot';
35
export { perpTradePrechecks } from './perp';
6+
export { cancelOrderPrechecks, cancelAllOrdersForSymbolPrechecks } from './cancel';
Lines changed: 56 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,79 +1,76 @@
1+
import { ethers } from 'ethers';
12
import * as hyperliquid from '@nktkas/hyperliquid';
23
import { SymbolConverter } from '@nktkas/hyperliquid/utils';
34

5+
export type PerpTradePrechecksResult =
6+
| PerpTradePrechecksResultSuccess
7+
| PerpTradePrechecksResultFailure;
8+
9+
export interface PerpTradePrechecksResultSuccess {
10+
success: true;
11+
availableMargin: string;
12+
}
13+
14+
export interface PerpTradePrechecksResultFailure {
15+
success: false;
16+
reason: string;
17+
availableMargin?: string;
18+
}
19+
420
export interface PerpTradeParams {
521
symbol: string;
6-
price: string;
7-
size: string;
822
}
923

1024
/**
1125
* Check if perpetual trade can be executed
1226
*/
13-
export async function perpTradePrechecks(
14-
transport: hyperliquid.HttpTransport,
15-
userAddress: string,
16-
params: PerpTradeParams,
17-
): Promise<{ success: boolean; reason?: string; assetId?: number }> {
18-
try {
19-
const converter = await SymbolConverter.create({ transport });
20-
const assetId = converter.getAssetId(params.symbol);
27+
export async function perpTradePrechecks({
28+
transport,
29+
ethAddress,
30+
params,
31+
}: {
32+
transport: hyperliquid.HttpTransport;
33+
ethAddress: string;
34+
params: PerpTradeParams;
35+
}): Promise<PerpTradePrechecksResult> {
36+
const converter = await SymbolConverter.create({ transport });
37+
const assetId = converter.getAssetId(params.symbol);
2138

22-
if (assetId === undefined) {
23-
return {
24-
success: false,
25-
reason: `Perpetual contract ${params.symbol} does not exist`,
26-
};
27-
}
28-
29-
// Check if account exists
30-
const infoClient = new hyperliquid.InfoClient({ transport });
31-
try {
32-
await infoClient.clearinghouseState({ user: userAddress as `0x${string}` });
33-
// If this succeeds, account exists
34-
} catch (error) {
35-
const errorMessage = error instanceof Error ? error.message : String(error);
36-
if (errorMessage.includes('does not exist')) {
37-
return {
38-
success: false,
39-
reason: 'Hyperliquid account does not exist. Please deposit first.',
40-
assetId,
41-
};
42-
}
43-
throw error;
44-
}
45-
46-
// Check perp balance to ensure user has funds
47-
const perpState = await infoClient.clearinghouseState({
48-
user: userAddress as `0x${string}`,
49-
});
50-
51-
// Check if user has any cross margin value or margin summary
52-
if (!perpState.crossMarginSummary) {
53-
return {
54-
success: false,
55-
reason: 'No perp account found. Please transfer funds to perp account first.',
56-
assetId,
57-
};
58-
}
39+
if (assetId === undefined) {
40+
return {
41+
success: false,
42+
reason: `Perpetual contract ${params.symbol} does not exist`,
43+
};
44+
}
5945

60-
const accountValue = parseFloat(perpState.crossMarginSummary.accountValue);
61-
if (accountValue <= 0) {
62-
return {
63-
success: false,
64-
reason: 'Insufficient perp account balance. Please transfer funds to perp account first.',
65-
assetId,
66-
};
67-
}
46+
// Check perp balance to ensure user has funds
47+
const infoClient = new hyperliquid.InfoClient({ transport });
48+
const perpState = await infoClient.clearinghouseState({
49+
user: ethAddress,
50+
});
6851

52+
// Check if user has any cross margin value or margin summary
53+
if (!perpState.crossMarginSummary) {
6954
return {
70-
success: true,
71-
assetId,
55+
success: false,
56+
reason: 'No perp account found. Please transfer funds to perp account first.',
7257
};
73-
} catch (error) {
58+
}
59+
60+
// Check if user has available margin
61+
// accountValue is in USDC with 6 decimals
62+
const availableMarginBN = ethers.utils.parseUnits(perpState.crossMarginSummary.accountValue, 6);
63+
64+
if (availableMarginBN.lte(0)) {
7465
return {
7566
success: false,
76-
reason: error instanceof Error ? error.message : String(error),
67+
reason: 'Insufficient perp account balance. Please transfer funds to perp account first.',
68+
availableMargin: '0',
7769
};
7870
}
71+
72+
return {
73+
success: true,
74+
availableMargin: perpState.crossMarginSummary.accountValue,
75+
};
7976
}

0 commit comments

Comments
 (0)