从零开始构建 NFT 网站(3)- 前端应用

Ethers.js
Vue3
DAPP
Alchemy

源码地址:nft-minter-tutorial。这个是 alchemy 的示例代码,使用 React 和 Alchemy SDK 实现,我使用了这段代码的 UI,并用 Vue3 和 Ethers.js 重新实现了一次。

创建项目

首先,使用 Vite 创建一个 Vue3 项目(假设我们对 Vue 或 React 一类的前端框架已经有了一定的了解)

npm create vite@latest

安装相关依赖:

npm install --save axios ethers

然后,编写页面 UI:

13_01

代码:

<script setup>
import { ref } from "vue";
defineProps({
	msg: String,
});
const walletAddress = ref("");
const url = ref("");
const name = ref("");
const description = ref("");
const status = ref("");

const connectWalletPressed = () => {
	// 处理连接钱包按钮的逻辑
};

const onMintPressed = () => {
	// 处理 Mint NFT 按钮的逻辑
};
</script>

<template>
	<h1>{{ msg }}</h1>
	<div class="Minter">
		<button id="walletButton" @click="connectWalletPressed">
			<span v-if="walletAddress.length > 0">
				Connected: {{ walletAddress.substring(0, 6) }}...
				{{ walletAddress.substring(38) }}
			</span>
			<span v-else> Connect Wallet </span>
		</button>
		<br />
		<form>
			<h2>🖼 Link to asset:</h2>
			<input
				type="text"
				placeholder="e.g. https://gateway.pinata.cloud/ipfs/<hash>"
				v-model="url"
			/>
			<h2>🤔 Name:</h2>
			<input type="text" placeholder="e.g. My first NFT!" v-model="name" />
			<h2>✍️ Description:</h2>
			<input
				type="text"
				placeholder="e.g. Even cooler than cryptokitties ;)"
				v-model="description"
			/>
		</form>
		<button id="mintButton" @click="onMintPressed">Mint NFT</button>
		<p id="status">
			{{ status }}
		</p>
	</div>
</template>

<style scoped></style>

连接 Metamask

connectWallet

首先,建立一个 utils 文件夹,再创建一个 interact.js 文件,把 connectWalletPressed 函数里的逻辑抽象出来:

//  interact.js
export const connectWallet = async () => {
	if (window.ethereum) {
		try {
			const addressArray = await window.ethereum.request({
				method: "eth_requestAccounts",
			});
			const obj = {
				status: "👆🏽 Write a message in the text-field above.",
				address: addressArray[0],
			};
			return obj;
		} catch (err) {
			return {
				address: "",
				status: "😥 " + err.message,
			};
		}
	} else {
		return {
			address: "",
			status: "you must install Metamask in your browser",
		};
	}
};

这一步是检测 Metamask 是否安装,如果已安装,就唤起钱包请求连接。

然后,在 connectWalletPressed 函数里使用这个方法:

const connectWalletPressed = async () => {
	// 处理连接钱包按钮的逻辑
	const walletResponse = await connectWallet();
	status.value = walletResponse.status;
	walletAddress.value = walletResponse.address;
};

点击按钮,在 Metamask 上点击确认,成功后会显示 Connected: 钱包地址

13_02

getCurrentWalletConnected

新的问题是,每次刷新,都要重新连接 Metamask 钱包,我们可以再写一个方法。

export const getCurrentWalletConnected = async () => {
	if (window.ethereum) {
		try {
			const addressArray = await window.ethereum.request({
				method: "eth_accounts",
			});
			if (addressArray.length > 0) {
				return {
					address: addressArray[0],
					status: "👆🏽 Write a message in the text-field above.",
				};
			} else {
				return {
					address: "",
					status: "🦊 Connect to Metamask using the top right button.",
				};
			}
		} catch (err) {
			return {
				address: "",
				status: "😥 " + err.message,
			};
		}
	} else {
		return {
			address: "",
			status: "you must install Metamask in your browser",
		};
	}
};

getCurrentWalletConnected 和 connectWallet 方法的区别是,getCurrentWalletConnected 调用的不是 eth_requestAccounts,而是 eth_accounts,如果我们已经连接过,它将返回一个包含已连接到 DApp 的以太坊地址的数组。

我们可以在 onMounted 里调用这个方法:

onMounted(async () => {
	const walletResponse = await getCurrentWalletConnected();
	status.value = walletResponse.status;
	walletAddress.value = walletResponse.address;
});

addWalletListener

钱包设置的最后一步是监听钱包,当钱包状态发生变化时(断开或切换账户),UI 可以更新。

