Front-end Development

This section will walk you through implementing the front-end interaction with the cis2-multi smart contract and implementing the contract client functionality.

Step 1: Create a React App

  1. Use the npx create-react-app command to create a new React application with a TypeScript template:

npx create-react-app mint-ui --template typescript

After you run the command, you will have the following structure in your code editor:

  1. Run the application to view the template application interface:

Step 2: Install Dependencies

Add necessary dependencies, including:

  • @mui/material

  • @emotion/react

  • @mui/icons-material

  • @emotion/styled

  • @concordium/web-sdk

  • @concordium/browser-wallet-api-helpers

Run the following command to install the dependencies:

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

After running the command, it will create a package.json file that includes all dependencies in it:

Step 3: Create Header Component:

  • Create a folder named "components" and a file "Header.tsx" containing a HeaderButton component. Then, implement UI and functionality for wallet connection using the following:

import { Button } from "@mui/material";

export default function HeaderButton(props: {
 name: string;
 isSelected: boolean;
 onClick: () => void;
}) {
 return (
  <Button
   variant={props.isSelected ? "outlined" : "contained"}
   key={props.name}
   onClick={() => props.onClick()}
   sx={{
    my: 2,
    color: "white",
    display: "block",
    borderColor: "white",
    borderRadius: "4px",
    ":hover": {
     my: 2,
     color: "white",
     display: "block",
     borderColor: "white",
     borderRadius: "4px",
    },
   }}
  >
   {props.name}
  </Button>
 );
}

Step 4: Update the App Component

Update your App Component to be able to connect your wallet:

  • Open up the App.tsx file and delete the content inside App().

  • Add the helpers from the concordium-browser-wallet-api-helpers.

  • Add the connect() function.

Use the following code:

import React from 'react';
import logo from './logo.svg';
import './App.css';
import { useEffect, useState } from "react";
import {
 detectConcordiumProvider,
 WalletApi,
} from "@concordium/browser-wallet-api-helpers";


import HeaderButton from "./components/Header";
import { AppBar } from '@mui/material';

function App() {
 const [state, setState] = useState<{
  provider?: WalletApi;
  account?: string;
 }>({});

 function connect() {
  detectConcordiumProvider()
   .then((provider) => {
    provider
     .getMostRecentlySelectedAccount()
     .then((account) =>
      !!account ? Promise.resolve(account) : provider.connect()
     )
     .then((account) => {
      setState({ ...state, provider, account });
     })
     .catch((_) => {
      alert("Please allow wallet connection");
     });
    provider.on("accountDisconnected", () => {
     setState({ ...state, account: undefined });
    });
    provider.on("accountChanged", (account) => {
     setState({ ...state, account });
    });
    provider.on("chainChanged", () => {
     setState({ ...state, account: undefined, provider: undefined });
    });
   })
   .catch((_) => {
    console.error(`could not find provider`);
    alert("Please download Concordium Wallet");
   });
 }


  useEffect(() => {
  if (!state.provider || !state.account) {
   connect();
  }

  return () => {
   state.provider?.removeAllListeners();
  };
 }, [state.account]);

 function isConnected() {
  return !!state.provider && !!state.account;
 }

 const isConnectedVar = isConnected();

return (
<AppBar>
<HeaderButton
        name={isConnectedVar ? "Connected" : "Connect"}
        isSelected={isConnectedVar}
        onClick={connect}
       />
</AppBar>);

}

export default App;

Run the application to view the "CONNECTED" button:

Step 5: Beautify UI

Let’s add some material-ui components to beautify it. Do the following:

  • Add material-ui components to enhance the UI appearance.

  • Update the return function in App.tsx with enhanced UI components.

Use the following code:

import {
 AppBar,
 Box,
 Container,
 Link,
 Paper,
 Toolbar,
 Typography,
} from "@mui/material";

/// .....
///
/// ..... 

