tristero 0.1.7__py3-none-any.whl → 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
tristero/permit2.py CHANGED
@@ -1,50 +1,60 @@
1
+ from dataclasses import dataclass
1
2
  import logging
2
- from typing import Any, List, Optional, TypeVar, cast
3
+ import random
4
+ import time
5
+ from typing import Any, Dict, List, Optional, TypeVar
3
6
  from eth_account import Account
4
7
  from eth_account.datastructures import SignedMessage, SignedTransaction
5
- from eth_account.signers.base import BaseAccount
6
8
  from eth_account.signers.local import LocalAccount
7
- from eth_account.types import TransactionDictType
8
- from pydantic import BaseModel, ConfigDict, Field, field_validator
9
+ from eth_account.messages import encode_typed_data
10
+ from pydantic import BaseModel, ConfigDict, Field
9
11
  from pydantic.alias_generators import to_camel
10
12
  from web3 import AsyncBaseProvider, AsyncWeb3
11
- from eth_typing import Address, ChecksumAddress
12
- import time
13
- import math
14
- import random
15
- import web3
16
13
  from web3.contract import AsyncContract
17
14
  from functools import cache, lru_cache
18
15
  import json
19
- from pathlib import Path
20
16
  from importlib import resources as impresources
21
17
 
22
18
  from web3 import Web3
23
19
  from web3.eth import AsyncEth
20
+ from eth_utils import to_checksum_address
24
21
 
