Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
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: 5 additions & 0 deletions .nx/version-plans/version-plan-1763101098136.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
ability-hyperliquid: minor
---

Add Vincent builders fee to spot sell and perp long and short trades.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"ipfsCid": "QmaGop5dGvYeGpLd19FPH2i8fDqnHAHSgzyuhmetUqmU3C"
"ipfsCid": "QmeRz1hh6ejpaXiK2JDJbWpcNbAQvd4BzYgHCFErxGMCfS"
}
6 changes: 6 additions & 0 deletions packages/apps/ability-hyperliquid/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,9 @@ export type { CancelOrderResult } from './lib/ability-helpers/cancel-order/cance
export type { CancelAllOrdersResult } from './lib/ability-helpers/cancel-order/cancel-all-orders';
export type { SpotOrderResult } from './lib/ability-helpers/execute-spot-order';
export type { PerpOrderResult } from './lib/ability-helpers/execute-perp-order';
export type { ApproveBuilderCodeResult } from './lib/ability-helpers/approve-builder-code';
export type { WithdrawUsdcResult } from './lib/ability-helpers/withdraw-usdc';
export type { SendPerpUsdcResult } from './lib/ability-helpers/send-perp-usdc';
export type { SendSpotAssetResult } from './lib/ability-helpers/send-spot-asset';

export { HYPERLIQUID_BUILDER_ADDRESS, HYPERLIQUID_BUILDER_FEE_RATE } from './lib/constants';
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ export { cancelOrderPrechecks, cancelAllOrdersForSymbolPrechecks } from './cance
export { withdrawPrechecks } from './withdraw-usdc';
export { sendSpotAssetPrechecks } from './send-spot-asset';
export { sendPerpUsdcPrechecks } from './send-perp-usdc';
export { isBuilderCodeApproved } from './is-builder-code-approved';
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import type { InfoClient } from '@nktkas/hyperliquid';

