Skip to content

Commit 6cf5aeb

Browse files
authored
Add more loan record status (#3797)
* Init loanState impl * try to add close_position_test * rename * update * update * improve nonce handling * renames * error out * refactor the fields in loanRecord * fix state * add notional validation * fix rounding issue * try to fix the transfer amount * more fix
1 parent 5f96dce commit 6cf5aeb

File tree

10 files changed

+2207
-1373
lines changed

10 files changed

+2207
-1373
lines changed

tee-worker/omni-executor/executor-storage/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ pub use pumpx_account_profile::PumpxProfileStorage;
1717
mod wildmeta_timestamp;
1818
pub use wildmeta_timestamp::WildmetaTimestampStorage;
1919
pub mod loan_record;
20-
pub use loan_record::{LoanRecord, LoanRecordStorage};
20+
pub use loan_record::{LoanRecord, LoanRecordStorage, LoanState};
2121

2222
pub use asset_lock::AssetLockStorage;
2323
pub use asset_lock::Key as AssetLockStorageKey;

tee-worker/omni-executor/executor-storage/src/loan_record.rs

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,47 @@ pub struct Key {
1313
pub nonce: u64,
1414
}
1515

16+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode, Serialize, Deserialize)]
17+
pub enum LoanState {
18+
SpotSold,
19+
ToPerpMoved,
20+
HedgeOpened,
21+
HedgeClosed,
22+
ToSpotMoved,
23+
SpotBought,
24+
}
25+
1626
#[derive(Debug, Clone, Encode, Decode, Serialize, Deserialize)]
1727
pub struct LoanRecord {
1828
pub collateral_ticker: String,
1929
pub collateral_size: String,
2030
pub usdc_sold: String,
2131
pub usdc_loaned: String,
22-
pub spot_sell_cloid: String,
23-
pub hedge_open_cloid: String,
32+
pub usdc_for_perp: String,
33+
// contains all tx hashes that the worker submits to interact with corewriter
34+
// each tuple is a name -> hash mapping, e.g.:
35+
// ("spot_sell", "0x1234...")
36+
// we expect 6 hashes for a full loan request and payback cycle in normal cases,
37+
// and up to 7 hashes if the hedge were partially filled and need to canceled and closed
38+
pub txs: Vec<(String, String)>,
39+
// contains all cloids that the worker generate to track the hypercore orders
40+
// each tuple is a name -> cloid mapping, e.g.:
41+
// ("spot_sell", "173494584")
42+
// we expect 4 cloids for a full loan request and payback cycle
43+
pub cloids: Vec<(String, String)>,
2444
/// Actual position size that was opened for this loan (filled or partially filled)
2545
/// This should be set after the hedge order completes in request_loan
2646
pub position_size: String,
47+
pub state: LoanState,
48+
}
49+
50+
/// Parameters for creating a new loan record
51+
pub struct NewLoanRecord {
52+
pub collateral_ticker: String,
53+
pub collateral_size: String,
54+
pub usdc_sold: String,
55+
pub usdc_loaned: String,
56+
pub usdc_for_perp: String,
2757
}
2858

2959
pub struct LoanRecordStorage {
@@ -95,6 +125,46 @@ impl LoanRecordStorage {
95125
}
96126
}
97127

