⛏ī¸Simple Minting dApp

This tutorial demonstrates building a React-based decentralized application (dApp) that interacts with the Concordium blockchain for minting and managing non-fungible tokens (NFTs).

Introduction

In this tutorial, you'll create a basic NFT Minting decentralized application (dApp) that connects to the Concordium blockchain. You'll learn how to integrate your dApp with Concordium, enabling interactions with smart contracts deployed on the blockchain.

Overview

The dApp will consist of three main functionalities:

  1. Connect to Web Wallet: Allow users to connect their dApp to the Concordium web wallet.

  2. Initialize Contract: Initialize the smart contract responsible for NFT minting.

  3. Mint NFT: Mint a new non-fungible token (NFT) using the initialized contract.

Prerequisites

Before getting started, ensure you have:

Let's dive in!

Step 1: Set up the React Project

  1. Create a new directory for your dApp.

  2. Inside the directory, initialize a new React project using the TypeScript template:

yarn create react-app <YOUR-DAPP-NAME> --template typescript

Running this command will fetch and install all the packages and dependencies and then create your project.

  1. Open the project in your code editor:

  1. Run the following command to start the development server and view the application interface in your browser:

Step 2: Install Dependencies

Install necessary dependencies for some react components from material-ui and necessary libraries from concordium-web-sdk and concordium-web-wallet-helper. To do this, run the following command:

yarn add @mui/material @emotion/react @mui/icons-material @emotion/styled @concordium/web-sdk @concordium/browser-wallet-api-helpers

Afterward, a "package.json" file that includes all the dependencies will be created:

Step 3: Create Header Component

  1. Create a file named "Header.tsx" " containing a button and handle the connect() to the web wallet.

  2. Add the following code to Header.tsx:

import {
 detectConcordiumProvider,
 WalletApi,
} from "@concordium/browser-wallet-api-helpers";
import { AppBar, Toolbar, Typography, Button } from "@mui/material";
import { useState } from "react";

export default function Header(props: {
 onConnected: (provider: WalletApi, account: string) => void;
 onDisconnected: () => void;
}) {
 const [isConnected, setConnected] = useState(false);

 function connect() {
  detectConcordiumProvider()
   .then((provider) => {
    provider
     .connect()
     .then((account) => {
      setConnected(true);
      props.onConnected(provider, account!);
     })
     .catch((_) => {
      alert("Please allow wallet connection");
      setConnected(false);
     });
    provider.removeAllListeners();
    provider.on("accountDisconnected", () => {
     setConnected(false);
     props.onDisconnected();
    });
    provider.on("accountChanged", (account) => {
     props.onDisconnected();
     props.onConnected(provider, account);
     setConnected(true);
    });
    provider.on("chainChanged", () => {
     props.onDisconnected();
     setConnected(false);
    });
   })
   .catch((_) => {
    console.error(`could not find provider`);
    alert("Please download Concordium Wallet");
   });
 }

 return (
  <AppBar>
   <Toolbar>
    <Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
     Concordium NFT Minting
    </Typography>
    <Button color="inherit" onClick={connect} disabled={isConnected}>
     {isConnected ? "Connected" : "Connect"}
    </Button>
   </Toolbar>
  </AppBar>
 );
}
  1. Call the "Header.tsx" component from "App.tsx" instead of using its default values. Then, run the application:

  1. Click on the "CONNECT" button. A popup window will then open, asking you to confirm that you'd like to connect to this application:

Note: In the following steps, you'll be adding minting functionality. To interact with a contract, ensure it's implemented in Rust and deployed on the chain using concordium-client. When you deploy a contract, Concordium provides you with a module reference containing a unique hash value, which you'll use to invoke the contract.

Step 4: Create Initialize Component

  1. Create a file named "Initialize.tsx" to invoke the init() function in your deployed CIS-2 NFT contract.

  2. Add the following code to "Initialize.tsx":

import { detectConcordiumProvider, SmartContractParameters } from "@concordium/browser-wallet-api-helpers";
import {
 AccountTransactionType,
 CcdAmount,
 InitContractPayload,
 ModuleReference,
} from "@concordium/web-sdk";
import { Button, Link } from "@mui/material";
import { Buffer } from "buffer/";
import { useState } from "react";