return (
  <AppBar position="static">
    <Container maxWidth="xl" sx={{ height: "100%" }}>
      <Toolbar disableGutters>
        <Typography
          variant="h6"
          noWrap
          component="a"
          sx={{
            mr: 2,
            display: "flex",
            fontFamily: "monospace",
            fontWeight: 700,
            letterSpacing: ".3rem",
            color: "inherit",
            textDecoration: "none",
          }}
        >
          Concordium
        </Typography>
        <Box
          sx={{
            flexGrow: 1,
            display: "flex",
            flexDirection: "row-reverse",
          }}
        >
          <HeaderButton
            name={isConnectedVar ? "Connected" : "Connect"}
            isSelected={isConnectedVar}
            onClick={connect}
          />
        </Box>
      </Toolbar>
    </Container>
  </AppBar>
);

Step 6: Contract Client Development Steps

It's essential to understand that interacting with a smart contract deployed on a blockchain requires using Software Development Kits (SDKs). For your React project, you'll utilize the Concordium web-SDK, which provides reusable code for integrating smart contract functionality into our application, with potential adjustments depending on SDK updates.

Use the following steps:

  1. Create Client Helper Functions:

    • Create a file named "CIS2ContractHelper.ts" in a folder named "modules" to implement helper functions for interacting with the smart contract.

    • Implement functions like waitForTransaction, ensureValidOutcome, serializeParams, etc.

    Add the following code:

import { WalletApi } from "@concordium/browser-wallet-api-helpers";
import { Buffer } from "buffer/";

import {
    ContractAddress,
    AccountTransactionType,
    UpdateContractPayload,
    serializeUpdateContractParameters,
    ModuleReference,
    InitContractPayload,
    TransactionStatusEnum,
    TransactionSummary,
    CcdAmount,
} from "@concordium/web-sdk";


/**
 * Waits for the input transaction to Finalize.
 * @param provider Wallet Provider.
 * @param txnHash Hash of Transaction.
 * @returns Transaction outcomes.
 */
export function waitForTransaction(
    provider: WalletApi,
    txnHash: string
): Promise<Record<string, TransactionSummary> | undefined> {
    return new Promise((res, rej) => {
        _wait(provider, txnHash, res, rej);
    });
}

export function ensureValidOutcome(
    outcomes?: Record<string, TransactionSummary>
): Record<string, TransactionSummary> {
    if (!outcomes) {
        throw Error("Null Outcome");
    }

    let successTxnSummary = Object.keys(outcomes)
        .map((k) => outcomes[k])
        .find((s) => s.result.outcome === "success");

    if (!successTxnSummary) {
        let failures = Object.keys(outcomes)
            .map((k) => outcomes[k])
            .filter((s) => s.result.outcome === "reject")
            .map((s) => (s.result as any).rejectReason.tag)
            .join(",");
        throw Error(`Transaction failed, reasons: ${failures}`);
    }

    return outcomes;
}


/**
 * Uses Contract Schema to serialize the contract parameters.
 * @param contractName Name of the Contract.
 * @param schema  Buffer of Contract Schema.
 * @param methodName Contract method name.
 * @param params Contract Method params in JSON.
 * @returns Serialize buffer of the input params.
 */
export function serializeParams<T>(
    contractName: string,
    schema: Buffer,
    methodName: string,
    params: T
): Buffer {
    return serializeUpdateContractParameters(
        contractName,
        methodName,
        params,
        schema
    );
}


export function _wait(
    provider: WalletApi,
    txnHash: string,
    res: (p: Record<string, TransactionSummary> | undefined) => void,
    rej: (reason: any) => void
) {
    setTimeout(() => {
        provider
            .getJsonRpcClient()
            .getTransactionStatus(txnHash)
            .then((txnStatus) => {
                if (!txnStatus) {
                    return rej("Transaction Status is null");
                }

                console.info(`txn : ${txnHash}, status: ${txnStatus?.status}`);
                if (txnStatus?.status === TransactionStatusEnum.Finalized) {
                    return res(txnStatus.outcomes);
                }

                _wait(provider, txnHash, res, rej);
            })
            .catch((err) => rej(err));
    }, 1000);
}

export function parseContractAddress(
    outcomes: Record<string, TransactionSummary>
): ContractAddress {
    for (const blockHash in outcomes) {
        const res = outcomes[blockHash];

        if (res.result.outcome === "success") {
            for (const event of res.result.events) {
                if (event.tag === "ContractInitialized") {
                    return {
                        index: toBigInt((event as any).address.index),
                        subindex: toBigInt((event as any).address.subindex),
                    };
                }
            }
        }
    }

    throw Error(`unable to parse Contract Address from input outcomes`);
}

