Part 2) How to create a Minting UI for Collection Deployed with Metaplex (Sugar 2.1.1 or greater) using Umi

Introduction

This is for the developer who has deployed, or wants to deploy, an NFT or Programmable NFT collection to Solana with the Metaplex Candy Machine v3 – Account v2, using sugar 2.1.1 (or greater). As of this writing, the Metaplex documentation says you should use umi, but does not have a guide out yet.

This post is Part 2 of a 3-Part series for cmv3. Part 1 is steps to take to deploy a collection of pNFTs with a rule set on devnet, covering a couple more steps than the Metaplex guide. Part 3 is about my experience deploying to production.

Motivation

You may wonder why you want to continue with sugar 2.1.1+. Should you withdraw your candy machine and deploy a new one with sugar 2.0, which has a minting guide and other community-made minting UIs out there as well as a well-documented JS SDK? You could downgrade, but with later sugar, you are able to mint Programmable NFTs (pNFTs) which offer protections for creator’s royalties. Metaplex provided an upgrade for NFT collections, but that window has closed. I wanted to be at the latest and greatest release and not go backward and at least have the option to decide about pNFTs or NFTs.

Because the royalties for Porcupine Playground Pals go to our donor-advised fund for causes and my kid artist, I wanted to use the latest Metaplex tools to make sure that protections could be set. The documentation had not quite caught up to the implementation, though, so I pulled from multiple sources and frequented Discord to build our mint page. I wanted to pay it forward for other developers so you can find a complete solution in one place.

If you are not interested in protecting creator royalties and want to continue with the NFT token standard, sugar 2.1.1 still allows you to mint NFTs, so you can still use this guide. Afterall, Metaplex recommends that you use the latest version, and as of the original writing that was it, but releases come out fast.

7/6/2023 Update - sugar is already at v2.4.1

Objective

Upon completion of this tutorial, you’ll have a Next.js React Minting UI dApp that

  • checks how many items have been minted so you can determine if there are any items left
  • checks if there is enough SOL in the connected wallet using the solPayment guard
  • mints a pNFT (or NFT)

You should have enough skills and know which documentation to turn to in order to implement other default candy guards upon completion. This app will be minimal in look and may not have the most efficient or best coding practices in place and is intended as a starting point.

Getting Started

Prerequisites

  • basic knowledge of using a terminal
  • some web development knowledge with JavaScript, preferably TypeScript
  • already deployed your Candy Machine v3 Account v2 using sugar 2.1.1 and know your candy machine ID
  • added the solPayment Candy Guard to your cmv3
  • have Node.js (v18.x) installed

Create Project

We will create a Next.js project, which is a React framework. In your terminal, run:

npx create-next-app@latest
Screenshot of terminal running npx create-next-app@latest
Screenshot of terminal running npx create-next-app@latest

Add Metaplex and Solana Dependencies

I like to use yarn. If you do, too, remove package-lock.json in favor of yarn.lock that gets generated. In the directory of your newly created project, install dependencies.

cd {name-of-project}
yarn add @metaplex-foundation/umi \
  @metaplex-foundation/umi-bundle-defaults \
  @metaplex-foundation/umi-signer-wallet-adapters \
  @metaplex-foundation/mpl-candy-machine@alpha \
  @metaplex-foundation/mpl-token-metadata@alpha \
  @solana/web3.js \
  @solana/wallet-adapter-base \
  @solana/wallet-adapter-react \
  @solana/wallet-adapter-react-ui \
  @solana/wallet-adapter-wallets

At the time of this writing, you need the @alpha versions of mpl-candy-machine and mpl-token-metadata.

Screenshot of terminal change directory and add dependences, output is truncated
Screenshot of terminal change directory and add dependences, output is truncated

Run

In the directory of your new project, run the app to make sure it’s working. If you like to use Visual Studio Code, open your project and start a new terminal from within VSC.

yarn dev
Screenshot of terminal running yarn dev
Screenshot of terminal running yarn dev

Open a browser and view your project at http://localhost:3000, or if something is already running on port 3000, try 3001, etc.

Screenshot of default UI
Screenshot of default UI

Environment Variables

Add a .env file to the root of your project to hold environment variables for your RPC URL, the network aka cluster, and your candy machine ID. This will make it easy to switch from devnet to production. For Next.js projects, prefix your environment variables with NEXT_PUBLIC_.

NEXT_PUBLIC_NETWORK=devnet
NEXT_PUBLIC_RPC_URL=api.devnet.solana.com
NEXT_PUBLIC_CANDY_MACHINE_ID={your candy machine ID}
Screenshot of .env file
7/6/2023 Update - I was seeing a 403 Forbidden when using the metaplex RPC so replaced it with solana's devnet RPC

Customize

In layout.tsx, update the metadata title and description.

Screenshot of layout.tsx
Screenshot of layout.tsx