export default function Initialize() {

 const [hash, setHash] = useState("");

 const initialize = async () => {
  const provider = await detectConcordiumProvider();
  const account = await provider.connect();

  if (!account) {
   alert("Please connect");
  }
  var REACT_APP_CONTRACT_NAME="CIS2-Multi";
  var REACT_APP_MODULE_REF="312f99d6406868e647359ea816e450eac0ecc4281c2665a24936e6793535c9f6";
  const txnHash = await provider.sendTransaction(
   account!,
   AccountTransactionType.InitContract as any,
   {
    amount: new CcdAmount(BigInt(0)),
    initName: REACT_APP_CONTRACT_NAME,
    moduleRef: new ModuleReference(REACT_APP_MODULE_REF),
    param: Buffer.alloc(0),
    maxContractExecutionEnergy: BigInt(9999),
   } as InitContractPayload
  );

  setHash(txnHash);
 };

 return hash ? (
  <Link
   href={`https://dashboard.testnet.concordium.com/lookup/${hash}`}
   target="_blank"
  >
   View Transaction <br />
   {hash}
  </Link>
 ) : (
  <Button fullWidth variant="outlined" onClick={initialize}>
   Initialize Contract
  </Button>
 );
}
  1. Call the component from "App.tsx" and add some UI logic using useState. This action will depend on whether the wallet is connected or not:

import "./App.css";
import Header from "./Header";
import Initialize from "./Initialize";
import { useState } from "react";
import { Container } from "@mui/material";
export default function App() {
  const [isConnected, setConnected] = useState(false);
 return (
  <div className="App">
   <Header
        onConnected={() => setConnected(true)}
        onDisconnected={() => setConnected(false)}
   />
      
      <Container sx={{ mt: 15 }}>
   {isConnected && <Initialize/>}
   </Container>

  </div>
 );
}
  1. Run the application and click the “INITIALIZE CONTRACT” button. It should pop up your web wallet to create a new instance:

Step 5: Create the Minting Component

  1. Create a file named "Mint.tsx" containing validation checks for the necessary inputs for the data that will be read from the form during the minting of the NFT.

  2. Add the following code to Mint.tsx:

import { detectConcordiumProvider, SmartContractParameters } from "@concordium/browser-wallet-api-helpers";
import {
 AccountTransactionType,
 CcdAmount,
 serializeUpdateContractParameters,
 UpdateContractPayload,
} from "@concordium/web-sdk";
import { Button, Link, Stack, TextField, Typography } from "@mui/material";
import { FormEvent, useState } from "react";
import { Buffer } from "buffer/";

