tristero 0.2.1__py3-none-any.whl → 0.3.2__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 +63 -2
- tristero/api.py +190 -8
- tristero/client.py +498 -22
- tristero/config.py +16 -0
- tristero/data.py +28 -7
- 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/permit2.py +225 -26
- tristero-0.3.2.dist-info/METADATA +279 -0
- tristero-0.3.2.dist-info/RECORD +22 -0
- tristero-0.3.2.dist-info/WHEEL +5 -0
- tristero-0.3.2.dist-info/licenses/LICENSE +201 -0
- tristero-0.3.2.dist-info/top_level.txt +1 -0
- tristero-0.2.1.dist-info/METADATA +0 -284
- tristero-0.2.1.dist-info/RECORD +0 -14
- tristero-0.2.1.dist-info/WHEEL +0 -4
tristero/permit2.py
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
from dataclasses import dataclass
|
|
2
2
|
import logging
|
|
3
|
-
|
|
3
|
+
import random
|
|
4
|
+
import time
|
|
5
|
+
from typing import Any, Dict, List, Optional, TypeVar
|
|
4
6
|
from eth_account import Account
|
|
5
7
|
from eth_account.datastructures import SignedMessage, SignedTransaction
|
|
6
|
-
from eth_account.signers.base import BaseAccount
|
|
7
8
|
from eth_account.signers.local import LocalAccount
|
|
8
|
-
from eth_account.
|
|
9
|
-
from pydantic import BaseModel, ConfigDict, Field
|
|
9
|
+
from eth_account.messages import encode_typed_data
|
|
10
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
10
11
|
from pydantic.alias_generators import to_camel
|
|
11
12
|
from web3 import AsyncBaseProvider, AsyncWeb3
|
|
12
|
-
import random
|
|
13
13
|
from web3.contract import AsyncContract
|
|
14
14
|
from functools import cache, lru_cache
|
|
15
15
|
import json
|
|
16
|
-
from pathlib import Path
|
|
17
16
|
from importlib import resources as impresources
|
|
18
17
|
|
|
19
18
|
from web3 import Web3
|
|
20
19
|
from web3.eth import AsyncEth
|
|
20
|
+
from eth_utils import to_checksum_address
|
|
21
21
|
|
|
22
22
|
from tristero.api import get_quote
|
|
23
23
|
|
|
@@ -25,23 +25,36 @@ from .data import (
|
|
|
25
25
|
get_permit2_addr,
|
|
26
26
|
get_wrapped_gas_addr,
|
|
27
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,
|
|
36
|
+
)
|
|
28
37
|
|
|
29
38
|
logger = logging.getLogger(__name__)
|
|
30
39
|
|
|
31
40
|
P = TypeVar("P", bound=AsyncBaseProvider)
|
|
32
41
|
|
|
42
|
+
DEFAULT_PERMIT2_ADDRESS = "0x000000000022D473030F116dDEE9F6B43aC78BA3"
|
|
43
|
+
|
|
33
44
|
PERMIT2_ABI_FILE = impresources.files("tristero.files") / "permit2_abi.json"
|
|
34
45
|
ERC20_ABI_FILE = impresources.files("tristero.files") / "erc20_abi.json"
|
|
35
46
|
|
|
36
47
|
PERMIT2_ABI = json.loads(PERMIT2_ABI_FILE.read_text())
|
|
37
48
|
ERC20_ABI = json.loads(ERC20_ABI_FILE.read_text())
|
|
38
49
|
|
|
50
|
+
|
|
39
51
|
@lru_cache(maxsize=None)
|
|
40
|
-
def
|
|
52
|
+
def get_permit2_contract(eth: AsyncEth, permit2_address: str):
|
|
41
53
|
return eth.contract(
|
|
42
54
|
address=Web3.to_checksum_address(permit2_address), abi=PERMIT2_ABI
|
|
43
55
|
)
|
|
44
56
|
|
|
57
|
+
|
|
45
58
|
@cache
|
|
46
59
|
def get_erc20_contract(w3: AsyncWeb3[P], token_address: str) -> AsyncContract:
|
|
47
60
|
"""Get ERC20 contract instance."""
|
|
@@ -149,6 +162,11 @@ async def get_permit2_unordered_nonce(c: AsyncContract, wallet_address: str):
|
|
|
149
162
|
return wordPos
|
|
150
163
|
|
|
151
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
|
+
|
|
152
170
|
async def prepare_data_for_signature(
|
|
153
171
|
eth: AsyncEth,
|
|
154
172
|
sell_data: ChainData,
|
|
@@ -170,7 +188,6 @@ async def prepare_data_for_signature(
|
|
|
170
188
|
Returns:
|
|
171
189
|
SignatureData with domain, types, primaryType, and message for signing
|
|
172
190
|
"""
|
|
173
|
-
# Validate required fields
|
|
174
191
|
if not quote.order_data.parameters.min_quantity:
|
|
175
192
|
raise ValueError("Min quantity is required in the order_data.parameters")
|
|
176
193
|
|
|
@@ -184,14 +201,12 @@ async def prepare_data_for_signature(
|
|
|
184
201
|
|
|
185
202
|
deadline = quote.order_data.deadline
|
|
186
203
|
|
|
187
|
-
# Handle native token address conversion
|
|
188
204
|
token_address = (
|
|
189
205
|
get_wrapped_gas_addr(from_chain)
|
|
190
206
|
if sell_data.token.address == "native"
|
|
191
207
|
else sell_data.token.address
|
|
192
208
|
)
|
|
193
209
|
|
|
194
|
-
# Build witness object
|
|
195
210
|
witness = SignedOrder(
|
|
196
211
|
sender=wallet_address,
|
|
197
212
|
parameters=OrderParameters(
|
|
@@ -209,24 +224,21 @@ async def prepare_data_for_signature(
|
|
|
209
224
|
custom_data=quote.order_data.custom_data or [],
|
|
210
225
|
)
|
|
211
226
|
|
|
212
|
-
# Get Permit2 address
|
|
213
227
|
permit2_address = get_permit2_addr(from_chain)
|
|
214
228
|
if not permit2_address:
|
|
215
229
|
raise ValueError("Permit2 not deployed on this chain.")
|
|
216
230
|
|
|
217
231
|
spender = quote.order_data.router_address
|
|
218
232
|
nonce = await get_permit2_unordered_nonce(
|
|
219
|
-
|
|
233
|
+
get_permit2_contract(eth, permit2_address), wallet_address
|
|
220
234
|
)
|
|
221
235
|
|
|
222
|
-
# EIP-712 domain
|
|
223
236
|
domain = EIP712Domain(
|
|
224
237
|
name="Permit2",
|
|
225
238
|
chain_id=sell_data.chain_id,
|
|
226
239
|
verifying_contract=permit2_address,
|
|
227
240
|
)
|
|
228
241
|
|
|
229
|
-
# EIP-712 types
|
|
230
242
|
types = {
|
|
231
243
|
"TokenPermissions": [
|
|
232
244
|
{"name": "token", "type": "address"},
|
|
@@ -258,7 +270,6 @@ async def prepare_data_for_signature(
|
|
|
258
270
|
],
|
|
259
271
|
}
|
|
260
272
|
|
|
261
|
-
# Build message
|
|
262
273
|
message = PermitMessage(
|
|
263
274
|
permitted=TokenPermissions(
|
|
264
275
|
token=token_address,
|
|
@@ -306,16 +317,192 @@ async def sign_permit2(
|
|
|
306
317
|
wallet_address,
|
|
307
318
|
Quote(order_data=order_data),
|
|
308
319
|
)
|
|
309
|
-
# print(
|
|
310
|
-
# "Signing the following full message:",
|
|
311
|
-
# json.dumps(to_sign.model_dump(mode="json", by_alias=True)),
|
|
312
|
-
# )
|
|
313
320
|
signature = account.sign_typed_data(
|
|
314
321
|
full_message=to_sign.model_dump(mode="json", by_alias=True)
|
|
315
322
|
)
|
|
316
323
|
return (to_sign, signature)
|
|
317
324
|
|
|
318
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
|
+
|
|
319
506
|
async def approve_permit2(
|
|
320
507
|
w3: AsyncWeb3[P],
|
|
321
508
|
account: LocalAccount,
|
|
@@ -325,29 +512,28 @@ async def approve_permit2(
|
|
|
325
512
|
maxGas: int = 100000,
|
|
326
513
|
):
|
|
327
514
|
wallet_address = account.address
|
|
515
|
+
permit2_address = get_permit2_addr(chain)
|
|
328
516
|
|
|
329
517
|
erc20 = get_erc20_contract(w3, token_address)
|
|
330
|
-
permit2_contract = get_permit2(chain)
|
|
331
518
|
current_allowance = await erc20.functions.allowance(
|
|
332
|
-
wallet_address,
|
|
519
|
+
wallet_address, permit2_address
|
|
333
520
|
).call()
|
|
334
521
|
if current_allowance < required_quantity:
|
|
335
522
|
logger.info(
|
|
336
523
|
f"Approving {token_address}: allowance={current_allowance}, required={required_quantity}"
|
|
337
524
|
)
|
|
338
|
-
approve_fn = erc20.functions.approve(
|
|
525
|
+
approve_fn = erc20.functions.approve(permit2_address, 2**256 - 1)
|
|
339
526
|
tx = await approve_fn.build_transaction(
|
|
340
527
|
{
|
|
341
528
|
"from": wallet_address,
|
|
342
529
|
"nonce": await w3.eth.get_transaction_count(
|
|
343
530
|
w3.to_checksum_address(wallet_address)
|
|
344
531
|
),
|
|
345
|
-
"gas": maxGas,
|
|
532
|
+
"gas": maxGas,
|
|
346
533
|
"gasPrice": await w3.eth.gas_price,
|
|
347
534
|
}
|
|
348
535
|
)
|
|
349
536
|
|
|
350
|
-
# Sign and send transaction
|
|
351
537
|
tx_data = tx if isinstance(tx, dict) else tx.__dict__
|
|
352
538
|
signed_tx: SignedTransaction = account.sign_transaction(tx_data)
|
|
353
539
|
tx_hash = await w3.eth.send_raw_transaction(signed_tx.raw_transaction)
|
|
@@ -355,11 +541,13 @@ async def approve_permit2(
|
|
|
355
541
|
logger.debug(f"→ Approval tx hash: {tx_hash.hex()}")
|
|
356
542
|
return tx_hash.hex()
|
|
357
543
|
|
|
544
|
+
|
|
358
545
|
@dataclass
|
|
359
546
|
class Permit2Order:
|
|
360
547
|
msg: SignatureData
|
|
361
548
|
sig: SignedMessage
|
|
362
549
|
|
|
550
|
+
|
|
363
551
|
async def create_permit2_order(
|
|
364
552
|
w3: AsyncWeb3[P],
|
|
365
553
|
account: LocalAccount,
|
|
@@ -381,8 +569,19 @@ async def create_permit2_order(
|
|
|
381
569
|
dst_token,
|
|
382
570
|
raw_amount,
|
|
383
571
|
)
|
|
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
|
+
|
|
386
585
|
_ = await approve_permit2(w3, account, src_chain, src_token, raw_amount)
|
|
387
586
|
msg, sig = await sign_permit2(w3.eth, account, account.address, raw_amount, q)
|
|
388
587
|
return Permit2Order(msg, sig)
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tristero
|
|
3
|
+
Version: 0.3.2
|
|
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
|
+
|
|
26
|
+
### How it works
|
|
27
|
+
|
|
28
|
+
Tristero supports two primary swap mechanisms:
|
|
29
|
+
|
|
30
|
+
#### Permit2 Swaps (EVM-to-EVM)
|
|
31
|
+
- **Quote & Approve** - Request a quote and approve tokens via Permit2 (gasless approval)
|
|
32
|
+
- **Sign & Submit** - Sign an EIP-712 order and submit for execution
|
|
33
|
+
- **Monitor** - Track swap progress via WebSocket updates
|
|
34
|
+
|
|
35
|
+
#### Feather Swaps (UTXO-based)
|
|
36
|
+
- **Quote & Deposit** - Request a quote to receive a deposit address
|
|
37
|
+
- **Manual Transfer** - Send funds to the provided deposit address
|
|
38
|
+
- **Monitor** - Track swap completion via WebSocket updates
|
|
39
|
+
|
|
40
|
+
This library provides both high-level convenience functions and lower-level components for precise control.
|
|
41
|
+
|
|
42
|
+
### Installation
|
|
43
|
+
```
|
|
44
|
+
pip install tristero
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Quick Start
|
|
48
|
+
|
|
49
|
+
Execute a cross-chain swap in just a few lines:
|
|
50
|
+
|
|
51
|
+
```py
|
|
52
|
+
import os
|
|
53
|
+
import asyncio
|
|
54
|
+
|
|
55
|
+
from eth_account import Account
|
|
56
|
+
|
|
57
|
+
from tristero import ChainID, TokenSpec, execute_permit2_swap, make_async_w3
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
async def main() -> None:
|
|
61
|
+
private_key = os.getenv("TEST_ACCOUNT_PRIVKEY")
|
|
62
|
+
if not private_key:
|
|
63
|
+
raise RuntimeError("Set TEST_ACCOUNT_PRIVKEY")
|
|
64
|
+
|
|
65
|
+
account = Account.from_key(private_key)
|
|
66
|
+
|
|
67
|
+
arbitrum_rpc = os.getenv("ARBITRUM_RPC_URL", "https://arbitrum-one-rpc.publicnode.com")
|
|
68
|
+
w3 = make_async_w3(arbitrum_rpc)
|
|
69
|
+
|
|
70
|
+
# Example: USDC on Arbitrum -> USDT on Base
|
|
71
|
+
result = await execute_permit2_swap(
|
|
72
|
+
w3=w3,
|
|
73
|
+
account=account,
|
|
74
|
+
src_t=TokenSpec(chain_id=ChainID(42161), token_address="0xaf88d065e77c8cC2239327C5EDb3A432268e5831"), # USDC (Arbitrum)
|
|
75
|
+
dst_t=TokenSpec(chain_id=ChainID(8453), token_address="0xfde4C96c8593536E31F229EA8f37b2ADa2699bb2"), # USDT (Base)
|
|
76
|
+
raw_amount=1_000_000, # 1 USDC (6 decimals)
|
|
77
|
+
timeout=300,
|
|
78
|
+
)
|
|
79
|
+
print(result)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
asyncio.run(main())
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Usage Examples
|
|
86
|
+
|
|
87
|
+
#### Quote (read-only)
|
|
88
|
+
|
|
89
|
+
```py
|
|
90
|
+
import asyncio
|
|
91
|
+
import os
|
|
92
|
+
|
|
93
|
+
from eth_account import Account
|
|
94
|
+
|
|
95
|
+
from tristero import ChainID, get_quote
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
async def main() -> None:
|
|
99
|
+
private_key = os.getenv("TEST_ACCOUNT_PRIVKEY")
|
|
100
|
+
if not private_key:
|
|
101
|
+
raise RuntimeError("Set TEST_ACCOUNT_PRIVKEY")
|
|
102
|
+
|
|
103
|
+
wallet = Account.from_key(private_key).address
|
|
104
|
+
|
|
105
|
+
quote = await get_quote(
|
|
106
|
+
from_wallet=wallet,
|
|
107
|
+
to_wallet=wallet,
|
|
108
|
+
from_chain_id=str(ChainID(42161).value),
|
|
109
|
+
from_address="0xaf88d065e77c8cC2239327C5EDb3A432268e5831", # USDC (Arbitrum)
|
|
110
|
+
to_chain_id=str(ChainID(42161).value),
|
|
111
|
+
to_address="0x82aF49447D8a07e3bd95BD0d56f35241523fBab1", # WETH (Arbitrum)
|
|
112
|
+
amount=1_000_000,
|
|
113
|
+
)
|
|
114
|
+
print(quote)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
asyncio.run(main())
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
#### Margin: Direct Open
|
|
121
|
+
|
|
122
|
+
```py
|
|
123
|
+
import asyncio
|
|
124
|
+
import os
|
|
125
|
+
|
|
126
|
+
from eth_account import Account
|
|
127
|
+
from tristero import open_margin_position
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
async def main() -> None:
|
|
131
|
+
private_key = os.getenv("TEST_ACCOUNT_PRIVKEY", "")
|
|
132
|
+
if not private_key:
|
|
133
|
+
raise RuntimeError("Set TEST_ACCOUNT_PRIVKEY")
|
|
134
|
+
|
|
135
|
+
wallet = Account.from_key(private_key).address
|
|
136
|
+
|
|
137
|
+
chain_id = "42161"
|
|
138
|
+
quote_currency = "0xaf88d065e77c8cC2239327C5EDb3A432268e5831" # USDC (Arbitrum)
|
|
139
|
+
base_currency = "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1" # WETH (Arbitrum)
|
|
140
|
+
leverage = 2
|
|
141
|
+
collateral = "1000000" # 1 USDC (6 decimals)
|
|
142
|
+
|
|
143
|
+
result = await open_margin_position(
|
|
144
|
+
private_key=private_key,
|
|
145
|
+
chain_id=chain_id,
|
|
146
|
+
wallet_address=wallet,
|
|
147
|
+
quote_currency=quote_currency,
|
|
148
|
+
base_currency=base_currency,
|
|
149
|
+
leverage_ratio=leverage,
|
|
150
|
+
collateral_amount=collateral,
|
|
151
|
+
wait_for_result=True,
|
|
152
|
+
timeout=120,
|
|
153
|
+
)
|
|
154
|
+
print(result)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
asyncio.run(main())
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
#### Margin: List Positions / Close Position
|
|
161
|
+
|
|
162
|
+
```py
|
|
163
|
+
import asyncio
|
|
164
|
+
import os
|
|
165
|
+
|
|
166
|
+
from eth_account import Account
|
|
167
|
+
from tristero import close_margin_position, list_margin_positions
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
async def main() -> None:
|
|
171
|
+
private_key = os.getenv("TEST_ACCOUNT_PRIVKEY", "")
|
|
172
|
+
if not private_key:
|
|
173
|
+
raise RuntimeError("Set TEST_ACCOUNT_PRIVKEY")
|
|
174
|
+
|
|
175
|
+
wallet = Account.from_key(private_key).address
|
|
176
|
+
chain_id = "42161"
|
|
177
|
+
|
|
178
|
+
positions = await list_margin_positions(wallet)
|
|
179
|
+
open_pos = next((p for p in positions if p.status == "open"), None)
|
|
180
|
+
if not open_pos:
|
|
181
|
+
raise RuntimeError("no open positions")
|
|
182
|
+
|
|
183
|
+
result = await close_margin_position(
|
|
184
|
+
private_key=private_key,
|
|
185
|
+
chain_id=chain_id,
|
|
186
|
+
position_id=open_pos.taker_token_id,
|
|
187
|
+
escrow_contract=open_pos.escrow_address,
|
|
188
|
+
authorized=open_pos.filler_address,
|
|
189
|
+
cash_settle=False,
|
|
190
|
+
fraction_bps=10_000,
|
|
191
|
+
deadline_seconds=3600,
|
|
192
|
+
wait_for_result=True,
|
|
193
|
+
timeout=120,
|
|
194
|
+
)
|
|
195
|
+
print(result)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
asyncio.run(main())
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
#### Feather: Start (get deposit address)
|
|
203
|
+
|
|
204
|
+
Feather swaps are deposit-based: you start an order to receive a `deposit_address`, send funds to it manually, then optionally wait for completion.
|
|
205
|
+
|
|
206
|
+
Submit only:
|
|
207
|
+
|
|
208
|
+
```py
|
|
209
|
+
import asyncio
|
|
210
|
+
|
|
211
|
+
from tristero import ChainID, TokenSpec, start_feather_swap
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
async def main() -> None:
|
|
215
|
+
# Example: ETH (native) -> XMR (native)
|
|
216
|
+
src_t = TokenSpec(chain_id=ChainID.ethereum, token_address="native")
|
|
217
|
+
dst_t = TokenSpec(chain_id=ChainID.monero, token_address="native")
|
|
218
|
+
|
|
219
|
+
# Replace with your own destination address on the destination chain.
|
|
220
|
+
dst_addr = "YOUR_XMR_ADDRESS"
|
|
221
|
+
|
|
222
|
+
swap = await start_feather_swap(
|
|
223
|
+
src_t=src_t,
|
|
224
|
+
dst_t=dst_t,
|
|
225
|
+
dst_addr=dst_addr,
|
|
226
|
+
raw_amount=100_000_000_000_000_000, # 0.1 ETH in wei
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
order_id = (
|
|
230
|
+
(swap.data or {}).get("id")
|
|
231
|
+
or (swap.data or {}).get("order_id")
|
|
232
|
+
or (swap.data or {}).get("orderId")
|
|
233
|
+
or ""
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
print("order_id:", order_id)
|
|
237
|
+
print("deposit_address:", swap.deposit_address)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
asyncio.run(main())
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
Submit + wait (WebSocket):
|
|
244
|
+
|
|
245
|
+
```py
|
|
246
|
+
import asyncio
|
|
247
|
+
|
|
248
|
+
from tristero import ChainID, OrderType, TokenSpec, start_feather_swap, wait_for_completion
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
async def main() -> None:
|
|
252
|
+
src_t = TokenSpec(chain_id=ChainID.ethereum, token_address="native")
|
|
253
|
+
dst_t = TokenSpec(chain_id=ChainID.monero, token_address="native")
|
|
254
|
+
dst_addr = "YOUR_XMR_ADDRESS"
|
|
255
|
+
|
|
256
|
+
swap = await start_feather_swap(
|
|
257
|
+
src_t=src_t,
|
|
258
|
+
dst_t=dst_t,
|
|
259
|
+
dst_addr=dst_addr,
|
|
260
|
+
raw_amount=100_000_000_000_000_000,
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
order_id = (
|
|
264
|
+
(swap.data or {}).get("id")
|
|
265
|
+
or (swap.data or {}).get("order_id")
|
|
266
|
+
or (swap.data or {}).get("orderId")
|
|
267
|
+
or ""
|
|
268
|
+
)
|
|
269
|
+
if not order_id:
|
|
270
|
+
raise RuntimeError(f"Feather swap response missing order id: {swap.data}")
|
|
271
|
+
|
|
272
|
+
print("deposit_address:", swap.deposit_address)
|
|
273
|
+
print("Waiting for completion...")
|
|
274
|
+
completion = await wait_for_completion(order_id, order_type=OrderType.FEATHER)
|
|
275
|
+
print(completion)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
asyncio.run(main())
|
|
279
|
+
```
|
|
@@ -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.2.dist-info/licenses/LICENSE,sha256=b-9ikwk9ICk964mtUbqVkDgSf3S6d52HTQxyGHmNo9M,10925
|
|
19
|
+
tristero-0.3.2.dist-info/METADATA,sha256=B1vn317_Yr9qROxJbBuzyqFxtyTtmJnRVIZhT0CIlhg,7380
|
|
20
|
+
tristero-0.3.2.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
21
|
+
tristero-0.3.2.dist-info/top_level.txt,sha256=xO745nTllCKw6Fdu-a2ZYv5-ZVOKl2vt9ccRUjCVXfI,9
|
|
22
|
+
tristero-0.3.2.dist-info/RECORD,,
|