bloklake 0.1.0__tar.gz

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 (32) hide show
  1. bloklake-0.1.0/.gitignore +6 -0
  2. bloklake-0.1.0/LICENSE +21 -0
  3. bloklake-0.1.0/PKG-INFO +161 -0
  4. bloklake-0.1.0/README.md +144 -0
  5. bloklake-0.1.0/pyproject.toml +32 -0
  6. bloklake-0.1.0/scripts/generate.py +110 -0
  7. bloklake-0.1.0/scripts/schema.json +814 -0
  8. bloklake-0.1.0/src/bloklake/__init__.py +16 -0
  9. bloklake-0.1.0/src/bloklake/client.py +254 -0
  10. bloklake-0.1.0/src/bloklake/decode.py +67 -0
  11. bloklake-0.1.0/src/bloklake/errors.py +25 -0
  12. bloklake-0.1.0/src/bloklake/fields.py +317 -0
  13. bloklake-0.1.0/src/bloklake/py.typed +0 -0
  14. bloklake-0.1.0/src/bloklake/types.py +135 -0
  15. bloklake-0.1.0/tests/fixtures/eth-all-fields.arrows +0 -0
  16. bloklake-0.1.0/tests/fixtures/eth-all-fields.json +1 -0
  17. bloklake-0.1.0/tests/fixtures/eth-all-fields.request.json +1 -0
  18. bloklake-0.1.0/tests/fixtures/eth-empty.arrows +0 -0
  19. bloklake-0.1.0/tests/fixtures/eth-empty.json +1 -0
  20. bloklake-0.1.0/tests/fixtures/eth-empty.request.json +1 -0
  21. bloklake-0.1.0/tests/fixtures/eth-logs-only.arrows +0 -0
  22. bloklake-0.1.0/tests/fixtures/eth-logs-only.json +1 -0
  23. bloklake-0.1.0/tests/fixtures/eth-logs-only.request.json +1 -0
  24. bloklake-0.1.0/tests/fixtures/rh-all-fields.arrows +0 -0
  25. bloklake-0.1.0/tests/fixtures/rh-all-fields.json +1 -0
  26. bloklake-0.1.0/tests/fixtures/rh-all-fields.request.json +1 -0
  27. bloklake-0.1.0/tests/fixtures/synthetic-short.arrows +0 -0
  28. bloklake-0.1.0/tests/fixtures/synthetic-u256.arrows +0 -0
  29. bloklake-0.1.0/tests/fixtures/synthetic-u256.json +46 -0
  30. bloklake-0.1.0/tests/test_client.py +155 -0
  31. bloklake-0.1.0/tests/test_decode.py +52 -0
  32. bloklake-0.1.0/tests/test_live.py +71 -0
