Skip to content

Add self-contained close(pos_id) that reads on-chain state #60

Description

@Praz314159

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:

  1. 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.
  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions