tristero 0.1.4__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 +12 -0
- tristero/api.py +158 -0
- tristero/client.py +147 -0
- tristero/config.py +19 -0
- tristero/files/erc20_abi.json +671 -0
- tristero/files/permit2_abi.json +411 -0
- tristero/permit2.py +384 -0
- tristero/py.typed +0 -0
- tristero-0.1.4.dist-info/METADATA +157 -0
- tristero-0.1.4.dist-info/RECORD +11 -0
- tristero-0.1.4.dist-info/WHEEL +4 -0
tristero/permit2.py
ADDED
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Any, List, Optional, TypeVar, cast
|
|
3
|
+
from eth_account import Account
|
|
4
|
+
from eth_account.datastructures import SignedMessage, SignedTransaction
|
|
5
|
+
from eth_account.signers.base import BaseAccount
|
|
6
|
+
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 pydantic.alias_generators import to_camel
|
|
10
|
+
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
|
+
from web3.contract import AsyncContract
|
|
17
|
+
from functools import cache, lru_cache
|
|
18
|
+
import json
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from importlib import resources as impresources
|
|
21
|
+
|
|
22
|
+
from web3 import Web3
|
|
23
|
+
from web3.eth import AsyncEth
|
|
24
|
+
|
|
25
|
+
from .api import (
|
|
26
|
+
_WRAPPED_GAS_ADDRESSES,
|
|
27
|
+
get_quote,
|
|
28
|
+
_PERMIT2_CONTRACT_ADDRESSES,
|
|
29
|
+
ChainID,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
logger = logging.getLogger(__name__)
|
|
33
|
+
|
|
34
|
+
P = TypeVar("P", bound=AsyncBaseProvider)
|
|
35
|
+
|
|
36
|
+
PERMIT2_ABI_FILE = impresources.files("tristero.files") / "permit2_abi.json"
|
|
37
|
+
ERC20_ABI_FILE = impresources.files("tristero.files") / "erc20_abi.json"
|
|
38
|
+
PERMIT2_ABI = json.loads(PERMIT2_ABI_FILE.read_text())
|
|
39
|
+
ERC20_ABI = json.loads(ERC20_ABI_FILE.read_text())
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@lru_cache(maxsize=None)
|
|
43
|
+
def get_permit2(eth: AsyncEth, permit2_address: str):
|
|
44
|
+
return eth.contract(
|
|
45
|
+
address=Web3.to_checksum_address(permit2_address), abi=PERMIT2_ABI
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
@cache
|
|
49
|
+
def get_erc20_contract(w3: AsyncWeb3[P], token_address: str) -> AsyncContract:
|
|
50
|
+
"""Get ERC20 contract instance."""
|
|
51
|
+
return w3.eth.contract(
|
|
52
|
+
address=Web3.to_checksum_address(token_address), abi=ERC20_ABI
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class BaseSchema(BaseModel, frozen=True):
|
|
57
|
+
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class TokenData(BaseSchema, frozen=True):
|
|
61
|
+
address: str
|
|
62
|
+
value: int
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class ChainData(BaseSchema, frozen=True):
|
|
66
|
+
chain_id: int
|
|
67
|
+
token: TokenData
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class OrderParameters(BaseSchema, frozen=True):
|
|
71
|
+
src_asset: str
|
|
72
|
+
dst_asset: str
|
|
73
|
+
src_quantity: str
|
|
74
|
+
dst_quantity: str
|
|
75
|
+
min_quantity: str
|
|
76
|
+
dark_salt: str
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class OrderData(BaseSchema, frozen=True):
|
|
80
|
+
parameters: OrderParameters
|
|
81
|
+
deadline: int
|
|
82
|
+
router_address: str
|
|
83
|
+
filler_wallet_address: str
|
|
84
|
+
order_type: str
|
|
85
|
+
custom_data: List[bytes] = Field(default_factory=list)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class Quote(BaseSchema, frozen=True):
|
|
89
|
+
order_data: OrderData
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class SignedOrder(BaseSchema, frozen=True):
|
|
93
|
+
sender: str
|
|
94
|
+
parameters: OrderParameters
|
|
95
|
+
deadline: str
|
|
96
|
+
target: str
|
|
97
|
+
filler: str
|
|
98
|
+
order_type: str
|
|
99
|
+
custom_data: List[bytes]
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class TokenPermissions(BaseSchema, frozen=True):
|
|
103
|
+
token: str
|
|
104
|
+
amount: str
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class EIP712Domain(BaseSchema, frozen=True):
|
|
108
|
+
name: str
|
|
109
|
+
chain_id: int
|
|
110
|
+
verifying_contract: str
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class PermitMessage(BaseSchema, frozen=True):
|
|
114
|
+
permitted: TokenPermissions
|
|
115
|
+
spender: str
|
|
116
|
+
nonce: str
|
|
117
|
+
deadline: str
|
|
118
|
+
witness: SignedOrder
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class SignatureData(BaseSchema, frozen=True):
|
|
122
|
+
domain: EIP712Domain
|
|
123
|
+
types: dict[str, Any]
|
|
124
|
+
primary_type: str
|
|
125
|
+
message: PermitMessage
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def first_zero_bit(n: int) -> int | None:
|
|
129
|
+
if n > 0 and (n & (n + 1)) == 0:
|
|
130
|
+
return None
|
|
131
|
+
return (n ^ (n + 1)).bit_length() - 1
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
async def get_permit2_unordered_nonce(c: AsyncContract, wallet_address: str):
|
|
135
|
+
startWord = random.randint(2**126, 2**189)
|
|
136
|
+
endWord = startWord + 10
|
|
137
|
+
wordPos = await anext(
|
|
138
|
+
(
|
|
139
|
+
((w << 8) | bit)
|
|
140
|
+
for w in range(startWord, endWord)
|
|
141
|
+
if (
|
|
142
|
+
bit := first_zero_bit(
|
|
143
|
+
await c.functions.nonceBitmap(wallet_address, w).call()
|
|
144
|
+
)
|
|
145
|
+
)
|
|
146
|
+
is not None
|
|
147
|
+
),
|
|
148
|
+
None,
|
|
149
|
+
)
|
|
150
|
+
if wordPos is None:
|
|
151
|
+
raise Exception(f"No free unordered nonces in words {startWord}-{endWord}")
|
|
152
|
+
return wordPos
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
async def prepare_data_for_signature(
|
|
156
|
+
eth: AsyncEth,
|
|
157
|
+
sell_data: ChainData,
|
|
158
|
+
buy_data: ChainData,
|
|
159
|
+
wallet_address: str,
|
|
160
|
+
quote: Quote,
|
|
161
|
+
destination_address: str | None = None,
|
|
162
|
+
) -> SignatureData:
|
|
163
|
+
"""
|
|
164
|
+
Prepare EIP-712 signature data for Permit2 witness transfer.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
sell_data: Source chain and token data
|
|
168
|
+
buy_data: Destination chain and token data
|
|
169
|
+
wallet_address: User's wallet address
|
|
170
|
+
quote: Quote data with order parameters and deadline
|
|
171
|
+
destination_address: Optional destination address
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
SignatureData with domain, types, primaryType, and message for signing
|
|
175
|
+
"""
|
|
176
|
+
# Validate required fields
|
|
177
|
+
if not quote.order_data.parameters.min_quantity:
|
|
178
|
+
raise ValueError("Min quantity is required in the order_data.parameters")
|
|
179
|
+
|
|
180
|
+
if not quote.order_data.router_address:
|
|
181
|
+
raise ValueError("Router address is required in the order_data")
|
|
182
|
+
|
|
183
|
+
if not quote.order_data.deadline:
|
|
184
|
+
raise ValueError("Deadline is required in the order_data")
|
|
185
|
+
|
|
186
|
+
from_chain = ChainID(str(sell_data.chain_id))
|
|
187
|
+
|
|
188
|
+
deadline = quote.order_data.deadline
|
|
189
|
+
|
|
190
|
+
# Handle native token address conversion
|
|
191
|
+
token_address = (
|
|
192
|
+
_WRAPPED_GAS_ADDRESSES[from_chain]
|
|
193
|
+
if sell_data.token.address == "native"
|
|
194
|
+
else sell_data.token.address
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
# Build witness object
|
|
198
|
+
witness = SignedOrder(
|
|
199
|
+
sender=wallet_address,
|
|
200
|
+
parameters=OrderParameters(
|
|
201
|
+
src_asset=token_address,
|
|
202
|
+
dst_asset=buy_data.token.address,
|
|
203
|
+
src_quantity=str(sell_data.token.value),
|
|
204
|
+
dst_quantity=str(buy_data.token.value),
|
|
205
|
+
min_quantity=quote.order_data.parameters.min_quantity,
|
|
206
|
+
dark_salt=quote.order_data.parameters.dark_salt,
|
|
207
|
+
),
|
|
208
|
+
deadline=str(deadline),
|
|
209
|
+
target=destination_address or wallet_address,
|
|
210
|
+
filler=quote.order_data.filler_wallet_address,
|
|
211
|
+
order_type=quote.order_data.order_type,
|
|
212
|
+
custom_data=quote.order_data.custom_data or [],
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
# Get Permit2 address
|
|
216
|
+
permit2_address = _PERMIT2_CONTRACT_ADDRESSES.get(from_chain)
|
|
217
|
+
if not permit2_address:
|
|
218
|
+
raise ValueError("Permit2 not deployed on this chain.")
|
|
219
|
+
|
|
220
|
+
spender = quote.order_data.router_address
|
|
221
|
+
nonce = await get_permit2_unordered_nonce(
|
|
222
|
+
get_permit2(eth, permit2_address), wallet_address
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
# EIP-712 domain
|
|
226
|
+
domain = EIP712Domain(
|
|
227
|
+
name="Permit2",
|
|
228
|
+
chain_id=sell_data.chain_id,
|
|
229
|
+
verifying_contract=permit2_address,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
# EIP-712 types
|
|
233
|
+
types = {
|
|
234
|
+
"TokenPermissions": [
|
|
235
|
+
{"name": "token", "type": "address"},
|
|
236
|
+
{"name": "amount", "type": "uint256"},
|
|
237
|
+
],
|
|
238
|
+
"OrderParameters": [
|
|
239
|
+
{"name": "srcAsset", "type": "address"},
|
|
240
|
+
{"name": "dstAsset", "type": "address"},
|
|
241
|
+
{"name": "srcQuantity", "type": "uint256"},
|
|
242
|
+
{"name": "dstQuantity", "type": "uint256"},
|
|
243
|
+
{"name": "minQuantity", "type": "uint256"},
|
|
244
|
+
{"name": "darkSalt", "type": "uint128"},
|
|
245
|
+
],
|
|
246
|
+
"SignedOrder": [
|
|
247
|
+
{"name": "sender", "type": "address"},
|
|
248
|
+
{"name": "parameters", "type": "OrderParameters"},
|
|
249
|
+
{"name": "deadline", "type": "uint256"},
|
|
250
|
+
{"name": "target", "type": "address"},
|
|
251
|
+
{"name": "filler", "type": "address"},
|
|
252
|
+
{"name": "orderType", "type": "string"},
|
|
253
|
+
{"name": "customData", "type": "bytes[]"},
|
|
254
|
+
],
|
|
255
|
+
"PermitWitnessTransferFrom": [
|
|
256
|
+
{"name": "permitted", "type": "TokenPermissions"},
|
|
257
|
+
{"name": "spender", "type": "address"},
|
|
258
|
+
{"name": "nonce", "type": "uint256"},
|
|
259
|
+
{"name": "deadline", "type": "uint256"},
|
|
260
|
+
{"name": "witness", "type": "SignedOrder"},
|
|
261
|
+
],
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
# Build message
|
|
265
|
+
message = PermitMessage(
|
|
266
|
+
permitted=TokenPermissions(
|
|
267
|
+
token=token_address,
|
|
268
|
+
amount=str(sell_data.token.value),
|
|
269
|
+
),
|
|
270
|
+
spender=spender,
|
|
271
|
+
nonce=str(nonce),
|
|
272
|
+
deadline=str(deadline),
|
|
273
|
+
witness=witness,
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
return SignatureData(
|
|
277
|
+
domain=domain,
|
|
278
|
+
types=types,
|
|
279
|
+
primary_type="PermitWitnessTransferFrom",
|
|
280
|
+
message=message,
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
async def sign_permit2(
|
|
285
|
+
eth: AsyncEth,
|
|
286
|
+
account: LocalAccount,
|
|
287
|
+
wallet_address: str,
|
|
288
|
+
src_amount: int,
|
|
289
|
+
quote_data: dict[str, Any],
|
|
290
|
+
) -> tuple[SignatureData, SignedMessage]:
|
|
291
|
+
from_chain_id = quote_data["src_token"]["chain_id"]
|
|
292
|
+
from_address = quote_data["src_token"]["address"]
|
|
293
|
+
to_chain_id = quote_data["dst_token"]["chain_id"]
|
|
294
|
+
to_address = quote_data["dst_token"]["address"]
|
|
295
|
+
|
|
296
|
+
order_data = quote_data["order_data"]
|
|
297
|
+
dst_amount = int(order_data["parameters"]["dst_quantity"])
|
|
298
|
+
|
|
299
|
+
to_sign = await prepare_data_for_signature(
|
|
300
|
+
eth,
|
|
301
|
+
ChainData(
|
|
302
|
+
chain_id=from_chain_id,
|
|
303
|
+
token=TokenData(address=from_address, value=src_amount),
|
|
304
|
+
),
|
|
305
|
+
ChainData(
|
|
306
|
+
chain_id=to_chain_id,
|
|
307
|
+
token=TokenData(address=to_address, value=dst_amount),
|
|
308
|
+
),
|
|
309
|
+
wallet_address,
|
|
310
|
+
Quote(order_data=order_data),
|
|
311
|
+
)
|
|
312
|
+
# print(
|
|
313
|
+
# "Signing the following full message:",
|
|
314
|
+
# json.dumps(to_sign.model_dump(mode="json", by_alias=True)),
|
|
315
|
+
# )
|
|
316
|
+
signature = account.sign_typed_data(
|
|
317
|
+
full_message=to_sign.model_dump(mode="json", by_alias=True)
|
|
318
|
+
)
|
|
319
|
+
return (to_sign, signature)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
async def approve_permit2(
|
|
323
|
+
w3: AsyncWeb3[P],
|
|
324
|
+
account: LocalAccount,
|
|
325
|
+
chain: ChainID,
|
|
326
|
+
token_address: str,
|
|
327
|
+
required_quantity: int,
|
|
328
|
+
maxGas: int = 100000,
|
|
329
|
+
):
|
|
330
|
+
wallet_address = account.address
|
|
331
|
+
|
|
332
|
+
erc20 = get_erc20_contract(w3, token_address)
|
|
333
|
+
permit2_contract = _PERMIT2_CONTRACT_ADDRESSES.get(chain)
|
|
334
|
+
current_allowance = await erc20.functions.allowance(
|
|
335
|
+
wallet_address, permit2_contract
|
|
336
|
+
).call()
|
|
337
|
+
if current_allowance < required_quantity:
|
|
338
|
+
logger.info(
|
|
339
|
+
f"Approving {token_address}: allowance={current_allowance}, required={required_quantity}"
|
|
340
|
+
)
|
|
341
|
+
approve_fn = erc20.functions.approve(permit2_contract, 2**256 - 1)
|
|
342
|
+
tx = await approve_fn.build_transaction(
|
|
343
|
+
{
|
|
344
|
+
"from": wallet_address,
|
|
345
|
+
"nonce": await w3.eth.get_transaction_count(
|
|
346
|
+
w3.to_checksum_address(wallet_address)
|
|
347
|
+
),
|
|
348
|
+
"gas": maxGas, # Adjust as needed
|
|
349
|
+
"gasPrice": await w3.eth.gas_price,
|
|
350
|
+
}
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
# Sign and send transaction
|
|
354
|
+
signed_tx: SignedTransaction = account.sign_transaction(tx.__dict__)
|
|
355
|
+
tx_hash = await w3.eth.send_raw_transaction(signed_tx.raw_transaction)
|
|
356
|
+
|
|
357
|
+
logger.debug(f"→ Approval tx hash: {tx_hash.hex()}")
|
|
358
|
+
return tx_hash.hex()
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
async def create_order(
|
|
362
|
+
w3: AsyncWeb3[P],
|
|
363
|
+
account: LocalAccount,
|
|
364
|
+
src_chain: ChainID,
|
|
365
|
+
src_token: str,
|
|
366
|
+
dst_chain: ChainID,
|
|
367
|
+
dst_token: str,
|
|
368
|
+
raw_amount: int,
|
|
369
|
+
to_address: str | None = None,
|
|
370
|
+
):
|
|
371
|
+
if not to_address:
|
|
372
|
+
to_address = account.address
|
|
373
|
+
q = await get_quote(
|
|
374
|
+
account.address,
|
|
375
|
+
to_address,
|
|
376
|
+
src_chain,
|
|
377
|
+
src_token,
|
|
378
|
+
dst_chain,
|
|
379
|
+
dst_token,
|
|
380
|
+
raw_amount,
|
|
381
|
+
)
|
|
382
|
+
await approve_permit2(w3, account, src_chain, src_token, raw_amount)
|
|
383
|
+
# print("Quote: ", json.dumps(q))
|
|
384
|
+
return await sign_permit2(w3.eth, account, account.address, raw_amount, q)
|
tristero/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: tristero
|
|
3
|
+
Version: 0.1.4
|
|
4
|
+
Summary: Library for trading on Tristero
|
|
5
|
+
Author: pty1
|
|
6
|
+
Author-email: pty1 <pty11@proton.me>
|
|
7
|
+
Requires-Dist: eth-account>=0.13.7
|
|
8
|
+
Requires-Dist: httpx>=0.28.1
|
|
9
|
+
Requires-Dist: pydantic>=2.12.4
|
|
10
|
+
Requires-Dist: tenacity>=9.1.2
|
|
11
|
+
Requires-Dist: web3>=7.14.0
|
|
12
|
+
Requires-Dist: websockets>=15.0.1
|
|
13
|
+
Requires-Python: >=3.13
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# Tristero
|
|
17
|
+
[](https://badge.fury.io/py/tristero)
|
|
18
|
+
[](https://pypi.org/project/tristero/)
|
|
19
|
+
|
|
20
|
+
This repository is home to Tristero's trading library.
|
|
21
|
+
|
|
22
|
+
### Installation
|
|
23
|
+
```
|
|
24
|
+
pip install tristero
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Quick Start
|
|
28
|
+
|
|
29
|
+
Execute a cross-chain swap in just a few lines:
|
|
30
|
+
|
|
31
|
+
```py
|
|
32
|
+
import os
|
|
33
|
+
from tristero.client import TokenSpec, execute_swap
|
|
34
|
+
from eth_account import Account
|
|
35
|
+
from web3 import AsyncWeb3
|
|
36
|
+
from tristero.api import ChainID
|
|
37
|
+
|
|
38
|
+
private_key = os.getenv("EVM_PRIVATE_KEY")
|
|
39
|
+
account = Account.from_key(private_key)
|
|
40
|
+
w3 = AsyncWeb3(AsyncWeb3.AsyncHTTPProvider("https://arbitrum-one-rpc.publicnode.com"))
|
|
41
|
+
|
|
42
|
+
result = await execute_swap(
|
|
43
|
+
w3=w3,
|
|
44
|
+
account=account,
|
|
45
|
+
src_t=TokenSpec(chain_id=ChainID.arbitrum, token_address="0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9"), # USDT
|
|
46
|
+
dst_t=TokenSpec(chain_id=ChainID.base, token_address="0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"), # USDC
|
|
47
|
+
raw_amount=10000000 # Raw token amount (multiply by 10^decimals)
|
|
48
|
+
)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### How it works
|
|
52
|
+
|
|
53
|
+
Tristero swaps happen in two steps:
|
|
54
|
+
- **Quote & Sign** - Request a quote from the server and sign it with your private key
|
|
55
|
+
- **Submit & Fill** - Submit the signed order to be filled at a later date
|
|
56
|
+
|
|
57
|
+
This library provides both high-level convenience functions and lower-level components for precise control.
|
|
58
|
+
|
|
59
|
+
### API Reference
|
|
60
|
+
|
|
61
|
+
#### Execute Full Swap
|
|
62
|
+
`execute_swap` handles the entire workflow automatically: quoting, signing, submitting, and monitoring.
|
|
63
|
+
```py
|
|
64
|
+
from tristero.swap import execute_swap, TokenSpec
|
|
65
|
+
from web3 import AsyncWeb3
|
|
66
|
+
from eth_account.signers.local import LocalAccount
|
|
67
|
+
|
|
68
|
+
w3 = AsyncWeb3(...) # Your Web3 provider
|
|
69
|
+
account: LocalAccount = ... # Your account
|
|
70
|
+
|
|
71
|
+
result = await execute_swap(
|
|
72
|
+
w3=w3,
|
|
73
|
+
account=account,
|
|
74
|
+
src_t=TokenSpec(chain_id=ChainID.ethereum, token_address="0xA0b8..."),
|
|
75
|
+
dst_t=TokenSpec(chain_id=ChainID.arbitrum, token_address="0xaf88..."),
|
|
76
|
+
raw_amount=10*(10**18),
|
|
77
|
+
retry=True,
|
|
78
|
+
timeout=300.0 # 5 minutes
|
|
79
|
+
)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
#### Requesting a quote
|
|
83
|
+
|
|
84
|
+
`get_quote` requests a quote for a particular swap, letting you see output amounts and gas fees.
|
|
85
|
+
|
|
86
|
+
```py
|
|
87
|
+
from tristero.api import get_quote, ChainID
|
|
88
|
+
|
|
89
|
+
quote = await get_quote(
|
|
90
|
+
from_wallet="0x1234...", # Source wallet address
|
|
91
|
+
to_wallet="0x5678...", # Destination wallet address
|
|
92
|
+
from_chain_id=ChainID.ethereum, # Source chain
|
|
93
|
+
from_address="0xA0b8...", # Source token address (or "native")
|
|
94
|
+
to_chain_id=ChainID.arbitrum, # Destination chain
|
|
95
|
+
to_address="0xaf88...", # Destination token address (or "native")
|
|
96
|
+
amount=10*(10**18), # Amount in smallest unit (wei)
|
|
97
|
+
)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
#### Creating a signed order
|
|
101
|
+
`create_order` creates and signs an order without submitting to be filled.
|
|
102
|
+
|
|
103
|
+
```py
|
|
104
|
+
from tristero.api import get_quote, ChainID
|
|
105
|
+
|
|
106
|
+
w3 = AsyncWeb3(...) # Your Web3 provider
|
|
107
|
+
account: LocalAccount = ... # Your account
|
|
108
|
+
|
|
109
|
+
data, sig = await create_order(
|
|
110
|
+
w3,
|
|
111
|
+
account,
|
|
112
|
+
from_chain_id=ChainID.ethereum,
|
|
113
|
+
from_address="0xA0b8...",
|
|
114
|
+
to_chain_id=ChainID.arbitrum,
|
|
115
|
+
to_address="0xaf88...",
|
|
116
|
+
raw_amount=10*(10**18),
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
#### Submit order
|
|
122
|
+
|
|
123
|
+
`fill_order` submits a signed order for execution.
|
|
124
|
+
|
|
125
|
+
```py
|
|
126
|
+
from tristero.api import fill_order
|
|
127
|
+
|
|
128
|
+
data, sig = ... # from earlier
|
|
129
|
+
|
|
130
|
+
response = await fill_order(
|
|
131
|
+
signature=str(sig.signature.to_0x_hex()),
|
|
132
|
+
domain=data.domain.model_dump(by_alias=True, mode="json"),
|
|
133
|
+
message=data.message.model_dump(by_alias=True, mode="json"),
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
order_id = response['id']
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
#### Subscribing for updates
|
|
140
|
+
|
|
141
|
+
Orders can be monitored for changes and status live
|
|
142
|
+
|
|
143
|
+
```py
|
|
144
|
+
from tristero.api import poll_updates
|
|
145
|
+
import json
|
|
146
|
+
|
|
147
|
+
ws = await poll_updates(order_id)
|
|
148
|
+
|
|
149
|
+
async for msg in ws:
|
|
150
|
+
update = json.loads(msg)
|
|
151
|
+
print(f"Completed: {update['completed']}")
|
|
152
|
+
print(f"Failed: {update['failed']}")
|
|
153
|
+
|
|
154
|
+
if update["completed"] or update["failed"]:
|
|
155
|
+
await ws.close()
|
|
156
|
+
break
|
|
157
|
+
```
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
tristero/__init__.py,sha256=Jmbdg3LwOD4rrO0eerQTmn-wO-Sr4xRZ2r_RtM4iNDg,315
|
|
2
|
+
tristero/api.py,sha256=l4Qk_MJnu77bOjiqIdS7DYQbAg-JvYWBYpSmpeyFXEU,5507
|
|
3
|
+
tristero/client.py,sha256=DEcupVyEbPf78JDu-FrG8a60JlNQM0B10BsfieH8ddE,4073
|
|
4
|
+
tristero/config.py,sha256=_0PP2gufvq_t-gdKqDrxht2BxoWpNiGXk4m_u57WOYY,530
|
|
5
|
+
tristero/files/erc20_abi.json,sha256=jvsJ6aCwhMcmo3Yy1ajt5lPl_nTRg7tv-tGj87xzTOg,12800
|
|
6
|
+
tristero/files/permit2_abi.json,sha256=NV0AUUA9kqFPk56njvRRzUyjBhrBncKIMd3PrSH0LCc,17817
|
|
7
|
+
tristero/permit2.py,sha256=pOqNNZjd78QTHxhAPXjyN5NxBbXd5hF4o0pWP8WDvco,11329
|
|
8
|
+
tristero/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
+
tristero-0.1.4.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
|
|
10
|
+
tristero-0.1.4.dist-info/METADATA,sha256=WSV-y4KXNWNea8NHCUw2geHBDyUjkWBAwKgVp_NodYE,4389
|
|
11
|
+
tristero-0.1.4.dist-info/RECORD,,
|