polyester-sdk 0.1.0a0__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.
Files changed (276) hide show
  1. polyester/__init__.py +32 -0
  2. polyester/_wire.py +55 -0
  3. polyester/auth.py +116 -0
  4. polyester/auth_interceptor.py +43 -0
  5. polyester/catalogs/__init__.py +157 -0
  6. polyester/catalogs/zipper.py +71 -0
  7. polyester/catalogs/zipper_supply.py +84 -0
  8. polyester/client.py +486 -0
  9. polyester/codecs/__init__.py +30 -0
  10. polyester/codecs/address_book.py +73 -0
  11. polyester/codecs/decode/__init__.py +95 -0
  12. polyester/codecs/decode/address_book.py +458 -0
  13. polyester/codecs/decode/api_keys.py +57 -0
  14. polyester/codecs/decode/auth.py +56 -0
  15. polyester/codecs/decode/balances.py +119 -0
  16. polyester/codecs/decode/common.py +8 -0
  17. polyester/codecs/decode/deposit.py +28 -0
  18. polyester/codecs/decode/guard_signer.py +78 -0
  19. polyester/codecs/decode/heatmap.py +9 -0
  20. polyester/codecs/decode/internal_transfers.py +16 -0
  21. polyester/codecs/decode/ledger_write.py +15 -0
  22. polyester/codecs/decode/lifecycle.py +72 -0
  23. polyester/codecs/decode/market_data.py +88 -0
  24. polyester/codecs/decode/market_overview.py +23 -0
  25. polyester/codecs/decode/orderbook.py +39 -0
  26. polyester/codecs/decode/orders.py +172 -0
  27. polyester/codecs/decode/policies.py +146 -0
  28. polyester/codecs/decode/resolve.py +21 -0
  29. polyester/codecs/decode/sub_accounts.py +166 -0
  30. polyester/codecs/decode/transfers.py +31 -0
  31. polyester/codecs/decode/triggers.py +111 -0
  32. polyester/codecs/decode/withdraw.py +11 -0
  33. polyester/codecs/decode/zipper.py +116 -0
  34. polyester/codecs/enums.py +25 -0
  35. polyester/codecs/guard_signer.py +55 -0
  36. polyester/codecs/heatmap.py +34 -0
  37. polyester/codecs/ledger.py +34 -0
  38. polyester/codecs/ledger_amounts.py +17 -0
  39. polyester/codecs/ledger_write.py +104 -0
  40. polyester/codecs/orderbook.py +20 -0
  41. polyester/codecs/orders.py +361 -0
  42. polyester/codecs/proto_build.py +28 -0
  43. polyester/codecs/proto_helpers.py +80 -0
  44. polyester/codecs/realtime_decode.py +186 -0
  45. polyester/codecs/scalars.py +130 -0
  46. polyester/codecs/triggers.py +135 -0
  47. polyester/codecs/wire_decode.py +652 -0
  48. polyester/codecs/withdraw.py +66 -0
  49. polyester/connect_transport.py +85 -0
  50. polyester/errors.py +59 -0
  51. polyester/gen/__init__.py +0 -0
  52. polyester/gen/auth/__init__.py +0 -0
  53. polyester/gen/auth/v1/__init__.py +0 -0
  54. polyester/gen/auth/v1/address_book_connect.py +976 -0
  55. polyester/gen/auth/v1/address_book_pb2.py +412 -0
  56. polyester/gen/auth/v1/address_book_pb2.pyi +640 -0
  57. polyester/gen/auth/v1/api_keys_connect.py +391 -0
  58. polyester/gen/auth/v1/api_keys_pb2.py +114 -0
  59. polyester/gen/auth/v1/api_keys_pb2.pyi +142 -0
  60. polyester/gen/auth/v1/auth_connect.py +261 -0
  61. polyester/gen/auth/v1/auth_pb2.py +81 -0
  62. polyester/gen/auth/v1/auth_pb2.pyi +158 -0
  63. polyester/gen/auth/v1/mfa_connect.py +1041 -0
  64. polyester/gen/auth/v1/mfa_pb2.py +231 -0
  65. polyester/gen/auth/v1/mfa_pb2.pyi +299 -0
  66. polyester/gen/auth/v1/policies_connect.py +846 -0
  67. polyester/gen/auth/v1/policies_pb2.py +218 -0
  68. polyester/gen/auth/v1/policies_pb2.pyi +436 -0
  69. polyester/gen/auth/v1/profile_connect.py +261 -0
  70. polyester/gen/auth/v1/profile_pb2.py +93 -0
  71. polyester/gen/auth/v1/profile_pb2.pyi +113 -0
  72. polyester/gen/auth/v1/resolve_connect.py +131 -0
  73. polyester/gen/auth/v1/resolve_pb2.py +55 -0
  74. polyester/gen/auth/v1/resolve_pb2.pyi +53 -0
  75. polyester/gen/auth/v1/social_verification_connect.py +261 -0
  76. polyester/gen/auth/v1/social_verification_pb2.py +80 -0
  77. polyester/gen/auth/v1/social_verification_pb2.pyi +127 -0
  78. polyester/gen/auth/v1/subaccounts_connect.py +888 -0
  79. polyester/gen/auth/v1/subaccounts_pb2.py +221 -0
  80. polyester/gen/auth/v1/subaccounts_pb2.pyi +369 -0
  81. polyester/gen/buf/__init__.py +0 -0
  82. polyester/gen/buf/validate/__init__.py +0 -0
  83. polyester/gen/buf/validate/validate_pb2.py +450 -0
  84. polyester/gen/buf/validate/validate_pb2.pyi +627 -0
  85. polyester/gen/chain/__init__.py +0 -0
  86. polyester/gen/chain/analytics/__init__.py +0 -0
  87. polyester/gen/chain/analytics/v1/__init__.py +0 -0
  88. polyester/gen/chain/analytics/v1/analytics_read_connect.py +261 -0
  89. polyester/gen/chain/analytics/v1/analytics_read_pb2.py +98 -0
  90. polyester/gen/chain/analytics/v1/analytics_read_pb2.pyi +132 -0
  91. polyester/gen/chain/deposit/__init__.py +0 -0
  92. polyester/gen/chain/deposit/v1/__init__.py +0 -0
  93. polyester/gen/chain/deposit/v1/deposit_connect.py +196 -0
  94. polyester/gen/chain/deposit/v1/deposit_pb2.py +62 -0
  95. polyester/gen/chain/deposit/v1/deposit_pb2.pyi +46 -0
  96. polyester/gen/chain/guard/__init__.py +0 -0
  97. polyester/gen/chain/guard/v1/__init__.py +0 -0
  98. polyester/gen/chain/guard/v1/guard_signer_connect.py +456 -0
  99. polyester/gen/chain/guard/v1/guard_signer_pb2.py +124 -0
  100. polyester/gen/chain/guard/v1/guard_signer_pb2.pyi +170 -0
  101. polyester/gen/chain/lifecycle/__init__.py +0 -0
  102. polyester/gen/chain/lifecycle/v1/__init__.py +0 -0
  103. polyester/gen/chain/lifecycle/v1/lifecycle_read_connect.py +261 -0
  104. polyester/gen/chain/lifecycle/v1/lifecycle_read_pb2.py +132 -0
  105. polyester/gen/chain/lifecycle/v1/lifecycle_read_pb2.pyi +396 -0
  106. polyester/gen/chain/lifecycle/v1/types_pb2.py +52 -0
  107. polyester/gen/chain/lifecycle/v1/types_pb2.pyi +118 -0
  108. polyester/gen/chain/withdraw/__init__.py +0 -0
  109. polyester/gen/chain/withdraw/v1/__init__.py +0 -0
  110. polyester/gen/chain/withdraw/v1/withdraw_connect.py +196 -0
  111. polyester/gen/chain/withdraw/v1/withdraw_pb2.py +79 -0
  112. polyester/gen/chain/withdraw/v1/withdraw_pb2.pyi +72 -0
  113. polyester/gen/chain/zipper/__init__.py +0 -0
  114. polyester/gen/chain/zipper/v1/__init__.py +0 -0
  115. polyester/gen/chain/zipper/v1/zipper_connect.py +131 -0
  116. polyester/gen/chain/zipper/v1/zipper_pb2.py +58 -0
  117. polyester/gen/chain/zipper/v1/zipper_pb2.pyi +130 -0
  118. polyester/gen/collab/__init__.py +0 -0
  119. polyester/gen/collab/v1/__init__.py +0 -0
  120. polyester/gen/collab/v1/whiteboard_connect.py +521 -0
  121. polyester/gen/collab/v1/whiteboard_pb2.py +155 -0
  122. polyester/gen/collab/v1/whiteboard_pb2.pyi +259 -0
  123. polyester/gen/gnostic/__init__.py +0 -0
  124. polyester/gen/gnostic/openapi/__init__.py +0 -0
  125. polyester/gen/gnostic/openapi/v3/__init__.py +0 -0
  126. polyester/gen/gnostic/openapi/v3/annotations_pb2.py +37 -0
  127. polyester/gen/gnostic/openapi/v3/annotations_pb2.pyi +14 -0
  128. polyester/gen/gnostic/openapi/v3/openapiv3_pb2.py +192 -0
  129. polyester/gen/gnostic/openapi/v3/openapiv3_pb2.pyi +895 -0
  130. polyester/gen/google/__init__.py +0 -0
  131. polyester/gen/google/api/__init__.py +0 -0
  132. polyester/gen/google/api/annotations_pb2.py +37 -0
  133. polyester/gen/google/api/annotations_pb2.pyi +8 -0
  134. polyester/gen/google/api/field_behavior_pb2.py +40 -0
  135. polyester/gen/google/api/field_behavior_pb2.pyi +29 -0
  136. polyester/gen/google/api/http_pb2.py +41 -0
  137. polyester/gen/google/api/http_pb2.pyi +47 -0
  138. polyester/gen/layout/__init__.py +0 -0
  139. polyester/gen/layout/v1/__init__.py +0 -0
  140. polyester/gen/layout/v1/layout_connect.py +1041 -0
  141. polyester/gen/layout/v1/layout_pb2.py +194 -0
  142. polyester/gen/layout/v1/layout_pb2.pyi +384 -0
  143. polyester/gen/ledger/__init__.py +0 -0
  144. polyester/gen/ledger/read/__init__.py +0 -0
  145. polyester/gen/ledger/read/v1/__init__.py +0 -0
  146. polyester/gen/ledger/read/v1/ledger_read_connect.py +456 -0
  147. polyester/gen/ledger/read/v1/ledger_read_pb2.py +113 -0
  148. polyester/gen/ledger/read/v1/ledger_read_pb2.pyi +318 -0
  149. polyester/gen/ledger/v1/__init__.py +0 -0
  150. polyester/gen/ledger/v1/catalog_pb2.py +39 -0
  151. polyester/gen/ledger/v1/catalog_pb2.pyi +43 -0
  152. polyester/gen/ledger/write/v1/ledger_write_connect.py +326 -0
  153. polyester/gen/ledger/write/v1/ledger_write_pb2.py +55 -0
  154. polyester/gen/ledger/write/v1/ledger_write_pb2.pyi +98 -0
  155. polyester/gen/marketdata/__init__.py +0 -0
  156. polyester/gen/marketdata/v1/__init__.py +0 -0
  157. polyester/gen/marketdata/v1/heatmap_connect.py +131 -0
  158. polyester/gen/marketdata/v1/heatmap_pb2.py +92 -0
  159. polyester/gen/marketdata/v1/heatmap_pb2.pyi +194 -0
  160. polyester/gen/marketdata/v1/marketdata_connect.py +326 -0
  161. polyester/gen/marketdata/v1/marketdata_pb2.py +116 -0
  162. polyester/gen/marketdata/v1/marketdata_pb2.pyi +289 -0
  163. polyester/gen/marketoverview/__init__.py +0 -0
  164. polyester/gen/marketoverview/v1/__init__.py +0 -0
  165. polyester/gen/marketoverview/v1/marketoverview_connect.py +131 -0
  166. polyester/gen/marketoverview/v1/marketoverview_pb2.py +77 -0
  167. polyester/gen/marketoverview/v1/marketoverview_pb2.pyi +144 -0
  168. polyester/gen/orderbook/__init__.py +0 -0
  169. polyester/gen/orderbook/v1/__init__.py +0 -0
  170. polyester/gen/orderbook/v1/orderbook_connect.py +131 -0
  171. polyester/gen/orderbook/v1/orderbook_pb2.py +58 -0
  172. polyester/gen/orderbook/v1/orderbook_pb2.pyi +86 -0
  173. polyester/gen/orders/__init__.py +0 -0
  174. polyester/gen/orders/v1/__init__.py +0 -0
  175. polyester/gen/orders/v1/orders_connect.py +586 -0
  176. polyester/gen/orders/v1/orders_pb2.py +257 -0
  177. polyester/gen/orders/v1/orders_pb2.pyi +634 -0
  178. polyester/gen/orders/v1/orders_read_connect.py +326 -0
  179. polyester/gen/orders/v1/orders_read_pb2.py +145 -0
  180. polyester/gen/orders/v1/orders_read_pb2.pyi +347 -0
  181. polyester/gen/polychart/__init__.py +0 -0
  182. polyester/gen/polychart/events/__init__.py +0 -0
  183. polyester/gen/polychart/events/v1/__init__.py +0 -0
  184. polyester/gen/polychart/events/v1/polychart_events_pb2.py +50 -0
  185. polyester/gen/polychart/events/v1/polychart_events_pb2.pyi +71 -0
  186. polyester/gen/polychart/v1/__init__.py +0 -0
  187. polyester/gen/polychart/v1/polychart_connect.py +1041 -0
  188. polyester/gen/polychart/v1/polychart_pb2.py +171 -0
  189. polyester/gen/polychart/v1/polychart_pb2.pyi +355 -0
  190. polyester/gen/polyester/__init__.py +0 -0
  191. polyester/gen/polyester/api/__init__.py +0 -0
  192. polyester/gen/polyester/api/options_pb2.py +36 -0
  193. polyester/gen/polyester/api/options_pb2.pyi +9 -0
  194. polyester/gen/polyester/api/validation/__init__.py +0 -0
  195. polyester/gen/polyester/api/validation/v1/__init__.py +0 -0
  196. polyester/gen/polyester/api/validation/v1/predefined_string_rules_pb2.py +42 -0
  197. polyester/gen/polyester/api/validation/v1/predefined_string_rules_pb2.pyi +11 -0
  198. polyester/gen/polyester/type/__init__.py +0 -0
  199. polyester/gen/polyester/type/v1/__init__.py +0 -0
  200. polyester/gen/polyester/type/v1/u128_pb2.py +37 -0
  201. polyester/gen/polyester/type/v1/u128_pb2.pyi +13 -0
  202. polyester/gen/tradews/__init__.py +0 -0
  203. polyester/gen/tradews/v1/__init__.py +0 -0
  204. polyester/gen/tradews/v1/trade_ws_pb2.py +52 -0
  205. polyester/gen/tradews/v1/trade_ws_pb2.pyi +91 -0
  206. polyester/gen/transfer/__init__.py +0 -0
  207. polyester/gen/transfer/v1/__init__.py +0 -0
  208. polyester/gen/transfer/v1/internal_transfer_connect.py +131 -0
  209. polyester/gen/transfer/v1/internal_transfer_pb2.py +66 -0
  210. polyester/gen/transfer/v1/internal_transfer_pb2.pyi +71 -0
  211. polyester/gen/triggers/__init__.py +0 -0
  212. polyester/gen/triggers/v1/__init__.py +0 -0
  213. polyester/gen/triggers/v1/triggers_connect.py +586 -0
  214. polyester/gen/triggers/v1/triggers_pb2.py +171 -0
  215. polyester/gen/triggers/v1/triggers_pb2.pyi +430 -0
  216. polyester/market_overview/__init__.py +3 -0
  217. polyester/market_overview/subscription.py +49 -0
  218. polyester/models/__init__.py +255 -0
  219. polyester/models/address_book.py +212 -0
  220. polyester/models/auth.py +52 -0
  221. polyester/models/common.py +66 -0
  222. polyester/models/guard_signer.py +34 -0
  223. polyester/models/market.py +46 -0
  224. polyester/models/policies.py +71 -0
  225. polyester/models/realtime.py +17 -0
  226. polyester/models/sub_accounts.py +93 -0
  227. polyester/models/trading.py +306 -0
  228. polyester/models/zipper.py +114 -0
  229. polyester/orderbook/__init__.py +17 -0
  230. polyester/orderbook/local_book.py +166 -0
  231. polyester/orderbook/subscription.py +55 -0
  232. polyester/py.typed +1 -0
  233. polyester/realtime/__init__.py +16 -0
  234. polyester/realtime/auth.py +66 -0
  235. polyester/realtime/client.py +234 -0
  236. polyester/realtime/snapshot_then_stream.py +144 -0
  237. polyester/services/__init__.py +59 -0
  238. polyester/services/_base.py +47 -0
  239. polyester/services/_generated.py +84 -0
  240. polyester/services/_realtime_subscribe.py +42 -0
  241. polyester/services/_scope.py +92 -0
  242. polyester/services/_symbols.py +24 -0
  243. polyester/services/address_book.py +424 -0
  244. polyester/services/api_keys.py +173 -0
  245. polyester/services/auth.py +25 -0
  246. polyester/services/balances.py +171 -0
  247. polyester/services/chain_analytics.py +104 -0
  248. polyester/services/deposit.py +67 -0
  249. polyester/services/guard_signer.py +191 -0
  250. polyester/services/heatmap.py +143 -0
  251. polyester/services/internal_transfers.py +77 -0
  252. polyester/services/layout.py +200 -0
  253. polyester/services/ledger_write.py +128 -0
  254. polyester/services/lifecycle.py +166 -0
  255. polyester/services/market_data.py +251 -0
  256. polyester/services/market_overview.py +131 -0
  257. polyester/services/orderbook.py +233 -0
  258. polyester/services/orders.py +429 -0
  259. polyester/services/policies.py +450 -0
  260. polyester/services/polychart.py +208 -0
  261. polyester/services/profile.py +70 -0
  262. polyester/services/resolve.py +53 -0
  263. polyester/services/social_verification.py +74 -0
  264. polyester/services/sub_accounts.py +398 -0
  265. polyester/services/trades.py +78 -0
  266. polyester/services/transfers.py +66 -0
  267. polyester/services/triggers.py +299 -0
  268. polyester/services/whiteboard.py +139 -0
  269. polyester/services/withdraw.py +144 -0
  270. polyester/services/zipper.py +85 -0
  271. polyester/sync_subscribe.py +86 -0
  272. polyester/transport.py +94 -0
  273. polyester_sdk-0.1.0a0.dist-info/METADATA +236 -0
  274. polyester_sdk-0.1.0a0.dist-info/RECORD +276 -0
  275. polyester_sdk-0.1.0a0.dist-info/WHEEL +4 -0
  276. polyester_sdk-0.1.0a0.dist-info/licenses/LICENSE +7 -0
