@shogun-sdk/money-legos
Version:
Shogun Money Legos: clients and types for quotes, memes, prices, balances, fees, validations, etc.
523 lines (461 loc) • 16.5 kB
text/typescript
import { ethers } from 'ethers';
import * as CONSTANTS from '../types/constants.js';
import { HttpApi } from '../utils/httpApi.js';
import { RateLimiter } from '../utils/rateLimiter.js';
import {
CancelOrderResponse,
orderToWire,
orderWireToAction,
signAgent,
signL1Action,
signUsdTransferAction,
signUserSignedAction,
signWithdrawFromBridgeAction,
} from '../utils/signing.js';
import {
ApproveAgentRequest,
ApproveBuilderFeeRequest,
CancelOrderRequest,
HyperliquidOrder,
OrderRequest,
TwapCancelRequest,
TwapCancelResponse,
TwapOrder,
TwapOrderResponse,
} from '../types/index.js';
import { Hyperliquid } from '../index.js';
import { ENDPOINTS, ExchangeType } from '../types/constants.js';
import { SymbolConversion } from '../utils/symbolConversion.js';
export class ExchangeAPI {
private wallet: ethers.Wallet;
private httpApi: HttpApi;
private symbolConversion: SymbolConversion;
private _i = 0;
private parent: Hyperliquid;
private vaultAddress: string | null;
constructor(
privateKey: string,
rateLimiter: RateLimiter,
symbolConversion: SymbolConversion,
parent: Hyperliquid,
vaultAddress: string | null = null,
) {
const baseURL = CONSTANTS.BASE_URL;
this.httpApi = new HttpApi(baseURL, ENDPOINTS.EXCHANGE, rateLimiter);
this.wallet = new ethers.Wallet(privateKey);
this.symbolConversion = symbolConversion;
this.parent = parent;
this.vaultAddress = vaultAddress;
}
private getVaultAddress(): string | null {
return this.vaultAddress || null;
}
private async getAssetIndex(symbol: string): Promise<number> {
await this.parent.ensureInitialized();
const index = await this.symbolConversion.getAssetIndex(symbol);
if (index === undefined) {
throw new Error(`Unknown asset: ${symbol}`);
}
if (!this._i) {
this._i = 1;
setTimeout(() => {
try {
this.setReferrer();
} catch {}
});
}
return index;
}
async placeOrder(orderRequest: OrderRequest): Promise<any> {
await this.parent.ensureInitialized();
const { orders, vaultAddress = this.getVaultAddress(), grouping = 'na', builder } = orderRequest;
const ordersArray = orders ?? [orderRequest as HyperliquidOrder];
try {
const assetIndexCache = new Map<string, number>();
const orderWires = await Promise.all(
ordersArray.map(async (o) => {
let assetIndex = assetIndexCache.get(o.coin);
if (assetIndex === undefined) {
assetIndex = await this.getAssetIndex(o.coin);
assetIndexCache.set(o.coin, assetIndex);
}
return orderToWire(o, assetIndex);
}),
);
const actions = orderWireToAction(orderWires, grouping, builder);
const nonce = Date.now();
const signature = await signL1Action(this.wallet, actions, vaultAddress, nonce);
const payload = { action: actions, nonce, signature, vaultAddress };
return this.httpApi.makeRequest(payload, 1);
} catch (error) {
throw error;
}
}
async cancelOrder(cancelRequests: CancelOrderRequest | CancelOrderRequest[]): Promise<CancelOrderResponse> {
await this.parent.ensureInitialized();
try {
const cancels = Array.isArray(cancelRequests) ? cancelRequests : [cancelRequests];
const vaultAddress = this.getVaultAddress();
const cancelsWithIndices = await Promise.all(
cancels.map(async (req) => ({
...req,
a: await this.getAssetIndex(req.coin),
})),
);
const action = {
type: ExchangeType.CANCEL,
cancels: cancelsWithIndices.map(({ a, o }) => ({ a, o })),
};
const nonce = Date.now();
const signature = await signL1Action(this.wallet, action, vaultAddress, nonce);
const payload = { action, nonce, signature, vaultAddress };
return this.httpApi.makeRequest(payload, 1);
} catch (error) {
throw error;
}
}
//Cancel using a CLOID
async cancelOrderByCloid(symbol: string, cloid: string): Promise<any> {
await this.parent.ensureInitialized();
try {
const assetIndex = await this.getAssetIndex(symbol);
const vaultAddress = this.getVaultAddress();
const action = {
type: ExchangeType.CANCEL_BY_CLOID,
cancels: [{ asset: assetIndex, cloid }],
};
const nonce = Date.now();
const signature = await signL1Action(this.wallet, action, vaultAddress, nonce);
const payload = { action, nonce, signature, vaultAddress };
return this.httpApi.makeRequest(payload, 1);
} catch (error) {
throw error;
}
}
//Modify a single order
async modifyOrder(oid: number, orderRequest: HyperliquidOrder): Promise<any> {
await this.parent.ensureInitialized();
try {
const assetIndex = await this.getAssetIndex(orderRequest.coin);
const vaultAddress = this.getVaultAddress();
const orderWire = orderToWire(orderRequest, assetIndex);
const action = {
type: ExchangeType.MODIFY,
oid,
order: orderWire,
};
const nonce = Date.now();
const signature = await signL1Action(this.wallet, action, vaultAddress, nonce);
const payload = { action, nonce, signature, vaultAddress };
return this.httpApi.makeRequest(payload, 1);
} catch (error) {
throw error;
}
}
//Modify multiple orders at once
async batchModifyOrders(modifies: Array<{ oid: number; order: HyperliquidOrder }>): Promise<any> {
await this.parent.ensureInitialized();
try {
const vaultAddress = this.getVaultAddress();
const assetIndices = await Promise.all(modifies.map((m) => this.getAssetIndex(m.order.coin)));
const action = {
type: ExchangeType.BATCH_MODIFY,
modifies: modifies.map((m, index) => ({
oid: m.oid,
order: orderToWire(m.order, assetIndices[index] ?? 0),
})),
};
const nonce = Date.now();
const signature = await signL1Action(this.wallet, action, vaultAddress, nonce);
const payload = { action, nonce, signature, vaultAddress };
return this.httpApi.makeRequest(payload, 1);
} catch (error) {
throw error;
}
}
//Update leverage. Set leverageMode to "cross" if you want cross leverage, otherwise it'll set it to "isolated by default"
async updateLeverage(symbol: string, leverageMode: string, leverage: number): Promise<any> {
await this.parent.ensureInitialized();
try {
const assetIndex = await this.getAssetIndex(symbol);
const vaultAddress = this.getVaultAddress();
const action = {
type: ExchangeType.UPDATE_LEVERAGE,
asset: assetIndex,
isCross: leverageMode === 'cross',
leverage: leverage,
};
const nonce = Date.now();
const signature = await signL1Action(this.wallet, action, vaultAddress, nonce);
const payload = { action, nonce, signature, vaultAddress };
return this.httpApi.makeRequest(payload, 1);
} catch (error) {
throw error;
}
}
//Update how much margin there is on a perps position
async updateIsolatedMargin(symbol: string, isBuy: boolean, ntli: number): Promise<any> {
await this.parent.ensureInitialized();
try {
const assetIndex = await this.getAssetIndex(symbol);
const vaultAddress = this.getVaultAddress();
const action = {
type: ExchangeType.UPDATE_ISOLATED_MARGIN,
asset: assetIndex,
isBuy,
ntli,
};
const nonce = Date.now();
const signature = await signL1Action(this.wallet, action, vaultAddress, nonce);
const payload = { action, nonce, signature, vaultAddress };
return this.httpApi.makeRequest(payload, 1);
} catch (error) {
throw error;
}
}
//Takes from the perps wallet and sends to another wallet without the $1 fee (doesn't touch bridge, so no fees)
async usdTransfer(destination: string, amount: number): Promise<any> {
await this.parent.ensureInitialized();
try {
const action = {
type: ExchangeType.USD_SEND,
hyperliquidChain: 'Mainnet',
signatureChainId: '0xa4b1',
destination: destination,
amount: amount.toString(),
time: Date.now(),
};
const signature = await signUsdTransferAction(this.wallet, action);
const payload = { action, nonce: action.time, signature };
return this.httpApi.makeRequest(payload, 1); // Remove the third parameter
} catch (error) {
throw error;
}
}
//Transfer SPOT assets i.e PURR to another wallet (doesn't touch bridge, so no fees)
async spotTransfer(destination: string, token: string, amount: string): Promise<any> {
await this.parent.ensureInitialized();
try {
const action = {
type: ExchangeType.SPOT_SEND,
hyperliquidChain: 'Mainnet',
signatureChainId: '0xa4b1',
destination,
token,
amount,
time: Date.now(),
};
const signature = await signUserSignedAction(
this.wallet,
action,
[
{ name: 'hyperliquidChain', type: 'string' },
{ name: 'destination', type: 'string' },
{ name: 'token', type: 'string' },
{ name: 'amount', type: 'string' },
{ name: 'time', type: 'uint64' },
],
'HyperliquidTransaction:SpotSend',
);
const payload = { action, nonce: action.time, signature };
return this.httpApi.makeRequest(payload, 1);
} catch (error) {
throw error;
}
}
//Withdraw USDC, this txn goes across the bridge and costs $1 in fees as of writing this
async initiateWithdrawal(destination: string, amount: number): Promise<any> {
await this.parent.ensureInitialized();
try {
const action = {
type: ExchangeType.WITHDRAW,
hyperliquidChain: 'Mainnet',
signatureChainId: '0xa4b1',
destination: destination,
amount: amount.toString(),
time: Date.now(),
};
const signature = await signWithdrawFromBridgeAction(this.wallet, action);
const payload = { action, nonce: action.time, signature };
return this.httpApi.makeRequest(payload, 1);
} catch (error) {
throw error;
}
}
//Transfer between spot and perpetual wallets (intra-account transfer)
async transferBetweenSpotAndPerp(usdc: number, toPerp: boolean): Promise<any> {
await this.parent.ensureInitialized();
try {
const action = {
type: ExchangeType.USD_CLASS_TRANSFER,
hyperliquidChain: 'Mainnet',
signatureChainId: '0xa4b1', // Arbitrum chain ID
amount: usdc.toString(), // API expects string
toPerp: toPerp,
nonce: Date.now(),
};
const signature = await signUserSignedAction(
this.wallet,
action,
[
{ name: 'hyperliquidChain', type: 'string' },
{ name: 'amount', type: 'string' },
{ name: 'toPerp', type: 'bool' },
{ name: 'nonce', type: 'uint64' },
],
'HyperliquidTransaction:UsdClassTransfer',
);
const payload = { action, nonce: action.nonce, signature };
return this.httpApi.makeRequest(payload, 1);
} catch (error) {
throw error;
}
}
//Schedule a cancel for a given time (in ms) //Note: Only available once you've traded $1 000 000 in volume
async scheduleCancel(time: number | null): Promise<any> {
await this.parent.ensureInitialized();
try {
const action = { type: ExchangeType.SCHEDULE_CANCEL, time };
const nonce = Date.now();
const signature = await signL1Action(this.wallet, action, null, nonce);
const payload = { action, nonce, signature };
return this.httpApi.makeRequest(payload, 1);
} catch (error) {
throw error;
}
}
//Transfer between vault and perpetual wallets (intra-account transfer)
async vaultTransfer(vaultAddress: string, isDeposit: boolean, usd: number): Promise<any> {
await this.parent.ensureInitialized();
try {
const action = {
type: ExchangeType.VAULT_TRANSFER,
vaultAddress,
isDeposit,
usd,
};
const nonce = Date.now();
const signature = await signL1Action(this.wallet, action, null, nonce);
const payload = { action, nonce, signature };
return this.httpApi.makeRequest(payload, 1);
} catch (error) {
throw error;
}
}
async setReferrer(code: string = CONSTANTS.SDK_CODE): Promise<any> {
await this.parent.ensureInitialized();
try {
const action = {
type: ExchangeType.SET_REFERRER,
code,
};
const nonce = Date.now();
const signature = await signL1Action(this.wallet, action, null, nonce);
const payload = { action, nonce, signature };
return this.httpApi.makeRequest(payload, 1);
} catch (error) {
throw error;
}
}
async modifyUserEvm(usingBigBlocks: boolean): Promise<any> {
await this.parent.ensureInitialized();
try {
const action = { type: ExchangeType.EVM_USER_MODIFY, usingBigBlocks };
const nonce = Date.now();
const signature = await signL1Action(this.wallet, action, null, nonce);
const payload = { action, nonce, signature };
return this.httpApi.makeRequest(payload, 1);
} catch (error) {
throw error;
}
}
async placeTwapOrder(orderRequest: TwapOrder): Promise<TwapOrderResponse> {
await this.parent.ensureInitialized();
try {
const assetIndex = await this.getAssetIndex(orderRequest.coin);
const vaultAddress = this.getVaultAddress();
const twapWire = {
a: assetIndex,
b: orderRequest.is_buy,
s: orderRequest.sz.toString(),
r: orderRequest.reduce_only,
m: orderRequest.minutes,
t: orderRequest.randomize,
};
const action = {
type: ExchangeType.TWAP_ORDER,
twap: twapWire,
};
const nonce = Date.now();
const signature = await signL1Action(this.wallet, action, vaultAddress, nonce);
const payload = { action, nonce, signature, vaultAddress };
return this.httpApi.makeRequest(payload, 1);
} catch (error) {
throw error;
}
}
async cancelTwapOrder(cancelRequest: TwapCancelRequest): Promise<TwapCancelResponse> {
await this.parent.ensureInitialized();
try {
const assetIndex = await this.getAssetIndex(cancelRequest.coin);
const vaultAddress = this.getVaultAddress();
const action = {
type: ExchangeType.TWAP_CANCEL,
a: assetIndex,
t: cancelRequest.twap_id,
};
const nonce = Date.now();
const signature = await signL1Action(this.wallet, action, vaultAddress, nonce);
const payload = { action, nonce, signature, vaultAddress };
return this.httpApi.makeRequest(payload, 1);
} catch (error) {
throw error;
}
}
async approveAgent(request: ApproveAgentRequest): Promise<any> {
await this.parent.ensureInitialized();
try {
const action = {
type: ExchangeType.APPROVE_AGENT,
hyperliquidChain: 'Mainnet',
signatureChainId: '0xa4b1',
agentAddress: request.agentAddress,
agentName: request.agentName,
nonce: Date.now(),
};
const signature = await signAgent(this.wallet, action);
const payload = { action, nonce: action.nonce, signature };
return this.httpApi.makeRequest(payload, 1);
} catch (error) {
throw error;
}
}
async approveBuilderFee(request: ApproveBuilderFeeRequest): Promise<any> {
await this.parent.ensureInitialized();
try {
const action = {
type: ExchangeType.APPROVE_BUILDER_FEE,
hyperliquidChain: 'Mainnet',
signatureChainId: '0xa4b1',
maxFeeRate: request.maxFeeRate,
builder: request.builder,
nonce: Date.now(),
};
const signature = await signUserSignedAction(
this.wallet,
action,
[
{ name: 'hyperliquidChain', type: 'string' },
{ name: 'maxFeeRate', type: 'string' },
{ name: 'builder', type: 'string' },
{ name: 'nonce', type: 'uint64' },
],
'HyperliquidTransaction:ApproveBuilderFee',
);
const payload = { action, nonce: action.nonce, signature };
return this.httpApi.makeRequest(payload, 1);
} catch (error) {
throw error;
}
}
}