Construct and sign Quantum transactions from Python.
py-quantum bundles the controlled native implementation for key generation, address derivation, signing, verification, and canonical transaction construction. Python never reimplements the cryptography.
Create a clean environment and install the private wheel.
Supported releases target CPython 3.10 through 3.14 on Linux x86-64 with glibc 2.34 or newer and Windows x86-64. Normal consumers do not need a compiler or system native dependency.
Linux
mkdir quantum-python-demo
cd quantum-python-demo
python3 -m venv .venv
. .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install \
--index-url "https://SDK_USERNAME:SDK_TOKEN@sdk.quantumapi.io/simple/" \
py-quantum==0.3.0Windows PowerShell
mkdir quantum-python-demo
cd quantum-python-demo
py -m venv .venv
.\.venv\Scripts\Activate.ps1
python -m pip install --upgrade pip
python -m pip install `
--index-url "https://SDK_USERNAME:SDK_TOKEN@sdk.quantumapi.io/simple/" `
py-quantum==0.3.0Send a real transaction on Quantum Sandbox.
Create send_quantum.py. This uses only the Python standard library for HTTP. The SDK remains RPC-free and owns only native keys and canonical transaction construction.
import json
import time
from urllib.request import Request, urlopen
from py_quantum import (
LegacyTransactionRequest,
Signer,
build_and_sign_transaction,
parse_quantum,
)
RPC = "https://rpc.sandbox.qustody.io"
FAUCET = "https://faucet.sandbox.qustody.io/fund"
CHAIN_ID = 20804
GENESIS = "0x3d9c6813ee3edd227abb5345aff55d6e2e76bc9db3a031d92d0e2bd54c8997e7"
def post_json(url, payload):
request = Request(
url,
data=json.dumps(payload).encode("utf-8"),
headers={"content-type": "application/json"},
method="POST",
)
with urlopen(request, timeout=30) as response:
return json.loads(response.read())
def rpc(method, params):
response = post_json(RPC, {
"jsonrpc": "2.0", "id": 1, "method": method, "params": params,
})
if "error" in response:
raise RuntimeError(f"{method} failed: {response['error']}")
return response["result"]
with Signer.generate() as sender, Signer.generate() as recipient:
chain_id = int(rpc("qc_chainId", []), 16)
genesis = rpc("qc_getBlockByNumber", ["0x0", False])["hash"].lower()
if chain_id != CHAIN_ID or genesis != GENESIS:
raise RuntimeError("refusing to sign for an unexpected network")
funding = post_json(FAUCET, {"address": sender.address_hex})
print("sender", sender.address_hex)
print("faucet", funding)
deadline = time.monotonic() + 120
while int(rpc("qc_getBalance", [sender.address_hex, "latest"]), 16) == 0:
if time.monotonic() > deadline:
raise TimeoutError("faucet balance did not arrive")
time.sleep(1)
value = parse_quantum("0.01")
call = {
"from": sender.address_hex,
"to": recipient.address_hex,
"value": hex(value),
"data": "0x",
}
nonce = int(rpc("qc_getTransactionCount", [sender.address_hex, "pending"]), 16)
gas_limit = int(rpc("qc_estimateGas", [call]), 16)
gas_price = int(rpc("qc_gasPrice", []), 16)
result = build_and_sign_transaction(
LegacyTransactionRequest(
chain_id=CHAIN_ID,
nonce=nonce,
gas_limit=gas_limit,
gas_price=gas_price,
to=recipient.address_hex,
value=value,
),
sender,
)
node_hash = rpc("qc_sendRawTransaction", [result.raw_transaction]).lower()
if node_hash != result.transaction_hash.lower():
raise RuntimeError("node returned a different transaction hash")
deadline = time.monotonic() + 120
receipt = None
while receipt is None:
receipt = rpc("qc_getTransactionReceipt", [result.transaction_hash])
if receipt is None and time.monotonic() > deadline:
raise TimeoutError("transaction receipt did not arrive")
if receipt is None:
time.sleep(1)
if int(receipt["status"], 16) != 1:
raise RuntimeError("transaction execution failed")
print("transaction", result.transaction_hash)
print("block", int(receipt["blockNumber"], 16))
print("status", int(receipt["status"], 16))
print("explorer", f"https://explorer.sandbox.qustody.io/tx/{result.transaction_hash}")python send_quantum.pyExpected final output includes the sender address, transaction hash, block number, receipt status 1, and an explorer URL. HTTP 403 means your public egress address is not approved for the hosted Sandbox.
The Python SDK is intentionally RPC-free.
| The SDK owns | Your application owns |
|---|---|
| Native key generation and address derivation | RPC endpoint and network identity policy |
| Signing an exact 32-byte digest | Nonce and fee discovery |
| Native signature verification | Gas estimation |
| Canonical transaction construction and serialization | Raw transaction submission and retry policy |
| Local transaction hash | Receipt polling, confirmations, and lifecycle |
The quickstart shows one minimal caller implementation so a Python team can integrate end to end. Production adapters should use their established HTTP, observability, retry, and nonce coordination layers.
Keys stay behind an opaque native handle.
Signer.generate()create a native signersigner.public_keycanonical 1,952-byte public keysigner.address20-byte addresssigner.address_hexlowercase 0x addresssigner.closedwhether the handle was releasedsigner.sign_digest(digest)3,293-byte signature over exactly 32 bytessigner.close()idempotently release the native handleverify_digest(key, digest, signature)boolean for a valid or invalid signaturefrom py_quantum import Signer, verify_digest
digest = bytes.fromhex(
"5b336411c12d54f162ef83653f01897ffb8b0987c666d34bed2920641832a4ef"
)
with Signer.generate() as signer:
signature = signer.sign_digest(digest)
assert verify_digest(signer.public_key, digest, signature)
print(signer.address_hex)Accepted binary inputs are bytes, bytearray, and contiguous one-dimensional memoryview values. Strings are never silently converted into signing data.
Supply every resolved chain field.
LegacyTransactionRequest(chain_id, nonce, gas_limit, gas_price, to=None, value=0, data=b"")DynamicTransactionRequest(chain_id, nonce, gas_limit, max_fee_per_gas, max_priority_fee_per_gas, to=None, value=0, data=b"", access_list=())Integer values are atomic units. A decimal string value is Quantum and may have at most 18 fractional digits. Omit to for contract creation. A string data value must be strict, even-length, and 0x-prefixed.
build_transaction(request)immutable normalized unsigned transactionsign_transaction(transaction, signer)sign a previously built transactionbuild_and_sign_transaction(request, signer)build and sign in one callserialize_transaction(transaction)canonical bytesserialize_transaction_hex(transaction)submission-ready 0x bytesdeserialize_transaction(raw)structured transactionget_transaction_signing_digest(unsigned)exact 32-byte digestattach_transaction_signature(...)signed transactionget_transaction_hash(signed)canonical local hashReturn all adapter integration material.
| Attribute | Type | Meaning |
|---|---|---|
unsigned_transaction | transaction | Normalized unsigned fields |
signed_transaction | transaction | Same fields with signature and public key |
signing_digest | bytes | Exact 32-byte signer input |
signature | bytes | Signature embedded in the transaction |
public_key | bytes | Public key embedded in the transaction |
raw_transaction_bytes | bytes | Canonical signed bytes |
raw_transaction | str | 0x bytes for qc_sendRawTransaction |
transaction_hash | str | Hash computed from the exact signed bytes |
Reject malformed input before native signing.
The hierarchy includes QuantumError, InvalidArgumentError, ClosedSignerError, NativeLibraryError, UnsupportedPlatformError, InvalidTransactionError, SerializationError, and SigningError. An invalid signature normally returns False. Invalid key or digest lengths raise an argument error.
- Use a context manager so native handles are released.
- Never log private key material or place it in an environment variable.
- Validate chain ID and genesis outside the SDK before signing.
- Compare the node-returned hash with
transaction_hash. - Never retry raw submission without reconciling the transaction and pending nonce.
- Pin the private wheel version and retain the issued credentials in a secret store.