Skip to content

Commit 039205b

Browse files
Merge pull request #405 from LIT-Protocol/feat/hl-sendasset-buildercode
Hyperliquid send Spot assets and send Perp USDC
2 parents 12c2d14 + ccc0a8a commit 039205b

31 files changed

+1184
-26
lines changed

.cursorignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Add directories or file patterns to ignore during indexing (e.g. foo/ or *.csv)
2+
packages/**/generated/lit-action.js
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
ability-hyperliquid: minor
3+
---
4+
5+
Added support for the send Spot assets and send Perp USDC Hyperliquid actions

packages/apps/ability-hyperliquid/README.md

Lines changed: 118 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,14 @@ A Vincent Ability for securely interacting with the Hyperliquid API.
2121
- [Spot Sell E2E Tests](#spot-sell-e2e-tests)
2222
- [Cancel Order E2E Tests](#cancel-order-e2e-tests)
2323
- [Cancel All Orders E2E Tests](#cancel-all-orders-e2e-tests)
24+
- [Send Spot Asset E2E Tests](#send-spot-asset-e2e-tests)
2425
- [Trade History E2E Tests](#trade-history-e2e-tests)
2526
- [Open Orders E2E Tests](#open-orders-e2e-tests)
2627
- [Transfer to Perp E2E Tests](#transfer-to-perp-e2e-tests)
2728
- [Perp Long E2E Tests](#perp-long-e2e-tests)
2829
- [Perp Short E2E Tests](#perp-short-e2e-tests)
30+
- [Send Perp USDC E2E Tests](#send-perp-usdc-e2e-tests)
31+
- [Withdraw E2E Tests](#withdraw-e2e-tests)
2932

3033
# Testing the Ability
3134

@@ -42,10 +45,13 @@ The E2E tests for this Ability are not expected to be ran concurrently. This is
4245
6. [Open Orders E2E Tests](#running-the-open-orders-e2e-tests) - Fetches all open orders (Spot and Perp) for the Agent Wallet PKP, and logs them to the console.
4346
7. [Cancel Order E2E Tests](#running-the-cancel-order-e2e-tests) - Cancels a Spot order for the order ID specified by `ORDER_ID_TO_CANCEL`.
4447
8. [Cancel All Orders E2E Tests](#running-the-cancel-all-orders-e2e-tests) - Cancels all open Spot orders for the market specified by `TRADING_PAIR`.
45-
9. [Trade History E2E Tests](#running-the-trade-history-e2e-tests) - Fetches the Spot and Perp trade history of the Agent Wallet PKP on Hyperliquid mainnet or testnet.
46-
10. [Transfer to Perp E2E Tests](#running-the-transfer-to-perp-e2e-tests) - Transfers USDC from the Agent Wallet PKP's Hyperliquid spot balance to it's Hyperliquid Perp balance. The PKP must have at least a Spot balance of `USDC_TRANSFER_AMOUNT` USDC.
47-
11. [Perp Long E2E Tests](#running-the-perp-long-e2e-tests) - Opens a long position for the token specified by `PERP_SYMBOL` using USDC. The Agent Wallet PKP must have at least a Perp balance of `PERP_LONG_USD_NOTIONAL` USDC.
48-
12. [Perp Short E2E Tests](#running-the-perp-short-e2e-tests) - Opens a short position for the token specified by `PERP_SYMBOL` using USDC. The Agent Wallet PKP must have at least a Perp balance of `PERP_SHORT_USD_NOTIONAL` USDC.
48+
9. [Send Spot Asset E2E Tests](#running-the-send-spot-asset-e2e-tests) - Sends a spot asset (USDC or other tokens) from the Agent Wallet PKP's Hyperliquid spot balance to another Hyperliquid spot account. The PKP must have at least a Spot balance of `SPOT_ASSET_SEND_AMOUNT` USDC.
49+
10. [Trade History E2E Tests](#running-the-trade-history-e2e-tests) - Fetches the Spot and Perp trade history of the Agent Wallet PKP on Hyperliquid mainnet or testnet.
50+
11. [Transfer to Perp E2E Tests](#running-the-transfer-to-perp-e2e-tests) - Transfers USDC from the Agent Wallet PKP's Hyperliquid spot balance to it's Hyperliquid Perp balance. The PKP must have at least a Spot balance of `USDC_TRANSFER_AMOUNT` USDC.
51+
12. [Perp Long E2E Tests](#running-the-perp-long-e2e-tests) - Opens a long position for the token specified by `PERP_SYMBOL` using USDC. The Agent Wallet PKP must have at least a Perp balance of `PERP_LONG_USD_NOTIONAL` USDC.
52+
13. [Perp Short E2E Tests](#running-the-perp-short-e2e-tests) - Opens a short position for the token specified by `PERP_SYMBOL` using USDC. The Agent Wallet PKP must have at least a Perp balance of `PERP_SHORT_USD_NOTIONAL` USDC.
53+
14. [Send Perp USDC E2E Tests](#running-the-send-perp-usdc-e2e-tests) - Sends USDC from the Agent Wallet PKP's Hyperliquid Perp balance to another Hyperliquid perp account. The PKP must have at least a Perp balance of `USDC_SEND_AMOUNT` USDC.
54+
15. [Withdraw E2E Tests](#running-the-withdraw-e2e-tests) - Withdraws USDC from the Agent Wallet PKP's Hyperliquid Perp balance to the PKP's ETH address on Arbitrum. The PKP must have at least a Perp balance of `WITHDRAW_AMOUNT_USDC` USDC.
4955

5056
The E2E tests by default expected the Agent Wallet PKP to have a Perp or Spot balance of at least `15` USDC. Each test has a `const` like `USDC_TRANSFER_AMOUNT` (for the transfer E2E tests) that configures how much of the USDC balance is used to run the tests. There are order amount minimums associated with each market on Hyperliquid. So the minimum amount accepted by Hyperliquid to Spot Buy on the market `BTC/USDC` is not the same as the minimum amount accepted by Hyperliquid to Spot Buy on the market `ETH/USDC`.
5157

@@ -370,6 +376,42 @@ You should see the following logs indicating the Cancel All Orders was successfu
370376
}
371377
```
372378

379+
## Send Spot Asset E2E Tests
380+
381+
This test will work on both Hyperliquid mainnet and testnet.
382+
383+
This test will send a spot asset (USDC or other tokens) from the Agent Wallet PKP's Hyperliquid spot balance to another Hyperliquid spot account. The PKP must have at least a Spot balance of `SEND_AMOUNT` of `TOKEN`.
384+
385+
```
386+
pnpx nx run ability-hyperliquid:test-e2e packages/apps/ability-hyperliquid/test/e2e/spot/send-spot-asset.spec.ts
387+
```
388+
389+
You should see the following logs indicating the Send Spot Asset was successful:
390+
391+
```
392+
[should execute send spot asset of 1.0 USDC] {
393+
success: true,
394+
result: {
395+
action: 'sendSpotAsset',
396+
sendResult: { status: 'ok', response: { type: 'default' } }
397+
},
398+
context: {
399+
delegation: {
400+
delegateeAddress: '0xb50aBA1E265B52067aF97401C25C39efF57Fe83b',
401+
delegatorPkpInfo: {
402+
tokenId: '64398341638522492941822855531388288999933789608071521616433988119257635428447',
403+
ethAddress: '0x17f51B528A0eA0ea19ABa0F343Dc9beED0FCc428',
404+
publicKey: '0x04576960d83a4eaf042e585c1cf90035b869b087631312719fa2b96fecde30705dbf80711fc30f6435e6a0739d1f47201320563a7f51357c146421c96355f6cbe3'
405+
}
406+
},
407+
abilityIpfsCid: 'QmQtGCkrvTJsLvvLvZWFhtrschv7N3SDC3nMzFQY95pkt1',
408+
appId: 47700028661,
409+
appVersion: 19,
410+
policiesContext: { allow: true, evaluatedPolicies: [], allowedPolicies: {} }
411+
}
412+
}
413+
```
414+
373415
## Trade History E2E Tests
374416

375417
This test will work on both Hyperliquid mainnet and testnet.
@@ -806,3 +848,75 @@ Note: if you open a position using `OrderType.MARKET`, your order is likely to b
806848
time: 1762844136785
807849
}
808850
```
851+
852+
## Send Perp USDC E2E Tests
853+
854+
This test will work on both Hyperliquid mainnet and testnet.
855+
856+
This test will send USDC from the Agent Wallet PKP's Hyperliquid Perp balance to another Hyperliquid perp account. The PKP must have at least a Perp balance of `SEND_AMOUNT_USDC` USDC.
857+
858+
```
859+
pnpx nx run ability-hyperliquid:test-e2e packages/apps/ability-hyperliquid/test/e2e/perp/send-perp-usdc.spec.ts
860+
```
861+
862+
You should see the following logs indicating the Send Perp USDC was successful:
863+
864+
```
865+
[should execute send perp USDC of 1.0 USDC] {
866+
success: true,
867+
result: {
868+
action: 'sendPerpUsdc',
869+
sendResult: { status: 'ok', response: { type: 'default' } }
870+
},
871+
context: {
872+
delegation: {
873+
delegateeAddress: '0xb50aBA1E265B52067aF97401C25C39efF57Fe83b',
874+
delegatorPkpInfo: {
875+
tokenId: '64398341638522492941822855531388288999933789608071521616433988119257635428447',
876+
ethAddress: '0x17f51B528A0eA0ea19ABa0F343Dc9beED0FCc428',
877+
publicKey: '0x04576960d83a4eaf042e585c1cf90035b869b087631312719fa2b96fecde30705dbf80711fc30f6435e6a0739d1f47201320563a7f51357c146421c96355f6cbe3'
878+
}
879+
},
880+
abilityIpfsCid: 'QmcKrDBzYPhBoe1CTAqrKSNcpZpDRsSuAt3PCUG5ZAWWXN',
881+
appId: 47700028661,
882+
appVersion: 18,
883+
policiesContext: { allow: true, evaluatedPolicies: [], allowedPolicies: {} }
884+
}
885+
}
886+
```
887+
888+
## Withdraw E2E Tests
889+
890+
This test will work on both Hyperliquid mainnet and testnet.
891+
892+
This test will withdraw USDC from the Agent Wallet PKP's Hyperliquid Perp balance to the PKP's ETH address on Arbitrum. The PKP must have at least a Perp balance of `WITHDRAW_AMOUNT_USDC` USDC.
893+
894+
```
895+
pnpx nx run ability-hyperliquid:test-e2e packages/apps/ability-hyperliquid/test/e2e/withdraw.spec.ts
896+
```
897+
898+
You should see the following logs indicating the Withdraw was successful:
899+
900+
```
901+
[should execute withdrawal of 5.0 USDC to Arbitrum] {
902+
success: true,
903+
result: {
904+
action: 'withdraw',
905+
withdrawResult: { status: 'ok', response: { type: 'default' } }
906+
},
907+
context: {
908+
delegation: {
909+
delegateeAddress: '0xb50aBA1E265B52067aF97401C25C39efF57Fe83b',
910+
delegatorPkpInfo: {
911+
tokenId: '64398341638522492941822855531388288999933789608071521616433988119257635428447',
912+
ethAddress: '0x17f51B528A0eA0ea19ABa0F343Dc9beED0FCc428',
913+
publicKey: '0x04576960d83a4eaf042e585c1cf90035b869b087631312719fa2b96fecde30705dbf80711fc30f6435e6a0739d1f47201320563a7f51357c146421c96355f6cbe3'
914+
}
915+
},
916+
abilityIpfsCid: 'QmQtGCkrvTJsLvvLvZWFhtrschv7N3SDC3nMzFQY95pkt1',
917+
appId: 47700028661,
918+
appVersion: 19,
919+
policiesContext: { allow: true, evaluatedPolicies: [], allowedPolicies: {} }
920+
}
921+
}
922+
```

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": "QmVSTjgvv9jEGiuyvTf3ESLx3hUMkPFshnyhqLXHgiNRN8"
2+
"ipfsCid": "QmaGop5dGvYeGpLd19FPH2i8fDqnHAHSgzyuhmetUqmU3C"
33
}
Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
export { hyperliquidAccountExists } from './hyperliquid-account-exists';
2-
export { depositPrechecks } from './deposit';
3-
export { transferPrechecks } from './transfer';
2+
export { depositPrechecks } from './deposit-usdc';
3+
export { transferPrechecks } from './transfer-usdc';
44
export { spotTradePrechecks } from './spot';
55
export { perpTradePrechecks } from './perp';
66
export { cancelOrderPrechecks, cancelAllOrdersForSymbolPrechecks } from './cancel';
7-
export { withdrawPrechecks } from './withdraw';
7+
export { withdrawPrechecks } from './withdraw-usdc';
8+
export { sendSpotAssetPrechecks } from './send-spot-asset';
9+
export { sendPerpUsdcPrechecks } from './send-perp-usdc';
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { ethers } from 'ethers';
2+
import * as hyperliquid from '@nktkas/hyperliquid';
3+
4+
export type SendPerpUsdcPrechecksResult =
5+
| SendPerpUsdcPrechecksResultSuccess
6+
| SendPerpUsdcPrechecksResultFailure;
7+
8+
export interface SendPerpUsdcPrechecksResultSuccess {
9+
success: true;
10+
availableBalance: string;
11+
requiredBalance: string;
12+
}
13+
14+
export interface SendPerpUsdcPrechecksResultFailure {
15+
success: false;
16+
reason: string;
17+
availableBalance?: string;
18+
requiredBalance?: string;
19+
}
20+
21+
export interface SendPerpUsdcParams {
22+
destination: string;
23+
amount: string;
24+
}
25+
26+
/**
27+
* Check if sending USDC from perp account to another Hyperliquid perp account can be executed
28+
*/
29+
export async function sendPerpUsdcPrechecks({
30+
infoClient,
31+
ethAddress,
32+
params,
33+
}: {
34+
infoClient: hyperliquid.InfoClient;
35+
ethAddress: string;
36+
params: SendPerpUsdcParams;
37+
}): Promise<SendPerpUsdcPrechecksResult> {
38+
// Validate destination address
39+
if (!ethers.utils.isAddress(params.destination)) {
40+
return {
41+
success: false,
42+
reason: `Invalid destination address: ${params.destination}`,
43+
};
44+
}
45+
46+
// Get perp account balance
47+
const clearinghouseState = await infoClient.clearinghouseState({
48+
user: ethAddress,
49+
});
50+
51+
const availableUsdcBalance = ethers.utils.parseUnits(
52+
clearinghouseState.marginSummary.accountValue,
53+
6, // USDC has 6 decimals
54+
);
55+
console.log('[sendPerpUsdcPrechecks] Available balance:', availableUsdcBalance);
56+
57+
const requestedAmount = ethers.BigNumber.from(params.amount);
58+
console.log('[sendPerpUsdcPrechecks] Requested amount:', requestedAmount);
59+
60+
if (availableUsdcBalance.lt(requestedAmount)) {
61+
return {
62+
success: false,
63+
reason: `Insufficient perp balance for send. Available: ${ethers.utils.formatUnits(availableUsdcBalance, 6)} USDC, Requested: ${ethers.utils.formatUnits(requestedAmount, 6)} USDC`,
64+
availableBalance: ethers.utils.formatUnits(availableUsdcBalance, 6),
65+
requiredBalance: ethers.utils.formatUnits(requestedAmount, 6),
66+
};
67+
}
68+
69+
return {
70+
success: true,
71+
availableBalance: ethers.utils.formatUnits(availableUsdcBalance, 6),
72+
requiredBalance: ethers.utils.formatUnits(requestedAmount, 6),
73+
};
74+
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import { ethers } from 'ethers';
2+
import * as hyperliquid from '@nktkas/hyperliquid';
3+
4+
export type SendSpotAssetPrechecksResult =
5+
| SendSpotAssetPrechecksResultSuccess
6+
| SendSpotAssetPrechecksResultFailure;
7+
8+
export interface SendSpotAssetPrechecksResultSuccess {
9+
success: true;
10+
availableBalance: string;
11+
requiredBalance: string;
12+
}
13+
14+
export interface SendSpotAssetPrechecksResultFailure {
15+
success: false;
16+
reason: string;
17+
availableBalance?: string;
18+
requiredBalance?: string;
19+
}
20+
21+
export interface SendSpotAssetParams {
22+
destination: string;
23+
token: string;
24+
amount: string;
25+
}
26+
27+
/**
28+
* Check if sending spot assets to another Hyperliquid spot account can be executed
29+
*/
30+
export async function sendSpotAssetPrechecks({
31+
infoClient,
32+
ethAddress,
33+
params,
34+
}: {
35+
infoClient: hyperliquid.InfoClient;
36+
ethAddress: string;
37+
params: SendSpotAssetParams;
38+
}): Promise<SendSpotAssetPrechecksResult> {
39+
// Validate destination address
40+
if (!ethers.utils.isAddress(params.destination)) {
41+
return {
42+
success: false,
43+
reason: `Invalid destination address: ${params.destination}`,
44+
};
45+
}
46+
47+
// Fetch token metadata to get the correct decimal places
48+
const spotMeta = await infoClient.spotMeta();
49+
const tokenInfo = spotMeta.tokens.find((t) => t.name === params.token);
50+
51+
if (!tokenInfo) {
52+
return {
53+
success: false,
54+
reason: `Token ${params.token} not found in spot metadata`,
55+
};
56+
}
57+
58+
// Get spot balances
59+
const spotClearinghouseState = await infoClient.spotClearinghouseState({
60+
user: ethAddress,
61+
});
62+
63+
// Find the token balance
64+
const tokenBalance = spotClearinghouseState.balances.find(
65+
(balance) => balance.coin === params.token,
66+
);
67+
68+
if (!tokenBalance) {
69+
return {
70+
success: false,
71+
reason: `No balance found for token: ${params.token}`,
72+
availableBalance: '0',
73+
requiredBalance: ethers.utils.formatUnits(params.amount, tokenInfo.weiDecimals),
74+
};
75+
}
76+
77+
// Convert the balance from human-readable format to smallest units using token's weiDecimals
78+
// weiDecimals represents the exchange precision for amounts
79+
const availableBalance = ethers.BigNumber.from(
80+
ethers.utils.parseUnits(tokenBalance.total, tokenInfo.weiDecimals),
81+
);
82+
console.log('[sendAssetPrechecks] Available balance:', availableBalance);
83+
84+
const requestedAmount = ethers.BigNumber.from(params.amount);
85+
console.log('[sendAssetPrechecks] Requested amount:', requestedAmount);
86+
87+
if (availableBalance.lt(requestedAmount)) {
88+
return {
89+
success: false,
90+
reason: `Insufficient ${params.token} balance for send. Available: ${ethers.utils.formatUnits(availableBalance, tokenInfo.weiDecimals)} ${params.token}, Requested: ${ethers.utils.formatUnits(requestedAmount, tokenInfo.weiDecimals)} ${params.token}`,
91+
availableBalance: ethers.utils.formatUnits(availableBalance, tokenInfo.weiDecimals),
92+
requiredBalance: ethers.utils.formatUnits(requestedAmount, tokenInfo.weiDecimals),
93+
};
94+
}
95+
96+
return {
97+
success: true,
98+
availableBalance: ethers.utils.formatUnits(availableBalance, tokenInfo.weiDecimals),
99+
requiredBalance: ethers.utils.formatUnits(requestedAmount, tokenInfo.weiDecimals),
100+
};
101+
}

0 commit comments

Comments
 (0)