guides
Guide

Simple NFT Starter Kit

Magic Staff · November 17, 2023

👉 Repo for this Starter Kit

This guide is meant to walk you through how this site works and how you can customize and build on top of it to create your own project.

Magic makes it super simple to spin up secure, non-custodial wallets for users while also supporting third party wallets. This NFT Starter Kit illustrates how to integrate Dedicated Wallet with a basic NFT site that allows users to mint and view NFTs from a particular NFT collection. The guide below walks through the Dedicated Wallet integration as well as the remaining items that may be new for developers just getting into Web3 (e.g. connecting to the Ethereum network, reading data from the network, submitting transactions, etc.)

#Create your Magic instance

The src/lib/magic.ts file shows the first step to working with the Magic SDK: create an instance of Magic using an API Key from your Magic Dashboard and a config object. This site uses the config object to set the network to Sepolia, an Ethereum test network that we'll be using throughout this guide.

Javascript
01import { Magic } from "magic-sdk";
02
03// Initialize the Magic instance
04const createMagic = () => {
05  return (
06    typeof window !== "undefined" &&
07    new Magic(process.env.NEXT_PUBLIC_MAGIC_PUBLISHABLE_KEY, {
08      network: {
09        rpcUrl: "https://rpc2.sepolia.org",
10        chainId: 11155111,
11      },
12    })
13  );
14};
15
16export const magic = createMagic();

#Use the Wallet module

The remainder of the app uses the Wallet module magic.wallet and the User module magic.user. Specifically, the following three methods:

  1. magic.wallet.connectWithUI() - displays the Magic Login UI for sign-up and authentication. This is called when the user clicks the button to connect or sign up. You can see example usage in src/components/AppHeader.tsx and src/components/LoginWithMagic.tsx.

    Javascript
    01await magic.wallet.connectWithUI();
  1. magic.wallet.showUI() - once you're connected, you can use this method to show the wallet UI. User interaction is then handled by the SDK. Note that this only works for Dedicated Wallet. If you support other wallets and the user connects with one of those, this method will throw an error.

    Javascript
    01await magic.wallet.showUI();
  2. magic.user.logout() - disconnects from any connected wallet, Dedicated Wallet or otherwise.

    Javascript
    01await magic.wallet.disconnect();

#Interact with the blockchain

After you have initialized your Magic object, you can send transactions and read from your chosen blockchain network using popular web3 libraries like web3.js or ethers.js. In this demo, we are using the Sepolia test network.

You can see all of the EVM RPC methods that Magic supports here. In this project, we use the following methods:

  • getAccounts - returns a list of addresses owned by client. Typically, this is a single address representing the connected user's address.
  • getBalance - returns the balance of the given address's account
  • estimateGas - generates and returns an estimate of how much gas is necessary to allow the transaction to complete

The syntax for calling these methods depends on the library you're using. This codebase uses web3.js.

#Use web3.js

For a comprehensive review of working with web3.js package, you should check out their documentation. That being said, let's go through the way that this site uses it.

The starting point for working with web3.js is to create a new instance of Web3 and an instance of Contract using the eth module. You can see how this is done on the site in context/Web3Context.tsx:

Javascript
01// Get the provider from the Magic instance
02const provider = await magic.wallet.getProvider();
03
04// Create a new instance of Web3 with the provider
05const web3Instance = new Web3(provider);
06
07// Create a contract instance
08const contractInstance = new web3Instance.eth.Contract(
09  contractABI as any,
10  CONTRACT_ADDRESS,
11);

Notice that in addition to web3.js, we needed magic.wallet.getProvider(), the address of the contract we plan to interact with, and that same contract's Application Binary Interface (ABI). In this case, the ABI is in lib/abi.ts, but if you want the site to use a different contract you'll need to update this file to be that contract's ABI.

#Call RPC methods

To call RPC methods (not smart contract methods), you can use web3.eth followed by the RPC method, as is shown in lib/utils.ts when getting a user's ETH balance:

Javascript
01import { web3 } from "@/lib/web3";
02...
03// Get the user's address
04const [address] = await web3.eth.getAccounts();
05
06// Get the user's balance
07const balanceInWei = await web3.eth.getBalance(address);
08const balance = web3.utils.fromWei(balanceInWei);

