# Tristero
[![PyPI version](https://badge.fury.io/py/tristero.svg)](https://badge.fury.io/py/tristero)
[![Python Support](https://img.shields.io/pypi/pyversions/tristero.svg)](https://pypi.org/project/tristero/)

This repository is home to Tristero's trading library.


### How it works

Tristero supports two primary swap mechanisms:

#### Permit2 Swaps (EVM-to-EVM)
- **Quote & Approve** - Request a quote and approve tokens via Permit2 (gasless approval)
- **Sign & Submit** - Sign an EIP-712 order and submit for execution
- **Monitor** - Track swap progress via WebSocket updates

#### Feather Swaps (UTXO-based)
- **Quote & Deposit** - Request a quote to receive a deposit address
- **Manual Transfer** - Send funds to the provided deposit address
- **Monitor** - Track swap completion via WebSocket updates

This library provides both high-level convenience functions and lower-level components for precise control.

### Installation
```
pip install tristero
```

### Quick Start

Execute a cross-chain swap in just a few lines:

```py
import os
import asyncio

from eth_account import Account

from tristero import ChainID, TokenSpec, execute_permit2_swap, make_async_w3


async def main() -> None:
    private_key = os.getenv("TEST_ACCOUNT_PRIVKEY")
    if not private_key:
        raise RuntimeError("Set TEST_ACCOUNT_PRIVKEY")

    account = Account.from_key(private_key)

    arbitrum_rpc = os.getenv("ARBITRUM_RPC_URL", "https://arbitrum-one-rpc.publicnode.com")
    w3 = make_async_w3(arbitrum_rpc)

    # Example: USDC on Arbitrum -> USDT on Base
    result = await execute_permit2_swap(
        w3=w3,
        account=account,
        src_t=TokenSpec(chain_id=ChainID(42161), token_address="0xaf88d065e77c8cC2239327C5EDb3A432268e5831"),  # USDC (Arbitrum)
        dst_t=TokenSpec(chain_id=ChainID(8453), token_address="0xfde4C96c8593536E31F229EA8f37b2ADa2699bb2"),  # USDT (Base)
        raw_amount=1_000_000,  # 1 USDC (6 decimals)
        timeout=300,
    )
    print(result)


asyncio.run(main())
```

### Usage Examples

#### Quote (read-only)

```py
import asyncio
import os

from eth_account import Account

from tristero import ChainID, get_quote


async def main() -> None:
    private_key = os.getenv("TEST_ACCOUNT_PRIVKEY")
    if not private_key:
        raise RuntimeError("Set TEST_ACCOUNT_PRIVKEY")

    wallet = Account.from_key(private_key).address

    quote = await get_quote(
        from_wallet=wallet,
        to_wallet=wallet,
        from_chain_id=str(ChainID(42161).value),
        from_address="0xaf88d065e77c8cC2239327C5EDb3A432268e5831",  # USDC (Arbitrum)
        to_chain_id=str(ChainID(42161).value),
        to_address="0x82aF49447D8a07e3bd95BD0d56f35241523fBab1",  # WETH (Arbitrum)
        amount=1_000_000,
    )
    print(quote)


asyncio.run(main())
```

#### Margin: Direct Open

```py
import asyncio
import os

from eth_account import Account
from tristero import open_margin_position


async def main() -> None:
    private_key = os.getenv("TEST_ACCOUNT_PRIVKEY", "")
    if not private_key:
        raise RuntimeError("Set TEST_ACCOUNT_PRIVKEY")

    wallet = Account.from_key(private_key).address

    chain_id = "42161"
    quote_currency = "0xaf88d065e77c8cC2239327C5EDb3A432268e5831"  # USDC (Arbitrum)
    base_currency = "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"  # WETH (Arbitrum)
    leverage = 2
    collateral = "1000000"  # 1 USDC (6 decimals)

    result = await open_margin_position(
        private_key=private_key,
        chain_id=chain_id,
        wallet_address=wallet,
        quote_currency=quote_currency,
        base_currency=base_currency,
        leverage_ratio=leverage,
        collateral_amount=collateral,
        wait_for_result=True,
        timeout=120,
    )
    print(result)


asyncio.run(main())
```

#### Margin: List Positions / Close Position

```py
import asyncio
import os

from eth_account import Account
from tristero import close_margin_position, list_margin_positions


async def main() -> None:
    private_key = os.getenv("TEST_ACCOUNT_PRIVKEY", "")
    if not private_key:
        raise RuntimeError("Set TEST_ACCOUNT_PRIVKEY")

    wallet = Account.from_key(private_key).address
    chain_id = "42161"

    positions = await list_margin_positions(wallet)
    open_pos = next((p for p in positions if p.status == "open"), None)
    if not open_pos:
        raise RuntimeError("no open positions")

    result = await close_margin_position(
        private_key=private_key,
        chain_id=chain_id,
        position_id=open_pos.taker_token_id,
        escrow_contract=open_pos.escrow_address,
        authorized=open_pos.filler_address,
        cash_settle=False,
        fraction_bps=10_000,
        deadline_seconds=3600,
        wait_for_result=True,
        timeout=120,
    )
    print(result)


asyncio.run(main())
```


#### Feather: Start (get deposit address)

Feather swaps are deposit-based: you start an order to receive a `deposit_address`, send funds to it manually, then optionally wait for completion.

Submit only:

```py
import asyncio

from tristero import ChainID, TokenSpec, start_feather_swap


async def main() -> None:
    # Example: ETH (native) -> XMR (native)
    src_t = TokenSpec(chain_id=ChainID.ethereum, token_address="native")
    dst_t = TokenSpec(chain_id=ChainID.monero, token_address="native")

    # Replace with your own destination address on the destination chain.
    dst_addr = "YOUR_XMR_ADDRESS"

    swap = await start_feather_swap(
        src_t=src_t,
        dst_t=dst_t,
        dst_addr=dst_addr,
        raw_amount=100_000_000_000_000_000,  # 0.1 ETH in wei
    )

    order_id = (
        (swap.data or {}).get("id")
        or (swap.data or {}).get("order_id")
        or (swap.data or {}).get("orderId")
        or ""
    )

    print("order_id:", order_id)
    print("deposit_address:", swap.deposit_address)


asyncio.run(main())
```

Submit + wait (WebSocket):

```py
import asyncio

from tristero import ChainID, OrderType, TokenSpec, start_feather_swap, wait_for_completion


async def main() -> None:
    src_t = TokenSpec(chain_id=ChainID.ethereum, token_address="native")
    dst_t = TokenSpec(chain_id=ChainID.monero, token_address="native")
    dst_addr = "YOUR_XMR_ADDRESS"

    swap = await start_feather_swap(
        src_t=src_t,
        dst_t=dst_t,
        dst_addr=dst_addr,
        raw_amount=100_000_000_000_000_000,
    )

    order_id = (
        (swap.data or {}).get("id")
        or (swap.data or {}).get("order_id")
        or (swap.data or {}).get("orderId")
        or ""
    )
    if not order_id:
        raise RuntimeError(f"Feather swap response missing order id: {swap.data}")

    print("deposit_address:", swap.deposit_address)
    print("Waiting for completion...")
    completion = await wait_for_completion(order_id, order_type=OrderType.FEATHER)
    print(completion)


asyncio.run(main())
```
