Hello, World! - with Mesh
Covered in this tutorial
- Interact with a validator on the
Preview
network; - Using Mesh (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 assume that you have followed the Hello, World!'s First steps and thus, have Aiken installed an ready-to-use. We will also use Mesh (opens in a new tab), so make sure you have your dev environment ready for some JavaScript!.
You can install Mesh (opens in a new tab) as follows:
yarn init -y
yarn add @meshsdk/core
You will also need the cbor
npm library, so let us also install it:
yarn add cbor
Getting funds
For this tutorial, we will use the validator we built in First steps. Yet, before moving on, we'll need some funds, and a public/private key pair to hold them. We can generate a private key and an address using Mesh.
Let's write our first script as generate-credentials.mjs
:
import { MeshWallet } from '@meshsdk/core';
import fs from 'node:fs';
const secret_key = MeshWallet.brew(true);
fs.writeFileSync('me.sk', secret_key);
const wallet = new MeshWallet({
networkId: 0,
key: {
type: 'root',
bech32: secret_key,
},
});
fs.writeFileSync('me.addr', wallet.getUnusedAddresses()[0]);
You can run the instructions above using Deno via:
node generate-credentials.mjs
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 Mesh (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 Mesh with Blockfrost as a provider. This will allow us to let Mesh 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.mjs
in the root of your project and add the following code:
import { BlockfrostProvider, MeshWallet } from '@meshsdk/core';
import fs from 'node:fs';
const blockchainProvider = new BlockfrostProvider(process.env.BLOCKFROST_PROJECT_ID);
const wallet = new MeshWallet({
networkId: 0,
fetcher: blockchainProvider,
submitter: blockchainProvider,
key: {
type: 'root',
bech32: fs.readFileSync('me.sk').toString(),
},
});
Note that the highlighted line above looks for an environment variable named BLOCKFROST_PROJECT_ID
which its 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 Mesh understands. This is done by serializing the validator and then converting it to a hexadecimal text string as shown below:
import { BlockfrostProvider, MeshWallet } from '@meshsdk/core';
import fs from 'node:fs';
const blockchainProvider = new BlockfrostProvider(process.env.BLOCKFROST_PROJECT_ID);
const wallet = new MeshWallet({
networkId: 0,
fetcher: blockchainProvider,
submitter: blockchainProvider,
key: {
type: 'root',
bech32: fs.readFileSync('me.sk').toString(),
},
});
const blueprint = JSON.parse(fs.readFileSync('./plutus.json'));
const script = {
code: cbor
.encode(Buffer.from(blueprint.validators[0].compiledCode, "hex"))
.toString("hex"),
version: "V3",
};
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 (from
the wallet created with our me.sk
) . This will be needed to unlock the funds.
import cbor from "cbor";
import {
resolvePaymentKeyHash,
resolvePlutusScriptAddress,
BlockfrostProvider,
MeshWallet,
Transaction,
} from '@meshsdk/core';
import fs from 'node:fs';
const blockchainProvider = new BlockfrostProvider(process.env.BLOCKFROST_PROJECT_ID);
const wallet = new MeshWallet({
networkId: 0,
fetcher: blockchainProvider,
submitter: blockchainProvider,
key: {
type: 'root',
bech32: fs.readFileSync('me.sk').toString(),
},
});
const blueprint = JSON.parse(fs.readFileSync('./plutus.json'));
const script = {
code: cbor
.encode(Buffer.from(blueprint.validators[0].compiledCode, "hex"))
.toString("hex"),
version: "V3",
};
const owner = resolvePaymentKeyHash((await wallet.getUsedAddresses())[0]);
const datum = {
value: {
alternative: 0,
fields: [owner],
},
};
const unsignedTx = await new Transaction({ initiator: wallet }).sendLovelace(
{
address: resolvePlutusScriptAddress(script, 0),
datum,
},
"1000000"
).build();
const signedTx = await wallet.signTx(unsignedTx);
const txHash = await wallet.submitTx(signedTx);
console.log(`1 tADA locked into the contract at:
Tx ID: ${txHash}
Datum: ${JSON.stringify(datum)}
`);
You can run the excerpt above by executing:
node hello-world-lock.mjs
The above code requires you to:
-
have a
BLOCKFROST_PROJECT_ID
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.mjs
placed at the root of yourhello-world
folder.
At this stage, your folder should look roughly like this:
./hello-world
│
├── README.md
├── aiken.toml
├── plutus.json
├── generate-credentials.mjs
├── hello-world-lock.mjs
├── me.addr
├── me.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: 48b8178e3a8842227dfbb0f73669efc163f73fd7c8758b7dafc0a5a5f07a5445
Datum: {"value":{"alternative":0,"fields":["4d871c3f74db9ea19e2ca678ac92672ada301a0d8ce2dc6091692a30"]}}
Inspecting the transaction
Now is a good moment to pause and have a look at CardanoScan. Here's an example of a 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": "4d871c3f74db9ea19e2ca678ac92672ada301a0d8ce2dc6091692a30"
}
]
}
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.mjs
and copy over some of the boilerplate
from the first one.
import cbor from "cbor";
import { BlockfrostProvider, MeshWallet } from '@meshsdk/core';
import { applyParamsToScript } from "@meshsdk/core-csl";
import fs from 'node:fs';
const blockchainProvider = new BlockfrostProvider(process.env.BLOCKFROST_PROJECT_ID);
const wallet = new MeshWallet({
networkId: 0,
fetcher: blockchainProvider,
submitter: blockchainProvider,
key: {
type: 'root',
bech32: fs.readFileSync('me.sk').toString(),
},
});
const blueprint = JSON.parse(fs.readFileSync('./plutus.json'));
const script = {
code: cbor
.encode(Buffer.from(blueprint.validators[0].compiledCode, "hex"))
.toString("hex"),
version: "V3",
};
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.mjs
)
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.
Note that we need to explicitly add a signer using .setRequiredSigners
so that it
gets added to the extra_signatories
of our transaction and becomes
accessible for our script.
import cbor from "cbor";
import {
resolvePaymentKeyHash,
resolvePlutusScriptAddress,
BlockfrostProvider,
MeshWallet,
Transaction,
} from '@meshsdk/core';
import { applyParamsToScript } from "@meshsdk/core-csl";
import fs from 'node:fs';
const blockchainProvider = new BlockfrostProvider(process.env.BLOCKFROST_PROJECT_ID);
const wallet = new MeshWallet({
networkId: 0,
fetcher: blockchainProvider,
submitter: blockchainProvider,
key: {
type: 'root',
bech32: fs.readFileSync('me.sk').toString(),
},
});
const blueprint = JSON.parse(fs.readFileSync('./plutus.json'));
const script = {
code: cbor
.encode(Buffer.from(blueprint.validators[0].compiledCode, "hex"))
.toString("hex"),
version: "V3",
};
async function fetchUtxo(addr) {
const utxos = await blockchainProvider.fetchAddressUTxOs(addr);
return utxos.find((utxo) => {
return utxo.input.txHash == process.argv[2];
});
}
const utxo = await fetchUtxo(resolvePlutusScriptAddress(script, 0))
const address = (await wallet.getUsedAddresses())[0];
const owner = resolvePaymentKeyHash(address);
const datum = {
alternative: 0,
fields: [owner],
};
const redeemer = {
data: {
alternative: 0,
fields: ['Hello, World!'],
},
};
const unsignedTx = await new Transaction({ initiator: wallet })
.redeemValue({
value: utxo,
script: script,
datum: datum,
redeemer: redeemer,
})
.sendValue(address, utxo)
.setRequiredSigners([address])
.build();
const signedTx = await wallet.signTx(unsignedTx, true);
const txHash = await wallet.submitTx(signedTx);
console.log(`1 tADA unlocked from the contract at:
Tx ID: ${txHash}
Redeemer: ${JSON.stringify(redeemer)}
`);
Run this script as usual, but this time, also passing the transaction id obtained from the previous command locking the funds. For example:
node hello-world-unlock.mjs 48b8178e3a8842227dfbb0f73669efc163f73fd7c8758b7dafc0a5a5f07a5445
If everything worked as planned you should see something resembling the following output:
1 tADA unlocked from the contract at:
Tx ID: 1e1987e0d0aaa35a631bdc0a0dfbadcba5b33b349d1153612cac4167262fdae2
Redeemer: {"data":{"alternative":0,"fields":["Hello, World!"]}}
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.