Skip to content
2 changes: 1 addition & 1 deletion tee-worker/omni-executor/executor-storage/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ pub use pumpx_account_profile::PumpxProfileStorage;
mod wildmeta_timestamp;
pub use wildmeta_timestamp::WildmetaTimestampStorage;
pub mod loan_record;
pub use loan_record::{LoanRecord, LoanRecordStorage};
pub use loan_record::{LoanRecord, LoanRecordStorage, LoanState};

pub use asset_lock::AssetLockStorage;
pub use asset_lock::Key as AssetLockStorageKey;
Expand Down
74 changes: 72 additions & 2 deletions tee-worker/omni-executor/executor-storage/src/loan_record.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,47 @@ pub struct Key {
pub nonce: u64,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode, Serialize, Deserialize)]
pub enum LoanState {
SpotSold,
ToPerpMoved,
HedgeOpened,
HedgeClosed,
ToSpotMoved,
SpotBought,
}

#[derive(Debug, Clone, Encode, Decode, Serialize, Deserialize)]
pub struct LoanRecord {
pub collateral_ticker: String,
pub collateral_size: String,
pub usdc_sold: String,
pub usdc_loaned: String,
pub spot_sell_cloid: String,
pub hedge_open_cloid: String,
pub usdc_for_perp: String,
// contains all tx hashes that the worker submits to interact with corewriter
// each tuple is a name -> hash mapping, e.g.:
// ("spot_sell", "0x1234...")
// we expect 6 hashes for a full loan request and payback cycle in normal cases,
// and up to 7 hashes if the hedge were partially filled and need to canceled and closed
pub txs: Vec<(String, String)>,
// contains all cloids that the worker generate to track the hypercore orders
// each tuple is a name -> cloid mapping, e.g.:
// ("spot_sell", "173494584")
// we expect 4 cloids for a full loan request and payback cycle
pub cloids: Vec<(String, String)>,
/// Actual position size that was opened for this loan (filled or partially filled)
/// This should be set after the hedge order completes in request_loan
pub position_size: String,
pub state: LoanState,
}

/// Parameters for creating a new loan record
pub struct NewLoanRecord {
pub collateral_ticker: String,
pub collateral_size: String,
pub usdc_sold: String,
pub usdc_loaned: String,
pub usdc_for_perp: String,
}

pub struct LoanRecordStorage {
Expand Down Expand Up @@ -95,6 +125,46 @@ impl LoanRecordStorage {
}
}

impl LoanRecordStorage {
pub fn create(&self, key: &Key, params: NewLoanRecord) -> Result<(), String> {
if self.contains_key(key) {
return Err(format!(
"Key already exists, account_id: {:?}, nonce: {}",
key.account_id, key.nonce
));
}

let record = LoanRecord {
collateral_ticker: params.collateral_ticker,
collateral_size: params.collateral_size,
usdc_sold: params.usdc_sold,
usdc_loaned: params.usdc_loaned,
usdc_for_perp: params.usdc_for_perp,
txs: Vec::new(),
cloids: Vec::new(),
position_size: "0".to_string(),
state: LoanState::SpotSold,
};

self.insert(key, record)
.map_err(|e| format!("Failed to insert record: {:?}", e))
}

/// Update loan record using a closure
pub fn update<F>(&self, key: &Key, updater: F) -> Result<(), String>
where
F: FnOnce(&mut LoanRecord),
{
let mut record = self
.get(key)
.map_err(|e| format!("Failed to get record: {:?}", e))?
.ok_or_else(|| "Record not found".to_string())?;
updater(&mut record);
self.insert(key, record)
.map_err(|e| format!("Failed to insert record: {:?}", e))
}
}

impl Storage<Key, LoanRecord> for LoanRecordStorage {
fn db(&self) -> Arc<crate::StorageDB> {
self.db.clone()
Expand Down
25 changes: 25 additions & 0 deletions tee-worker/omni-executor/hyperliquid/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ pub const PERP_CLOSE_PRICE_RATIO: f64 = 0.98;
/// Ratio for spot buy orders (2% above market to ensure fill)
pub const SPOT_BUY_PRICE_RATIO: f64 = 1.02;

/// Minimum notional value for perp and spot orders ($10 minimum)
pub const MIN_NOTIONAL_VALUE: f64 = 10.0;

// Unit conversion multipliers
/// Multiplier for converting decimal prices to HyperLiquid price units (8 decimals)
pub const PRICE_UNIT_MULTIPLIER: f64 = 100_000_000.0;
Expand Down Expand Up @@ -72,6 +75,28 @@ pub fn get_bid_ask_prices(mark_price: f64, mid_price: f64) -> (f64, f64) {
}
}

/// Validate that the notional value (price * size) meets the minimum requirement
///
/// # Arguments
/// * `price` - The price of the asset
/// * `size` - The size/quantity (should be the clamped size)
/// * `operation` - Description of the operation for error messages (e.g., "spot sell", "perp open")
///
/// # Returns
/// * `Ok(notional)` - The calculated notional value if it meets the minimum
/// * `Err(String)` - Error message if notional is below minimum
pub fn validate_notional_value(price: f64, size: f64, operation: &str) -> Result<f64, String> {
let notional = price * size;
if notional < MIN_NOTIONAL_VALUE {
Err(format!(
"{} notional value ({:.2}) is below minimum required ({:.2})",
operation, notional, MIN_NOTIONAL_VALUE
))
} else {
Ok(notional)
}
}

pub fn get_core_writer_address() -> Address {
CORE_WRITER_ADDRESS.parse().unwrap()
}
Expand Down
6 changes: 3 additions & 3 deletions tee-worker/omni-executor/hyperliquid/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ pub fn get_perp_asset_id(ticker: &str, meta: &MetaResponse) -> Result<u32, Strin
/// Calculate the USDC received from a spot sell fill
/// For a sell order: USDC received = price * size - fee (if fee is in USDC)
/// Returns the net USDC amount received
pub fn calculate_usdc_received_from_spot_sell(fill: &Fill) -> Result<f64, String> {
pub fn usdc_from_spot_fill(fill: &Fill) -> Result<f64, String> {
// Parse price and size
let price = fill
.px
Expand Down Expand Up @@ -155,7 +155,7 @@ pub fn calculate_usdc_received_from_spot_sell(fill: &Fill) -> Result<f64, String
};

debug!(
"Spot sell fill: price={}, size={}, fee={} {}, gross_usdc={:.2}, net_usdc={:.2}",
"Spot sell fill: price={}, size={}, fee={} {}, gross_usdc={}, net_usdc={}",
price, size, fee, fill.fee_token, gross_usdc, net_usdc
);

Expand Down Expand Up @@ -272,7 +272,7 @@ pub fn clamp_price(price: f64, sz_decimals: u8, is_spot: bool) -> String {
///
/// Size rules:
/// - Sizes are truncated (rounded down) to the sz_decimals of the asset
/// - Example: if sz_decimals = 3, then 1.001 is valid but 1.0001 truncates to 1.001
/// - Example: if sz_decimals = 3, then 1.001 is valid but 1.0001 truncates to 1.000
///
/// # Arguments
/// * `size` - The size to clamp
Expand Down
Loading