在 Minter.vue 里,添加 addWalletListener 函数:

const addWalletListener = () => {
	if (window.ethereum) {
		window.ethereum.on("accountsChanged", (accounts) => {
			if (accounts.length > 0) {
				walletAddress.value = accounts[0];
				status.value = "👆🏽 Write a message in the text-field above.";
			} else {
				setWawalletAddress.value = "";
				status.value = "🦊 Connect to Metamask using the top right button.";
			}
		});
	} else {
		status.value = "You must install Metamask in your browser.";
	}
};

最后,在 onMounted 里调用它

onMounted(async () => {
	const walletResponse = await getCurrentWalletConnected();
	status.value = walletResponse.status;
	walletAddress.value = walletResponse.address;

	addWalletListener();
});

现在,所有钱包功能的设置都已经完成了,下一步是铸造 NFT。

使用 Pinata 把元数据上传到 IPFS

第二部分里已经使用过 Pinata 了,现在我需要用代码和 Pinata 交互。

首先,到 Pinata 上创建一个 API Key,复制 API Key 和 API Secret 到 .env 里:

VITE_PINATA_KEY = <pinata-api-key>
VITE_PINATA_SECRET = <pinata-api-secret>

然后,在 utils 文件夹里创建一个名为 pinata.js 的文件,并从 .env 文件导入 Key 和 Secret:

const key = process.env.VITE_PINATA_KEY;
const secret = process.env.VITE_PINATA_SECRET;

接下来,使用 axios 向 Pinata 发送请求:

import axios from "axios";
const key = process.env.VITE_PINATA_KEY;
const secret = process.env.VITE_PINATA_SECRET;

export const pinJSONToIPFS = async (JSONBody) => {
	const url = `https://api.pinata.cloud/pinning/pinJSONToIPFS`;
	return axios
		.post(url, JSONBody, {
			headers: {
				pinata_api_key: key,
				pinata_secret_api_key: secret,
			},
		})
		.then(function (response) {
			return {
				success: true,
				pinataUrl:
					"https://gateway.pinata.cloud/ipfs/" + response.data.IpfsHash,
			};
		})
		.catch(function (error) {
			console.log(error);
			return {
				success: false,
				message: error.message,
			};
		});
};

加载智能合约

这里我用 ethers.js 来和合约交互。

回到 interact.js 文件,在文件顶部添加代码:

import { ethers } from "ethers";
import { pinJSONToIPFS } from "./pinata.js";
import contract from "./MyNFT.json";

然后添加合约 ABI 和合约地址(上一部分添加过的):

import { ethers } from "ethers";
import { pinJSONToIPFS } from "./pinata.js";
import contract from "./MyNFT.json";

const contractABI = contract.abi;
const contractAddress = "0x7130Df343097ED88d112Cec1B366bDaa3530a67e";

实现 mintNFT 函数

mintNFT 接收三个参数:url;name;description;就是我们在表单上输入的内容。

export const mintNFT = async (url, name, description) => {
	const metadata = new Object();
	metadata.name = name;
	metadata.image = url;
	metadata.description = description;
	const pinataResponse = await pinJSONToIPFS(metadata);
	if (!pinataResponse.success) {
		return {
			success: false,
			status: "😢 Something went wrong while uploading your tokenURI.",
		};
	}
	const tokenURI = pinataResponse.pinataUrl;

	try {
		const web3Provider = new ethers.BrowserProvider(window.ethereum);
		const signer = await web3Provider.getSigner();

		// 连接钱包到合约
		const nftContract = new ethers.Contract(
			contractAddress,
			contractABI,
			signer
		);

		// 执行合约方法
		let nftTx = await nftContract.mintNFT(
			window.ethereum.selectedAddress,
			tokenURI
		);

		// 等待交易确认
		await nftTx.wait();

		return {
			success: true,
			status: `Check out your transaction on Etherscan: https://etherscan.io/tx/${nftTx.hash}`,
		};
	} catch (error) {
		return {
			success: false,
			status: "😥 Something went wrong: " + error.message,
		};
	}
};

这段代码里做了两件事:获取 Pinata 的 Url,调用合约方法。

点击 Mint NFT 按钮,如果代码正确,应该会唤起 Metamask 钱包:

13_03

点击确认,成功完成交互之后,status 会变成如下图所示:

13_03

我们就可以到 Sepolia 测试网上查看交易详情了。

和上一部分的最后一样,我们也可以到 Opensea 测试网上找到刚才上传的 NFT。

13_03