# swissDAO Simple cNFT Build ###### tags: `swissDAO`, `Solana`, `cNFT`, `Helius` ## :memo: What are we building? Welcome builder, glad you could make it 😃. My name is [Matt](https://twitter.com/_matt_xyz) and I'm here to help you add some cool new tools to your Solana toolbox. Today what we are building is an app that can generate a "digital business card" live in your browser, mint it as a compressed NFT (cNFT), and airdrop it to any Wallet or SNS (.sol) you want. Check out the live demo here: [https://solana-biz-cards-ruddy.vercel.app/](https://solana-biz-cards-ruddy.vercel.app/) We will be building a *simplified* version of a cNFT minter using Helius' API. This will allow us to mint/airdrop cNFTs without worrying about the technical side of things like creating a merkle tree and managing it. **But have no fear**, if you would like a tutorial that dives into the technical side of cNFTs and how to create the whole process from scratch. **Then you are in luck**, swissDAO also has a tutorial for that here -> ([SolanaPay/cNFT Build](https://github.com/swissDAO/solana-pay-cnfts)) where we not only teach you how to create your own collection, but also combine it with Solana Pay for practical use. :::info :pushpin: Have any questions? Reach out to us. ➜ [swissDAO Telegram](https://t.me/+8kAfO-simRkxY2Jh) ::: ### What are our objectives? - [ ] Generate a digital business card as a SVG in your browser - [ ] Convert that SVG to a PNG and upload it to Irys (formerly Bundlr) - [ ] Mint the PNG as a cNFT using Helius - [ ] Display all of the business card cNFTs in a gallery - [ ] Deploy our site Live to the world and allow people to mint business cards ==**without connecting their wallet**== Once all of this is said and done, you will have a solid grasp on cNFTs and how to **quickly** generate them. ### Other possible use-cases - Minting a scorecard on-chain when a user completes a game - Dynamic Proof of Attendance (POAP), so each cNFT can contain user details of the Original Attendee - Personalized Receipts/Coupons The possibilities are endless because you can customize not only the attributes, but the displayed image of the cNFT as well! ### Pre-Requisites We will be using TypeScript and Next.JS to build our app and Vercel to deploy. With that being said, if you don't know TypeScript, but are familiar with JavaScript it's ok. TypeScript is just a *suped* up version that helps catch/prevent errors before you run into them in the browser. If you are unfamiliar with Next.JS, again no worries, just follow along. And I'll explain later during deployment on why I prefer to use Vercel for this build. ***Spoiler alert*** it has a `temp` directory, amongst other strengths. Let's try it out! Apply different styling to this paragraph: **HackMD gets everyone on the same page with Markdown.** ==Real-time collaborate on any documentation in markdown.== Capture fleeting ideas and formalize tribal knowledge. Now, let's get started! ## 💻 Grabbing the Essentials There are a few things we will set up first before we start hacking away. ### Item #1 **Fork the repo.** This will contain all of the starter code that we need to begin building. **Make sure you uncheck** `Copy the main branch only` because we will be begin building from the `starter` branch. ![Screenshot 2023-11-06 at 10.48.32 AM.png](https://hackmd.io/_uploads/Sk0OLE87a.png) Once you have the repo forked, you can clone it down to your local machine. Make sure you switch to the `starter` branch by running the command: ``` git checkout -b starter ``` Your current file structure should look like this: ![Screenshot 2023-11-06 at 10.53.59 AM.png](https://hackmd.io/_uploads/S14awNLXp.png) From here you will need to open up your `example.env` and add a few items. If you do not have a Solana Browser Wallet or Helius API key, then follow the next 2 items. If you do then skip over it and add the values. ### Item #2 **Solana Browser Wallet.** My go to for this is Phantom Wallet, but you can use whatever you prefer. We will be exporting the private key (base58 version needed), so I suggest creating/using a **burner** wallet so you don't accidently upload anything sensitive to GitHub and get drained 😓. [Phantom Wallet](https://phantom.app/) ### Item #3 **Helius API Key/RPC.** As mentioned before, we will be using a simplified version of minting our cNFTs and to do so we'll be using Helius so head on over, connect your wallet and get the goods. No need to sign up for any plan, the free **Hacker Plan** will be enough for this build. [Helius Portal](https://www.helius.dev/) **Once you have all 3 items above it's time to start hacking!** ## ⌨️ Let's Get Started! Once you have changed all of the values in your `example.env` make sure you rename the file to just `.env`. Now you have everything you need to get started so let's open up our terminal and from the root of your repo let's run: ``` npm install ``` Once that is complete go ahead and run the following command to make sure all of the dependencies installed correctly. ``` npm run dev ``` Navigate to `http://localhost:3000/` and you should see the following: ![Screenshot 2023-11-06 at 11.28.18 AM.png](https://hackmd.io/_uploads/B1X0JSUQT.png) Cool, now let's implement our form next. ### Form Data Let's switch back to our code and navigate to `/app/page.tsx`. To handle the collection of the data we will be sending to the back-end, we will be using React's `useForm`, a simple way to create and batch form data. Because we are building business cards, we are going to collect the standard info that is listed on a business card: - First/Last Name - Job Title - Company - Email - Phone Number - Website And because we will be offering variations that include less "private info" we will also include: - Twitter Handle - Telegram Handle All of the info above will be displayed on the image of the cNFT and stored as attributes as well. Lastly, we will want to include both the: - Air Drop to Address - Creator Address The Air Drop to address will tell Helius where to send the cNFT while the Creator Address will provide a reference to your wallet and also store it as an attribute. Ok, now that we have the fields we know we want, let's begin to implement them. First thing we need to do is install the `react-hook-form` package. In your terminal run: ``` npm install react-hook-form ``` At the top of our code let's now add the import: ``` import { useForm } from "react-hook-form"; ``` And now where we define the `Page` let's include the following: ``` /app/page.tsx const Page: React.FC = () => { const { register, handleSubmit } = useForm(); const [data, setData] = useState<string>(""); const [selectedSvg, setSelectedSvg] = useState<string>("svg-container"); ...rest of code... }; export default Page; ``` I've also included a `selectedSvg` state that will be used later, but for now this will help prevent any errors. Now, if you wanted to generate a "scorecard" instead of a "business card" you might utilize `useState` instead of `useForm` to capture the "score" and "level" of the game while auto-filling the `airDropTo` state with the connected wallet of the game. Again the possibilities are endless, so have fun with it! Next, let's create the form. Our app comes bootstrapped with Tailwind to make the styling simple and fast, but feel free to use a custom CSS if you prefer. Locate the `renderForm` function and let's finish building it out. ``` /app/page.tsx const renderForm = () => { return ( <form className="bg-white sm:max-w-sm shadow-md rounded px-8 pt-6 pb-8 mb-4 items-center justify-center" style={{ width: "60vw", justifyContent: "center", alignContent: "center", overflowX: "hidden", alignItems: "center", margin: "0 auto", }} onChange={handleSubmit((data) => setData(JSON.stringify(data)))} > <div className="mb-2 sm:max-w-sm sm:mx-auto"> <input className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight sm:max-w-sm sm:mx-auto" {...register("firstName")} placeholder="First name" /> <input className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight sm:max-w-sm sm:mx-auto" {...register("lastName")} placeholder="Last name" /> <input className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight sm:max-w-sm sm:mx-auto" {...register("jobTitle")} placeholder="Job title" /> {selectedSvg === "svg-container" || selectedSvg === "svg-container-style2" ? ( <> <input className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight sm:max-w-sm sm:mx-auto" {...register("email")} placeholder="Email" /> <input className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight sm:max-w-sm sm:mx-auto" {...register("phone")} placeholder="Phone" /> </> ) : ( <> <input className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight sm:max-w-sm sm:mx-auto" {...register("twitter")} placeholder="Twitter" /> <input className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight sm:max-w-sm sm:mx-auto" {...register("telegram")} placeholder="Telegram" /> </> )} <input className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight sm:max-w-sm sm:mx-auto" {...register("website")} placeholder="Website" /> <input className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight sm:max-w-sm sm:mx-auto" {...register("airdropTo")} placeholder="Airdrop To" /> <input className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight sm:max-w-sm sm:mx-auto" {...register("creatorAddress")} placeholder="Creator Address" /> </div> </form> ); }; ``` As you can see, we are using `onChange` to update the state of our data. You might have also noticed this piece as well: ``` { selectedSvg === "svg-container" || selectedSvg === "svg-container-style2" ? ( ... ):( ... ) } ``` This is a ternary operator that is telling our form what to display depending on which SVG template is selected. In this specific case we are telling our form to either display the `Phone Number` & `Email` fields or the `Twitter` & `Telegram` fields depending on which SVG template is selected. Now let's actually display our form. Head down to the bottom of the `page.tsx` where we have our `return()` and let's render the form below our logo. ``` /app/page.tsx ... const Page: React.FC = () => { ...rest of code... return ( <div style={{ display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", alignContent: "center", overflowX: "hidden", backgroundColor: "black", }} > <ToastContainer /> <Logo /> <div className="flex flex-col gap-4 justify-center"> {renderForm()} </div> </div> ); }; ``` Once we have `{renderForm()}` called in our `return()` let's run our code again and check it out in the browser. **Make sure to save your code first.** ``` npm run dev ``` In your browser, your page should look like this: ![Screenshot 2023-11-07 at 10.39.10 AM.png](https://hackmd.io/_uploads/H1gRHKv7p.png) Awesome job so far! Now let's display our SVGs so we can see a sample of what we are creating! ### SVG Samples If you scroll back to the top of our code you will see we import several sample SVGs from our `components` directory. ``` /app/page.tsx import Sample from "../components/sample"; import Sample_Style2 from "@/components/sample_style2"; import Sample_Style3 from "@/components/sample_style3"; import Sample_Style4 from "@/components/sample_style4"; ``` If you open up these SVGs you will see that the `Sample` expects a certain set of imports: ``` /components/sample.tsx const Sample = ({ firstName, lastName, jobTitle, phone, email, website, }: { firstName: string; lastName: string; jobTitle: string; phone: string; email: string; website: string; }) => ( ... ); export default Sample; ``` What we want to do here is render the imported `Sample` into our code with the expected inputs in order to display the Sample SVG properly. So let's head back to our `page.tsx` and below our `renderForm()` in the `return` let's include the following: ``` /app/page.tsx ...rest of code... return( ...rest of code... <div className="flex flex-col gap-4 justify-center align-center md:flex-col sm:flex-col xs:flex-col w-fit self-center"> <div className="flex flex-col gap-4 justify-center align-center cursor-pointer" style={{ border: selectedSvg === "svg-container" ? "1px solid yellow" : ""}} onClick={() => {setSelectedSvg("svg-container")}} > <span className="text-white font-bold pl-5"> Style 1 </span> <Sample firstName={data ? JSON.parse(data).firstName : ""} lastName={data ? JSON.parse(data).lastName : ""} jobTitle={data ? JSON.parse(data).jobTitle : ""} phone={data ? JSON.parse(data).phone : ""} email={data ? JSON.parse(data).email : ""} website={data ? JSON.parse(data).website : ""} /> </div> </div> ...rest of code... )}; export default Page; ``` One thing to note here is we are running a ternary operator that is telling the code "if there is data parse the JSON and for 'x' and display it or else display nothing". This is what helps our sample dynamically update when info is entered into the form. Now when you run `npm run dev` and fill out the form, you should see your Sample update! ![Screenshot 2023-11-06 at 1.02.38 PM.png](https://hackmd.io/_uploads/ry61LUUXp.png) You'll notice that when you click it, the yellow border appears. This happens because of the `onClick()` is setting this SVG to our `selectedSvg` state. Go ahead and follow the same format for `Sample_Style2`, `Sample_Style3`, and `Sample_Style4`. If you are unsure how to do so, no worries give it a shot and compare your code with the solution at the end of this section. For the `onClick()` make sure you structure it to `setSelectedSvg("svg-container-style2");` and so forth so we process the right SVG on the server side. Once you get the other samples included, save your code (at this point if you don't have auto-save turned on I sugesst you toggle it on, it makes life much easier!), and run `npm run dev`. You should now be seeing all 4 samples on page. ![Screenshot 2023-11-06 at 1.12.32 PM.png](https://hackmd.io/_uploads/B1lrOIUQ6.png) ![Screenshot 2023-11-06 at 1.13.05 PM.png](https://hackmd.io/_uploads/BJpIdI8X6.png) Nice job! Now let's implement one more thing before we work on the `convertAndSubmit()` function. At the top of our code you will see we also import `Svg`, `Svg_style2`, and so forth. Now you are probably wondering what the difference between these and the Samples are and the answer is: styling. In order to make the sizing and border better for display in our app, I seperated the Samples and actual SVGs submitted the the server. Other than that all the info is the same. So in your `return()` let's include the following: ``` /app/page.tsx ...rest of code... return( ...rest of code... {renderForm()} <Svg firstName={data ? JSON.parse(data).firstName : ""} lastName={data ? JSON.parse(data).lastName : ""} jobTitle={data ? JSON.parse(data).jobTitle : ""} phone={data ? JSON.parse(data).phone : ""} email={data ? JSON.parse(data).email : ""} website={data ? JSON.parse(data).website : ""} /> ...rest of code... ) } export default Page; ``` Follow the same structure for SVG_Style2-4, but don't forget the difference in 3 & 4 ;-). Now when you save and run the code you will see nothing changed, and that's because of this line in the parent `div` of the `svg`: ``` style={{ width: "660px", height: "660px", display: "none" }} ``` Here we are telling the browser not to display it. But you might be wondering, "So how does the app know what to send to the server for minting?" Wise question. Each SVG is still "rendered", per say, in the app it's just not being displayed. And remember how I said to structure the `onClick()` to `setSelectedSvg`? Well if you look at the code of each `svg` they all have id's that look like: ``` id="svg-container" ``` So even though the actual svg is not displayed, it's still present and we will structure our `convertAndSubmit()` function to grab the selected one with all of the data. Cool stuff. Let's proceed. ### Convert and Submit Now that we have the majority of our Front-End working, it's time to send our SVG to the server and actually make it a compressed NFT. To do so we are going to first serialize our SVG into a string and include it in the body of our API request along with the info we are including. Locate your `convertAndSubmit` function on your `page.tsx` and let's edit it to like this: ``` /app/page.tsx async function convertAndSubmit() { const image = document.getElementById(selectedSvg); const svg = new XMLSerializer().serializeToString(image!); const airdropTo = JSON.parse(data).airdropTo; const airdrop_publickey = await checkForSolanaDomain(airdropTo); console.log("airdrop_publickey", airdrop_publickey); console.log("minting business card"); const res = await fetch("/api/mint", { method: "POST", headers: { "Content-Type": "application/json", }, const res = await fetch("/api/mint", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ info: data, image: svg, }), }); } ``` What we are doing is pretty straightforward: - Grabbing the selectedSvg container - Converting it into a string - Checking for a Solana Domain name - Sending the info and image off to our `/api/mint` endpoint Now when you implement this code, you will receive an error because `checkForSolanaDomain` does not exist as a function. So let's fix that. Above your `convertAndSubmit` function let's create `checkForSolanaDomain`: ``` /app/page.tsx async function checkForSolanaDomain(address: string) { // if the airdropTo address has the last 4 characters of .sol then getPublicKeyFromSolDomain else return the airdropTo address if (address.slice(-4) === ".sol") { const solana_domain = address; const solana_domain_owner = await getPublicKeyFromSolDomain(solana_domain); return solana_domain_owner; } else { return address; } } ``` Here we are doing a quick check to see if the `airdropTo` contains `.sol` at the end, if it doesn't we simply return the address. If it does, we send it off to another function `getPublicKeyFromSolDomain()`. Solana domains are created using [Bonfida's naming service](https://docs.bonfida.org/collection/naming-service/an-introduction-to-the-solana-name-service), these allow people to transact with a name like Matt.sol instead of a long PublicKey address. So to give our app some flexibility we are including this as a feature to make airdropping the business cards easier. In order for this to work we will add another import and write the function about `checkForSolanaDomain()`. ``` /app/page.tsx import { getDomainKeySync, NameRegistryState } from "@bonfida/spl-name-service"; async function getPublicKeyFromSolDomain(domain: string): Promise<string> { const { pubkey } = await getDomainKeySync(domain); const owner = ( await NameRegistryState.retrieve(SOLANA_CONNECTION, pubkey) ).registry.owner.toBase58(); console.log(`The owner of SNS Domain: ${domain} is: `, owner); return owner; } ``` Basically if our app recognizes a SNS domain with `checkForSolanaDomain` we then cross-reference it with Bonfida's Name Registry to obtain the PublicKey associated with it and return it to our `convertAndSubmit` function. Once you have those 2 new functions and new import line included in your code, you should currently have no errors. If you do, then cross reference your code with the solution from this section [here](https://github.com/swissDAO/solana-business-card-cnfts/tree/form/svg). ## 👾 Minting a Compressed NFT Ok now that we have handled most of the Front-End work, it's time to make it interact with our Back-End where all the magic happens. ### Outline Real quick, let's outline our steps here: - Receive the data server-side - Write our SVG image to a the temporary directory - Convert that SVG image to a PNG using `sharp` and write that to our temporary directory - Calculate the price of the file upload to Irys then fund Irys - Upload the PNG image file to Irys and grab the returned URL - Mint cNFT using Helius' `mintCompressedNft` API endpoint with all of our data and new image URL - Return the assetId of our newly minted cNFT business card **** ### FAQ's A few questions you might have: **Why are we writing to a temporary directory?** If you just want to run your app locally, you could easily use the file system to write to any directory of your choice. **However**, since we are all about getting off local host and shipping our builds to deployment where it can be used by the masses, the file system can not actually write files to someone's machine. A workaround would be to upload the images to a database then provide that link, but that's more work for us and one more thing to manage. This is where the `/tmp` directory comes in to play. [Vercel](https://vercel.com/guides/how-can-i-use-files-in-serverless-functions) and Next.JS make it somewhat simple to use files in a serverless environment by writing to the `/tmp` directory. One thing to note is that the response time from making all of this happen might make Vercel ["timeout"](https://vercel.com/guides/what-can-i-do-about-vercel-serverless-functions-timing-out). If this happens, try upgrading to the free-trial of Vercel's pro version, this will allow your program more time to respond. **Why are we not using Bundlr?** If you are familiar with minting NFTs then you have probably heard of Bundlr, this was the go to place to store NFT images. Well, Bundlr has now changed to [Irys](https://irys.xyz/what-is-irys), the premise is the same, but connecting and uploading images is a little different. We'll walk through the basics of using it here in a second. **Do I need to know anything about Merkle Trees to create my cNFT?** No, this is why we are using Helius' `mintCompressedNft` API endpoint. They have create and manage the merkle trees to handle all the cNFTs minted from this API, thus is also why we assign the wallet address from your `.env` as the creator so we can then locate all of the created cNFTs from this app. **If you have anymore questions feel free to ping me on Twitter ([@_matt_xyz](https://twitter.com/_matt_xyz)) or in the [swissDAO Telegram Channel](https://t.me/+8kAfO-simRkxY2Jh)** **** ### 🎫 Let's get minting The first thing we are going to do is set up our basic server functions and establish a connection to Irys inside our `mint.tsx` API endpoint. If you are unfamiliar on how API routes work with Next.JS you can review the details [here](https://nextjs.org/docs/pages/building-your-application/routing/api-routes). ``` /pages/api/mint.tsx ...rest of code... async function post(req: NextApiRequest, res: NextApiResponse) { if (req.method === "POST") { try { const fileName = uuidv4(); const privateKeySecret = process.env.NEXT_PUBLIC_WALLET_PRIVATE_KEY; const url = process.env.NEXT_PUBLIC_RPC_URL!; const image = req.body.image; const info = JSON.parse(req.body.info); const { firstName, lastName, jobTitle, email, phone, twitter, telegram, website, airdropTo, creatorAddress, } = info; const getIrys = async () => { const url = "https://node1.irys.xyz"; const token = "solana"; const privateKey = privateKeySecret; const irys = new Irys({ url, // URL of the node you want to connect to token, // Token used for payment key: privateKey, //SOL private key in base58 format // config: { providerUrl: providerUrl }, // Optional provider URL, only required when using Devnet }); return irys; }; } catch (error) { console.log(error); return res.status(500).json({ status: "error" }); } } } export default async function GET(req: NextApiRequest, res: NextApiResponse) { if (req.method === "GET") { return res.status(405).json({ error: "Method not allowed" }); } else if (req.method === "POST") { return await post(req, res); } else { return res.status(405).json({ error: "Method not allowed" }); } } ``` Here we are setting up our our `POST` call, as you can see at the bottom, this API route will only be used as a `POST` method, any other type we will return "Method not allowed." To start it off we are assigning a random string to our `fileName` using `uuidv4()`, which we will then reference in our upload later. Next we go ahead and call in our `privateKey` from our `.env`, remember this must be in Base58 format for both Irys and Helius later. If you exported it into a Unit8Array just make sure you convert that to Base58 or you will receive an error while trying to use it. After that we destruct our info from `req.body` to reference the data we are passing in. Lastly we create a connection to Irys using `getIrys`, there are [different node's of Irys](https://docs.irys.xyz/overview/nodes) you can use if you prefer. Now that we have the basis setup, let's convert and upload our SVG right below `getIrys()`: ``` /pages/api/mint.tsx const uploadImage = async () => { const irys = await getIrys(); // write the image to the vercel tmp directory fs.writeFileSync(`/tmp/${fileName}.svg`, image); console.log("wrote svg file"); // convert the svg to png with sharp await sharp(`/tmp/${fileName}.svg`) .resize(500, 500) .png() .toFile(`/tmp/${fileName}.png`) // @ts-ignore .then((data) => console.log("data", data)) // @ts-ignore .catch((err) => console.log(err)); const fileToUpload = `/tmp/${fileName}.png`; const token = "solana"; // Get size of file const { size } = await fs.promises.stat(fileToUpload); // Get cost to upload "size" bytes const price = await irys.getPrice(size); console.log( `Uploading ${size} bytes costs ${irys.utils.fromAtomic( price, )} ${token}`, ); // Fund the node await irys.fund(price); // Upload metadata try { const response = await irys.uploadFile(fileToUpload); console.log( `File uploaded ==> https://gateway.irys.xyz/${response.id}`, ); return `https://gateway.irys.xyz/${response.id}`; } catch (e) { console.log("Error uploading file ", e); } }; const image_url = await uploadImage(); ...rest of code... ``` I left comments in the code to help explain, but the synopsis is: - Write the SVG to `/tmp` directory - Tell `sharp` that we want to convert that file to `.png()` - Tell `sharp` where to write the converted file - Get the cost of upload from Irys based on file size - Fund the Irys node, upload the file, grab the returned URL 🤯**Boom!** You now just converted and uploaded a SVG live from the browser without having to actually store it anywhere besides Irys (which uses areweave). Now that we have our Image URL, we are ready to mint our compressed NFT. Below our `const image_url` let's build our `mintCompressedNft` function: ``` /pages/api/mint.tsx const mintCompressedNft = async () => { const response = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ jsonrpc: "2.0", id: "helius-test", method: "mintCompressedNft", params: { name: "Business Card", symbol: "swissDAO", owner: airdropTo, description: "A business card courtesy of swissDAO", attributes: [ { trait_type: "Name", value: `${firstName} ${lastName}`, }, { trait_type: "Job Title", value: jobTitle, }, { trait_type: "Email", value: email || "", }, { trait_type: "Phone", value: phone || "", }, { trait_type: "Twitter", value: twitter || "", }, { trait_type: "Telegram", value: telegram || "", }, { trait_type: "Website", value: website, }, { trait_type: "Creator Address", value: creatorAddress, }, ], imageUrl: image_url, externalUrl: "https://www.swissDAO.space", sellerFeeBasisPoints: 6500, creators: [ { address: process.env.NEXT_PUBLIC_WALLET_ADDRESS, share: 100, }, ], }, }), }); const { result } = await response.json(); console.log("result", result); console.log("Minted asset: ", result.assetId); return result; }; const response = await mintCompressedNft(); return res.status(200).json({ status: "success", assetId: response.assetId, }); ...rest of code... ``` Here we are sending compressed NFT data off to Helius to process, if you are familiar with minting NFTs then it looks pretty similar and that's on purpose. Again, Helius has made this SUPER easy for us which is awesome. The key things to note are: - **owner** : this is where the cNFT is going to be minted/airdropped to - **Email/Phone/Twitter/Telegram** : here we use the `||` to subsitute in a "" if these items are not chosen. This will prevent any errors occuring from them being blank. - creators -> address : this is the address we will use later to display all of our minted cNFT's in the Gallery. - **result.assetId** : we are specifically returning this item because that is what we will use on Helius' X-Ray to find our newly minted cNFT. Again, these data points are pretty much standard with NFTs and you can read more about them [here](https://www.quicknode.com/guides/solana-development/nfts/solana-nft-metadata-deep-dive). Awesome! Our Mint API route is complete. One last thing to do before we get to minting, head back to your `page.tsx` and let's add a button to trigger the `convertAndSubmit()` function. Right below the `<div></div>` containing your Sample SVGs let's include this code ``` /app/page.tsx {selectedSvg != "" && ( <div className="flex justify-center"> <button className="bg-red-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded" onClick={() => convertAndSubmit()} > Create cNFT </button> </div> )} ``` This instructs our app to only display the mint button when a SVG has been selected. If you want, you can take it a step further and only display after all data point has been filled out. Now it's time to give it a test run! 🏎️💨 Open up your terminal again, start it up and take it for a spin! ``` npm run dev ``` After selecting a SVG template and filling out the data, press `Create cNFT` and watch your terminal go to work! If all is good, you should see the following comments in your terminal (size and prices may vary). ![Screenshot 2023-11-06 at 5.42.12 PM.png](https://hackmd.io/_uploads/S1EOvcIXp.png) Nice job, you minted a cNFT!!! You can check out the Irys upload from that gateway link or even take that `assetId` and plug it into [Helius' X-Ray Explorer](https://xray.helius.xyz/) to view it. If you are getting errors, make sure you have some SOL in the wallet that is tied to the private key and double check the solution code [here](https://github.com/swissDAO/solana-business-card-cnfts/tree/mintApi) for any errors. ## 🖼️ Building a Gallery Congrats! You are a cNFT wizard 🧙‍♂️!!! A lot of people have heard of compressed NFTs, but not many know how to make them. You my friend, are a head of the curve! Now let's show them off by building a gallery! To do this we will be using the Helius API call `getAssetsByCreator`, this is why in the `mint.tsx` we assigned the creator address to the wallet saved in our `.env`. The reasoning behind this is because we are using Helius' simple `mintCompressedNft` call where Helius manages the merkle tree and ANYONE can add to it, not all of the cNFTs on that merkle tree belong to us. So if we were to just display all of the cNFTs on that tree we would see something like [this](https://xray.helius.xyz/account/5TYT7EAzT9XszzAo4ki6wKhzk3Zu4RJobt5ioV76yKrn?network=mainnet). Those are all of the cNFTs currently attached to this merkle tree. ### Building the component So let's get started with the gallery. The first thing we want to do is create a new file in our component directory and call it `gallery.tsx`. Inside this component let's start by adding the following: ``` /components/gallery.tsx import { useState, useEffect } from "react"; const Gallery = () => { const [biz_cards, setBizCards] = useState<Array<any>>([]); const [loading, setLoading] = useState<boolean>(true); return( <div> <h1>Gallery</h1> </div> ) } export default Gallery; ``` Our objective for this component is pretty simple: - Call on `getAssetsByCreator` - Store the returned data in our `bizCards` array - Map through and display each `bizCard` I'll give you the Helius' API call here, but I want to challenge you to complete the rest of your code on your own. As always, if you get stuck you can refer to the solution at the end of this segment :-). ``` /components/gallery.tsx const check_for_biz_cards = async (address: string) => { const url = process.env.NEXT_PUBLIC_RPC_URL; const response = await fetch(url!, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ jsonrpc: "2.0", id: "helius-test", method: "getAssetsByCreator", params: { creatorAddress: address, // Required onlyVerified: false, // Optional page: 1, // Starts at 1 limit: 1000, // Max 1000 }, }), }); const { result } = await response.json(); console.log("Assets by Creator: ", result.items); setBizCards(result.items); setLoading(false); }; useEffect(() => { check_for_biz_cards(process.env.NEXT_PUBLIC_WALLET_ADDRESS!); }, []); ``` One thing that tripped me up here when building this was the `onlyVerified` optional param. Here we need to flag this as `false` because Helius' merkle tree for this `helius-test` is not verified. If you were querying your cNFTs from your own merkle tree, this may not be necessary. Once you think you have the data and display correctly added (or so you think ;-) ), let's go back to our `page.tsx` and import the Gallery. What we are going to do is build a button for it along with a `useState` boolean that tells our app to either display or not display the gallery. ``` /app/page.tsx import Gallery from "@/components/gallery"; const Page: React.FC = () => { const [showGallery, setShowGallery] = useState<boolean>(false); ...rest of code... return( <div> <button className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded mb-4" onClick={() => setShowGallery(!showGallery)} > {showGallery ? "Hide Gallery" : "Show Gallery"} </button> {showGallery && <Gallery />} ...rest of code... </div ) } export default Page; ``` Now, one thing to note here is if we keep the code like this and display the gallery then there is A LOT happening on our screen. So let's wrap the rest of the display with `{!showGallery && ()}` which tells our code to only display the rest of the code if `showGallery === false`. Make sense? If you run `npm run dev` you should now see this on your main page: ![Screenshot 2023-11-07 at 12.25.36 PM.png](https://hackmd.io/_uploads/SJAn05v7T.png) And when you click the "Show Gallery" button you should see all of your minted business cards displayed similar to: ![Screenshot 2023-11-07 at 12.29.47 PM.png](https://hackmd.io/_uploads/ryin1swXT.png) Nice job friend!! If you are having issues, check the solution code here. ## 👨‍🎨 Final Touches Congrats you are officially a Compressed NFT buildoooor!! Now let's add some final touches. ### 🔗 Notify when Complete You may have noticed `<ToastContainer />` on our main page and ``` const CustomToastWithLink = (url: string) => ( <Link href={url} target="_blank" rel="noopener noreferrer"> View your business card on the blockchain </Link> ); ``` This is a more stylistic way to notify the user of things other than using `alert()`. You can check out all the things you can do with Toast [here](https://www.npmjs.com/package/react-toastify). What we will be using it for is to display a clickable link for the user to check out their newly minted cNFT on Helius' X-Ray Explorer. In order to do so, we need to adjust our `convertAndSubmit` function to account for a response from our `mint` API route. So below our `fetch` call let's add the following: ``` /app/page.tsx async function convertAndSubmit(){ ...rest of code... const response_status = res.status; const res_text = await JSON.parse(await res.text()); console.log("res_text", res_text); const asset_id = res_text.assetId; console.log("asset_id", asset_id); const xray_url = `https://xray.helius.xyz/token/${asset_id}?network=mainnet`; if (response_status === 200) { console.log("business card minted"); // get json data from response console.log("xray url", xray_url); toast.success(CustomToastWithLink(xray_url), { position: "top-right", autoClose: false, hideProgressBar: false, closeOnClick: true, pauseOnHover: false, draggable: false, progress: undefined, theme: "dark", }); } else if (response_status === 500) { console.log("error minting business card"); toast.error("Error minting business card", { position: "top-right", autoClose: false, hideProgressBar: false, closeOnClick: true, pauseOnHover: false, draggable: false, progress: undefined, theme: "dark", }); } } ``` What we are doing is waiting for a response from our `mint` API and if it sends back a status of 200 we grab the assetId provided and display it within the `xray_url`. If it's an error we also pass that into the toast message to notify the user. Now let's try to create a new cNFT and see if it works without having to check the console! You should see this: ![Screenshot 2023-11-07 at 12.46.27 PM.png](https://hackmd.io/_uploads/Hkuo7jv7T.png) Nice job! Now you may also notice a big error in your console saying something like: `Warning: React does not recognize the `toastProps` prop on a DOM element.` This is because of the Toast package and not your code, but it is annoying so if you want to fix it follow these steps: -Open up your `node_modules` directory and locate `react-toastify` -Go to `/dist/types/index.ts` -Find `toastProps` on line 11 and change it to all lowercase Should now look like this: ``` export interface ToastContentProps<Data = {}> { closeToast?: () => void; toastprops: ToastProps; data?: Data; } ``` Now when you run it there should be no errors in the console of your browser :-). ### 🚢 Deploy on Vercel Everything is working, woo-hoo! Now it's time to get off localhost and ship our creation to the masses! My go to for deployments is Vercel because it works so well with Next.JS. I like to connect to Vercel via my Github because it makes it super simple. After selecting "Add New" click "Project" and you should see something like this: ![Screenshot 2023-11-07 at 1.02.48 PM.png](https://hackmd.io/_uploads/S1FODow7a.png) Then choose the repo you want to import and before you click "Deploy" open up the dropdown for "Environment Variables" and copy/paste your `.env` here. ![Screenshot 2023-11-07 at 1.03.38 PM.png](https://hackmd.io/_uploads/ryPovjPXa.png) After that, your deployment will start and upon completion you will see: ![Screenshot 2023-11-07 at 1.06.55 PM.png](https://hackmd.io/_uploads/B1euuowma.png) SICK!!! YOU ARE OFFICIALLY OFF LOCALHOST!!!! If you head to the dashboard you will see Vercel has already created a domain for you. ![Screenshot 2023-11-07 at 1.07.22 PM.png](https://hackmd.io/_uploads/BJ9Y_iD7p.png) Now send that to your friends and let them get mint their very own Business Card directly on Solana! ## 🎉 Congratulations You have now made a cNFT minter that dynamically creates a cNFT from an in-browser image. That's pretty cool if you ask me! Here is a recap on what we covered: - [x] Generate a digital business card as a SVG in your browser - [x] Convert that SVG to a PNG and upload it to Irys (formerly Bundlr) - [x] Mint the PNG as a cNFT using Helius - [x] Display all of the business card cNFTs in a gallery - [x] Deploy our site Live to the world and allow people to mint business cards ==**without connecting their wallet**== If you need compare your final code, you can check the [complete solution](https://github.com/swissDAO/solana-business-card-cnfts/tree/final). Now spice it up! Don't just stop with my business card model, create your own SVG with something like this [SVG Editor](https://boxy-svg.com/), or add it to something you've already built. Maybe you have a Solana Pay store you want to hook it up to it for receipts or loyalty cNFTs, or a game where the user can get a cNFT of their score. Don't just stop here, keep building! And if you do, share it with me on [twitter](https://twitter.com/_matt_xyz), I always love checking out new projects in the Solana space. Also, if you think you're ready to take on a harder challenge, check out our other build where we combine [Solana Pay with cNFTs](https://github.com/swissDAO/solana-pay-cnfts). This will teach you how to build your own Merkle Tree for cNFTs and how to issue them upon verification of a Solana Pay transaction. It's a pretty sweet bundle and worth checking out! If you have any questions, don't hesitate to reach out!! **Until next time, friend** ✌️.