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. This NFT Starter Kit illustrates how to integrate Magic's 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 some 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.)
Magic's Dedicated Wallet is a seamless authentication and wallet solution for Web3. It provides simple onboarding, a unified wallet UX, secure transaction and wallet management, and streamlined on-ramp payments.
All of this is backed by Magic's Delegated Key Management system. It's a non-custodial solution to wallet management that's both more secure and more user-friendly than traditional wallet solutions. This guide doesn't cover the underlying technology, but it's worth reading the Whitepaper if you're interested.
The Magic SDK provides all the developer tools you need to get Magic up and running.
In package.json
, you'll notice that this site has the magic-sdk
JS package
listed as a dependency. Once the SDK is installed, you simply need to create an
instance of Magic
and use the appropriate methods from the
Wallet module.
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.
import { Magic } from "magic-sdk";
// Initialize the Magic instance
const createMagic = () => {
return (
typeof window !== "undefined" &&
new Magic(process.env.NEXT_PUBLIC_MAGIC_PUBLISHABLE_KEY, {
network: {
rpcUrl: "https://rpc2.sepolia.org",
chainId: 11155111,
},
})
);
};
export const magic = createMagic();
The remainder of the app uses the
Wallet module
magic.wallet
and the User module magic.user
. Specifically, the following three methods:
-
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 insrc/components/AppHeader.tsx
andsrc/components/LoginWithMagic.tsx
.await magic.wallet.connectWithUI();
-
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.await magic.wallet.showUI()
-
magic.user.logout()
- disconnects from any connected wallet and logs out the user.await magic.user.logout();
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.
While this isn't a comprehensive list of supported RPC methods, we use the following in this project:
getAccounts
- returns a list of addresses owned by the client. Typically, this is a single address representing the connected user's address.getBalance
- returns the balance of the given address's accountestimateGas
- 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
.
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
:
// Get the provider from the Magic instance
const provider = await magic.wallet.getProvider();
// Create a new instance of Web3 with the provider
const web3Instance = new Web3(provider);
// Create a contract instance
const contractInstance = new web3Instance.eth.Contract(
contractABI as any,
CONTRACT_ADDRESS,
);
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.
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:
import { web3 } from "@/lib/web3";
...
// Get the user's address
const [address] = await web3.eth.getAccounts();
// Get the user's balance
const balanceInWei = await web3.eth.getBalance(address);
const balance = web3.utils.fromWei(balanceInWei, "ether");
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 is 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:
// Get the total count of tokens owned by the `address`.
const 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 a Magic wallet will have their
transaction signed by the Magic SDK, while users who choose 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.
// Estimate the gas required to mint the NFT.
const estimatedGas = await contract.methods
.safeMint(address)
.estimateGas({ from: address });
console.log(`Estimated gas: ${estimatedGas}`);
// Prepare the transaction to mint the NFT.
const transaction = contract.methods.safeMint(address);
// Send the transaction and wait for its receipt.
const receipt = await transaction.send({
from: address,
gas: estimatedGas,
});
console.log("Transaction receipt:", receipt);
// Extract the minted tokenId from the transaction receipt.
const tokenId = receipt?.events?.Transfer?.returnValues?.tokenId;
console.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
!
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!
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:
npm install
# or
yarn 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 alreadyNEXT_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:
npm run dev
# or
yarn 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.
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!