End to End Guide with plu-ts

A guide to deploying an app with a smart contract written in plu-ts in TypeScript.

In this guide, we will build an app that interacts with a smart contract written in plu-ts. plu-ts is for building Cardano smart contracts which are entirely written, compiled and serialized using TypeScript. Here is an introduction to plu-ts on Gimbalabs PPBL.

This guide is a continuation from the Hello plu-ts example. You can get the entire source code for this guide on GitHub, and start a plu-ts project with this starter kit (or start on Demeter).

At the end of this guide, you will be able to deploy an app where users can connect their wallets and interact with a smart contract, end-to-end written in TypeScript.

Project Set Up

Firstly, follow the instructions on Start a Web3 app on Next.js, a step-by-step guide to setup a Next.js web application.

Or begin the project with this starter kit with a single Mesh CLI command:

npx create-mesh-app project-name -t starter -s next -l ts

Next, install plu-ts with npm (or yarn):

npm install @harmoniclabs/plu-ts

Always Succeed Script

In this section, we will be locking 2 ADA from your wallet to an "always succeed" smart contract. In practice, multiple assets (both native assets and lovelace) can be sent to the contract in a single transaction.

Let's import the necessary modules from plu-ts:

import {
  bool,
  compile,
  makeValidator,
  pBool,
  pfn,
  pstruct,
  Script,
  V2,
} from '@harmoniclabs/plu-ts';

Let's see how a smart contract that always succeeds is written in plu-ts:

const Data = pstruct({
  Anything: {},
});

const contract = pfn(
  [Data.type, Data.type, V2.PScriptContext.type],
  bool
)((datum, redeemer, ctx) =>
  pBool(true) // always suceeds 
);

Yea, I know right? Thats pretty much it!

Next, we will execute makeValidator() so that the node will be able to evaluate it, then compile() to compile the validator, and then wrap it in a Script that can be used offchain:

const untypedValidator = makeValidator(contract);
const compiledContract = compile(untypedValidator);

const script = new Script('PlutusScriptV2', compiledContract);

Finally, we will get the compiled contract's CBOR:

const scriptCbor = script.cbor.toString();

As you can see, we wrote and compiled the smart contract and serialized it to CBOR entirely in TypeScript. You can learn more about it on plu-ts documentation.

Lock Assets in the Always Succeed Script

Asset locking is a feature wherein certain assets are reserved on the smart contract. The assets can only be unlocked again when certain conditions are met. In this example, we will lock 2 ADA in the always succeed script. By "always succeeds", we mean that the validator does not check for any specific conditions, and so will always return true.

First, we initialize a new PlutusScript with the serialized CBOR, and get the script's address.

const script: PlutusScript = {
  code: scriptCbor,
  version: 'V2',
};
const scriptAddress = resolvePlutusScriptAddress(script, 0);

Next, we get the wallet's address (for multiple address wallet, we select the first address) and use that to build the hash, which will be used as the datum value.

const address = (await wallet.getUsedAddresses())[0];
const walletKeyhash = resolvePaymentKeyHash(address);

Then, we build the transaction and send the ADA to the script's address. Lastly, we sign and submit the transaction.

const tx = new Transaction({ initiator: wallet })
  .sendLovelace(
    {
      address: scriptAddress,
      datum: {
        value: walletKeyhash,
      },
    },
    '2000000'
  );

const unsignedTx = await tx.build();
const signedTx = await wallet.signTx(unsignedTx);
const txHash = await wallet.submitTx(signedTx);

That is all! You can now lock your ADA (and other assets) in the script. You can also give the demo a try: you can lock your ADA by clicking the button below.

No wallets installed

Here is an example of a successful transaction on preprod.cardanoscan.io.

Next, we will see how to unlock the asset.

Unlock Asset from Always Succeed Script

As we may have locked assets in the contract, you can create transactions to unlock the assets.

Similar to locking assets, let's get the PlutusScript, script address, wallet address and the wallet's keyhash.

const script: PlutusScript = {
  code: scriptCbor,
  version: 'V2',
};
const scriptAddress = resolvePlutusScriptAddress(script, 0);

const address = (await wallet.getUsedAddresses())[0];
const walletKeyhash = resolvePaymentKeyHash(address);

Then, we fetch the input UTXO from the script address. This input UTXO is needed for transaction builder. In this demo, we are using KoiosProvider, but this can be interchanged with other providers that Mesh provides, see Providers.

const blockchainProvider = new KoiosProvider('preprod');
const dataHash = resolveDataHash(walletKeyhash);
const utxos = await blockchainProvider.fetchAddressUTxOs(
  scriptAddress,
  'lovelace'
);
let utxo = utxos.find((utxo: any) => {
  return utxo.output.dataHash == dataHash;
});

Next, we build the transaction and send the ADA back to the wallet address. We use redeemValue() to consume the UTXO on the script, and sendValue() to send the ADA back to the wallet address. Note that here we do a partial sign. Lastly, we sign and submit the transaction.

const tx = new Transaction({ initiator: wallet })
  .redeemValue({
    value: utxo,
    script: script,
    datum: walletKeyhash,
  })
  .sendValue(address, utxo)
  .setRequiredSigners([address]);

const unsignedTx = await tx.build();
const signedTx = await wallet.signTx(unsignedTx, true);
const txHash = await wallet.submitTx(signedTx);

