# dex-exec

Minimal async Python library for Hyperliquid perp execution and Uniswap V3 / Sushiswap / Pancakeswap swaps on Arbitrum One.

Install: `pip install dex-exec`
Import: `from dexec import HLClient, AMMClient, create_agent, approve_agent`

---

## HLClient

    async with HLClient(private_key, address, testnet=False) as hl: ...

- private_key: agent key (use create_agent() to generate). address: master wallet address (always required).

### Methods

    await hl.place_order(symbol, side, size, price, order_type='gtc', reduce_only=False, cloid=None) -> OrderResult
    # side: 'buy'|'sell'. order_type: 'gtc'|'ioc'|'alo'. price required (no market orders).
    # IOC tip: aggressive limit + order_type='ioc' for market-like fill (e.g. mark*1.01 for buys).
    # Min notional: $10. Prices must be within 80% of mark price.

    await hl.cancel_order(symbol, cloid) -> bool
    await hl.cancel_all(symbol=None) -> int
    await hl.get_balance() -> AccountBalance
    await hl.get_positions() -> list[Position]
    await hl.get_open_orders(symbol=None) -> list[Order]
    await hl.get_fills(start_ms=None) -> list[Fill]
    hl.fills_stream()         # async context manager -> async iterator[Fill]
    hl.order_updates_stream() # async context manager -> async iterator[OrderUpdate]
    await hl.deposit_usdc(amount_usd, arb_private_key) -> str   # tx_hash
    await hl.withdraw_usdc(amount_usd) -> bool

### Agent setup (one-time)

    creds = await create_agent(master_private_key, agent_name='bot', testnet=False)
    # -> AgentCredentials(address, private_key). Use creds.private_key for HLClient.

---

## AMMClient

    async with AMMClient(private_key=None, rpc_url=None) as amm: ...

- private_key: omit for read-only. rpc_url: defaults to public Arbitrum endpoint.
- Most Arbitrum pairs trade WETH not native ETH — wrap first with wrap_eth().

### Methods

    await amm.wrap_eth(amount_eth) -> str                          # tx_hash
    await amm.get_quote(token_in, token_out, amount_in, venue, fee_tier) -> SwapQuote
    await amm.swap(token_in, token_out, amount_in, venue, fee_tier, min_amount_out, deadline=60) -> SwapResult
    await amm.approve_token(token, spender, amount=None) -> str    # spender: venue name or 0x addr

- venue: 'uni_v3' | 'sushi' | 'pancake'
- fee_tier in ppm: 100=0.01%, 500=0.05%, 3000=0.3%, 10000=1%
- Always quote first: min_amount_out = quote.amount_out * (1 - slippage)
- swap() auto-approves on first use per token/venue pair.
- Tokens: WETH, USDC, USDC.e, USDT, WBTC, ARB, LINK, DAI, GMX, or raw 0x address.

### Canonical swap pattern

    async with AMMClient(private_key='0x...') as amm:
        quote  = await amm.get_quote('WETH', 'USDC', 0.1, venue='uni_v3', fee_tier=500)
        result = await amm.swap('WETH', 'USDC', 0.1, venue='uni_v3', fee_tier=500,
                                 min_amount_out=quote.amount_out * 0.995)

---

## Common gotchas

### Hyperliquid: agent key vs master key
HLClient takes the AGENT key, not the master key. address= is always the MASTER wallet address.
One-time setup generates an agent key from the master key; after that, master key can go offline.

    # One-time setup
    creds = await create_agent(master_private_key='0xMASTER', agent_name='bot')
    # Daily use — master key not needed
    async with HLClient(private_key=creds.private_key, address='0xMASTER_ADDR') as hl: ...

### Hyperliquid: no market orders
price= is required. For market-like fills use IOC with an aggressive limit:

    # Buy at market: limit 1% above current price, fill-or-cancel
    await hl.place_order('ETH', 'buy', size=0.01, price=mark * 1.01, order_type='ioc')

### AMM: WETH not ETH
Most Arbitrum pools trade WETH. Wrap native ETH before swapping:

    await amm.wrap_eth(0.1)   # wrap 0.1 ETH -> WETH first

### AMM: always quote before swapping
min_amount_out is required and enforced on-chain. Call get_quote() first:

    quote  = await amm.get_quote('WETH', 'USDC', 0.1, venue='uni_v3', fee_tier=500)
    result = await amm.swap('WETH', 'USDC', 0.1, venue='uni_v3', fee_tier=500,
                             min_amount_out=quote.amount_out * 0.995)

### WebSocket streams: async context manager required

    async with hl.fills_stream() as fills:   # NOT: async for fill in hl.fills_stream()
        async for fill in fills: ...

---

## Return types (all dataclasses)

    AccountBalance:   account_value, margin_used, free_collateral, withdrawable  # floats, USDC
    Position:         symbol, side('long'|'short'), size, entry_price, unrealized_pnl, margin_used, leverage, liquidation_price(float|None)
    Order:            cloid, oid(int|None), symbol, side, size, price, tif('Gtc'|'Ioc'|'Alo'), reduce_only
    Fill:             cloid(str|None), symbol, side, size, price, fee, fee_token, filled_at(epoch ms)
    OrderResult:      success(bool), cloid, status('resting'|'filled'|'error'), oid(int|None), error(str|None)
    OrderUpdate:      cloid, status, symbol, filled_size(float|None)
    AgentCredentials: address(0x), private_key(0x)
    SwapQuote:        token_in, token_out, amount_in, amount_out, price_impact_pct, venue, fee_tier(ppm)
    SwapResult:       success(bool), tx_hash(str|None), amount_in, amount_out, venue, error(str|None)

---

## File layout

    src/dexec/
      __init__.py      public exports
      types.py         all dataclasses
      hl/client.py     HLClient
      hl/agent.py      create_agent, approve_agent
      hl/_api.py       raw HL REST calls
      hl/_signing.py   EIP-712 signing
      hl/_ws.py        WebSocket streams
      hl/_assets.py    asset ID registry (lazy-loaded)
      hl/deposit.py    on-chain USDC deposit to HL bridge
      amm/client.py    AMMClient
      amm/_contracts.py router/quoter addresses per venue
      amm/_quoter.py   on-chain QuoterV2 calls
      amm/_router.py   swap tx builder
      amm/_tokens.py   token address + decimals registry