Replace the favicon.ico in src/app, and add a preview.gif to your public folder.

Page.tsx

Open the page.tsx file. This is where the rest of the coding will go. Replace the contents between the tags <main className={styles.main}></main> with a header and image. {styles.main} can be found in page.module.css and is a flex column layout, so we’ll just stack everything in a column.

      <h1>Mint Porcupine Playground Pals</h1>
      <Image
        className={styles.logo}
        src="/preview.gif"
        alt="Preview of NFTs"
        width={300}
        height={300}
        priority
      />
Screenshot of header and preview image code
Screenshot of header and preview image code

When you save page.tsx, you’ll see the changes reflected immediately in the browser without having to refresh.

Screenshot of header and preview image
Screenshot of header and preview image

Web3 Code

Imports

Add 'use client'; and the following imports to the top of page.tsx.

'use client';

import dynamic from 'next/dynamic';
import { useEffect, useMemo, useState } from "react";

import { WalletAdapterNetwork } from "@solana/wallet-adapter-base";
import { WalletProvider, useWallet } from "@solana/wallet-adapter-react";
import { WalletModalProvider } from "@solana/wallet-adapter-react-ui";
import { LedgerWalletAdapter, SolflareWalletAdapter} from "@solana/wallet-adapter-wallets";
import "@solana/wallet-adapter-react-ui/styles.css";

import { createUmi } from "@metaplex-foundation/umi-bundle-defaults";
import { base58PublicKey,
  generateSigner,
  Option,
  PublicKey,
  publicKey,
  SolAmount,
  some,
  transactionBuilder,
  Umi,
  unwrapSome
} from "@metaplex-foundation/umi";
import { walletAdapterIdentity } from "@metaplex-foundation/umi-signer-wallet-adapters";
import { setComputeUnitLimit } from '@metaplex-foundation/mpl-essentials';
import { mplTokenMetadata, fetchDigitalAsset, TokenStandard } from "@metaplex-foundation/mpl-token-metadata";
import { mplCandyMachine, 
  fetchCandyMachine, 
  mintV2, 
  safeFetchCandyGuard, 
  DefaultGuardSetMintArgs, 
  DefaultGuardSet, 
  SolPayment, 
  CandyMachine, 
  CandyGuard } from "@metaplex-foundation/mpl-candy-machine";

We’ll use all those imports eventually.

6/26/2023 Update - mpl-toolbox replaces mpl-essentials, so change the import to import { setComputeUnitLimit } from '@metaplex-foundation/mpl-toolbox';

Connect Wallet Button

Add code to display the wallet adapters based on your environment variables within export default function Home() { } before return.

  const network = process.env.NEXT_PUBLIC_NETWORK === 'devnet' ? WalletAdapterNetwork.Devnet :
    process.env.NEXT_PUBLIC_NETWORK === 'testnet' ? WalletAdapterNetwork.Testnet :
    WalletAdapterNetwork.Mainnet;

  const endpoint = `https://${process.env.NEXT_PUBLIC_RPC_URL}`;

  const wallets = useMemo(
    () => [
      new LedgerWalletAdapter(),
      new SolflareWalletAdapter({ network }),
    ],
    [network]
  );

  const WalletMultiButtonDynamic = dynamic(
    async () =>
      (await import("@solana/wallet-adapter-react-ui")).WalletMultiButton,
    { ssr: false }
  );

Add the <WalletMultiButtonDynamic/> and wallet providers to provide context to the return. Place it where you like. Our sample has the Connect Wallet above the header.

<WalletProvider wallets={wallets} autoConnect>
  <WalletModalProvider>
    <WalletMultiButtonDynamic/>
  </WalletModalProvider>
</WalletProvider>
    
Screenshot of wallet adapter code
Screenshot of wallet adapter code

The page.module.css can be reduced down to just .main for now and can be modified to display the column more compactly.

.main {
  display: flex;
  flex-direction: column;
  justify-content: flex-start;
  gap: 1em;
  align-items: center;
  padding: 6rem;
  min-height: 100vh;
}

Check your browser and test that you are able to connect a Solana wallet.

Screenshot of UI with Select Wallet button
Screenshot of Select Wallet button

Create Umi

Create umi from your RPC Url and chain uses. Because umi will be updated within the wallet context, we’re using let here instead of const.

let umi = createUmi(endpoint)
            .use(mplTokenMetadata())
            .use(mplCandyMachine());
6/26/2023 Update - if you create an UmiContext and wrap your WalletProvider around UmiProvider, you could directly call

  const wallet = useWallet();
  const umi = createUmi(endpoint)
    .use(walletAdapterIdentity(wallet))
    .use(mplTokenMetadata())
    .use(mplCandyMachine()); 

Add useState Variables

