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. The contract is written in Haskell, and it is open-source and available on GitHub.
If you would like to try out the demo in this guide, you can mint a Mesh token which we will use for listing on this marketplace demo. Connect your wallet and mint the token on preprod/preview testnet.
If you now have the Mesh token, we can list it on the marketplace in this demo.
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);
Give the demo a try!
Now implement your own marketplace. Note: you may need a database to store the listing information.
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);
Give the demo a try! Try cancelling the listing and get your NFT back if you have listed one.
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);
Give the demo a try! Try purchasing the NFT by connecting another wallet.
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);
Give the demo a try! Try updating the listing. Just simplicity for this demo, we will update the price to the same price, but in a real application you would update the price to a new price.
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!