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.
Files changed (96) hide show
  1. qorechain/__init__.py +239 -0
  2. qorechain/accounts.py +173 -0
  3. qorechain/address.py +79 -0
  4. qorechain/client.py +115 -0
  5. qorechain/denom.py +87 -0
  6. qorechain/errors.py +150 -0
  7. qorechain/fees.py +67 -0
  8. qorechain/gas.py +186 -0
  9. qorechain/jsonrpc.py +112 -0
  10. qorechain/messages/__init__.py +97 -0
  11. qorechain/messages/_composer.py +60 -0
  12. qorechain/messages/cosmos.py +105 -0
  13. qorechain/messages/qorechain.py +242 -0
  14. qorechain/messages/registry.py +210 -0
  15. qorechain/networks.py +109 -0
  16. qorechain/pqc.py +134 -0
  17. qorechain/proto/__init__.py +1 -0
  18. qorechain/proto/qorechain/__init__.py +1 -0
  19. qorechain/proto/qorechain/abstractaccount/__init__.py +1 -0
  20. qorechain/proto/qorechain/abstractaccount/v1/__init__.py +1 -0
  21. qorechain/proto/qorechain/abstractaccount/v1/tx_pb2.py +64 -0
  22. qorechain/proto/qorechain/abstractaccount/v1/tx_pb2.pyi +51 -0
  23. qorechain/proto/qorechain/amm/__init__.py +1 -0
  24. qorechain/proto/qorechain/amm/v1/__init__.py +1 -0
  25. qorechain/proto/qorechain/amm/v1/tx_pb2.py +139 -0
  26. qorechain/proto/qorechain/amm/v1/tx_pb2.pyi +127 -0
  27. qorechain/proto/qorechain/bridge/__init__.py +1 -0
  28. qorechain/proto/qorechain/bridge/v1/__init__.py +1 -0
  29. qorechain/proto/qorechain/bridge/v1/tx_pb2.py +84 -0
  30. qorechain/proto/qorechain/bridge/v1/tx_pb2.pyi +89 -0
  31. qorechain/proto/qorechain/crossvm/__init__.py +1 -0
  32. qorechain/proto/qorechain/crossvm/v1/__init__.py +1 -0
  33. qorechain/proto/qorechain/crossvm/v1/query_pb2.py +51 -0
  34. qorechain/proto/qorechain/crossvm/v1/query_pb2.pyi +74 -0
  35. qorechain/proto/qorechain/crossvm/v1/tx_pb2.py +67 -0
  36. qorechain/proto/qorechain/crossvm/v1/tx_pb2.pyi +42 -0
  37. qorechain/proto/qorechain/license/__init__.py +1 -0
  38. qorechain/proto/qorechain/license/v1/__init__.py +1 -0
  39. qorechain/proto/qorechain/license/v1/tx_pb2.py +90 -0
  40. qorechain/proto/qorechain/license/v1/tx_pb2.pyi +68 -0
  41. qorechain/proto/qorechain/lightnode/__init__.py +1 -0
  42. qorechain/proto/qorechain/lightnode/v1/__init__.py +1 -0
  43. qorechain/proto/qorechain/lightnode/v1/query_pb2.py +59 -0
  44. qorechain/proto/qorechain/lightnode/v1/query_pb2.pyi +108 -0
  45. qorechain/proto/qorechain/lightnode/v1/tx_pb2.py +73 -0
  46. qorechain/proto/qorechain/lightnode/v1/tx_pb2.pyi +54 -0
  47. qorechain/proto/qorechain/multilayer/__init__.py +1 -0
  48. qorechain/proto/qorechain/multilayer/v1/__init__.py +1 -0
  49. qorechain/proto/qorechain/multilayer/v1/tx_pb2.py +118 -0
  50. qorechain/proto/qorechain/multilayer/v1/tx_pb2.pyi +155 -0
  51. qorechain/proto/qorechain/pqc/__init__.py +1 -0
  52. qorechain/proto/qorechain/pqc/v1/__init__.py +1 -0
  53. qorechain/proto/qorechain/pqc/v1/query_pb2.py +43 -0
  54. qorechain/proto/qorechain/pqc/v1/query_pb2.pyi +41 -0
  55. qorechain/proto/qorechain/pqc/v1/tx_pb2.py +96 -0
  56. qorechain/proto/qorechain/pqc/v1/tx_pb2.pyi +92 -0
  57. qorechain/proto/qorechain/qca/__init__.py +1 -0
  58. qorechain/proto/qorechain/qca/v1/__init__.py +1 -0
  59. qorechain/proto/qorechain/qca/v1/query_pb2.py +41 -0
  60. qorechain/proto/qorechain/qca/v1/query_pb2.pyi +49 -0
  61. qorechain/proto/qorechain/rdk/__init__.py +1 -0
  62. qorechain/proto/qorechain/rdk/v1/__init__.py +1 -0
  63. qorechain/proto/qorechain/rdk/v1/tx_pb2.py +114 -0
  64. qorechain/proto/qorechain/rdk/v1/tx_pb2.pyi +122 -0
  65. qorechain/proto/qorechain/reputation/__init__.py +1 -0
  66. qorechain/proto/qorechain/reputation/v1/__init__.py +1 -0
  67. qorechain/proto/qorechain/reputation/v1/query_pb2.py +41 -0
  68. qorechain/proto/qorechain/reputation/v1/query_pb2.pyi +24 -0
  69. qorechain/proto/qorechain/rlconsensus/__init__.py +1 -0
  70. qorechain/proto/qorechain/rlconsensus/v1/__init__.py +1 -0
  71. qorechain/proto/qorechain/rlconsensus/v1/query_pb2.py +62 -0
  72. qorechain/proto/qorechain/rlconsensus/v1/query_pb2.pyi +115 -0
  73. qorechain/proto/qorechain/rlconsensus/v1/tx_pb2.py +69 -0
  74. qorechain/proto/qorechain/rlconsensus/v1/tx_pb2.pyi +61 -0
  75. qorechain/proto/qorechain/svm/__init__.py +1 -0
  76. qorechain/proto/qorechain/svm/v1/__init__.py +1 -0
  77. qorechain/proto/qorechain/svm/v1/query_pb2.py +53 -0
  78. qorechain/proto/qorechain/svm/v1/query_pb2.pyi +71 -0
  79. qorechain/proto/qorechain/svm/v1/tx_pb2.py +90 -0
  80. qorechain/proto/qorechain/svm/v1/tx_pb2.pyi +83 -0
  81. qorechain/py.typed +0 -0
  82. qorechain/qor.py +218 -0
  83. qorechain/query/__init__.py +27 -0
  84. qorechain/query/grpc.py +296 -0
  85. qorechain/rest.py +222 -0
  86. qorechain/search.py +92 -0
  87. qorechain/subscribe.py +162 -0
  88. qorechain/track.py +184 -0
  89. qorechain/tx.py +427 -0
  90. qorechain/utils/__init__.py +41 -0
  91. qorechain/utils/hash.py +58 -0
  92. qorechain/utils/units.py +94 -0
  93. qorechain/utils/validation.py +94 -0
  94. qorechain_sdk-0.3.0.dist-info/METADATA +301 -0
  95. qorechain_sdk-0.3.0.dist-info/RECORD +96 -0
  96. 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