polyester/__init__.py ADDED
@@ -0,0 +1,32 @@
1
+ from polyester.client import AsyncPolyester, Polyester
2
+ from polyester.codecs.ledger_amounts import LEDGER_SCALE, format_ledger_u128
3
+ from polyester.errors import (
4
+ PolyesterApiError,
5
+ PolyesterAuthError,
6
+ PolyesterError,
7
+ PolyesterRateLimitError,
8
+ PolyesterRealtimeError,
9
+ PolyesterRouteNotFoundError,
10
+ PolyesterServerError,
11
+ PolyesterTransportError,
12
+ PolyesterValidationError,
13
+ )
14
+
15
+ __version__ = "0.1.0a0"
16
+
17
+ __all__ = [
18
+ "AsyncPolyester",
19
+ "LEDGER_SCALE",
20
+ "Polyester",
21
+ "PolyesterApiError",
22
+ "PolyesterAuthError",
23
+ "PolyesterError",
24
+ "PolyesterRateLimitError",
25
+ "PolyesterRealtimeError",
26
+ "PolyesterRouteNotFoundError",
27
+ "PolyesterServerError",
28
+ "PolyesterTransportError",
29
+ "PolyesterValidationError",
30
+ "__version__",
31
+ "format_ledger_u128",
32
+ ]
polyester/_wire.py ADDED
@@ -0,0 +1,55 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from connectrpc.errors import ConnectError
6
+ from google.protobuf.json_format import MessageToDict
7
+ from google.protobuf.message import Message
8
+
9
+ from polyester.errors import (
10
+ PolyesterApiError,
11
+ PolyesterAuthError,
12
+ PolyesterRouteNotFoundError,
13
+ PolyesterServerError,
14
+ PolyesterTransportError,
15
+ )
16
+ from polyester.gen.orders.v1 import orders_pb2
17
+
18
+ _ROUTE_NOT_FOUND_MESSAGES = frozenset(
19
+ {"not found", "404 page not found", "404 not found"}
20
+ )
21
+
22
+
23
+ def protobuf_to_public_dict(message: Message) -> dict[str, Any]:
24
+ return MessageToDict(
25
+ message,
26
+ preserving_proto_field_name=False,
27
+ always_print_fields_with_no_presence=True,
28
+ )
29
+
30
+
31
+ def map_connect_error(exc: ConnectError):
32
+ if exc.details:
33
+ for detail in exc.details:
34
+ full_name = detail.DESCRIPTOR.full_name
35
+ if full_name.endswith("ErrorDetail") and hasattr(detail, "code"):
36
+ code_name = orders_pb2.ErrorCode.Name(detail.code)
37
+ if (
38
+ "POLICY" in code_name
39
+ or "UNAUTHENTICATED" in code_name
40
+ or "PERMISSION" in code_name
41
+ ):
42
+ return PolyesterAuthError(exc.message)
43
+ if code_name.startswith("ERROR_CODE_"):
44
+ return PolyesterApiError(exc.message, code=code_name)
45
+ code = exc.code.value
46
+ if code in ("unauthenticated", "permission_denied"):
47
+ return PolyesterAuthError(exc.message)
48
+ if code in ("unavailable", "internal"):
49
+ return PolyesterServerError(exc.message)
50
+ if code == "deadline_exceeded":
51
+ return PolyesterTransportError(exc.message)
52
+ message = (exc.message or "").strip().lower()
53
+ if code == "unimplemented" and message in _ROUTE_NOT_FOUND_MESSAGES:
54
+ return PolyesterRouteNotFoundError()
55
+ return PolyesterApiError(exc.message, code=code)
polyester/auth.py ADDED
@@ -0,0 +1,116 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import os
5
+ import time
6
+ from dataclasses import dataclass
7
+ from urllib.parse import parse_qsl, quote, urlsplit
8
+
9
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
10
+
11
+ from polyester.errors import PolyesterAuthError
12
+
13
+ API_KEY_ID_ENV = "POLYESTER_API_KEY_ID"
14
+ API_PRIVATE_KEY_ENV = "POLYESTER_API_PRIVATE_KEY"
15
+ ACCOUNT_ID_ENV = "POLYESTER_ACCOUNT_ID"
16
+
17
+
18
+ @dataclass(frozen=True, slots=True)
19
+ class ApiKeyCredentials:
20
+ key_id: str
21
+ private_key: bytes
22
+
23
+
24
+ def load_api_key_credentials(
25
+ *,
26
+ api_key_id: str | None = None,
27
+ api_private_key: str | bytes | None = None,
28
+ from_env: bool = True,
29
+ ) -> ApiKeyCredentials | None:
30
+ key_id = api_key_id
31
+ private = api_private_key
32
+ if from_env:
33
+ key_id = key_id or os.getenv(API_KEY_ID_ENV)
34
+ private = private if private is not None else os.getenv(API_PRIVATE_KEY_ENV)
35
+ if not key_id and not private:
36
+ return None
37
+ if not key_id or private is None:
38
+ if from_env:
39
+ message = "Both POLYESTER_API_KEY_ID and POLYESTER_API_PRIVATE_KEY are required"
40
+ else:
41
+ message = "Both api_key_id and api_private_key are required"
42
+ raise PolyesterAuthError(
43
+ message
44
+ )
45
+ return ApiKeyCredentials(key_id=key_id, private_key=normalize_private_key(private))
46
+
47
+
48
+ def normalize_private_key(value: str | bytes) -> bytes:
49
+ if isinstance(value, bytes):
50
+ private = value
51
+ else:
52
+ try:
53
+ private = bytes.fromhex(value.strip())
54
+ except ValueError as exc:
55
+ raise PolyesterAuthError(
56
+ "API private key must be a valid hex string or raw bytes"
57
+ ) from exc
58
+ if len(private) != 32:
59
+ raise PolyesterAuthError("Ed25519 API private key must be exactly 32 bytes")
60
+ return private
61
+
62
+
63
+ def generate_ed25519_keypair() -> tuple[bytes, bytes]:
64
+ """Generate a new Ed25519 keypair (public, secret), each 32 bytes."""
65
+ private_key = Ed25519PrivateKey.generate()
66
+ public_key = private_key.public_key()
67
+ return public_key.public_bytes_raw(), private_key.private_bytes_raw()
68
+
69
+
70
+ def sha256_hex(body: bytes) -> str:
71
+ return hashlib.sha256(body).hexdigest()
72
+
73
+
74
+ def canonical_query(url: str) -> str:
75
+ pairs = []
76
+ for key, value in parse_qsl(urlsplit(url).query, keep_blank_values=True):
77
+ pairs.append(f"{quote(key, safe='')}={quote(value, safe='')}")
78
+ pairs.sort()
79
+ return "&".join(pairs)
80
+
81
+
82
+ def canonical_signing_string(
83
+ *,
84
+ timestamp_ms: str,
85
+ method: str,
86
+ url: str,
87
+ body: bytes,
88
+ ) -> str:
89
+ pathname = urlsplit(url).path or "/"
90
+ return "\n".join(
91
+ [timestamp_ms, method.upper(), pathname, canonical_query(url), sha256_hex(body)]
92
+ )
93
+
94
+
95
+ def sign_request(
96
+ credentials: ApiKeyCredentials,
97
+ *,
98
+ method: str,
99
+ url: str,
100
+ body: bytes = b"",
101
+ timestamp_ms: str | None = None,
102
+ ) -> dict[str, str]:
103
+ timestamp = timestamp_ms or str(int(time.time() * 1000))
104
+ canonical = canonical_signing_string(
105
+ timestamp_ms=timestamp,
106
+ method=method,
107
+ url=url,
108
+ body=body,
109
+ )
110
+ key = Ed25519PrivateKey.from_private_bytes(credentials.private_key)
111
+ signature = key.sign(canonical.encode("utf-8")).hex()
112
+ return {
113
+ "X-API-KEY-ID": credentials.key_id,
114
+ "X-API-TIMESTAMP": timestamp,
115
+ "X-API-SIGNATURE": signature,
116
+ }
@@ -0,0 +1,43 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Awaitable, Callable
4
+
5
+ from connectrpc.codec import Codec
6
+ from connectrpc.interceptor import UnaryInterceptor
7
+ from connectrpc.request import RequestContext
8
+ from google.protobuf.message import Message
9
+
10
+ from polyester.auth import ApiKeyCredentials, sign_request
11
+
12
+
13
+ class ApiKeyAuthUnaryInterceptor(UnaryInterceptor):
14
+ """Attach Polyester API-key headers to generated Connect unary calls."""
15
+
16
+ def __init__(
17
+ self,
18
+ credentials: ApiKeyCredentials,
19
+ *,
20
+ base_url: str,
21
+ codec: Codec,
22
+ ) -> None:
23
+ self._credentials = credentials
24
+ self._base_url = base_url.rstrip("/")
25
+ self._codec = codec
26
+
27
+ async def intercept_unary(
28
+ self,
29
+ call_next: Callable[[Message, RequestContext], Awaitable[Message]],
30
+ request: Message,
31
+ ctx: RequestContext,
32
+ ) -> Message:
33
+ info = ctx.method()
34
+ url = f"{self._base_url}/{info.service_name}/{info.name}"
35
+ body = self._codec.encode(request)
36
+ headers = sign_request(
37
+ self._credentials,
38
+ method=ctx.http_method(),
39
+ url=url,
40
+ body=body,
41
+ )
42
+ ctx.request_headers().update(headers)
43
+ return await call_next(request, ctx)
@@ -0,0 +1,157 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Any
5
+
6
+ from polyester.catalogs.zipper import build_zipper_catalog_data
7
+ from polyester.catalogs.zipper_supply import patch_zipper_catalog_supply
8
+ from polyester.models.zipper import (
9
+ DepositWithdrawConfig,
10
+ ZippedAssetSupplyUpdate,
11
+ ZipperCatalogData,
12
+ )
13
+
14
+
15
+ @dataclass(slots=True)
16
+ class CatalogManager:
17
+ spot_config: dict[str, Any] = field(default_factory=dict)
18
+ zipper: ZipperCatalogData | None = None
19
+ deposit_withdraw_config: DepositWithdrawConfig | None = None
20
+ _legacy_zipper_raw: dict[str, Any] = field(default_factory=dict)
21
+
22
+ @property
23
+ def zipper_config(self) -> dict[str, Any]:
24
+ """Backward-compatible raw dict view of the typed zipper catalog."""
25
+ if self.deposit_withdraw_config is None:
26
+ return {}
27
+ config = self.deposit_withdraw_config
28
+ return {
29
+ "chains": [msgspec_to_dict(chain) for chain in config.chains],
30
+ "assets": [
31
+ {
32
+ **msgspec_to_dict(asset),
33
+ "variants": [msgspec_to_dict(variant) for variant in asset.variants],
34
+ }
35
+ for asset in config.assets
36
+ ],
37
+ "contracts": [msgspec_to_dict(item) for item in config.contracts],
38
+ "polyesterChainId": config.polyester_chain_id,
39
+ "tsSec": config.ts_ms // 1000 if config.ts_ms else 0,
40
+ }
41
+
42
+ def hydrate_spot_config(self, config: dict[str, Any]) -> None:
43
+ self.spot_config = config
44
+
45
+ def hydrate_zipper_config(
46
+ self,
47
+ config: DepositWithdrawConfig | dict[str, Any],
48
+ ) -> None:
49
+ if isinstance(config, DepositWithdrawConfig):
50
+ self.deposit_withdraw_config = config
51
+ self.zipper = build_zipper_catalog_data(config)
52
+ return
53
+ self.deposit_withdraw_config = None
54
+ self.zipper = None
55
+ self._legacy_zipper_raw = config
56
+
57
+ def hydrate_deposit_withdraw_config(self, config: DepositWithdrawConfig) -> None:
58
+ self.hydrate_zipper_config(config)
59
+
60
+ def symbol_id_for_symbol(self, symbol: str) -> int | None:
61
+ for pair in self._pairs():
62
+ if pair.get("symbol") == symbol:
63
+ value = pair.get("symbol_id") or pair.get("symbolId")
64
+ return int(value) if value is not None else None
65
+ return None
66
+
67
+ def base_quantity_scale_for_symbol(self, symbol: str) -> int:
68
+ for pair in self._pairs():
69
+ if pair.get("symbol") == symbol:
70
+ value = (
71
+ pair.get("base_quantity_scale")
72
+ or pair.get("baseQuantityScale")
73
+ or pair.get("qtyScale")
74
+ )
75
+ return int(value) if value is not None else 8
76
+ return 8
77
+
78
+ def orderbook_price_buckets_for_symbol(self, symbol: str) -> list[str]:
79
+ pair = self._pair_for_symbol(symbol)
80
+ if pair is None:
81
+ return []
82
+ marketdata = pair.get("marketdata") or pair.get("marketData") or {}
83
+ buckets = marketdata.get("orderbook_price_buckets") or marketdata.get(
84
+ "orderbookPriceBuckets"
85
+ )
86
+ if not isinstance(buckets, list):
87
+ return []
88
+ result: list[str] = []
89
+ for value in buckets:
90
+ if isinstance(value, (int, float)):
91
+ text = format(value, "f").rstrip("0").rstrip(".") or "0"
92
+ result.append(text)
93
+ elif value is not None:
94
+ result.append(str(value))
95
+ return result
96
+
97
+ def ledger_id_for_asset(self, asset_symbol: str) -> int | None:
98
+ catalog = self.zipper
99
+ if catalog is not None:
100
+ for asset in catalog.assets:
101
+ if asset.asset == asset_symbol:
102
+ return asset.ledger_id or None
103
+ raw = self._legacy_zipper_raw or self.zipper_config
104
+ for row in raw.get("assets") or []:
105
+ symbol = row.get("asset") or row.get("code")
106
+ if symbol == asset_symbol:
107
+ value = row.get("ledgerId") or row.get("ledger_id")
108
+ return int(value) if value is not None else None
109
+ return None
110
+
111
+ def quantity_scale_for_asset(self, asset_symbol: str) -> int | None:
112
+ catalog = self.zipper
113
+ if catalog is not None:
114
+ for asset in catalog.assets:
115
+ if asset.asset == asset_symbol:
116
+ return asset.quantity_scale
117
+ raw = self._legacy_zipper_raw or self.zipper_config
118
+ for row in raw.get("assets") or []:
119
+ symbol = row.get("asset") or row.get("code")
120
+ if symbol == asset_symbol:
121
+ value = row.get("quantityScale") or row.get("quantity_scale")
122
+ return int(value) if value is not None else None
123
+ return None
124
+
125
+ def quantity_scale_for_zipped_asset_id(self, zipped_asset_id: int) -> int:
126
+ catalog = self.zipper
127
+ if catalog is not None:
128
+ for asset in catalog.assets:
129
+ for chain in asset.chains:
130
+ if chain.zipped_asset_id == zipped_asset_id:
131
+ return asset.quantity_scale
132
+ return 18
133
+
134
+ def patch_zipper_supply(self, updates: list[ZippedAssetSupplyUpdate]) -> bool:
135
+ if self.zipper is None or not updates:
136
+ return False
137
+ patched = patch_zipper_catalog_supply(self.zipper, updates)
138
+ if patched is self.zipper:
139
+ return False
140
+ self.zipper = patched
141
+ return True
142
+
143
+ def _pair_for_symbol(self, symbol: str) -> dict[str, Any] | None:
144
+ for pair in self._pairs():
145
+ if pair.get("symbol") == symbol:
146
+ return pair
147
+ return None
148
+
149
+ def _pairs(self) -> list[dict[str, Any]]:
150
+ pairs = self.spot_config.get("pairs") or self.spot_config.get("symbols") or []
151
+ return [pair for pair in pairs if isinstance(pair, dict)]
152
+
153
+
154
+ def msgspec_to_dict(value: object) -> dict[str, Any]:
155
+ import msgspec
156
+
157
+ return msgspec.to_builtins(value)
@@ -0,0 +1,71 @@
1
+ from __future__ import annotations
2
+
3
+ from polyester.models.zipper import (
4
+ DepositWithdrawConfig,
5
+ ZipperAssetConfig,
6
+ ZipperCatalogData,
7
+ ZipperChainConfig,
8
+ ZipperEnrichedAssetChain,
9
+ ZipperEnrichedAssetConfig,
10
+ )
11
+
12
+
13
+ def build_zipper_catalog_data(config: DepositWithdrawConfig) -> ZipperCatalogData:
14
+ chains_by_id = {chain.chain_id: chain for chain in config.chains}
15
+ enriched_assets: list[ZipperEnrichedAssetConfig] = []
16
+ for asset in config.assets:
17
+ enriched_assets.append(
18
+ ZipperEnrichedAssetConfig(
19
+ asset=asset.asset,
20
+ ledger_id=asset.ledger_id,
21
+ name=asset.name,
22
+ icon=asset.icon,
23
+ quantity_scale=asset.quantity_scale,
24
+ quantity_display_decimals=asset.quantity_display_decimals,
25
+ u_asset_id=asset.u_asset_id,
26
+ chains=_enrich_asset_chains(asset, chains_by_id),
27
+ )
28
+ )
29
+ return ZipperCatalogData(
30
+ chains=list(config.chains),
31
+ assets=enriched_assets,
32
+ contracts=list(config.contracts),
33
+ ts_ms=config.ts_ms,
34
+ )
35
+
36
+
37
+ def _enrich_asset_chains(
38
+ asset: ZipperAssetConfig,
39
+ chains_by_id: dict[int, ZipperChainConfig],
40
+ ) -> list[ZipperEnrichedAssetChain]:
41
+ enriched: list[ZipperEnrichedAssetChain] = []
42
+ for variant in asset.variants:
43
+ chain = chains_by_id.get(variant.chain_id)
44
+ if chain is None:
45
+ continue
46
+ enriched.append(
47
+ ZipperEnrichedAssetChain(
48
+ chain_id=chain.chain_id,
49
+ code=chain.code,
50
+ name=chain.name,
51
+ native_chain_id=chain.native_chain_id,
52
+ native_currency_symbol=chain.native_currency_symbol,
53
+ explorer_url=chain.explorer_url,
54
+ icon=chain.icon,
55
+ required_confirmations=chain.required_confirmations,
56
+ confirmation_time_seconds=chain.confirmation_time_seconds,
57
+ is_case_sensitive=chain.is_case_sensitive,
58
+ min_address_length=chain.min_address_length,
59
+ max_address_length=chain.max_address_length,
60
+ zipped_asset_id=variant.zipped_asset_id,
61
+ is_native_asset=variant.is_native_asset,
62
+ network_fee=variant.network_fee,
63
+ network_fee_ts_sec=variant.network_fee_ts_sec,
64
+ deposit_min_amount=variant.deposit_min_amount,
65
+ withdraw_min_amount=variant.withdraw_min_amount,
66
+ supply=variant.supply,
67
+ source_token=variant.source_token,
68
+ z_token=variant.z_token,
69
+ )
70
+ )
71
+ return enriched
@@ -0,0 +1,84 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+
5
+ from polyester.models.zipper import (
6
+ ZippedAssetSupplyUpdate,
7
+ ZipperCatalogData,
8
+ ZipperEnrichedAssetChain,
9
+ ZipperEnrichedAssetConfig,
10
+ )
11
+
12
+
13
+ def patch_zipper_catalog_supply(
14
+ catalog: ZipperCatalogData,
15
+ updates: list[ZippedAssetSupplyUpdate],
16
+ ) -> ZipperCatalogData:
17
+ if not updates:
18
+ return catalog
19
+
20
+ supply_by_id = {item.zipped_asset_id: item.supply for item in updates}
21
+ changed = False
22
+ next_assets: list[ZipperEnrichedAssetConfig] = []
23
+
24
+ for asset in catalog.assets:
25
+ asset_changed = False
26
+ next_chains: list[ZipperEnrichedAssetChain] = []
27
+ for chain in asset.chains:
28
+ supply = supply_by_id.get(chain.zipped_asset_id)
29
+ if supply is None or supply == chain.supply:
30
+ next_chains.append(chain)
31
+ continue
32
+ asset_changed = True
33
+ changed = True
34
+ next_chains.append(
35
+ ZipperEnrichedAssetChain(
36
+ chain_id=chain.chain_id,
37
+ code=chain.code,
38
+ name=chain.name,
39
+ native_chain_id=chain.native_chain_id,
40
+ native_currency_symbol=chain.native_currency_symbol,
41
+ explorer_url=chain.explorer_url,
42
+ icon=chain.icon,
43
+ required_confirmations=chain.required_confirmations,
44
+ confirmation_time_seconds=chain.confirmation_time_seconds,
45
+ is_case_sensitive=chain.is_case_sensitive,
46
+ min_address_length=chain.min_address_length,
47
+ max_address_length=chain.max_address_length,
48
+ zipped_asset_id=chain.zipped_asset_id,
49
+ is_native_asset=chain.is_native_asset,
50
+ network_fee=chain.network_fee,
51
+ network_fee_ts_sec=chain.network_fee_ts_sec,
52
+ deposit_min_amount=chain.deposit_min_amount,
53
+ withdraw_min_amount=chain.withdraw_min_amount,
54
+ supply=supply,
55
+ source_token=chain.source_token,
56
+ z_token=chain.z_token,
57
+ )
58
+ )
59
+ if asset_changed:
60
+ next_assets.append(
61
+ ZipperEnrichedAssetConfig(
62
+ asset=asset.asset,
63
+ ledger_id=asset.ledger_id,
64
+ name=asset.name,
65
+ icon=asset.icon,
66
+ quantity_scale=asset.quantity_scale,
67
+ quantity_display_decimals=asset.quantity_display_decimals,
68
+ u_asset_id=asset.u_asset_id,
69
+ chains=next_chains,
70
+ )
71
+ )
72
+ else:
73
+ next_assets.append(asset)
74
+
75
+ if not changed:
76
+ return catalog
77
+
78
+ ts_ms = int(time.time() * 1000)
79
+ return ZipperCatalogData(
80
+ chains=list(catalog.chains),
81
+ assets=next_assets,
82
+ contracts=list(catalog.contracts),
83
+ ts_ms=ts_ms,
84
+ )