Smart Contract Transactions

In this guide, we will build a marketplace where users can list their assets for sale and purchase the listed assets. The seller can update or cancel the listing at any time.

Initialize the Plutus script

The first step is to initialize the Plutus script. The compiled plutus smart contract script CBOR is:

const scriptCbor = '59079559079201000033232323232323232323232323232332232323232323232222232325335333006300800530070043333573466e1cd55cea80124000466442466002006004646464646464646464646464646666ae68cdc39aab9d500c480008cccccccccccc88888888888848cccccccccccc00403403002c02802402001c01801401000c008cd4060064d5d0a80619a80c00c9aba1500b33501801a35742a014666aa038eb9406cd5d0a804999aa80e3ae501b35742a01066a0300466ae85401cccd54070091d69aba150063232323333573466e1cd55cea801240004664424660020060046464646666ae68cdc39aab9d5002480008cc8848cc00400c008cd40b9d69aba15002302f357426ae8940088c98c80c8cd5ce01981901809aab9e5001137540026ae854008c8c8c8cccd5cd19b8735573aa004900011991091980080180119a8173ad35742a004605e6ae84d5d1280111931901919ab9c033032030135573ca00226ea8004d5d09aba2500223263202e33573805e05c05826aae7940044dd50009aba1500533501875c6ae854010ccd540700808004d5d0a801999aa80e3ae200135742a00460446ae84d5d1280111931901519ab9c02b02a028135744a00226ae8940044d5d1280089aba25001135744a00226ae8940044d5d1280089aba25001135744a00226ae8940044d55cf280089baa00135742a00460246ae84d5d1280111931900e19ab9c01d01c01a101b13263201b3357389201035054350001b135573ca00226ea80054049404448c88c008dd6000990009aa80a911999aab9f0012500a233500930043574200460066ae880080548c8c8cccd5cd19b8735573aa004900011991091980080180118061aba150023005357426ae8940088c98c8054cd5ce00b00a80989aab9e5001137540024646464646666ae68cdc39aab9d5004480008cccc888848cccc00401401000c008c8c8c8cccd5cd19b8735573aa0049000119910919800801801180a9aba1500233500f014357426ae8940088c98c8068cd5ce00d80d00c09aab9e5001137540026ae854010ccd54021d728039aba150033232323333573466e1d4005200423212223002004357426aae79400c8cccd5cd19b875002480088c84888c004010dd71aba135573ca00846666ae68cdc3a801a400042444006464c6403866ae700740700680640604d55cea80089baa00135742a00466a016eb8d5d09aba2500223263201633573802e02c02826ae8940044d5d1280089aab9e500113754002266aa002eb9d6889119118011bab00132001355012223233335573e0044a010466a00e66442466002006004600c6aae754008c014d55cf280118021aba200301313574200222440042442446600200800624464646666ae68cdc3a800a40004642446004006600a6ae84d55cf280191999ab9a3370ea0049001109100091931900899ab9c01201100f00e135573aa00226ea80048c8c8cccd5cd19b875001480188c848888c010014c01cd5d09aab9e500323333573466e1d400920042321222230020053009357426aae7940108cccd5cd19b875003480088c848888c004014c01cd5d09aab9e500523333573466e1d40112000232122223003005375c6ae84d55cf280311931900899ab9c01201100f00e00d00c135573aa00226ea80048c8c8cccd5cd19b8735573aa004900011991091980080180118029aba15002375a6ae84d5d1280111931900699ab9c00e00d00b135573ca00226ea80048c8cccd5cd19b8735573aa002900011bae357426aae7940088c98c802ccd5ce00600580489baa001232323232323333573466e1d4005200c21222222200323333573466e1d4009200a21222222200423333573466e1d400d2008233221222222233001009008375c6ae854014dd69aba135744a00a46666ae68cdc3a8022400c4664424444444660040120106eb8d5d0a8039bae357426ae89401c8cccd5cd19b875005480108cc8848888888cc018024020c030d5d0a8049bae357426ae8940248cccd5cd19b875006480088c848888888c01c020c034d5d09aab9e500b23333573466e1d401d2000232122222223005008300e357426aae7940308c98c8050cd5ce00a80a00900880800780700680609aab9d5004135573ca00626aae7940084d55cf280089baa0012323232323333573466e1d400520022333222122333001005004003375a6ae854010dd69aba15003375a6ae84d5d1280191999ab9a3370ea0049000119091180100198041aba135573ca00c464c6401a66ae7003803402c0284d55cea80189aba25001135573ca00226ea80048c8c8cccd5cd19b875001480088c8488c00400cdd71aba135573ca00646666ae68cdc3a8012400046424460040066eb8d5d09aab9e500423263200a33573801601401000e26aae7540044dd500089119191999ab9a3370ea00290021091100091999ab9a3370ea00490011190911180180218031aba135573ca00846666ae68cdc3a801a400042444004464c6401666ae7003002c02402001c4d55cea80089baa0012323333573466e1d40052002212200223333573466e1d40092000212200123263200733573801000e00a00826aae74dd5000891999ab9a3370e6aae74dd5000a40004008464c6400866ae700140100092612001490103505431001123230010012233003300200200122212200201';

