Hello, World!
End-to-End
with PyCardano (Python)

Hello, World! - with PyCardano

Covered in this tutorial


Pre-requisites

We assume that you have followed the Hello, World!'s First steps and thus, have Aiken installed an ready-to-use. We will also use PyCardano (opens in a new tab), so make sure you have your dev environment ready for somePython (3).

You setup a new Python environment for PyCardano (opens in a new tab) as follows:

python3 -m venv ./venv
source venv/bin/activate
pip install pycardano

Getting funds

For this tutorial, we will use the validator we built in First steps. Yet, before moving one, we'll need some funds, and a public/private key pair to hold them. We can generate a private key and an address using PyCardano.

Let's write our first script as generate-credentials.py:

generate-credentials.py
from pycardano import Address, Network, PaymentSigningKey, PaymentVerificationKey
 
signing_key = PaymentSigningKey.generate()
signing_key.save("me.sk")
 
verification_key = PaymentVerificationKey.from_signing_key(signing_key)
 
address = Address(payment_part=verification_key.hash(), network=Network.TESTNET)
with open("me.addr", "w") as f:
    f.write(str(address))

You can run the instructions above using:

python generate-credentials.py

Now, we can head to the Cardano faucet (opens in a new tab) to get some funds on the preview network to our newly created address (inside me.addr).

πŸ‘‰ Make sure to select "Preview Testnet" as network.

Using CardanoScan (opens in a new tab) we can watch for the faucet sending some ADA our way. This should be pretty fast (a couple of seconds).

Using the contract

Now that we have some funds, we can lock them in our newly created contract. We'll use PyCardano (opens in a new tab) to construct and submit our transaction through Blockfrost (opens in a new tab).

This is only one example of possible setup using tools we love. For more tools, make sure to check out the Cardano Developer Portal (opens in a new tab)!

Setup

First, we setup PyCardano with Blockfrost as a provider. This will allow us to let PyCardano handle transaction building for us, which includes managing changes. It also gives us a direct way to submit the transaction later on.

Create a file named hello-world-lock.py in the root of your project and add the following code:

hello-world-lock.py
from pycardano import (
    BlockFrostChainContext,
)
import os
 
context = BlockFrostChainContext(
    project_id=os.environ["BLOCKFROST_PROJECT_ID"],
    base_url="https://cardano-preview.blockfrost.io/api/",
)
⚠️