#Call contract methods

To call contract methods, you use the instance of contract created earlier. This object has a methods field that you can use to call all of the available methods.

Contract methods are written such that the developer marks those that are read-only versus those that mutate data in some way. Any methods that involve writing data require a transaction, whereas read-only methods can be called without a transaction (and therefore no signature required from the user).

When calling read-only methods, simply use dot syntax to call the method, followed by .call(). For example, in lib/utils.ts we get the user's balance of NFTs by calling the contract's balanceOf method:

Javascript
01// Get the total count of tokens owned by the `address`.
02const tokenBalance = await contract.methods.balanceOf(address).call();

Methods that require a transaction can be more complicated. It's best to first get a gas estimate for the transaction by calling the contract method but then adding .estimateGas instead of .call. This will give you an estimate of the transaction cost, called gas.

Then you issue the actual transaction by calling the same method name and adding .send. send lets you add configuration options. We add the user's address and the estimated gas. Users connected to Dedicated Wallet will have their transaction signed by the Magic SDK, while users who chose to connect with other supported wallets will be prompted to sign using that wallet's normal UI.

The return value will be a transaction receipt that you can use to get events emitted by the transaction. The example below from lib/utils.ts shows the code for minting from the contract using the safeMint method.

Javascript
01// Estimate the gas required to mint the NFT.
02const estimatedGas = await contract.methods
03  .safeMint(address)
04  .estimateGas({ from: address });
05console.log(`Estimated gas: ${estimatedGas}`);
06
07// Prepare the transaction to mint the NFT.
08const transaction = contract.methods.safeMint(address);
09
10// Send the transaction and wait for its receipt.
11const receipt = await transaction.send({
12  from: address,
13  gas: estimatedGas,
14});
15console.log("Transaction receipt:", receipt);
16
17// Extract the minted tokenId from the transaction receipt.
18const tokenId = receipt?.events?.Transfer?.returnValues?.tokenId;
19console.log("Minted tokenId:", tokenId);

That covers the basics of how this project works! Feel free to browse the rest of the code to get an idea of how to use both the Magic SDK and web3.js!

#Use this template

When building something new, it often helps to build from an existing project rather than starting over. You're more than welcome to clone this repository and start modifying it to suit your needs. Just be sure to use your own IP for images, NFTs, etc. before publishing!

#Local setup

This is a simple Next.js app with a few basic dependencies. If you need to get familiar with Next.js, check out the Next.js documentation.

To start, make sure to install your dependencies with the following command line commands:

Bash
01npm install
02# or
03yarn install

You'll then need to copy rename .env.example to .env and fill in the values it mentions:

  • NEXT_PUBLIC_MAGIC_PUBLISHABLE_KEY - get this from your Magic Dashboard. You can sign up for a free account if you haven't already
  • NEXT_PUBLIC_CONTRACT_ADDRESS - the address of the NFT contract you want the site to use for minting NFTs. You can use ours (0xf4759a2bf9a8b6dc8318efc53e6e27b452c42310) for testing on the Sepolia testnet if you'd like, but eventually you should replace it with your own contract address.

With the environment variables set, you can run the development server:

Bash
01npm run dev
02# or
03yarn dev

The development server will run on port 3000 if available. If port 3000 is already in use, it'll use another one. You'll be able to see which port is being used in the console.

Open http://localhost:3000 (or whichever port the development server is running on) with your browser to see the result.

#Sepolia Testnet

This template uses the Sepolia Testnet so that you can experiment without using real ETH. Sepolia is an Ethereum test network, or testnet. Testnets are designed to be as close as possible to the production network without requiring tokens to have actual value. That way, you can execute transactions, deploy contracts, mint, and generally just experiment, without fear of losing money.

Before using Sepolia, you will need to first acquire some Sepolia ETH. To do so, you can use a faucet such as https://sepoliafaucet.com/. Faucets are sites that send you test ETH. At the time of writing, https://sepoliafaucet.com/ requires creating an Alchemy account, but once you have an account you can get 0.5 Sepolia ETH for testing. Once you have your Sepolia ETH, you are ready to begin minting!

Let's make some magic!