Bangun Dapp Pencetak NFT di Shardeum

Bangun Dapp Pencetak NFT di Shardeum

Panduan ini akan memandu Anda melalui proses membangun aplikasi pencetak NFT sederhana yang menggunakan React JS untuk front-end, IPFS untuk penyimpanan terdesentralisasi, dan menggunakan kontrak pintar Solidity di jaringan uji coba Shardeum. Aplikasi ini memungkinkan pengguna untuk mencetak NFT dengan metadata khusus seperti nama, deskripsi, dan gambar/gif.

1. Menyiapkan Proyek kita

Mari kita mulai dengan membuat berkas proyek kosong dan menginisialisasi npm.

mkdir shardeum-nft-minter
cd shardeum-nft-minter
npm init

2. Kontrak Pintar & Pengaturan Hardhat

Kita akan menggunakan Hardhat — Kerangka kerja Pengembangan untuk menyebar, menguji & men-debug kontrak pintar.

i) Sekarang, mari pasang hardhat sebagai dev-dependency; pilih ‘Create an empty hardhat.config.js’ dan pasang Openzeppelin Contracts Library. Selain itu, mari kita pasang semua pustaka hardhat lain yang dibutuhkan.

npm install --save-dev hardhat
npx hardhat
npm install @openzeppelin/contracts
npm install --save ethers@5.7.2 hardhat @nomiclabs/hardhat-waffle ethereum-waffle chai @nomiclabs/hardhat-ethers

ii) Buat folder ‘contracts’ dan ‘scripts’. Di dalam folder contracts, tambahkan kode berikut ini ke berkas NftMinter.sol:

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.3;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/utils/Counters.sol";

contract NFTMinter is ERC721URIStorage {
    using Counters for Counters.Counter;
    Counters.Counter private _tokenIds;
    constructor() ERC721("Shardeum Dev NFTMinter, "SNFT") {}
    function mintNFT(address recipient, string memory tokenURI) public returns (uint256) {
        _tokenIds.increment();
        uint256 newItemId = _tokenIds.current();
        _mint(recipient, newItemId);
        _setTokenURI(newItemId, tokenURI);
        return newItemId;
    }
}

Kontrak di atas mewarisi kontrak ERC721URIStorage dari OpenZeppelin, membuat sebagian besar fungsi yang dibutuhkan dari ERC721 tersedia untuk kita. Anda dapat menyesuaikan konstruktor dan mengganti nama Token ERC721 dan Simbol sesuka Anda. Kami juga mendefinisikan fungsi minNFT di mana kami melacak tokenID dan mengatur tokenURI yang menyimpan metadata token tersebut. Itu saja, kontrak pintar yang satu ini sudah cukup untuk aplikasi pencetak kita.

iii) Buat berkas deploy.js di folder skrip Anda dan tambahkan skrip penyebaran berikut:

const { ethers } = require("hardhat");

async function main() {
    const NFTMinter = await ethers.getContractFactory("NFTMinter");
    const nftMinter = await NFTMinter.deploy();
    await nftMinter.deployed();
    console.log("NFTMinter deployed to:", nftMinter.address);
  }

    main()
    .then(() => process.exit(0))
    .catch((error) => {
      console.error(error);
      process.exit(1);
    });

iv) Sekarang, mari tambahkan kode yang diperlukan untuk menyebarkan di jaringan uji coba Shardeum.

require("@nomiclabs/hardhat-waffle");
module.exports = {
  networks: {
    hardhat: {
    },
    sphinx: {
      url: "https://dapps.shardeum.org/",
      accounts:[``] // Add private key here
    },
  solidity: "0.8.3",
};

Tambahkan kunci pribadi Anda ke dalam variabel akun dan pastikan akun Anda memiliki token jaringan uji coba Shardeum yang cukup.

v) Sekarang, mari kita gunakan kontrak pintar kita pada Shardeum Sphinx Dapp untuk contoh ini.

npx hardhat run scripts/deploy.js --network sphinx

Skrip penyebaran akan menyebarkan kontrak pintar pada Jaringan Uji Coba Shardeum dan mengeluarkan alamat kontrak pintar yang digunakan. Anda akan membutuhkan alamat ini nanti, jadi simpanlah.

3. Membuat Aplikasi Front-End

Sekarang, mari kita buat aplikasi front-end dasar untuk berinteraksi dengan kontrak pintar yang kita sebarkan.