The Plutus script is initialized with the following code:

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

Let's say we are testing our marketplace implementation on preprod, we can resolve the Plutus script address with resolvePlutusScriptAddress where we input the PlutusScript and define the network (in our demo we use 0 for testnet):

const scriptAddress = resolvePlutusScriptAddress(script, 0);

Listing Asset for Sale

Firstly, we get the user's wallet address: this address is the seller's address. We can acquire the first wallet's address from the connected wallet with getUsedAddresses():

const addr = (await wallet.getUsedAddresses())[0];

Then, we create the datum that has the following schema:

const datumConstr: Data = {
  alternative: 0,
  fields: [
    resolvePaymentKeyHash(addr), // seller address as pubkeyhash
    listPriceInLovelace, // price
    policyId, // policy ID of token
    assetId, // asset name of token in hex
  ],
};

Lastly, we create a transaction that uses sendAssets(), to send the asset for sale to the script address with the datum we have defined. policyId + assetId is the asset name in hex. We build the transaction, the seller signs the transaction and submits the transaction to the blockchain.

const tx = new Transaction({ initiator: wallet })
.sendAssets(
  {
    address: scriptAddress,
    datum: {
      value: datumConstr,
    },
  },
  [
    {
      unit: policyId + assetId,
      quantity: '1',
    },
  ]
);

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

Now implement your own marketplace. Note: you may need a database to store the listing information.

Full code on how to list an asset for sale:

const addr = (await wallet.getUsedAddresses())[0];

const datumConstr: Data = {
  alternative: 0,
  fields: [
    resolvePaymentKeyHash(addr),
    listPriceInLovelace,
    policyId,
    assetId,
  ],
};

const tx = new Transaction({ initiator: wallet })
.sendAssets(
  {
    address: scriptAddress,
    datum: {
      value: datumConstr,
    },
  },
  [
    {
      unit: policyId + assetId,
      quantity: '1',
    },
  ]
);

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

Cancel the Listing

Next, we will learn how to cancel the listing. Only the seller of the NFT can cancel the listing, thus we will define the datum with the following information:

const datumConstr: Data = {
  alternative: 0,
  fields: [
    resolvePaymentKeyHash(addr), // seller address as pubkeyhash
    listPriceInLovelace, // price
    policyId, // policy ID of token
    assetId, // asset name of token in hex
  ],
};

For cancel, update and purchase endpoints, we need the UTxO in the script address as inputs to create the transaction. We use fetchAddressUTxOs() from one of the providers to query for UTxOs that contain the asset of our interest. Next, we filter the UTxO list by the datum hash, which we can get from the datum with resolveDataHash() (see resolvers). Here is the implementation for _getAssetUtxo(), to get the UTxO in the script address that consists of the listed asset, and we use the datum hash to filter and get the correct UTxO for the transaction's input:

async function _getAssetUtxo({ scriptAddress, asset, datum }) {
  const utxos = await blockchainFetcher.fetchAddressUTxOs(
    scriptAddress,
    asset
  );
  if (utxos.length == 0) {
    throw 'No listing found.';
  }
  const dataHash = resolveDataHash(datum);
  let utxo = utxos.find((utxo: any) => {
    return utxo.output.dataHash == dataHash;
  });
  return utxo;
}

Next, we define the redeemer for cancelling the listing:

const redeemer = { data: { alternative: 1, fields: [] } };

Finally, we can build the transaction with the following code. We use the redeemValue() method to redeem the UTxO in the script address, and send the value back to the seller's address. We also need to set the "required signers" to include just the seller's address.

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

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

Full code for cancelling a listing:

const addr = (await wallet.getUsedAddresses())[0];

const datumConstr: Data = {
  alternative: 0,
  fields: [
    resolvePaymentKeyHash(addr),
    listPriceInLovelace,
    policyId,
    assetId,
  ],
};

const assetUtxo = await _getAssetUtxo({
  scriptAddress: scriptAddress,
  asset: 'd9312da562da182b02322fd8acb536f37eb9d29fba7c49dc172555274d657368546f6b656e',
  datum: datumConstr,
});

if (assetUtxo === undefined) {
  throw 'No listing found.';
}

const redeemer = { data: { alternative: 1, fields: [] } };

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

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

Purchase the Listed Asset

A key feature of a marketplace is the ability to purchase the listed asset from the seller. The purchase endpoint will take the asset, the price and the seller address as parameters. These parameters will be used to create the datum for the validator. A successful purchase will result in the transfer of the asset to the buyer and the listed price to the seller.

First, we need the buyer's address to send the asset to:

const addr = (await wallet.getUsedAddresses())[0]; // buyer's address