export function toBigInt(num: BigInt | number): bigint {
    return BigInt(num.toString(10));
}

const MICRO_CCD_IN_CCD = 1000000;
export function toCcd(ccdAmount: bigint): CcdAmount {
    return new CcdAmount(ccdAmount * BigInt(MICRO_CCD_IN_CCD));
} 
  1. Implement Contract Interaction Functions:

    • Create a file named CIS2ContractClient.ts in the modules folder to implement functions for initializing and updating the smart contract.

    • For the initContract() function:

      • Invoke the initialize function in the contract using the schema, moduleRef, and contractName.

        • Utilize AccountTransactionType and WalletApi from the browser-wallet-api-helpers to facilitate the transaction.

        • Send a transaction with the required parameters using the wallet provider.

        • Wait for the transaction to be finalized and parse the return output.

    • For the updateContract() function:

      • Use this function for operations where a state change is required, such as transferring an asset, minting a token, or burning it.

        • Utilize the schema, moduleRef, contractName, and methodName (entrypoint).

        • Call AccountTransaction’s Update enum to specify the type of transaction that will be signed.

import { WalletApi } from "@concordium/browser-wallet-api-helpers";
import { Buffer } from "buffer/";
import {
    waitForTransaction,
    ensureValidOutcome,
    serializeParams,
    _wait,
    parseContractAddress,
    toBigInt,
    toCcd
} from "./CIS2ContractHelpers";

import {
    ContractAddress,
    AccountTransactionType,
    UpdateContractPayload,
    serializeUpdateContractParameters,
    ModuleReference,
    InitContractPayload,
    TransactionStatusEnum,
    TransactionSummary,
    CcdAmount,
} from "@concordium/web-sdk";

export interface Cis2ContractInfo {
    schemaBuffer: Buffer;
    contractName: string;
    moduleRef: ModuleReference;
    tokenIdByteSize: number;
}

/**
 * Initializes a Smart Contract.
 * @param provider Wallet Provider.
 * @param moduleRef Contract Module Reference. Hash of the Deployed Contract Module.
 * @param schemaBuffer Buffer of Contract Schema.
 * @param contractName Name of the Contract.
 * @param account Account to Initialize the contract with.
 * @param maxContractExecutionEnergy Maximum energy allowed to execute.
 * @param ccdAmount CCD Amount to initialize the contract with.
 * @returns Contract Address.
 */

export async function initContract<T>(provider: WalletApi,
    contractInfo: Cis2ContractInfo,
    account: string,
    params?: T,
    serializedParams?: Buffer,
    maxContractExecutionEnergy = BigInt(999),
    ccdAmount = BigInt(0)): Promise<ContractAddress> {

    const { moduleRef, schemaBuffer, contractName } = contractInfo;
    let txnHash = await provider.sendTransaction(
        account,
        AccountTransactionType.InitContract,
        {
            amount: toCcd(ccdAmount),
            moduleRef,
            initName: contractName,
            param: serializedParams || Buffer.from([]),
            maxContractExecutionEnergy,
        } as InitContractPayload,
        params || {},
        schemaBuffer.toString("base64"),
        2 //schema version
    );
    let outcomes = await waitForTransaction(provider, txnHash);
    outcomes = ensureValidOutcome(outcomes);
    return parseContractAddress(outcomes);
}

/**
 * Updates a Smart Contract.
 * @param provider Wallet Provider.
 * @param contractName Name of the Contract.
 * @param schema Buffer of Contract Schema.
 * @param paramJson Parameters to call the Contract Method with.
 * @param account  Account to Update the contract with.
 * @param contractAddress Contract Address.
 * @param methodName Contract Method name to Call.
 * @param maxContractExecutionEnergy Maximum energy allowed to execute.
 * @param amount CCD Amount to update the contract with.
 * @returns Update contract Outcomes.
 */