Mari kita mulai dengan menginisialisasi aplikasi react di folder yang sama. Setelah aplikasi react disiapkan, pasang juga semua paket front-end yang diperlukan.

npx create-react-app .
npm install @emotion/react @emotion/styled @mui/material axios

Kita akan membuat semua perubahan kode front-end di dalam folder src. Cari berkas NFTMinter.json dari folder artifacts Anda dan bawa ke folder src.

Berikut adalah empat berkas javascript baru yang perlu Anda buat untuk memiliki fungsi yang dibutuhkan:

  • connectWallet.js: Berkas ini akan memungkinkan kita menghubungkan front-end kita dengan kontrak pintar dan membuat kita terhubung ke metamask. Tambahkan alamat kontrak Anda di variabel yang ditentukan.
import { ethers } from "ethers";
import NFTMinter from "./NftMinter.json";
export async function connectWallet() {
  await window.ethereum.request({ method: "eth_requestAccounts" });
  const provider = new ethers.providers.Web3Provider(window.ethereum);
  const signer = provider.getSigner();
  // Insert deployed contract address here
  const contract = new ethers.Contract(``, NFTMinter.abi, signer);

  return { signer, contract };
  }

  export async function connectMetaMask (){
    const { signer } = await connectWallet();
    const address = await signer.getAddress();
    const balance = await signer.getBalance();
    const formattedBalance = ethers.utils.formatEther(balance);
    return {address, formattedBalance}
  };
  • ipfsUploader.js: Berkas ini berisi semua kode yang diperlukan untuk mengunggah berkas kita ke ipfs menggunakan Pinata. Buat akun di Pinata dan buat tautan API baru.
import axios from 'axios';
const pinataApiKey = ``; // Insert pinata Api Key
const pinataApiSecret = `` ; // Insert pinata Api secret
const pinataApiUrl = 'https://api.pinata.cloud/pinning/pinFileToIPFS';
const pinataHeaders = {
  headers: {
    'Content-Type': 'multipart/form-data',
    pinata_api_key: pinataApiKey,
    pinata_secret_api_key: pinataApiSecret,
  },
};
export async function uploadToIPFS(file) {
  const formData = new FormData();
  formData.append('file', file);
  try {
    const response = await axios.post(pinataApiUrl, formData, pinataHeaders);
    const ipfsHash = response.data.IpfsHash;
    return `https://gateway.pinata.cloud/ipfs/${ipfsHash}`;
  } catch (error) {
    console.error('Error uploading file to Pinata:', error);
    throw error;
  }
}
  • MintNFT.js: Berkas ini berisi komponen utama aplikasi pencetakan kami dan juga semua kode front-end kami.
