Interact with smart contracts

Create transactions to work with smart contracts.

Lock Assets in Smart Contract

Assets may be reserved in a smart contract by "locking" them at the script's address. The assets can only be subsequently unlocked when certain conditions are met, for example, in the case of making a purchase in a marketplace contract.

In this demo, we will lock selected assets from your wallet in analways succeed smart contract. Even though it is called "always succeed" because there is no actual "validating" logic, unlocking the assets still requires the correct datum to be supplied. Also note that in practice, multiple assets (both native assets and lovelace) can be sent to the contract in a single transaction.

We need to supply the script address. Luckily Mesh has a handy function to "resolve" (work out) the script address automatically using: Resolve Script Address from the script's CBOR (4e4d01000033222220051200120011). Here's how it's done:

import { resolvePlutusScriptAddress } from '@meshsdk/core';
import type { PlutusScript } from '@meshsdk/core';

const script: PlutusScript = {
  code: '4e4d01000033222220051200120011',
  version: 'V1',
};

const scriptAddress = resolvePlutusScriptAddress(script, 0);

To lock assets in this contract, here's the full code:

import { Transaction, Asset } from '@meshsdk/core';

// this is the script address of always succeed contract
const scriptAddress = 'addr_test1wpnlxv2xv9a9ucvnvzqakwepzl9ltx7jzgm53av2e9ncv4sysemm8';

const tx = new Transaction({ initiator: wallet })
  .sendAssets(
    {
      address: scriptAddress,
      datum: {
        value: 'supersecret',
      },
    },
    [
      {
        unit: "64af286e2ad0df4de2e7de15f8ff5b3d27faecf4ab2757056d860a424d657368546f6b656e",
        quantity: "1",
      },
    ],
  );

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

If the transaction is successful, you would usually want to keep a record of the asset's unit and the datum used in the transaction, as this information is required to unlock the assets.

Lock assets in smart contract

Define a datum and select assets to lock in smart contract. Note: this demo only works on preprod network.

Lock assets in smart contract
No wallets installed

Unlock Assets from Smart Contract

To unlock assets locked in a smart contract, you need to create a transaction that supplies the correct datum. In fact, only a transaction with the corrent datum supplied is able to unlock the assets. in the smart contract, which is required for the transaction's input. In our example, we shall also define the unit of the asset we are searching for so that we can search for the suitable UTxO.

First, let's create a function to fetch the correct input UTxO from the script address. This input UTxO is needed for the transaction builder. Notee that in this demo, we are using KoiosProvider, but any of the providers which are implemented by Mesgh can be used (see Providers for list).

async function _getAssetUtxo({ scriptAddress, asset, datum }) {
  const koios = new KoiosProvider('preprod');

  const utxos = await koios.fetchAddressUTxOs(
    scriptAddress,
    asset
  );

  const dataHash = resolveDataHash(datum);

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

  return utxo;
}

Then, we query the script address for the UTxO that contains the correct data hash:

// fetch input UTXO
const assetUtxo = await _getAssetUtxo({
  scriptAddress: 'addr_test1wpnlxv2xv9a9ucvnvzqakwepzl9ltx7jzgm53av2e9ncv4sysemm8',
  asset: '64af286e2ad0df4de2e7de15f8ff5b3d27faecf4ab2757056d860a424d657368546f6b656e',
  datum: 'supersecret',
});

Next, we create the transaction. 4e4d01000033222220051200120011 is the script CBOR for the always succeed smart contract.

// create the unlock asset transaction
const tx = new Transaction({ initiator: wallet })
  .redeemValue({
    value: assetUtxo,
    script: {
      version: 'V1',
      code: '4e4d01000033222220051200120011',
    },
    datum: 'supersecret',
  })
  .sendValue(address, assetUtxo) // address is recipient address
  .setRequiredSigners([address]);

Lastly, we build and sign the transaction. Note that here we need to set the 'partial sign' parameter to true.

const unsignedTx = await tx.build();
// note that the partial sign is set to true
const signedTx = await wallet.signTx(unsignedTx, true);

Putting it all together, here's the code to unlock assets from smart contract:

async function _getAssetUtxo({ scriptAddress, asset, datum }) {
  const koios = new KoiosProvider('preprod');

  const utxos = await koios.fetchAddressUTxOs(
    scriptAddress,
    asset
  );

  const dataHash = resolveDataHash(datum);

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

  return utxo;
}

// fetch input UTXO
const assetUtxo = await _getAssetUtxo({
  scriptAddress: 'addr_test1wpnlxv2xv9a9ucvnvzqakwepzl9ltx7jzgm53av2e9ncv4sysemm8',
  asset: '64af286e2ad0df4de2e7de15f8ff5b3d27faecf4ab2757056d860a424d657368546f6b656e',
  datum: 'supersecret',
});

// get wallet change address
const address = await wallet.getChangeAddress();

// create the unlock asset transaction
const tx = new Transaction({ initiator: wallet })
  .redeemValue({
    value: assetUtxo,
    script: {
      version: 'V1',
      code: '4e4d01000033222220051200120011',
    },
    datum: 'supersecret',
  })
  .sendValue(address, assetUtxo) // address is recipient address
  .setRequiredSigners([address]);

const unsignedTx = await tx.build();
// note that the partial sign is set to true
const signedTx = await wallet.signTx(unsignedTx, true);
const txHash = await wallet.submitTx(signedTx);
Unlock assets from smart contract