25
- from .api import (
26
- _WRAPPED_GAS_ADDRESSES,
27
- get_quote,
28
- _PERMIT2_CONTRACT_ADDRESSES,
29
- ChainID,
22
+ from tristero.api import get_quote
23
+
24
+ from .data import (
25
+ get_permit2_addr,
26
+ get_wrapped_gas_addr,
27
+ )
28
+ from .eip712 import (
29
+ EIP712OrderParameters,
30
+ EIP712TokenPermissions,
31
+ EIP712SignedOrder,
32
+ EIP712PermitWitnessTransferFromOrder,
33
+ EIP712CloseWithSwap,
34
+ get_escrow_domain,
35
+ get_close_with_swap_types,
30
36
  )
31
37
 
32
38
  logger = logging.getLogger(__name__)
33
39
 
34
40
  P = TypeVar("P", bound=AsyncBaseProvider)
35
41
 
42
+ DEFAULT_PERMIT2_ADDRESS = "0x000000000022D473030F116dDEE9F6B43aC78BA3"
43
+
36
44
  PERMIT2_ABI_FILE = impresources.files("tristero.files") / "permit2_abi.json"
37
45
  ERC20_ABI_FILE = impresources.files("tristero.files") / "erc20_abi.json"
46
+
38
47
  PERMIT2_ABI = json.loads(PERMIT2_ABI_FILE.read_text())
39
48
  ERC20_ABI = json.loads(ERC20_ABI_FILE.read_text())
40
49
 
41
50
 
42
51
  @lru_cache(maxsize=None)
43
- def get_permit2(eth: AsyncEth, permit2_address: str):
52
+ def get_permit2_contract(eth: AsyncEth, permit2_address: str):
44
53
  return eth.contract(
45
54
  address=Web3.to_checksum_address(permit2_address), abi=PERMIT2_ABI
46
55
  )
47
56
 
57
+
48
58
  @cache
49
59
  def get_erc20_contract(w3: AsyncWeb3[P], token_address: str) -> AsyncContract:
50
60
  """Get ERC20 contract instance."""
@@ -152,6 +162,11 @@ async def get_permit2_unordered_nonce(c: AsyncContract, wallet_address: str):
152
162
  return wordPos
153
163
 
154
164
 
165
+ def get_random_nonce() -> int:
166
+ """Generate a random nonce for EIP-712 signing."""
167
+ return random.randint(2**128, 2**256 - 1)
168
+
169
+
155
170
  async def prepare_data_for_signature(
156
171
  eth: AsyncEth,
157
172
  sell_data: ChainData,
@@ -173,7 +188,6 @@ async def prepare_data_for_signature(
173
188
  Returns:
174
189
  SignatureData with domain, types, primaryType, and message for signing
175
190
  """
176
- # Validate required fields
177
191
  if not quote.order_data.parameters.min_quantity:
178
192
  raise ValueError("Min quantity is required in the order_data.parameters")
179
193
 
@@ -183,18 +197,16 @@ async def prepare_data_for_signature(
183
197
  if not quote.order_data.deadline:
184
198
  raise ValueError("Deadline is required in the order_data")
185
199
 
186
- from_chain = ChainID(str(sell_data.chain_id))
200
+ from_chain = str(sell_data.chain_id)
187
201
 
188
202
  deadline = quote.order_data.deadline
189
203
 
190
- # Handle native token address conversion
191
204
  token_address = (
192
- _WRAPPED_GAS_ADDRESSES[from_chain]
205
+ get_wrapped_gas_addr(from_chain)
193
206
  if sell_data.token.address == "native"
194
207
  else sell_data.token.address
195
208
  )
196
209
 
197
- # Build witness object
198
210
  witness = SignedOrder(
199
211
  sender=wallet_address,
200
212
  parameters=OrderParameters(
@@ -212,24 +224,21 @@ async def prepare_data_for_signature(
212
224
  custom_data=quote.order_data.custom_data or [],
213
225
  )
214
226
 
215
- # Get Permit2 address
216
- permit2_address = _PERMIT2_CONTRACT_ADDRESSES.get(from_chain)
227
+ permit2_address = get_permit2_addr(from_chain)
217
228
  if not permit2_address:
218
229
  raise ValueError("Permit2 not deployed on this chain.")
219
230
 
220
231
  spender = quote.order_data.router_address
221
232
  nonce = await get_permit2_unordered_nonce(
222
- get_permit2(eth, permit2_address), wallet_address
233
+ get_permit2_contract(eth, permit2_address), wallet_address
223
234
  )
224
235
 
225
- # EIP-712 domain
226
236
  domain = EIP712Domain(
227
237
  name="Permit2",
228
238
  chain_id=sell_data.chain_id,
229
239
  verifying_contract=permit2_address,
230
240
  )
231
241
 
232
- # EIP-712 types
233
242
  types = {
234
243
  "TokenPermissions": [
235
244
  {"name": "token", "type": "address"},
@@ -261,7 +270,6 @@ async def prepare_data_for_signature(
261
270
  ],
262
271
  }
263
272
 
264
- # Build message
265
273
  message = PermitMessage(
266
274
  permitted=TokenPermissions(
267
275
  token=token_address,
@@ -309,48 +317,223 @@ async def sign_permit2(
309
317
  wallet_address,
310
318
  Quote(order_data=order_data),
311
319
  )
312
- # print(
313
- # "Signing the following full message:",
314
- # json.dumps(to_sign.model_dump(mode="json", by_alias=True)),
315
- # )
316
320
  signature = account.sign_typed_data(
317
321
  full_message=to_sign.model_dump(mode="json", by_alias=True)
318
322
  )
319
323
  return (to_sign, signature)
320
324
 
321
325
 
326
+ def sign_margin_order(
327
+ quote: Dict[str, Any],
328
+ private_key: str,
329
+ permit2_address: str = DEFAULT_PERMIT2_ADDRESS,
330
+ nonce: Optional[int] = None,
331
+ ) -> Dict[str, Any]:
332
+ """
333
+ Sign a margin order using EIP-712 typed data.
334
+
335
+ Args:
336
+ quote: Quote response from get_margin_quote
337
+ private_key: Private key for signing
338
+ permit2_address: Permit2 contract address
339
+ nonce: Optional nonce (random if not provided)
340
+
341
+ Returns:
342
+ Signed order payload ready for submission
343
+ """
344
+ order_data = quote.get("order_data")
345
+ if not isinstance(order_data, dict):
346
+ raise ValueError("quote missing order_data")
347
+
348
+ chain_id_str = quote.get("chain_id")
349
+ if isinstance(chain_id_str, dict) and "value" in chain_id_str:
350
+ chain_id = int(chain_id_str["value"])
351
+ else:
352
+ chain_id = int(chain_id_str)
353
+
354
+ domain_data = {
355
+ "name": "Permit2",
356
+ "chainId": chain_id,
357
+ "verifyingContract": to_checksum_address(permit2_address),
358
+ }
359
+
360
+ parameters = EIP712OrderParameters(
361
+ srcAsset=order_data["parameters"]["src_asset"],
362
+ dstAsset=order_data["parameters"]["dst_asset"],
363
+ srcQuantity=int(order_data["parameters"]["src_quantity"]),
364
+ dstQuantity=int(order_data["parameters"]["dst_quantity"]),
365
+ minQuantity=int(order_data["parameters"]["min_quantity"]),
366
+ darkSalt=int(order_data["parameters"]["dark_salt"]),
367
+ )
368
+
369
+ signed_order = EIP712SignedOrder(
370
+ sender=order_data["sender_wallet_address"],
371
+ parameters=parameters,
372
+ deadline=int(order_data["deadline"]),
373
+ target=order_data["target_wallet_address"],
374
+ filler=order_data["filler_wallet_address"],
375
+ orderType=str(order_data["order_type"]),
376
+ customData=[
377
+ bytes.fromhex(cd[2:]) if cd.startswith("0x") else bytes.fromhex(cd)
378
+ for cd in order_data.get("custom_data", [])
379
+ ],
380
+ )
381
+
382
+ token_permissions = EIP712TokenPermissions(
383
+ token=order_data["parameters"]["src_asset"],
384
+ amount=int(order_data["parameters"]["src_quantity"]),
385
+ )
386
+
387
+ if nonce is None:
388
+ nonce = get_random_nonce()
389
+
390
+ permit_witness = EIP712PermitWitnessTransferFromOrder(
391
+ permitted=token_permissions,
392
+ spender=order_data["router_address"],
393
+ nonce=nonce,
394
+ deadline=int(order_data["deadline"]),
395
+ witness=signed_order,
396
+ )
397
+
398
+ typed_data = {
399
+ "types": {
400
+ "EIP712Domain": [
401
+ {"name": "name", "type": "string"},
402
+ {"name": "chainId", "type": "uint256"},
403
+ {"name": "verifyingContract", "type": "address"},
404
+ ],
405
+ "PermitWitnessTransferFrom": permit_witness.TYPE_STRUCT,
406
+ "TokenPermissions": token_permissions.TYPE_STRUCT,
407
+ "SignedOrder": signed_order.TYPE_STRUCT,
408
+ "OrderParameters": parameters.TYPE_STRUCT,
409
+ },
410
+ "primaryType": "PermitWitnessTransferFrom",
411
+ "domain": domain_data,
412
+ "message": permit_witness.eip_signable_struct,
413
+ }
414
+
415
+ acct = Account.from_key(private_key)
416
+ signable = encode_typed_data(full_message=typed_data)
417
+ sig = acct.sign_message(signable).signature.hex()
418
+ if not sig.startswith("0x"):
419
+ sig = "0x" + sig
420
+
421
+ return {
422
+ "signature": sig,
423
+ "domain": domain_data,
424
+ "message": permit_witness.eip_signable_struct,
425
+ }
426
+
427
+
428
+ def sign_close_position(
429
+ chain_id: int,
430
+ position_id: int,
431
+ private_key: str,
432
+ escrow_contract: str,
433
+ authorized: str,
434
+ cash_settle: bool = False,
435
+ fraction_bps: int = 10_000,
436
+ deadline_seconds: int = 3600,
437
+ nonce: Optional[int] = None,
438
+ ) -> Dict[str, Any]:
439
+ """
440
+ Sign a close margin position request using EIP-712.
441
+
442
+ Args:
443
+ chain_id: Chain ID
444
+ position_id: Position ID (NFT token ID)
445
+ private_key: Private key for signing
446
+ escrow_contract: Escrow contract address
447
+ authorized: Authorized filler address
448
+ cash_settle: Whether to cash settle
449
+ fraction_bps: Fraction to close in basis points (10000 = 100%)
450
+ deadline_seconds: Deadline in seconds from now
451
+ nonce: Optional nonce (random if not provided)
452
+
453
+ Returns:
454
+ Signed close position payload
455
+ """
456
+ if not escrow_contract:
457
+ raise ValueError("escrow_contract is required")
458
+ if not authorized:
459
+ raise ValueError("authorized is required")
460
+
461
+ if nonce is None:
462
+ nonce = get_random_nonce()
463
+
464
+ deadline = int(time.time()) + int(deadline_seconds)
465
+
466
+ close_msg = EIP712CloseWithSwap(
467
+ positionId=int(position_id),
468
+ cashSettle=bool(cash_settle),
469
+ fractionBps=int(fraction_bps),
470
+ authorized=str(authorized),
471
+ nonce=int(nonce),
472
+ deadline=int(deadline),
473
+ )
474
+
475
+ domain_data = get_escrow_domain(chain_id, escrow_contract)
476
+ types = get_close_with_swap_types()
477
+
478
+ typed_data = {
479
+ "types": types,
480
+ "primaryType": "CloseWithSwap",
481
+ "domain": domain_data,
482
+ "message": {
483
+ "positionId": close_msg.positionId,
484
+ "cashSettle": close_msg.cashSettle,
485
+ "fractionBps": close_msg.fractionBps,
486
+ "authorized": close_msg.authorized,
487
+ "nonce": close_msg.nonce,
488
+ "deadline": close_msg.deadline,
489
+ },
490
+ }
491
+
492
+ acct = Account.from_key(private_key)
493
+ signable = encode_typed_data(full_message=typed_data)
494
+ sig = acct.sign_message(signable).signature.hex()
495
+ if not sig.startswith("0x"):
496
+ sig = "0x" + sig
497
+
498
+ return {
499
+ "signature": sig,
500
+ "domain": domain_data,
501
+ "message": typed_data["message"],
502
+ "chainId": str(chain_id),
503
+ }
504
+
505
+
322
506
  async def approve_permit2(
323
507
  w3: AsyncWeb3[P],
324
508
  account: LocalAccount,
325
- chain: ChainID,
509
+ chain: str,
326
510
  token_address: str,
327
511
  required_quantity: int,
328
512
  maxGas: int = 100000,
329
513
  ):
330
514
  wallet_address = account.address
515
+ permit2_address = get_permit2_addr(chain)
331
516
 
332
517
  erc20 = get_erc20_contract(w3, token_address)
333
- permit2_contract = _PERMIT2_CONTRACT_ADDRESSES.get(chain)
334
518
  current_allowance = await erc20.functions.allowance(
335
- wallet_address, permit2_contract
519
+ wallet_address, permit2_address
336
520
  ).call()
337
521
  if current_allowance < required_quantity:
338
522
  logger.info(
339
523
  f"Approving {token_address}: allowance={current_allowance}, required={required_quantity}"
340
524
  )
341
- approve_fn = erc20.functions.approve(permit2_contract, 2**256 - 1)
525
+ approve_fn = erc20.functions.approve(permit2_address, 2**256 - 1)
342
526
  tx = await approve_fn.build_transaction(
343
527
  {
344
528
  "from": wallet_address,
345
529
  "nonce": await w3.eth.get_transaction_count(
346
530
  w3.to_checksum_address(wallet_address)
347
531
  ),
348
- "gas": maxGas, # Adjust as needed
532
+ "gas": maxGas,
349
533
  "gasPrice": await w3.eth.gas_price,
350
534
  }
351
535
  )
352
536
 
353
- # Sign and send transaction
354
537
  tx_data = tx if isinstance(tx, dict) else tx.__dict__
355
538
  signed_tx: SignedTransaction = account.sign_transaction(tx_data)
356
539
  tx_hash = await w3.eth.send_raw_transaction(signed_tx.raw_transaction)
@@ -359,27 +542,46 @@ async def approve_permit2(
359
542
  return tx_hash.hex()
360
543
 
361
544
 
362
- async def create_order(
545
+ @dataclass
546
+ class Permit2Order:
547
+ msg: SignatureData
548
+ sig: SignedMessage
549
+
550
+
551
+ async def create_permit2_order(
363
552
  w3: AsyncWeb3[P],
364
553
  account: LocalAccount,
365
- src_chain: ChainID,
554
+ src_chain: str,
366
555
  src_token: str,
367
- dst_chain: ChainID,
556
+ dst_chain: str,
368
557
  dst_token: str,
369
558
  raw_amount: int,
370
- to_address: Optional[str] = None,
371
- ):
372
- if not to_address:
373
- to_address = account.address
559
+ dst_address: str | None = None,
560
+ ) -> Permit2Order:
561
+ if not dst_address:
562
+ dst_address = account.address
374
563
  q = await get_quote(
375
564
  account.address,
376
- to_address,
565
+ dst_address,
377
566
  src_chain,
378
567
  src_token,
379
568
  dst_chain,
380
569
  dst_token,
381
570
  raw_amount,
382
571
  )
383
- await approve_permit2(w3, account, src_chain, src_token, raw_amount)
384
- # print("Quote: ", json.dumps(q))
385
- return await sign_permit2(w3.eth, account, account.address, raw_amount, q)
572
+
573
+ order_type = q.get("orderType") or q.get("order_type")
574
+ if not order_type:
575
+ order_data = q.get("order_data")
576
+ if isinstance(order_data, dict):
577
+ order_type = order_data.get("order_type") or order_data.get("orderType")
578
+
579
+ if not order_type:
580
+ raise ValueError(f"quote missing order type (keys={sorted(q.keys())})")
581
+
582
+ if str(order_type).strip().upper() == "FEATHER":
583
+ raise Exception("Feather routes unsupported for automatic trades")
584
+
585
+ _ = await approve_permit2(w3, account, src_chain, src_token, raw_amount)
586
+ msg, sig = await sign_permit2(w3.eth, account, account.address, raw_amount, q)
587
+ return Permit2Order(msg, sig)
@@ -0,0 +1,198 @@
1
+ Metadata-Version: 2.4
2
+ Name: tristero
3
+ Version: 0.3.0
4
+ Summary: Library for trading on Tristero
5
+ Author-email: pty1 <pty11@proton.me>
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+ License-File: LICENSE
9
+ Requires-Dist: certifi>=2023.7.22
10
+ Requires-Dist: eth-account>=0.8.0
11
+ Requires-Dist: glom>=25.12.0
12
+ Requires-Dist: httpx>=0.23.0
13
+ Requires-Dist: pydantic>=2.0.0
14
+ Requires-Dist: tenacity>=8.0.0
15
+ Requires-Dist: web3>=6.0.0
16
+ Requires-Dist: websockets>=10.0
17
+ Dynamic: license-file
18
+
19
+ # Tristero
20
+ [![PyPI version](https://badge.fury.io/py/tristero.svg)](https://badge.fury.io/py/tristero)
21
+ [![Python Support](https://img.shields.io/pypi/pyversions/tristero.svg)](https://pypi.org/project/tristero/)
22
+
23
+ This repository is home to Tristero's trading library.
24
+
25
+ ### Installation
26
+ ```
27
+ pip install tristero
28
+ ```
29
+
30
+ ### Quick Start
31
+
32
+ Execute a cross-chain swap in just a few lines:
33
+
34
+ ```py
35
+ import os
36
+ import asyncio
37
+
38
+ from eth_account import Account
39
+
40
+ from tristero import ChainID, TokenSpec, execute_permit2_swap, make_async_w3
41
+
42
+
43
+ async def main() -> None:
44
+ private_key = os.getenv("TEST_ACCOUNT_PRIVKEY")
45
+ if not private_key:
46
+ raise RuntimeError("Set TEST_ACCOUNT_PRIVKEY")
47
+
48
+ account = Account.from_key(private_key)
49
+
50
+ arbitrum_rpc = os.getenv("ARBITRUM_RPC_URL", "https://arbitrum-one-rpc.publicnode.com")
51
+ w3 = make_async_w3(arbitrum_rpc)
52
+
53
+ # Example: USDC on Arbitrum -> USDT on Base
54
+ result = await execute_permit2_swap(
55
+ w3=w3,
56
+ account=account,
57
+ src_t=TokenSpec(chain_id=ChainID(42161), token_address="0xaf88d065e77c8cC2239327C5EDb3A432268e5831"), # USDC (Arbitrum)
58
+ dst_t=TokenSpec(chain_id=ChainID(8453), token_address="0xfde4C96c8593536E31F229EA8f37b2ADa2699bb2"), # USDT (Base)
59
+ raw_amount=1_000_000, # 1 USDC (6 decimals)
60
+ timeout=300,
61
+ )
62
+ print(result)
63
+
64
+
65
+ asyncio.run(main())
66
+ ```
67
+
68
+ ### Usage Examples
69
+
70
+ #### Quote (read-only)
71
+
72
+ ```py
73
+ import asyncio
74
+ import os
75
+
76
+ from eth_account import Account
77
+
78
+ from tristero import ChainID, get_quote
79
+
80
+
81
+ async def main() -> None:
82
+ private_key = os.getenv("TEST_ACCOUNT_PRIVKEY")
83
+ if not private_key:
84
+ raise RuntimeError("Set TEST_ACCOUNT_PRIVKEY")
85
+
86
+ wallet = Account.from_key(private_key).address
87
+
88
+ quote = await get_quote(
89
+ from_wallet=wallet,
90
+ to_wallet=wallet,
91
+ from_chain_id=str(ChainID(42161).value),
92
+ from_address="0xaf88d065e77c8cC2239327C5EDb3A432268e5831", # USDC (Arbitrum)
93
+ to_chain_id=str(ChainID(42161).value),
94
+ to_address="0x82aF49447D8a07e3bd95BD0d56f35241523fBab1", # WETH (Arbitrum)
95
+ amount=1_000_000,
96
+ )
97
+ print(quote)
98
+
99
+
100
+ asyncio.run(main())
101
+ ```
102
+
103
+ #### Margin: Direct Open
104
+
105
+ ```py
106
+ import asyncio
107
+ import os
108
+
109
+ from eth_account import Account
110
+ from tristero import open_margin_position
111
+
112
+
113
+ async def main() -> None:
114
+ private_key = os.getenv("TEST_ACCOUNT_PRIVKEY", "")
115
+ if not private_key:
116
+ raise RuntimeError("Set TEST_ACCOUNT_PRIVKEY")
117
+
118
+ wallet = Account.from_key(private_key).address
119
+
120
+ chain_id = "42161"
121
+ quote_currency = "0xaf88d065e77c8cC2239327C5EDb3A432268e5831" # USDC (Arbitrum)
122
+ base_currency = "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1" # WETH (Arbitrum)
123
+ leverage = 2
124
+ collateral = "1000000" # 1 USDC (6 decimals)
125
+
126
+ result = await open_margin_position(
127
+ private_key=private_key,
128
+ chain_id=chain_id,
129
+ wallet_address=wallet,
130
+ quote_currency=quote_currency,
131
+ base_currency=base_currency,
132
+ leverage_ratio=leverage,
133
+ collateral_amount=collateral,
134
+ wait_for_result=True,
135
+ timeout=120,
136
+ )
137
+ print(result)
138
+
139
+
140
+ asyncio.run(main())
141
+ ```
142
+
143
+ #### Margin: List Positions / Close Position
144
+
145
+ ```py
146
+ import asyncio
147
+ import os
148
+
149
+ from eth_account import Account
150
+ from tristero import close_margin_position, list_margin_positions
151
+
152
+
153
+ async def main() -> None:
154
+ private_key = os.getenv("TEST_ACCOUNT_PRIVKEY", "")
155
+ if not private_key:
156
+ raise RuntimeError("Set TEST_ACCOUNT_PRIVKEY")
157
+
158
+ wallet = Account.from_key(private_key).address
159
+ chain_id = "42161"
160
+
161
+ positions = await list_margin_positions(wallet)
162
+ open_pos = next((p for p in positions if p.status == "open"), None)
163
+ if not open_pos:
164
+ raise RuntimeError("no open positions")
165
+
166
+ result = await close_margin_position(
167
+ private_key=private_key,
168
+ chain_id=chain_id,
169
+ position_id=open_pos.taker_token_id,
170
+ escrow_contract=open_pos.escrow_address,
171
+ authorized=open_pos.filler_address,
172
+ cash_settle=False,
173
+ fraction_bps=10_000,
174
+ deadline_seconds=3600,
175
+ wait_for_result=True,
176
+ timeout=120,
177
+ )
178
+ print(result)
179
+
180
+
181
+ asyncio.run(main())
182
+ ```
183
+
184
+ ### How it works
185
+
186
+ Tristero supports two primary swap mechanisms:
187
+
188
+ #### Permit2 Swaps (EVM-to-EVM)
189
+ - **Quote & Approve** - Request a quote and approve tokens via Permit2 (gasless approval)
190
+ - **Sign & Submit** - Sign an EIP-712 order and submit for execution
191
+ - **Monitor** - Track swap progress via WebSocket updates
192
+
193
+ #### Feather Swaps (UTXO-based)
194
+ - **Quote & Deposit** - Request a quote to receive a deposit address
195
+ - **Manual Transfer** - Send funds to the provided deposit address
196
+ - **Monitor** - Track swap completion via WebSocket updates
197
+
198
+ This library provides both high-level convenience functions and lower-level components for precise control.
@@ -0,0 +1,22 @@
1
+ tristero/__init__.py,sha256=lKstpKnnzd2PnvI79SwWnOwOMkDla9Nt_HbyrhnV6s0,1854
2
+ tristero/api.py,sha256=DLNYsFj81TId3EfCwlXRfxUws-7lLTkMO1znF9D3abU,7900
3
+ tristero/client.py,sha256=WMPthsJ5Rb6tOkBiHe4533r3Kqcdk_6bvlOPOJEB9HA,19125
4
+ tristero/config.py,sha256=v7ohAv-KbnFpWQbyXYD78QABFTW1vm1P4GkIV2-5uC0,1002
5
+ tristero/data.py,sha256=8Y1865c5tLVHDCwDRjyMlp7xR4KXAej_4yK8WcZCTcU,1512
6
+ tristero/permit2.py,sha256=lYawwcechaxQuEe1QUMd4tR0FBnuNri1Gs0tXdTK_eY,17604
7
+ tristero/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ tristero/eip712/__init__.py,sha256=xTsAPEiwYidBaya3py_wMVFH4mOd4Md7hH5eTGFwvSI,1256
9
+ tristero/eip712/eip712_auto.py,sha256=h-AwA8BsIAvZoe3TB8fzlvNKX17ZVG_NwbroqZ_LWl4,7293
10
+ tristero/eip712/eip712_struct.py,sha256=aWBOdsGarQnxONA5hr-DVdlrUD-yvtu_yi3G9DrU9cQ,2286
11
+ tristero/eip712/escrow_utils.py,sha256=EOXpzxJ_29A0yvEloI5sxYe33atMhkoIAV4pkAia92s,869
12
+ tristero/eip712/nested_types.py,sha256=rsB-pZ6K8EeulU__6z0pY_6LHWkjh7N6776ekl9Jcnw,1972
13
+ tristero/eip712/simple_types.py,sha256=9rHqz7XbbdwdOMCKbQr8HGYwPgqC3S6sQbm2I7DbQVM,11865
14
+ tristero/files/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
+ tristero/files/chains.json,sha256=04jFywe_V0wrS0tcjdCUHMxIutaLJ_k5ABbbmkyZ9jY,256150
16
+ tristero/files/erc20_abi.json,sha256=jvsJ6aCwhMcmo3Yy1ajt5lPl_nTRg7tv-tGj87xzTOg,12800
17
+ tristero/files/permit2_abi.json,sha256=NV0AUUA9kqFPk56njvRRzUyjBhrBncKIMd3PrSH0LCc,17817
18
+ tristero-0.3.0.dist-info/licenses/LICENSE,sha256=b-9ikwk9ICk964mtUbqVkDgSf3S6d52HTQxyGHmNo9M,10925
19
+ tristero-0.3.0.dist-info/METADATA,sha256=Z4XmDOqGpCmt21zhOto5VB1FL8Lil8HGDf2GbJA-qQc,5318
20
+ tristero-0.3.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
21
+ tristero-0.3.0.dist-info/top_level.txt,sha256=xO745nTllCKw6Fdu-a2ZYv5-ZVOKl2vt9ccRUjCVXfI,9
22
+ tristero-0.3.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.10.2)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+