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/__init__.py +70 -3
- tristero/api.py +230 -102
- tristero/client.py +568 -47
- tristero/config.py +16 -0
- tristero/data.py +56 -0
- tristero/eip712/__init__.py +48 -0
- tristero/eip712/eip712_auto.py +212 -0
- tristero/eip712/eip712_struct.py +73 -0
- tristero/eip712/escrow_utils.py +32 -0
- tristero/eip712/nested_types.py +85 -0
- tristero/eip712/simple_types.py +392 -0
- tristero/files/chains.json +6557 -0
- tristero/permit2.py +250 -48
- tristero-0.3.0.dist-info/METADATA +198 -0
- tristero-0.3.0.dist-info/RECORD +22 -0
- tristero-0.3.0.dist-info/WHEEL +5 -0
- tristero-0.3.0.dist-info/licenses/LICENSE +201 -0
- tristero-0.3.0.dist-info/top_level.txt +1 -0
- tristero-0.1.7.dist-info/METADATA +0 -157
- tristero-0.1.7.dist-info/RECORD +0 -12
- tristero-0.1.7.dist-info/WHEEL +0 -4
tristero/permit2.py
CHANGED
|
@@ -1,50 +1,60 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
1
2
|
import logging
|
|
2
|
-
|
|
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.
|
|
8
|
-
from pydantic import BaseModel, ConfigDict, Field
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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,
|
|
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(
|
|
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,
|
|
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
|
-
|
|
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:
|
|
554
|
+
src_chain: str,
|
|
366
555
|
src_token: str,
|
|
367
|
-
dst_chain:
|
|
556
|
+
dst_chain: str,
|
|
368
557
|
dst_token: str,
|
|
369
558
|
raw_amount: int,
|
|
370
|
-
|
|
371
|
-
):
|
|
372
|
-
if not
|
|
373
|
-
|
|
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
|
-
|
|
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
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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
|
+
[](https://badge.fury.io/py/tristero)
|
|
21
|
+
[](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,,
|