Library for trading on Tristero
Project description
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
Environment Configuration
Tristero supports three environments: PRODUCTION (default), STAGING, and LOCAL.
Set the environment globally at startup:
from tristero import set_config
set_config("STAGING") # all subsequent calls use the staging API
Or override per call:
quote = await get_swap_quote(..., env="LOCAL")
Every user-facing function in the SDK accepts an optional env keyword argument.
Quick Start
Spot Swap (quote, sign, submit)
import asyncio
import json
import os
from eth_account import Account
from tristero import get_swap_quote, sign_and_submit, make_async_w3
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
w3 = make_async_w3(os.getenv("ARB_RPC_URL", "https://arbitrum-one-rpc.publicnode.com"))
# 1. Get a quote (USDC -> WETH on Arbitrum)
quote = await get_swap_quote(
wallet=wallet,
src_chain=42161,
src_token="0xaf88d065e77c8cC2239327C5EDb3A432268e5831", # USDC
dst_chain=42161,
dst_token="0x82aF49447D8a07e3bd95BD0d56f35241523fBab1", # WETH
amount=1_000_000, # 1 USDC (6 decimals)
)
print(json.dumps(quote, indent=2))
# 2. Sign and submit (w3 required for Permit2 approval on source chain)
result = await sign_and_submit(quote, private_key, w3=w3, wait=True, timeout=300)
print(result)
asyncio.run(main())
Margin Position (quote, sign, submit)
import asyncio
import json
import os
from eth_account import Account
from tristero import get_swap_quote, sign_and_submit
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
# 1. Get a margin quote (2x leveraged USDC/WETH on Arbitrum)
quote = await get_swap_quote(
wallet=wallet,
src_chain=42161,
src_token="0xaf88d065e77c8cC2239327C5EDb3A432268e5831", # USDC (collateral)
dst_chain=42161,
dst_token="0x82aF49447D8a07e3bd95BD0d56f35241523fBab1", # WETH (base)
amount=1_000_000, # 1 USDC collateral (6 decimals)
leverage=2,
)
print(json.dumps(quote, indent=2))
# 2. Sign and submit (no w3 needed for margin)
result = await sign_and_submit(quote, private_key, wait=True, timeout=120)
print(result)
asyncio.run(main())
More Examples
Spot Swap (direct execution)
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("ARB_RPC_URL", "https://arbitrum-one-rpc.publicnode.com")
w3 = make_async_w3(arbitrum_rpc)
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())
Margin: Direct Open
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
result = await open_margin_position(
private_key=private_key,
chain_id="42161",
wallet_address=wallet,
collateral_token="0xaf88d065e77c8cC2239327C5EDb3A432268e5831", # USDC
base_token="0x82aF49447D8a07e3bd95BD0d56f35241523fBab1", # WETH
leverage_ratio=2,
collateral_amount="1000000", # 1 USDC (6 decimals)
wait_for_result=True,
timeout=120,
)
print(result)
asyncio.run(main())
Any token can be used as collateral. For example, open a 2x long WETH position using USDT0:
result = await open_margin_position(
private_key=private_key,
chain_id="42161",
wallet_address=wallet,
collateral_token="0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9", # USDT0
base_token="0x82aF49447D8a07e3bd95BD0d56f35241523fBab1", # WETH
leverage_ratio=2,
collateral_amount="1000000", # 1 USDT0 (6 decimals)
)
Or use WETH itself as collateral:
result = await open_margin_position(
private_key=private_key,
chain_id="42161",
wallet_address=wallet,
collateral_token="0x82aF49447D8a07e3bd95BD0d56f35241523fBab1", # WETH
base_token="0x82aF49447D8a07e3bd95BD0d56f35241523fBab1", # WETH
leverage_ratio=2,
collateral_amount="500000000000000", # 0.0005 WETH (18 decimals)
)
The quote response includes loan_token, collateral_token, and base_token
objects with full metadata, plus interest_rate_bps for the borrowing cost.
Margin: List Positions / Close Position
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
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="42161",
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())
WebSocket Quote Streaming
subscribe_quotes opens a persistent WebSocket connection and delivers live quotes via an async callback (~500 ms updates).
If the callback is still running when a newer quote arrives, intermediate updates are dropped and only the latest quote is delivered once the callback finishes (latest-only pattern). This makes it safe to do slow work (e.g. sign + submit) inside the callback without worrying about duplicate executions.
Simple: print every quote
import asyncio
from tristero import subscribe_quotes
async def main() -> None:
async def on_quote(quote):
print(f"dst_qty={quote['dst_token_quantity']} order_id={quote['order_id'][:16]}...")
async def on_error(exc):
print(f"Error: {exc}")
async with await subscribe_quotes(
wallet="0xYOUR_WALLET",
src_chain=42161,
src_token="0xaf88d065e77c8cC2239327C5EDb3A432268e5831", # USDC
dst_chain=1,
dst_token="0xdAC17F958D2ee523a2206206994597C13D831ec7", # USDT
amount=1_000_000,
on_quote=on_quote,
on_error=on_error,
) as sub:
await asyncio.sleep(10) # stream for 10 seconds
asyncio.run(main())
Advanced: sign and submit the first quote, then stop
import asyncio
import os
from eth_account import Account
from tristero import subscribe_quotes, sign_and_submit, make_async_w3
async def main() -> None:
private_key = os.getenv("TEST_ACCOUNT_PRIVKEY", "")
wallet = Account.from_key(private_key).address
w3 = make_async_w3(os.getenv("ARB_RPC_URL", "https://arbitrum-one-rpc.publicnode.com"))
done = asyncio.Event()
async def on_quote(quote):
if done.is_set():
return
quote["_type"] = "swap"
result = await sign_and_submit(quote, private_key, w3=w3, wait=False)
print(result)
done.set()
sub = await subscribe_quotes(
wallet=wallet,
src_chain=42161,
src_token="0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
dst_chain=42161,
dst_token="0x82aF49447D8a07e3bd95BD0d56f35241523fBab1",
amount=1_000_000,
on_quote=on_quote,
)
await done.wait()
await sub.close()
asyncio.run(main())
Limit order: wait for a target price, then submit
import asyncio
import os
from eth_account import Account
from tristero import subscribe_quotes, sign_and_submit, make_async_w3
async def main() -> None:
private_key = os.getenv("TEST_ACCOUNT_PRIVKEY", "")
wallet = Account.from_key(private_key).address
w3 = make_async_w3(os.getenv("ARB_RPC_URL", "https://arbitrum-one-rpc.publicnode.com"))
done = asyncio.Event()
baseline: list[int] = []
improvement_bps = 10 # submit when price is 10 bps better than first quote
async def on_quote(quote):
if done.is_set():
return
dst_qty = int(quote.get("dst_token_quantity", 0))
if not baseline:
baseline.append(dst_qty)
print(f"Baseline: {dst_qty}")
return
threshold = baseline[0] * (10_000 + improvement_bps) / 10_000
if dst_qty < threshold:
print(f"dst_qty={dst_qty} (waiting for >= {threshold:.0f})")
return
# Threshold met — sign and submit THIS specific quote
print(f"Target reached: {dst_qty} >= {threshold:.0f}, submitting!")
quote["_type"] = "swap"
result = await sign_and_submit(quote, private_key, w3=w3, wait=False)
print(result)
done.set()
sub = await subscribe_quotes(
wallet=wallet,
src_chain=42161,
src_token="0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
dst_chain=42161,
dst_token="0x82aF49447D8a07e3bd95BD0d56f35241523fBab1",
amount=1_000_000,
on_quote=on_quote,
)
await done.wait()
await sub.close()
asyncio.run(main())
The callback receives the exact quote that triggered the condition. Because of the latest-only pattern, even if signing takes longer than 500 ms, that specific quote is what gets signed — newer arrivals simply queue up and don't cause duplicates.
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:
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):
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())
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file tristero-0.4.2.tar.gz.
File metadata
- Download URL: tristero-0.4.2.tar.gz
- Upload date:
- Size: 54.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.10.9 {"installer":{"name":"uv","version":"0.10.9","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
bc917e7330ecfe6eaabbc1bf07d07c2f5c516e2be111b05cac274dfb77048209
|
|
| MD5 |
068ae84cb66431178578bbd2f1fb0abd
|
|
| BLAKE2b-256 |
94128e0c0a48435e9496e5b5b99c11fec5b630d48c5ea877187766a5f4036e51
|
File details
Details for the file tristero-0.4.2-py3-none-any.whl.
File metadata
- Download URL: tristero-0.4.2-py3-none-any.whl
- Upload date:
- Size: 56.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.10.9 {"installer":{"name":"uv","version":"0.10.9","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
01da98d6a158e1c804aec2cfd99c67dfa315d56c8b13ee9b2b391052df51951a
|
|
| MD5 |
c9a19af12e30331120caa16145c0a691
|
|
| BLAKE2b-256 |
736b26cb776e7d1078785b54386afea686e5e81312a224450b4c5b734a957862
|