export default function Mint() {
 let [state, setState] = useState({
  checking: false,
  error: "",
  hash: "",
 });

 const submit = async (event: FormEvent<HTMLFormElement>) => {
  event.preventDefault();
  setState({ ...state, error: "", checking: true, hash: "" });
  const formData = new FormData(event.currentTarget);

  var formValues = {
   index: BigInt(formData.get("contractIndex")?.toString() || "-1"),
   subindex: BigInt(formData.get("contractSubindex")?.toString() || "-1"),
   metadataUrl: formData.get("metadataUrl")?.toString() || "",
   tokenId: formData.get("tokenId")?.toString() || "",
   quantity: parseInt(formData.get("quantity")?.toString() || "-1"),
  };

  if (!(formValues.index >= 0)) {
   setState({ ...state, error: "Invalid Contract Index" });
   return;
  }

  if (!(formValues.subindex >= 0)) {
   setState({ ...state, error: "Invalid Contract Subindex" });
   return;
  }

  if (!(formValues.quantity >= 0)) {
   setState({ ...state, error: "Invalid Quantity" });
   return;
  }

  if (!formValues.metadataUrl) {
   setState({ ...state, error: "Invalid Metadata Url" });
   return;
  }

  if (!formValues.tokenId) {
   setState({ ...state, error: "Invalid Token Id" });
   return;
  }

  const provider = await detectConcordiumProvider();
  const account = await provider.connect();

  if (!account) {
   alert("Please connect");
  }

  const address = { index: formValues.index, subindex: formValues.subindex };
  const paramJson = {
   owner: {
    Account: [account],
   },
   tokens: [
    [
     formValues.tokenId,
     [
      {
       url: formValues.metadataUrl,
       hash: "",
      },
      formValues.quantity.toString(),
     ],
    ],
   ],
  };

        var REACT_APP_CONTRACT_NAME="CIS2-Multi";
        var REACT_APP_CONTRACT_SCHEMA="FFFF02010000000A000000434953322D4D756C7469000A0000000900000062616C616E63654F6606100114000200000008000000746F6B656E5F69641D0007000000616464726573731502000000070000004163636F756E7401010000000B08000000436F6E747261637401010000000C10011B2500000015040000000E000000496E76616C6964546F6B656E49640211000000496E73756666696369656E7446756E6473020C000000556E617574686F72697A65640206000000437573746F6D010100000015060000000B0000005061727365506172616D7302070000004C6F6746756C6C020C0000004C6F674D616C666F726D65640213000000496E76616C6964436F6E74726163744E616D65020C000000436F6E74726163744F6E6C790213000000496E766F6B65436F6E74726163744572726F7202040000006D696E7404140002000000050000006F776E65721502000000070000004163636F756E7401010000000B08000000436F6E747261637401010000000C06000000746F6B656E7312021D000F1400020000000300000075726C1601040000006861736816011B2500000015040000000E000000496E76616C6964546F6B656E49640211000000496E73756666696369656E7446756E6473020C000000556E617574686F72697A65640206000000437573746F6D010100000015060000000B0000005061727365506172616D7302070000004C6F6746756C6C020C0000004C6F674D616C666F726D65640213000000496E76616C6964436F6E74726163744E616D65020C000000436F6E74726163744F6E6C790213000000496E766F6B65436F6E74726163744572726F72020F0000006F6E526563656976696E67434953320315040000000E000000496E76616C6964546F6B656E49640211000000496E73756666696369656E7446756E6473020C000000556E617574686F72697A65640206000000437573746F6D010100000015060000000B0000005061727365506172616D7302070000004C6F6746756C6C020C0000004C6F674D616C666F726D65640213000000496E76616C6964436F6E74726163744E616D65020C000000436F6E74726163744F6E6C790213000000496E766F6B65436F6E74726163744572726F72020A0000006F70657261746F724F66061001140002000000050000006F776E65721502000000070000004163636F756E7401010000000B08000000436F6E747261637401010000000C07000000616464726573731502000000070000004163636F756E7401010000000B08000000436F6E747261637401010000000C10010115040000000E000000496E76616C6964546F6B656E49640211000000496E73756666696369656E7446756E6473020C000000556E617574686F72697A65640206000000437573746F6D010100000015060000000B0000005061727365506172616D7302070000004C6F6746756C6C020C0000004C6F674D616C666F726D65640213000000496E76616C6964436F6E74726163744E616D65020C000000436F6E74726163744F6E6C790213000000496E766F6B65436F6E74726163744572726F72020F000000736574496D706C656D656E746F72730414000200000002000000696416000C000000696D706C656D656E746F727310020C15040000000E000000496E76616C6964546F6B656E49640211000000496E73756666696369656E7446756E6473020C000000556E617574686F72697A65640206000000437573746F6D010100000015060000000B0000005061727365506172616D7302070000004C6F6746756C6C020C0000004C6F674D616C666F726D65640213000000496E76616C6964436F6E74726163744E616D65020C000000436F6E74726163744F6E6C790213000000496E766F6B65436F6E74726163744572726F720208000000737570706F727473061001160010011503000000090000004E6F537570706F72740207000000537570706F72740209000000537570706F72744279010100000010000C15040000000E000000496E76616C6964546F6B656E49640211000000496E73756666696369656E7446756E6473020C000000556E617574686F72697A65640206000000437573746F6D010100000015060000000B0000005061727365506172616D7302070000004C6F6746756C6C020C0000004C6F674D616C666F726D65640213000000496E76616C6964436F6E74726163744E616D65020C000000436F6E74726163744F6E6C790213000000496E766F6B65436F6E74726163744572726F72020D000000746F6B656E4D657461646174610610011D0010011400020000000300000075726C160104000000686173681502000000040000004E6F6E650204000000536F6D65010100000013200000000215040000000E000000496E76616C6964546F6B656E49640211000000496E73756666696369656E7446756E6473020C000000556E617574686F72697A65640206000000437573746F6D010100000015060000000B0000005061727365506172616D7302070000004C6F6746756C6C020C0000004C6F674D616C666F726D65640213000000496E76616C6964436F6E74726163744E616D65020C000000436F6E74726163744F6E6C790213000000496E766F6B65436F6E74726163744572726F7202080000007472616E7366657204100114000500000008000000746F6B656E5F69641D0006000000616D6F756E741B250000000400000066726F6D1502000000070000004163636F756E7401010000000B08000000436F6E747261637401010000000C02000000746F1502000000070000004163636F756E7401010000000B08000000436F6E747261637401020000000C160104000000646174611D0115040000000E000000496E76616C6964546F6B656E49640211000000496E73756666696369656E7446756E6473020C000000556E617574686F72697A65640206000000437573746F6D010100000015060000000B0000005061727365506172616D7302070000004C6F6746756C6C020C0000004C6F674D616C666F726D65640213000000496E76616C6964436F6E74726163744E616D65020C000000436F6E74726163744F6E6C790213000000496E766F6B65436F6E74726163744572726F72020E0000007570646174654F70657261746F720410011400020000000600000075706461746515020000000600000052656D6F7665020300000041646402080000006F70657261746F721502000000070000004163636F756E7401010000000B08000000436F6E747261637401010000000C15040000000E000000496E76616C6964546F6B656E49640211000000496E73756666696369656E7446756E6473020C000000556E617574686F72697A65640206000000437573746F6D010100000015060000000B0000005061727365506172616D7302070000004C6F6746756C6C020C0000004C6F674D616C666F726D65640213000000496E76616C6964436F6E74726163744E616D65020C000000436F6E74726163744F6E6C790213000000496E766F6B65436F6E74726163744572726F720204000000766965770114000200000005000000737461746510020F1502000000070000004163636F756E7401010000000B08000000436F6E747261637401010000000C1400020000000800000062616C616E63657310020F1D001B25000000090000006F70657261746F727310021502000000070000004163636F756E7401010000000B08000000436F6E747261637401010000000C06000000746F6B656E7310021D00";

  try {
   const schemaBuffer = Buffer.from(
    REACT_APP_CONTRACT_SCHEMA!,
    "hex"
   );
   const serializedParams = serializeUpdateContractParameters(
    REACT_APP_CONTRACT_NAME!,
    "mint",
    paramJson,
    schemaBuffer
   );
   const txnHash = await provider.sendTransaction(
    account!,
    AccountTransactionType.Update as any,
    {
     address,
     message: serializedParams,
     receiveName: `${REACT_APP_CONTRACT_NAME!}.mint`,
     amount: new CcdAmount(BigInt(0)),
     maxContractExecutionEnergy: BigInt(9999),
    } as UpdateContractPayload,
    paramJson as SmartContractParameters,
    schemaBuffer.toString("base64")
   );

   setState({ checking: false, error: "", hash: txnHash });
  } catch (error: any) {
   setState({ checking: false, error: error.message || error, hash: "" });
  }
 };

 return (
  <Stack
   component={"form"}
   spacing={2}
   onSubmit={submit}
   autoComplete={"true"}
  >
   <TextField
    id="contract-index"
    name="contractIndex"
    label="Contract Index"
    variant="standard"
    type={"number"}
    disabled={state.checking}
   />
   <TextField
    id="contract-subindex"
    name="contractSubindex"
    label="Contract Sub Index"
    variant="standard"
    type={"number"}
    disabled={state.checking}
    value={0}
   />
   <TextField
    id="metadata-url"
    name="metadataUrl"
    label="Metadata Url"
    variant="standard"
    disabled={state.checking}
   />
   <TextField
    id="token-id"
    name="tokenId"
    label="Token Id"
    variant="standard"
    disabled={state.checking}
    defaultValue="01"
   />
   <TextField
    id="quantity"
    name="quantity"
    label="Token Quantity"
    variant="standard"
    type="number"
    disabled={state.checking}
    defaultValue="1"
   />
   {state.error && (
    <Typography component="div" color="error">
     {state.error}
    </Typography>
   )}
   {state.checking && <Typography component="div">Checking..</Typography>}
   {state.hash && (
    <Link
     href={`https://dashboard.testnet.concordium.com/lookup/${state.hash}`}
     target="_blank"
    >
     View Transaction <br />
     {state.hash}
    </Link>
   )}
   <Button
    type="submit"
    variant="contained"
    fullWidth
    size="large"
    disabled={state.checking}
   >
    Mint
   </Button>
  </Stack>
 );
}