Like the cancel endpoint, we need to create the datum for the validator. It is important to note that we are using the seller's address to create the datum:

const datumConstr: Data = {
  alternative: 0,
  fields: [
    resolvePaymentKeyHash(sellerAddr), // seller address as pubkeyhash
    listPriceInLovelace, // price
    policyId, // policy ID of token
    assetId, // asset name of token in hex
  ],
};

Then, we will define the redeemer:

const redeemer = { data: { alternative: 0, fields: [] } };

Finally, we can build the transaction and submit it to the blockchain. We will use the redeemValue() method to redeem the asset from the validator, use sendValue() to send the asset to the buyer and sendLovelace() to send the payment price to the seller:

const tx = new Transaction({ initiator: wallet })
  .redeemValue({
    value: assetUtxo,
    script: script,
    datum: datumConstr,
    redeemer: redeemer,
  })
  .sendValue(addr, assetUtxo)
  .sendLovelace(sellerAddr, listPriceInLovelace.toString())
  .setRequiredSigners([addr]);

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

Full code for purchasing an asset:

const addr = (await wallet.getUsedAddresses())[0]; // buyer's address

const datumConstr: Data = {
  alternative: 0,
  fields: [
    resolvePaymentKeyHash(sellerAddr),
    listPriceInLovelace,
    policyId,
    assetId,
  ],
};

const assetUtxo = await _getAssetUtxo({
  scriptAddress: scriptAddress,
  asset: 'd9312da562da182b02322fd8acb536f37eb9d29fba7c49dc172555274d657368546f6b656e',
  datum: datumConstr,
});

const redeemer = { data: { alternative: 0, fields: [] } };

const tx = new Transaction({ initiator: wallet })
  .redeemValue({
    value: assetUtxo,
    script: script,
    datum: datumConstr,
    redeemer: redeemer,
  })
  .sendValue(addr, assetUtxo)
  .sendLovelace(sellerAddr, listPriceInLovelace.toString())
  .setRequiredSigners([addr]);

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

Update the Listing

Finally, we will learn how to update the listing. Only the seller of the NFT can update the listing, thus we will define the datum with the following information:

const datumConstr: Data = {
  alternative: 0,
  fields: [
    resolvePaymentKeyHash(addr), // seller address as pubkeyhash
    listPriceInLovelace, // listed price
    policyId, // policy ID of token
    assetId, // asset name of token in hex
  ],
};

We will also need to create the updated datum with the new price:

const datumConstrNew: Data = {
  alternative: 0,
  fields: [
    resolvePaymentKeyHash(addr), // seller address as pubkeyhash
    updatedPriceInLovelace, // updated price
    policyId, // policy ID of token
    assetId, // asset name of token in hex
  ],
};

Next, we define the redeemer for updating the listing:

const redeemer = { data: { alternative: 1, fields: [] } };

Finally, we can build the transaction to update the listing. We use the redeemValue() method to redeem the UTxO in the script address with the original datum, and then we use the sendAssets() method to send the NFT to the same script address, with the new datum.

const tx = new Transaction({ initiator: wallet })
  .redeemValue({
    value: assetUtxo,
    script: script,
    datum: datumConstr,
    redeemer: redeemer,
  })
  .setRequiredSigners([addr])
  .sendAssets(
    {
      address: scriptAddress,
      datum: {
        value: datumConstrNew,
      },
    },
    [
      {
        unit: 'd9312da562da182b02322fd8acb536f37eb9d29fba7c49dc172555274d657368546f6b656e',
        quantity: '1',
      },
    ]
  );

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

Full code for updating the listing:

const addr = (await wallet.getUsedAddresses())[0];

const datumConstr: Data = {
  alternative: 0,
  fields: [
    resolvePaymentKeyHash(addr),
    listPriceInLovelace,
    policyId,
    assetId,
  ],
};

const datumConstrNew: Data = {
  alternative: 0,
  fields: [
    resolvePaymentKeyHash(addr),
    updatedPriceInLovelace,
    policyId,
    assetId,
  ],
};

const assetUtxo = await _getAssetUtxo({
  scriptAddress: scriptAddress,
  asset: 'd9312da562da182b02322fd8acb536f37eb9d29fba7c49dc172555274d657368546f6b656e',
  datum: datumConstr,
});

const redeemer = { data: { alternative: 1, fields: [] } };

const tx = new Transaction({ initiator: wallet })
  .redeemValue({
    value: assetUtxo,
    script: script,
    datum: datumConstr,
    redeemer: redeemer,
  })
  .setRequiredSigners([addr])
  .sendAssets(
    {
      address: scriptAddress,
      datum: {
        value: datumConstrNew,
      },
    },
    [
      {
        unit: 'd9312da562da182b02322fd8acb536f37eb9d29fba7c49dc172555274d657368546f6b656e',
        quantity: '1',
      },
    ]
  );

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

And there you go! I hope this is a good starting point for you to start building your own apps that use smart contracts!