Skip to main content

PyCardano

PyCardano is a standalone Cardano client written in Python. The library is able to create and sign transactions without depending on third-party Cardano serialization tools, such as cardano-cli and cardano-serialization-lib, making it a light-weight library that is easy and fast to set up in all kinds of environments.

Installation

PyCardano could be installed using pip as follows:

pip install pycardano

Using PyCardano

Create a payment key and a payment address:

from pycardano import Address, Network, PaymentSigningKey, PaymentVerificationKey

payment_signing_key = PaymentSigningKey.generate()
payment_signing_key.save("payment.skey")
payment_verification_key = PaymentVerificationKey.from_signing_key(payment_signing_key)
payment_verification_key.save("payment.vkey")

network = Network.TESTNET
address = Address(payment_part=payment_verification_key.hash(), network=network)
print(address)
# Output: addr_test1vr2p8st5t5cxqglyjky7vk98k7jtfhdpvhl4e97cezuhn0cqcexl7
warning

Your address generated by the python code above will be different from the one shown. Please use the address you generated. Anything sent to the address above will be lost.

Fund the address with some tADA (test ADA) through the faucet here.

With the address and key created above, you can now create and sign transactions, and submit the transaction to Cardano network.

Transaction Guide

Cardano transactions are usually involved with three child components, transaction input(s), transaction output(s), and transaction fee. There are two approaches of creating transactions in PyCardano. The first one is to provide child components explicitly, which is also referred as creating "raw transactions". The second one is to use a transaction builder, which is usually more user-friendly.

Below are two examples that generates the same transaction using different approaches. The transaction is simply sending 100000 ADA to ourselves, and paying the network fees.

Raw transaction

Raw transactions can be precisely constructed by specifying inputs, outputs, fees, etc. This approach is usually for power users or for debugging purposes. For most users, a transaction builder will be the way to go.

Step 1

Define Tx input:

from pycardano import (
PaymentSigningKey,
PaymentVerificationKey,
Transaction,
TransactionBody,
TransactionInput,
TransactionOutput,
TransactionWitnessSet,
VerificationKeyWitness,
)

# Assume the UTxO is sitting at index 0 of tx 732bfd67e66be8e8288349fcaaa2294973ef6271cc189a239bb431275401b8e5
tx_id = "732bfd67e66be8e8288349fcaaa2294973ef6271cc189a239bb431275401b8e5"
tx_in = TransactionInput.from_primitive([tx_id, 0])

Step 2

Define Tx output. Suppose we have total of 900000000000 lovelace, and we need to pay 165897 as network fee, then we will get 799999834103 as change:

address = "addr_test1vrm9x2zsux7va6w892g38tvchnzahvcd9tykqf3ygnmwtaqyfg52x"

# Define two transaction outputs, the first one is the amount we want to send, the second one is the change.
output1 = TransactionOutput.from_primitive([address, 100000000000])
output2 = TransactionOutput.from_primitive([address, 799999834103])

Step 3

Create a transaction body from the input and outputs defined above and add transaction fee:

tx_body = TransactionBody(inputs=[tx_in], outputs=[output1, output2], fee=165897)

Transaction ID could be obtained when we are done configuring the transaction body, because is essentially the hash of TransactionBody:

print(tx_body.id)
# Output: TransactionId(hex='1d40b950ded3a144fb4c100d1cf8b85719da91b06845530e34a0304427692ce4')

Step 4

Sign the transaction body hash and create a complete transaction:

sk = PaymentSigningKey.load("path/to/payment.skey")

# Derive a verification key from the signing key
vk = PaymentVerificationKey.from_signing_key(sk)

# Sign the transaction body hash
signature = sk.sign(tx_body.hash())
# Alternatively, we can sign the transaction ID as well
signature_alternative = sk.sign(tx_body.id.payload)
assert signature == signature_alternative

# Add verification key and the signature to the witness set
vk_witnesses = [VerificationKeyWitness(vk, signature)]

# Create final signed transaction
signed_tx = Transaction(tx_body, TransactionWitnessSet(vkey_witnesses=vk_witnesses))

A complete example could be found here.

Notice that, to create a transaction, we need to know which transaction input to use, the amount of changes to return to the sender, and the amount of fee to pay to the network, which is possible to calculate, but requiring additional efforts.

