edwin-sdk
Version:
SDK for integrating AI agents with DeFi protocols
282 lines (243 loc) • 10.7 kB
text/typescript
import * as ccxt from 'ccxt';
import { Hyperliquid, OrderType as HyperliquidOrderType } from 'hyperliquid';
import { TransactionRequest } from 'viem';
import { EdwinEVMWallet } from '../../core/wallets/evm_wallet/evm_wallet';
import { EdwinService } from '../../core/classes/edwinToolProvider';
import edwinLogger from '../../utils/logger';
import {
DepositParametersType,
WithdrawParametersType,
OpenPositionParametersType,
ClosePositionParametersType,
OrderType,
PositionType,
} from './parameters';
/**
* Service for interacting with the HyperLiquid protocol
*/
export class HyperLiquidService extends EdwinService {
private wallet: EdwinEVMWallet;
private exchange: ccxt.Exchange; // Using ccxt.Exchange type instead of any
private hyperliquid: Hyperliquid;
/**
* Creates a new HyperLiquidService
* @param wallet - The EVM wallet to use for transactions
*/
constructor(wallet: EdwinEVMWallet) {
super();
this.wallet = wallet;
// Initialize the CCXT HyperLiquid exchange
this.exchange = new ccxt.hyperliquid({
apiKey: '', // Will be set when needed
secret: '', // Will be set when needed
enableRateLimit: true,
});
// Initialize the HyperLiquid SDK with a private key
// Note: In a real implementation, we would need to securely access the private key
// For now, we'll use a placeholder approach
this.hyperliquid = new Hyperliquid({
privateKey: '0x' + '1'.repeat(64), // Placeholder private key
enableWs: false, // Disable WebSocket for now
testnet: false, // Use mainnet by default
});
}
/**
* Deposits funds to HyperLiquid
* @param params - Deposit parameters
* @returns Transaction result
*/
async deposit(params: DepositParametersType): Promise<Record<string, unknown>> {
try {
edwinLogger.info(`Depositing ${params.amount} ${params.asset} to HyperLiquid`);
// Get deposit address from HyperLiquid
const depositAddress = await this.exchange.fetchDepositAddress(params.asset);
if (!depositAddress || !depositAddress.address) {
throw new Error(`Failed to get deposit address for ${params.asset}`);
}
// Get wallet client for the transaction
const walletClient = this.wallet.getWalletClient('arbitrum');
if (!walletClient.account) {
throw new Error('Wallet not connected or no account available');
}
// Create transaction parameters
const txParams: TransactionRequest = {
to: depositAddress.address as `0x${string}`,
value: BigInt(params.amount * 10 ** 18), // Convert to wei
};
// Send the transaction with explicit typing
const txHash = await walletClient.sendTransaction({
account: walletClient.account,
chain: walletClient.chain,
to: txParams.to,
value: txParams.value,
});
const result = {
success: true,
amount: params.amount,
asset: params.asset,
address: depositAddress.address,
txHash: txHash,
};
edwinLogger.info(`Deposit successful: ${JSON.stringify(result)}`);
return result;
} catch (error) {
edwinLogger.error(`Error depositing to HyperLiquid: ${error}`);
throw error;
}
}
/**
* Withdraws funds from HyperLiquid
* @param params - Withdrawal parameters
* @returns Transaction result
*/
async withdraw(params: WithdrawParametersType): Promise<Record<string, unknown>> {
try {
edwinLogger.info(`Withdrawing ${params.amount} ${params.asset} from HyperLiquid`);
// Get the wallet address
const address = this.wallet.getAddress();
// Use the HyperLiquid SDK to initiate the withdrawal
const withdrawResult = await this.hyperliquid.exchange.initiateWithdrawal(address, params.amount);
// Convert to Record<string, unknown>
const result: Record<string, unknown> = {
...withdrawResult,
success: true,
};
edwinLogger.info(`Withdrawal successful: ${JSON.stringify(result)}`);
return result;
} catch (error) {
edwinLogger.error(`Error withdrawing from HyperLiquid: ${error}`);
throw error;
}
}
/**
* Opens a position on HyperLiquid
* @param params - Open position parameters
* @returns Order result
*/
async openPosition(params: OpenPositionParametersType): Promise<Record<string, unknown>> {
try {
const { asset, positionType, orderType, size, leverage, price, reduceOnly } = params;
edwinLogger.info(`Opening ${positionType} position for ${asset} with size ${size} USD`);
// Set leverage if specified
if (leverage > 1) {
await this.hyperliquid.exchange.updateLeverage(
asset,
'cross', // Use cross margin by default
leverage
);
edwinLogger.info(`Set leverage to ${leverage}x for ${asset}`);
}
// Determine order type and side
const is_buy = positionType === PositionType.LONG;
const sdk_order_type = orderType === OrderType.LIMIT ? 'limit' : 'market';
// Create the order using the HyperLiquid SDK
const orderResult = await this.hyperliquid.exchange.placeOrder({
coin: asset,
is_buy: is_buy,
sz: size,
limit_px: price || 0, // Use 0 for market orders
order_type: sdk_order_type as HyperliquidOrderType,
reduce_only: reduceOnly,
});
// Convert to Record<string, unknown>
const result: Record<string, unknown> = {
...orderResult,
success: true,
};
edwinLogger.info(`Position opened successfully: ${JSON.stringify(result)}`);
return result;
} catch (error) {
edwinLogger.error(`Error opening position on HyperLiquid: ${error}`);
throw error;
}
}
/**
* Closes a position on HyperLiquid
* @param params - Close position parameters
* @returns Order result
*/
async closePosition(params: ClosePositionParametersType): Promise<Record<string, unknown>> {
try {
const { asset, orderType, percentage, price } = params;
edwinLogger.info(`Closing position for ${asset} (${percentage}%)`);
// Get current position using CCXT as a fallback
// In a real implementation, we would use the HyperLiquid SDK's appropriate method
const positions = await this.exchange.fetchPositions([asset]);
// Use type assertion to handle the positions array
const position = (positions as ccxt.Position[]).find(pos => pos.symbol === asset);
if (!position) {
throw new Error(`No open position found for ${asset}`);
}
// Calculate amount to close based on percentage
// Use 'contracts' property for CCXT positions
const positionSize = (position['contracts'] as number) || 0;
const closeSize = positionSize * (percentage / 100);
// Determine side (opposite of current position)
const is_buy = (position['side'] as string) === 'short';
const sdk_order_type = orderType === OrderType.LIMIT ? 'limit' : 'market';
// Create the order using the HyperLiquid SDK
const orderResult = await this.hyperliquid.exchange.placeOrder({
coin: asset,
is_buy: is_buy,
sz: closeSize,
limit_px: price || 0, // Use 0 for market orders
order_type: sdk_order_type as HyperliquidOrderType,
reduce_only: true,
});
// Convert to Record<string, unknown>
const result: Record<string, unknown> = {
...orderResult,
success: true,
};
edwinLogger.info(`Position closed successfully: ${JSON.stringify(result)}`);
return result;
} catch (error) {
edwinLogger.error(`Error closing position on HyperLiquid: ${error}`);
throw error;
}
}
/**
* Gets the current balance on HyperLiquid
* @returns Balance information
*/
async getBalance(): Promise<Record<string, unknown>> {
try {
// Use the CCXT exchange to fetch balance as a fallback
// In a real implementation, we would use the HyperLiquid SDK's appropriate method
const balance = await this.exchange.fetchBalance();
return balance as Record<string, unknown>;
} catch (error) {
edwinLogger.error(`Error fetching balance from HyperLiquid: ${error}`);
throw error;
}
}
/**
* Gets open positions on HyperLiquid
* @returns List of open positions
*/
async getPositions(): Promise<Record<string, unknown>[]> {
try {
// Use the CCXT exchange to fetch positions as a fallback
// In a real implementation, we would use the HyperLiquid SDK's appropriate method
const positions = await this.exchange.fetchPositions();
// Convert to Record<string, unknown>[] using a more explicit approach
return (positions as ccxt.Position[]).map(pos => ({ ...pos }) as Record<string, unknown>);
} catch (error) {
edwinLogger.error(`Error fetching positions from HyperLiquid: ${error}`);
throw error;
}
}
/**
* Sets up authentication for the exchange
* @param privateKey - The private key to use for authentication
*/
private setupAuthentication(privateKey: string): void {
if (!privateKey) {
throw new Error('Private key is required for HyperLiquid authentication');
}
// In a real implementation, we would set up the authentication here
// For CCXT, this typically involves setting the apiKey and secret
this.exchange.apiKey = 'derived-from-private-key';
this.exchange.secret = 'derived-from-private-key';
}
}