UNPKG

@shogun-sdk/money-legos

Version:

Shogun Money Legos: clients and types for quotes, memes, prices, balances, fees, validations, etc.

328 lines (279 loc) 10.6 kB
import { ethers } from 'ethers'; import { USDC_ABI } from '../abi/usdc-arbitrum.abi.js'; import { Hyperliquid } from '../index.js'; import { batchedDepositWithPermitPayloadDto, FunctionResult, PermitPayload, UserOpenOrders, } from '../types/index.js'; import { CancelOrderRequest, OrderRequest, OrderResponse, OrderType } from '../types/index.js'; import { isSpotAsset } from '../utils/helpers.js'; import { CancelOrderResponse } from '../utils/signing.js'; import { SymbolConversion } from '../utils/symbolConversion.js'; import { ExchangeAPI } from './exchange.js'; import { InfoAPI } from './info.js'; import { HL_BRIDGE2_CA } from '../types/constants.js'; import { ARBITRUM_CHAIN_ID, getRpcUrls } from '../../config/chains.js'; import { STABLECOINS } from '../../config/stablecoins.js'; export class CustomOperations { private exchange: ExchangeAPI; private infoApi: InfoAPI; private wallet: ethers.Wallet; private symbolConversion: SymbolConversion; private parent: Hyperliquid; private DEFAULT_SLIPPAGE = 0.05; private MAX_DECIMALS_SPOT = 8; private MAX_DECIMALS_PERP = 6; constructor( exchange: ExchangeAPI, infoApi: InfoAPI, privateKey: string, symbolConversion: SymbolConversion, parent: Hyperliquid, ) { this.exchange = exchange; this.infoApi = infoApi; this.wallet = new ethers.Wallet(privateKey); this.symbolConversion = symbolConversion; this.parent = parent; } async cancelAllOrders(symbol?: string): Promise<CancelOrderResponse> { try { await this.parent.ensureInitialized(); const address = this.wallet.address; const openOrders: UserOpenOrders = await this.infoApi.getUserOpenOrders(address); let ordersToCancel: UserOpenOrders; for (let order of openOrders) { order.coin = await this.symbolConversion.convertSymbol(order.coin); } if (symbol) { ordersToCancel = openOrders.filter((order) => order.coin === symbol); } else { ordersToCancel = openOrders; } if (ordersToCancel.length === 0) { throw new Error('No orders to cancel'); } const cancelRequests: CancelOrderRequest[] = ordersToCancel.map((order) => ({ coin: order.coin, o: order.oid, })); const response = await this.exchange.cancelOrder(cancelRequests); return response; } catch (error) { throw error; } } async getAllAssets(): Promise<{ perp: string[]; spot: string[] }> { await this.parent.ensureInitialized(); return await this.symbolConversion.getAllAssets(); } private async getSlippagePrice(symbol: string, isBuy: boolean, slippage: number, px?: number): Promise<number> { const convertedSymbol = await this.symbolConversion.convertSymbol(symbol); if (!px) { const allMids = await this.infoApi.getAllMids(); px = Number(allMids[convertedSymbol]); } px *= isBuy ? 1 + slippage : 1 - slippage; if (!(await this.isValidPrice(symbol, px))) { const result = await this.adjustPrice(symbol, px); if (result.success === false) { return px; } else { px = result.data; } } return px; } async marketOpen( symbol: string, isBuy: boolean, size: number, px?: number, slippage: number = this.DEFAULT_SLIPPAGE, cloid?: string, ): Promise<OrderResponse> { await this.parent.ensureInitialized(); const convertedSymbol = await this.symbolConversion.convertSymbol(symbol); if (px && !this.isValidPrice(symbol, px)) { throw new Error(`Invalid price: ${px}`); } const slippagePrice = await this.getSlippagePrice(convertedSymbol, isBuy, slippage, px); const orderRequest: OrderRequest = { coin: convertedSymbol, is_buy: isBuy, sz: size, limit_px: slippagePrice, order_type: { limit: { tif: 'Ioc' } } as OrderType, reduce_only: false, }; if (cloid) { orderRequest.cloid = cloid; } console.log(orderRequest); return this.exchange.placeOrder(orderRequest); } async marketClose( symbol: string, size?: number, px?: number, slippage: number = this.DEFAULT_SLIPPAGE, cloid?: string, ): Promise<OrderResponse> { await this.parent.ensureInitialized(); const convertedSymbol = await this.symbolConversion.convertSymbol(symbol); const address = this.wallet.address; const positions = await this.infoApi.perpetuals.getClearinghouseState(address); for (const position of positions.assetPositions) { const item = position.position; if (convertedSymbol !== item.coin) { continue; } const szi = parseFloat(item.szi); const closeSize = size || Math.abs(szi); const isBuy = szi < 0; // Get aggressive Market Price const slippagePrice = await this.getSlippagePrice(convertedSymbol, isBuy, slippage, px); // Market Order is an aggressive Limit Order IoC const orderRequest: OrderRequest = { coin: convertedSymbol, is_buy: isBuy, sz: closeSize, limit_px: slippagePrice, order_type: { limit: { tif: 'Ioc' } } as OrderType, reduce_only: true, }; if (cloid) { orderRequest.cloid = cloid; } return this.exchange.placeOrder(orderRequest); } throw new Error(`No position found for ${convertedSymbol}`); } async closeAllPositions(slippage: number = this.DEFAULT_SLIPPAGE): Promise<OrderResponse[]> { try { await this.parent.ensureInitialized(); const address = this.wallet.address; const positions = await this.infoApi.perpetuals.getClearinghouseState(address); const closeOrders: Promise<OrderResponse>[] = []; console.log(positions); for (const position of positions.assetPositions) { const item = position.position; if (parseFloat(item.szi) !== 0) { const symbol = await this.symbolConversion.convertSymbol(item.coin, 'forward'); closeOrders.push(this.marketClose(symbol, undefined, undefined, slippage)); } } return await Promise.all(closeOrders); } catch (error) { throw error; } } // see docs: https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/tick-and-lot-size private async isValidPrice(symbol: string, price: number): Promise<boolean> { const isSpot = isSpotAsset(symbol); const maxDecimals = isSpot ? this.MAX_DECIMALS_SPOT : this.MAX_DECIMALS_PERP; const priceStr = price.toString(); const [_ = [], decimalPart = []] = priceStr.split('.'); const szDecimals = await this.symbolConversion.getAssetDecimals(symbol); if (typeof szDecimals !== 'number') { return false; } // Check significant figures const significantFigures = priceStr.replace('.', '').replace(/^0+/, '').length; // for example 12345.6 if (significantFigures > 5 && decimalPart) { return false; } // Check decimal places // for example 2.6789123123123123 if (decimalPart?.length > maxDecimals - szDecimals) { return false; } return true; } private async adjustPrice(symbol: string, price: number): Promise<FunctionResult<number>> { const isSpot = isSpotAsset(symbol); const priceStr = price.toString(); const [integerPart = [], decimalPart = []] = priceStr.split('.'); const szDecimals = await this.symbolConversion.getAssetDecimals(symbol); if (typeof szDecimals !== 'number') { return { success: false }; } let adjustedPrice = price; const significantFiguresInIntegerPart = Number(integerPart) === 0 ? 0 : Math.min(5, integerPart.length); if (significantFiguresInIntegerPart) { adjustedPrice = Number(price.toFixed(5 - significantFiguresInIntegerPart)); } else { let leadingZeroesInDecimalPart = 0; for (let i = 0; i < decimalPart.length; i++) { if (decimalPart[i] === '0') { leadingZeroesInDecimalPart++; } else { break; } } if (leadingZeroesInDecimalPart === 0) { adjustedPrice = Number(price.toFixed(5)); } else { const maxDecimals = isSpot ? this.MAX_DECIMALS_SPOT - szDecimals : this.MAX_DECIMALS_PERP - szDecimals; const decimals = leadingZeroesInDecimalPart + 5 > maxDecimals ? maxDecimals : leadingZeroesInDecimalPart + 5; adjustedPrice = Number(price.toFixed(decimals)); } } return { success: true, data: adjustedPrice }; } // See docs: https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/bridge2#:~:text=%7D-,Deposit%20with%20permit,-The%20bridge%20supports async signPermitToDeposit(usdcAmount: string): Promise<batchedDepositWithPermitPayloadDto[]> { const rpcUrl = getRpcUrls()[ARBITRUM_CHAIN_ID]?.rpc[0]; const provider = new ethers.JsonRpcProvider(rpcUrl); const ownerWallet = new ethers.Wallet(this.wallet.privateKey, provider); const usdcAddress = STABLECOINS[ARBITRUM_CHAIN_ID]?.USDC?.address; if (!usdcAddress) { throw new Error('USDC address not found'); } const usdcContract: ethers.Contract = new ethers.Contract(usdcAddress, USDC_ABI, ownerWallet); const domain = { name: 'USD Coin', version: '2', chainId: ARBITRUM_CHAIN_ID, verifyingContract: usdcAddress, }; const permitTypes = { Permit: [ { name: 'owner', type: 'address' }, { name: 'spender', type: 'address' }, { name: 'value', type: 'uint256' }, { name: 'nonce', type: 'uint256' }, { name: 'deadline', type: 'uint256' }, ], }; if (!usdcContract) { throw new Error('USDC contract not found'); } const nonces = await (usdcContract as any).nonces(ownerWallet.address); const payload: PermitPayload = { owner: ownerWallet.address as `0x${string}`, // The address of the user with funds we want to deposit spender: HL_BRIDGE2_CA, value: BigInt(usdcAmount), nonce: BigInt(nonces ?? '0'), deadline: BigInt(Math.floor(Date.now() / 1000) + 3600), // 1 hour from now }; const data = { domain, types: permitTypes, message: payload, }; const signature = await ownerWallet.signTypedData(data.domain, data.types, data.message); const deposits: batchedDepositWithPermitPayloadDto[] = [ { user: ownerWallet.address, usd: payload.value.toString(), deadline: payload.deadline.toString(), signature, }, ]; return deposits; } }