/**
* Check if a given address has approved a given builder address
* @param infoClient - Hyperliquid InfoClient instance
* @param ethAddress - Ethereum address to check approval for
* @param builderAddress - Builder address to check approval for
* @returns true if the builder is approved, false otherwise
*/
export const isBuilderCodeApproved = async ({
infoClient,
ethAddress,
builderAddress,
}: {
infoClient: InfoClient;
ethAddress: string;
builderAddress: string;
}): Promise<boolean> => {
// Call maxBuilderFee to get the maximum builder fee approved for this builder
// If the builder is not approved, this will return 0
const maxFee = await infoClient.maxBuilderFee({
user: ethAddress as `0x${string}`,
builder: builderAddress as `0x${string}`,
});

// Builder is approved if maxFee is greater than 0
const isApproved = typeof maxFee === 'number' && maxFee > 0;

console.log('[isBuilderCodeApproved] Builder approval check', {
ethAddress,
builderAddress,
maxFee,
isApproved,
});

return isApproved;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import * as hyperliquid from '@nktkas/hyperliquid';
import {
ApproveBuilderFeeRequest,
ApproveBuilderFeeTypes,
SuccessResponse,
parser,
} from '@nktkas/hyperliquid/api/exchange';
import { signUserSignedAction } from '@nktkas/hyperliquid/signing';
import { bigIntReplacer } from '@lit-protocol/vincent-ability-sdk';

import { LitActionPkpEthersWallet } from './lit-action-pkp-ethers-wallet';
import { getHyperliquidNonce } from './get-hyperliquid-nonce';
import { getHyperliquidChainId, getHyperliquidChainName } from './get-hyperliquid-chain-id';

export type ApproveBuilderCodeResult = {
approveResult: SuccessResponse | hyperliquid.ErrorResponse;
};

/**
* Approve a builder code (builder address) with a maximum fee rate on Hyperliquid
* @param transport - Hyperliquid HTTP transport instance
* @param pkpPublicKey - PKP public key for signing
* @param builderAddress - Builder address to approve
* @param maxFeeRate - Maximum fee rate as a percentage string (e.g., "0.01" for 0.01%)
* @param useTestnet - Whether to use testnet (default: false)
* @returns Approval result from Hyperliquid
*/
export async function approveBuilderCode({
transport,
pkpPublicKey,
builderAddress,
maxFeeRate,
useTestnet = false,
}: {
transport: hyperliquid.HttpTransport;
pkpPublicKey: string;
builderAddress: string;
maxFeeRate: string; // e.g., "0.01" for 0.01%
useTestnet?: boolean;
}): Promise<ApproveBuilderCodeResult> {
const pkpWallet = new LitActionPkpEthersWallet(pkpPublicKey);
const nonce = await getHyperliquidNonce();

// Ensure maxFeeRate has the % suffix (required by Hyperliquid schema)
const maxFeeRateWithPercent = maxFeeRate.endsWith('%') ? maxFeeRate : `${maxFeeRate}%`;

// Construct approve builder fee action
const approveAction = parser(ApproveBuilderFeeRequest.entries.action)({
type: 'approveBuilderFee',
signatureChainId: getHyperliquidChainId(useTestnet),
hyperliquidChain: getHyperliquidChainName(useTestnet),
maxFeeRate: maxFeeRateWithPercent,
builder: builderAddress,
nonce,
});

// ApproveBuilderFee is a user-signed action that uses EIP-712 typed data
const signature = await signUserSignedAction({
wallet: pkpWallet,
action: approveAction,
types: ApproveBuilderFeeTypes,
});

const approveResult = await Lit.Actions.runOnce(
{ waitForResponse: true, name: 'HyperLiquidApproveBuilderFeeRequest' },
async () => {
return JSON.stringify(
{
result: await transport.request('exchange', {
action: approveAction,
signature,
nonce,
}),
},
bigIntReplacer,
2,
);
},
);

const parsedApproveResult = JSON.parse(approveResult);
return {
approveResult: parsedApproveResult.result as SuccessResponse | hyperliquid.ErrorResponse,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,17 @@ export interface PerpTradeParams {
leverage: number; // 1-10x
isCross: boolean; // true for cross margin, false for isolated
};
/**
* Builder fee configuration.
* Applies to both long and short orders for perpetuals.
* Fee is specified in tenths of basis points (e.g., 10 = 0.01% = 1 basis point).
* Maximum builder fee is 0.1% (100 tenths of basis points) for perps.
* @see https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint#builder-codes
*/
builderFee?: {
builderAddress: string; // Builder address
feeInTenthsOfBps: number; // Fee in tenths of basis points (e.g., 10 = 0.01%)
};
}

export type PerpOrderResult = PerpOrderResultSuccess | PerpOrderResultFailure;
Expand Down Expand Up @@ -135,7 +146,22 @@ export async function executePerpOrder({
: { limit: { tif: orderType.tif } };

// Construct order action
const orderAction = parser(OrderRequest.entries.action)({
const orderActionParams: {
type: 'order';
orders: Array<{
a: number;
b: boolean;
p: string;
s: string;
r: boolean;
t: typeof orderTypeField;
}>;
grouping: 'na';
builder?: {
b: `0x${string}`;
f: number;
};
} = {
type: 'order',
orders: [
{
Expand All @@ -148,7 +174,18 @@ export async function executePerpOrder({
},
],
grouping: 'na',
});
};

// Add builder fee if provided
// Builder codes apply to both long and short orders for perpetuals
if (params.builderFee) {
orderActionParams.builder = {
b: params.builderFee.builderAddress as `0x${string}`,
f: params.builderFee.feeInTenthsOfBps,
};
}

const orderAction = parser(OrderRequest.entries.action)(orderActionParams);

// Sign and send
const signature = await signL1Action({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,16 @@ export interface SpotTradeParams {
* @default { type: 'limit', tif: 'Gtc' }
*/
orderType?: { type: 'limit'; tif: TimeInForce } | { type: 'market' };
/**
* Builder fee configuration.
* Only applies to sell orders (builder codes do not apply to buying side of spot trades).
* Fee is specified in tenths of basis points (e.g., 50 = 0.05% = 5 basis points).
* @see https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint#builder-codes
*/
builderFee?: {
builderAddress: string; // Builder address
feeInTenthsOfBps: number; // Fee in tenths of basis points (e.g., 50 = 0.05%)
};
}

export type SpotOrderResult = SpotOrderResultSuccess | SpotOrderResultFailure;
Expand Down Expand Up @@ -71,7 +81,23 @@ export async function executeSpotOrder({
: { limit: { tif: orderType.tif } };

// Construct order action
const orderAction = parser(OrderRequest.entries.action)({
// Note: Builder codes only apply to sell orders (not buy orders) for spot trades
const orderActionParams: {
type: 'order';
orders: Array<{
a: number;
b: boolean;
p: string;
s: string;
r: boolean;
t: typeof orderTypeField;
}>;
grouping: 'na';
builder?: {
b: `0x${string}`;
f: number;
};
} = {
type: 'order',
orders: [
{
Expand All @@ -84,7 +110,18 @@ export async function executeSpotOrder({
},
],
grouping: 'na',
});
};

// Add builder fee if provided and this is a sell order
// Builder codes do not apply to the buying side of spot trades
if (params.builderFee && !params.isBuy) {
orderActionParams.builder = {
b: params.builderFee.builderAddress as `0x${string}`,
f: params.builderFee.feeInTenthsOfBps,
};
}

const orderAction = parser(OrderRequest.entries.action)(orderActionParams);

// Sign and send
const signature = await signL1Action({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ export { executePerpOrder } from './execute-perp-order';
export { withdrawUsdc } from './withdraw-usdc';
export { sendSpotAsset } from './send-spot-asset';
export { sendPerpUsdc } from './send-perp-usdc';
export { approveBuilderCode } from './approve-builder-code';
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { getHyperliquidNonce } from './get-hyperliquid-nonce';
import { getHyperliquidChainId, getHyperliquidChainName } from './get-hyperliquid-chain-id';

export type SendPerpUsdcResult = {
sendResult: SuccessResponse;
sendResult: SuccessResponse | hyperliquid.ErrorResponse;
};

/**
Expand Down Expand Up @@ -75,6 +75,6 @@ export async function sendPerpUsdc({

const parsedSendResult = JSON.parse(sendResult);
return {
sendResult: parsedSendResult.result as SuccessResponse,
sendResult: parsedSendResult.result as SuccessResponse | hyperliquid.ErrorResponse,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { getHyperliquidNonce } from './get-hyperliquid-nonce';
import { getHyperliquidChainId, getHyperliquidChainName } from './get-hyperliquid-chain-id';

export type SendSpotAssetResult = {
sendResult: SuccessResponse;
sendResult: SuccessResponse | hyperliquid.ErrorResponse;
};

/**
Expand Down Expand Up @@ -89,6 +89,6 @@ export async function sendSpotAsset({

const parsedSendResult = JSON.parse(sendResult);
return {
sendResult: parsedSendResult.result as SuccessResponse,
sendResult: parsedSendResult.result as SuccessResponse | hyperliquid.ErrorResponse,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { getHyperliquidNonce } from './get-hyperliquid-nonce';
import { getHyperliquidChainId, getHyperliquidChainName } from './get-hyperliquid-chain-id';

export type TransferUsdcResult = {
transferResult: SuccessResponse;
transferResult: SuccessResponse | hyperliquid.ErrorResponse;
};

/**
Expand Down Expand Up @@ -79,6 +79,6 @@ export async function transferUsdcTo({

const parsedTransferResult = JSON.parse(transferResult);
return {
transferResult: parsedTransferResult.result as SuccessResponse,
transferResult: parsedTransferResult.result as SuccessResponse | hyperliquid.ErrorResponse,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { getHyperliquidNonce } from './get-hyperliquid-nonce';
import { getHyperliquidChainId, getHyperliquidChainName } from './get-hyperliquid-chain-id';

export type WithdrawUsdcResult = {
withdrawResult: SuccessResponse;
withdrawResult: SuccessResponse | hyperliquid.ErrorResponse;
};

/**
Expand Down Expand Up @@ -75,6 +75,6 @@ export async function withdrawUsdc({

const parsedWithdrawResult = JSON.parse(withdrawResult);
return {
withdrawResult: parsedWithdrawResult.result as SuccessResponse,
withdrawResult: parsedWithdrawResult.result as SuccessResponse | hyperliquid.ErrorResponse,
};
}
2 changes: 2 additions & 0 deletions packages/apps/ability-hyperliquid/src/lib/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const HYPERLIQUID_BUILDER_ADDRESS = '0x132Db5f531Ba38628F1640683354229daE04E8e6';
export const HYPERLIQUID_BUILDER_FEE_RATE = '0.05'; // Percentage as string (e.g., "0.05" = 0.05%)
6 changes: 6 additions & 0 deletions packages/apps/ability-hyperliquid/src/lib/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const actionTypeSchema = z.enum([
'perpShort',
'cancelOrder',
'cancelAllOrdersForSymbol',
'approveBuilderCode',
]);

export const depositParamsSchema = z.object({
Expand Down Expand Up @@ -206,6 +207,7 @@ export const abilityParamsSchema = z
if (data.action === 'perpLong' || data.action === 'perpShort') return !!data.perp;
if (data.action === 'cancelOrder') return !!data.cancelOrder;
if (data.action === 'cancelAllOrdersForSymbol') return !!data.cancelAllOrdersForSymbol;
if (data.action === 'approveBuilderCode') return true; // No params required
return false;
},
{
Expand Down Expand Up @@ -251,4 +253,8 @@ export const executeSuccessSchema = z.object({
.any()
.optional()
.describe('Cancel result (for spotCancelOrder/spotCancelAll action)'),
approveResult: z
.any()
.optional()
.describe('Approve builder code result (for approveBuilderCode action)'),
});
1 change: 1 addition & 0 deletions packages/apps/ability-hyperliquid/src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export enum HyperliquidAction {
PERP_SHORT = 'perpShort',
CANCEL_ORDER = 'cancelOrder',
CANCEL_ALL_ORDERS_FOR_SYMBOL = 'cancelAllOrdersForSymbol',
APPROVE_BUILDER_CODE = 'approveBuilderCode',
}

/**
Expand Down
Loading