@phantom/perps-client 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/README.md +147 -0
- package/jest.config.js +10 -0
- package/package.json +38 -0
- package/src/PerpsClient.test.ts +216 -0
- package/src/PerpsClient.ts +336 -0
- package/src/actions.test.ts +298 -0
- package/src/actions.ts +182 -0
- package/src/api.ts +366 -0
- package/src/constants.ts +65 -0
- package/src/index.ts +16 -0
- package/src/types.ts +227 -0
- package/src/validate.ts +18 -0
- package/tsconfig.json +17 -0
package/src/actions.ts
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EIP-712 typed data builders for Hyperliquid exchange actions.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors wallet2/wallet/packages/perps/src/sdk/orders/action.ts
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { encode } from "@msgpack/msgpack";
|
|
8
|
+
import { keccak256 } from "js-sha3";
|
|
9
|
+
import type {
|
|
10
|
+
Eip712TypedData,
|
|
11
|
+
HlAction,
|
|
12
|
+
HlOrderAction,
|
|
13
|
+
HlCancelAction,
|
|
14
|
+
HlUpdateLeverageAction,
|
|
15
|
+
HlUsdClassTransferAction,
|
|
16
|
+
} from "./types.js";
|
|
17
|
+
import {
|
|
18
|
+
HYPERLIQUID_EXCHANGE_DOMAIN,
|
|
19
|
+
HYPERLIQUID_SIGN_TRANSACTION_DOMAIN,
|
|
20
|
+
EIP712_DOMAIN_TYPE,
|
|
21
|
+
APPROVE_EXCHANGE_TYPE,
|
|
22
|
+
USD_CLASS_TRANSFER_TYPE,
|
|
23
|
+
HYPERLIQUID_MAINNET_CHAIN_ID,
|
|
24
|
+
MARKET_ORDER_SLIPPAGE,
|
|
25
|
+
} from "./constants.js";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Computes the connectionId hash for exchange actions (orders, cancel, leverage).
|
|
29
|
+
* Mirrors OrderAction.hash(), CancelOrderAction.hash(), UpdateLeverageAction.hash()
|
|
30
|
+
*/
|
|
31
|
+
function hashAction(action: HlOrderAction | HlCancelAction | HlUpdateLeverageAction, nonce: number): string {
|
|
32
|
+
const msgPackBytes = encode(action);
|
|
33
|
+
// vaultAddress === null: 9 extra bytes (8 for nonce uint64 + 1 for null indicator)
|
|
34
|
+
const data = new Uint8Array(msgPackBytes.length + 9);
|
|
35
|
+
data.set(msgPackBytes);
|
|
36
|
+
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
37
|
+
view.setBigUint64(msgPackBytes.length, BigInt(nonce));
|
|
38
|
+
view.setUint8(msgPackBytes.length + 8, 0); // vaultAddress === null
|
|
39
|
+
return `0x${keccak256(data)}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Builds EIP-712 typed data for order/cancel/leverage exchange actions.
|
|
44
|
+
* These use the "Agent" signing pattern with a msgpack+keccak256 connectionId.
|
|
45
|
+
*/
|
|
46
|
+
export function buildExchangeActionTypedData(
|
|
47
|
+
action: HlOrderAction | HlCancelAction | HlUpdateLeverageAction,
|
|
48
|
+
nonce: number,
|
|
49
|
+
isTestnet = false,
|
|
50
|
+
): Eip712TypedData {
|
|
51
|
+
const connectionId = hashAction(action, nonce);
|
|
52
|
+
return {
|
|
53
|
+
domain: HYPERLIQUID_EXCHANGE_DOMAIN,
|
|
54
|
+
primaryType: "Agent",
|
|
55
|
+
types: {
|
|
56
|
+
EIP712Domain: EIP712_DOMAIN_TYPE,
|
|
57
|
+
Agent: APPROVE_EXCHANGE_TYPE,
|
|
58
|
+
},
|
|
59
|
+
message: {
|
|
60
|
+
source: isTestnet ? "b" : "a",
|
|
61
|
+
connectionId,
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Builds EIP-712 typed data for UsdClassTransfer (deposit spot→perp or withdraw perp→spot).
|
|
68
|
+
* chainId is derived from action.signatureChainId (hex string) with a fallback to mainnet.
|
|
69
|
+
*/
|
|
70
|
+
export function buildUsdClassTransferTypedData(action: HlUsdClassTransferAction): Eip712TypedData {
|
|
71
|
+
const chainId = action.signatureChainId ? parseInt(action.signatureChainId, 16) : HYPERLIQUID_MAINNET_CHAIN_ID;
|
|
72
|
+
return {
|
|
73
|
+
domain: {
|
|
74
|
+
...HYPERLIQUID_SIGN_TRANSACTION_DOMAIN,
|
|
75
|
+
chainId,
|
|
76
|
+
},
|
|
77
|
+
primaryType: "HyperliquidTransaction:UsdClassTransfer",
|
|
78
|
+
types: {
|
|
79
|
+
EIP712Domain: EIP712_DOMAIN_TYPE,
|
|
80
|
+
"HyperliquidTransaction:UsdClassTransfer": USD_CLASS_TRANSFER_TYPE,
|
|
81
|
+
},
|
|
82
|
+
message: {
|
|
83
|
+
hyperliquidChain: action.hyperliquidChain,
|
|
84
|
+
amount: action.amount,
|
|
85
|
+
toPerp: action.toPerp,
|
|
86
|
+
nonce: action.nonce,
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Monotonically increasing nonce (uses current timestamp, falls back to lastNonce+1) */
|
|
92
|
+
let lastNonce = 0;
|
|
93
|
+
export function nextNonce(): number {
|
|
94
|
+
const now = Date.now();
|
|
95
|
+
lastNonce = now > lastNonce ? now : lastNonce + 1;
|
|
96
|
+
return lastNonce;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Split a 65-byte signature into r, s, v components.
|
|
101
|
+
*
|
|
102
|
+
* PhantomClient.ethereumSignTypedData() returns a base64url-encoded signature
|
|
103
|
+
* (the KMS API comment says "Return the base64 encoded signature").
|
|
104
|
+
* We decode it to raw bytes then extract r (bytes 0-31), s (bytes 32-63), v (byte 64).
|
|
105
|
+
*
|
|
106
|
+
* Also handles legacy 0x-prefixed hex strings (0x + 130 hex chars) for compatibility.
|
|
107
|
+
*/
|
|
108
|
+
export function splitSignature(signature: string): { r: string; s: string; v: number } {
|
|
109
|
+
const raw = signature.startsWith("0x") ? signature.slice(2) : signature;
|
|
110
|
+
|
|
111
|
+
// Standard hex format: exactly 130 hex chars (65 bytes)
|
|
112
|
+
if (raw.length === 130 && /^[0-9a-fA-F]+$/.test(raw)) {
|
|
113
|
+
return {
|
|
114
|
+
r: `0x${raw.slice(0, 64)}`,
|
|
115
|
+
s: `0x${raw.slice(64, 128)}`,
|
|
116
|
+
v: parseInt(raw.slice(128, 130), 16),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Base64url format (KMS response) — decode to bytes then extract components
|
|
121
|
+
const standardBase64 = raw.replace(/-/g, "+").replace(/_/g, "/");
|
|
122
|
+
const padded = standardBase64 + "=".repeat((4 - (standardBase64.length % 4)) % 4);
|
|
123
|
+
const bytes = Buffer.from(padded, "base64");
|
|
124
|
+
|
|
125
|
+
if (bytes.length < 65) {
|
|
126
|
+
throw new Error(`Signature too short after base64 decode: expected 65 bytes, got ${bytes.length} (raw="${raw}")`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
r: "0x" + bytes.slice(0, 32).toString("hex"),
|
|
131
|
+
s: "0x" + bytes.slice(32, 64).toString("hex"),
|
|
132
|
+
v: bytes[64],
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Formats a price to the required decimal precision for Hyperliquid.
|
|
138
|
+
* Uses at most 5 significant figures.
|
|
139
|
+
*/
|
|
140
|
+
export function formatPrice(price: number, szDecimals: number): string {
|
|
141
|
+
// Hyperliquid tick size rule: price decimals = max(0, 6 - szDecimals - floor(log10(price)))
|
|
142
|
+
const decimals = Math.max(0, 6 - szDecimals - Math.floor(Math.log10(Math.abs(price) + 1e-9)));
|
|
143
|
+
return price.toFixed(decimals);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Formats a size to the required decimal precision for Hyperliquid.
|
|
148
|
+
*/
|
|
149
|
+
export function formatSize(size: number, szDecimals: number): string {
|
|
150
|
+
return size.toFixed(szDecimals);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Resolves the limit price string to submit to Hyperliquid.
|
|
155
|
+
*
|
|
156
|
+
* - "limit" orders: validates limitPrice is present and a finite positive number,
|
|
157
|
+
* then formats it with the market's szDecimals.
|
|
158
|
+
* - "market" orders: applies MARKET_ORDER_SLIPPAGE to the current market price.
|
|
159
|
+
*
|
|
160
|
+
* Throws a descriptive error when a limit order is requested without a valid limitPrice.
|
|
161
|
+
*/
|
|
162
|
+
export function resolveLimitPrice(
|
|
163
|
+
orderType: "market" | "limit",
|
|
164
|
+
limitPrice: string | undefined,
|
|
165
|
+
marketPrice: number,
|
|
166
|
+
isBuy: boolean,
|
|
167
|
+
szDecimals: number,
|
|
168
|
+
): string {
|
|
169
|
+
if (orderType === "limit") {
|
|
170
|
+
if (!limitPrice) {
|
|
171
|
+
throw new Error("limitPrice is required for limit orders");
|
|
172
|
+
}
|
|
173
|
+
const parsed = parseFloat(limitPrice);
|
|
174
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
175
|
+
throw new Error(`limitPrice must be a finite positive number, got: ${limitPrice}`);
|
|
176
|
+
}
|
|
177
|
+
return formatPrice(parsed, szDecimals);
|
|
178
|
+
}
|
|
179
|
+
return formatPrice(marketPrice * (isBuy ? 1 + MARKET_ORDER_SLIPPAGE : 1 - MARKET_ORDER_SLIPPAGE), szDecimals);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export type { HlAction };
|
package/src/api.ts
ADDED
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thin HTTP wrapper for Phantom backend perps endpoints.
|
|
3
|
+
* Logs every request URL + params and every error response body.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import axios, { AxiosError } from "axios";
|
|
7
|
+
import type {
|
|
8
|
+
PerpAccountBalance,
|
|
9
|
+
PerpPosition,
|
|
10
|
+
PerpOrder,
|
|
11
|
+
PerpMarket,
|
|
12
|
+
HistoricalOrder,
|
|
13
|
+
FundingActivity,
|
|
14
|
+
SignatureComponents,
|
|
15
|
+
HlOrderAction,
|
|
16
|
+
HlCancelAction,
|
|
17
|
+
HlUpdateLeverageAction,
|
|
18
|
+
HlUsdClassTransferAction,
|
|
19
|
+
HlOrderResponse,
|
|
20
|
+
HlDefaultResponse,
|
|
21
|
+
HlCancelOrderResponse,
|
|
22
|
+
PerpsLogger,
|
|
23
|
+
} from "./types.js";
|
|
24
|
+
import { noopLogger } from "./types.js";
|
|
25
|
+
|
|
26
|
+
const DEFAULT_PHANTOM_VERSION = "mcp-server";
|
|
27
|
+
|
|
28
|
+
/** Keys whose values are redacted from debug logs. EVM addresses are public, only signatures are sensitive. */
|
|
29
|
+
const SENSITIVE_LOG_KEYS = new Set(["pubkey", "signature", "sig"]);
|
|
30
|
+
|
|
31
|
+
function sanitizeForLog(obj: Record<string, unknown>): Record<string, unknown> {
|
|
32
|
+
const result: Record<string, unknown> = {};
|
|
33
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
34
|
+
result[k] = SENSITIVE_LOG_KEYS.has(k.toLowerCase()) ? "[redacted]" : v;
|
|
35
|
+
}
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface PerpsApiOptions {
|
|
40
|
+
baseUrl: string;
|
|
41
|
+
appId?: string;
|
|
42
|
+
logger?: PerpsLogger;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export class PerpsApi {
|
|
46
|
+
private readonly baseUrl: string;
|
|
47
|
+
private readonly headers: Record<string, string>;
|
|
48
|
+
private readonly logger: PerpsLogger;
|
|
49
|
+
|
|
50
|
+
constructor(options: PerpsApiOptions) {
|
|
51
|
+
this.baseUrl = options.baseUrl.replace(/\/$/, "");
|
|
52
|
+
this.logger = options.logger ?? noopLogger;
|
|
53
|
+
|
|
54
|
+
this.headers = {
|
|
55
|
+
"x-phantom-platform": "ext-sdk",
|
|
56
|
+
"x-phantom-client": "mcp",
|
|
57
|
+
"X-Phantom-Version": process.env.PHANTOM_VERSION ?? DEFAULT_PHANTOM_VERSION,
|
|
58
|
+
};
|
|
59
|
+
if (options.appId) {
|
|
60
|
+
this.headers["x-api-key"] = options.appId;
|
|
61
|
+
this.headers["X-App-Id"] = options.appId;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private async get<T>(path: string, params?: Record<string, string>): Promise<T> {
|
|
66
|
+
const url = `${this.baseUrl}${path}`;
|
|
67
|
+
const safeParams = params ? sanitizeForLog(params as Record<string, unknown>) : undefined;
|
|
68
|
+
const paramStr = safeParams ? `?${new URLSearchParams(safeParams as Record<string, string>).toString()}` : "";
|
|
69
|
+
this.logger.debug(`GET ${url}${paramStr}`);
|
|
70
|
+
try {
|
|
71
|
+
const r = await axios.get<T>(url, { params, headers: this.headers });
|
|
72
|
+
this.logger.debug(`GET ${path} → ${r.status}`);
|
|
73
|
+
return r.data;
|
|
74
|
+
} catch (err) {
|
|
75
|
+
throw this.wrapAxiosError(err, "GET", url);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private async post<T>(path: string, body: unknown): Promise<T> {
|
|
80
|
+
const url = `${this.baseUrl}${path}`;
|
|
81
|
+
const safeBody = body && typeof body === "object" ? sanitizeForLog(body as Record<string, unknown>) : body;
|
|
82
|
+
this.logger.debug(`POST ${url} body=${JSON.stringify(safeBody)}`);
|
|
83
|
+
try {
|
|
84
|
+
const r = await axios.post<T>(url, body, { headers: this.headers });
|
|
85
|
+
this.logger.debug(`POST ${path} → ${r.status}`);
|
|
86
|
+
return r.data;
|
|
87
|
+
} catch (err) {
|
|
88
|
+
throw this.wrapAxiosError(err, "POST", url);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Wraps an AxiosError so the response body is visible in logs and error messages.
|
|
94
|
+
*/
|
|
95
|
+
private wrapAxiosError(err: unknown, method: string, url: string): Error {
|
|
96
|
+
if (err instanceof AxiosError) {
|
|
97
|
+
const status = err.response?.status ?? "no-response";
|
|
98
|
+
const body = err.response?.data ? JSON.stringify(err.response.data) : err.message;
|
|
99
|
+
const message = `Phantom API ${method} ${url} failed: HTTP ${status} — ${body}`;
|
|
100
|
+
this.logger.error(message);
|
|
101
|
+
return new Error(message);
|
|
102
|
+
}
|
|
103
|
+
return err instanceof Error ? err : new Error(String(err));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async getAccountBalance(user: string): Promise<PerpAccountBalance> {
|
|
107
|
+
this.logger.info(`getAccountBalance user=${user}`);
|
|
108
|
+
return this.get<PerpAccountBalance>("/swap/v2/perp/balance", { user });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async getFundingHistory(user: string): Promise<FundingActivity[]> {
|
|
112
|
+
this.logger.info(`getFundingHistory user=${user}`);
|
|
113
|
+
const data = await this.get<{ depositAndWithdrawals: RawFundingActivity[] }>(
|
|
114
|
+
"/swap/v2/perp/deposits-and-withdrawals",
|
|
115
|
+
{ user },
|
|
116
|
+
);
|
|
117
|
+
return data.depositAndWithdrawals.map(mapFundingActivity);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async getPositionsAndOpenOrders(user: string): Promise<{ positions: PerpPosition[]; openOrders: PerpOrder[] }> {
|
|
121
|
+
this.logger.info(`getPositionsAndOpenOrders user=${user}`);
|
|
122
|
+
const data = await this.get<{ positions: RawPosition[]; openOrders: RawOpenOrder[] }>(
|
|
123
|
+
"/swap/v2/perp/positions-and-open-orders",
|
|
124
|
+
{ user },
|
|
125
|
+
);
|
|
126
|
+
return {
|
|
127
|
+
positions: data.positions.map(mapPosition),
|
|
128
|
+
openOrders: data.openOrders.map(mapOpenOrder),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async getTradeHistory(user: string): Promise<HistoricalOrder[]> {
|
|
133
|
+
this.logger.info(`getTradeHistory user=${user}`);
|
|
134
|
+
const data = await this.get<{ tradeHistory: RawHistoricalOrder[] }>("/swap/v2/perp/trade-history", { user });
|
|
135
|
+
return data.tradeHistory.map(mapHistoricalOrder);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Fetch specific markets by CAIP-19 token address (e.g. "hypercore:mainnet/address:BTC").
|
|
140
|
+
* The backend requires at least one token — this is for targeted lookups.
|
|
141
|
+
*/
|
|
142
|
+
async getMarkets(tokens: string[]): Promise<PerpMarket[]> {
|
|
143
|
+
this.logger.info(`getMarkets tokens=${tokens.join(",")}`);
|
|
144
|
+
const data = await this.get<{ markets: RawMarket[] | Record<string, RawMarket> }>("/swap/v2/perp/markets", {
|
|
145
|
+
tokens: tokens.join(","),
|
|
146
|
+
});
|
|
147
|
+
// The API returns either an array or a keyed record depending on version
|
|
148
|
+
const items = Array.isArray(data.markets) ? data.markets : Object.values(data.markets);
|
|
149
|
+
return items.map(mapMarket);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Fetch trending/popular markets (no per-market tokens needed).
|
|
154
|
+
* Requires chainId, sortBy, sortDirection per backend DTO.
|
|
155
|
+
*/
|
|
156
|
+
async getTrendingMarkets(): Promise<PerpMarket[]> {
|
|
157
|
+
this.logger.info(`getTrendingMarkets`);
|
|
158
|
+
const data = await this.get<{ trendingMarkets: RawMarket[] }>("/swap/v2/perp/trending-markets", {
|
|
159
|
+
chainId: "hypercore:mainnet",
|
|
160
|
+
sortBy: "trending",
|
|
161
|
+
sortDirection: "desc",
|
|
162
|
+
});
|
|
163
|
+
return (data.trendingMarkets ?? []).map(mapMarket);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Fetch all available markets via the market-lists endpoint.
|
|
168
|
+
* Deduplicates across categories so each market symbol appears once.
|
|
169
|
+
*/
|
|
170
|
+
async getAllMarkets(): Promise<PerpMarket[]> {
|
|
171
|
+
this.logger.info(`getAllMarkets`);
|
|
172
|
+
const data = await this.get<Record<string, { markets: RawMarket[] }>>("/swap/v2/perp/market-lists");
|
|
173
|
+
const seen = new Set<string>();
|
|
174
|
+
const markets: PerpMarket[] = [];
|
|
175
|
+
for (const category of Object.values(data)) {
|
|
176
|
+
for (const raw of category.markets ?? []) {
|
|
177
|
+
if (!seen.has(raw.symbol)) {
|
|
178
|
+
seen.add(raw.symbol);
|
|
179
|
+
markets.push(mapMarket(raw));
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return markets;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* POST /swap/v2/place-order — place a single order (open/close position).
|
|
188
|
+
* Requires `taker` (user CAIP-19 address) unlike the generic /exchange endpoint.
|
|
189
|
+
*/
|
|
190
|
+
async postPlaceOrder(body: {
|
|
191
|
+
action: HlOrderAction;
|
|
192
|
+
nonce: number;
|
|
193
|
+
signature: SignatureComponents;
|
|
194
|
+
taker: string;
|
|
195
|
+
}): Promise<HlOrderResponse> {
|
|
196
|
+
this.logger.info(`postPlaceOrder nonce=${body.nonce} taker=${body.taker}`);
|
|
197
|
+
return this.post<HlOrderResponse>("/swap/v2/place-order", body);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* POST /swap/v2/cancel-order — cancel an open order.
|
|
202
|
+
* Requires `taker` (user CAIP-19 address) so the backend can identify the user.
|
|
203
|
+
*/
|
|
204
|
+
async postCancelOrder(body: {
|
|
205
|
+
action: HlCancelAction;
|
|
206
|
+
nonce: number;
|
|
207
|
+
signature: SignatureComponents;
|
|
208
|
+
taker: string;
|
|
209
|
+
}): Promise<HlCancelOrderResponse> {
|
|
210
|
+
this.logger.info(`postCancelOrder nonce=${body.nonce} taker=${body.taker}`);
|
|
211
|
+
return this.post<HlCancelOrderResponse>("/swap/v2/cancel-order", body);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* POST /swap/v2/perp/update-leverage — update leverage for a market.
|
|
216
|
+
* Requires `taker` (user CAIP-19 address) so the backend can identify the user.
|
|
217
|
+
*/
|
|
218
|
+
async postUpdateLeverage(body: {
|
|
219
|
+
action: HlUpdateLeverageAction;
|
|
220
|
+
nonce: number;
|
|
221
|
+
signature: SignatureComponents;
|
|
222
|
+
taker: string;
|
|
223
|
+
}): Promise<HlDefaultResponse> {
|
|
224
|
+
this.logger.info(
|
|
225
|
+
`postUpdateLeverage asset=${body.action.asset} leverage=${body.action.leverage} taker=${body.taker}`,
|
|
226
|
+
);
|
|
227
|
+
return this.post<HlDefaultResponse>("/swap/v2/perp/update-leverage", body);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async postTransferUsdcSpotPerp(body: {
|
|
231
|
+
action: HlUsdClassTransferAction;
|
|
232
|
+
nonce: number;
|
|
233
|
+
signature: SignatureComponents;
|
|
234
|
+
}): Promise<HlDefaultResponse> {
|
|
235
|
+
this.logger.info(`postTransferUsdcSpotPerp amount=${body.action.amount} toPerp=${body.action.toPerp}`);
|
|
236
|
+
return this.post<HlDefaultResponse>("/swap/v2/transfer-usdc-spot-perp", body);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ── Raw response shapes from Phantom backend ────────────────────────────────
|
|
241
|
+
|
|
242
|
+
interface RawPosition {
|
|
243
|
+
direction: "long" | "short";
|
|
244
|
+
leverage: string;
|
|
245
|
+
size: string;
|
|
246
|
+
margin: string;
|
|
247
|
+
entryPrice: string;
|
|
248
|
+
fundingPayments?: string;
|
|
249
|
+
market: { token: { address: string; chainId?: string }; logoUri?: string };
|
|
250
|
+
unrealizedPnl: { amount: string; percentage?: string } | null;
|
|
251
|
+
liquidationPrice: string;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
interface RawOpenOrder {
|
|
255
|
+
id: string;
|
|
256
|
+
market: { token: { address: string } };
|
|
257
|
+
isTrigger?: boolean;
|
|
258
|
+
direction: "long" | "short";
|
|
259
|
+
type: "limit" | "take_profit_market" | "stop_market";
|
|
260
|
+
limitPrice: string;
|
|
261
|
+
triggerPrice?: string;
|
|
262
|
+
size: string;
|
|
263
|
+
reduceOnly: boolean;
|
|
264
|
+
/** Backend sends timestamp as a string. */
|
|
265
|
+
timestamp: string;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
interface RawHistoricalOrder {
|
|
269
|
+
id: string;
|
|
270
|
+
market: { token: { address: string; chainId?: string }; logoUri?: string; szDecimals?: number };
|
|
271
|
+
type: string;
|
|
272
|
+
timestamp: number;
|
|
273
|
+
price: string;
|
|
274
|
+
size: string;
|
|
275
|
+
tradeValue: string;
|
|
276
|
+
fee: string;
|
|
277
|
+
closedPnl?: string;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
interface RawMarket {
|
|
281
|
+
symbol: string;
|
|
282
|
+
/** Numeric Hyperliquid asset index — used to construct orders. */
|
|
283
|
+
assetId: number;
|
|
284
|
+
name?: string;
|
|
285
|
+
logoUri?: string;
|
|
286
|
+
maxLeverage: number;
|
|
287
|
+
szDecimals: number;
|
|
288
|
+
price: string;
|
|
289
|
+
fundingRate: string;
|
|
290
|
+
openInterest: string;
|
|
291
|
+
volume24h: string;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** Raw shape of a single deposit-or-withdrawal item from the backend. */
|
|
295
|
+
interface RawFundingActivity {
|
|
296
|
+
id: string;
|
|
297
|
+
type: string;
|
|
298
|
+
/** Amount in USDC. */
|
|
299
|
+
usdcAmount: string;
|
|
300
|
+
timestamp: number;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function mapPosition(raw: RawPosition): PerpPosition {
|
|
304
|
+
const leverage = parseFloat(raw.leverage);
|
|
305
|
+
return {
|
|
306
|
+
coin: raw.market.token.address,
|
|
307
|
+
direction: raw.direction,
|
|
308
|
+
size: raw.size,
|
|
309
|
+
margin: raw.margin,
|
|
310
|
+
entryPrice: raw.entryPrice,
|
|
311
|
+
leverage: { type: "unknown", value: leverage },
|
|
312
|
+
unrealizedPnl: raw.unrealizedPnl?.amount ?? "0",
|
|
313
|
+
liquidationPrice: raw.liquidationPrice || null,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function mapOpenOrder(raw: RawOpenOrder): PerpOrder {
|
|
318
|
+
return {
|
|
319
|
+
id: raw.id,
|
|
320
|
+
coin: raw.market.token.address,
|
|
321
|
+
side: raw.direction,
|
|
322
|
+
type: raw.type,
|
|
323
|
+
isTrigger: raw.isTrigger ?? false,
|
|
324
|
+
limitPrice: raw.limitPrice,
|
|
325
|
+
triggerPrice: raw.triggerPrice,
|
|
326
|
+
size: raw.size,
|
|
327
|
+
reduceOnly: raw.reduceOnly,
|
|
328
|
+
timestamp: parseInt(raw.timestamp, 10),
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function mapHistoricalOrder(raw: RawHistoricalOrder): HistoricalOrder {
|
|
333
|
+
return {
|
|
334
|
+
id: raw.id,
|
|
335
|
+
coin: raw.market.token.address,
|
|
336
|
+
type: raw.type,
|
|
337
|
+
timestamp: raw.timestamp,
|
|
338
|
+
price: raw.price,
|
|
339
|
+
size: raw.size,
|
|
340
|
+
tradeValue: raw.tradeValue,
|
|
341
|
+
fee: raw.fee,
|
|
342
|
+
closedPnl: raw.closedPnl,
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function mapMarket(raw: RawMarket): PerpMarket {
|
|
347
|
+
return {
|
|
348
|
+
symbol: raw.symbol,
|
|
349
|
+
assetId: raw.assetId,
|
|
350
|
+
maxLeverage: raw.maxLeverage,
|
|
351
|
+
szDecimals: raw.szDecimals,
|
|
352
|
+
price: raw.price,
|
|
353
|
+
fundingRate: raw.fundingRate,
|
|
354
|
+
openInterest: raw.openInterest,
|
|
355
|
+
volume24h: raw.volume24h,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function mapFundingActivity(raw: RawFundingActivity): FundingActivity {
|
|
360
|
+
return {
|
|
361
|
+
id: raw.id,
|
|
362
|
+
type: raw.type,
|
|
363
|
+
amount: raw.usdcAmount,
|
|
364
|
+
timestamp: raw.timestamp,
|
|
365
|
+
};
|
|
366
|
+
}
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Constants for Hyperliquid EIP-712 signing and Phantom backend URLs.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export const HYPERLIQUID_MAINNET_CHAIN_ID = 42161;
|
|
6
|
+
export const HYPERLIQUID_TESTNET_CHAIN_ID = 421614;
|
|
7
|
+
|
|
8
|
+
/** EIP-712 domain for UsdClassTransfer actions (deposit/withdraw) */
|
|
9
|
+
export const HYPERLIQUID_SIGN_TRANSACTION_DOMAIN = {
|
|
10
|
+
name: "HyperliquidSignTransaction",
|
|
11
|
+
version: "1",
|
|
12
|
+
verifyingContract: "0x0000000000000000000000000000000000000000" as const,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/** EIP-712 domain for Exchange actions (orders, cancel, leverage) */
|
|
16
|
+
export const HYPERLIQUID_EXCHANGE_DOMAIN = {
|
|
17
|
+
name: "Exchange",
|
|
18
|
+
version: "1",
|
|
19
|
+
chainId: 1337,
|
|
20
|
+
verifyingContract: "0x0000000000000000000000000000000000000000" as const,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const EIP712_DOMAIN_TYPE = [
|
|
24
|
+
{ name: "name", type: "string" },
|
|
25
|
+
{ name: "version", type: "string" },
|
|
26
|
+
{ name: "chainId", type: "uint256" },
|
|
27
|
+
{ name: "verifyingContract", type: "address" },
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
export const APPROVE_EXCHANGE_TYPE = [
|
|
31
|
+
{ name: "source", type: "string" },
|
|
32
|
+
{ name: "connectionId", type: "bytes32" },
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
export const USD_CLASS_TRANSFER_TYPE = [
|
|
36
|
+
{ name: "hyperliquidChain", type: "string" },
|
|
37
|
+
{ name: "amount", type: "string" },
|
|
38
|
+
{ name: "toPerp", type: "bool" },
|
|
39
|
+
{ name: "nonce", type: "uint64" },
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
export const DEFAULT_API_BASE_URL = "https://api.phantom.app";
|
|
43
|
+
|
|
44
|
+
/** Arbitrum network ID used for EIP-712 signing on Hyperliquid */
|
|
45
|
+
export const ARBITRUM_NETWORK_ID = "eip155:42161";
|
|
46
|
+
|
|
47
|
+
/** CAIP-19 prefix for Hypercore (Hyperliquid) mainnet */
|
|
48
|
+
export const HYPERCORE_MAINNET_CHAIN_ID = "hypercore:mainnet";
|
|
49
|
+
|
|
50
|
+
/** 10% slippage for market orders */
|
|
51
|
+
export const MARKET_ORDER_SLIPPAGE = 0.1;
|
|
52
|
+
|
|
53
|
+
/** 2% slippage for spot sell orders (mirrors wallet2's BUY_SELL_PRICE_MULTIPLIER = 0.98) */
|
|
54
|
+
export const SPOT_SELL_SLIPPAGE = 0.02;
|
|
55
|
+
|
|
56
|
+
/** USDC token ID on Hyperliquid spot — no sell step needed when this is the bridged token */
|
|
57
|
+
export const USDC_SPOT_TOKEN_ID = "USDC";
|
|
58
|
+
|
|
59
|
+
/** Well-known USDC contract addresses on EVM chains */
|
|
60
|
+
export const USDC_ADDRESSES: Record<string, string> = {
|
|
61
|
+
"eip155:1": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
|
|
62
|
+
"eip155:8453": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
|
|
63
|
+
"eip155:42161": "0xaf88d065e77c8cc2239327c5edb3a432268e5831",
|
|
64
|
+
"eip155:137": "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359",
|
|
65
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export { PerpsClient } from "./PerpsClient.js";
|
|
2
|
+
export type { PerpsClientOptions } from "./PerpsClient.js";
|
|
3
|
+
export type {
|
|
4
|
+
PerpAccountBalance,
|
|
5
|
+
PerpPosition,
|
|
6
|
+
PerpOrder,
|
|
7
|
+
PerpMarket,
|
|
8
|
+
HistoricalOrder,
|
|
9
|
+
FundingActivity,
|
|
10
|
+
OpenPositionParams,
|
|
11
|
+
ClosePositionParams,
|
|
12
|
+
CancelOrderParams,
|
|
13
|
+
UpdateLeverageParams,
|
|
14
|
+
ActionResponse,
|
|
15
|
+
} from "./types.js";
|
|
16
|
+
export type { PerpsLogger } from "./types.js";
|