Skip to content

Commit e5f865d

Browse files
committed
feat: init send-spot-asset. Add debug false to getVincentAbilityClient in tests
1 parent 9fbc606 commit e5f865d

23 files changed

+585
-6
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

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": "Qmdakz7BTv3s9rma9RByEHcJeCUPFJc8AbSjhTkPWijhbR"
33
}
Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
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';
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+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ export { transferUsdcTo } from './transfer-usdc-to';
66
export { executeSpotOrder } from './execute-spot-order';
77
export { executePerpOrder } from './execute-perp-order';
88
export { withdrawUsdc } from './withdraw-usdc';
9+
export { sendSpotAsset } from './send-spot-asset';
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import * as hyperliquid from '@nktkas/hyperliquid';
2+
import {
3+
SpotSendRequest,
4+
SpotSendTypes,
5+
SuccessResponse,
6+
parser,
7+
} from '@nktkas/hyperliquid/api/exchange';
8+
import { signUserSignedAction } from '@nktkas/hyperliquid/signing';
9+
import { ethers } from 'ethers';
10+
import { bigIntReplacer } from '@lit-protocol/vincent-ability-sdk';
11+
12+
import { LitActionPkpEthersWallet } from './lit-action-pkp-ethers-wallet';
13+
import { getHyperliquidNonce } from './get-hyperliquid-nonce';
14+
15+
export type SendSpotAssetResult = {
16+
sendResult: SuccessResponse;
17+
};
18+
19+
/**
20+
* Send spot assets (USDC or other tokens) to another Hyperliquid spot account
21+
*/
22+
export async function sendSpotAsset({
23+
transport,
24+
pkpPublicKey,
25+
destination,
26+
token,
27+
amount,
28+
useTestnet = false,
29+
}: {
30+
transport: hyperliquid.HttpTransport;
31+
pkpPublicKey: string;
32+
destination: string;
33+
token: string;
34+
amount: string;
35+
useTestnet?: boolean;
36+
}): Promise<SendSpotAssetResult> {
37+
const pkpWallet = new LitActionPkpEthersWallet(pkpPublicKey);
38+
const nonce = await getHyperliquidNonce();
39+
40+
// Fetch token metadata to get the correct decimal places
41+
const infoClient = new hyperliquid.InfoClient({ transport });
42+
const spotMeta = await infoClient.spotMeta();
43+
44+
// Find the token in the metadata
45+
const tokenInfo = spotMeta.tokens.find((t) => t.name === token);
46+
if (!tokenInfo) {
47+
throw new Error(`Token ${token} not found in spot metadata`);
48+
}
49+
console.log('[sendAsset] Token info:', tokenInfo);
50+
51+
// Select chain ID and network based on testnet flag
52+
const signatureChainId = useTestnet
53+
? '0x66eee' // Arbitrum Sepolia testnet chain ID: 421614
54+
: '0xa4b1'; // Arbitrum mainnet chain ID: 42161
55+
const hyperliquidChain = useTestnet ? 'Testnet' : 'Mainnet';
56+
57+
// Construct send action
58+
const sendAction = parser(SpotSendRequest.entries.action)({
59+
type: 'spotSend',
60+
signatureChainId,
61+
hyperliquidChain,
62+
destination,
63+
// Construct token identifier in the format expected by SpotSend: "name:0xaddress"
64+
token: `${tokenInfo.name}:${tokenInfo.tokenId}`,
65+
// Convert amount from smallest units (micro-units) to human-readable format
66+
// using weiDecimals which represents the exchange precision for amounts
67+
amount: ethers.utils.formatUnits(amount, tokenInfo.weiDecimals),
68+
time: nonce,
69+
});
70+
71+
// SpotSend is a user-signed action that uses EIP-712 typed data
72+
const signature = await signUserSignedAction({
73+
wallet: pkpWallet,
74+
action: sendAction,
75+
types: SpotSendTypes,
76+
});
77+
78+
const sendResult = await Lit.Actions.runOnce(
79+
{ waitForResponse: true, name: 'HyperLiquidSendAssetRequest' },
80+
async () => {
81+
return JSON.stringify(
82+
{
83+
result: await transport.request('exchange', {
84+
action: sendAction,
85+
signature,
86+
nonce,
87+
}),
88+
},
89+
bigIntReplacer,
90+
2,
91+
);
92+
},
93+
);
94+
95+
const parsedSendResult = JSON.parse(sendResult);
96+
return {
97+
sendResult: parsedSendResult.result as SuccessResponse,
98+
};
99+
}

0 commit comments

Comments
 (0)