简介
什么是NFT代币?
-
NFT代币全称为 非同质化代币(Non-Fungible Token)
-
是一种基于区块链技术的加密货币资产,其特点在于每一个代币都是独一无二且不可互换的。这与比特币、以太坊等同质化代币(Fungible Token)不同,后者的每一个单位都是相同的,可以互换。
-
NFT 的主要特性
-
唯一性:
每一个 NFT 都包含独特的信息,使其与其他 NFT 区别开来。这种唯一性可以用于表示数字资产的所有权和真实性。 -
不可分割性:
大多数 NFT 是不可分割的,不能像比特币那样分成更小的单位进行交易。每一个 NFT 通常作为一个整体进行买卖。 -
所有权和可验证性:
由于 NFT 存储在区块链上,它们的所有权和交易历史是公开可验证的。每个所有者的信息和交易记录都可以在区块链上查询,确保了透明性和安全性。 -
互操作性:
NFT 通常遵循特定的标准(例如,以太坊的 ERC-721 和 ERC-1155 标准),使它们可以在不同的平台和应用之间互操作。这意味着一个平台上创建的 NFT 可以在另一个平台上展示或交易。
-
-
NFT 的应用场景
-
数字艺术:
艺术家可以创建数字艺术作品,并通过 NFT 证明其真实性和所有权。NFT 允许艺术家在区块链上销售和转移他们的作品,同时保留作品的版权和原始性。 -
收藏品:
类似于实体世界中的收藏品(如稀有卡片、邮票等),数字收藏品也可以通过 NFT 表示。这些数字收藏品可以是游戏中的道具、虚拟宠物、数字纪念品等。 -
游戏:
游戏开发者可以通过 NFT 创建和销售游戏内的物品、角色和皮肤。玩家可以拥有和交易这些数字资产,并且可以在不同游戏之间互操作。 -
虚拟地产:
在虚拟世界或元宇宙(Metaverse)中,土地和财产可以通过 NFT 表示。用户可以购买、出售和开发这些虚拟地产。 -
身份和证书:
NFT 可以用于表示数字身份和证书,例如学位证书、活动门票、会员资格等。这些证书可以存储在区块链上,确保其真实性和可验证性。
-
-
NFT 是一种创新的区块链技术应用,通过其独特性、不可分割性、所有权和可验证性等特性,使其在数字艺术、收藏品、游戏、虚拟地产和身份认证等领域具有广泛的应用前景。NFT 的发展正在改变我们对数字资产的理解和使用方式。
NFT代币案例及故事
-
Beeple的《Everydays: The First 5000 Days》
案例:
数字艺术家Beeple(本名Mike Winkelmann)创作的《Everydays: The First 5000 Days》是一幅由5000张每天创作的图像拼接而成的数字画作。故事:
Beeple每天创作并发布一张数字艺术作品,连续5000天从未间断。2021年3月,这幅作品在佳士得拍卖行以6934万美元的价格售出,成为历史上最昂贵的NFT艺术品。这次拍卖引发了广泛关注,标志着NFT艺术品进入主流市场。 -
CryptoPunks
案例:
CryptoPunks是一系列10,000个独特的24x24像素艺术图像,每个CryptoPunk都是由Larva Labs在以太坊区块链上创建的NFT。故事:
CryptoPunks最初是免费发布的,任何拥有以太坊钱包的人都可以领取。然而,随着NFT市场的发展,这些早期的NFT收藏品变得极其珍贵。一些稀有的CryptoPunks,如外星人、僵尸和猿人,曾以数百万美元的价格售出。例如,CryptoPunk #7804在2021年以4200 ETH(约750万美元)的价格售出。 -
NBA Top Shot
案例:
NBA Top Shot是由Dapper Labs与NBA合作推出的平台,用户可以购买、出售和交易NBA比赛的精彩瞬间,这些瞬间以NFT的形式存在。故事:
NBA Top Shot的NFT包含了NBA比赛中的关键时刻视频片段,每个片段都是独一无二的收藏品。2021年初,NBA Top Shot迅速走红,许多瞬间的价格飙升。例如,勒布朗·詹姆斯的一个扣篮瞬间曾以超过20万美元的价格售出。这一平台吸引了大量体育迷和收藏家的关注,推动了NFT在体育界的应用。 -
Decentraland
案例:
Decentraland是一个基于区块链的虚拟现实平台,用户可以在其中购买、出售和开发虚拟地产,这些地产以NFT的形式存在。故事:
Decentraland的用户可以使用加密货币购买虚拟土地,开发自己的虚拟世界和应用。2021年,一块虚拟土地曾以90万美元的价格售出,这显示了虚拟地产的潜在价值。Decentraland的成功也带动了其他类似项目的发展,虚拟现实和元宇宙成为了新的投资热点。 -
Jack Dorsey的首条推文
案例:
Twitter创始人兼CEO Jack Dorsey将他在2006年发布的第一条推文“just setting up my twttr”作为NFT进行拍卖。故事:
这条历史性的推文在2021年3月被作为NFT在Valuables平台上拍卖,最终以超过290万美元的价格售出。Dorsey表示,他将拍卖所得的全部收益捐给慈善机构。这一事件展示了NFT在数字内容所有权和收藏品市场中的潜力。
这些案例展示了NFT在艺术、体育、虚拟现实和数字内容领域的广泛应用和潜力,也突显了NFT市场的快速增长和巨大的商业价值。
环境部署与合约编写
配置环境
- 使用VScode编辑器
- 安装Solidity插件
Solidity 是一种面向对象的高级静态语言,用于实现智能合约,运行于 以太坊虚拟机(EVM)
- 安装node.js
Node.js是一个JavaScript 运行时环境,用于构建服务器端应用程序,便于开发人员使用JS编写服务器端代码,实现前后端一体化
- 安装truffle
Truffle 是一个功能强大、灵活易用的以太坊智能合约开发框架,广泛应用于以太坊生态系统中的智能合约开发、测试和部署
npm install -g truffle
- 安装Ganache
Ganache 是一个用于以太坊开发和测试的个人区块链网络,它提供了一个简单易用的方式来模拟以太坊网络,并且不需要实际连接到真实的区块链网络。
首先使用初始化命令对环境进行初始化(在vscode终端中进行)
truffle init
初始化环境之后 修改配置文件
在VScode 中打开truffle-config.js
这个文件
在文件中添加这两段代码,第一段代码是根据Ganache网络写的(注意:这里的from 地址为Ganache网络中真实存在的地址,这里我选的是第一个),第二段代码是修改自己solidity的版本
- 下载Pinata并获取密钥
首先需要注册Pinata的账户,然后在VScode终端中输入安装命令
npm install @pinata/sdk@1.1.0
Pinata 是一个提供 IPFS(InterPlanetary File System)服务的公司,专注于为用户提供简单易用的文件上传、管理和分发服务。IPFS 是一种分布式文件系统,旨在创建一个更开放、弹性和分散的网络。
将复制的密钥信息保存到某个文件中
然后编写一个上传图片的代码 (目的是使用一张图片作为NFT标识)以下是一个案例
const axios = require('axios');
const FormData = require('form-data');
const fs = require('fs');
const API_KEY = '8243868a73ce0b3c69a6';
const API_SECRET = '8314e946fe8551b1053c3af4147da5bc720cc94ba27a783ba05f0d045797fbcb';
const PINATA_BASE_URL = 'https://api.pinata.cloud';
async function uploadImageToPinata() {
const imagePath = './GoldDog.webp'; // 确保图片路径正确
const formData = new FormData();
formData.append('file', fs.createReadStream(imagePath));
try {
const response = await axios.post(`${PINATA_BASE_URL}/pinning/pinFileToIPFS`, formData, {
headers: {
'pinata_api_key': API_KEY,
'pinata_secret_api_key': API_SECRET,
...formData.getHeaders()
}
});
console.log('File uploaded:', response.data);
} catch (error) {
console.error('Error uploading file:', error.message);
}
}
uploadImageToPinata();
注意:修改API_KEY, APT_SECRET, imagePath
APT_KEY和API_SECRET是刚才复制的,imagePath是自己的图片,注意写对位置
编写完成后运行代码,会在Pinata中看到自己的图片
7. 下载OpenZepelin简化开发
npm install @openzeppelin/contracts@4.3.x
OpenZeppelin/Contracts 是一个用于安全智能合约开发的库。 它提供了ERC20、 ERC721、ERC777、ERC1155 等标准的实现,还提供Solidity 组件来构建自定义合同和更复杂的分散系统。
这个库提供了多种合约标准,可以帮助我们简化开发,不需要我们再去写其他的合约
编写智能合约
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "../node_modules/@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "../node_modules/@openzeppelin/contracts/access/Ownable.sol";
contract NFTAvatar is ERC721URIStorage, Ownable {
uint256 private _tokenIds;
mapping(uint256 => string) private _artists;
mapping(uint256 => string) private _descriptions;
mapping(uint256 => string) private _creationDates;
// 定义一个事件
event NFTMinted(uint256 indexed tokenId, address recipient);
constructor(
string memory name,
string memory symbol
) ERC721(name, symbol) Ownable() {
_tokenIds = 0;
}
function mintNFT(
address recipient,
string memory tokenURI,
string memory artist,
string memory description,
string memory creationDate
) public onlyOwner returns (uint256) {
_tokenIds++;
uint256 newItemId = _tokenIds;
_mint(recipient, newItemId);
_setTokenURI(newItemId, tokenURI);
_artists[newItemId] = artist;
_descriptions[newItemId] = description;
_creationDates[newItemId] = creationDate;
// 触发事件
emit NFTMinted(newItemId, recipient);
return newItemId;
}
function currentTokenId() public view returns (uint256) {
return _tokenIds;
}
function getArtist(uint256 tokenId) public view returns (string memory) {
return _artists[tokenId];
}
function getDescription(
uint256 tokenId
) public view returns (string memory) {
return _descriptions[tokenId];
}
function getCreationDate(
uint256 tokenId
) public view returns (string memory) {
return _creationDates[tokenId];
}
// 获取指定tokenId的NFT的所有者地址
function getOwnerOfToken(uint256 tokenId) public view returns (address) {
return ownerOf(tokenId);
}
// 安全转移NFT
function safeTransferNFT(address from, address to, uint256 tokenId) public {
// 调用ERC721标准的safeTransferFrom
safeTransferFrom(from, to, tokenId, "");
}
}
编写完成之后在VScode终端中输入truffle compile
查看是否可以编译成功
编写部署代码
const NFTAvatar = artifacts.require("NFTAvatar");
module.exports = function (deployer) {
deployer.deploy(NFTAvatar, "NFTAvatar", "NFTA").then(function(instance) {
// 铸造一个NFT
return instance.mintNFT(
deployer.networks.development.from, // 假设部署账户就是接收者
"https://gateway.pinata.cloud/ipfs/QmQ5PzKzksKXxbht1Nh9dqfUcbShnUCZcjDSRC4wzxpdpG", // IMGURI
"xxx(艺术家姓名)", // artist
"这是一件由著名艺术家xxx创作的数字艺术作品。它具有独特的视觉表现力和深刻的艺术价值。", // description
"2024-05-012" // creationDate
);
});
}
同样的在终端中输入truffle console --network development
进入开发环境,truffle migrate
部署智能合约
编写获取TokenID的代码文件
写这个文件的目的是 查看当前NFT代币的ID以及当前NFT所有者
const NFTAvatar = artifacts.require("NFTAvatar");
module.exports = function(callback) {
// 使用 async 以便使用 await
async function getOwner() {
const instance = await NFTAvatar.deployed();
const tokenId = await instance.currentTokenId(); // 使用 instance 获取当前 Token ID
console.log("Current Token ID:", tokenId.toString());
try {
let owner = await instance.ownerOf(tokenId);
console.log(`Owner of token ${tokenId} is: ${owner}`);
} catch (error) {
console.error(`Failed to fetch owner for token ${tokenId}: ${error}`);
}
}
getOwner().then(callback);
};
编写安全转移NFT代币的代码文件
const NFTAvatar = artifacts.require("NFTAvatar");
module.exports = function(callback) {
// 使用 async 以便使用 await
async function getOwner() {
const instance = await NFTAvatar.deployed();
const tokenId = await instance.currentTokenId(); // 使用 instance 获取当前 Token ID
console.log("Current Token ID:", tokenId.toString());
try {
let owner = await instance.ownerOf(tokenId);
console.log(`Owner of token ${tokenId} is: ${owner}`);
} catch (error) {
console.error(`Failed to fetch owner for token ${tokenId}: ${error}`);
}
}
getOwner().then(callback);
};
编写此文件以实现 将NFT代币转移到另一个账户的功能
编写HTML网页文件
编写此文件 以便我们在网页中查看
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>NFT Viewer</title>
</head>
<body>
<h1>NFT Viewer</h1>
<div id="tokenInfo"></div>
<br>
<div>
<label for="ownerAddress">NFT Owner Address:</label>
<input type="text" id="ownerAddress">
</div><br>
<div>
<label for="transferAddress">Transfer Address:</label>
<input type="text" id="transferAddress">
</div><br>
<button onclick="transferNFT()">Transfer NFT</button>
<script src="https://cdn.jsdelivr.net/npm/web3@1.5.2/dist/web3.min.js"></script>
<script>
// 初始化 web3 实例
const web3 = new Web3(new Web3.providers.HttpProvider("http://127.0.0.1:7545"));
// web3.eth.defaultAccount = "0x3c716f9ee390A4997a958c2818e15f59A94fCad1";
// 定义合约地址和 ABI
const contractAddress = '0xd1a9FB991fcFa782563437cD40911474F0F7591F'; // 替换为你的合约地址
const contractABI = [
{
"inputs": [
{
"internalType": "string",
"name": "name",
"type": "string"
},
{
"internalType": "string",
"name": "symbol",
"type": "string"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "owner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "approved",
"type": "address"
},
{
"indexed": true,
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "Approval",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "owner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "operator",
"type": "address"
},
{
"indexed": false,
"internalType": "bool",
"name": "approved",
"type": "bool"
}
],
"name": "ApprovalForAll",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint256",
"name": "_fromTokenId",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "_toTokenId",
"type": "uint256"
}
],
"name": "BatchMetadataUpdate",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint256",
"name": "_tokenId",
"type": "uint256"
}
],
"name": "MetadataUpdate",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
},
{
"indexed": false,
"internalType": "address",
"name": "recipient",
"type": "address"
}
],
"name": "NFTMinted",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "previousOwner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "OwnershipTransferred",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "from",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "to",
"type": "address"
},
{
"indexed": true,
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "Transfer",
"type": "event"
},
{
"inputs": [
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "approve",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "owner",
"type": "address"
}
],
"name": "balanceOf",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function",
"constant": true
},
{
"inputs": [
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "getApproved",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function",
"constant": true
},
{
"inputs": [
{
"internalType": "address",
"name": "owner",
"type": "address"
},
{
"internalType": "address",
"name": "operator",
"type": "address"
}
],
"name": "isApprovedForAll",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function",
"constant": true
},
{
"inputs": [],
"name": "name",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function",
"constant": true
},
{
"inputs": [],
"name": "owner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function",
"constant": true
},
{
"inputs": [
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "ownerOf",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function",
"constant": true
},
{
"inputs": [],
"name": "renounceOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "from",
"type": "address"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "safeTransferFrom",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "from",
"type": "address"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
},
{
"internalType": "bytes",
"name": "data",
"type": "bytes"
}
],
"name": "safeTransferFrom",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "operator",
"type": "address"
},
{
"internalType": "bool",
"name": "approved",
"type": "bool"
}
],
"name": "setApprovalForAll",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes4",
"name": "interfaceId",
"type": "bytes4"
}
],
"name": "supportsInterface",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function",
"constant": true
},
{
"inputs": [],
"name": "symbol",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function",
"constant": true
},
{
"inputs": [
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "tokenURI",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function",
"constant": true
},
{
"inputs": [
{
"internalType": "address",
"name": "from",
"type": "address"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "transferFrom",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "transferOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "recipient",
"type": "address"
},
{
"internalType": "string",
"name": "tokenURI",
"type": "string"
},
{
"internalType": "string",
"name": "artist",
"type": "string"
},
{
"internalType": "string",
"name": "description",
"type": "string"
},
{
"internalType": "string",
"name": "creationDate",
"type": "string"
}
],
"name": "mintNFT",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "currentTokenId",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function",
"constant": true
},
{
"inputs": [
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "getArtist",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function",
"constant": true
},
{
"inputs": [
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "getDescription",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function",
"constant": true
},
{
"inputs": [
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "getCreationDate",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function",
"constant": true
},
{
"inputs": [
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "getOwnerOfToken",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function",
"constant": true
},
{
"inputs": [
{
"internalType": "address",
"name": "from",
"type": "address"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "safeTransferNFT",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}
];
// 创建合约实例
const contract = new web3.eth.Contract(contractABI, contractAddress);
// 获取 NFT 的相关信息并显示在页面上
async function displayTokenInfo() {
try {
const tokenId = await contract.methods.currentTokenId().call();
const owner = await contract.methods.getOwnerOfToken(tokenId).call();
// 设置默认发送者地址
web3.eth.defaultAccount = owner;
const artist = await contract.methods.getArtist(tokenId).call();
const description = await contract.methods.getDescription(tokenId).call();
const creationDate = await contract.methods.getCreationDate(tokenId).call();
const tokenURI = await contract.methods.tokenURI(tokenId).call();
document.getElementById('tokenInfo').innerHTML = `
<p>Token ID: ${tokenId}</p>
<p>NFT Owner: ${owner}</p>
<p>Artist: ${artist}</p>
<p>Description: ${description}</p>
<p>Creation Date: ${creationDate}</p>
<img src="${tokenURI}" style="max-width: 300px;">
`;
} catch (error) {
console.error("Error fetching token info:", error);
}
}
// 转移 NFT 资产的函数
async function transferNFT() {
const ownerAddress = document.getElementById('ownerAddress').value;
const transferAddress = document.getElementById('transferAddress').value;
try {
const tokenId = await contract.methods.currentTokenId().call();
await contract.methods.safeTransferNFT(ownerAddress, transferAddress, tokenId).send({ from: web3.eth.defaultAccount });
alert('NFT successfully transferred.');
} catch (error) {
console.error("Error transferring NFT:", error);
alert('Failed to transfer NFT.');
}
}
// 页面加载时调用函数显示 NFT 的相关信息
displayTokenInfo();
</script>
</body>
</html>
注意:修改合约地址和ABI
合约地址需要在Gananche中点击上方的CONTRACTS
然后点击中间的按钮
然后点击ADD PROJECT
, 文件选择最开始修改的truffle-config.js
文件 选择完成后点击RESTART
保存
ABI在build目录下的NFTAvatar.json
文件中
测试代码功能
在VScode中安装 live server插件,以便与实时预览静态网页和动态网站
安装完成之后就可以像下面这样打开网页了
以下就是打开的网页,可以查看到NFT的ID,所有者,作者,描述,甚至创建时间
同时还实现了转移NFT的功能