Transaction builder

Step 1

To use a transaction builder, we first need to create a chain context (for example by a provider like Blockfrost), so the builder can read protocol parameters and search proper transaction inputs to use:

from blockfrost import ApiUrls
from pycardano import BlockFrostChainContext
network = Network.TESTNET
context = BlockFrostChainContext("your_blockfrost_project_id", base_url=ApiUrls.preprod.value)

Step 2

Read signing key into the program and generate its corresponding verification key:

from pycardano import PaymentSigningKey, PaymentVerificationKey, Address, Network
network = Network.TESTNET
sk = PaymentSigningKey.load("path/to/payment.skey")
vk = PaymentVerificationKey.from_signing_key(sk)
address = Address(vk.hash(), network)

Step 3

Create a transaction builder from chain context:

builder = TransactionBuilder(context)

Step 4

Tell the builder that transaction input will come from our own address:

builder.add_input_address(address)

Step 5

Specify output amount:

builder.add_output(TransactionOutput.from_primitive([address, 100000000000]))

Step 6

Add additional transaction information as needed:

builder.ttl = 3600
builder.reference_inputs.add(utxo)

Step 7

Create a signed transaction using transaction builder. Unlike building a raw transaction, where we need to manually sign a transaction and build a transaction witness set, transaction builder can build and sign a transaction directly with its build_and_sign method. The code below tells the builder to build a transaction and sign the transaction with a list of signing keys (in this case, we only need the signature from one signing key, sk) and send the change back to sender's address:

signed_tx = builder.build_and_sign([sk], change_address=address)

Transaction ID could be obtained from the transaction object:

print(signed_tx.id)
# Output: TransactionId(hex='1d40b950ded3a144fb4c100d1cf8b85719da91b06845530e34a0304427692ce4')

By using transaction builder, we no longer need to specify which UTxO to use as transaction input or calculate transaction fee, because they are taken care by the transaction builder. Also, the code becomes much more concise.

A more complex example of using transaction builder could be found in this Github example.

Transaction submission

Once we have a signed transaction, it could be submitted to the network. The easiest way to do so is through a chain context:

context.submit_tx(signed_tx)

Smart Contracts

Smart Contracts on Cardano allow us to incorporate expressive logics to determine when a particular UTxO can be spent. In this tutorial, we will focus on opshin, a Smart Contract language based on python.

In order to understand how Smart Contracts work on Cardanos eUTxO model we need to understand a couple of concepts.

  • Plutus script: the smart contract that acts as the validator of the transaction. By evaluating the inputs from someone who wants to spend the UTxO, they either approve or deny it (by returning either True or False). The script is compiled into Plutus Core binary and sits on-chain.
  • Script address: the hash of the Plutus script binary. They hold UTxOs like typical public key address, but every time a transaction tries to consume the UTxOs on this address, the Plutus script generated this address will be executed by evaluating the input of the transaction, namely datum, redeemer and script context. The transaction is only valid if the script returns True.
  • Datum: the datum is a piece of information associated with a UTxO. When someone sends fund to script address, he or she attaches the hash of the datum to "lock" the fund. When someone tries to consume the UTxO, he or she needs to provide datum whose hash matches the attached datum hash and redeemer that meets the conditions specified by the Plutus script to "unlock" the fund.
  • Redeemer: the redeemer shares the same data format as datum, but is a separate input. It includes datum, along with other information such as types of actions to take with the target UTxO and computational resources to reserve. Redeemer value is attached to the input transaction to unlock funds from a script and is used by the script to validate the transaction.
  • Script context: The script context provides information about the pending transaction, along with which input triggered the validation.

Datum and Redeemer Serialization

To calculate the hash of a datum, we can leverage the helper class PlutusData. PlutusData can serialize itself into a CBOR format, which can be interpreted as a data structure in Plutus scripts. Wrapping datum in PlutusData class will reduce the complexity of serialization and deserialization tremendously. It supports data type of int, bytes, List and hashmap. Below are some examples on how to construct some arbitrary datums.

Empty datum:

from pycardano import PlutusData, Unit
empty_datum = Unit()
print(empty_datum.to_cbor().hex())
# Output: d87980

Sample datum with int, bytes, List and hashmap inputs:

# Create sample datum
from typing import List, Dict
from dataclasses import dataclass

