Hello, World!
Let's write and execute a smart contract on Cardano in 10 minutes. Yes, you read that well.
You can find code supporting this tutorial on Aiken's main repository (opens in a new tab).
Covered in this tutorial
- Writing a basic
Aiken
validator. - Using Lucid (opens in a new tab) through Blockfrost (opens in a new tab).
- Getting test funds from the Cardano Faucet (opens in a new tab)
- Using web explorers such as CardanoScan (opens in a new tab).
Pre-requisites
We'll use Aiken
to write the script, and
Lucid
(opens in a new tab) to construct and submit the
transaction. So make sure you have your dev environment ready for some
TypeScript★ and have Aiken installed already.
★ For TypeScript, we recommend installing deno (opens in a new tab).
Scaffolding
First, let's create a new Aiken
project:
aiken new aiken-lang/hello_world
cd hello_world
This command scaffolds an Aiken project. In particular, it creates a lib
and validators
folders in which you can put Aiken source files.
./hello_world
│
├── README.md
├── aiken.toml
├── lib
│ └── hello_world
└── validators
Using the standard library
We'll use the standard library (opens in a new tab) for writing our validator. Fortunately, aiken new
did automatically add the standard
library to our aiken.toml
for us. It should look roughly like that:
name = "aiken-lang/hello_world"
description = "Aiken contracts for project 'aiken-lang/hello_world'"
version = "0.0.0"
license = "Apache-2.0"
[repository]
user = 'aiken-lang'
project = 'hello_world'
platform = 'github'
[[dependencies]]
name = "aiken-lang/stdlib"
version = "main"
source = "github"
Now, running aiken check
, we should see dependencies being downloaded. That shouldn't take long.
❯ aiken check
Resolving versions
Downloading packages
Downloaded 1 package in 0.91s
Compiling aiken-lang/stdlib main (/Users/aiken/Documents/aiken-lang/hello_world/build/packages/aiken-lang-stdlib)
Compiling aiken-lang/hello_world 0.0.0 (/Users/aiken/Projects/aiken-lang/hello_world)
Summary
0 error, 0 warning(s)
Our first validator
Let's write our first validator as validators/hello_world.ak
:
use aiken/hash.{Blake2b_224, Hash}
use aiken/list
use aiken/transaction.{ScriptContext}
use aiken/transaction/credential.{VerificationKey}
type Datum {
owner: Hash<Blake2b_224, VerificationKey>,
}
type Redeemer {
msg: ByteArray,
}
validator {
fn hello_world(datum: Datum, redeemer: Redeemer, context: ScriptContext) -> Bool {
let must_say_hello =
redeemer.msg == "Hello, World!"
let must_be_signed =
list.has(context.transaction.extra_signatories, datum.owner)
must_say_hello && must_be_signed
}
}
Our first validator is rudimentary. It is parameterized by a verification key hash (owner
) and a message (msg
). Remember that, in the eUTxO model, the datum is set when locking funds in the contract and can be therefore seen as configuration. Here, we'll indicate the owner of contract and require a signature from them to unlock funds -- very much like it already works on a typical non-script address. Moreover, because there's no "Hello, World!" without a proper "Hello, World!" our little contract also demands this very message, as a UTF-8-encoded byte array, to be passed as redeemer (i.e. when spending from the contract).
It's now time to build our first contract!
aiken build
This command generate a CIP-0057 Plutus blueprint (opens in a new tab) as plutus.json
at the root of your project. This blueprint describes your on-chain contract and its binary interface. In particular, it contains the generated on-chain code that will be executed by the ledger, and a hash of your validator(s) that can be used to construct addresses.
This format is framework-agnostic and is meant to facilitate interoperability between tools. The blueprint is fully integrated into Aiken, which can automatically generate it based on your type definitions and comments.
Let's see the validator in action!
Getting funds
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 Lucid
.
Let's write our first script as generate-credentials.ts
:
import { Lucid } from "https://deno.land/x/[email protected]/mod.ts";
const lucid = await Lucid.new(undefined, "Preview");
const privateKey = lucid.utils.generatePrivateKey();
await Deno.writeTextFile("key.sk", privateKey);
const address = await lucid
.selectWalletFromPrivateKey(privateKey)
.wallet.address();
await Deno.writeTextFile("key.addr", address);
You can run the instructions above using Deno via:
deno run --allow-net --allow-write generate-credentials.ts
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 key.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.
Using the contract
Now that we have some funds, we can lock them in our newly created contract. We'll use Lucid
(opens in a new tab) to construct and submit our transaction through Blockfrost.
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 Lucid with Blockfrost as a provider. This will allow us to let Lucid 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.ts
in the root of your project and add the following code:
import {
Blockfrost,
C,
Constr,
Data,
Lucid,
SpendingValidator,
TxHash,
fromHex,
toHex,
utf8ToHex,
} from "https://deno.land/x/[email protected]/mod.ts";
import * as cbor from "https://deno.land/x/[email protected]/index.js";
const lucid = await Lucid.new(
new Blockfrost(
"https://cardano-preview.blockfrost.io/api/v0",
Deno.env.get("BLOCKFROST_API_KEY")
),
"Preview"
);
Note that the highlighted line above looks for an environment variable named BLOCKFROST_API_KEY
which value must be set to your Blockfrost API key.
You can define a new environment variable in your terminal by running (in the same session you're also executing the script!):
export BLOCKFROST_API_KEY=preview...
Replace preview...
with your actual API key.
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 Lucid understands. This is done by serializing the validator and then converting it to an hexadecimal text string as shown below:
lucid.selectWalletFromPrivateKey(await Deno.readTextFile("./key.sk"));
const validator = await readValidator();
// --- Supporting functions
async function readValidator(): Promise<SpendingValidator> {
const validator = JSON.parse(await Deno.readTextFile("plutus.json")).validators[0];
return {
type: "PlutusV2",
script: toHex(cbor.encode(fromHex(validator.compiledCode))),
};
}
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 key. This will be needed to unlock the funds.
const publicKeyHash = lucid.utils.getAddressDetails(
await lucid.wallet.address()
).paymentCredential?.hash;
const datum = Data.to(new Constr(0, [publicKeyHash]));
const txHash = await lock(1000000n, { into: validator, owner: datum });
await lucid.awaitTx(txHash);
console.log(`1 tADA locked into the contract at:
Tx ID: ${txHash}
Datum: ${datum}
`);
// --- Supporting functions
async function lock(
lovelace: bigint,
{ into, owner }: { into: SpendingValidator; owner: string }
): Promise<TxHash> {
const contractAddress = lucid.utils.validatorToAddress(into);
const tx = await lucid
.newTx()
.payToContract(contractAddress, { inline: owner }, { lovelace })
.complete();
const signedTx = await tx.sign().complete();
return signedTx.submit();
}
You can run the excerpt above by executing:
deno run --allow-net --allow-read --allow-env hello_world-lock.ts
The above code requires you to:
-
have a
BLOCKFROST_API_KEY
environment variable set. You can get one by signing up for a Blockfrost account (opens in a new tab). -
have the file
hello_world-lock.ts
placed at the root of yourhello_world
folder.
At this stage, your folder should looks roughly like this:
./hello_world
│
├── README.md
├── aiken.toml
├── plutus.json
├── generate-credentials.ts
├── hello_world-lock.ts
├── key.addr
├── key.sk
├── lib
│ └── ...
└── validators
└── hello_world.ak
If everything went well, you should see something like this:
1 tADA locked into the contract at:
Tx ID: 8559f57234407204d8e9a6bf57ef6943c65ec7119eb1c2ca6224f8bad8e71c1e
Datum: d8799f581c10073fd2997d2f7dc6dadcf24966bd06b01930e5210e5de7aebf792dff
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):
{
"constructor": 0,
"fields": [
{
"bytes": "10073fd2997d2f7dc6dadcf24966bd06b01930e5210e5de7aebf792d"
}
]
}
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).
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.
Let's make a new file hello_world-unlock.ts
and copy over some of the boilerplate
from the first one.
import {
Blockfrost,
C,
Constr,
Data,
Lucid,
SpendingValidator,
TxHash,
fromHex,
toHex,
utf8ToHex,
} from "https://deno.land/x/[email protected]/mod.ts";
import * as cbor from "https://deno.land/x/[email protected]/index.js";
const lucid = await Lucid.new(
new Blockfrost(
"https://cardano-preview.blockfrost.io/api/v0",
Deno.env.get("BLOCKFROST_API_KEY")
),
"Preview"
);
lucid.selectWalletFromPrivateKey(await Deno.readTextFile("./key.sk"));
const validator = await readValidator();
// --- Supporting functions
async function readValidator(): Promise<SpendingValidator> {
const validator = JSON.parse(await Deno.readTextFile("plutus.json"))
.validators[0];
return {
type: "PlutusV2",
script: toHex(cbor.encode(fromHex(validator.compiledCode))),
};
}
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.ts
)
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.
const utxo: OutRef = { txHash: Deno.args[0], outputIndex: 0 };
const redeemer = Data.to(new Constr(0, [utf8ToHex("Hello, World!")]));
const txHash = await unlock(utxo, {
from: validator,
using: redeemer,
});
await lucid.awaitTx(txHash);
console.log(`1 tADA unlocked from the contract
Tx ID: ${txHash}
Redeemer: ${redeemer}
`);
// --- Supporting functions
async function unlock(
ref: OutRef,
{ from, using }: { from: SpendingValidator; using: Redeemer }
): Promise<TxHash> {
const [utxo] = await lucid.utxosByOutRef([ref]);
const tx = await lucid
.newTx()
.collectFrom([utxo], using)
.addSigner(await lucid.wallet.address())
.attachSpendingValidator(from)
.complete();
const signedTx = await tx
.sign()
.complete();
return signedTx.submit();
}
Run this script as usual, but this time, also passing the transaction id obtained from the previous command locking the funds. For example:
deno run --allow-net --allow-read --allow-env hello_world-unlock.ts 8559f57234407204d8e9a6bf57ef6943c65ec7119eb1c2ca6224f8bad8e71c1e
If everything worked as planned you should see something resembling the following output:
1 tADA unlocked from the contract
Tx ID: d3d5e828a3989691b0960d22a265c8c9ae4723134b52aa05ec0fb7d40f060392
Redeemer: d8799f4d48656c6c6f2c20576f726c6421ff
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.