Define the datum and Unit of the asset to unlock it from the smart contract. Note: remember that this requires interaction with a blockchain: allow some time for the transaction to confirm before attempt unlocking. This demo only works on preprod network.

Unlock assets in smart contract
No wallets installed

Minting Assets with Smart Contract

In this demo, we will use a Plutus script to mint tokens. This script is designed to always succeed, meaning that anyone can sign and mint tokens with it, as there is no extra validation logic carried out by this script.

Firstly, we create a new PlutusScript and "redeemer" (Action):

import { Action, PlutusScript } from '@meshsdk/core';

const script: PlutusScript = {
  code: plutusMintingScriptCbor,
  version: 'V2',
};

const redeemer: Partial<Action> = {
  tag: 'MINT',
};

You can get the 'always succeed' Plutus script CBOR (to replace plutusMintingScriptCbor) from this gist.

Next, we define the asset and its metadata, and add the script (PlutusScript), redeemer (Action), and theasset (Mint) to the transaction:

import { AssetMetadata, Mint } from '@meshsdk/core';

const assetMetadata1: AssetMetadata = {
  "name": "Mesh Token",
  ...
}

const asset: Mint = {
  assetName: 'MeshToken',
  ...
}

tx.mintAsset(script, asset, redeemer);

Here is the full code:

import { Transaction } from '@meshsdk/core';
import { AssetMetadata, Mint, Action, PlutusScript } from '@meshsdk/core';

const script: PlutusScript = {
  code: plutusMintingScriptCbor,
  version: 'V2',
};

const redeemer: Partial<Action> = {
  tag: 'MINT',
};

const tx = new Transaction({ initiator: wallet });

// define asset#1 metadata
const assetMetadata1: AssetMetadata = {
  "name": "Mesh Token",
  "image": "ipfs://QmRzicpReutwCkM6aotuKjErFCUD213DpwPq6ByuzMJaua",
  "mediaType": "image/jpg",
  "description": "This NFT was minted by Mesh (https://meshjs.dev/)."
};
const asset1: Mint = {
  assetName: 'MeshToken',
  assetQuantity: '1',
  metadata: assetMetadata1,
  label: '721',
  recipient: 'addr_test1vpvx0sacufuypa2k4sngk7q40zc5c4npl337uusdh64kv0c7e4cxr',
};
tx.mintAsset(
  script,
  asset1,
  redeemer,
);

const unsignedTx = await tx.build();
const signedTx = await wallet.signTx(unsignedTx);
const txHash = await wallet.submitTx(signedTx);
Mint assets and send to recipients

Add or remove recipients, input the address and define the asset metadata before minting and sending.

Recipients
Recipient #1
No wallets installed

Inline Datum

It is possible to attach a "datum" (piece of data) inline to a UTxO outputs, which allows us to use the UTxO to hold information which we can then use without having to spend it (as with normal UTxOs). You can learn more from CIP-32.

Here's an example of creating a UTxO with inline datum:

sendAssets(
  {
    address: someAddress,
    datum: {
      value: 'supersecret',
      inline: true,
    },
  },
  [
    {
      unit: "64af286e2ad0df4de2e7de15f8ff5b3d27faecf4ab2757056d860a424d657368546f6b656e",
      quantity: "1",
    },
  ],
)

As you can see, you simply have to define the datum field in the Recipient input parameter, including a (value) and setting inline to true. This works for every transaction endpoint (e.g. sendLovelace(), sendAssets(), sendValue()).

Reference Script

Validation requires access to any scripts that are involved, but adding entire scripts to transactions increases the transaction size and can lead to bloating of the blockchain. A useful solution is to instead store script references in a UTxO, which allows us to later use that UTxO to build future transactions without having to include the entire script. Thus we can reduce the size of the transaction, which may allow us to send (for example) multiple scripts within one transaction without exceeding the maximum transaction size.

sendAssets(
  {
    address: someAddress,
    script: {
      version: 'V2',
      code: '...',
    },
  },
  [
    {
      unit: "64af286e2ad0df4de2e7de15f8ff5b3d27faecf4ab2757056d860a424d657368546f6b656e",
      quantity: "1",
    },
  ],
)

Simply define the script as the Recipient input parameter. This works for every transaction endpoints (e.g.. sendLovelace(), sendAssets(), sendValue()).

Designing Datum

Mesh allows you to freely design the datum structure to suit the plutus smart contract requirements. You can import the Data type to help you design the datum.

import { resolveDataHash } from '@meshsdk/core';
import type { Data } from '@meshsdk/core';

A string

A datum as simple as just a string, preferably a hex string.

A number

It can also be a number.

An array

Or an array, where each item can be either a string, number, a list, or a map.

A Map

It can also be a map, where both the keys and its values can be a string, number, a list, or a map.

With constructor

Or a datum with a constructor, where alternative is a number, and fields is an array.

Using Redeemer

For redeemers in Mesh, you use the type Action and you only supply the Data part to construct it.

import { resolvePaymentKeyHash } from '@meshsdk/core';
import type { Data } from '@meshsdk/core';

Designing Redeemer

Similarly to the datum, there is freedom in design to suit any smart contract, but the redeemer needs to be supplied a little differently.

In this example, we represent a redeemer which matches the StartRedeemeras defined above with the first Used Address as input:

Supplying the SecondRedeemer as defined above:

Supplying the EndRedeemer as defined above:

Transaction construction

Within the transaction, we can include the redeemer within redeemValue: