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
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
.
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
Open a browser and view your project at http://localhost:3000, or if something is already running on port 3000, try 3001, etc.
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}
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.
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 />
When you save page.tsx, you’ll see the changes reflected immediately in the browser without having to refresh.
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>
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.
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 yourWalletProvider
aroundUmiProvider
, you could directly callconst 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);}}>×</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.
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.