qorechain-sdk 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.
- qorechain/__init__.py +239 -0
- qorechain/accounts.py +173 -0
- qorechain/address.py +79 -0
- qorechain/client.py +115 -0
- qorechain/denom.py +87 -0
- qorechain/errors.py +150 -0
- qorechain/fees.py +67 -0
- qorechain/gas.py +186 -0
- qorechain/jsonrpc.py +112 -0
- qorechain/messages/__init__.py +97 -0
- qorechain/messages/_composer.py +60 -0
- qorechain/messages/cosmos.py +105 -0
- qorechain/messages/qorechain.py +242 -0
- qorechain/messages/registry.py +210 -0
- qorechain/networks.py +109 -0
- qorechain/pqc.py +134 -0
- qorechain/proto/__init__.py +1 -0
- qorechain/proto/qorechain/__init__.py +1 -0
- qorechain/proto/qorechain/abstractaccount/__init__.py +1 -0
- qorechain/proto/qorechain/abstractaccount/v1/__init__.py +1 -0
- qorechain/proto/qorechain/abstractaccount/v1/tx_pb2.py +64 -0
- qorechain/proto/qorechain/abstractaccount/v1/tx_pb2.pyi +51 -0
- qorechain/proto/qorechain/amm/__init__.py +1 -0
- qorechain/proto/qorechain/amm/v1/__init__.py +1 -0
- qorechain/proto/qorechain/amm/v1/tx_pb2.py +139 -0
- qorechain/proto/qorechain/amm/v1/tx_pb2.pyi +127 -0
- qorechain/proto/qorechain/bridge/__init__.py +1 -0
- qorechain/proto/qorechain/bridge/v1/__init__.py +1 -0
- qorechain/proto/qorechain/bridge/v1/tx_pb2.py +84 -0
- qorechain/proto/qorechain/bridge/v1/tx_pb2.pyi +89 -0
- qorechain/proto/qorechain/crossvm/__init__.py +1 -0
- qorechain/proto/qorechain/crossvm/v1/__init__.py +1 -0
- qorechain/proto/qorechain/crossvm/v1/query_pb2.py +51 -0
- qorechain/proto/qorechain/crossvm/v1/query_pb2.pyi +74 -0
- qorechain/proto/qorechain/crossvm/v1/tx_pb2.py +67 -0
- qorechain/proto/qorechain/crossvm/v1/tx_pb2.pyi +42 -0
- qorechain/proto/qorechain/license/__init__.py +1 -0
- qorechain/proto/qorechain/license/v1/__init__.py +1 -0
- qorechain/proto/qorechain/license/v1/tx_pb2.py +90 -0
- qorechain/proto/qorechain/license/v1/tx_pb2.pyi +68 -0
- qorechain/proto/qorechain/lightnode/__init__.py +1 -0
- qorechain/proto/qorechain/lightnode/v1/__init__.py +1 -0
- qorechain/proto/qorechain/lightnode/v1/query_pb2.py +59 -0
- qorechain/proto/qorechain/lightnode/v1/query_pb2.pyi +108 -0
- qorechain/proto/qorechain/lightnode/v1/tx_pb2.py +73 -0
- qorechain/proto/qorechain/lightnode/v1/tx_pb2.pyi +54 -0
- qorechain/proto/qorechain/multilayer/__init__.py +1 -0
- qorechain/proto/qorechain/multilayer/v1/__init__.py +1 -0
- qorechain/proto/qorechain/multilayer/v1/tx_pb2.py +118 -0
- qorechain/proto/qorechain/multilayer/v1/tx_pb2.pyi +155 -0
- qorechain/proto/qorechain/pqc/__init__.py +1 -0
- qorechain/proto/qorechain/pqc/v1/__init__.py +1 -0
- qorechain/proto/qorechain/pqc/v1/query_pb2.py +43 -0
- qorechain/proto/qorechain/pqc/v1/query_pb2.pyi +41 -0
- qorechain/proto/qorechain/pqc/v1/tx_pb2.py +96 -0
- qorechain/proto/qorechain/pqc/v1/tx_pb2.pyi +92 -0
- qorechain/proto/qorechain/qca/__init__.py +1 -0
- qorechain/proto/qorechain/qca/v1/__init__.py +1 -0
- qorechain/proto/qorechain/qca/v1/query_pb2.py +41 -0
- qorechain/proto/qorechain/qca/v1/query_pb2.pyi +49 -0
- qorechain/proto/qorechain/rdk/__init__.py +1 -0
- qorechain/proto/qorechain/rdk/v1/__init__.py +1 -0
- qorechain/proto/qorechain/rdk/v1/tx_pb2.py +114 -0
- qorechain/proto/qorechain/rdk/v1/tx_pb2.pyi +122 -0
- qorechain/proto/qorechain/reputation/__init__.py +1 -0
- qorechain/proto/qorechain/reputation/v1/__init__.py +1 -0
- qorechain/proto/qorechain/reputation/v1/query_pb2.py +41 -0
- qorechain/proto/qorechain/reputation/v1/query_pb2.pyi +24 -0
- qorechain/proto/qorechain/rlconsensus/__init__.py +1 -0
- qorechain/proto/qorechain/rlconsensus/v1/__init__.py +1 -0
- qorechain/proto/qorechain/rlconsensus/v1/query_pb2.py +62 -0
- qorechain/proto/qorechain/rlconsensus/v1/query_pb2.pyi +115 -0
- qorechain/proto/qorechain/rlconsensus/v1/tx_pb2.py +69 -0
- qorechain/proto/qorechain/rlconsensus/v1/tx_pb2.pyi +61 -0
- qorechain/proto/qorechain/svm/__init__.py +1 -0
- qorechain/proto/qorechain/svm/v1/__init__.py +1 -0
- qorechain/proto/qorechain/svm/v1/query_pb2.py +53 -0
- qorechain/proto/qorechain/svm/v1/query_pb2.pyi +71 -0
- qorechain/proto/qorechain/svm/v1/tx_pb2.py +90 -0
- qorechain/proto/qorechain/svm/v1/tx_pb2.pyi +83 -0
- qorechain/py.typed +0 -0
- qorechain/qor.py +218 -0
- qorechain/query/__init__.py +27 -0
- qorechain/query/grpc.py +296 -0
- qorechain/rest.py +222 -0
- qorechain/search.py +92 -0
- qorechain/subscribe.py +162 -0
- qorechain/track.py +184 -0
- qorechain/tx.py +427 -0
- qorechain/utils/__init__.py +41 -0
- qorechain/utils/hash.py +58 -0
- qorechain/utils/units.py +94 -0
- qorechain/utils/validation.py +94 -0
- qorechain_sdk-0.3.0.dist-info/METADATA +301 -0
- qorechain_sdk-0.3.0.dist-info/RECORD +96 -0
- qorechain_sdk-0.3.0.dist-info/WHEEL +4 -0
qorechain/__init__.py
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"""QoreChain Python SDK.
|
|
2
|
+
|
|
3
|
+
A typed, pip-installable mirror of the QoreChain TypeScript SDK: network presets,
|
|
4
|
+
denom/address utilities, HD account derivation (native / EVM / SVM), post-quantum
|
|
5
|
+
(ML-DSA-87) signing primitives, and read clients (REST + ``qor_`` JSON-RPC).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from .accounts import (
|
|
11
|
+
Ed25519Account,
|
|
12
|
+
Secp256k1Account,
|
|
13
|
+
derive_evm_account,
|
|
14
|
+
derive_native_account,
|
|
15
|
+
derive_svm_account,
|
|
16
|
+
generate_mnemonic,
|
|
17
|
+
validate_mnemonic,
|
|
18
|
+
)
|
|
19
|
+
from .address import (
|
|
20
|
+
bech32_to_hex,
|
|
21
|
+
bytes_to_bech32,
|
|
22
|
+
hex_to_bech32,
|
|
23
|
+
is_valid_bech32,
|
|
24
|
+
)
|
|
25
|
+
from .client import QoreChainClient, create_client
|
|
26
|
+
from .denom import from_base, to_base
|
|
27
|
+
from .errors import (
|
|
28
|
+
DecodedTxError,
|
|
29
|
+
QoreTxError,
|
|
30
|
+
decode_tx_error,
|
|
31
|
+
is_tx_failure,
|
|
32
|
+
tx_error_from,
|
|
33
|
+
)
|
|
34
|
+
from .fees import estimate_fee
|
|
35
|
+
from .gas import (
|
|
36
|
+
DEFAULT_GAS_MULTIPLIER,
|
|
37
|
+
DEFAULT_GAS_PRICE,
|
|
38
|
+
GasPrice,
|
|
39
|
+
auto_fee,
|
|
40
|
+
calculate_fee,
|
|
41
|
+
estimate_gas,
|
|
42
|
+
simulate_gas_used,
|
|
43
|
+
)
|
|
44
|
+
from .jsonrpc import AsyncJsonRpcClient, JsonRpcClient, JsonRpcError
|
|
45
|
+
from .messages import (
|
|
46
|
+
COSMOS_REGISTRY_TYPES,
|
|
47
|
+
QORECHAIN_REGISTRY_TYPES,
|
|
48
|
+
Msg,
|
|
49
|
+
composer,
|
|
50
|
+
decode_any,
|
|
51
|
+
msg,
|
|
52
|
+
qorechain_registry,
|
|
53
|
+
resolve_message_type,
|
|
54
|
+
)
|
|
55
|
+
from .networks import (
|
|
56
|
+
NETWORKS,
|
|
57
|
+
Bech32Prefixes,
|
|
58
|
+
CoinInfo,
|
|
59
|
+
NetworkConfig,
|
|
60
|
+
NetworkEndpoints,
|
|
61
|
+
get_network,
|
|
62
|
+
list_networks,
|
|
63
|
+
)
|
|
64
|
+
from .pqc import (
|
|
65
|
+
ALGORITHM_DILITHIUM5,
|
|
66
|
+
ALGORITHM_MLKEM1024,
|
|
67
|
+
ALGORITHM_UNSPECIFIED,
|
|
68
|
+
HYBRID_SIG_TYPE_URL,
|
|
69
|
+
ML_DSA_87_PUBLIC_KEY_LENGTH,
|
|
70
|
+
ML_DSA_87_SECRET_KEY_LENGTH,
|
|
71
|
+
ML_DSA_87_SIGNATURE_LENGTH,
|
|
72
|
+
PqcKeypair,
|
|
73
|
+
algorithm_name,
|
|
74
|
+
build_hybrid_signature_extension,
|
|
75
|
+
generate_pqc_keypair,
|
|
76
|
+
is_signature_algorithm,
|
|
77
|
+
pqc_sign,
|
|
78
|
+
pqc_verify,
|
|
79
|
+
)
|
|
80
|
+
from .qor import QOR_METHODS, AsyncQorClient, QorClient
|
|
81
|
+
from .query import QueryClients, connect_query_clients
|
|
82
|
+
from .rest import AsyncRestClient, QoreHttpError, RestClient
|
|
83
|
+
from .search import (
|
|
84
|
+
build_events_query,
|
|
85
|
+
get_block,
|
|
86
|
+
get_latest_block,
|
|
87
|
+
get_tx,
|
|
88
|
+
search_txs,
|
|
89
|
+
)
|
|
90
|
+
from .subscribe import SubscriptionClient, build_tx_query
|
|
91
|
+
from .track import (
|
|
92
|
+
IncludedTx,
|
|
93
|
+
broadcast_and_wait,
|
|
94
|
+
wait_for_tx,
|
|
95
|
+
with_retry,
|
|
96
|
+
)
|
|
97
|
+
from .tx import (
|
|
98
|
+
MSG_SEND_TYPE_URL,
|
|
99
|
+
BroadcastMode,
|
|
100
|
+
BuiltTx,
|
|
101
|
+
bank_send,
|
|
102
|
+
broadcast,
|
|
103
|
+
build_hybrid_tx,
|
|
104
|
+
send_messages,
|
|
105
|
+
)
|
|
106
|
+
from .utils import (
|
|
107
|
+
format_units,
|
|
108
|
+
is_checksum_address,
|
|
109
|
+
is_valid_evm_address,
|
|
110
|
+
is_valid_svm_address,
|
|
111
|
+
keccak256,
|
|
112
|
+
keccak256_hex,
|
|
113
|
+
parse_units,
|
|
114
|
+
ripemd160,
|
|
115
|
+
ripemd160_hex,
|
|
116
|
+
sha256,
|
|
117
|
+
sha256_hex,
|
|
118
|
+
to_checksum_address,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
__version__ = "0.3.0"
|
|
122
|
+
|
|
123
|
+
__all__ = [
|
|
124
|
+
"__version__",
|
|
125
|
+
# networks
|
|
126
|
+
"NETWORKS",
|
|
127
|
+
"Bech32Prefixes",
|
|
128
|
+
"CoinInfo",
|
|
129
|
+
"NetworkConfig",
|
|
130
|
+
"NetworkEndpoints",
|
|
131
|
+
"get_network",
|
|
132
|
+
"list_networks",
|
|
133
|
+
# denom
|
|
134
|
+
"to_base",
|
|
135
|
+
"from_base",
|
|
136
|
+
# address
|
|
137
|
+
"bech32_to_hex",
|
|
138
|
+
"hex_to_bech32",
|
|
139
|
+
"bytes_to_bech32",
|
|
140
|
+
"is_valid_bech32",
|
|
141
|
+
# accounts
|
|
142
|
+
"Secp256k1Account",
|
|
143
|
+
"Ed25519Account",
|
|
144
|
+
"generate_mnemonic",
|
|
145
|
+
"validate_mnemonic",
|
|
146
|
+
"derive_native_account",
|
|
147
|
+
"derive_evm_account",
|
|
148
|
+
"derive_svm_account",
|
|
149
|
+
# pqc
|
|
150
|
+
"ALGORITHM_UNSPECIFIED",
|
|
151
|
+
"ALGORITHM_DILITHIUM5",
|
|
152
|
+
"ALGORITHM_MLKEM1024",
|
|
153
|
+
"HYBRID_SIG_TYPE_URL",
|
|
154
|
+
"ML_DSA_87_PUBLIC_KEY_LENGTH",
|
|
155
|
+
"ML_DSA_87_SECRET_KEY_LENGTH",
|
|
156
|
+
"ML_DSA_87_SIGNATURE_LENGTH",
|
|
157
|
+
"PqcKeypair",
|
|
158
|
+
"algorithm_name",
|
|
159
|
+
"is_signature_algorithm",
|
|
160
|
+
"generate_pqc_keypair",
|
|
161
|
+
"pqc_sign",
|
|
162
|
+
"pqc_verify",
|
|
163
|
+
"build_hybrid_signature_extension",
|
|
164
|
+
# query
|
|
165
|
+
"RestClient",
|
|
166
|
+
"AsyncRestClient",
|
|
167
|
+
"QoreHttpError",
|
|
168
|
+
"JsonRpcClient",
|
|
169
|
+
"AsyncJsonRpcClient",
|
|
170
|
+
"JsonRpcError",
|
|
171
|
+
"QorClient",
|
|
172
|
+
"AsyncQorClient",
|
|
173
|
+
"QOR_METHODS",
|
|
174
|
+
"estimate_fee",
|
|
175
|
+
# tx
|
|
176
|
+
"MSG_SEND_TYPE_URL",
|
|
177
|
+
"BroadcastMode",
|
|
178
|
+
"BuiltTx",
|
|
179
|
+
"bank_send",
|
|
180
|
+
"send_messages",
|
|
181
|
+
"build_hybrid_tx",
|
|
182
|
+
"broadcast",
|
|
183
|
+
# messages
|
|
184
|
+
"Msg",
|
|
185
|
+
"composer",
|
|
186
|
+
"msg",
|
|
187
|
+
"qorechain_registry",
|
|
188
|
+
"resolve_message_type",
|
|
189
|
+
"decode_any",
|
|
190
|
+
"QORECHAIN_REGISTRY_TYPES",
|
|
191
|
+
"COSMOS_REGISTRY_TYPES",
|
|
192
|
+
# typed query clients
|
|
193
|
+
"QueryClients",
|
|
194
|
+
"connect_query_clients",
|
|
195
|
+
# gas / auto-fee
|
|
196
|
+
"GasPrice",
|
|
197
|
+
"calculate_fee",
|
|
198
|
+
"estimate_gas",
|
|
199
|
+
"simulate_gas_used",
|
|
200
|
+
"auto_fee",
|
|
201
|
+
"DEFAULT_GAS_MULTIPLIER",
|
|
202
|
+
"DEFAULT_GAS_PRICE",
|
|
203
|
+
# errors
|
|
204
|
+
"DecodedTxError",
|
|
205
|
+
"QoreTxError",
|
|
206
|
+
"decode_tx_error",
|
|
207
|
+
"is_tx_failure",
|
|
208
|
+
"tx_error_from",
|
|
209
|
+
# tx tracking
|
|
210
|
+
"IncludedTx",
|
|
211
|
+
"wait_for_tx",
|
|
212
|
+
"broadcast_and_wait",
|
|
213
|
+
"with_retry",
|
|
214
|
+
# search
|
|
215
|
+
"get_tx",
|
|
216
|
+
"get_block",
|
|
217
|
+
"get_latest_block",
|
|
218
|
+
"search_txs",
|
|
219
|
+
"build_events_query",
|
|
220
|
+
# subscriptions
|
|
221
|
+
"SubscriptionClient",
|
|
222
|
+
"build_tx_query",
|
|
223
|
+
# utils
|
|
224
|
+
"sha256",
|
|
225
|
+
"sha256_hex",
|
|
226
|
+
"keccak256",
|
|
227
|
+
"keccak256_hex",
|
|
228
|
+
"ripemd160",
|
|
229
|
+
"ripemd160_hex",
|
|
230
|
+
"parse_units",
|
|
231
|
+
"format_units",
|
|
232
|
+
"is_valid_evm_address",
|
|
233
|
+
"to_checksum_address",
|
|
234
|
+
"is_checksum_address",
|
|
235
|
+
"is_valid_svm_address",
|
|
236
|
+
# client
|
|
237
|
+
"QoreChainClient",
|
|
238
|
+
"create_client",
|
|
239
|
+
]
|
qorechain/accounts.py
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""Mnemonic generation/validation and hierarchical-deterministic (HD) derivation
|
|
2
|
+
of QoreChain accounts in all three supported schemes:
|
|
3
|
+
|
|
4
|
+
1. native — Cosmos-style secp256k1, BIP-44 path ``m/44'/118'/0'/0/{index}``,
|
|
5
|
+
address = bech32(``qor``, ripemd160(sha256(compressed_pubkey))).
|
|
6
|
+
2. evm — secp256k1, BIP-44 path ``m/44'/60'/0'/0/{index}``,
|
|
7
|
+
address = ``0x`` + last 20 bytes of keccak256(uncompressed_pubkey[1:]),
|
|
8
|
+
rendered with an EIP-55 mixed-case checksum.
|
|
9
|
+
3. svm — ed25519, SLIP-0010 path ``m/44'/501'/{index}'/0'`` (all hardened,
|
|
10
|
+
the Solana standard), address = base58(32-byte ed25519 public key).
|
|
11
|
+
|
|
12
|
+
Derivation uses the audited ``bip-utils`` library (BIP-39 mnemonic + seed,
|
|
13
|
+
BIP-44/SLIP-0010 HD for secp256k1 and ed25519, and the address schemes). Secret
|
|
14
|
+
material is returned explicitly from the derive functions and is never logged.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import hashlib
|
|
20
|
+
from dataclasses import dataclass
|
|
21
|
+
|
|
22
|
+
import base58
|
|
23
|
+
from bip_utils import (
|
|
24
|
+
Bip32Slip10Ed25519,
|
|
25
|
+
Bip32Slip10Secp256k1,
|
|
26
|
+
Bip39MnemonicGenerator,
|
|
27
|
+
Bip39MnemonicValidator,
|
|
28
|
+
Bip39SeedGenerator,
|
|
29
|
+
Bip39WordsNum,
|
|
30
|
+
EthAddrEncoder,
|
|
31
|
+
)
|
|
32
|
+
from bip_utils.bech32 import Bech32Encoder
|
|
33
|
+
|
|
34
|
+
#: Bech32 human-readable prefix for native QoreChain account addresses.
|
|
35
|
+
NATIVE_PREFIX = "qor"
|
|
36
|
+
|
|
37
|
+
# Coin types per SLIP-0044.
|
|
38
|
+
_COIN_TYPE_NATIVE = 118 # Cosmos
|
|
39
|
+
_COIN_TYPE_EVM = 60 # Ethereum
|
|
40
|
+
_COIN_TYPE_SVM = 501 # Solana
|
|
41
|
+
|
|
42
|
+
_STRENGTH_TO_WORDS = {
|
|
43
|
+
128: Bip39WordsNum.WORDS_NUM_12,
|
|
44
|
+
256: Bip39WordsNum.WORDS_NUM_24,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True)
|
|
49
|
+
class Secp256k1Account:
|
|
50
|
+
"""A secp256k1-based account (native or EVM). Treat ``private_key`` secret."""
|
|
51
|
+
|
|
52
|
+
type: str
|
|
53
|
+
address: str
|
|
54
|
+
public_key: bytes
|
|
55
|
+
private_key: bytes
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass(frozen=True)
|
|
59
|
+
class Ed25519Account:
|
|
60
|
+
"""An ed25519-based (SVM/Solana) account. Treat ``secret_key`` as secret."""
|
|
61
|
+
|
|
62
|
+
type: str
|
|
63
|
+
address: str
|
|
64
|
+
public_key: bytes
|
|
65
|
+
#: 64-byte Solana-style secret key (``private_seed32 || public_key32``).
|
|
66
|
+
secret_key: bytes
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def generate_mnemonic(strength: int = 128) -> str:
|
|
70
|
+
"""Generate a fresh BIP-39 mnemonic.
|
|
71
|
+
|
|
72
|
+
:param strength: Entropy in bits: ``128`` -> 12 words (default),
|
|
73
|
+
``256`` -> 24 words.
|
|
74
|
+
:returns: A space-separated English mnemonic phrase.
|
|
75
|
+
"""
|
|
76
|
+
words_num = _STRENGTH_TO_WORDS.get(strength)
|
|
77
|
+
if words_num is None:
|
|
78
|
+
raise ValueError(f"unsupported strength: {strength} (use 128 or 256)")
|
|
79
|
+
return str(Bip39MnemonicGenerator().FromWordsNumber(words_num).ToStr())
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def validate_mnemonic(mnemonic: str) -> bool:
|
|
83
|
+
"""Validate a BIP-39 mnemonic against the English wordlist and checksum.
|
|
84
|
+
|
|
85
|
+
:returns: ``True`` if valid; ``False`` otherwise. Never raises.
|
|
86
|
+
"""
|
|
87
|
+
return bool(Bip39MnemonicValidator().IsValid(mnemonic))
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _resolve_index(account_index: int) -> int:
|
|
91
|
+
if not isinstance(account_index, int) or isinstance(account_index, bool) or account_index < 0:
|
|
92
|
+
raise ValueError(
|
|
93
|
+
f"account_index must be a non-negative integer, got {account_index}"
|
|
94
|
+
)
|
|
95
|
+
return account_index
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _seed_from_mnemonic(mnemonic: str) -> bytes:
|
|
99
|
+
"""Validate a mnemonic and derive its BIP-39 seed.
|
|
100
|
+
|
|
101
|
+
Centralizing this here guards against the fund-loss footgun where a typo'd
|
|
102
|
+
phrase (valid words, wrong checksum) would silently derive a valid-looking
|
|
103
|
+
but WRONG account. The error deliberately omits the mnemonic text.
|
|
104
|
+
"""
|
|
105
|
+
if not validate_mnemonic(mnemonic):
|
|
106
|
+
raise ValueError("invalid mnemonic")
|
|
107
|
+
return bytes(Bip39SeedGenerator(mnemonic).Generate())
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def derive_native_account(mnemonic: str, account_index: int = 0) -> Secp256k1Account:
|
|
111
|
+
"""Derive a native QoreChain account (Cosmos-style secp256k1).
|
|
112
|
+
|
|
113
|
+
Path: ``m/44'/118'/0'/0/{account_index}``. The address is the bech32
|
|
114
|
+
(``qor``) encoding of ``ripemd160(sha256(compressed_public_key))``.
|
|
115
|
+
"""
|
|
116
|
+
index = _resolve_index(account_index)
|
|
117
|
+
seed = _seed_from_mnemonic(mnemonic)
|
|
118
|
+
node = Bip32Slip10Secp256k1.FromSeed(seed).DerivePath(
|
|
119
|
+
f"44'/{_COIN_TYPE_NATIVE}'/0'/0/{index}"
|
|
120
|
+
)
|
|
121
|
+
compressed = bytes(node.PublicKey().RawCompressed().ToBytes())
|
|
122
|
+
digest = hashlib.new("ripemd160", hashlib.sha256(compressed).digest()).digest()
|
|
123
|
+
address = str(Bech32Encoder.Encode(NATIVE_PREFIX, digest))
|
|
124
|
+
private_key = bytes(node.PrivateKey().Raw().ToBytes())
|
|
125
|
+
return Secp256k1Account(
|
|
126
|
+
type="native", address=address, public_key=compressed, private_key=private_key
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def derive_evm_account(mnemonic: str, account_index: int = 0) -> Secp256k1Account:
|
|
131
|
+
"""Derive an EVM account from a mnemonic.
|
|
132
|
+
|
|
133
|
+
Path: ``m/44'/60'/0'/0/{account_index}``. The address is the last 20 bytes
|
|
134
|
+
of ``keccak256(uncompressed_public_key[1:])``, EIP-55 checksummed.
|
|
135
|
+
"""
|
|
136
|
+
index = _resolve_index(account_index)
|
|
137
|
+
seed = _seed_from_mnemonic(mnemonic)
|
|
138
|
+
node = Bip32Slip10Secp256k1.FromSeed(seed).DerivePath(
|
|
139
|
+
f"44'/{_COIN_TYPE_EVM}'/0'/0/{index}"
|
|
140
|
+
)
|
|
141
|
+
pub = node.PublicKey()
|
|
142
|
+
compressed = bytes(pub.RawCompressed().ToBytes())
|
|
143
|
+
# EthAddrEncoder applies keccak256(uncompressed[1:])[-20:] with EIP-55.
|
|
144
|
+
address = str(EthAddrEncoder.EncodeKey(pub.KeyObject()))
|
|
145
|
+
private_key = bytes(node.PrivateKey().Raw().ToBytes())
|
|
146
|
+
return Secp256k1Account(
|
|
147
|
+
type="evm", address=address, public_key=compressed, private_key=private_key
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def derive_svm_account(mnemonic: str, account_index: int = 0) -> Ed25519Account:
|
|
152
|
+
"""Derive an SVM (Solana-style ed25519) account from a mnemonic.
|
|
153
|
+
|
|
154
|
+
Path: ``m/44'/501'/{account_index}'/0'`` — the conventional Solana
|
|
155
|
+
derivation, all segments hardened (SLIP-0010 for ed25519 supports hardened
|
|
156
|
+
keys only). The address is the base58 encoding of the 32-byte public key.
|
|
157
|
+
The returned ``secret_key`` is the 64-byte Solana form
|
|
158
|
+
(``private_seed32 || public_key32``).
|
|
159
|
+
"""
|
|
160
|
+
index = _resolve_index(account_index)
|
|
161
|
+
seed = _seed_from_mnemonic(mnemonic)
|
|
162
|
+
node = Bip32Slip10Ed25519.FromSeed(seed).DerivePath(
|
|
163
|
+
f"44'/{_COIN_TYPE_SVM}'/{index}'/0'"
|
|
164
|
+
)
|
|
165
|
+
# bip-utils prepends a 0x00 prefix byte to raw ed25519 public keys.
|
|
166
|
+
raw_pub = bytes(node.PublicKey().RawCompressed().ToBytes())
|
|
167
|
+
public_key = raw_pub[1:] if len(raw_pub) == 33 else raw_pub
|
|
168
|
+
private_seed = bytes(node.PrivateKey().Raw().ToBytes())
|
|
169
|
+
secret_key = private_seed + public_key
|
|
170
|
+
address = base58.b58encode(public_key).decode("ascii")
|
|
171
|
+
return Ed25519Account(
|
|
172
|
+
type="svm", address=address, public_key=public_key, secret_key=secret_key
|
|
173
|
+
)
|
qorechain/address.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Conversion and validation for QoreChain bech32 addresses (e.g. ``qor1...``)
|
|
2
|
+
and their underlying byte payloads expressed as ``0x``-prefixed hex.
|
|
3
|
+
|
|
4
|
+
bech32 stores data as 5-bit groups ("words"), so encoding/decoding converts
|
|
5
|
+
between those groups and the 8-bit byte representation callers work with.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
|
|
12
|
+
from bech32 import bech32_decode, bech32_encode, convertbits
|
|
13
|
+
|
|
14
|
+
#: Default bech32 human-readable prefix for QoreChain account addresses.
|
|
15
|
+
DEFAULT_PREFIX = "qor"
|
|
16
|
+
|
|
17
|
+
_HEX_RE = re.compile(r"^[0-9a-fA-F]+$")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _strip_hex_prefix(hex_str: str) -> str:
|
|
21
|
+
return hex_str[2:] if hex_str[:2] in ("0x", "0X") else hex_str
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _hex_to_bytes(hex_str: str) -> bytes:
|
|
25
|
+
body = _strip_hex_prefix(hex_str)
|
|
26
|
+
if not body or len(body) % 2 != 0 or not _HEX_RE.match(body):
|
|
27
|
+
raise ValueError(f"invalid hex string: {hex_str}")
|
|
28
|
+
return bytes.fromhex(body)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def bytes_to_bech32(data: bytes, prefix: str = DEFAULT_PREFIX) -> str:
|
|
32
|
+
"""Encode raw bytes to a bech32 address with the given prefix.
|
|
33
|
+
|
|
34
|
+
This is the primitive encoder; callers holding a ``bytes`` payload (e.g. the
|
|
35
|
+
20-byte ``ripemd160(sha256(pubkey))`` account hash) should use it directly
|
|
36
|
+
rather than round-tripping through hex.
|
|
37
|
+
"""
|
|
38
|
+
words = convertbits(data, 8, 5, pad=True)
|
|
39
|
+
if words is None:
|
|
40
|
+
raise ValueError("failed to convert bytes to bech32 words")
|
|
41
|
+
encoded = bech32_encode(prefix, words)
|
|
42
|
+
if encoded is None:
|
|
43
|
+
raise ValueError("failed to encode bech32 address")
|
|
44
|
+
return encoded
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def hex_to_bech32(hex_str: str, prefix: str = DEFAULT_PREFIX) -> str:
|
|
48
|
+
"""Encode hex bytes to a bech32 address with the given prefix.
|
|
49
|
+
|
|
50
|
+
:raises ValueError: If ``hex_str`` is not a valid hex string.
|
|
51
|
+
"""
|
|
52
|
+
return bytes_to_bech32(_hex_to_bytes(hex_str), prefix)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def bech32_to_hex(addr: str) -> str:
|
|
56
|
+
"""Decode a bech32 address to a ``0x``-prefixed hex string of its payload.
|
|
57
|
+
|
|
58
|
+
:raises ValueError: If ``addr`` is not a valid bech32 string.
|
|
59
|
+
"""
|
|
60
|
+
_hrp, words = bech32_decode(addr)
|
|
61
|
+
if words is None:
|
|
62
|
+
raise ValueError(f"invalid bech32 address: {addr}")
|
|
63
|
+
data = convertbits(words, 5, 8, pad=False)
|
|
64
|
+
if data is None:
|
|
65
|
+
raise ValueError(f"invalid bech32 payload: {addr}")
|
|
66
|
+
return "0x" + bytes(data).hex()
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def is_valid_bech32(addr: str, prefix: str | None = None) -> bool:
|
|
70
|
+
"""Validate a bech32 address, optionally requiring a specific prefix.
|
|
71
|
+
|
|
72
|
+
:returns: ``True`` if ``addr`` is a structurally valid bech32 string
|
|
73
|
+
(correct checksum) and, when ``prefix`` is supplied, its prefix matches;
|
|
74
|
+
``False`` otherwise. Never raises.
|
|
75
|
+
"""
|
|
76
|
+
hrp, words = bech32_decode(addr)
|
|
77
|
+
if hrp is None or words is None:
|
|
78
|
+
return False
|
|
79
|
+
return True if prefix is None else hrp == prefix
|
qorechain/client.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""Top-level ``create_client`` factory for the QoreChain Python SDK.
|
|
2
|
+
|
|
3
|
+
:func:`create_client` resolves a :class:`~qorechain.networks.NetworkConfig`
|
|
4
|
+
(applying any endpoint overrides) and composes the read clients
|
|
5
|
+
(:class:`~qorechain.rest.RestClient` and the ``qor_`` :class:`~qorechain.qor.QorClient`)
|
|
6
|
+
plus a fee-estimate convenience.
|
|
7
|
+
|
|
8
|
+
Network resolution rules:
|
|
9
|
+
- The default network is ``testnet``. Both ``testnet`` and ``mainnet`` are live
|
|
10
|
+
and ship localhost endpoint defaults; callers can override them with real
|
|
11
|
+
hostnames.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from dataclasses import dataclass, replace
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
from .fees import estimate_fee
|
|
20
|
+
from .networks import NETWORKS, NetworkConfig, NetworkEndpoints
|
|
21
|
+
from .qor import QorClient
|
|
22
|
+
from .rest import FeeUrgency, RestClient
|
|
23
|
+
|
|
24
|
+
_ENDPOINT_KEYS = ("rest", "grpc", "rpc", "evm_rpc", "evm_ws", "svm_rpc")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class _Fees:
|
|
28
|
+
"""Fee-estimate convenience surface bound to a :class:`RestClient`."""
|
|
29
|
+
|
|
30
|
+
def __init__(self, rest: RestClient) -> None:
|
|
31
|
+
self._rest = rest
|
|
32
|
+
|
|
33
|
+
def estimate(self, urgency: FeeUrgency = "normal") -> dict[str, Any]:
|
|
34
|
+
"""Estimate a fee for the given urgency via the AI fee oracle.
|
|
35
|
+
|
|
36
|
+
Falls back to a deterministic static fee when the oracle is unavailable.
|
|
37
|
+
Returns a Cosmos ``StdFee``-shaped dict: ``{"amount": [...], "gas": ...}``.
|
|
38
|
+
"""
|
|
39
|
+
return estimate_fee(self._rest, urgency=urgency)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class QoreChainClient:
|
|
44
|
+
"""A composed QoreChain client: resolved config, read clients, fee helper."""
|
|
45
|
+
|
|
46
|
+
network: NetworkConfig
|
|
47
|
+
rest: RestClient
|
|
48
|
+
qor: QorClient
|
|
49
|
+
fees: _Fees
|
|
50
|
+
|
|
51
|
+
def close(self) -> None:
|
|
52
|
+
self.rest.close()
|
|
53
|
+
self.qor.close()
|
|
54
|
+
|
|
55
|
+
def __enter__(self) -> QoreChainClient:
|
|
56
|
+
return self
|
|
57
|
+
|
|
58
|
+
def __exit__(self, *exc: object) -> None:
|
|
59
|
+
self.close()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _resolve_network(
|
|
63
|
+
network: str, endpoints: dict[str, str] | None, chain_id: str | None
|
|
64
|
+
) -> NetworkConfig:
|
|
65
|
+
overrides = endpoints or {}
|
|
66
|
+
bad = set(overrides) - set(_ENDPOINT_KEYS)
|
|
67
|
+
if bad:
|
|
68
|
+
raise ValueError(f"unknown endpoint keys: {sorted(bad)}")
|
|
69
|
+
|
|
70
|
+
base = NETWORKS.get(network)
|
|
71
|
+
if base is None:
|
|
72
|
+
raise ValueError(f"unknown network: {network}")
|
|
73
|
+
|
|
74
|
+
# Live preset (testnet or mainnet): overlay endpoint overrides onto the defaults.
|
|
75
|
+
current = {k: getattr(base.endpoints, k) for k in _ENDPOINT_KEYS}
|
|
76
|
+
current.update(overrides)
|
|
77
|
+
return replace(
|
|
78
|
+
base,
|
|
79
|
+
chain_id=chain_id or base.chain_id,
|
|
80
|
+
endpoints=NetworkEndpoints(**current),
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _require_endpoint(network: NetworkConfig, key: str) -> str:
|
|
85
|
+
value: str | None = getattr(network.endpoints, key, None) if network.endpoints else None
|
|
86
|
+
if not value:
|
|
87
|
+
raise ValueError(
|
|
88
|
+
f'endpoint "{key}" is not configured — pass it via '
|
|
89
|
+
f"create_client(endpoints={{'{key}': '...'}})"
|
|
90
|
+
)
|
|
91
|
+
return value
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def create_client(
|
|
95
|
+
network: str = "testnet",
|
|
96
|
+
endpoints: dict[str, str] | None = None,
|
|
97
|
+
*,
|
|
98
|
+
chain_id: str | None = None,
|
|
99
|
+
timeout: float = 30.0,
|
|
100
|
+
) -> QoreChainClient:
|
|
101
|
+
"""Create a composed :class:`QoreChainClient`.
|
|
102
|
+
|
|
103
|
+
:param network: Network preset to target. Defaults to ``"testnet"``.
|
|
104
|
+
:param endpoints: Endpoint overrides (keys: ``rest``, ``grpc``, ``rpc``,
|
|
105
|
+
``evm_rpc``, ``evm_ws``, ``svm_rpc``). Both presets default to localhost.
|
|
106
|
+
:param chain_id: Chain ID override.
|
|
107
|
+
:raises ValueError: If the network or an endpoint key is unknown.
|
|
108
|
+
"""
|
|
109
|
+
resolved = _resolve_network(network, endpoints, chain_id)
|
|
110
|
+
rest = RestClient(_require_endpoint(resolved, "rest"), timeout=timeout)
|
|
111
|
+
qor = QorClient(_require_endpoint(resolved, "evm_rpc"), timeout=timeout)
|
|
112
|
+
return QoreChainClient(network=resolved, rest=rest, qor=qor, fees=_Fees(rest))
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
__all__ = ["QoreChainClient", "create_client"]
|
qorechain/denom.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Conversion between human display amounts and integer base amounts.
|
|
2
|
+
|
|
3
|
+
All value math is performed with integer arithmetic on decimal strings — there
|
|
4
|
+
is no floating-point arithmetic anywhere in this module, so conversions are
|
|
5
|
+
exact for any magnitude and never drift (e.g. ``to_base("0.1") == "100000"``).
|
|
6
|
+
|
|
7
|
+
QoreChain's staking coin uses a default exponent of ``6`` (1 QOR = 10^6 uqor),
|
|
8
|
+
but every function accepts a custom ``exponent`` for other denominations.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import re
|
|
14
|
+
|
|
15
|
+
#: The QoreChain staking coin's default decimal exponent (1 QOR = 10^6 uqor).
|
|
16
|
+
DEFAULT_EXPONENT = 6
|
|
17
|
+
|
|
18
|
+
_DECIMAL_RE = re.compile(r"^\d+(\.\d+)?$")
|
|
19
|
+
_INT_RE = re.compile(r"^\d+$")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _resolve_exponent(exponent: int) -> int:
|
|
23
|
+
if not isinstance(exponent, int) or isinstance(exponent, bool) or exponent < 0:
|
|
24
|
+
raise ValueError(f"invalid exponent: {exponent} (must be a non-negative integer)")
|
|
25
|
+
return exponent
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def to_base(amount: str, exponent: int = DEFAULT_EXPONENT) -> str:
|
|
29
|
+
"""Convert a human display amount to its integer base amount string.
|
|
30
|
+
|
|
31
|
+
:param amount: A non-negative decimal string, e.g. ``"1.5"``. Surrounding
|
|
32
|
+
whitespace and a single leading ``+`` are tolerated. Scientific
|
|
33
|
+
notation, thousands separators, and other formatting are rejected.
|
|
34
|
+
:param exponent: Decimal exponent (defaults to 6).
|
|
35
|
+
:returns: The integer base amount as a string with no leading zeros.
|
|
36
|
+
:raises ValueError: If ``amount`` is not a valid decimal string, is
|
|
37
|
+
negative, or has more fractional digits than ``exponent`` allows.
|
|
38
|
+
"""
|
|
39
|
+
exponent = _resolve_exponent(exponent)
|
|
40
|
+
body = amount.strip()
|
|
41
|
+
|
|
42
|
+
if body.startswith("-"):
|
|
43
|
+
raise ValueError(f"negative amounts are not supported: {amount}")
|
|
44
|
+
if body.startswith("+"):
|
|
45
|
+
body = body[1:]
|
|
46
|
+
|
|
47
|
+
if not _DECIMAL_RE.match(body):
|
|
48
|
+
raise ValueError(f"invalid decimal amount: {amount}")
|
|
49
|
+
|
|
50
|
+
int_part, _, frac_part = body.partition(".")
|
|
51
|
+
if len(frac_part) > exponent:
|
|
52
|
+
raise ValueError(
|
|
53
|
+
f"too many decimal places in {amount}: {len(frac_part)} > exponent {exponent}"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
padded = frac_part.ljust(exponent, "0")
|
|
57
|
+
return str(int(int_part + padded))
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def from_base(base: str, exponent: int = DEFAULT_EXPONENT) -> str:
|
|
61
|
+
"""Convert an integer base amount string to a normalized display string.
|
|
62
|
+
|
|
63
|
+
:param base: A non-negative integer string, e.g. ``"1500000"``.
|
|
64
|
+
:param exponent: Decimal exponent (defaults to 6).
|
|
65
|
+
:returns: The display amount with no trailing zeros and no trailing dot,
|
|
66
|
+
e.g. ``"1.5"``. ``"1000000"`` becomes ``"1"``, ``"1"`` becomes
|
|
67
|
+
``"0.000001"``, ``"0"`` becomes ``"0"``.
|
|
68
|
+
:raises ValueError: If ``base`` is not a valid non-negative integer string.
|
|
69
|
+
"""
|
|
70
|
+
exponent = _resolve_exponent(exponent)
|
|
71
|
+
trimmed = base.strip()
|
|
72
|
+
|
|
73
|
+
if trimmed.startswith("-"):
|
|
74
|
+
raise ValueError(f"negative amounts are not supported: {base}")
|
|
75
|
+
if not _INT_RE.match(trimmed):
|
|
76
|
+
raise ValueError(f"invalid base amount: {base}")
|
|
77
|
+
|
|
78
|
+
if exponent == 0:
|
|
79
|
+
return str(int(trimmed))
|
|
80
|
+
|
|
81
|
+
padded = trimmed.rjust(exponent + 1, "0")
|
|
82
|
+
int_part = padded[: len(padded) - exponent]
|
|
83
|
+
frac_part = padded[len(padded) - exponent :]
|
|
84
|
+
|
|
85
|
+
normalized_int = str(int(int_part))
|
|
86
|
+
trimmed_frac = frac_part.rstrip("0")
|
|
87
|
+
return f"{normalized_int}.{trimmed_frac}" if trimmed_frac else normalized_int
|