export async function updateContract<T>(
    provider: WalletApi,
    contractInfo: Cis2ContractInfo,
    paramJson: T,
    account: string,
    contractAddress: { index: number; subindex: number },
    methodName: string,
    maxContractExecutionEnergy: bigint = BigInt(9999),
    amount: bigint = BigInt(0)
): Promise<Record<string, TransactionSummary>> {
    const { schemaBuffer, contractName } = contractInfo;
    const parameter = serializeParams(
        contractName,
        schemaBuffer,
        methodName,
        paramJson
    );
    let txnHash = await provider.sendTransaction(
        account,
        AccountTransactionType.Update,
        {
            maxContractExecutionEnergy,
            address: {
                index: BigInt(contractAddress.index),
                subindex: BigInt(contractAddress.subindex),
            },
            message: parameter,
            amount: toCcd(amount),
            receiveName: `${contractName}.${methodName}`,
        } as UpdateContractPayload,
        paramJson as any,
        schemaBuffer.toString("base64"),
        2 //Schema Version
    );

    let outcomes = await waitForTransaction(provider, txnHash);

    return ensureValidOutcome(outcomes);
}

Step 7: Init Component Development Steps

  1. Create Init Component:

    • Create a file named Init.tsx in the components folder to handle smart contract instance creation.

    • Implement UI and functionality for initializing the smart contract.

    • Collect necessary parameters like verify key from the user.

import { FormEvent, useState } from "react";
import { WalletApi } from '@concordium/browser-wallet-api-helpers';
// import { ContractAddress } from "@concordium/common-sdk";

import { Cis2ContractInfo } from "../models/CIS2ContractClient";
import { ContractAddress, serializeInitContractParameters } from '@concordium/web-sdk';
import * as connClient from '../models/CIS2ContractClient';

import { Typography, Button, Stack, Container } from '@mui/material';

export default function Cis2Init(props: {
    provider: WalletApi;
    account: string;
    contractInfo: Cis2ContractInfo;
    verifyKey: string;
    onDone: (address: ContractAddress, contractInfo: Cis2ContractInfo) => void;
}) {
    const [state, setState] = useState({
        error: '',
        processing: false,
    });

    function submit(event: FormEvent<HTMLFormElement>) {
        event.preventDefault();
        const initParams = {
            verify_key: props.verifyKey
        };
        const serializedParams = serializeInitContractParameters(props.contractInfo.contractName, initParams, props.contractInfo.schemaBuffer);
        setState({ ...state, processing: true });
        connClient
            .initContract(props.provider, props.contractInfo, props.account, initParams, serializedParams)
            .then((address) => {
                setState({ ...state, processing: false });
                props.onDone(address, props.contractInfo);
            })
            .catch((err: Error) => {
                setState({ ...state, processing: false, error: err.message });
            });
    }

    return (
        <Container sx={{ maxWidth: 'xl', pt: '10px' }}>
            <Stack component={'form'} spacing={2} onSubmit={submit}>
                {state.error && (
                    <Typography component="div" color="error" variant="body1">
                        {state.error}
                    </Typography>
                )}
                {state.processing && (
                    <Typography component="div" variant="body1">
                        Deploying..
                    </Typography>
                )}
                <Button variant="contained" disabled={state.processing} type="submit">
                    Deploy New
                </Button>
            </Stack>
        </Container>
    );
}

Step 8: Getting the Signature Steps:

  1. Create Verifier Backend Client:

    • Implement API request functions in a file named VerifierBackendClient.ts in the modules folder to communicate with the verifier backend.

    • Implement functions like getChallenge, getStatement, and getSignature.

import { IdStatement, IdProofOutput } from '@concordium/web-sdk';

/**
 * Fetch a challenge from the backend
 */
export async function getChallenge(verifier: string, accountAddress: string) {
    const response = await fetch(`${verifier}/challenge?address=` + accountAddress, { method: 'get' });
    const body = await response.json();
    return body.challenge;
}

/**
 * Fetch the statement to prove from the backend
 */
export async function getStatement(verifier: string): Promise<IdStatement> {
    const response = await fetch(`${verifier}/statement`, { method: 'get' });
    const body = await response.json();
    return JSON.parse(body);
}

/**
 *  Authorize with the backend, and get a auth token.
 */
