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/config.py CHANGED
@@ -1,19 +1,35 @@
1
1
  from contextvars import ContextVar
2
2
 
3
+
3
4
  class Config:
4
5
  filler_url = "https://api.tristero.com/v2/orders"
5
6
  quoter_url = "https://api.tristero.com/v2/quotes"
6
7
  ws_url = "wss://api.tristero.com/v2/orders"
8
+
9
+ margin_quoter_url = "https://api.tristero.com/v2/quotes"
10
+ margin_filler_url = "https://api.tristero.com/v2/orders"
11
+ wallet_server_url = "https://api.tristero.com/v2/wallets"
12
+
13
+ # filler_url = "http://localhost:8070"
14
+ # quoter_url = "http://localhost:8060"
15
+ # ws_url = "ws://localhost:8070"
16
+
17
+ # margin_quoter_url = "http://localhost:8060"
18
+ # margin_filler_url = "http://localhost:8070"
19
+ # wallet_server_url = "http://localhost:8090"
7
20
 
8
21
  headers = {
9
22
  "User-Agent": "Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148"
10
23
  }
11
24
 
25
+
12
26
  config_var = ContextVar("config", default=Config())
13
27
 
28
+
14
29
  def get_config():
15
30
  return config_var.get()
16
31
 
32
+
17
33
  def set_config(new_config: Config):
18
34
  return config_var.set(new_config)
19
35
 
