Skip to content

Commit 57554d6

Browse files
committed
feat: perp long and short
1 parent bc32293 commit 57554d6

File tree

16 files changed

+1175
-17
lines changed

16 files changed

+1175
-17
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": "QmTEJvuurEEB3zKxVHU8zMFWEfV2CgoNZiUTxxPK4FkZ13"
2+
"ipfsCid": "QmUpGqvnfLEthsyK9MYM59794SEKerPhe7Jv1fDLLggZTz"
33
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export { depositPrechecks } from './deposit';
22
export { spotTradePrechecks } from './spot';
3+
export { perpTradePrechecks } from './perp';
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import * as hyperliquid from '@nktkas/hyperliquid';
2+
import { SymbolConverter } from '@nktkas/hyperliquid/utils';
3+
4+
export interface PerpTradeParams {
5+
symbol: string;
6+
price: string;
7+
size: string;
8+
}
9+
10+
/**
11+
* Check if perpetual trade can be executed
12+
*/
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);
21+
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+
const crossMarginSummary = perpState.crossMarginSummary;
53+
if (!crossMarginSummary) {
54+
return {
55+
success: false,
56+
reason: 'No perp account found. Please transfer funds to perp account first.',
57+
assetId,
58+
};
59+
}
60+
61+
const accountValue = parseFloat(crossMarginSummary.accountValue);
62+
if (accountValue <= 0) {
63+
return {
64+
success: false,
65+
reason: 'Insufficient perp account balance. Please transfer funds to perp account first.',
66+
assetId,
67+
};
68+
}
69+
70+
return {
71+
success: true,
72+
assetId,
73+
};
74+
} catch (error) {
75+
return {
76+
success: false,
77+
reason: error instanceof Error ? error.message : String(error),
78+
};
79+
}
80+
}