export async function getSignature(verifier: string, challenge: string, proof: IdProofOutput) {
    const response = await fetch(`${verifier}/prove`, {
        method: 'post',
        headers: new Headers({ 'content-type': 'application/json' }),
        body: JSON.stringify({ challenge, proof }),
    });
    if (!response.ok) {
        throw new Error('Unable to authorize');
    }
    const body = await response.text();
    if (body) {
        return body;
    }
    throw new Error('Unable to authorize');
}
  1. Implement Get Signature Button Component:

  • Create a file named VerifierGetSignature.tsx in the components folder to handle signature retrieval from the verifier.

  • Implement UI and functionality for getting the signature from the verifier.

import { WalletApi } from '@concordium/browser-wallet-api-helpers';
import { Button } from '@mui/material';
import { getChallenge, getSignature, getStatement } from '../models/VerifierBackendClient';

export default function VerifierGetSignature(props: {
    provider: WalletApi;
    account: string;
    verifierUrl: string;
    disabled: boolean;
    onSign: (signature: string) => void;
}) {
    async function sign(e: React.MouseEvent) {
        e.preventDefault();
        var challenge = await getChallenge(props.verifierUrl, props.account);
        const statement = await getStatement(props.verifierUrl);
        const proof = await props.provider.requestIdProof(props.account, statement, challenge);
        const signature = await getSignature(props.verifierUrl, challenge, proof);
        props.onSign(signature.replaceAll('"', ''));
    }

    return (
        <Button type="button" variant="contained" disabled={props.disabled} fullWidth size="large" onClick={sign}>
            Get Signature
        </Button>
    );
}

Step 9: Mint Component Development Steps:

  1. Create Mint Component:

    • Create a file named Mint.tsx in the components folder to handle minting functionality.

    • Implement UI and functionality for minting tokens.

    • Collect necessary parameters like metadata URL, amount of tokens, signature, etc., from the user.

import { Buffer } from 'buffer/';
import { FormEvent, useState } from 'react';
import { WalletApi } from '@concordium/browser-wallet-api-helpers';
import { Typography, Button, Stack, TextField } from '@mui/material';
import { Container } from '@mui/system';
import { TransactionSummary, ContractAddress } from '@concordium/web-sdk';

import * as connClient from '../models/CIS2ContractClient';
import { Cis2ContractInfo } from '../models/CIS2ContractClient';

async function mint(
    provider: WalletApi,
    account: string,
    tokens: { [tokenId: string]: [{ url: string; hash: string }, string] },
    signature: string,
    nftContractAddress: { index: number; subindex: number },
    contractInfo: Cis2ContractInfo,
    maxContractExecutionEnergy = BigInt(9999)
): Promise<Record<string, TransactionSummary>> {
    const paramJson = {
        owner: {
            Account: [account],
        },
        tokens: Object.keys(tokens).map((tokenId) => [tokenId, tokens[tokenId]]),
        signature,
    };

    return connClient.updateContract(
        provider,
        contractInfo,
        paramJson,
        account,
        nftContractAddress,
        'mint',
        maxContractExecutionEnergy,
        BigInt(0)
    );
}  
  1. Implement Mint Logic:

  • Implement minting logic using the mint function and contract client functionalities.

  • Validate user inputs and handle minting process accordingly.