tristero/data.py ADDED
@@ -0,0 +1,56 @@
1
+ from importlib import resources as impresources
2
+ import json
3
+ from typing import TYPE_CHECKING
4
+ import enum
5
+
6
+ CHAINS_FILE = impresources.files("tristero.files") / "chains.json"
7
+ CHAINS = json.loads(CHAINS_FILE.read_text())
8
+
9
+ def _chain_key(chain: str | int) -> str:
10
+ """Resolve a chain key from either a chain key (e.g. 'arbitrum') or a chain id (e.g. 42161/'42161')."""
11
+ s = str(chain)
12
+ if s in CHAINS:
13
+ return s
14
+
15
+ try:
16
+ chain_id = int(s)
17
+ except ValueError as exc:
18
+ raise KeyError(f"Unknown chain: {chain}") from exc
19
+
20
+ for key, data in CHAINS.items():
21
+ if data.get("chainId") == chain_id:
22
+ return key
23
+
24
+ raise KeyError(f"Unknown chain id: {chain}")
25
+
26
+
27
+ def get_address(chain: str | int, id: str) -> str | None:
28
+ key = _chain_key(chain)
29
+ addresses = CHAINS[key].get("addresses") or {}
30
+ return addresses.get(id)
31
+
32
+ def get_permit2_addr(chain: str | int):
33
+ return get_address(chain, 'permit2')
34
+
35
+ def get_wrapped_gas_addr(chain: str | int):
36
+ return get_address(chain, 'wrappedGasToken')
37
+
38
+ def get_gas_addr(chain: str | int):
39
+ return get_address(chain, 'gasToken')
40
+
41
+ def chain(id: str | int) -> str | None:
42
+ """Return the chain key for a given chain id or chain key."""
43
+ return _chain_key(id)
44
+
45
+ if TYPE_CHECKING:
46
+ from enum import IntEnum
47
+ class ChainID(IntEnum):
48
+ _value_: int
49
+ else:
50
+ ChainID = enum.Enum(
51
+ 'ChainID',
52
+ {
53
+ name: data['chainId']
54
+ for name, data in CHAINS.items()
55
+ }
56
+ )
@@ -0,0 +1,48 @@
1
+ from .simple_types import (
2
+ EIP712OrderParameters,
3
+ EIP712TokenPermissions,
4
+ EIP712Domain,
5
+ EIP712Permit,
6
+ EIP712MMWitness,
7
+ EIP712MarginWitness,
8
+ EIP712CloseWithSwap,
9
+ )
10
+
11
+ from .nested_types import (
12
+ EIP712SignedOrder,
13
+ EIP712PermitWitnessTransferFromOrder,
14
+ EIP712PermitWitnessTransferFromMM,
15
+ EIP712PermitWitnessTransferFromMargin,
16
+ )
17
+
18
+ from .eip712_struct import EIP712Struct
19
+ from .eip712_auto import EIP712AutoStruct
20
+ from .escrow_utils import get_escrow_domain, get_close_with_swap_types, ESCROW_NAME, ESCROW_VERSION
21
+ from eth_utils import keccak
22
+
23
+
24
+ def eip712_digest(domain: "EIP712Struct", message: "EIP712Struct") -> bytes:
25
+ prefix = b"\x19\x01"
26
+ return keccak(prefix + domain.hash() + message.hash())
27
+
28
+
29
+ __all__ = [
30
+ "EIP712OrderParameters",
31
+ "EIP712TokenPermissions",
32
+ "EIP712SignedOrder",
33
+ "EIP712PermitWitnessTransferFromOrder",
34
+ "EIP712PermitWitnessTransferFromMM",
35
+ "EIP712PermitWitnessTransferFromMargin",
36
+ "EIP712MMWitness",
37
+ "EIP712MarginWitness",
38
+ "EIP712CloseWithSwap",
39
+ "EIP712Struct",
40
+ "EIP712Domain",
41
+ "EIP712Permit",
42
+ "EIP712AutoStruct",
43
+ "eip712_digest",
44
+ "get_escrow_domain",
45
+ "get_close_with_swap_types",
46
+ "ESCROW_NAME",
47
+ "ESCROW_VERSION",
48
+ ]
@@ -0,0 +1,212 @@
1
+ from __future__ import annotations
2
+ from web3 import Web3
3
+
4
+ import typing as _t
5
+ from typing import get_args, get_origin, ClassVar
6
+ from typing import Any, Dict, Type
7
+
8
+ from eth_abi import encode as abi_encode
9
+ from eth_utils import keccak
10
+ from pydantic import BaseModel
11
+
12
+ from .eip712_struct import EIP712Struct
13
+
14
+
15
+ def _struct_string(cls: Type["EIP712Struct"]) -> str:
16
+ sol_types = getattr(cls, "SOL_TYPES", {})
17
+ if not sol_types:
18
+ return cls.TYPE_STRING
19
+
20
+ hints = _t.get_type_hints(cls)
21
+ parts: list[str] = []
22
+
23
+ for field, t in hints.items():
24
+ if get_origin(t) is ClassVar:
25
+ continue
26
+ if isinstance(t, type) and issubclass(t, EIP712Struct):
27
+ parts.append(f"{sol_types.get(field, t.SOL_TYPE_NAME)} {field}")
28
+ else:
29
+ parts.append(f"{sol_types[field]} {field}")
30
+
31
+ return f"{cls.SOL_TYPE_NAME}(" + ",".join(parts) + ")"
32
+
33
+
34
+ def _gather_deps(
35
+ cls: Type[EIP712Struct], seen: set[Type[EIP712Struct]]
36
+ ) -> list[Type[EIP712Struct]]:
37
+ """Depth-first collect all user-defined nested structs (no duplicates)."""
38
+ deps: list[Type[EIP712Struct]] = []
39
+ for t in _t.get_type_hints(cls).values():
40
+ if get_origin(t) is ClassVar:
41
+ continue
42
+ if isinstance(t, type) and issubclass(t, EIP712Struct) and t not in seen:
43
+ seen.add(t)
44
+ deps.extend(_gather_deps(t, seen)) # recurse first
45
+ deps.append(t) # then this struct itself
46
+ return deps
47
+
48
+
49
+ def _canonical_type_string(cls: Type[EIP712Struct]) -> str:
50
+ """Root encodeType + ASCII-sorted dependencies (EIP-712 protocol definition)."""
51
+ deps = sorted(_gather_deps(cls, set()), key=lambda c: c.SOL_TYPE_NAME)
52
+ return _struct_string(cls) + "".join(_struct_string(d) for d in deps)
53
+
54
+
55
+ class _AutoMeta(type(BaseModel)):
56
+ """Populate TYPE_STRING / TYPE_STRUCT at **class-definition** time."""
57
+
58
+ def __init__(cls, name, bases, ns, **kw):
59
+ super().__init__(name, bases, ns, **kw)
60
+
61
+ if cls.__name__ == "EIP712AutoStruct":
62
+ return
63
+
64
+ cls.TYPE_STRING = _canonical_type_string(cls) # type: ignore[attr-defined]
65
+
66
+ hints = _t.get_type_hints(cls)
67
+ struct: list[Dict[str, str]] = []
68
+ for field, t in hints.items():
69
+ if get_origin(t) is ClassVar:
70
+ continue
71
+ if isinstance(t, type) and issubclass(t, EIP712Struct):
72
+ struct.append(
73
+ {
74
+ "name": field,
75
+ "type": getattr(cls, "SOL_TYPES", {}).get(field, t.SOL_TYPE_NAME),
76
+ }
77
+ )
78
+ else:
79
+ struct.append({"name": field, "type": cls.SOL_TYPES[field]})
80
+ cls.TYPE_STRUCT = struct # type: ignore[attr-defined]
81
+
82
+
83
+ def _coerce_bytes(val: Any) -> bytes:
84
+ """Accept raw bytes *or* '0x…' str/bytes and return binary bytes."""
85
+ if isinstance(val, (bytes, bytearray)):
86
+ b = bytes(val)
87
+ if b.startswith(b"0x"):
88
+ return Web3.to_bytes(hexstr=b.decode())
89
+ return b
90
+ if isinstance(val, str):
91
+ if val.startswith("0x"):
92
+ return Web3.to_bytes(hexstr=val)
93
+ return val.encode()
94
+ raise TypeError("Expected bytes-like or 0x-string")
95
+
96
+
97
+ def _hash_fixed_bytes_array(n: int, seq: list[bytes]) -> bytes:
98
+ """Hash a ``bytesN[]`` value-type array (N ≤ 32)."""
99
+ packed = bytearray(32 * len(seq))
100
+ for i, raw in enumerate(seq):
101
+ b = _coerce_bytes(raw)
102
+ if len(b) != n:
103
+ raise ValueError(
104
+ f"bytes{n} element length mismatch: Expected {n} bytes, got {len(b)}, value: {b.hex()}"
105
+ )
106
+ packed[i * 32 : i * 32 + n] = b
107
+ return keccak(bytes(packed))
108
+
109
+
110
+ def _hash_dynamic_bytes_array(seq: list[bytes]) -> bytes:
111
+ """Hash a ``bytes[]`` (or ``string[]``) array."""
112
+ packed = bytearray(32 * len(seq))
113
+ for i, raw in enumerate(seq):
114
+ packed[i * 32 : (i + 1) * 32] = keccak(_coerce_bytes(raw))
115
+ return keccak(bytes(packed))
116
+
117
+
118
+ def _hash_value_type_array(base: str, seq: list[Any]) -> bytes:
119
+ packed = b"".join(abi_encode([base], [x]) for x in seq)
120
+ return keccak(packed)
121
+
122
+
123
+ def _hash_array(sol_array_type: str, seq: list[Any]) -> bytes:
124
+ base = sol_array_type[:-2]
125
+
126
+ if base in ("bytes", "string"):
127
+ return _hash_dynamic_bytes_array(seq)
128
+
129
+ if base.startswith("bytes") and base != "bytes":
130
+ n = int(base[5:])
131
+ return _hash_fixed_bytes_array(n, seq)
132
+
133
+ return _hash_value_type_array(base, seq)
134
+
135
+
136
+ class EIP712AutoStruct(EIP712Struct, metaclass=_AutoMeta):
137
+ SOL_TYPES: ClassVar[Dict[str, str]] = {}
138
+ model_config = {"arbitrary_types_allowed": True}
139
+
140
+ @classmethod
141
+ def from_dict(cls, data: Dict[str, Any]) -> "EIP712AutoStruct":
142
+ field_types = _t.get_type_hints(cls)
143
+ kwargs = {}
144
+
145
+ for field in cls.__annotations__.keys():
146
+ if get_origin(field_types.get(field)) is ClassVar:
147
+ continue
148
+
149
+ if field not in data:
150
+ continue
151
+
152
+ field_value = data[field]
153
+ field_type = field_types.get(field)
154
+
155
+ if isinstance(field_type, type) and issubclass(field_type, EIP712Struct):
156
+ kwargs[field] = field_type.from_dict(field_value)
157
+ else:
158
+ if (
159
+ field_type is bytes
160
+ and isinstance(field_value, str)
161
+ and field_value.startswith("0x")
162
+ ):
163
+ kwargs[field] = Web3.to_bytes(hexstr=field_value)
164
+ elif (
165
+ field_type is str
166
+ and cls.SOL_TYPES.get(field) == "address"
167
+ and isinstance(field_value, str)
168
+ ):
169
+ kwargs[field] = Web3.to_checksum_address(field_value)
170
+ elif (
171
+ get_origin(field_type) in (list, _t.List)
172
+ and get_args(field_type)[0] is bytes
173
+ ):
174
+ cleaned = []
175
+ for elem in field_value:
176
+ if isinstance(elem, str) and elem.startswith("0x"):
177
+ cleaned.append(Web3.to_bytes(hexstr=elem))
178
+ else:
179
+ cleaned.append(elem)
180
+ kwargs[field] = cleaned
181
+ else:
182
+ kwargs[field] = field_value
183
+
184
+ return cls(**kwargs)
185
+
186
+ def hash(self) -> bytes:
187
+ abi_types: list[str] = ["bytes32"]
188
+ abi_vals: list[Any] = [self.type_hash]
189
+
190
+ for field in self.__class__.__annotations__.keys():
191
+ sol_type = self.SOL_TYPES[field]
192
+ val = getattr(self, field)
193
+
194
+ if isinstance(val, EIP712Struct):
195
+ abi_types.append("bytes32")
196
+ abi_vals.append(val.hash())
197
+ continue
198
+
199
+ if sol_type in ("bytes", "string"):
200
+ abi_types.append("bytes32")
201
+ abi_vals.append(keccak(_coerce_bytes(val)))
202
+ continue
203
+
204
+ if sol_type.endswith("[]"):
205
+ abi_types.append("bytes32")
206
+ abi_vals.append(_hash_array(sol_type, val))
207
+ continue
208
+
209
+ abi_types.append(sol_type)
210
+ abi_vals.append(val)
211
+
212
+ return keccak(abi_encode(abi_types, abi_vals))
@@ -0,0 +1,73 @@
1
+ from __future__ import annotations
2
+ from abc import ABC, abstractmethod
3
+ from typing import List, Dict, Any, ClassVar
4
+ from pydantic import BaseModel
5
+ from web3 import Web3
6
+ from eth_utils import keccak
7
+
8
+
9
+ class EIP712Struct(ABC, BaseModel):
10
+ SOL_TYPE_NAME: ClassVar[str]
11
+ TYPE_STRING: ClassVar[str]
12
+ TYPE_STRUCT: ClassVar[List[Dict]]
13
+
14
+ @property
15
+ def sol_type_name(self) -> str:
16
+ return self.__class__.SOL_TYPE_NAME
17
+
18
+ @property
19
+ def type_string(self) -> str:
20
+ return self.__class__.TYPE_STRING
21
+
22
+ @property
23
+ def type_struct(self) -> List[Dict]:
24
+ return self.__class__.TYPE_STRUCT
25
+
26
+ @property
27
+ def type_hash(self) -> bytes:
28
+ return keccak(text=self.__class__.TYPE_STRING)
29
+
30
+ @abstractmethod
31
+ def hash(self) -> bytes:
32
+ pass
33
+
34
+ @classmethod
35
+ def from_dict(cls, data: Dict[str, Any]) -> "EIP712Struct":
36
+ field_names = cls.__annotations__.keys()
37
+ kwargs = {field: data.get(field) for field in field_names if field in data}
38
+
39
+ for field, value in kwargs.items():
40
+ field_type = cls.__annotations__.get(field, None)
41
+
42
+ if (
43
+ isinstance(value, dict)
44
+ and hasattr(field_type, "__origin__")
45
+ and issubclass(field_type.__origin__, EIP712Struct)
46
+ ):
47
+ kwargs[field] = field_type.from_dict(value)
48
+
49
+ elif (
50
+ isinstance(value, dict)
51
+ and isinstance(field_type, type)
52
+ and issubclass(field_type, EIP712Struct)
53
+ ):
54
+ kwargs[field] = field_type.from_dict(value)
55
+
56
+ return cls(**kwargs)
57
+
58
+ @staticmethod
59
+ def _clean(value: Any):
60
+ if isinstance(value, (bytes, bytearray)):
61
+ return Web3.to_hex(value)
62
+ if isinstance(value, EIP712Struct):
63
+ return value.eip_signable_struct
64
+ if isinstance(value, (list, tuple)):
65
+ return [EIP712Struct._clean(v) for v in value]
66
+ if isinstance(value, dict):
67
+ return {k: EIP712Struct._clean(v) for k, v in value.items()}
68
+ return value
69
+
70
+ @property
71
+ def eip_signable_struct(self) -> Dict[str, Any]:
72
+ raw = self.model_dump(mode="python", exclude_none=True)
73
+ return {k: self._clean(v) for k, v in raw.items()}
@@ -0,0 +1,32 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Dict
4
+
5
+ from eth_utils import to_checksum_address
6
+
7
+ from .simple_types import EIP712CloseWithSwap
8
+
9
+
10
+ ESCROW_NAME = "Tristero Margin Position"
11
+ ESCROW_VERSION = "3"
12
+
13
+
14
+ def get_escrow_domain(chain_id: int, verifying_contract: str) -> Dict[str, Any]:
15
+ return {
16
+ "name": ESCROW_NAME,
17
+ "version": ESCROW_VERSION,
18
+ "chainId": int(chain_id),
19
+ "verifyingContract": to_checksum_address(verifying_contract),
20
+ }
21
+
22
+
23
+ def get_close_with_swap_types() -> Dict[str, Any]:
24
+ return {
25
+ "EIP712Domain": [
26
+ {"name": "name", "type": "string"},
27
+ {"name": "version", "type": "string"},
28
+ {"name": "chainId", "type": "uint256"},
29
+ {"name": "verifyingContract", "type": "address"},
30
+ ],
31
+ "CloseWithSwap": EIP712CloseWithSwap.TYPE_STRUCT,
32
+ }
@@ -0,0 +1,85 @@
1
+ from __future__ import annotations
2
+ from typing import List
3
+ from .eip712_auto import EIP712AutoStruct
4
+ from .simple_types import (
5
+ EIP712OrderParameters,
6
+ EIP712TokenPermissions,
7
+ EIP712MMWitness,
8
+ EIP712MarginWitness,
9
+ )
10
+
11
+
12
+ class EIP712SignedOrder(EIP712AutoStruct):
13
+ SOL_TYPE_NAME = "SignedOrder"
14
+
15
+ sender: str
16
+ parameters: EIP712OrderParameters
17
+ deadline: int
18
+ target: str
19
+ filler: str
20
+ orderType: str
21
+ customData: List[bytes]
22
+
23
+ SOL_TYPES = {
24
+ "sender": "address",
25
+ "parameters": "OrderParameters",
26
+ "deadline": "uint256",
27
+ "target": "address",
28
+ "filler": "address",
29
+ "orderType": "string",
30
+ "customData": "bytes[]",
31
+ }
32
+
33
+
34
+ class EIP712PermitWitnessTransferFromOrder(EIP712AutoStruct):
35
+ SOL_TYPE_NAME = "PermitWitnessTransferFrom"
36
+
37
+ permitted: EIP712TokenPermissions
38
+ spender: str
39
+ nonce: int
40
+ deadline: int
41
+ witness: EIP712SignedOrder
42
+
43
+ SOL_TYPES = {
44
+ "permitted": "TokenPermissions",
45
+ "spender": "address",
46
+ "nonce": "uint256",
47
+ "deadline": "uint256",
48
+ "witness": "SignedOrder",
49
+ }
50
+
51
+
52
+ class EIP712PermitWitnessTransferFromMM(EIP712AutoStruct):
53
+ SOL_TYPE_NAME = "PermitWitnessTransferFrom"
54
+
55
+ permitted: EIP712TokenPermissions
56
+ spender: str
57
+ nonce: int
58
+ deadline: int
59
+ witness: EIP712MMWitness
60
+
61
+ SOL_TYPES = {
62
+ "permitted": "TokenPermissions",
63
+ "spender": "address",
64
+ "nonce": "uint256",
65
+ "deadline": "uint256",
66
+ "witness": "MMWitness",
67
+ }
68
+
69
+
70
+ class EIP712PermitWitnessTransferFromMargin(EIP712AutoStruct):
71
+ SOL_TYPE_NAME = "PermitWitnessTransferFrom"
72
+
73
+ permitted: EIP712TokenPermissions
74
+ spender: str
75
+ nonce: int
76
+ deadline: int
77
+ witness: EIP712MarginWitness
78
+
79
+ SOL_TYPES = {
80
+ "permitted": "TokenPermissions",
81
+ "spender": "address",
82
+ "nonce": "uint256",
83
+ "deadline": "uint256",
84
+ "witness": "MarginWitness",
85
+ }