packages/apps/ability-hyperliquid/src/lib/ability-helpers/spot/cancel-all-orders.ts renamed to packages/apps/ability-hyperliquid/src/lib/ability-helpers/cancel-order/cancel-all-orders.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { LitActionPkpEthersWallet } from '../lit-action-pkp-ethers-wallet';
99
/**
1010
* Cancel all open spot orders for a specific symbol
1111
*/
12-
export async function cancelAllSpotOrders({
12+
export async function cancelAllOrdersForSymbol({
1313
transport,
1414
pkpPublicKey,
1515
symbol,

packages/apps/ability-hyperliquid/src/lib/ability-helpers/spot/cancel-order.ts renamed to packages/apps/ability-hyperliquid/src/lib/ability-helpers/cancel-order/cancel-order.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export interface CancelSpotOrderParams {
1414
/**
1515
* Cancel a specific spot order on Hyperliquid
1616
*/
17-
export async function cancelSpotOrder({
17+
export async function cancelOrder({
1818
transport,
1919
pkpPublicKey,
2020
params,
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
import * as hyperliquid from '@nktkas/hyperliquid';
2+
import { SymbolConverter } from '@nktkas/hyperliquid/utils';
3+
import { OrderRequest, UpdateLeverageRequest, parser } from '@nktkas/hyperliquid/api/exchange';
4+
import { signL1Action } from '@nktkas/hyperliquid/signing';
5+
import { bigIntReplacer } from '@lit-protocol/vincent-ability-sdk';
6+
7+
import { LitActionPkpEthersWallet } from './lit-action-pkp-ethers-wallet';
8+
9+
export type TimeInForce = 'Gtc' | 'Ioc' | 'Alo';
10+
11+
export interface PerpTradeParams {
12+
symbol: string;
13+
price: string;
14+
size: string;
15+
isLong: boolean; // true for long, false for short
16+
/**
17+
* Order type configuration.
18+
* - For limit orders: specify { type: 'limit', tif: 'Gtc' | 'Ioc' | 'Alo' }
19+
* - For market orders: specify { type: 'market' }
20+
* @default { type: 'limit', tif: 'Gtc' }
21+
*/
22+
orderType?: { type: 'limit'; tif: TimeInForce } | { type: 'market' };
23+
/**
24+
* Leverage configuration.
25+
* @default { leverage: 2, isCross: true }
26+
*/
27+
leverage?: {
28+
leverage: number; // 1-50x
29+
isCross: boolean; // true for cross margin, false for isolated
30+
};
31+
}
32+
33+
/**
34+
* Execute a perpetual trade on Hyperliquid
35+
*/
36+
export async function executePerpOrder({
37+
transport,
38+
pkpPublicKey,
39+
params,
40+
useTestnet = false,
41+
}: {
42+
transport: hyperliquid.HttpTransport;
43+
pkpPublicKey: string;
44+
params: PerpTradeParams;
45+
useTestnet?: boolean;
46+
}) {
47+
// Get converter for symbol to asset ID
48+
const converter = await SymbolConverter.create({ transport });
49+
const assetId = converter.getAssetId(params.symbol);
50+
51+
if (assetId === undefined) {
52+
throw new Error(
53+
`Failed to get asset ID for ${params.symbol}. The perpetual contract may not exist.`,
54+
);
55+
}
56+
57+
// Create PKP wallet
58+
const pkpWallet = new LitActionPkpEthersWallet(pkpPublicKey);
59+
const pkpAddress = await pkpWallet.getAddress();
60+
61+
// Set leverage if specified
62+
if (params.leverage) {
63+
const leverageNonceResponse = await Lit.Actions.runOnce(
64+
{ waitForResponse: true, name: 'HyperLiquidPerpLeverageNonce' },
65+
async () => {
66+
return Date.now().toString();
67+
},
68+
);
69+
const leverageNonce = parseInt(leverageNonceResponse);
70+
71+
const updateLeverageAction = parser(UpdateLeverageRequest.entries.action)({
72+
type: 'updateLeverage',
73+
asset: assetId,
74+
isCross: params.leverage.isCross,
75+
leverage: params.leverage.leverage,
76+
});
77+
78+
const leverageSignature = await signL1Action({
79+
wallet: pkpWallet,
80+
action: updateLeverageAction,
81+
nonce: leverageNonce,
82+
isTestnet: useTestnet,
83+
});
84+
85+
await Lit.Actions.runOnce(
86+
{ waitForResponse: true, name: 'HyperLiquidPerpLeverageRequest' },
87+
async () => {
88+
return JSON.stringify(
89+
await transport.request('exchange', {
90+
action: updateLeverageAction,
91+
signature: leverageSignature,
92+
nonce: leverageNonce,
93+
}),
94+
);
95+
},
96+
);
97+
98+
console.log(
99+
`[executePerpOrder] Set leverage to ${params.leverage.leverage}x (${params.leverage.isCross ? 'cross' : 'isolated'})`,
100+
);
101+
}
102+
103+
// Generate deterministic nonce for order in runOnce
104+
const nonceResponse = await Lit.Actions.runOnce(
105+
{ waitForResponse: true, name: 'HyperLiquidPerpOrderNonce' },
106+
async () => {
107+
return Date.now().toString();
108+
},
109+
);
110+
const nonce = parseInt(nonceResponse);
111+
112+
// Determine order type configuration
113+
const orderType = params.orderType || { type: 'limit', tif: 'Gtc' };
114+
115+
// Construct order type field based on orderType
116+
const orderTypeField =
117+
orderType.type === 'market'
118+
? { limit: { tif: 'FrontendMarket' } }
119+
: { limit: { tif: orderType.tif } };
120+
121+
// Construct order action
122+
const orderAction = parser(OrderRequest.entries.action)({
123+
type: 'order',
124+
orders: [
125+
{
126+
a: assetId,
127+
b: params.isLong, // true for long (buy), false for short (sell)
128+
p: params.price,
129+
s: params.size,
130+
r: false, // reduce only (false for opening positions)
131+
t: orderTypeField,
132+
},
133+
],
134+
grouping: 'na',
135+
});
136+
137+
// Sign and send
138+
const signature = await signL1Action({
139+
wallet: pkpWallet,
140+
action: orderAction,
141+
nonce,
142+
isTestnet: useTestnet,
143+
});
144+
145+
const orderResult = await Lit.Actions.runOnce(
146+
{ waitForResponse: true, name: 'HyperLiquidPerpOrderRequest' },
147+
async () => {
148+
return JSON.stringify(
149+
{
150+
result: await transport.request('exchange', {
151+
action: orderAction,
152+
signature,
153+
nonce,
154+
}),
155+
},
156+
bigIntReplacer,
157+
2,
158+
);
159+
},
160+
);
161+
162+
const parsedOrderResult = JSON.parse(orderResult);
163+
console.log('[executePerpOrder] Order result', parsedOrderResult);
164+
165+
const perpOrderStatus = parsedOrderResult.result.response.data.statuses[0];
166+
167+
if (perpOrderStatus.error !== undefined) {
168+
return {
169+
status: 'error',
170+
error: perpOrderStatus.error,
171+
};
172+
}
173+
174+
return {
175+
status: 'success',
176+
orderResult: parsedOrderResult.result as Record<string, unknown>,
177+
};
178+
}

packages/apps/ability-hyperliquid/src/lib/ability-helpers/spot/execute-order.ts renamed to packages/apps/ability-hyperliquid/src/lib/ability-helpers/execute-spot-order.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { OrderRequest, parser } from '@nktkas/hyperliquid/api/exchange';
44
import { signL1Action } from '@nktkas/hyperliquid/signing';
55
import { bigIntReplacer } from '@lit-protocol/vincent-ability-sdk';
66

7-
import { LitActionPkpEthersWallet } from '../lit-action-pkp-ethers-wallet';
7+
import { LitActionPkpEthersWallet } from './lit-action-pkp-ethers-wallet';
88

99
export type TimeInForce = 'Gtc' | 'Ioc' | 'Alo';
1010

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
export * from './perp-trade';
2-
export * from './spot';
31
export { LitActionPkpEthersWallet } from './lit-action-pkp-ethers-wallet';
2+
export { cancelOrder } from './cancel-order/cancel-order';
3+
export { cancelAllOrdersForSymbol } from './cancel-order/cancel-all-orders';
44
export { sendDepositTx } from './send-deposit-tx';
55
export { transferUsdcTo } from './transfer-usdc-to';
6+
export { executeSpotOrder } from './execute-spot-order';
7+
export { executePerpOrder } from './execute-perp-order';

packages/apps/ability-hyperliquid/src/lib/ability-helpers/spot/index.ts

Lines changed: 0 additions & 3 deletions
This file was deleted.

0 commit comments

Comments
 (0)