function MintPage(props: {
    verifierUrl: string;
    provider: WalletApi;
    account: string;
    contractInfo: Cis2ContractInfo;
    contract?: ContractAddress;
}) {
    let [state, setState] = useState({
        checking: false,
        error: '',
    });
    const [signature, setSignature] = useState('');

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

        var formValues = {
            index: parseInt(formData.get('contractIndex')?.toString() || '-1'),
            subindex: parseInt(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 (!isValidTokenId(formValues.tokenId, props.contractInfo)) {
            setState({ ...state, error: 'Invalid Token Id' });
            return;
        }

        if (!signature) {
            setState({ ...state, error: 'Invalid Signature' });
            return;
        }

        const address = { index: formValues.index, subindex: formValues.subindex };
        mint(
            props.provider,
            props.account,
            {
                [formValues.tokenId]: [{ url: formValues.metadataUrl, hash: '' }, formValues.quantity.toString()],
            },
            signature,
            address,
            props.contractInfo
        )
            .then((_) => {
                setState({ ...state, error: '', checking: false });
                alert('Minted');
            })
            .catch((err: Error) => setState({ ...state, error: err.message, checking: false }));
    }

    return (
        <Container sx={{ maxWidth: 'xl', pt: '10px' }}>
            <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"
                />
                <TextField
                    id="signature"
                    name="signature"
                    label="Signature"
                    variant="standard"
                    disabled
                    defaultValue=""
                    value={signature}
                />
                <VerifierGetSignature
                    provider={props.provider}
                    account={props.account}
                    verifierUrl={props.verifierUrl}
                    disabled={state.checking}
                    onSign={setSignature}
                />
                {state.error && (
                    <Typography component="div" color="error">
                        {state.error}
                    </Typography>
                )}
                {state.checking && <Typography component="div">Checking..</Typography>}
                <Button type="submit" variant="contained" disabled={state.checking} fullWidth size="large">
                    Mint
                </Button>
            </Stack>
        </Container>
    );
}

export default MintPage;

function isValidTokenId(tokenIdHex: string, contractInfo: Cis2ContractInfo): boolean {
    try {
        let buff = Buffer.from(tokenIdHex, 'hex');
        let parsedTokenIdHex = Buffer.from(buff.subarray(0, contractInfo.tokenIdByteSize)).toString('hex');
        console.log(tokenIdHex, parsedTokenIdHex);
        return parsedTokenIdHex === tokenIdHex;
    } catch (error) {
        console.error(error);
        return false;
    }
}

Step 10: Constants File Setup:

  1. Create Constants File:

    • Create a file named Constants.ts to store constant values like schema, moduleRef, contractInfo, verify_key, and verifier_url.

  2. Define Constant Values:

    • Define constant values for schema, moduleRef, contractInfo, verify_key, and verifier_url in the Constants.ts file.

Use the following code:

import { Buffer } from "buffer/";
import { ModuleReference } from "@concordium/web-sdk";
import {
    Cis2ContractInfo,
} from "./models/CIS2ContractClient";

const MULTI_CONTRACT_MODULE_REF =
    "27b813fa34babde7a7e337f05a9cc031f81db60f8b88b0200c234e8b48cb7fa3";
const MULTI_CONTRACT_SCHEMA =
    "FFFF02010000000A000000434953322D4D756C746901001400010000000A0000007665726966795F6B65791E200000000A0000000900000062616C616E63654F6606100114000200000008000000746F6B656E5F69641D0007000000616464726573731502000000070000004163636F756E7401010000000B08000000436F6E747261637401010000000C10011B2500000015040000000E000000496E76616C6964546F6B656E49640211000000496E73756666696369656E7446756E6473020C000000556E617574686F72697A65640206000000437573746F6D010100000015080000000B0000005061727365506172616D7302070000004C6F6746756C6C020C0000004C6F674D616C666F726D65640213000000496E76616C6964436F6E74726163744E616D65020C000000436F6E74726163744F6E6C79020B0000004163636F756E744F6E6C790213000000496E766F6B65436F6E74726163744572726F720212000000546F6B656E416C72656164794D696E74656402040000006D696E7404140003000000050000006F776E65721502000000070000004163636F756E7401010000000B08000000436F6E747261637401010000000C06000000746F6B656E7312021D000F1400020000000300000075726C1601040000006861736816011B25000000090000007369676E61747572651E4000000015040000000E000000496E76616C6964546F6B656E49640211000000496E73756666696369656E7446756E6473020C000000556E617574686F72697A65640206000000437573746F6D010100000015080000000B0000005061727365506172616D7302070000004C6F6746756C6C020C0000004C6F674D616C666F726D65640213000000496E76616C6964436F6E74726163744E616D65020C000000436F6E74726163744F6E6C79020B0000004163636F756E744F6E6C790213000000496E766F6B65436F6E74726163744572726F720212000000546F6B656E416C72656164794D696E746564020F0000006F6E526563656976696E67434953320315040000000E000000496E76616C6964546F6B656E49640211000000496E73756666696369656E7446756E6473020C000000556E617574686F72697A65640206000000437573746F6D010100000015080000000B0000005061727365506172616D7302070000004C6F6746756C6C020C0000004C6F674D616C666F726D65640213000000496E76616C6964436F6E74726163744E616D65020C000000436F6E74726163744F6E6C79020B0000004163636F756E744F6E6C790213000000496E766F6B65436F6E74726163744572726F720212000000546F6B656E416C72656164794D696E746564020A0000006F70657261746F724F66061001140002000000050000006F776E65721502000000070000004163636F756E7401010000000B08000000436F6E747261637401010000000C07000000616464726573731502000000070000004163636F756E7401010000000B08000000436F6E747261637401010000000C10010115040000000E000000496E76616C6964546F6B656E49640211000000496E73756666696369656E7446756E6473020C000000556E617574686F72697A65640206000000437573746F6D010100000015080000000B0000005061727365506172616D7302070000004C6F6746756C6C020C0000004C6F674D616C666F726D65640213000000496E76616C6964436F6E74726163744E616D65020C000000436F6E74726163744F6E6C79020B0000004163636F756E744F6E6C790213000000496E766F6B65436F6E74726163744572726F720212000000546F6B656E416C72656164794D696E746564020F000000736574496D706C656D656E746F72730414000200000002000000696416000C000000696D706C656D656E746F727310020C15040000000E000000496E76616C6964546F6B656E49640211000000496E73756666696369656E7446756E6473020C000000556E617574686F72697A65640206000000437573746F6D010100000015080000000B0000005061727365506172616D7302070000004C6F6746756C6C020C0000004C6F674D616C666F726D65640213000000496E76616C6964436F6E74726163744E616D65020C000000436F6E74726163744F6E6C79020B0000004163636F756E744F6E6C790213000000496E766F6B65436F6E74726163744572726F720212000000546F6B656E416C72656164794D696E7465640208000000737570706F727473061001160010011503000000090000004E6F537570706F72740207000000537570706F72740209000000537570706F72744279010100000010000C15040000000E000000496E76616C6964546F6B656E49640211000000496E73756666696369656E7446756E6473020C000000556E617574686F72697A65640206000000437573746F6D010100000015080000000B0000005061727365506172616D7302070000004C6F6746756C6C020C0000004C6F674D616C666F726D65640213000000496E76616C6964436F6E74726163744E616D65020C000000436F6E74726163744F6E6C79020B0000004163636F756E744F6E6C790213000000496E766F6B65436F6E74726163744572726F720212000000546F6B656E416C72656164794D696E746564020D000000746F6B656E4D657461646174610610011D0010011400020000000300000075726C160104000000686173681502000000040000004E6F6E650204000000536F6D65010100000013200000000215040000000E000000496E76616C6964546F6B656E49640211000000496E73756666696369656E7446756E6473020C000000556E617574686F72697A65640206000000437573746F6D010100000015080000000B0000005061727365506172616D7302070000004C6F6746756C6C020C0000004C6F674D616C666F726D65640213000000496E76616C6964436F6E74726163744E616D65020C000000436F6E74726163744F6E6C79020B0000004163636F756E744F6E6C790213000000496E766F6B65436F6E74726163744572726F720212000000546F6B656E416C72656164794D696E74656402080000007472616E7366657204100114000500000008000000746F6B656E5F69641D0006000000616D6F756E741B250000000400000066726F6D1502000000070000004163636F756E7401010000000B08000000436F6E747261637401010000000C02000000746F1502000000070000004163636F756E7401010000000B08000000436F6E747261637401020000000C160104000000646174611D0115040000000E000000496E76616C6964546F6B656E49640211000000496E73756666696369656E7446756E6473020C000000556E617574686F72697A65640206000000437573746F6D010100000015080000000B0000005061727365506172616D7302070000004C6F6746756C6C020C0000004C6F674D616C666F726D65640213000000496E76616C6964436F6E74726163744E616D65020C000000436F6E74726163744F6E6C79020B0000004163636F756E744F6E6C790213000000496E766F6B65436F6E74726163744572726F720212000000546F6B656E416C72656164794D696E746564020E0000007570646174654F70657261746F720410011400020000000600000075706461746515020000000600000052656D6F7665020300000041646402080000006F70657261746F721502000000070000004163636F756E7401010000000B08000000436F6E747261637401010000000C15040000000E000000496E76616C6964546F6B656E49640211000000496E73756666696369656E7446756E6473020C000000556E617574686F72697A65640206000000437573746F6D010100000015080000000B0000005061727365506172616D7302070000004C6F6746756C6C020C0000004C6F674D616C666F726D65640213000000496E76616C6964436F6E74726163744E616D65020C000000436F6E74726163744F6E6C79020B0000004163636F756E744F6E6C790213000000496E766F6B65436F6E74726163744572726F720212000000546F6B656E416C72656164794D696E7465640204000000766965770114000200000005000000737461746510020F1502000000070000004163636F756E7401010000000B08000000436F6E747261637401010000000C1400020000000800000062616C616E63657310020F1D001B25000000090000006F70657261746F727310021502000000070000004163636F756E7401010000000B08000000436F6E747261637401010000000C06000000746F6B656E7310021D00";
export const CIS2_MULTI_CONTRACT_INFO: Cis2ContractInfo = {
    contractName: "CIS2-Multi",
    moduleRef: new ModuleReference(MULTI_CONTRACT_MODULE_REF),
    schemaBuffer: Buffer.from(MULTI_CONTRACT_SCHEMA, "hex"),
    tokenIdByteSize: 1,
};
export const VERIFY_KEY = "ccc7b73e381125ccc7dbd82f2ccef80c2877ae2eacbd57c536a67a767e94395c"
export const VERIFIER_URL = 'http://localhost:8100/api';]

