âī¸ Simple Minting dAppThis 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:
Connect to Web Wallet: Allow users to connect their dApp to the Concordium web wallet.
Initialize Contract: Initialize the smart contract responsible for NFT minting.
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
Create a new directory for your dApp.
Inside the directory, initialize a new React project using the TypeScript template:
Copy yarn create react-app < YOUR-DAPP-NAM E > --template typescript
Running this command will fetch and install all the packages and dependencies and then create your project.
Open the project in your code editor:
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:
Copy 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:
Create a file named "Header.tsx" " containing a button and handle the connect()
to the web wallet.
Add the following code to Header.tsx
:
Copy 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 >
);
}
Call the "Header.tsx" component from "App.tsx" instead of using its default values. Then, run the application:
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
Create a file named "Initialize.tsx" to invoke the init()
function in your deployed CIS-2 NFT contract.
Add the following code to "Initialize.tsx":
Copy 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 >
);
}
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:
Copy 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 >
);
}
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
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.
Add the following code to Mint.tsx
:
Copy 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.
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()
:
Copy 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" )
);
Add your Mint component to the App.tsx:
Copy < Container sx = {{ mt : 15 }} >
{isConnected && < Initialize />}
{isConnected && < Mint />}
</ Container >
Run your application and view it in your browser:
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 .