128+
impl LoanRecordStorage {
129+
pub fn create(&self, key: &Key, params: NewLoanRecord) -> Result<(), String> {
130+
if self.contains_key(key) {
131+
return Err(format!(
132+
"Key already exists, account_id: {:?}, nonce: {}",
133+
key.account_id, key.nonce
134+
));
135+
}
136+
137+
let record = LoanRecord {
138+
collateral_ticker: params.collateral_ticker,
139+
collateral_size: params.collateral_size,
140+
usdc_sold: params.usdc_sold,
141+
usdc_loaned: params.usdc_loaned,
142+
usdc_for_perp: params.usdc_for_perp,
143+
txs: Vec::new(),
144+
cloids: Vec::new(),
145+
position_size: "0".to_string(),
146+
state: LoanState::SpotSold,
147+
};
148+
149+
self.insert(key, record)
150+
.map_err(|e| format!("Failed to insert record: {:?}", e))
151+
}
152+
153+
/// Update loan record using a closure
154+
pub fn update<F>(&self, key: &Key, updater: F) -> Result<(), String>
155+
where
156+
F: FnOnce(&mut LoanRecord),
157+
{
158+
let mut record = self
159+
.get(key)
160+
.map_err(|e| format!("Failed to get record: {:?}", e))?
161+
.ok_or_else(|| "Record not found".to_string())?;
162+
updater(&mut record);
163+
self.insert(key, record)
164+
.map_err(|e| format!("Failed to insert record: {:?}", e))
165+
}
166+
}
167+
98168
impl Storage<Key, LoanRecord> for LoanRecordStorage {
99169
fn db(&self) -> Arc<crate::StorageDB> {
100170
self.db.clone()

tee-worker/omni-executor/hyperliquid/src/lib.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ pub const PERP_CLOSE_PRICE_RATIO: f64 = 0.98;
2424
/// Ratio for spot buy orders (2% above market to ensure fill)
2525
pub const SPOT_BUY_PRICE_RATIO: f64 = 1.02;
2626

27+
/// Minimum notional value for perp and spot orders ($10 minimum)
28+
pub const MIN_NOTIONAL_VALUE: f64 = 10.0;
29+
2730
// Unit conversion multipliers
2831
/// Multiplier for converting decimal prices to HyperLiquid price units (8 decimals)
2932
pub const PRICE_UNIT_MULTIPLIER: f64 = 100_000_000.0;
@@ -72,6 +75,28 @@ pub fn get_bid_ask_prices(mark_price: f64, mid_price: f64) -> (f64, f64) {
7275
}
7376
}
7477

78+
/// Validate that the notional value (price * size) meets the minimum requirement
79+
///
80+
/// # Arguments
81+
/// * `price` - The price of the asset
82+
/// * `size` - The size/quantity (should be the clamped size)
83+
/// * `operation` - Description of the operation for error messages (e.g., "spot sell", "perp open")
84+
///
85+
/// # Returns
86+
/// * `Ok(notional)` - The calculated notional value if it meets the minimum
87+
/// * `Err(String)` - Error message if notional is below minimum
88+
pub fn validate_notional_value(price: f64, size: f64, operation: &str) -> Result<f64, String> {
89+
let notional = price * size;
90+
if notional < MIN_NOTIONAL_VALUE {
91+
Err(format!(
92+
"{} notional value ({:.2}) is below minimum required ({:.2})",
93+
operation, notional, MIN_NOTIONAL_VALUE
94+
))
95+
} else {
96+
Ok(notional)
97+
}
98+
}
99+
75100
pub fn get_core_writer_address() -> Address {
76101
CORE_WRITER_ADDRESS.parse().unwrap()
77102
}

tee-worker/omni-executor/hyperliquid/src/utils.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ pub fn get_perp_asset_id(ticker: &str, meta: &MetaResponse) -> Result<u32, Strin
121121
/// Calculate the USDC received from a spot sell fill
122122
/// For a sell order: USDC received = price * size - fee (if fee is in USDC)
123123
/// Returns the net USDC amount received
124-
pub fn calculate_usdc_received_from_spot_sell(fill: &Fill) -> Result<f64, String> {
124+
pub fn usdc_from_spot_fill(fill: &Fill) -> Result<f64, String> {
125125
// Parse price and size
126126
let price = fill
127127
.px
@@ -155,7 +155,7 @@ pub fn calculate_usdc_received_from_spot_sell(fill: &Fill) -> Result<f64, String
155155
};
156156

157157
debug!(
158-
"Spot sell fill: price={}, size={}, fee={} {}, gross_usdc={:.2}, net_usdc={:.2}",
158+
"Spot sell fill: price={}, size={}, fee={} {}, gross_usdc={}, net_usdc={}",
159159
price, size, fee, fill.fee_token, gross_usdc, net_usdc
160160
);
161161

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

0 commit comments

Comments
 (0)