import React, { useState } from "react";
import { connectWallet, connectMetaMask } from "./connectWallet";
import { uploadToIPFS } from "./ipfsUploader";
import {
  TextField,
  Button,
  Typography,
  Container,
  Box,
  Link,
  Grid,
  Snackbar,
  Alert,
  LinearProgress,
} from "@mui/material";
function MintNFT() {
  const [name, setName] = useState("");
  const [description, setDescription] = useState("");
  const [image, setImage] = useState(null);
  const [status, setStatus] = useState("");
  const [ipfsLink, setIpfsLink] = useState("");
  const [imageStatus, setImageStatus] = useState("");
  const [alertOpen, setAlertOpen] = useState(false);
  const [loading, setLoading] = useState(false);
  const [walletAddress, setWalletAddress] = useState("");
  const [walletBalance, setWalletBalance] = useState("");
  const [imagePreviewUrl, setImagePreviewUrl] = useState(null);
  const [transactionHistory, setTransactionHistory] = useState([]);
  const handleConnectMetaMask = async () => {
    const { address, formattedBalance } = await connectMetaMask();
    setWalletAddress(address);
    setWalletBalance(formattedBalance);
  };
  const handleImageChange = (e) => {
    setImage(e.target.files[0]);
    setImageStatus("Image selected for upload");
    setImagePreviewUrl(URL.createObjectURL(e.target.files[0]));
  };
  const mint = async () => {
    setStatus("Uploading to IPFS...");
    const imageURI = await uploadToIPFS(image);
    setIpfsLink(imageURI);
    setStatus("Minting NFT...");
    setLoading(true);
    const { signer, contract } = await connectWallet();
    const tokenURI = `data:application/json;base64,${btoa(
      JSON.stringify({
        name,
        description,
        image: imageURI,
      })
    )}`;
    const transaction = await contract.mintNFT(signer.getAddress(), tokenURI);
    await transaction.wait();
    setTransactionHistory((prevHistory) => [
      ...prevHistory,
      transaction.hash,
    ]);
    setStatus("NFT minted!");
    setAlertOpen(true);
    setLoading(false);
  };
  return (

    <Container maxWidth="lg">
      <Box sx={{ mt: 4, mb: 2 }}>
        <Typography variant="h4" align="center" gutterBottom>
          Shardeum NFT Minter
        </Typography>
      </Box>
      <Grid container spacing={2}>
        <Grid item xs={12} md={6}>
        <Box mt={2}>
              <Button
                fullWidth
                variant="contained"
                color="primary"
                onClick={handleConnectMetaMask}
                size="small"
                disabled={walletAddress} 
              >
                {walletAddress ? "Wallet Connected" : "Connect Wallet to Shardeum Sphinx Dapp 1.X"}
              </Button>
            </Box>
          {walletAddress && (
            <Box mt={2}>
              <Typography align="center">
                Wallet Address: {walletAddress}
              </Typography>
              <Typography align="center">
                Wallet Balance: {walletBalance} SHM
              </Typography>
            </Box>
          )}
          <TextField
            fullWidth
            label="NFT Name"
            variant="outlined"
            margin="normal"
            onChange={(e) => setName(e.target.value)}
          />
          <TextField
            fullWidth
            label="NFT Description"
            variant="outlined"
            margin="normal"
            onChange={(e) => setDescription(e.target.value)}
          />
          <input
            type="file"
            style={{ display: "none" }}
            id="image-upload"
            onChange={handleImageChange}
          />
          <p></p>
          <label      htmlFor="image-upload">
        <Button variant="contained" color="primary" component="span">
          Upload Image
        </Button>
      </label>
      {imageStatus && (
        <Typography variant="caption" display="block" gutterBottom>
          {imageStatus}
        </Typography>
      )}
      <Box mt={2}>
        <Button
          fullWidth
          variant="contained"
          color="secondary"
          onClick={mint}
        >
          Mint NFT
        </Button>
      </Box>
      {loading && <LinearProgress />}

      <Snackbar
        open={alertOpen}
        autoHideDuration={6000}
        onClose={() => setAlertOpen(false)}
        anchorOrigin={{ vertical: "top", horizontal: "right" }}
      >
        <Alert
          onClose={() => setAlertOpen(false)}
          severity="success"
          variant="filled"
          sx={{ width: "100%" }}
        >
          NFT minted successfully!
        </Alert>
      </Snackbar>
    </Grid>
    <Grid item xs={12} md={6}>
  <Box
    mt={2}
    sx={{
      border: "1px dashed #999",
      borderRadius: "12px",
      padding: "16px",
      display: "flex",
      justifyContent: "center",
      alignItems: "center",
      minHeight: "300px",
      background: imagePreviewUrl
        ? "none"
        : "linear-gradient(45deg, rgba(255,255,255,1) 0%, rgba(255,255,255,0) 100%)",
    }}
  >
    {imagePreviewUrl ? (
      <img
        src={imagePreviewUrl}
        alt="Uploaded preview"
        style={{
          width: "100%",
          maxHeight: "300px",
          objectFit: "contain",
          borderRadius: "12px",
        }}
      />
    ) : (
      <Typography variant="caption" color="text.secondary">
        Preview image will be displayed here
      </Typography>
    )}
  </Box>
</Grid>
    <Box mt={2}>
        <Typography align="center" color="textSecondary">
          {status}
        </Typography>
        {ipfsLink && (
      <Typography align="left">
    IPFS Link:{" "}
    <Link href={ipfsLink} target="_blank" rel="noopener noreferrer">
      {ipfsLink}
    </Link>
      </Typography>
)}
      </Box>
  </Grid>
  <Box mt={4}>
        <Typography variant="h7" align="center">
          Transaction History:
        </Typography>
        {transactionHistory.length > 0 ? (
          transactionHistory.map((hash, index) => (
            <Box key={index} mt={1} textAlign="left">
              <Link
                href={`https://explorer-dapps.shardeum.org/transaction/${hash}`}
                target="_blank"
                rel="noopener noreferrer"
              >
                {`Transaction ${index + 1}: ${hash}`}
              </Link>
            </Box>
          ))
        ) : (
          <Typography align="center" mt={1}>
            No transactions yet.
          </Typography>
        )}import React, { useState } from "react";
import { connectWallet, connectMetaMask } from "./connectWallet";
import { uploadToIPFS } from "./ipfsUploader";
import {TextField,Button,Typography,Container,Box,Link,Grid,Snackbar,Alert,LinearProgress,} from "@mui/material";
function MintNFT() {
  const [name, setName] = useState("");
  const [description, setDescription] = useState("");
  const [image, setImage] = useState(null);
  const [status, setStatus] = useState("");
  const [ipfsLink, setIpfsLink] = useState("");
  const [imageStatus, setImageStatus] = useState("");
  const [alertOpen, setAlertOpen] = useState(false);
  const [loading, setLoading] = useState(false);
  const [walletAddress, setWalletAddress] = useState("");
  const [walletBalance, setWalletBalance] = useState("");
  const [imagePreviewUrl, setImagePreviewUrl] = useState(null);
  const [transactionHistory, setTransactionHistory] = useState([]);
  const handleConnectMetaMask = async () => {
    const { address, formattedBalance } = await connectMetaMask();
    setWalletAddress(address);
    setWalletBalance(formattedBalance);
  };
  const handleImageChange = (e) => {
    setImage(e.target.files[0]);
    setImageStatus("Image selected for upload");
    setImagePreviewUrl(URL.createObjectURL(e.target.files[0]));
  };
  const mint = async () => {
    setStatus("Uploading to IPFS...");
    const imageURI = await uploadToIPFS(image);
    setIpfsLink(imageURI);
    setStatus("Minting NFT...");
    setLoading(true);
    const { signer, contract } = await connectWallet();
    const tokenURI = `data:application/json;base64,${btoa(
      JSON.stringify({
        name,
        description,
        image: imageURI,
      })
    )}`;
    const transaction = await contract.mintNFT(signer.getAddress(), tokenURI);
    await transaction.wait();
    setTransactionHistory((prevHistory) => [
      ...prevHistory,
      transaction.hash,
    ]);
    setStatus("NFT minted!");
    setAlertOpen(true);
    setLoading(false);
  };
  return (

    <Container maxWidth="lg">
      <Box sx={{ mt: 4, mb: 2 }}>
        <Typography variant="h4" align="center" gutterBottom>
          Shardeum NFT Minter
        </Typography>
      </Box>
      <Grid container spacing={2}>
        <Grid item xs={12} md={6}>
        <Box mt={2}>
              <Button
                fullWidth
                variant="contained"
                color="primary"
                onClick={handleConnectMetaMask}
                size="small"
                disabled={walletAddress} 
              >
                {walletAddress ? "Wallet Connected" : "Connect Wallet to Shardeum Sphinx Dapp 1.X"}
              </Button>
            </Box>
          {walletAddress && (
            <Box mt={2}>
              <Typography align="center">
                Wallet Address: {walletAddress}
              </Typography>
              <Typography align="center">
                Wallet Balance: {walletBalance} SHM
              </Typography>
            </Box>
          )}
          <TextField
            fullWidth
            label="NFT Name"
            variant="outlined"
            margin="normal"
            onChange={(e) => setName(e.target.value)}
          />
          <TextField
            fullWidth
            label="NFT Description"
            variant="outlined"
            margin="normal"
            onChange={(e) => setDescription(e.target.value)}
          />
          <input
            type="file"
            style={{ display: "none" }}
            id="image-upload"
            onChange={handleImageChange}
          />
          <p></p>
          <label      htmlFor="image-upload">
        <Button variant="contained" color="primary" component="span">
          Upload Image
        </Button>
      </label>
      {imageStatus && (
        <Typography variant="caption" display="block" gutterBottom>
          {imageStatus}
        </Typography>
      )}
      <Box mt={2}>
        <Button
          fullWidth
          variant="contained"
          color="secondary"
          onClick={mint}
        >
          Mint NFT
        </Button>
      </Box>
      {loading && <LinearProgress />}

      <Snackbar
        open={alertOpen}
        autoHideDuration={6000}
        onClose={() => setAlertOpen(false)}
        anchorOrigin={{ vertical: "top", horizontal: "right" }}
      >
        <Alert
          onClose={() => setAlertOpen(false)}
          severity="success"
          variant="filled"
          sx={{ width: "100%" }}
        >
          NFT minted successfully!
        </Alert>
      </Snackbar>
    </Grid>
    <Grid item xs={12} md={6}>
  <Box
    mt={2}
    sx={{
      border: "1px dashed #999",
      borderRadius: "12px",
      padding: "16px",
      display: "flex",
      justifyContent: "center",
      alignItems: "center",
      minHeight: "300px",
      background: imagePreviewUrl
        ? "none"
        : "linear-gradient(45deg, rgba(255,255,255,1) 0%, rgba(255,255,255,0) 100%)",
    }}
  >
    {imagePreviewUrl ? (
      <img
        src={imagePreviewUrl}
        alt="Uploaded preview"
        style={{
          width: "100%",
          maxHeight: "300px",
          objectFit: "contain",
          borderRadius: "12px",
        }}
      />
    ) : (
      <Typography variant="caption" color="text.secondary">
        Preview image will be displayed here
      </Typography>
    )}
  </Box>
</Grid>
    <Box mt={2}>
        <Typography align="center" color="textSecondary">
          {status}
        </Typography>
        {ipfsLink && (
      <Typography align="left">
    IPFS Link:{" "}
    <Link href={ipfsLink} target="_blank" rel="noopener noreferrer">
      {ipfsLink}
    </Link>
      </Typography>
)}
      </Box>
  </Grid>
  <Box mt={4}>
        <Typography variant="h7" align="center">
          Transaction History:
        </Typography>
        {transactionHistory.length > 0 ? (
          transactionHistory.map((hash, index) => (
            <Box key={index} mt={1} textAlign="left">
              <Link
                href={`https://explorer-dapps.shardeum.org/transaction/${hash}`}
                target="_blank"
                rel="noopener noreferrer"
              >
                {`Transaction ${index + 1}: ${hash}`}
              </Link>
            </Box>
          ))
        ) : (
          <Typography align="center" mt={1}>
            No transactions yet.
          </Typography>
        )}
      </Box>
</Container>
);
}
export default MintNFT;
  • Theme.js: Berkas ini berisi semua tema yang diperlukan untuk menata aplikasi kita.
import { createTheme } from "@mui/material/styles";
const theme = createTheme({
  palette: {
    mode: "dark",

    primary: {
      main: "#ffc926",
    },
    secondary: {
      main: "#088ef3",
    },
  },
  typography: {
    fontFamily: "Roboto, Arial, sans-serif",
    h4: {
      fontWeight: 700,
      marginBottom: "16px",
    },
    h5: {
      fontWeight: 600,
      marginBottom: "12px",
    },
    h6: {
      fontWeight: 500,
      marginBottom: "8px",
    },
    subtitle1: {
      fontWeight: 400,
      marginBottom: "8px",
    },
    caption: {
      fontStyle: "italic",
    },
  },
});
export default theme;

Dengan berkas-berkas di atas, sekarang kita telah memiliki sebagian besar front-end kita. Lakukan perubahan yang diperlukan pada App.js, App.css, index.js & index.css untuk menggabungkan semua gaya dan mengimpor berkas yang diperlukan. Anda bisa menemukan berkas-berkas akhir untuk ini di Github Jist di sini.

Anda juga dapat menemukan aplikasi yang sudah jadi di sini. Jangan ragu untuk mencocokkannya dengan kode Anda sendiri di mana pun Anda mengalami kebuntuan.

4. Jalankan Aplikasi Secara Lokal

Sekarang kita telah menulis semua kode yang diperlukan, saatnya menjalankan aplikasi kita secara lokal. Jalankan perintah berikut untuk menjalankannya di localhost Anda.

npm start

Buka http://localhost:3000 di peramban web Anda untuk mulai menggunakan pencetak NFT Shardeum yang baru Anda buat!

Berikut adalah gambar demo dari aplikasi tersebut


Tentang penulis: Sandipan Kundu adalah Insinyur Hubungan Pengembang di Shardeum. Dia telah menjadi kontributor awal ekosistem Web3 sejak 2017 dan juga berkontribusi dalam mengembangkan tim pengembang Polygon sebelumnya. Dia secara aktif membangun program penginjilan pengembang yang kuat dengan bantuan hackathon, lokakarya, konten teknis, dll. Untuk menumbuhkan dan menyebarkan berita tentang Web3 dan desentralisasi.

Tautan Sosial penulis :

Surel : sandipan@shardeum.org
Twitter: https://twitter.com/SandipanKundu42


Situs Web | Telegram | Discord | Blog | Twitter | Youtube | Reddit | GitHub | GitLab | Buku Putih