qweb3 1.1.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.
- qweb3/__init__.py +107 -0
- qweb3/_keccak.py +73 -0
- qweb3/abi.py +341 -0
- qweb3/batch.py +97 -0
- qweb3/bech32m.py +88 -0
- qweb3/cli.py +168 -0
- qweb3/contract.py +176 -0
- qweb3/crypto_backend.py +46 -0
- qweb3/errors.py +53 -0
- qweb3/fee.py +131 -0
- qweb3/hooks.py +205 -0
- qweb3/qns.py +126 -0
- qweb3/rest.py +154 -0
- qweb3/rpc.py +139 -0
- qweb3/signer.py +63 -0
- qweb3/utils.py +256 -0
- qweb3/wallet.py +142 -0
- qweb3-1.1.0.dist-info/METADATA +75 -0
- qweb3-1.1.0.dist-info/RECORD +23 -0
- qweb3-1.1.0.dist-info/WHEEL +5 -0
- qweb3-1.1.0.dist-info/entry_points.txt +2 -0
- qweb3-1.1.0.dist-info/licenses/LICENSE +110 -0
- qweb3-1.1.0.dist-info/top_level.txt +1 -0
qweb3/__init__.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""qweb3 — Quantova Post-Quantum Web3 Client SDK (Python).
|
|
2
|
+
|
|
3
|
+
A synchronous client for the Quantova Layer-1 blockchain: post-quantum wallets
|
|
4
|
+
and signing (Falcon, Dilithium, SPHINCS+), the full ``q_*`` JSON-RPC namespace,
|
|
5
|
+
a QVM contract layer with ABI codec and event-log decoding, QNS ``.q`` name
|
|
6
|
+
resolution, dynamic fee/gas estimation, batch requests, real-time event hooks,
|
|
7
|
+
and REST-gateway access.
|
|
8
|
+
|
|
9
|
+
Quick start::
|
|
10
|
+
|
|
11
|
+
from qweb3 import QWeb3
|
|
12
|
+
q = QWeb3("http://127.0.0.1:9944", rest_url="http://127.0.0.1:8080")
|
|
13
|
+
block = q.rpc.block_number()
|
|
14
|
+
fees = q.fees.estimate()
|
|
15
|
+
|
|
16
|
+
Post-quantum signing requires Quantova's crypto backend; install it and call
|
|
17
|
+
``qweb3.crypto_backend.set_backend(...)`` (see that module's docstring).
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from . import abi, crypto_backend
|
|
21
|
+
from .abi import (
|
|
22
|
+
decode_function_result,
|
|
23
|
+
decode_parameters,
|
|
24
|
+
encode_function_call,
|
|
25
|
+
encode_parameters,
|
|
26
|
+
event_topic,
|
|
27
|
+
function_selector,
|
|
28
|
+
)
|
|
29
|
+
from .batch import BatchRequest, BatchResult
|
|
30
|
+
from .contract import QContract
|
|
31
|
+
from .errors import (
|
|
32
|
+
ConnectionErrorQ,
|
|
33
|
+
InvalidArgumentError,
|
|
34
|
+
QWeb3Error,
|
|
35
|
+
RpcError,
|
|
36
|
+
TransactionError,
|
|
37
|
+
)
|
|
38
|
+
from .fee import FeeOracle
|
|
39
|
+
from .hooks import EventHooks, TxTracker
|
|
40
|
+
from .qns import QNS
|
|
41
|
+
from .rest import QRestClient
|
|
42
|
+
from .rpc import QRpcClient
|
|
43
|
+
from .signer import QuantumSigner
|
|
44
|
+
from .utils import AddressUtils, FormatUtils, HexUtils, ValidationUtils
|
|
45
|
+
from .wallet import Account, QuantumWallet
|
|
46
|
+
|
|
47
|
+
__version__ = "1.1.0"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class QWeb3:
|
|
51
|
+
"""High-level facade wiring the RPC client, wallet, fees, hooks, and helpers."""
|
|
52
|
+
|
|
53
|
+
def __init__(self, url: str = "http://127.0.0.1:9944", *,
|
|
54
|
+
rest_url: str = None, session=None) -> None:
|
|
55
|
+
self.url = url
|
|
56
|
+
self.rpc = QRpcClient(url, session=session)
|
|
57
|
+
self.rest = QRestClient(rest_url, session=session) if rest_url else None
|
|
58
|
+
self.wallet = QuantumWallet()
|
|
59
|
+
self.signer = QuantumSigner
|
|
60
|
+
self.abi = abi
|
|
61
|
+
self.fees = FeeOracle(rpc=self.rpc, rest_client=self.rest)
|
|
62
|
+
self.hooks = EventHooks(rpc=self.rpc, rest_client=self.rest)
|
|
63
|
+
|
|
64
|
+
def contract(self, abi_list, address: str) -> QContract:
|
|
65
|
+
return QContract(abi_list, address, rpc=self.rpc, wallet=self.wallet, rest_client=self.rest)
|
|
66
|
+
|
|
67
|
+
def qns(self, registry_address: str, **kwargs) -> QNS:
|
|
68
|
+
return QNS(registry_address, rpc=self.rpc, wallet=self.wallet,
|
|
69
|
+
rest_client=self.rest, **kwargs)
|
|
70
|
+
|
|
71
|
+
def batch(self) -> BatchRequest:
|
|
72
|
+
return BatchRequest(self.url)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
__all__ = [
|
|
76
|
+
"QWeb3",
|
|
77
|
+
"QRpcClient",
|
|
78
|
+
"QRestClient",
|
|
79
|
+
"QuantumWallet",
|
|
80
|
+
"Account",
|
|
81
|
+
"QuantumSigner",
|
|
82
|
+
"QContract",
|
|
83
|
+
"QNS",
|
|
84
|
+
"BatchRequest",
|
|
85
|
+
"BatchResult",
|
|
86
|
+
"FeeOracle",
|
|
87
|
+
"EventHooks",
|
|
88
|
+
"TxTracker",
|
|
89
|
+
"AddressUtils",
|
|
90
|
+
"HexUtils",
|
|
91
|
+
"FormatUtils",
|
|
92
|
+
"ValidationUtils",
|
|
93
|
+
"QWeb3Error",
|
|
94
|
+
"ConnectionErrorQ",
|
|
95
|
+
"InvalidArgumentError",
|
|
96
|
+
"RpcError",
|
|
97
|
+
"TransactionError",
|
|
98
|
+
"abi",
|
|
99
|
+
"crypto_backend",
|
|
100
|
+
"function_selector",
|
|
101
|
+
"event_topic",
|
|
102
|
+
"encode_parameters",
|
|
103
|
+
"decode_parameters",
|
|
104
|
+
"encode_function_call",
|
|
105
|
+
"decode_function_result",
|
|
106
|
+
"__version__",
|
|
107
|
+
]
|
qweb3/_keccak.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Pure-Python keccak-256 (Ethereum/Solidity padding, 0x01).
|
|
2
|
+
|
|
3
|
+
This is the default selector/topic hash used by the QVM ABI codec, which speaks
|
|
4
|
+
the standard Solidity ABI. It is dependency-free so the codec works out of the
|
|
5
|
+
box; it can be overridden via ``qweb3.abi.set_keccak`` (for example to use a
|
|
6
|
+
faster C implementation, or the hash provided by Quantova's crypto backend).
|
|
7
|
+
|
|
8
|
+
Note: this is keccak-256, not SHA3-256. They differ only in the padding byte.
|
|
9
|
+
Quantova hashes *transactions/state* with SHA3-256 (handled in the signing
|
|
10
|
+
layer); contract-call *selectors* use keccak-256, which is what this provides.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from typing import List
|
|
14
|
+
|
|
15
|
+
_RC = [
|
|
16
|
+
0x0000000000000001, 0x0000000000008082, 0x800000000000808A, 0x8000000080008000,
|
|
17
|
+
0x000000000000808B, 0x0000000080000001, 0x8000000080008081, 0x8000000000008009,
|
|
18
|
+
0x000000000000008A, 0x0000000000000088, 0x0000000080008009, 0x000000008000000A,
|
|
19
|
+
0x000000008000808B, 0x800000000000008B, 0x8000000000008089, 0x8000000000008003,
|
|
20
|
+
0x8000000000008002, 0x8000000000000080, 0x000000000000800A, 0x800000008000000A,
|
|
21
|
+
0x8000000080008081, 0x8000000000008080, 0x0000000080000001, 0x8000000080008008,
|
|
22
|
+
]
|
|
23
|
+
_ROT = [
|
|
24
|
+
[0, 36, 3, 41, 18],
|
|
25
|
+
[1, 44, 10, 45, 2],
|
|
26
|
+
[62, 6, 43, 15, 61],
|
|
27
|
+
[28, 55, 25, 21, 56],
|
|
28
|
+
[27, 20, 39, 8, 14],
|
|
29
|
+
]
|
|
30
|
+
_MASK = (1 << 64) - 1
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _rotl(x: int, n: int) -> int:
|
|
34
|
+
return ((x << n) | (x >> (64 - n))) & _MASK
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _keccak_f(s: List[List[int]]) -> None:
|
|
38
|
+
for rnd in range(24):
|
|
39
|
+
c = [s[x][0] ^ s[x][1] ^ s[x][2] ^ s[x][3] ^ s[x][4] for x in range(5)]
|
|
40
|
+
d = [c[(x + 4) % 5] ^ _rotl(c[(x + 1) % 5], 1) for x in range(5)]
|
|
41
|
+
for x in range(5):
|
|
42
|
+
for y in range(5):
|
|
43
|
+
s[x][y] ^= d[x]
|
|
44
|
+
b = [[0] * 5 for _ in range(5)]
|
|
45
|
+
for x in range(5):
|
|
46
|
+
for y in range(5):
|
|
47
|
+
b[y][(2 * x + 3 * y) % 5] = _rotl(s[x][y], _ROT[x][y])
|
|
48
|
+
for x in range(5):
|
|
49
|
+
for y in range(5):
|
|
50
|
+
s[x][y] = (b[x][y] ^ ((~b[(x + 1) % 5][y]) & b[(x + 2) % 5][y])) & _MASK
|
|
51
|
+
s[0][0] ^= _RC[rnd]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def keccak256(data: bytes) -> bytes:
|
|
55
|
+
"""Return the 32-byte keccak-256 digest of ``data``."""
|
|
56
|
+
rate = 136 # bytes for keccak-256
|
|
57
|
+
msg = bytearray(data)
|
|
58
|
+
pad_len = rate - (len(msg) % rate)
|
|
59
|
+
msg += bytearray(pad_len)
|
|
60
|
+
msg[len(data)] ^= 0x01 # keccak padding (Ethereum/Solidity)
|
|
61
|
+
msg[-1] ^= 0x80
|
|
62
|
+
|
|
63
|
+
s = [[0] * 5 for _ in range(5)]
|
|
64
|
+
for off in range(0, len(msg), rate):
|
|
65
|
+
for i in range(rate // 8):
|
|
66
|
+
lane = int.from_bytes(msg[off + i * 8: off + i * 8 + 8], "little")
|
|
67
|
+
s[i % 5][i // 5] ^= lane
|
|
68
|
+
_keccak_f(s)
|
|
69
|
+
|
|
70
|
+
out = bytearray()
|
|
71
|
+
for i in range(4): # 4 lanes * 8 bytes = 32 bytes
|
|
72
|
+
out += (s[i % 5][i // 5]).to_bytes(8, "little")
|
|
73
|
+
return bytes(out)
|
qweb3/abi.py
ADDED
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
"""ABI encoder & decoder for the Quantova Virtual Machine (QVM).
|
|
2
|
+
|
|
3
|
+
The QVM runs Solidity compiled to PolkaVM via pallet-revive and speaks the
|
|
4
|
+
standard Ethereum/Solidity ABI at the contract-call boundary: keccak-256 4-byte
|
|
5
|
+
function selectors and 32-byte-word ("head/tail") argument encoding. This module
|
|
6
|
+
turns method arguments into hex calldata and decodes return data and event
|
|
7
|
+
topics/data back into Python values.
|
|
8
|
+
|
|
9
|
+
The keccak implementation defaults to the bundled pure-Python one and can be
|
|
10
|
+
swapped via :func:`set_keccak` (e.g. to use Quantova's crypto backend).
|
|
11
|
+
|
|
12
|
+
Supported types: ``uint<M>`` / ``int<M>`` (default 256), ``address``, ``bool``,
|
|
13
|
+
``bytes``, ``bytes<N>``, ``string``, and one level of dynamic (``T[]``) or fixed
|
|
14
|
+
(``T[k]``) arrays of those.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from typing import Any, Callable, Dict, List, Sequence
|
|
18
|
+
|
|
19
|
+
from ._keccak import keccak256
|
|
20
|
+
|
|
21
|
+
_keccak: Callable[[bytes], bytes] = keccak256
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def set_keccak(fn: Callable[[bytes], bytes]) -> None:
|
|
25
|
+
"""Override the keccak-256 implementation (takes/returns ``bytes``)."""
|
|
26
|
+
global _keccak
|
|
27
|
+
_keccak = fn
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# --------------------------------------------------------------------------- #
|
|
31
|
+
# type parsing
|
|
32
|
+
# --------------------------------------------------------------------------- #
|
|
33
|
+
|
|
34
|
+
def parse_type(type_str: str) -> Dict[str, Any]:
|
|
35
|
+
if type_str.endswith("]"):
|
|
36
|
+
idx = type_str.rfind("[")
|
|
37
|
+
base = parse_type(type_str[:idx])
|
|
38
|
+
inner = type_str[idx + 1:-1]
|
|
39
|
+
length = int(inner) if inner != "" else None
|
|
40
|
+
return {"kind": "array", "base": base, "length": length,
|
|
41
|
+
"dynamic": length is None or base["dynamic"]}
|
|
42
|
+
if type_str == "address":
|
|
43
|
+
return {"kind": "address", "dynamic": False}
|
|
44
|
+
if type_str == "bool":
|
|
45
|
+
return {"kind": "bool", "dynamic": False}
|
|
46
|
+
if type_str == "string":
|
|
47
|
+
return {"kind": "string", "dynamic": True}
|
|
48
|
+
if type_str == "bytes":
|
|
49
|
+
return {"kind": "bytes", "dynamic": True}
|
|
50
|
+
if type_str.startswith("bytes"):
|
|
51
|
+
n = int(type_str[5:])
|
|
52
|
+
if not (1 <= n <= 32):
|
|
53
|
+
raise ValueError(f"invalid fixed bytes size: {type_str}")
|
|
54
|
+
return {"kind": "fbytes", "size": n, "dynamic": False}
|
|
55
|
+
if type_str.startswith("uint"):
|
|
56
|
+
bits = int(type_str[4:]) if type_str[4:] else 256
|
|
57
|
+
return {"kind": "uint", "bits": bits, "dynamic": False}
|
|
58
|
+
if type_str.startswith("int"):
|
|
59
|
+
bits = int(type_str[3:]) if type_str[3:] else 256
|
|
60
|
+
return {"kind": "int", "bits": bits, "dynamic": False}
|
|
61
|
+
raise ValueError(f"unsupported ABI type: {type_str}")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def canonical_type(type_str: str) -> str:
|
|
65
|
+
t = parse_type(type_str)
|
|
66
|
+
return _canonical(t)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _canonical(t: Dict[str, Any]) -> str:
|
|
70
|
+
k = t["kind"]
|
|
71
|
+
if k == "array":
|
|
72
|
+
return _canonical(t["base"]) + "[" + ("" if t["length"] is None else str(t["length"])) + "]"
|
|
73
|
+
if k == "uint":
|
|
74
|
+
return "uint" + str(t["bits"])
|
|
75
|
+
if k == "int":
|
|
76
|
+
return "int" + str(t["bits"])
|
|
77
|
+
if k == "fbytes":
|
|
78
|
+
return "bytes" + str(t["size"])
|
|
79
|
+
return {"address": "address", "bool": "bool", "string": "string", "bytes": "bytes"}[k]
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _is_dynamic(t: Dict[str, Any]) -> bool:
|
|
83
|
+
if t["kind"] in ("string", "bytes"):
|
|
84
|
+
return True
|
|
85
|
+
if t["kind"] == "array":
|
|
86
|
+
return t["length"] is None or t["base"]["dynamic"]
|
|
87
|
+
return False
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _head_size(t: Dict[str, Any]) -> int:
|
|
91
|
+
if _is_dynamic(t):
|
|
92
|
+
return 32
|
|
93
|
+
if t["kind"] == "array" and t["length"] is not None:
|
|
94
|
+
return t["length"] * _head_size(t["base"])
|
|
95
|
+
return 32
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# --------------------------------------------------------------------------- #
|
|
99
|
+
# word helpers (two's complement)
|
|
100
|
+
# --------------------------------------------------------------------------- #
|
|
101
|
+
|
|
102
|
+
def _int_to_word(value: int, signed: bool) -> bytes:
|
|
103
|
+
v = int(value)
|
|
104
|
+
mod = 1 << 256
|
|
105
|
+
if v < 0:
|
|
106
|
+
if not signed:
|
|
107
|
+
raise ValueError("negative value for unsigned type")
|
|
108
|
+
v = (mod + (v % mod)) % mod
|
|
109
|
+
if v >= mod:
|
|
110
|
+
raise ValueError("value exceeds 256 bits")
|
|
111
|
+
return v.to_bytes(32, "big")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _word_to_int(word: bytes, signed: bool) -> int:
|
|
115
|
+
v = int.from_bytes(word, "big")
|
|
116
|
+
if signed and v >= (1 << 255):
|
|
117
|
+
v -= 1 << 256
|
|
118
|
+
return v
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
# --------------------------------------------------------------------------- #
|
|
122
|
+
# encoding
|
|
123
|
+
# --------------------------------------------------------------------------- #
|
|
124
|
+
|
|
125
|
+
def _encode_value(t: Dict[str, Any], value: Any) -> bytes:
|
|
126
|
+
k = t["kind"]
|
|
127
|
+
if k == "uint":
|
|
128
|
+
return _int_to_word(value, False)
|
|
129
|
+
if k == "int":
|
|
130
|
+
return _int_to_word(value, True)
|
|
131
|
+
if k == "bool":
|
|
132
|
+
return _int_to_word(1 if value else 0, False)
|
|
133
|
+
if k == "address":
|
|
134
|
+
# Accept a Q-branded account address ("Q1...") as well as a 0x contract address.
|
|
135
|
+
if isinstance(value, str) and value[:2].upper() == "Q1":
|
|
136
|
+
from .utils import AddressUtils
|
|
137
|
+
value = AddressUtils.to_node_address(value)
|
|
138
|
+
b = HexBytes(value)
|
|
139
|
+
if len(b) != 20:
|
|
140
|
+
raise ValueError("address must be 20 bytes")
|
|
141
|
+
return b"\x00" * 12 + b
|
|
142
|
+
if k == "fbytes":
|
|
143
|
+
b = HexBytes(value)
|
|
144
|
+
if len(b) != t["size"]:
|
|
145
|
+
raise ValueError(f"bytes{t['size']} must be {t['size']} bytes")
|
|
146
|
+
return b + b"\x00" * (32 - len(b))
|
|
147
|
+
if k in ("bytes", "string"):
|
|
148
|
+
data = value.encode("utf-8") if k == "string" else HexBytes(value)
|
|
149
|
+
length = _int_to_word(len(data), False)
|
|
150
|
+
padded = data + b"\x00" * ((32 - len(data) % 32) % 32)
|
|
151
|
+
return length + padded
|
|
152
|
+
if k == "array":
|
|
153
|
+
if not isinstance(value, (list, tuple)):
|
|
154
|
+
raise ValueError("expected array value")
|
|
155
|
+
if t["length"] is not None and len(value) != t["length"]:
|
|
156
|
+
raise ValueError(f"fixed array expected {t['length']} items, got {len(value)}")
|
|
157
|
+
parts = [_EncodedItem(t["base"], _encode_value(t["base"], v)) for v in value]
|
|
158
|
+
body = _pack(t["base"], parts)
|
|
159
|
+
if t["length"] is None:
|
|
160
|
+
return _int_to_word(len(value), False) + body
|
|
161
|
+
return body
|
|
162
|
+
raise ValueError(f"cannot encode kind {k}")
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class _EncodedItem:
|
|
166
|
+
__slots__ = ("t", "data")
|
|
167
|
+
|
|
168
|
+
def __init__(self, t, data):
|
|
169
|
+
self.t = t
|
|
170
|
+
self.data = data
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _pack(t: Dict[str, Any], parts: List[_EncodedItem]) -> bytes:
|
|
174
|
+
if not _is_dynamic(t):
|
|
175
|
+
return b"".join(p.data for p in parts)
|
|
176
|
+
heads, tails = [], []
|
|
177
|
+
offset = len(parts) * 32
|
|
178
|
+
for p in parts:
|
|
179
|
+
heads.append(_int_to_word(offset, False))
|
|
180
|
+
tails.append(p.data)
|
|
181
|
+
offset += len(p.data)
|
|
182
|
+
return b"".join(heads) + b"".join(tails)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def encode_parameters(types: Sequence[str], values: Sequence[Any]) -> bytes:
|
|
186
|
+
if len(types) != len(values):
|
|
187
|
+
raise ValueError(f"type/value count mismatch: {len(types)} vs {len(values)}")
|
|
188
|
+
descs = [parse_type(t) for t in types]
|
|
189
|
+
encoded = [_encode_value(d, v) for d, v in zip(descs, values)]
|
|
190
|
+
|
|
191
|
+
heads, tails = [], []
|
|
192
|
+
offset = sum(_head_size(d) for d in descs)
|
|
193
|
+
for d, e in zip(descs, encoded):
|
|
194
|
+
if _is_dynamic(d):
|
|
195
|
+
heads.append(_int_to_word(offset, False))
|
|
196
|
+
tails.append(e)
|
|
197
|
+
offset += len(e)
|
|
198
|
+
else:
|
|
199
|
+
heads.append(e)
|
|
200
|
+
return b"".join(heads) + b"".join(tails)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def function_selector(signature: str) -> str:
|
|
204
|
+
"""4-byte selector for a signature like ``transfer(address,uint256)``."""
|
|
205
|
+
return "0x" + _keccak(signature.encode("utf-8"))[:4].hex()
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def build_signature(name: str, inputs: Sequence[dict]) -> str:
|
|
209
|
+
types = [canonical_type(i["type"]) for i in (inputs or [])]
|
|
210
|
+
return f"{name}({','.join(types)})"
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def encode_function_call(fn_abi: dict, args: Sequence[Any]) -> str:
|
|
214
|
+
sig = build_signature(fn_abi["name"], fn_abi.get("inputs"))
|
|
215
|
+
selector = function_selector(sig)
|
|
216
|
+
types = [i["type"] for i in (fn_abi.get("inputs") or [])]
|
|
217
|
+
return selector + encode_parameters(types, args or []).hex()
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def event_topic(signature: str) -> str:
|
|
221
|
+
"""keccak-256 topic hash of an event signature."""
|
|
222
|
+
return "0x" + _keccak(signature.encode("utf-8")).hex()
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
# --------------------------------------------------------------------------- #
|
|
226
|
+
# decoding
|
|
227
|
+
# --------------------------------------------------------------------------- #
|
|
228
|
+
|
|
229
|
+
def _decode_value(t: Dict[str, Any], view: bytes, base: int) -> Any:
|
|
230
|
+
k = t["kind"]
|
|
231
|
+
if k == "uint":
|
|
232
|
+
return _word_to_int(view[base:base + 32], False)
|
|
233
|
+
if k == "int":
|
|
234
|
+
return _word_to_int(view[base:base + 32], True)
|
|
235
|
+
if k == "bool":
|
|
236
|
+
return _word_to_int(view[base:base + 32], False) != 0
|
|
237
|
+
if k == "address":
|
|
238
|
+
return "0x" + view[base + 12:base + 32].hex()
|
|
239
|
+
if k == "fbytes":
|
|
240
|
+
return "0x" + view[base:base + t["size"]].hex()
|
|
241
|
+
if k in ("string", "bytes"):
|
|
242
|
+
offset = _word_to_int(view[base:base + 32], False)
|
|
243
|
+
length = _word_to_int(view[offset:offset + 32], False)
|
|
244
|
+
data = view[offset + 32:offset + 32 + length]
|
|
245
|
+
return data.decode("utf-8") if k == "string" else "0x" + data.hex()
|
|
246
|
+
if k == "array":
|
|
247
|
+
if _is_dynamic(t):
|
|
248
|
+
offset = _word_to_int(view[base:base + 32], False)
|
|
249
|
+
return _decode_array_at(t, view, offset)
|
|
250
|
+
out = []
|
|
251
|
+
cur = base
|
|
252
|
+
for _ in range(t["length"]):
|
|
253
|
+
out.append(_decode_value(t["base"], view, cur))
|
|
254
|
+
cur += _head_size(t["base"])
|
|
255
|
+
return out
|
|
256
|
+
raise ValueError(f"cannot decode kind {k}")
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _decode_array_at(t: Dict[str, Any], view: bytes, offset: int) -> list:
|
|
260
|
+
if t["length"] is None:
|
|
261
|
+
count = _word_to_int(view[offset:offset + 32], False)
|
|
262
|
+
body = view[offset + 32:]
|
|
263
|
+
return _decode_items(t["base"], body, count)
|
|
264
|
+
body = view[offset:]
|
|
265
|
+
return _decode_items(t["base"], body, t["length"])
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
# [QW3PY-001] Absolute upper bound on any decoded array/element count, mirroring
|
|
269
|
+
# eth-abi's guard against attacker-controlled length fields causing huge allocations.
|
|
270
|
+
_MAX_ARRAY_COUNT = 1_000_000
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _check_array_count(count: int, body: bytes) -> None:
|
|
274
|
+
"""[QW3PY-001] Validate an attacker-controlled element count against the
|
|
275
|
+
remaining buffer before iterating. Each element occupies at least one 32-byte
|
|
276
|
+
head word, so the buffer can hold at most len(body) // 32 elements; also impose
|
|
277
|
+
a hard absolute cap to bound work even when the buffer is large."""
|
|
278
|
+
if count < 0:
|
|
279
|
+
raise ValueError("array length exceeds buffer")
|
|
280
|
+
if count > _MAX_ARRAY_COUNT or count > len(body) // 32:
|
|
281
|
+
raise ValueError("array length exceeds buffer")
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _decode_items(base_t: Dict[str, Any], body: bytes, count: int) -> list:
|
|
285
|
+
_check_array_count(count, body)
|
|
286
|
+
out = []
|
|
287
|
+
if _is_dynamic(base_t):
|
|
288
|
+
for i in range(count):
|
|
289
|
+
item_offset = _word_to_int(body[i * 32:i * 32 + 32], False)
|
|
290
|
+
out.append(_decode_value_absolute(base_t, body, item_offset))
|
|
291
|
+
else:
|
|
292
|
+
cur = 0
|
|
293
|
+
for _ in range(count):
|
|
294
|
+
out.append(_decode_value(base_t, body, cur))
|
|
295
|
+
cur += _head_size(base_t)
|
|
296
|
+
return out
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def _decode_value_absolute(t: Dict[str, Any], view: bytes, start: int) -> Any:
|
|
300
|
+
if t["kind"] in ("string", "bytes"):
|
|
301
|
+
length = _word_to_int(view[start:start + 32], False)
|
|
302
|
+
data = view[start + 32:start + 32 + length]
|
|
303
|
+
return data.decode("utf-8") if t["kind"] == "string" else "0x" + data.hex()
|
|
304
|
+
if t["kind"] == "array":
|
|
305
|
+
return _decode_array_at(t, view, start)
|
|
306
|
+
return _decode_value(t, view, start)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def decode_parameters(types: Sequence[str], data) -> list:
|
|
310
|
+
view = HexBytes(data)
|
|
311
|
+
descs = [parse_type(t) for t in types]
|
|
312
|
+
out = []
|
|
313
|
+
head = 0
|
|
314
|
+
for d in descs:
|
|
315
|
+
out.append(_decode_value(d, view, head))
|
|
316
|
+
head += _head_size(d)
|
|
317
|
+
return out
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def decode_function_result(fn_abi: dict, data) -> Any:
|
|
321
|
+
types = [o["type"] for o in (fn_abi.get("outputs") or [])]
|
|
322
|
+
if not types:
|
|
323
|
+
return None
|
|
324
|
+
decoded = decode_parameters(types, data)
|
|
325
|
+
return decoded[0] if len(decoded) == 1 else decoded
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
# --------------------------------------------------------------------------- #
|
|
329
|
+
# small helper
|
|
330
|
+
# --------------------------------------------------------------------------- #
|
|
331
|
+
|
|
332
|
+
def HexBytes(value) -> bytes:
|
|
333
|
+
"""Coerce a 0x-hex string or bytes-like into ``bytes``."""
|
|
334
|
+
if isinstance(value, (bytes, bytearray)):
|
|
335
|
+
return bytes(value)
|
|
336
|
+
if isinstance(value, str):
|
|
337
|
+
s = value[2:] if value.startswith(("0x", "0X")) else value
|
|
338
|
+
if len(s) % 2:
|
|
339
|
+
raise ValueError(f"invalid hex length: {value}")
|
|
340
|
+
return bytes.fromhex(s)
|
|
341
|
+
raise TypeError("expected hex string or bytes")
|
qweb3/batch.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Batch requests portal.
|
|
2
|
+
|
|
3
|
+
Groups multiple ``q_*`` JSON-RPC calls into a single payload (a JSON-RPC 2.0
|
|
4
|
+
batch array) to cut round-trips. Results come back in insertion order with
|
|
5
|
+
per-call success/error, so one failing call doesn't sink the batch.
|
|
6
|
+
|
|
7
|
+
batch = BatchRequest("http://127.0.0.1:9944")
|
|
8
|
+
batch.add("q_blockNumber").add("q_getBalance", [addr, "latest"])
|
|
9
|
+
results = batch.execute() # [BatchResult(...), ...]
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from typing import Any, List, Optional
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
import requests as _requests
|
|
17
|
+
except Exception: # pragma: no cover
|
|
18
|
+
_requests = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class BatchResult:
|
|
23
|
+
success: bool
|
|
24
|
+
result: Any = None
|
|
25
|
+
error: Optional[str] = None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class BatchRequest:
|
|
29
|
+
def __init__(self, endpoint: str = "http://127.0.0.1:9944",
|
|
30
|
+
session: Optional[Any] = None, timeout: int = 30) -> None:
|
|
31
|
+
self.endpoint = endpoint
|
|
32
|
+
self.timeout = timeout
|
|
33
|
+
self._calls: List[dict] = []
|
|
34
|
+
self._next_id = 1
|
|
35
|
+
if session is not None:
|
|
36
|
+
self.session = session
|
|
37
|
+
else:
|
|
38
|
+
if _requests is None:
|
|
39
|
+
raise RuntimeError("the 'requests' package is required (or inject a session)")
|
|
40
|
+
self.session = _requests.Session()
|
|
41
|
+
|
|
42
|
+
def add(self, method: str, params: Optional[list] = None) -> "BatchRequest":
|
|
43
|
+
self._calls.append({"id": self._next_id, "method": method, "params": params or []})
|
|
44
|
+
self._next_id += 1
|
|
45
|
+
return self
|
|
46
|
+
|
|
47
|
+
def __len__(self) -> int:
|
|
48
|
+
return len(self._calls)
|
|
49
|
+
|
|
50
|
+
def reset(self) -> "BatchRequest":
|
|
51
|
+
self._calls = []
|
|
52
|
+
self._next_id = 1
|
|
53
|
+
return self
|
|
54
|
+
|
|
55
|
+
def execute(self) -> List[BatchResult]:
|
|
56
|
+
if not self._calls:
|
|
57
|
+
return []
|
|
58
|
+
payload = [{"jsonrpc": "2.0", "id": c["id"], "method": c["method"], "params": c["params"]}
|
|
59
|
+
for c in self._calls]
|
|
60
|
+
try:
|
|
61
|
+
resp = self.session.post(self.endpoint, json=payload,
|
|
62
|
+
headers={"content-type": "application/json"},
|
|
63
|
+
timeout=self.timeout)
|
|
64
|
+
data = resp.json()
|
|
65
|
+
except Exception as exc:
|
|
66
|
+
out = [BatchResult(False, error=f"batch request failed: {exc}") for _ in self._calls]
|
|
67
|
+
self.reset()
|
|
68
|
+
return out
|
|
69
|
+
|
|
70
|
+
by_id = {}
|
|
71
|
+
if isinstance(data, list):
|
|
72
|
+
for r in data:
|
|
73
|
+
by_id[r.get("id")] = r
|
|
74
|
+
elif isinstance(data, dict) and data.get("id") is not None:
|
|
75
|
+
by_id[data["id"]] = data
|
|
76
|
+
|
|
77
|
+
out: List[BatchResult] = []
|
|
78
|
+
for c in self._calls:
|
|
79
|
+
r = by_id.get(c["id"])
|
|
80
|
+
if r is None:
|
|
81
|
+
out.append(BatchResult(False, error=f"no response for {c['method']} (id {c['id']})"))
|
|
82
|
+
elif r.get("error"):
|
|
83
|
+
err = r["error"]
|
|
84
|
+
out.append(BatchResult(False, error=err.get("message") if isinstance(err, dict) else str(err)))
|
|
85
|
+
else:
|
|
86
|
+
out.append(BatchResult(True, result=r.get("result")))
|
|
87
|
+
self.reset()
|
|
88
|
+
return out
|
|
89
|
+
|
|
90
|
+
def execute_or_throw(self) -> list:
|
|
91
|
+
results = self.execute()
|
|
92
|
+
values = []
|
|
93
|
+
for r in results:
|
|
94
|
+
if not r.success:
|
|
95
|
+
raise RuntimeError(r.error)
|
|
96
|
+
values.append(r.result)
|
|
97
|
+
return values
|