Note the two parts in the code:

  • Connection with the wallet.

  • The transaction parameters.

  1. Add a check for a successful wallet connection. Then, create parameters from form data for your minting function: address, paramJson, schemaBuffer, and serializedParams. Use these when invoking mint() with provider.sendTransaction():

const provider = await detectConcordiumProvider();
  const account = await provider.connect();

  if (!account) {
   alert("Please connect");
  }

  const address = { index: formValues.index, subindex: formValues.subindex };
  const paramJson = {
   owner: {
    Account: [account],
   },
   tokens: [
    [
     formValues.tokenId,
     [
      {
       url: formValues.metadataUrl,
       hash: "",
      },
      formValues.quantity.toString(),
     ],
    ],
   ],
  };

  var REACT_APP_CONTRACT_NAME="CIS2-Multi";
  var REACT_APP_CONTRACT_SCHEMA="FFFF02010000000A000000434953322D4D756C7469000A0000000900000062616C616E63654F6606100114000200000008000000746F6B656E5F69641D0007000000616464726573731502000000070000004163636F756E7401010000000B08000000436F6E747261637401010000000C10011B2500000015040000000E000000496E76616C6964546F6B656E49640211000000496E73756666696369656E7446756E6473020C000000556E617574686F72697A65640206000000437573746F6D010100000015060000000B0000005061727365506172616D7302070000004C6F6746756C6C020C0000004C6F674D616C666F726D65640213000000496E76616C6964436F6E74726163744E616D65020C000000436F6E74726163744F6E6C790213000000496E766F6B65436F6E74726163744572726F7202040000006D696E7404140002000000050000006F776E65721502000000070000004163636F756E7401010000000B08000000436F6E747261637401010000000C06000000746F6B656E7312021D000F1400020000000300000075726C1601040000006861736816011B2500000015040000000E000000496E76616C6964546F6B656E49640211000000496E73756666696369656E7446756E6473020C000000556E617574686F72697A65640206000000437573746F6D010100000015060000000B0000005061727365506172616D7302070000004C6F6746756C6C020C0000004C6F674D616C666F726D65640213000000496E76616C6964436F6E74726163744E616D65020C000000436F6E74726163744F6E6C790213000000496E766F6B65436F6E74726163744572726F72020F0000006F6E526563656976696E67434953320315040000000E000000496E76616C6964546F6B656E49640211000000496E73756666696369656E7446756E6473020C000000556E617574686F72697A65640206000000437573746F6D010100000015060000000B0000005061727365506172616D7302070000004C6F6746756C6C020C0000004C6F674D616C666F726D65640213000000496E76616C6964436F6E74726163744E616D65020C000000436F6E74726163744F6E6C790213000000496E766F6B65436F6E74726163744572726F72020A0000006F70657261746F724F66061001140002000000050000006F776E65721502000000070000004163636F756E7401010000000B08000000436F6E747261637401010000000C07000000616464726573731502000000070000004163636F756E7401010000000B08000000436F6E747261637401010000000C10010115040000000E000000496E76616C6964546F6B656E49640211000000496E73756666696369656E7446756E6473020C000000556E617574686F72697A65640206000000437573746F6D010100000015060000000B0000005061727365506172616D7302070000004C6F6746756C6C020C0000004C6F674D616C666F726D65640213000000496E76616C6964436F6E74726163744E616D65020C000000436F6E74726163744F6E6C790213000000496E766F6B65436F6E74726163744572726F72020F000000736574496D706C656D656E746F72730414000200000002000000696416000C000000696D706C656D656E746F727310020C15040000000E000000496E76616C6964546F6B656E49640211000000496E73756666696369656E7446756E6473020C000000556E617574686F72697A65640206000000437573746F6D010100000015060000000B0000005061727365506172616D7302070000004C6F6746756C6C020C0000004C6F674D616C666F726D65640213000000496E76616C6964436F6E74726163744E616D65020C000000436F6E74726163744F6E6C790213000000496E766F6B65436F6E74726163744572726F720208000000737570706F727473061001160010011503000000090000004E6F537570706F72740207000000537570706F72740209000000537570706F72744279010100000010000C15040000000E000000496E76616C6964546F6B656E49640211000000496E73756666696369656E7446756E6473020C000000556E617574686F72697A65640206000000437573746F6D010100000015060000000B0000005061727365506172616D7302070000004C6F6746756C6C020C0000004C6F674D616C666F726D65640213000000496E76616C6964436F6E74726163744E616D65020C000000436F6E74726163744F6E6C790213000000496E766F6B65436F6E74726163744572726F72020D000000746F6B656E4D657461646174610610011D0010011400020000000300000075726C160104000000686173681502000000040000004E6F6E650204000000536F6D65010100000013200000000215040000000E000000496E76616C6964546F6B656E49640211000000496E73756666696369656E7446756E6473020C000000556E617574686F72697A65640206000000437573746F6D010100000015060000000B0000005061727365506172616D7302070000004C6F6746756C6C020C0000004C6F674D616C666F726D65640213000000496E76616C6964436F6E74726163744E616D65020C000000436F6E74726163744F6E6C790213000000496E766F6B65436F6E74726163744572726F7202080000007472616E7366657204100114000500000008000000746F6B656E5F69641D0006000000616D6F756E741B250000000400000066726F6D1502000000070000004163636F756E7401010000000B08000000436F6E747261637401010000000C02000000746F1502000000070000004163636F756E7401010000000B08000000436F6E747261637401020000000C160104000000646174611D0115040000000E000000496E76616C6964546F6B656E49640211000000496E73756666696369656E7446756E6473020C000000556E617574686F72697A65640206000000437573746F6D010100000015060000000B0000005061727365506172616D7302070000004C6F6746756C6C020C0000004C6F674D616C666F726D65640213000000496E76616C6964436F6E74726163744E616D65020C000000436F6E74726163744F6E6C790213000000496E766F6B65436F6E74726163744572726F72020E0000007570646174654F70657261746F720410011400020000000600000075706461746515020000000600000052656D6F7665020300000041646402080000006F70657261746F721502000000070000004163636F756E7401010000000B08000000436F6E747261637401010000000C15040000000E000000496E76616C6964546F6B656E49640211000000496E73756666696369656E7446756E6473020C000000556E617574686F72697A65640206000000437573746F6D010100000015060000000B0000005061727365506172616D7302070000004C6F6746756C6C020C0000004C6F674D616C666F726D65640213000000496E76616C6964436F6E74726163744E616D65020C000000436F6E74726163744F6E6C790213000000496E766F6B65436F6E74726163744572726F720204000000766965770114000200000005000000737461746510020F1502000000070000004163636F756E7401010000000B08000000436F6E747261637401010000000C1400020000000800000062616C616E63657310020F1D001B25000000090000006F70657261746F727310021502000000070000004163636F756E7401010000000B08000000436F6E747261637401010000000C06000000746F6B656E7310021D00";

  try {
   const schemaBuffer = Buffer.from(
    REACT_APP_CONTRACT_SCHEMA!,
    "hex"
   );
   const serializedParams = serializeUpdateContractParameters(
    REACT_APP_CONTRACT_NAME!,
    "mint",
    paramJson,
    schemaBuffer
   );
   const txnHash = await provider.sendTransaction(
    account!,
    AccountTransactionType.Update as any,
    {
     address,
     message: serializedParams,
     receiveName: `${REACT_APP_CONTRACT_NAME!}.mint`,
     amount: new CcdAmount(BigInt(0)),
     maxContractExecutionEnergy: BigInt(9999),
    } as UpdateContractPayload,
    paramJson,
    schemaBuffer.toString("base64")
   );
  1. Add your Mint component to the App.tsx:

<Container sx={{ mt: 15 }}>
   {isConnected && <Initialize/>}
   {isConnected && <Mint/>}
   </Container>
  1. Run your application and view it in your browser:

  1. Try the application by initializing your contract and filling out the form with the required information.

If you require assistance with Metadata, refer to this tutorial.

Last updated