To help with display, we add useState variables. You could use context or other methods, but for simplicity’s sake, we’re going to set the candy machine and candy guard as state variables.

  // state
  const [loading, setLoading] = useState(false);
  const [mintCreated, setMintCreated] = useState<PublicKey | null>(null);
  const [mintMsg, setMintMsg] = useState<string>();
  const [costInSol, setCostInSol] = useState<number>(0);
  const [cmv3v2, setCandyMachine] = useState<CandyMachine>();
  const [defaultCandyGuardSet, setDefaultCandyGuardSet] = useState<CandyGuard<DefaultGuardSet>>();
  const [countTotal, setCountTotal] = useState<number>();
  const [countRemaining, setCountRemaining] = useState<number>();
  const [countMinted, setCountMinted] = useState<number>();
  const [mintDisabled, setMintDisabled] = useState<boolean>(true);

Retrieve Item Counts and Cost

Fetch the candy machine to determine if there are any more items to mint and the cost. I used Umi helpers from the Umi Docs to help me figure out how to get the cost in lamports from the candy guard. The Managing Candy Machine docs demonstrate how to fetch the Candy Machine.

  // retrieve item counts to determine availability and
  // from the solPayment, display cost on the Mint button
  const retrieveAvailability = async () => {
    const cmId = process.env.NEXT_PUBLIC_CANDY_MACHINE_ID;
    if (!cmId) {
      setMintMsg("No candy machine ID found. Add environment variable.");
      return;
    }
    const candyMachine: CandyMachine = await fetchCandyMachine(umi, publicKey(cmId));
    setCandyMachine(candyMachine);

    // Get counts
    setCountTotal(candyMachine.itemsLoaded);
    setCountMinted(Number(candyMachine.itemsRedeemed));
    const remaining = candyMachine.itemsLoaded - Number(candyMachine.itemsRedeemed)
    setCountRemaining(remaining);
    
    // Get cost
    const candyGuard = await safeFetchCandyGuard(umi, candyMachine.mintAuthority);
    if (candyGuard) {
      setDefaultCandyGuardSet(candyGuard);
    }
    const defaultGuards: DefaultGuardSet | undefined = candyGuard?.guards;
    const solPaymentGuard: Option<SolPayment> | undefined = defaultGuards?.solPayment;

    if (solPaymentGuard) {
      const solPayment: SolPayment | null  = unwrapSome(solPaymentGuard);
      if (solPayment) {
        const lamports: SolAmount = solPayment.lamports;
        const solCost = Number(lamports.basisPoints) / 1000000000;
        setCostInSol(solCost);
      }
    }

    if (remaining > 0) {
      setMintDisabled(false);
    }
  };

  useEffect(() => {
    retrieveAvailability();
  }, [mintCreated]);

Add the counts to the return within <main></main>.

          <div className={styles.countsContainer}>
            <div>Minted: {countMinted} / {countTotal}</div>
            <div>Remaining: {countRemaining}</div>
          </div>

Style this how you like in page.module.css. We’ll just use flex again, this time in a row.

.countsContainer {
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: 2em;
}

mintV2 Sample Code

