Motivation
We have close_taker(pos_id, current_perp_delta, urgency) and close_maker(pos_id, current_liquidity, urgency) (PR #57) — caller-supplied: the caller must already know the position type and the exact amount to reverse. That works when the caller tracks positions locally (e.g. Legion), but it has two weaknesses:
- Drift risk. If the caller's locally-tracked size/liquidity has drifted from on-chain, the reversal misses zero and the position doesn't fully close (or reverts). Most acute on shutdown/retreat, where we want a guaranteed full close.
- Caller must branch taker-vs-maker and supply the right amount/method.
Proposal
Add a self-contained close(pos_id, urgency) that reads on-chain state and reverses the actual amount:
pub async fn close(&self, pos_id: U256, urgency: Urgency) -> Result<AdjustTakerResult> {
let maker = perp.makerDetails(pos_id).call().await?;
if maker.liquidity != 0 {
// maker → reverse actual liquidity (deltas 0.0)
self.close_maker(pos_id, maker.liquidity, urgency).await.map(into_unified)
} else {
// taker → read actual delta, unpack, reverse exactly
let pos = self.get_position(pos_id).await?;
let size = scale_from_6dec(unpack_balance_delta(pos.delta).0);
self.close_taker(pos_id, size, urgency).await
}
}
This is a thin layer over the existing close_taker/close_maker primitives — it adds the on-chain reads to (a) detect type and (b) reverse the real delta/liquidity, so the close is type-agnostic and drift-proof.
Notes / building blocks (all already present)
makerDetails(uint256) -> Maker is already in the binding (contracts.rs); positions(uint256) -> Position too.
unpack_balance_delta exists but is private to feeds/events.rs — relocate it to convert.rs as pub (next to scale_from_6dec, its natural home) and have feeds import it.
- Return
AdjustTakerResult, not a new CloseResult — the fields (tx_hash, realized perp_delta/usd_delta) are identical, so a separate type would be redundant. Maker closes report 0.0 deltas.
- Safe to read-then-close: a position's
delta only changes via a swap, so it's stable between the read and the close tx.
- Cost: ~1–2 extra RPC reads per close. Keep
close_taker/close_maker as the zero-read primitives for callers that already have the data.
- Add a fork test that opens then closes by id (taker and maker).
Why deferred
For the Legion migration we're branching on is_taker + locally-tracked (realized) size/liquidity, which is accurate enough for the normal path. This SDK method is the more correct primitive, especially for retreat/shutdown.
Motivation
We have
close_taker(pos_id, current_perp_delta, urgency)andclose_maker(pos_id, current_liquidity, urgency)(PR #57) — caller-supplied: the caller must already know the position type and the exact amount to reverse. That works when the caller tracks positions locally (e.g. Legion), but it has two weaknesses:Proposal
Add a self-contained
close(pos_id, urgency)that reads on-chain state and reverses the actual amount:This is a thin layer over the existing
close_taker/close_makerprimitives — it adds the on-chain reads to (a) detect type and (b) reverse the real delta/liquidity, so the close is type-agnostic and drift-proof.Notes / building blocks (all already present)
makerDetails(uint256) -> Makeris already in the binding (contracts.rs);positions(uint256) -> Positiontoo.unpack_balance_deltaexists but is private tofeeds/events.rs— relocate it toconvert.rsaspub(next toscale_from_6dec, its natural home) and havefeedsimport it.AdjustTakerResult, not a newCloseResult— the fields (tx_hash, realizedperp_delta/usd_delta) are identical, so a separate type would be redundant. Maker closes report0.0deltas.deltaonly changes via a swap, so it's stable between the read and the close tx.close_taker/close_makeras the zero-read primitives for callers that already have the data.Why deferred
For the Legion migration we're branching on
is_taker+ locally-tracked (realized) size/liquidity, which is accurate enough for the normal path. This SDK method is the more correct primitive, especially for retreat/shutdown.