@shogun-sdk/money-legos
Version:
Shogun Money Legos: clients and types for quotes, memes, prices, balances, fees, validations, etc.
296 lines • 11.7 kB
JavaScript
import { ethers } from 'ethers';
import { USDC_ABI } from '../abi/usdc-arbitrum.abi.js';
import { isSpotAsset } from '../utils/helpers.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 {
constructor(exchange, infoApi, privateKey, symbolConversion, parent) {
Object.defineProperty(this, "exchange", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "infoApi", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "wallet", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "symbolConversion", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "parent", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "DEFAULT_SLIPPAGE", {
enumerable: true,
configurable: true,
writable: true,
value: 0.05
});
Object.defineProperty(this, "MAX_DECIMALS_SPOT", {
enumerable: true,
configurable: true,
writable: true,
value: 8
});
Object.defineProperty(this, "MAX_DECIMALS_PERP", {
enumerable: true,
configurable: true,
writable: true,
value: 6
});
this.exchange = exchange;
this.infoApi = infoApi;
this.wallet = new ethers.Wallet(privateKey);
this.symbolConversion = symbolConversion;
this.parent = parent;
}
async cancelAllOrders(symbol) {
try {
await this.parent.ensureInitialized();
const address = this.wallet.address;
const openOrders = await this.infoApi.getUserOpenOrders(address);
let ordersToCancel;
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 = ordersToCancel.map((order) => ({
coin: order.coin,
o: order.oid,
}));
const response = await this.exchange.cancelOrder(cancelRequests);
return response;
}
catch (error) {
throw error;
}
}
async getAllAssets() {
await this.parent.ensureInitialized();
return await this.symbolConversion.getAllAssets();
}
async getSlippagePrice(symbol, isBuy, slippage, px) {
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, isBuy, size, px, slippage = this.DEFAULT_SLIPPAGE, cloid) {
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 = {
coin: convertedSymbol,
is_buy: isBuy,
sz: size,
limit_px: slippagePrice,
order_type: { limit: { tif: 'Ioc' } },
reduce_only: false,
};
if (cloid) {
orderRequest.cloid = cloid;
}
console.log(orderRequest);
return this.exchange.placeOrder(orderRequest);
}
async marketClose(symbol, size, px, slippage = this.DEFAULT_SLIPPAGE, cloid) {
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 = {
coin: convertedSymbol,
is_buy: isBuy,
sz: closeSize,
limit_px: slippagePrice,
order_type: { limit: { tif: 'Ioc' } },
reduce_only: true,
};
if (cloid) {
orderRequest.cloid = cloid;
}
return this.exchange.placeOrder(orderRequest);
}
throw new Error(`No position found for ${convertedSymbol}`);
}
async closeAllPositions(slippage = this.DEFAULT_SLIPPAGE) {
try {
await this.parent.ensureInitialized();
const address = this.wallet.address;
const positions = await this.infoApi.perpetuals.getClearinghouseState(address);
const closeOrders = [];
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
async isValidPrice(symbol, price) {
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;
}
async adjustPrice(symbol, price) {
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) {
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 = 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.nonces(ownerWallet.address);
const payload = {
owner: ownerWallet.address, // 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 = [
{
user: ownerWallet.address,
usd: payload.value.toString(),
deadline: payload.deadline.toString(),
signature,
},
];
return deposits;
}
}
//# sourceMappingURL=custom.js.map