Step 11: App Component Integration:

  1. Integrate Components in App Component:

    • Update the App.tsx file to integrate the created components including Header, Init, VerifierGetSignature, and Mint components.

    • Use the defined constant values and contract client functionalities to interact with the smart contract.

Use the following code:

import React from 'react';
import logo from './logo.svg';
import './App.css';
import { useEffect, useState } from "react";
import {
 detectConcordiumProvider,
 WalletApi,
} from "@concordium/browser-wallet-api-helpers";

import {
 AppBar,
 Box,
 Container,
 Link,
 Paper,
 Toolbar,
 Typography,
} from "@mui/material";

import MintPage from "./components/Mint";
import { CIS2_MULTI_CONTRACT_INFO, VERIFIER_URL, VERIFY_KEY } from "./Constants";
import HeaderButton from "./components/Header";
import Cis2Init from "./components/Init";

function App() {
 const [state, setState] = useState<{
  provider?: WalletApi;
  account?: string;
 }>({});

 function connect() {
  detectConcordiumProvider()
   .then((provider) => {
    provider
     .getMostRecentlySelectedAccount()
     .then((account) =>
      !!account ? Promise.resolve(account) : provider.connect()
     )
     .then((account) => {
      setState({ ...state, provider, account });
     })
     .catch((_) => {
      alert("Please allow wallet connection");
     });
    provider.on("accountDisconnected", () => {
     setState({ ...state, account: undefined });
    });
    provider.on("accountChanged", (account) => {
     setState({ ...state, account });
    });
    provider.on("chainChanged", () => {
     setState({ ...state, account: undefined, provider: undefined });
    });
   })
   .catch((_) => {
    console.error(`could not find provider`);
    alert("Please download Concordium Wallet");
   });
 }

 useEffect(() => {
  if (!state.provider || !state.account) {
   connect();
  }

  return () => {
   state.provider?.removeAllListeners();
  };
 }, [state.account]);

 function isConnected() {
  return !!state.provider && !!state.account;
 }

 const isConnectedVar = isConnected();

 return (
  <>
   <AppBar position="static">
    <Container maxWidth="xl" sx={{ height: "100%" }}>
     <Toolbar disableGutters>
      <Typography
       variant="h6"
       noWrap
       component="a"
       sx={{
        mr: 2,
        display: "flex",
        fontFamily: "monospace",
        fontWeight: 700,
        letterSpacing: ".3rem",
        color: "inherit",
        textDecoration: "none",
       }}
      >
       Concordium
      </Typography>
      <Box
       sx={{
        flexGrow: 1,
        display: "flex",
        flexDirection: "row-reverse",
       }}
      >
       <HeaderButton
        name={isConnectedVar ? "Connected" : "Connect"}
        isSelected={isConnectedVar}
        onClick={connect}
       />
      </Box>
     </Toolbar>
    </Container>
   </AppBar>
   <Box className="App">
    <Paper>