Add a component to handle minting and checking the wallet balance. We’ll make it an inner component to take advantage of not having to pass around the state variables. I used Connecting with an RPC of the Umi docs to know how to check the wallet balance and Mark Sackerberg’s ui-create-nft project as a starting point for using the transactionBuilder to mintV2. It also demonstrated how to set up umi to use the identity of the connected wallet. From the Metaplex SolPayment docs, I knew the solPayment guard required mintArgs. So you can refer to the guard docs for other default guards.

  // Inner Mint component to handle showing the Mint button,
  // and mint messages
  const Mint = () => {
    const wallet = useWallet();
    umi = umi.use(walletAdapterIdentity(wallet));

    // check wallet balance
    const checkWalletBalance = async () => {
      const balance: SolAmount = await umi.rpc.getBalance(umi.identity.publicKey);
      if (Number(balance.basisPoints) / 1000000000 < costInSol) {
        setMintMsg("Add more SOL to your wallet.");
        setMintDisabled(true);
      } else {
        if (countRemaining !== undefined && countRemaining > 0) {
          setMintDisabled(false);
        }
      }
    };
  
    if (!wallet.connected) {
      return <p>Please connect your wallet.</p>;
    } 

    checkWalletBalance();

    const mintBtnHandler = async () => {
  
      if (!cmv3v2 || !defaultCandyGuardSet) {
        setMintMsg("There was an error fetching the candy machine. Try refreshing your browser window.");
        return;
      }
      setLoading(true);
      setMintMsg(undefined);
  
      try {
        const candyMachine = cmv3v2;
        const candyGuard = defaultCandyGuardSet;
  
        const nftSigner = generateSigner(umi);
        
        const mintArgs: Partial<DefaultGuardSetMintArgs> = {};
  
        // solPayment has mintArgs
        const defaultGuards: DefaultGuardSet | undefined = candyGuard?.guards;
        const solPaymentGuard: Option<SolPayment> | undefined = defaultGuards?.solPayment;
        if (solPaymentGuard) {
          const solPayment: SolPayment | null  = unwrapSome(solPaymentGuard);
          if (solPayment) {
            const treasury = solPayment.destination;
  
            mintArgs.solPayment = some({
              destination: treasury
            });
          }
        }
        
        const tx = transactionBuilder()
          .add(setComputeUnitLimit(umi, { units: 600_000 }))
          .add(mintV2(umi, {
            candyMachine: candyMachine.publicKey,
            collectionMint: candyMachine.collectionMint, 
            collectionUpdateAuthority: candyMachine.authority, 
            nftMint: nftSigner,
            candyGuard: candyGuard?.publicKey,
            mintArgs: mintArgs,
            tokenStandard: TokenStandard.ProgrammableNonFungible
          }))
  
  
        const { signature } = await tx.sendAndConfirm(umi, {
          confirm: { commitment: "finalized" }, send: {
            skipPreflight: true,
          },
        });
  
        const nft = await fetchDigitalAsset(umi, nftSigner.publicKey);
        setMintCreated(nftSigner.publicKey);
        setMintMsg("Mint was successful!");
        
  
      } catch (err: any) {
        console.error(err);
        setMintMsg(err.message);
      } finally {
        setLoading(false);
      }
    };
  
    if (mintCreated) {
      return (
        <a className={styles.success} target="_blank" rel="noreferrer"
          href={`https://solscan.io/token/${base58PublicKey(mintCreated)}${network === 'devnet' ? '?cluster=devnet' : ''}`}>
          <Image className={styles.logo} src="/nftHolder.png" alt="Blank NFT" width={300} height={300} priority/>
          <p className="mintAddress">
            <code>{base58PublicKey(mintCreated)}</code>
          </p>
        </a>
      );
    }
    return (
      <>          
        <button onClick={mintBtnHandler} className={styles.mintBtn} disabled={mintDisabled || loading}>
          MINT<br/>({costInSol} SOL)
        </button>
        {loading && (<div className={styles.loadingDots}>. . .</div>)}
      </> 
    );
  }; // </Mint>

If the mint is successful, we’ll display a placeholder image, nftHolder.png that you can customize, and provide a link to the NFT on solscan.io.

Add the <Mint/> component and an area to display mint messages within <main></main>.

          <Mint/>
          {mintMsg && (
            <div className={styles.mintMsg}>
              <button className={styles.mintMsgClose} onClick={() => {setMintMsg(undefined);}}>&times;</button>
              <span>{mintMsg}</span>
            </div>)} 

Add Supporting CSS

.countsContainer {
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: 2em;
}

.mintBtn {
  height: 4em;
  width: 300px;
  font-size: 16px;
  font-weight: 600;
  padding: 0 24px;
  background-color: yellowgreen;
  cursor: pointer;
}

.mintBtn:not(:disabled):hover {
  background-color: #aef643;
}

.mintMsg {
  padding: 2em;
  border: 1px dotted;
}

.mintMsgClose {
  float: right;
  border-radius: 10px;
  padding: 2px 5px;
  border: none;
  background-color: lightgray;
  cursor: pointer;
  margin-left: 1em;
}

.loadingDots {
  font-weight: bold;
  line-height: 1px;
  font-size: 40px;
  animation: blink 1s steps(5, start) infinite;
}

@keyframes blink {
  to {
    visibility: hidden;
  }
}

.success {
  text-align: center;
  border-radius: 5px;
  border: 1px solid;
  padding: 1em;
}

.success:hover {
  box-shadow: 0px 0px 15px 0px rgba(0,0,0,0.75);
}

What Minting Looks Like

This is what the flow looks like when you run.

Counts and instructions are displayed
Counts and instructions are displayed
Wallet type selection
Wallet type selection
Unlock wallet
Unlock wallet
Insufficient SOL
Cost displayed and if wallet has insufficient SOL a message is displayed
Mint button enabled screenshot
Mint button with cost in SOL is enabled if sufficient funds
Approval screenshot
Transaction must be approved after Mint button is clicked
Loading screenshot
Loading indicator blinks (this screenshot caught at blink)
Mint successful screenshot
Successful mint message and link to view NFT on solscan.io

The complete starter project is on github.

The devnet demo is on Netlify.

Acknowledgements

I wrote this guide as a starting point for developers because I really could have used one when I created the minting dApp for Porcupine Playground Pals. I could not have done it without the Metaplex Discord community, especially TonyBoyle.sol, Mark Sackerberg, and Loris.

Next Steps

When you are done thoroughly testing in devnet, visit Part 3 for my notes on deploying to mainnet-beta.