Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 61 additions & 7 deletions src/commands/clob.rs
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,11 @@ fn parse_date(s: &str) -> Result<NaiveDate> {
.map_err(|_| anyhow::anyhow!("Invalid date: expected YYYY-MM-DD format"))
}

/// detect the CLOB "not enough balance / allowance" error so we can refresh and retry
fn is_balance_error(e: &polymarket_client_sdk::error::Error) -> bool {
e.to_string().contains("not enough balance")
}

#[allow(clippy::too_many_lines)]
pub async fn execute(
args: ClobArgs,
Expand Down Expand Up @@ -676,18 +681,44 @@ pub async fn execute(
let size_dec =
Decimal::from_str(&size).map_err(|_| anyhow::anyhow!("Invalid size: {size}"))?;

let sdk_side = Side::from(side);
let sdk_order_type = OrderType::from(order_type);
let token_id = parse_token_id(&token)?;

let order = client
.limit_order()
.token_id(parse_token_id(&token)?)
.side(Side::from(side))
.token_id(token_id)
.side(sdk_side)
.price(price_dec)
.size(size_dec)
.order_type(OrderType::from(order_type))
.order_type(sdk_order_type.clone())
.post_only(post_only)
.build()
.await?;
let order = client.sign(&signer, order).await?;
let result = client.post_order(order).await?;
let result = match client.post_order(order).await {
Ok(r) => r,
Err(e) if is_balance_error(&e) => {
eprintln!("Balance cache may be stale, refreshing and retrying...");
let req = BalanceAllowanceRequest::builder()
.asset_type(AssetType::Collateral)
.build();
let _ = client.update_balance_allowance(req).await;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Retry always refreshes collateral, ignoring sell-side conditional tokens

Medium Severity

The retry logic always refreshes AssetType::Collateral regardless of order side. For sell orders, a "not enough balance" error indicates stale conditional token (shares) balance, not collateral. Refreshing collateral won't fix that, so the retry will always fail for sell-side stale-balance errors. Additionally, conditional balance updates require a token_id (as seen in the manual UpdateBalance command), which the retry code doesn't provide.

Additional Locations (1)
Fix in Cursor Fix in Web

let order = client
.limit_order()
.token_id(token_id)
.side(sdk_side)
.price(price_dec)
.size(size_dec)
.order_type(sdk_order_type)
.post_only(post_only)
.build()
.await?;
let order = client.sign(&signer, order).await?;
client.post_order(order).await?
}
Err(e) => return Err(e.into()),
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicated retry logic across order command paths

Low Severity

The balance-refresh-and-retry logic is duplicated nearly identically between the CreateOrder and MarketOrder match arms — including the is_balance_error guard, the eprintln message, the BalanceAllowanceRequest construction, and the re-sign-and-post pattern. Fixing bugs in this retry logic (like the asset-type issue) requires updating both copies in lockstep, increasing the risk of inconsistency.

Additional Locations (1)
Fix in Cursor Fix in Web

print_post_order_result(&result, output)?;
}

Expand Down Expand Up @@ -757,16 +788,39 @@ pub async fn execute(
Amount::usdc(amount_dec)?
};

let sdk_order_type = OrderType::from(order_type);
let token_id = parse_token_id(&token)?;

let order = client
.market_order()
.token_id(parse_token_id(&token)?)
.token_id(token_id)
.side(sdk_side)
.amount(parsed_amount)
.order_type(OrderType::from(order_type))
.order_type(sdk_order_type.clone())
.build()
.await?;
let order = client.sign(&signer, order).await?;
let result = client.post_order(order).await?;
let result = match client.post_order(order).await {
Ok(r) => r,
Err(e) if is_balance_error(&e) => {
eprintln!("Balance cache may be stale, refreshing and retrying...");
let req = BalanceAllowanceRequest::builder()
.asset_type(AssetType::Collateral)
.build();
let _ = client.update_balance_allowance(req).await;
let order = client
.market_order()
.token_id(token_id)
.side(sdk_side)
.amount(parsed_amount)
.order_type(sdk_order_type)
.build()
.await?;
let order = client.sign(&signer, order).await?;
client.post_order(order).await?
}
Err(e) => return Err(e.into()),
};
print_post_order_result(&result, output)?;
}

Expand Down