@@ -0,0 +1,6 @@
1
+ __pycache__/
2
+ *.egg-info/
3
+ .venv/
4
+ dist/
5
+ .pytest_cache/
6
+ .mypy_cache/
bloklake-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Bloklake
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,161 @@
1
+ Metadata-Version: 2.5
2
+ Name: bloklake
3
+ Version: 0.1.0
4
+ Summary: Bloklake API client: typed queries, Apache Arrow transport, JSON-like rows.
5
+ Project-URL: Documentation, https://docs.bloklake.com
6
+ License-Expression: MIT
7
+ License-File: LICENSE
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Typing :: Typed
10
+ Requires-Python: >=3.9
11
+ Requires-Dist: pyarrow>=14
12
+ Requires-Dist: requests>=2.31
13
+ Provides-Extra: dev
14
+ Requires-Dist: mypy>=1.10; extra == 'dev'
15
+ Requires-Dist: pytest>=8; extra == 'dev'
16
+ Description-Content-Type: text/markdown
17
+
18
+ # bloklake
19
+
20
+ The official Python client for the [Bloklake](https://docs.bloklake.com) API.
21
+
22
+ - **Typed queries**: filters, field names and rows are `TypedDict`s and `Literal`s, so your
23
+ editor and mypy catch a misspelled field.
24
+ - **Arrow on the wire, JSON-like rows in your code**: responses travel as Apache Arrow (about
25
+ 2–2.5× smaller than JSON, so cheaper) and are decoded here into plain dicts with the same
26
+ values `/query/json` returns.
27
+ - **Pagination, retries and integrity built in**: follows `next_block`, reuses prepared
28
+ queries, retries rate limits and busy servers, and refuses truncated responses.
29
+
30
+ ```bash
31
+ pip install bloklake
32
+ ```
33
+
34
+ Python 3.9+. Depends on `pyarrow` and `requests`.
35
+
36
+ ## Quick start
37
+
38
+ ```python
39
+ from bloklake import Bloklake
40
+
41
+ bl = Bloklake(chain="eth") # API key from BLOKLAKE_API_KEY
42
+
43
+ page = bl.query({
44
+ "from_block": 19_426_000,
45
+ "to_block": 19_426_009,
46
+ "logs": [{
47
+ "address": ["0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"],
48
+ "topic0": ["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"],
49
+ }],
50
+ "fields": {"log": ["block_number", "transaction_hash", "topic1", "topic2", "data"]},
51
+ })
52
+
53
+ for log in page.logs:
54
+ print(log["block_number"], log["data"])
55
+ print(page.meta.next_block)
56
+ ```
57
+
58
+ Write the query in snake_case, as above, or in the API's camelCase (`"fromBlock"`,
59
+ `"blockNumber"`, as in the HTTP API and curl examples); the client sends camelCase either way.
60
+ Give each key one way only. Rows always come back in snake_case (`log["block_number"]`).
61
+
62
+ ## Reading a whole range
63
+
64
+ `paginate` follows `next_block` until `to_block` (or the chain head when you leave `to_block`
65
+ out). After the first page it sends only the prepared-query id, and falls back to the full
66
+ query if the id expired.
67
+
68
+ ```python
69
+ for page in bl.paginate({
70
+ "from_block": 19_000_000,
71
+ "to_block": 19_100_000,
72
+ "logs": [{"topic0": [TRANSFER]}],
73
+ "fields": {"log": ["block_number", "address", "topic1", "topic2", "data"]},
74
+ }):
75
+ store(page.logs)
76
+ ```
77
+
78
+ `collect(query)` returns all pages merged into one `Page`. Use it for ranges that fit in memory.
79
+
80
+ ## Results
81
+
82
+ A `Page` has `logs`, `transactions` and `blocks` lists (empty when not requested) and `meta`
83
+ (`from_block`, `to_block`, `next_block`, `objects`, `archive_height`, `finalized_block`,
84
+ `to_block_hash`, `query_id`, `unavailable_fields`).
85
+
86
+ Rows use the API's column names (`block_number`, `transaction_hash`) and its JSON encoding:
87
+
88
+ | Field kind | Value |
89
+ |---|---|
90
+ | block numbers, indexes, gas, timestamps | `int` |
91
+ | amounts (`value`, gas prices, fees, uint256) | decimal `str` (`int(x)` to compute) |
92
+ | hashes, addresses, topics, `input`, `data` | `0x` hex `str` |
93
+ | `withdrawals`, `access_list`, `authorization_list` | lists of dicts |
94
+ | not available or not applicable | `None` |
95
+
96
+ To get a dataframe: `pandas.DataFrame(page.logs)` or `polars.DataFrame(page.logs)`. If you'd
97
+ rather keep the raw Arrow columns, use the API directly as shown in
98
+ [Reading Arrow](https://docs.bloklake.com/responses/reading-arrow).
99
+
100
+ ## Other calls
101
+
102
+ ```python
103
+ s = bl.create_set("address", pools) # idempotent: same values, same id
104
+ bl.query({"logs": [{"address_set": s.set_id, "topic0": [SWAP]}], "fields": {"log": ["address", "data"]}})
105
+ bl.get_set(s.set_id)
106
+ bl.delete_set(s.set_id)
107
+
108
+ bl.prepare({"logs": [...], "fields": {...}}) # Prepared(query_id, expires_in)
109
+ bl.status() # ChainStatus(chain_id, first_block, archive_height, finalized_block), free
110
+ bl.schema() # every field and whether this chain has it, free
111
+ bl.account() # plan, rps, balance or usage, free
112
+ ```
113
+
114
+ ## Errors
115
+
116
+ Failures raise `BloklakeError` with `status` (0 for client-side failures), `code`
117
+ (`invalid_query`, `rate_limited`, `balance_exhausted`, `truncated`, …), `retry_after` and
118
+ `request_id`. The client retries `429`, `503`, `5xx`, network errors and truncated responses
119
+ itself, up to `max_retries` (default 4), honoring `Retry-After`. Other errors need a fix and are
120
+ raised at once. See [Errors](https://docs.bloklake.com/api-reference/errors).
121
+
122
+ ```python
123
+ from bloklake import BloklakeError
124
+
125
+ try:
126
+ bl.query(q)
127
+ except BloklakeError as e:
128
+ if e.code == "balance_exhausted":
129
+ alert_someone()
130
+ else:
131
+ raise
132
+ ```
133
+
134
+ ## Options
135
+
136
+ ```python
137
+ Bloklake(
138
+ api_key="bl_live_…", # default: BLOKLAKE_API_KEY
139
+ chain="rh", # or base_url="https://api.bloklake.com/v1/chains/rh"
140
+ max_retries=4,
141
+ timeout=120.0,
142
+ session=requests.Session(), # proxies, custom adapters, tests
143
+ )
144
+ ```
145
+
146
+ ## Development
147
+
148
+ ```bash
149
+ pip install -e ".[dev]"
150
+ pytest # unit and fixture tests
151
+ mypy src/bloklake # strict
152
+ BLOKLAKE_TEST_URL=https://api.bloklake.com/v1/chains/eth BLOKLAKE_API_KEY=… pytest # plus live tests
153
+ python scripts/generate.py # regenerate src/bloklake/fields.py after updating scripts/schema.json
154
+ ```
155
+
156
+ The fixture tests decode recorded Arrow responses and compare them with the server's own
157
+ `/query/json` output for the same requests, field by field.
158
+
159
+ ## License
160
+
161
+ MIT
@@ -0,0 +1,144 @@
1
+ # bloklake
2
+
3
+ The official Python client for the [Bloklake](https://docs.bloklake.com) API.
4
+
5
+ - **Typed queries**: filters, field names and rows are `TypedDict`s and `Literal`s, so your
6
+ editor and mypy catch a misspelled field.
7
+ - **Arrow on the wire, JSON-like rows in your code**: responses travel as Apache Arrow (about
8
+ 2–2.5× smaller than JSON, so cheaper) and are decoded here into plain dicts with the same
9
+ values `/query/json` returns.
10
+ - **Pagination, retries and integrity built in**: follows `next_block`, reuses prepared
11
+ queries, retries rate limits and busy servers, and refuses truncated responses.
12
+
13
+ ```bash
14
+ pip install bloklake
15
+ ```
16
+
17
+ Python 3.9+. Depends on `pyarrow` and `requests`.
18
+
19
+ ## Quick start
20
+
21
+ ```python
22
+ from bloklake import Bloklake
23
+
24
+ bl = Bloklake(chain="eth") # API key from BLOKLAKE_API_KEY
25
+
26
+ page = bl.query({
27
+ "from_block": 19_426_000,
28
+ "to_block": 19_426_009,
29
+ "logs": [{
30
+ "address": ["0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"],
31
+ "topic0": ["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"],
32
+ }],
33
+ "fields": {"log": ["block_number", "transaction_hash", "topic1", "topic2", "data"]},
34
+ })
35
+
36
+ for log in page.logs:
37
+ print(log["block_number"], log["data"])
38
+ print(page.meta.next_block)
39
+ ```
40
+
41
+ Write the query in snake_case, as above, or in the API's camelCase (`"fromBlock"`,
42
+ `"blockNumber"`, as in the HTTP API and curl examples); the client sends camelCase either way.
43
+ Give each key one way only. Rows always come back in snake_case (`log["block_number"]`).
44
+
45
+ ## Reading a whole range
46
+
47
+ `paginate` follows `next_block` until `to_block` (or the chain head when you leave `to_block`
48
+ out). After the first page it sends only the prepared-query id, and falls back to the full
49
+ query if the id expired.
50
+
51
+ ```python
52
+ for page in bl.paginate({
53
+ "from_block": 19_000_000,
54
+ "to_block": 19_100_000,
55
+ "logs": [{"topic0": [TRANSFER]}],
56
+ "fields": {"log": ["block_number", "address", "topic1", "topic2", "data"]},
57
+ }):
58
+ store(page.logs)
59
+ ```
60
+
61
+ `collect(query)` returns all pages merged into one `Page`. Use it for ranges that fit in memory.
62
+
63
+ ## Results
64
+
65
+ A `Page` has `logs`, `transactions` and `blocks` lists (empty when not requested) and `meta`
66
+ (`from_block`, `to_block`, `next_block`, `objects`, `archive_height`, `finalized_block`,
67
+ `to_block_hash`, `query_id`, `unavailable_fields`).
68
+
69
+ Rows use the API's column names (`block_number`, `transaction_hash`) and its JSON encoding:
70
+
71
+ | Field kind | Value |
72
+ |---|---|
73
+ | block numbers, indexes, gas, timestamps | `int` |
74
+ | amounts (`value`, gas prices, fees, uint256) | decimal `str` (`int(x)` to compute) |
75
+ | hashes, addresses, topics, `input`, `data` | `0x` hex `str` |
76
+ | `withdrawals`, `access_list`, `authorization_list` | lists of dicts |
77
+ | not available or not applicable | `None` |
78
+
79
+ To get a dataframe: `pandas.DataFrame(page.logs)` or `polars.DataFrame(page.logs)`. If you'd
80
+ rather keep the raw Arrow columns, use the API directly as shown in
81
+ [Reading Arrow](https://docs.bloklake.com/responses/reading-arrow).
82
+
83
+ ## Other calls
84
+
85
+ ```python
86
+ s = bl.create_set("address", pools) # idempotent: same values, same id
87
+ bl.query({"logs": [{"address_set": s.set_id, "topic0": [SWAP]}], "fields": {"log": ["address", "data"]}})
88
+ bl.get_set(s.set_id)
89
+ bl.delete_set(s.set_id)
90
+
91
+ bl.prepare({"logs": [...], "fields": {...}}) # Prepared(query_id, expires_in)
92
+ bl.status() # ChainStatus(chain_id, first_block, archive_height, finalized_block), free
93
+ bl.schema() # every field and whether this chain has it, free
94
+ bl.account() # plan, rps, balance or usage, free
95
+ ```
96
+
97
+ ## Errors
98
+
99
+ Failures raise `BloklakeError` with `status` (0 for client-side failures), `code`
100
+ (`invalid_query`, `rate_limited`, `balance_exhausted`, `truncated`, …), `retry_after` and
101
+ `request_id`. The client retries `429`, `503`, `5xx`, network errors and truncated responses
102
+ itself, up to `max_retries` (default 4), honoring `Retry-After`. Other errors need a fix and are
103
+ raised at once. See [Errors](https://docs.bloklake.com/api-reference/errors).
104
+
105
+ ```python
106
+ from bloklake import BloklakeError
107
+
108
+ try:
109
+ bl.query(q)
110
+ except BloklakeError as e:
111
+ if e.code == "balance_exhausted":
112
+ alert_someone()
113
+ else:
114
+ raise
115
+ ```
116
+
117
+ ## Options
118
+
119
+ ```python
120
+ Bloklake(
121
+ api_key="bl_live_…", # default: BLOKLAKE_API_KEY
122
+ chain="rh", # or base_url="https://api.bloklake.com/v1/chains/rh"
123
+ max_retries=4,
124
+ timeout=120.0,
125
+ session=requests.Session(), # proxies, custom adapters, tests
126
+ )
127
+ ```
128
+
129
+ ## Development
130
+
131
+ ```bash
132
+ pip install -e ".[dev]"
133
+ pytest # unit and fixture tests
134
+ mypy src/bloklake # strict
135
+ BLOKLAKE_TEST_URL=https://api.bloklake.com/v1/chains/eth BLOKLAKE_API_KEY=… pytest # plus live tests
136
+ python scripts/generate.py # regenerate src/bloklake/fields.py after updating scripts/schema.json
137
+ ```
138
+
139
+ The fixture tests decode recorded Arrow responses and compare them with the server's own
140
+ `/query/json` output for the same requests, field by field.
141
+
142
+ ## License
143
+
144
+ MIT
@@ -0,0 +1,32 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.21"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "bloklake"
7
+ version = "0.1.0"
8
+ description = "Bloklake API client: typed queries, Apache Arrow transport, JSON-like rows."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.9"
12
+ dependencies = ["pyarrow>=14", "requests>=2.31"]
13
+ classifiers = ["Programming Language :: Python :: 3", "Typing :: Typed"]
14
+
15
+ [project.optional-dependencies]
16
+ dev = ["pytest>=8", "mypy>=1.10"]
17
+
18
+ [project.urls]
19
+ Documentation = "https://docs.bloklake.com"
20
+
21
+ [tool.hatch.build.targets.wheel]
22
+ packages = ["src/bloklake"]
23
+
24
+ [tool.pytest.ini_options]
25
+ testpaths = ["tests"]
26
+
27
+ [tool.mypy]
28
+ strict = true
29
+
30
+ [[tool.mypy.overrides]]
31
+ module = ["pyarrow", "pyarrow.*"]
32
+ ignore_missing_imports = true
@@ -0,0 +1,110 @@
1
+ """Generate src/bloklake/fields.py from scripts/schema.json (a copy of GET /v1/schema).
2
+
3
+ Run `python scripts/generate.py` after refreshing schema.json when the API adds fields.
4
+ """
5
+ import json
6
+ import re
7
+ from pathlib import Path
8
+
9
+ ROOT = Path(__file__).resolve().parent.parent
10
+ schema = json.loads((ROOT / "scripts" / "schema.json").read_text())
11
+
12
+
13
+ def camel_case(name: str) -> str:
14
+ """``block_number`` -> ``blockNumber``; must match ``bloklake.client._camel``."""
15
+ return re.sub(r"_([a-z0-9])", lambda m: m.group(1).upper(), name)
16
+
17
+
18
+ # Requests may name a field by its column name (``block_number``); the client turns it into the
19
+ # request name. That only works while every request name is its column name in camelCase.
20
+ for rt, fs in schema["row_types"].items():
21
+ for f in fs:
22
+ if camel_case(f["name"]) != f["dsl_name"]:
23
+ raise SystemExit(f'{rt}.{f["name"]}: request name {f["dsl_name"]} is not the column name in camelCase')
24
+
25
+ # uint256 amounts: 32 bytes like a hash, but decimal strings in JSON (the server's catalogue)
26
+ UINT256 = ["max_refund", "submission_fee_refund", "deposit_value", "retry_value", "max_submission_fee", "l1_base_fee"]
27
+ STRUCT_NAMES = {"withdrawals": "Withdrawal", "access_list": "AccessListEntry", "authorization_list": "Authorization"}
28
+ structs: dict[str, str] = {}
29
+
30
+
31
+ def parse(s: str):
32
+ pos = 0
33
+
34
+ def ws():
35
+ nonlocal pos
36
+ while pos < len(s) and s[pos] == " ":
37
+ pos += 1
38
+
39
+ def ident():
40
+ nonlocal pos
41
+ m = re.match(r"[A-Za-z0-9_]+", s[pos:])
42
+ pos += len(m.group(0))
43
+ return m.group(0)
44
+
45
+ def typ():
46
+ nonlocal pos
47
+ ws()
48
+ name = ident()
49
+ if name == "FixedSizeBinary":
50
+ pos += 1; n = ident(); pos += 1
51
+ return ("fsb", int(n))
52
+ if name == "List":
53
+ pos += 1; t = typ(); pos += 1
54
+ return ("list", t)
55
+ if name == "Struct":
56
+ pos += 1; fields = []
57
+ while True:
58
+ ws(); n = ident(); ws(); pos += 1; t = typ(); fields.append((n, t)); ws()
59
+ if s[pos] == ",":
60
+ pos += 1; continue
61
+ pos += 1; break
62
+ return ("struct", fields)
63
+ return ("prim", name)
64
+
65
+ return typ()
66
+
67
+
68
+ def py_type(t, column: str) -> str:
69
+ kind = t[0]
70
+ if kind == "prim":
71
+ return "int" if t[1].startswith("UInt") else "str"
72
+ if kind == "fsb":
73
+ return "str"
74
+ if kind == "list":
75
+ return f"List[{py_type(t[1], column)}]"
76
+ name = STRUCT_NAMES.get(column, f"{column}_entry")
77
+ if name not in structs:
78
+ body = ", ".join(f'"{n}": {py_type(ft, n)}' for n, ft in t[1])
79
+ structs[name] = f'{name} = TypedDict("{name}", {{{body}}})'
80
+ return name
81
+
82
+
83
+ out = ['"""Generated by scripts/generate.py from scripts/schema.json (GET /v1/schema). Do not edit."""',
84
+ "from typing import FrozenSet, List, Literal, Mapping, Optional, TypedDict", ""]
85
+ rows = []
86
+ for key, field_alias, row in (("logs", "LogField", "Log"), ("transactions", "TransactionField", "Transaction"), ("blocks", "BlockField", "Block")):
87
+ fields = schema["row_types"][key]
88
+ names = [f["dsl_name"] for f in fields] + (["topics"] if key == "logs" else [])
89
+ # The same fields by column name, for snake_case requests (``block_number``).
90
+ names += [f["name"] for f in fields if f["name"] != f["dsl_name"]]
91
+ rows.append(f"{field_alias} = Literal[\n" + "".join(f' "{n}",\n' for n in names) + "]")
92
+ rows.append(f'"""Field names for `fields.{ {"logs": "log", "transactions": "transaction", "blocks": "block"}[key] }`: '
93
+ f'the request name (camelCase) or the column name (snake_case)."""')
94
+ rows.append("")
95
+ entries = []
96
+ for f in fields:
97
+ t = py_type(parse(f["arrow_type"]), f["name"])
98
+ if f["nullable"]:
99
+ t = f"Optional[{t}]"
100
+ entries.append(f' "{f["name"]}": {t}, # {f["arrow_type"]}')
101
+ rows.append(f'{row} = TypedDict(\n "{row}",\n {{\n' + "\n".join(entries) + "\n },\n total=False,\n)")
102
+ rows.append(f'"""A {key[:-1]} row: keys are present only for the fields you selected."""')
103
+ rows.append("")
104
+ out += list(structs.values()) + [""] + rows
105
+ out.append("UINT256_COLUMNS: Mapping[str, FrozenSet[str]] = {")
106
+ out.append(f' "transactions": frozenset({sorted(UINT256)!r}),')
107
+ out.append("}")
108
+ out.append('"""Response keys that hold uint256 amounts (decimal strings), per row type."""')
109
+ (ROOT / "src" / "bloklake" / "fields.py").write_text("\n".join(out) + "\n")
110
+ print("wrote src/bloklake/fields.py")