Phantasma Python SDK
Typed Python SDK for the Phantasma blockchain with support for the Phoenix chain update.
The package provides JSON-RPC access, transaction building and signing, VM script helpers, Ed25519 keys/signatures, and Carbon wire-format support.
The public API uses Python naming conventions, dataclasses, exceptions, and type hints. Fixture tests lock shared VM and Carbon wire formats against reference SDK vectors.
Requirements
- Python 3.11 or newer
cryptographyrequests
Development uses uv, ruff, mypy, and pytest.
Install
pip install phantasma-sdk-py
For local development:
uv sync --extra dev
just check
Modules
phantasma_py.crypto: addresses, hashes, WIF keys, Ed25519 signaturesphantasma_py.vm: VM objects, opcodes, andScriptBuilderphantasma_py.transaction: VM script transaction serialization/signingphantasma_py.rpc: JSON-RPC client and typed response dataclassesphantasma_py.carbon: Carbon primitives, VM schemas, module call args, token builders, and transaction messages
RPC
from phantasma_py.rpc import PhantasmaRPC
rpc = PhantasmaRPC.mainnet()
account = rpc.get_account("P...")
balance = account.get_token_balance("SOUL", decimals=8)
print(balance.decimal_amount())
JsonRpcClient validates JSON-RPC response ids, propagates RPC errors as
RPCError, and accepts endpoints that echo numeric ids as strings.
Keys And Signatures
from phantasma_py.crypto import PhantasmaKeys
keys = PhantasmaKeys.from_wif("...")
signature = keys.sign(b"message")
assert signature.verify(b"message", [keys.address])
Address parsing rejects malformed Base58/checksum data. Address.from_text("NULL")
and Address.null() produce the system null address.
VM Scripts
from phantasma_py.crypto import Address, PhantasmaKeys
from phantasma_py.vm import ScriptBuilder
keys = PhantasmaKeys.from_wif("...")
script = (
ScriptBuilder.begin()
.allow_gas(keys.address, Address.null(), gas_price=10_000, gas_limit=210_000)
.call_contract("stake", "GetStake", keys.address)
.spend_gas(keys.address)
.end_script()
)
end_script() raises BuilderError if labels or user input are invalid.
end_script_with_error() returns (script, error) for callers that prefer an
explicit checked path.
VM Script Transactions
from phantasma_py.crypto import PhantasmaKeys
from phantasma_py.transaction import Transaction
from phantasma_py.vm import ScriptBuilder
keys = PhantasmaKeys.from_wif("...")
script = ScriptBuilder.begin().call_interop("Runtime.Time").end_script()
tx = Transaction("mainnet", "main", script, expiration=1_754_000_000)
tx.sign(keys)
raw_hex = tx.to_bytes().hex()
Broadcasting is intentionally separate from signing:
tx_hash = rpc.send_raw_transaction(raw_hex)
Do not run broadcasting examples without explicit credentials, funds, and an endpoint you intend to use.
Carbon
Carbon serialization uses fixed-width little-endian integers, zero-terminated
strings, fixed byte types, compact signed Int256 values, and typed transaction
payloads. Use serialize() and deserialize() for stable wire round-trips.
from phantasma_py.carbon import (
Bytes32,
IntX,
build_token_info,
build_token_metadata,
prepare_standard_token_schemas,
serialize,
)
owner = Bytes32()
schemas = prepare_standard_token_schemas(shared_metadata=False)
token = build_token_info(
symbol="ART",
max_supply=IntX(0),
is_nft=True,
decimals=0,
owner=owner,
metadata=build_token_metadata(
{
"name": "Art Token",
"icon": "data:image/png;base64,AA==",
"url": "https://example.invalid/art",
"description": "Example token metadata",
}
),
token_schemas=serialize(schemas),
)
payload = serialize(token)
Carbon token and NFT helpers validate required metadata, token symbol casing,
standard schema fields, Carbon NFT address packing, and result parsing. Token
symbols follow the Carbon token-module rule of uppercase ASCII letters A-Z.
Fee planning: build, plan, sign, send
Under gas model v2 the chain bills every byte a transaction puts in the block,
and it escrows every new storage row. The fee of a native operation is therefore
a function of the message and the chain's prices, and the SDK computes it from
the message. Builders carry no prices. A message built without a max_gas has a
zero offer, which marks it as unplanned and refuses to sign. The steps are:
- Build the message with a builder (
build_transfer_fungible_tx,build_create_token_tx,build_mint_phantasma_non_fungible_tx, ...). The builders write only the limits you pass (TxLimits). - Plan it against the chain:
rpc.fees.plan(msg)reads the chain's gas config through the client (cached for a minute), recognises the operation and prices it. For every operation the SDK models, the bill is exact for the facts it was given. Some of the price depends on chain state the message does not carry. Examples are whether the recipient already holds the token, and which mode a series mints in. Each such fact is taken at the value that costs MORE, so an unstated plan is an upper bound the settlement can only undercut, and the unused part of the offer is refunded. State what you know inPlanRequestOptionsto get the exact quote. A burn of an NFT is planned for what the NFT holds: the client reads the NFT's address for you, and the pureplan_feesdemands the list. - Sign with every witness the message needs:
sign_tx_msg(planned, *keys)for in-memory keys,sign_tx_msg_with(planned, *signers)for aTxSignersuch as a hardware wallet. A gas-payer transfer takes the payer's and the owner's keys; the SDK puts them in the order the chain reads. - Send the envelope with
rpc.send_carbon_transaction(raw).
rpc.send_transaction(msg, signers) does all four in one step, plus a
pre-flight. A token creation pays its policy fee before the chain looks at the
symbol, so the client asks whether the symbol is taken and refuses to send
unless the chain answered that it is free. rpc.preflight_transaction(msg)
reports that verdict to callers that want to decide for themselves.
from phantasma_py import PhantasmaRPC, build_transfer_fungible_tx, summarize_fee_plan
from phantasma_py.carbon import bytes32_from_public_key
from phantasma_py.crypto import PhantasmaKeys
rpc = PhantasmaRPC("http://localhost:5172/rpc")
keys = PhantasmaKeys.from_wif("...")
msg = build_transfer_fungible_tx(
from_address=bytes32_from_public_key(keys.public_key),
to=receiver,
token_id=1, # KCAL
amount=100_000_000,
)
plan = rpc.fees.plan(msg)
summary = summarize_fee_plan(plan)
print(f"gas {summary.gas_bill} KCAL, storage deposit up to {summary.storage_ceiling} SOUL")
# The wallet shows the summary and asks; then:
tx_hash = rpc.send_transaction(msg, [keys])
Which fact each operation reads
Only the facts an operation reads can move its price, so this table is the whole
of what is worth stating. Every default is the reading that costs MORE. One
storage quantum is data_escrow_per_row of escrow plus 25 gas units of block
data, and the chain's fee_multiplier scales both. Balance rows of the gas and
data tokens are free, so for those tokens recipient_holds_token changes
nothing.
NativeFeeKind |
facts it reads | what the default assumes | what the default costs |
|---|---|---|---|
TRANSFER_FUNGIBLE |
recipient_holds_token |
the recipient has no row for this token | 1 quantum |
TRANSFER_NON_FUNGIBLE |
recipient_holds_token |
the recipient has no row for this token | 1 quantum |
MINT_FUNGIBLE |
recipient_holds_token, supply_row_exists, big_fungible |
no recipient row; the supply row was dropped and must be recreated; the resulting balance needs the widest answer | 1 quantum each, and 24 more result bytes (33 against 9) |
BURN_FUNGIBLE |
token_burned_before, supply_row_exists, big_fungible |
the token's burnt counter does not exist yet; the supply row must be recreated; widest answer | 1 quantum each, and 24 more result bytes |
MINT_NON_FUNGIBLE |
recipient_holds_token, supply_row_exists, rom_has_meta_id |
as above, plus: the ROM carries an _i id, which is indexed in one more row |
1 quantum each, and 1 quantum per instance for the id |
MINT_PHANTASMA_NON_FUNGIBLE |
recipient_holds_token, supply_row_exists, duplicated_series |
as above, plus: the series mints duplicates | 1 quantum each, and one query fee per instance plus one per distinct series |
BURN_NON_FUNGIBLE |
token_burned_before, supply_row_exists, infusions (required) |
burnt counter does not exist; supply row must be recreated | 1 quantum each. infusions has no default at all. A burn returns whatever the NFT holds, and there is no upper bound on that, so the plan demands the list and rpc.fees reads it from the chain. rom_has_meta_id is read here too but cannot move the bill: a burn deletes more rows than it creates, so its net storage growth is zero either way |
CREATE_TOKEN |
none | nothing is assumed | the price comes entirely from the message: the symbol length, the serialized TokenInfo, and which keys its metadata carries |
CREATE_TOKEN_SERIES |
series_has_meta_id |
the series metadata carries an _i id |
1 quantum |
REGISTER_NAME |
none | nothing is assumed | governance rows are free data; the price is the length-shifted policy fee and the envelope |
SCRIPT |
none | 5000 work units, 512 event bytes and 4 storage quanta, per unmodelled call | a budget and never a prediction. plan.exact is False whenever one is present |
Notes:
plan.kindssays which operations were priced. ACALL_MULTIperforms several, and each one is priced and then summed, because the chain bills a batch as the sum of its calls with the envelope counted once. One kind is a budget and not a formula:NativeFeeKind.SCRIPT, which covers VM scripts and calls the SDK does not model. Their work depends on execution.plan.exactsays whether the number is a prediction or a ceiling. It isTruewhen nothing the plan had to assume could have changed it. It isFalsewhen a costlier reading decided part of the price, or when a part of the message had to be budgeted. A wallet shows the amount when the flag isTrue, and "up to" in front of the amount when it isFalse. The flag is answered by pricing the message a second time with every fact at its cheaper reading, so it is about THIS message and not about which options you set. A KCAL transfer is exact with nothing stated, because the chain's own token rows are free andrecipient_holds_tokencannot move its price.burned_instances(msg)names the NFT instances a message burns, in any shape. It covers the native burn types, aToken.BurnNonFungiblecall, and every such call inside aCALL_MULTI. Use it to tell whetherinfusionsis required. An empty answer means the list is not required.summarize_fee_planrenders a plan in KCAL and SOUL.plan.apply(msg)returns the message with the plan written in when you sign yourself.- A message keeps a default lifetime of 45 seconds. When a person sits between
building and signing, set
TxLimits.expiryfrom the chain's own window withexpiry_within(rpc.fees.chain_params().expiry_window_ms). - Calls whose witness set the caller chooses (token creation, series creation,
Phantasma mints, name registration, and every batch) need
witness_countin the plan options.send_transactionand thebuild_*_tx_and_signhelpers fill it in from the signers they are given. - The node's
estimate_transactiongives the exact bill of a script, which the SDK can only budget. - A node that refuses a request with an HTTP error and a JSON-RPC body surfaces
the body as an
RPCErrorwith the node's code and message, so the node's refusal is told from a transport failure.
examples/plan_carbon_transfer_fee.py plans a one-atom KCAL transfer against a
node and prints the summary without signing or sending anything.
Carbon transaction signing is available without going through RPC. Without a chain to plan against, state the offer yourself; a 170-byte KCAL transfer bills 0.00426 KCAL on mainnet, and the unused part of the offer is refunded:
from phantasma_py.carbon import TxLimits, build_transfer_fungible_tx, sign_and_serialize_tx_msg_hex
from phantasma_py.crypto import PhantasmaKeys
keys = PhantasmaKeys.from_wif("...")
msg = build_transfer_fungible_tx(
from_address=sender,
to=receiver,
token_id=1,
amount=1_000_000,
limits=TxLimits(max_gas=50_000_000), # 0.005 KCAL
)
raw_hex = sign_and_serialize_tx_msg_hex(msg, keys)
Development
just f # format and autofix
just f-check # verify formatting and lint
just typecheck # strict mypy
just test # pytest
just build # package build
just check # all checks above
The shared Carbon vector fixture in tests/fixtures/carbon_vectors.tsv is
copied from the Go SDK and should stay byte-for-byte compatible across SDKs.
Metadata
Release files for phantasma-sdk-py 4.0.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| phantasma_sdk_py-4.0.2.tar.gz | 366.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| phantasma_sdk_py-4.0.2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 465.6 kB
Release files / phantasma_sdk_py-4.0.2.tar.gz
| Download URL | phantasma_sdk_py-4.0.2.tar.gz |
|---|---|
| Size | 366.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
61945ab4c0fa61b36b66d007b9c613172320172e578c54b1fc8ed36fa9e2f240
|
|
BLAKE2b-256 checksum How to use checksums |
122bfe7a74ad8294bedc3d14ca938b8b8514ca18dc2c680b3a77bc3fa3fef329
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.6.14
|
Release files / phantasma_sdk_py-4.0.2-py3-none-any.whl
| Download URL | phantasma_sdk_py-4.0.2-py3-none-any.whl |
|---|---|
| Size | 98.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
94aa9dc2dda7de94227df523b8623b888fb128667139dd8879a92bb6508a2fec
|
|
BLAKE2b-256 checksum How to use checksums |
65c45fff1dd5867be3a6a1da1518bc8f9e7e75db1cd5a266a04ef946ae484b82
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.6.14
|