Note that the highlighted line above looks for an environment variable named BLOCKFROST_PROJECT_ID which value must be set to your Blockfrost project id. You can define a new environment variable in your terminal by running (in the same session you're also executing the script!):

export BLOCKFROST_PROJECT_ID=preview...

Replace preview... with your actual project id.

Next, we'll need to read the validator from the blueprint (plutus.json) we generated earlier. We'll also need to convert it to a format that PyCardano understands. This is done by serializing the validator and then converting it to an hexadecimal text string as shown below:

hello-world-lock.py
from pycardano import (
    BlockFrostChainContext,
    PaymentSigningKey,
)
import json
import os
 
def read_validator() -> dict:
    with open("plutus.json", "r") as f:
        validator = json.load(f)
    script_bytes = PlutusV2Script(
        bytes.fromhex(validator["validators"][0]["compiledCode"])
    )
    script_hash = ScriptHash(bytes.fromhex(validator["validators"][0]["hash"]))
    return {
        "type": "PlutusV2",
        "script_bytes": script_bytes,
        "script_hash": script_hash,
    }
 
context = BlockFrostChainContext(
    project_id=os.environ["BLOCKFROST_PROJECT_ID"],
    base_url="https://cardano-preview.blockfrost.io/api/",
)
 
signing_key = PaymentSigningKey.load("me.sk")
 
validator = read_validator()

Locking funds into the contract

Now that we can read our validator, we can make our first transaction to lock funds into the contract. The datum must match the representation expected by the validator (and as specified in the blueprint), so this is a constructor with a single field that is a byte array.

As value for that byte array, we provide a hash digest of our public me. This will be needed to unlock the funds.

hello-world-lock.py
from dataclasses import dataclass
from pycardano import (
    Address,
    BlockFrostChainContext,
    Network,
    PaymentSigningKey,
    PaymentVerificationKey,
    PlutusData,
    PlutusV2Script,
    TransactionBuilder,
    TransactionOutput,
)
from pycardano.hash import (
    VerificationKeyHash,
    TransactionId,
    ScriptHash,
)
import json
import os
 
def read_validator() -> dict:
    with open("plutus.json", "r") as f:
        validator = json.load(f)
    script_bytes = PlutusV2Script(
        bytes.fromhex(validator["validators"][0]["compiledCode"])
    )
    script_hash = ScriptHash(bytes.fromhex(validator["validators"][0]["hash"]))
    return {
        "type": "PlutusV2",
        "script_bytes": script_bytes,
        "script_hash": script_hash,
    }
 
def lock(
    amount: int,
    into: ScriptHash,
    datum: PlutusData,
    signing_key: PaymentSigningKey,
    context: BlockFrostChainContext,
) -> TransactionId:
    # read addresses
    with open("me.addr", "r") as f:
        input_address = Address.from_primitive(f.read())
    contract_address = Address(
        payment_part = into,
        network=Network.TESTNET,
    )
 
    # build transaction
    builder = TransactionBuilder(context=context)
    builder.add_input_address(input_address)
    builder.add_output(
        TransactionOutput(
            address=contract_address,
            amount=amount,
            datum=datum,
        )
    )
    signed_tx = builder.build_and_sign(
        signing_keys=[signing_key],
        change_address=input_address,
    )
 
    # submit transaction
    return context.submit_tx(signed_tx)
 
@dataclass
class HelloWorldDatum(PlutusData):
    CONSTR_ID = 0
    owner: bytes
 
context = BlockFrostChainContext(
    project_id=os.environ["BLOCKFROST_PROJECT_ID"],
    base_url="https://cardano-preview.blockfrost.io/api/",
)
 
signing_key = PaymentSigningKey.load("me.sk")
 
validator = read_validator()
 
owner = PaymentVerificationKey.from_signing_key(signing_key).hash()
 
datum = HelloWorldDatum(owner=owner.to_primitive())
 
tx_hash = lock(
    amount=2_000_000,
    into=validator["script_hash"],
    datum=datum,
    signing_key=signing_key,
    context=context,
)
 
print(
    f"2 tADA locked into the contract\n\tTx ID: {tx_hash}\n\tDatum: {datum.to_cbor_hex()}"
)

You can run the excerpt above by executing:

python hello-world-lock.py

The above code requires you to:

At this stage, your folder should looks roughly like this:

./hello_world
β”‚
β”œβ”€β”€ README.md
β”œβ”€β”€ aiken.toml
β”œβ”€β”€ plutus.json
β”œβ”€β”€ generate-credentials.py
β”œβ”€β”€ hello-world-lock.py
β”œβ”€β”€ me.addr
β”œβ”€β”€ me.sk
β”œβ”€β”€ lib
β”‚Β Β  └── ...
└── validators
 Β Β  └── hello-world.ak

If everything went well, you should see something like this:

2 tADA locked into the contract
    Tx ID: 2ea959e4b51b2b6046931fab80957b39e534f1c954d326e506814d3ca47726c6
    Datum: d8799f581c1defd7502e25b312e48dc57712c434d317ea16a57728cd3c31828ea1ff

Inspecting the transaction

Now is a good moment to pause and have a look at CardanoScan. Here's an example of an Hello World transaction (opens in a new tab) that we generated using this tutorial.

If you notice the small icon next to the contract output address, we can even inspect the datum (opens in a new tab):

d8799f581c10073fd2997d2f7dc6dadcf24966bd06b01930e5210e5de7aebf792dff
{
  "constructor": 0,
  "fields": [
    {
      "bytes": "1defd7502e25b312e48dc57712c434d317ea16a57728cd3c31828ea1"
    }
  ]
}

Unlocking funds from the contract

Finally, as a last step: we now want to spend the UTxO that is locked by our hello-world contract.

To be valid, our transaction must meet two conditions:

  • it must provide "Hello, World!" as a redeemer; and
  • it must be signed by the key referenced as datum (i.e the owner).

Let's make a new file hello-world-unlock.py and copy over some of the boilerplate from the first one.

hello-world-unlock.py
from pycardano import (
    BlockFrostChainContext,
    PaymentSigningKey,
)
import json
import os
 
def read_validator() -> dict:
    with open("plutus.json", "r") as f:
        validator = json.load(f)
    script_bytes = PlutusV2Script(
        bytes.fromhex(validator["validators"][0]["compiledCode"])
    )
    script_hash = ScriptHash(bytes.fromhex(validator["validators"][0]["hash"]))
    return {
        "type": "PlutusV2",
        "script_bytes": script_bytes,
        "script_hash": script_hash,
    }
 
context = BlockFrostChainContext(
    project_id=os.environ["BLOCKFROST_PROJECT_ID"],
    base_url="https://cardano-preview.blockfrost.io/api/",
)
 
signing_key = PaymentSigningKey.load("me.sk")
 
validator = read_validator()

Now, let's add the bits to unlock the funds in the contract. We'll need the transaction identifier (i.e. Tx ID) obtained when you ran the previous script (hello-world-lock.py)

That transaction identifier (a.k.a. transaction hash), and the corresponding output index (here, 0) uniquely identify the UTxO (Unspent Transaction Output) in which the funds are currently locked. And that's the one we're about to unlock. Since we know we have created a single UTxO, we'll take a little shortcut and simply look for the transaction id.

hello-world-unlock.py
from pycardano import (
    Address,
    BlockFrostChainContext,
    Network,
    PaymentSigningKey,
    UTxO,
)
import json
import os
import sys
 
def read_validator() -> dict:
    with open("plutus.json", "r") as f:
        validator = json.load(f)
    script_bytes = PlutusV2Script(
        bytes.fromhex(validator["validators"][0]["compiledCode"])
    )
    script_hash = ScriptHash(bytes.fromhex(validator["validators"][0]["hash"]))
    return {
        "type": "PlutusV2",
        "script_bytes": script_bytes,
        "script_hash": script_hash,
    }
 
def get_utxo_from_str(tx_id: str, contract_address: Address) -> UTxO:
    for utxo in context.utxos(str(contract_address)):
        if str(utxo.input.transaction_id) == tx_id:
            return utxo
    raise Exception(f"UTxO not found for transaction {tx_id}")
 
context = BlockFrostChainContext(
    project_id=os.environ["BLOCKFROST_PROJECT_ID"],
    base_url="https://cardano-preview.blockfrost.io/api/",
)
 
signing_key = PaymentSigningKey.load("me.sk")
 
validator = read_validator()
 
# get utxo to spend
utxo = get_utxo_from_str(sys.argv[1], Address(
    payment_part = validator["script_hash"],
    network=Network.TESTNET,
))

Finally, we can add the rest of the code and build a transaction that unlock the funds we previously locked. We'll need to construct a redeemer this time.

⚠️

Note that we need to explicitly add a signer using .addSigner so that it gets added to the extra_signatories of our transaction -- and becomes accessible for our script.

Furthermore, .add_input_address is necessary to ensure the transaction builder adds inputs to cover for fees and collateral.

hello-world-unlock.py
from dataclasses import dataclass
from pycardano import (
    Address,
    BlockFrostChainContext,
    Network,
    PaymentSigningKey,
    PaymentVerificationKey,
    PlutusData,
    PlutusV2Script,
    Redeemer,
    TransactionBuilder,
    TransactionOutput,
    UTxO,
)
from pycardano.hash import (
    VerificationKeyHash,
    TransactionId,
    ScriptHash,
)
import json
import os
import sys
 
def read_validator() -> dict:
    with open("plutus.json", "r") as f:
        validator = json.load(f)
    script_bytes = PlutusV2Script(
        bytes.fromhex(validator["validators"][0]["compiledCode"])
    )
    script_hash = ScriptHash(bytes.fromhex(validator["validators"][0]["hash"]))
    return {
        "type": "PlutusV2",
        "script_bytes": script_bytes,
        "script_hash": script_hash,
    }
 
def unlock(
    utxo: UTxO,
    from_script: PlutusV2Script,
    redeemer: Redeemer,
    signing_key: PaymentSigningKey,
    owner: VerificationKeyHash,
    context: BlockFrostChainContext,
) -> TransactionId:
    # read addresses
    with open("me.addr", "r") as f:
        input_address = Address.from_primitive(f.read())
 
    # build transaction
    builder = TransactionBuilder(context=context)
    builder.add_script_input(
        utxo=utxo,
        script=from_script,
        redeemer=redeemer,
    )
    builder.add_input_address(input_address)
    builder.add_output(
        TransactionOutput(
            address=input_address,
            amount=utxo.output.amount.coin,
        )
    )
    builder.required_signers = [owner]
    signed_tx = builder.build_and_sign(
        signing_keys=[signing_key],
        change_address=input_address,
    )
 
    # submit transaction
    return context.submit_tx(signed_tx)
 
def get_utxo_from_str(tx_id: str, contract_address: Address) -> UTxO:
    for utxo in context.utxos(str(contract_address)):
        if str(utxo.input.transaction_id) == tx_id:
            return utxo
    raise Exception(f"UTxO not found for transaction {tx_id}")
 
@dataclass
class HelloWorldRedeemer(PlutusData):
    CONSTR_ID = 0
    msg: bytes
 
context = BlockFrostChainContext(
    project_id=os.environ["BLOCKFROST_PROJECT_ID"],
    base_url="https://cardano-preview.blockfrost.io/api/",
)
 
signing_key = PaymentSigningKey.load("me.sk")
 
validator = read_validator()
 
# get utxo to spend
utxo = get_utxo_from_str(sys.argv[1], Address(
    payment_part = validator["script_hash"],
    network=Network.TESTNET,
))
 
# build redeemer
redeemer = Redeemer(data=HelloWorldRedeemer(msg=b"Hello, World!"))
 
# execute transaction
tx_hash = unlock(
    utxo=utxo,
    from_script=validator["script_bytes"],
    redeemer=redeemer,
    signing_key=signing_key,
    owner=PaymentVerificationKey.from_signing_key(signing_key).hash(),
    context=context,
)
 
print(
    f"2 tADA unlocked from the contract\n\tTx ID: {tx_hash}\n\tRedeemer: {redeemer.to_cbor_hex()}"
)

Run this script as usual, but this time, also passing the transaction id obtained from the previous command locking the funds. For example:

python hello-world-unlock.py 2ea959e4b51b2b6046931fab80957b39e534f1c954d326e506814d3ca47726c6

If everything worked as planned you should see something resembling the following output:

2 tADA unlocked from the contract
    Tx ID:    d3d5e828a3989691b0960d22a265c8c9ae4723134b52aa05ec0fb7d40f060392
    Redeemer: 840000d8799f4d48656c6c6f2c20576f726c6421ff82198ee61a00bd3334

And, tada πŸŽ‰!

We can inspect our redeeming transaction on CardanoScan (opens in a new tab) and see that it successfully executed our Hello World contract.