If you want to see it in action, click the button below to unlock your ADA.

No wallets installed

Here is an example of a successful transaction on preprod.cardanoscan.io.

Now, you have successfully locked and unlocked assets from the script. You can start your own project with our plu-ts starter kit.

Hello plu-ts Script

In this section, we will be locking 2 ADA from your wallet to a Hello plu-ts smart contract. Unlike the Always Succeed script, this script will only be able to unlock if we redeem with a string Hello plu-ts in our redeemer. Let's learn how we can do that.

Let's import the necessary modules from plu-ts:

import {
  bool,
  compile,
  makeValidator,
  pfn,
  Script,
  PPubKeyHash,
  PScriptContext,
  bs,
} from '@harmoniclabs/plu-ts';

Here is a Hello plu-ts script written in plu-ts:

const contract = pfn(
  [PPubKeyHash.type, bs, PScriptContext.type],
  bool
)((owner, message, ctx) => {
  const isBeingPolite = message.eq('Hello plu-ts');

  const signedByOwner = ctx.tx.signatories.some(owner.eqTerm);

  return isBeingPolite.and(signedByOwner);
});

Next, we will execute makeValidator() so that the node will be able to evaluate it, compile() to compile the validator, and wrapping it in a Script that can be used offchain:

const untypedValidator = makeValidator(contract);
const compiledContract = compile(untypedValidator);

const script = new Script('PlutusScriptV2', compiledContract);

Lasly, we will get the compiled contract's CBOR:

const scriptCbor = script.cbor.toString();

Lock ADA in Hello plu-ts Script

Similar to the Always Succeed script, we will be locking 2 ADA from your wallet to the Hello plu-ts script. Let's see how we can do that.

Firstly, we initialize a new PlutusScript with the serialized CBOR, and get the script's address.

const script: PlutusScript = {
  code: scriptCbor,
  version: 'V2',
};
const scriptAddress = resolvePlutusScriptAddress(script, 0);

Next, we get the wallet's address (for multiple address wallet, we select the first address) and use that to build the hash, which will be used as the datum value.

const address = (await wallet.getUsedAddresses())[0];
const walletKeyhash = resolvePaymentKeyHash(address);

Then, we build the transaction and send the ADA to the script's address. We also include the datum value, which is the wallet's hash. Lastly, we sign and submit the transaction.

const tx = new Transaction({ initiator: wallet })
  .sendLovelace(
    {
      address: scriptAddress,
      datum: {
        value: walletKeyhash,
        inline: true,
      },
    },
    '2000000'
  );

const unsignedTx = await tx.build();
const signedTx = await wallet.signTx(unsignedTx);
const txHash = await wallet.submitTx(signedTx);

To see it in action, connect your wallet and give it a try.

No wallets installed

Here is an example of a successful transaction on preprod.cardanoscan.io.

Unlock ADA from Hello plu-ts Script

Now that we have locked ADA in the Hello plu-ts script, let's see how we can unlock it. We will be using the same script as before. Let's get the PlutusScript, script address, wallet address and the wallet's keyhash.

const script: PlutusScript = {
  code: scriptCbor,
  version: 'V2',
};
const scriptAddress = resolvePlutusScriptAddress(script, 0);

const address = (await wallet.getUsedAddresses())[0];
const walletKeyhash = resolvePaymentKeyHash(address);

Then, we use KoiosProvider to fetch input UTXO from the script address. This input UTXO is needed for transaction builder. In this demo, we are using KoiosProvider, but this can be interchange with other providers that Mesh provides, see Providers.

We use resolveDataHash() to get the data hash of the wallet address. Doing so allow us to retrive the UTXO that is locked in the script. We then use find() to filter the UTXO that is locked by the user who locked it.

const dataHash = resolveDataHash(walletKeyhash);

const blockchainProvider = new KoiosProvider('preprod');
const utxos = await blockchainProvider.fetchAddressUTxOs(
  scriptAddress,
  'lovelace'
);

let utxo = utxos.find((utxo: any) => {
  return utxo.output.dataHash == dataHash;
});

Next, we need to create a redeemer to redeem the locked asset from the script. The redeemer is the argument specified by the user that interacts with the smart contract In this contract, we want to send the string "Hello plu-ts".

const redeemer = {
  data: 'Hello plu-ts',
};

Now that we have the input UTXO and the redeemer, we can build the transaction. We use redeemValue() to consume the UTXO on the script, and sendValue() to send the ADA back to the wallet address. Notice that we use utxo in the redeemValue(), this is how we can use reference input. Note that here we do a partial sign because we have smart contracts in the transaction. Lastly, we sign and submit the transaction.

const tx = new Transaction({ initiator: wallet })
  .redeemValue({
    value: utxo,
    script: script,
    datum: utxo,
    redeemer: redeemer,
  })
  .sendValue(address, utxo)
  .setRequiredSigners([address]);

const unsignedTx = await tx.build();
const signedTx = await wallet.signTx(unsignedTx, true);
const txHash = await wallet.submitTx(signedTx);

If you want to see it in action, click the button below to unlock your ADA.

No wallets installed

Here is an example of a successful transaction on preprod.cardanoscan.io.

Now, you have successfully locked and unlocked assets from the script. If you want to see the full code, you can check it out in our GitHub repo or start your own project with our plu-ts starter kit.