Skip to content

Commit 6d75f5e

Browse files
committed
Initial commit
0 parents  commit 6d75f5e

25 files changed

+39435
-0
lines changed

.env.example

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Sorry haxors, these values are made up!
2+
HARDHAT_CHAIN_ID=1337
3+
NEXT_PUBLIC_MINTER_ADDRESS=0x5FbDB231541a8fbcb341f032d93F642f64180bb3

.eslintrc

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"extends": ["next", "next/core-web-vitals"]
3+
}

.gitattributes

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Auto detect text files and perform LF normalization
2+
* text=auto

.gitignore

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2+
3+
# dependencies
4+
/node_modules
5+
/.pnp
6+
.pnp.js
7+
8+
# testing
9+
/coverage
10+
11+
# next.js
12+
/.next/
13+
/out/
14+
15+
# production
16+
/build
17+
18+
# misc
19+
.DS_Store
20+
*.pem
21+
22+
# debug
23+
npm-debug.log*
24+
yarn-debug.log*
25+
yarn-error.log*
26+
27+
# env file
28+
.env
29+
30+
# vercel
31+
.vercel
32+
33+
node_modules
34+
35+
#Hardhat files
36+
cache
37+
artifacts

README.md

+111
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
# NFT Minting dApp Starter
2+
3+
A full stack dApp starter for minting NFTs built on Ethereum (Solidity) with Next.js (React).
4+
5+
This repo contains boilerplate code for minting NFTs from the client-side using [Solidity](https://soliditylang.org/), [React](https://reactjs.org/) and [TailwindCSS](https://tailwindcss.com/).
6+
7+
![NFT Minting dApp Starter](/public/screenshot.png)
8+
9+
## Prerequisites
10+
11+
- [Node.js](https://nodejs.org/en/download/)
12+
- [MetaMask wallet browser extension](https://metamask.io/download.html).
13+
14+
## Getting Started
15+
16+
### Clone This Repo
17+
18+
Use `git clone https://github.com/tomhirst/nft-minting-dapp-starter.git` to get the files within this repository onto your local machine.
19+
20+
### Environment Setup
21+
22+
Duplicate `.env.example` to `.env` and fill out the `HARDHAT_CHAIN_ID` environment variable. The port from the example file, if it's free, will be fine in most cases.
23+
24+
Run `npm install`.
25+
26+
### Running The Smart Contract Locally
27+
28+
Compile the ABI for the smart contract using `npx hardhat compile`.
29+
30+
If you're successful, you'll recieve a confirmation message of:
31+
32+
```
33+
Compilation finished successfully
34+
```
35+
36+
And, a `src/artifacts` folder will be created in your project.
37+
38+
Deploy the smart contract to the local blockchain for testing with `npx hardhat node`.
39+
40+
If you're successful, you'll be presented with a number of account details in the CLI. Here's an example:
41+
42+
```
43+
Account #0: 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 (10000 ETH)
44+
Private Key: 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
45+
```
46+
47+
Then in a new terminal window, `npx hardhat run scripts/deploy.js --network localhost`.
48+
49+
If you're successful, you'll get something like the following CLI output:
50+
51+
```
52+
Minter deployed to: 0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0
53+
```
54+
55+
### Adding A Local Account To MetaMask
56+
57+
Open your MetaMask browser extension and change the network to `Localhost 8545`.
58+
59+
Next, import one of the accounts by adding its Private Key (for example, `0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80` to MetaMask.
60+
61+
If you're successful, you should see the a balance resembling something like `10000 ETH` in the wallet.
62+
63+
### Connecting The Front-End
64+
65+
In `.env` set the `NEXT_PUBLIC_MINTER_ADDRESS` environment variable to the address your smart contract was deployed to. For example, `0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0`.
66+
67+
In a new terminal window, load the front-end with `npm run dev`. If you want to use an alternate port from `3000`, use `npm run dev -- --port=1234`, or whatever port number you prefer.
68+
69+
## Demo'ing The Functionality
70+
71+
Once set up, go to `localhost:3000` (or whatever post number you used), to view your dApp in the browser.
72+
73+
First, connect your wallet by clicking `Connect wallet`. Ensure you're connected to the `Localhost 8454` network in your MetaMask extension. Select the wallet that you imported earlier.
74+
75+
You can now test minting tokens, between 1 and 10 per transaction, by filling out the input with your desired amount and clicking the `Mint` button.
76+
77+
If you successfully mint a number of NFTs, you should see the `Tokens minted` amount increment.
78+
79+
Switching accounts in MetaMask will update the wallet address in the top right hand corner. Disconnecting all accounts will prompt you to connect your wallet.
80+
81+
All state is retained on browser refresh.
82+
83+
## Editing The Front-End
84+
85+
To modify the front page of your application, edit `pages/index.js`.
86+
87+
All [TailwindCSS classes](https://tailwindcss.com/docs) are available to you.
88+
89+
To lint your front-end code, use `npm run lint`.
90+
91+
## Testing
92+
93+
To test the smart contract, run `npx hardhat test`.
94+
95+
Basic tests can be found in `test/Minter.test.js`.
96+
97+
## Roadmap
98+
99+
- Show the funds available in the connected account's wallet
100+
- Add common owner functionality to the contract
101+
- Reserve tokens
102+
- Flip sale state
103+
- Set starting index
104+
- Set base URI for asset metadata
105+
- Set provenance hash
106+
- Withdraw funds
107+
- Attach image data to minted tokens with IPFS
108+
- Deploy to the Ropsten test network
109+
- Introduce code style rules and linting
110+
- Write more extensive tests
111+
- Create a TypeScript fork

components/TotalSupply.js

+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { useEffect, useState } from 'react'
2+
import { ethers } from 'ethers'
3+
import { hasEthereum, requestAccount } from '../utils/ethereum'
4+
import Minter from '../src/artifacts/contracts/Minter.sol/Minter.json'
5+
6+
export default function TotalSupply() {
7+
// UI state
8+
const [loading, setLoading] = useState(true)
9+
const [totalMinted, setTotalMinted] = useState(0)
10+
const [totalValue, setTotalValue] = useState(0)
11+
12+
// Constants
13+
const TOTAL = 10000;
14+
15+
useEffect( function() {
16+
async function fetchTotals() {
17+
if(! hasEthereum()) {
18+
console.log('Install MetaMask')
19+
setLoading(false)
20+
return
21+
}
22+
23+
await getTotalSupply()
24+
await getTotalValue()
25+
26+
setLoading(false)
27+
}
28+
fetchTotals();
29+
});
30+
31+
// Get total supply of tokens from smart contract
32+
async function getTotalSupply() {
33+
try {
34+
// Interact with contract
35+
const provider = new ethers.providers.Web3Provider(window.ethereum)
36+
const contract = new ethers.Contract(process.env.NEXT_PUBLIC_MINTER_ADDRESS, Minter.abi, provider)
37+
const data = await contract.totalSupply()
38+
39+
setTotalMinted(data.toNumber());
40+
} catch(error) {
41+
console.log(error)
42+
}
43+
}
44+
45+
// Get total value collected by the smart contract
46+
async function getTotalValue() {
47+
try {
48+
// Interact with contract
49+
const provider = new ethers.providers.Web3Provider(window.ethereum)
50+
const contract = new ethers.Contract(process.env.NEXT_PUBLIC_MINTER_ADDRESS, Minter.abi, provider)
51+
const data = await contract.getBalance()
52+
53+
setTotalValue(ethers.utils.formatEther(data).toString());
54+
} catch(error) {
55+
console.log(error)
56+
}
57+
}
58+
59+
return (
60+
<>
61+
<p>
62+
Tokens minted: { loading ? 'Loading...' : `${totalMinted}/${TOTAL}` }<br />
63+
Contract value: { loading ? 'Loading...' : `${totalValue}ETH` }
64+
</p>
65+
</>
66+
)
67+
}

components/Wallet.js

+95
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { useEffect, useState } from 'react'
2+
import { ethers } from 'ethers'
3+
import { hasEthereum, requestAccount } from '../utils/ethereum'
4+
5+
export default function Wallet() {
6+
// UI state
7+
const [loading, setLoading] = useState(true)
8+
const [connected, setConnected] = useState(false)
9+
const [message, setMessage] = useState('Connect wallet')
10+
11+
// First load
12+
useEffect( function() {
13+
async function fetchConnectedAccount() {
14+
if(! hasEthereum()) {
15+
setMessage('Install MetaMask')
16+
setLoading(false)
17+
return
18+
}
19+
20+
await setConnectedAccount()
21+
22+
setLoading(false)
23+
}
24+
fetchConnectedAccount()
25+
},[])
26+
27+
// Account changes
28+
useEffect( function() {
29+
async function listenMMAccount() {
30+
if(! hasEthereum()) return
31+
window.ethereum.on('accountsChanged', async function(accounts) {
32+
if(accounts && accounts[0]) {
33+
setMessage(accounts[0])
34+
} else {
35+
setConnected(false)
36+
setMessage('Connect wallet');
37+
}
38+
})
39+
}
40+
41+
listenMMAccount()
42+
},[])
43+
44+
// Request connection to wallet
45+
async function requestConnection() {
46+
try {
47+
await requestAccount()
48+
} catch(error) {
49+
if(error.message) setMessage(error.message)
50+
}
51+
}
52+
53+
// Set address of connected wallet
54+
async function setConnectedAccount() {
55+
try {
56+
const provider = new ethers.providers.Web3Provider(window.ethereum)
57+
const signer = provider.getSigner()
58+
const address = await signer.getAddress()
59+
60+
if(address) {
61+
setConnected(true)
62+
setMessage(address);
63+
}
64+
} catch {
65+
setMessage('Connect wallet')
66+
}
67+
}
68+
69+
// Handle connect wallet click
70+
async function handleConnectWallet() {
71+
setLoading(true);
72+
73+
await requestConnection()
74+
await setConnectedAccount()
75+
76+
setLoading(false);
77+
}
78+
79+
return (
80+
<button
81+
className="bg-gray-100 hover:bg-gray-200 focus:bg-gray-200 border hover:border-gray-300 focus:border-gray-300 rounded shadow-lg absolute top-4 right-4 lg:top-8 lg:right-8 p-4 flex items-center text-xs disabled:cursor-not-allowed"
82+
onClick={handleConnectWallet}
83+
disabled={connected || message === 'Install MetaMask'}
84+
>
85+
{ ! loading ? (
86+
<>
87+
<span className={ connected ? "rounded-full h-2 w-2 block mr-2 bg-green-500" : "rounded-full h-2 w-2 block mr-2 bg-red-500" } />
88+
{ message }
89+
</>
90+
) : (
91+
<span>Loading...</span>
92+
) }
93+
</button>
94+
)
95+
}

components/YourNFTs.js

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { useEffect, useState } from 'react'
2+
import { ethers } from 'ethers'
3+
import { hasEthereum, requestAccount } from '../utils/ethereum'
4+
import Minter from '../src/artifacts/contracts/Minter.sol/Minter.json'
5+
6+
export default function YourNFTs() {
7+
// UI state
8+
const [nfts, setNfts] = useState([])
9+
10+
useEffect( function() {
11+
getNftsOfCurrentWallet()
12+
});
13+
14+
// Get NFTs owned by current wallet
15+
async function getNftsOfCurrentWallet() {
16+
if(! hasEthereum()) return
17+
18+
try {
19+
// Fetch data from contract
20+
const provider = new ethers.providers.Web3Provider(window.ethereum)
21+
const signer = provider.getSigner()
22+
const contract = new ethers.Contract(process.env.NEXT_PUBLIC_MINTER_ADDRESS, Minter.abi, provider)
23+
const address = await signer.getAddress()
24+
// Get amount of tokens owned by this address
25+
const tokensOwned = await contract.balanceOf(address)
26+
27+
// For each token owned, get the tokenId
28+
const tokenIds = []
29+
30+
for(let i = 0; i < tokensOwned; i++) {
31+
const tokenId = await contract.tokenOfOwnerByIndex(address, i)
32+
tokenIds.push(tokenId.toString())
33+
}
34+
35+
setNfts(tokenIds)
36+
} catch(error) {
37+
console.log(error)
38+
}
39+
}
40+
41+
if(nfts.length < 1) return null;
42+
43+
return (
44+
<>
45+
<h2 className="text-2xl font-semibold mb-2">Your NFTs</h2>
46+
<ul className="grid grid-cols-4 gap-6">
47+
{ nfts.map( (nft) => <div key={nft} className="bg-gray-100 p-4 h-24 lg:h-28 flex justify-center items-center text-lg">{nft}</div>)}
48+
</ul>
49+
</>
50+
)
51+
}

0 commit comments

Comments
 (0)