@dataclass
class MyDatum(PlutusData):
CONSTR_ID = 1
a: int
b: bytes
c: List[int]
d: Dict[int, bytes]

datum = MyDatum(123, b"1234", [4, 5, 6], {1: b"1", 2: b"2"})
print(datum.to_cbor().hex())
# Output: d87a9f187b443132333483040506a2014131024132ff

You can also wrap PlutusData within PlutusData:

@dataclass
class InclusionDatum(PlutusData):
CONSTR_ID = 1
beneficiary: bytes
deadline: int
other_data: MyDatum

key_hash = bytes.fromhex("c2ff616e11299d9094ce0a7eb5b7284b705147a822f4ffbd471f971a")
deadline = 1643235300000
other_datum = MyDatum(123, b"1234", [4, 5, 6], {1: b"1", 2: b"2"})
include_datum = InclusionDatum(key_hash, deadline, other_datum)
print(include_datum.to_cbor().hex())
# Output: d87a9f581cc2ff616e11299d9094ce0a7eb5b7284b705147a822f4ffbd471f971a1b0000017e9874d2a0d8668218829f187b44313233349f040506ffa2014131024132ffff

PlutusData supports conversion from/to JSON format, which is easier to read and write. The above could be converted to JSON like this:

encoded_json = include_datum.to_json(separators=(",", ":"))

Similarly, redeemer can be serialized like following:

data = MyDatum(123, b"234", IndefiniteList([]), {1: b"1", 2: b"2"})
redeemer = Redeemer(data, ExecutionUnits(1000000, 1000000))
print(redeemer.to_cbor().hex())
# Output: 840000d8668218829f187b433233349fffa2014131024132ff821a000f42401a000f4240

Datum Deserialization

Deserialization of PlutusData generally has two different paths, based on whether you know the structure of the Plutus Datum you are trying to deserialize or not. If you know the structure in advance, subclass the PlutusData type and configure it to match the data type that you expect to receive. If the datatype does not match, the deserialization will throw an Exception! So make sure that the data really follows the format that you expect:

# Create sample datum
from typing import List, Dict

@dataclass
class MyDatum(PlutusData):
CONSTR_ID = 1
a: int
b: bytes
c: List[int]
d: Dict[int, bytes]

result = MyDatum.from_cbor(bytes.fromhex('d87a9f187b443132333483040506a2014131024132ff'))
print(result)
# Output: MyDatum(a=123, b=b'1234', c=[4, 5, 6], d={1: b'1', 2: b'2'})

# The Inclusion Datum will not be correctly deserialized
try:
MyDatum.from_cbor(bytes.fromhex('d87a9f581cc2ff616e11299d9094ce0a7eb5b7284b705147a822f4ffbd471f971a1b0000017e9874d2a0d8668218829f187b44313233349f040506ffa2014131024132ffff'))
except Exception as e:
print(f"DeserializeException: {e}")
# Output: Cannot deserialize object: b'\xc2\xffan\x11)\x9d\x90\x94\xce\n~\xb5\xb7(KpQG\xa8"\xf4\xff\xbdG\x1f\x97\x1a' to type <class 'int'>.

If you do not know the structure of the Datum in advance, use RawPlutusDatum.from_cbor. As you can see, this will not tell you anything about the meaning of specific fields, CBOR Tags etc - this is because the meaning are not stored on chain. In the CBOR, just the types are known and hence restoring a raw datum will return to you just the types:

from pycardano import RawPlutusData
result = RawPlutusData.from_cbor(bytes.fromhex("d87a9f187b443132333483040506a2014131024132ff"))
print(result)
# Output: RawPlutusData(data=CBORTag(122, [123, b'1234', [4, 5, 6], {1: b'1', 2: b'2'}]))

Note that there are specific fields you may need.

  • Builtin: If you don't know the structure of a datum inside a PlutusDatum. It will be decoded as RawPlutusDatum.
  • IndefiniteList: A list that is in theory unbounded. This may be required by the Cardano node in case a list has more than 64 elements.
  • ByteString: Similarly to IndefiniteList, this denotes a bytes element that may be longer than 64 bytes and correctly encodes it in CBOR so that the result is accepted by the Cardano node.

Resources

Visit PyCardano Documentation for indepth guides. Watch PyCardano Application Demo.