本文环境准备以 Mac OS 为例,在 version 12.0.1 下进行开发 本文教程基于🔗 buildspace的课程:Mint your own NFT collection and ship a Web3 app to show them off
HardHat是一个允许我们快速创建智能合约并在本地部署的工具,具体介绍见官网链接:🔗 HardHat 前置安装brew -> Node/npm,若没有brew请自行安装,安装好brew后可以通过brew直接安装node:
等待时间较长,可以通过🪜解决。 接下来准备本地文件夹,并初始化文件夹内环境:
mkdir epic-nfts
cd epic-nfts
npm init -y
npm install --save-dev hardhat
等待进度完成即可。
启动HardHat:
选择Create a basic sample project后一路回车确认,并等待其中自动触发的npm安装。 待命令行显示✨ Project created ✨说明project初始化成功。 之后安装OpenZeppelin,这是创建智能合约的另一个常用库。
npm install @openzeppelin/contracts
安装完毕后启动HardHat:
npx hardhat run scripts/sample-script.js
你会在命令行中看到:
Downloading compiler 0.8.4
Compiled 2 Solidity files successfully
Deploying a Greeter with greeting: Hello, Hardhat!
Greeter deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3
如果看到了,则说明本地环境已建立,且已将智能合约运行在本地区块链上,其中智能合约在:
less contracts/Greeter.sol
而该合约的执行脚本在:
less scripts/sample-script.js
OpenZeppelin已经为我们实现了NFT标准ERC721中的基本内容,我们在此基础上修改即可。包内源码可以从Git上拉下来看:🔗 OpenZeppelin源码。同时,ERC721的接口文档见:🔗 ERC721 另外需要解释的是Solidity已经提供了一些自有变量,如下面能看到的msg.sender能够返回当前处理的call的public address。通过这些变量可以更轻松的进行一些操作,具体见:🔗 Solidity自有变量 修改后的智能合约如下:
pragma solidity ^0.8.1;
// We first import some OpenZeppelin Contracts.
// 首先我们import了OpenZeppelin包内的一些契约
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
import "hardhat/console.sol";
// We inherit the contract we imported. This means we'll have access
// to the inherited contract's methods.
// 我们继承了OpenZeppelin中导入的契约
contract MyEpicNFT is ERC721URIStorage {
// Magic given to us by OpenZeppelin to help us keep track of tokenIds.
// _tokenIds是NFT的唯一标识符,例如#001、#002
using Counters for Counters.Counter;
Counters.Counter private _tokenIds;
// We need to pass the name of our NFTs token and its symbol.
constructor() ERC721 ("SquareNFT", "SQUARE") {
console.log("This is my NFT contract. Woah!");
}
// A function our user will hit to get their NFT.
// 通过Mint获取NFT的函数
function makeAnEpicNFT() public {
// Get the current tokenId, this starts at 0.
// 拿去当前NFT的ID
uint256 newItemId = _tokenIds.current();
// Actually mint the NFT to the sender using msg.sender.
// 将当前ID的NFT发送给该调用地址
_safeMint(msg.sender, newItemId);
// Set the NFTs data.
// 设置NFTs唯一标识符以及与该唯一标识符相关的数据
// 通过Json申明该NFT的metadata
_setTokenURI(newItemId, "https://jsonkeeper.com/b/ONMH");
console.log("An NFT w/ ID %s has been minted to %s", newItemId, msg.sender);
// Increment the counter for when the next NFT is minted.
// NFT ID ++
_tokenIds.increment();
}
}
其中tokenURI用来link一个用Json描述的metadata,类似于:
{
"name": "Spongebob Cowboy Pants",
"description": "A silent hero. A watchful protector.",
"image": "https://i.imgur.com/v7U019j.png"
}
而像OpenSea之类符合ERC721标准的网站就可以读取metadata的内容并展示在该NFT的对应位置。 可以通过🔗 JSON Keeper保存你的metadata JSON。
touch ./scripts/run.js
使用该js调用智能合约中的makeAnEpicNFT()方法:
const main = async () => {
const nftContractFactory = await hre.ethers.getContractFactory('MyEpicNFT');
const nftContract = await nftContractFactory.deploy();
await nftContract.deployed();
console.log("Contract deployed to:", nftContract.address);
// Call the function.
let txn = await nftContract.makeAnEpicNFT()
// Wait for it to be mined.
await txn.wait()
// Mint another NFT for fun.
txn = await nftContract.makeAnEpicNFT()
// Wait for it to be mined.
await txn.wait()
};
const runMain = async () => {
try {
await main();
process.exit(0);
} catch (error) {
console.log(error);
process.exit(1);
}
};
runMain();
运行:
npx hardhat run scripts/run.js
得到结果:
Compiled 12 Solidity files successfully
This is my NFT contract. Woah!
Contract deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3
An NFT w/ ID 0 has been minted to 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
An NFT w/ ID 1 has been minted to 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
则说明运行成功,且#0和#1已被Mint。但是目前所有人Mint的都是相同metadata描述的NFT。
Transaction: 当我们进行一次改变区块链的动作时,我们将其称为Transaction。
Alchemy本质上帮助我们传播我们的合约创建transaction,以便它可以尽快被矿工接收。一旦挖掘到transaction,它就会将其作为合法合约广播到区块链,以便人们更新他们的区块链副本。 打开🔗 Alchemy官网并注册账号,新建一个App,选择Ethereum链以及Rinkeby网络(测试网络),点击你新创建的App,并在右上角view key按钮下找到你的API(API是私密的)。
接下来需要获取Rinkeby测试网络中的测试ETH,在小狐狸切换网络界面中选择打开测试网络,并切换到Rinkeby,通过🔗 faucets链接钱包并获取测试ETH。等待交易完成且链上确认后,就能够在钱包中看到测试ETH了(需要一段时间)。
建议区分deploy.js以及run.js,将run.js复制一份到deploy.js:
const main = async () => {
const nftContractFactory = await hre.ethers.getContractFactory('MyEpicNFT');
const nftContract = await nftContractFactory.deploy();
await nftContract.deployed();
console.log("Contract deployed to:", nftContract.address);
// Call the function.
let txn = await nftContract.makeAnEpicNFT()
// Wait for it to be mined.
await txn.wait()
console.log("Minted NFT #1")
txn = await nftContract.makeAnEpicNFT()
// Wait for it to be mined.
await txn.wait()
console.log("Minted NFT #2")
};
const runMain = async () => {
try {
await main();
process.exit(0);
} catch (error) {
console.log(error);
process.exit(1);
}
};
runMain();
需要修改项目根目录的hardhat.config.js:
require('@nomiclabs/hardhat-waffle');
require("dotenv").config({ path: ".env" });
module.exports = {
solidity: '0.8.1',
networks: {
rinkeby: {
url: process.env.ALCHEMY_API_KEY_URL,
accounts: [process.env.RINKEBY_PRIVATE_KEY],
},
},
};
安装dotenv以便使用env变量:
在根目录新建一个.env,由于需要将私钥登记在此,所以需要将该后缀名在.gitignore中确认登记。 编写该env文件:
ALCHEMY_API_KEY_URL=<YOUR API URL>
RINKEBY_PRIVATE_KEY=<YOUR PRIVATE KEY>
其中YOUR API URL为Alchemy中VIEW KEY按钮下的HTTP,私钥请参考🔗 小狐狸私钥获取指南获取。
尽量使用空钱包进行操作! 部署完成后在项目根目录运行:
npx hardhat run scripts/deploy.js --network rinkeby
应该能够看到:
Contract deployed to: 0xCaBb395dd8eCa90410B7e0fBBDA3Eb079F377C74
Minted NFT #1
Minted NFT #2
在🔗 Rinkeby Etherscan中输入你的合约部署地址查看记录是否正确。
🔗 OS Test 将合约地址复制进搜索框,等结果出来后点击结果即可查看。 如果NFT暂时未显示,请稍等片刻即可,OS需要刷新metadata。
在这一章我们需要建造随机的由三个字母组成的NFT。
NFT通常采用SVG格式,该格式通过代码构建,例如:
<svg xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMinYMin meet" viewBox="0 0 350 350">
<style>.base { fill: white; font-family: serif; font-size: 14px; }</style>
<rect width="100%" height="100%" fill="black" />
<text x="50%" y="50%" class="base" dominant-baseline="middle" text-anchor="middle">EpicLordHamburger</text>
</svg>
🔗 base64 encoder 我们将上述的svg利用这个网站转换为base64编码:
PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHByZXNlcnZlQXNwZWN0UmF0aW89InhNaW5ZTWluIG1lZXQiIHZpZXdCb3g9IjAgMCAzNTAgMzUwIj4KICAgIDxzdHlsZT4uYmFzZSB7IGZpbGw6IHdoaXRlOyBmb250LWZhbWlseTogc2VyaWY7IGZvbnQtc2l6ZTogMTRweDsgfTwvc3R5bGU+CiAgICA8cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSJibGFjayIgLz4KICAgIDx0ZXh0IHg9IjUwJSIgeT0iNTAlIiBjbGFzcz0iYmFzZSIgZG9taW5hbnQtYmFzZWxpbmU9Im1pZGRsZSIgdGV4dC1hbmNob3I9Im1pZGRsZSI+RXBpY0xvcmRIYW1idXJnZXI8L3RleHQ+Cjwvc3ZnPg==
将其拼接并在浏览器中查看图片:
data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHByZXNlcnZlQXNwZWN0UmF0aW89InhNaW5ZTWluIG1lZXQiIHZpZXdCb3g9IjAgMCAzNTAgMzUwIj4KICAgIDxzdHlsZT4uYmFzZSB7IGZpbGw6IHdoaXRlOyBmb250LWZhbWlseTogc2VyaWY7IGZvbnQtc2l6ZTogMTRweDsgfTwvc3R5bGU+CiAgICA8cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSJibGFjayIgLz4KICAgIDx0ZXh0IHg9IjUwJSIgeT0iNTAlIiBjbGFzcz0iYmFzZSIgZG9taW5hbnQtYmFzZWxpbmU9Im1pZGRsZSIgdGV4dC1hbmNob3I9Im1pZGRsZSI+RXBpY0xvcmRIYW1idXJnZXI8L3RleHQ+Cjwvc3ZnPg==
只要有这个base64,就可以还原出该svg。
将matadata指向上述base64字符串:
{
"name": "EpicLordHamburger",
"description": "An NFT from the highly acclaimed square collection",
"image": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHByZXNlcnZlQXNwZWN0UmF0aW89InhNaW5ZTWluIG1lZXQiIHZpZXdCb3g9IjAgMCAzNTAgMzUwIj4KICAgIDxzdHlsZT4uYmFzZSB7IGZpbGw6IHdoaXRlOyBmb250LWZhbWlseTogc2VyaWY7IGZvbnQtc2l6ZTogMTRweDsgfTwvc3R5bGU+CiAgICA8cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSJibGFjayIgLz4KICAgIDx0ZXh0IHg9IjUwJSIgeT0iNTAlIiBjbGFzcz0iYmFzZSIgZG9taW5hbnQtYmFzZWxpbmU9Im1pZGRsZSIgdGV4dC1hbmNob3I9Im1pZGRsZSI+RXBpY0xvcmRIYW1idXJnZXI8L3RleHQ+Cjwvc3ZnPg=="
}
并再用🔗 base64 encoder对整个Json串进行一次base64编码:
ewogICAgIm5hbWUiOiAiRXBpY0xvcmRIYW1idXJnZXIiLAogICAgImRlc2NyaXB0aW9uIjogIkFuIE5GVCBmcm9tIHRoZSBoaWdobHkgYWNjbGFpbWVkIHNxdWFyZSBjb2xsZWN0aW9uIiwKICAgICJpbWFnZSI6ICJkYXRhOmltYWdlL3N2Zyt4bWw7YmFzZTY0LFBITjJaeUI0Yld4dWN6MGlhSFIwY0RvdkwzZDNkeTUzTXk1dmNtY3ZNakF3TUM5emRtY2lJSEJ5WlhObGNuWmxRWE53WldOMFVtRjBhVzg5SW5oTmFXNVpUV2x1SUcxbFpYUWlJSFpwWlhkQ2IzZzlJakFnTUNBek5UQWdNelV3SWo0S0lDQWdJRHh6ZEhsc1pUNHVZbUZ6WlNCN0lHWnBiR3c2SUhkb2FYUmxPeUJtYjI1MExXWmhiV2xzZVRvZ2MyVnlhV1k3SUdadmJuUXRjMmw2WlRvZ01UUndlRHNnZlR3dmMzUjViR1UrQ2lBZ0lDQThjbVZqZENCM2FXUjBhRDBpTVRBd0pTSWdhR1ZwWjJoMFBTSXhNREFsSWlCbWFXeHNQU0ppYkdGamF5SWdMejRLSUNBZ0lEeDBaWGgwSUhnOUlqVXdKU0lnZVQwaU5UQWxJaUJqYkdGemN6MGlZbUZ6WlNJZ1pHOXRhVzVoYm5RdFltRnpaV3hwYm1VOUltMXBaR1JzWlNJZ2RHVjRkQzFoYm1Ob2IzSTlJbTFwWkdSc1pTSStSWEJwWTB4dmNtUklZVzFpZFhKblpYSThMM1JsZUhRK0Nqd3ZjM1puUGc9PSIKfQ==
用浏览器查看该Json:
data:application/json;base64,ewogICAgIm5hbWUiOiAiRXBpY0xvcmRIYW1idXJnZXIiLAogICAgImRlc2NyaXB0aW9uIjogIkFuIE5GVCBmcm9tIHRoZSBoaWdobHkgYWNjbGFpbWVkIHNxdWFyZSBjb2xsZWN0aW9uIiwKICAgICJpbWFnZSI6ICJkYXRhOmltYWdlL3N2Zyt4bWw7YmFzZTY0LFBITjJaeUI0Yld4dWN6MGlhSFIwY0RvdkwzZDNkeTUzTXk1dmNtY3ZNakF3TUM5emRtY2lJSEJ5WlhObGNuWmxRWE53WldOMFVtRjBhVzg5SW5oTmFXNVpUV2x1SUcxbFpYUWlJSFpwWlhkQ2IzZzlJakFnTUNBek5UQWdNelV3SWo0S0lDQWdJRHh6ZEhsc1pUNHVZbUZ6WlNCN0lHWnBiR3c2SUhkb2FYUmxPeUJtYjI1MExXWmhiV2xzZVRvZ2MyVnlhV1k3SUdadmJuUXRjMmw2WlRvZ01UUndlRHNnZlR3dmMzUjViR1UrQ2lBZ0lDQThjbVZqZENCM2FXUjBhRDBpTVRBd0pTSWdhR1ZwWjJoMFBTSXhNREFsSWlCbWFXeHNQU0ppYkdGamF5SWdMejRLSUNBZ0lEeDBaWGgwSUhnOUlqVXdKU0lnZVQwaU5UQWxJaUJqYkdGemN6MGlZbUZ6WlNJZ1pHOXRhVzVoYm5RdFltRnpaV3hwYm1VOUltMXBaR1JzWlNJZ2RHVjRkQzFoYm1Ob2IzSTlJbTFwWkdSc1pTSStSWEJwWTB4dmNtUklZVzFpZFhKblpYSThMM1JsZUhRK0Nqd3ZjM1puUGc9PSIKfQ==
将上述内容以下面的格式复制进合约中并修改Token URI:
_setTokenURI(newItemId, "data:application/json;base64,INSERT_BASE_64_ENCODED_JSON_HERE")
修改完成后再部署合约并Mint:
npx hardhat run scripts/deploy.js --network rinkeby
与第二章相同,去🔗 OS Test上查看我们的NFT。 如果OS Test刷新太慢可以通过以下网址拼接检查:
https://rinkeby.rarible.com/token/INSERT_DEPLOY_CONTRACT_ADDRESS_HERE:INSERT_TOKEN_ID_HERE
在Svg生成随机单词的思路是:
申明随机单词数组;
从申明的数组中随机选出单词;
将随机选出的单词拼接进Svg表达式中;
将Svg转为Base64并拼接进Json;
将Json转为Base64;
将得到的Base64拼接加入前缀得到最终URI
最后的到的URI作为finalTokenUri通过_setTokenURI与NFT编号绑定
由上述分析可知我们需要一个String转换Base64的方法:
mkdir ./contracts/libraries
touch Base64.sol
并写入转换逻辑:
/**
*Submitted for verification at Etherscan.io on 2021-09-05
*/
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.1;
/// [MIT License]
/// @title Base64
/// @notice Provides a function for encoding some bytes in base64
/// @author Brecht Devos <brecht@loopring.org>
library Base64 {
bytes internal constant TABLE =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
/// @notice Encodes some bytes to the base64 representation
function encode(bytes memory data) internal pure returns (string memory) {
uint256 len = data.length;
if (len == 0) return "";
// multiply by 4/3 rounded up
uint256 encodedLen = 4 * ((len + 2) / 3);
// Add some extra buffer at the end
bytes memory result = new bytes(encodedLen + 32);
bytes memory table = TABLE;
assembly {
let tablePtr := add(table, 1)
let resultPtr := add(result, 32)
for {
let i := 0
} lt(i, len) {
} {
i := add(i, 3)
let input := and(mload(add(data, i)), 0xffffff)
let out := mload(add(tablePtr, and(shr(18, input), 0x3F)))
out := shl(8, out)
out := add(
out,
and(mload(add(tablePtr, and(shr(12, input), 0x3F))), 0xFF)
)
out := shl(8, out)
out := add(
out,
and(mload(add(tablePtr, and(shr(6, input), 0x3F))), 0xFF)
)
out := shl(8, out)
out := add(
out,
and(mload(add(tablePtr, and(input, 0x3F))), 0xFF)
)
out := shl(224, out)
mstore(resultPtr, out)
resultPtr := add(resultPtr, 4)
}
switch mod(len, 3)
case 1 {
mstore(sub(resultPtr, 2), shl(240, 0x3d3d))
}
case 2 {
mstore(sub(resultPtr, 1), shl(248, 0x3d))
}
mstore(result, encodedLen)
}
return string(result);
}
}
有了转换Base64的方法类,就很容易根据上述逻辑编写合约了:
pragma solidity ^0.8.1;
// We need some util functions for strings.
import "@openzeppelin/contracts/utils/Strings.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
import "hardhat/console.sol";
// import Base64.sol
import { Base64 } from "./libraries/Base64.sol";
contract MyEpicNFT is ERC721URIStorage {
using Counters for Counters.Counter;
Counters.Counter private _tokenIds;
// This is our SVG code. All we need to change is the word that's displayed. Everything else stays the same.
// So, we make a baseSvg variable here that all our NFTs can use.
// 除了<text>其他属性都一致,需要通过random方法获取随机词组后拼接完成Svg的描述
string baseSvg = "<svg xmlns='http://www.w3.org/2000/svg' preserveAspectRatio='xMinYMin meet' viewBox='0 0 350 350'><style>.base { fill: white; font-family: serif; font-size: 24px; }</style><rect width='100%' height='100%' fill='black' /><text x='50%' y='50%' class='base' dominant-baseline='middle' text-anchor='middle'>";
// I create three arrays, each with their own theme of random words.
// Pick some random funny words, names of anime characters, foods you like, whatever!
// 申明随机单词数组
string[] firstWords = ["Fantastic", "Epic", "Terrible", "Crazy", "Wild", "Terrifying"];
string[] secondWords = ["Cupcake", "Pizza", "Milk", "Curry", "Chicken", "Salad"];
string[] thirdWords = ["InMyMouth", "OnBed", "UnderFloor", "InBox", "InPlayGround", "OnTheMoon"];
constructor() ERC721 ("SquareNFT", "SQUARE") {
console.log("This is my NFT contract. Woah!");
}
// I create a function to randomly pick a word from each array.
// 从数组中随机获取一个单词
function pickRandomFirstWord(uint256 tokenId) public view returns (string memory) {
// I seed the random generator.
// 随机种子为"FIRST_WORD" + tokenId
uint256 rand = random(string(abi.encodePacked("FIRST_WORD", Strings.toString(tokenId))));
// Squash the # between 0 and the length of the array to avoid going out of bounds.
rand = rand % firstWords.length;
return firstWords[rand];
}
function pickRandomSecondWord(uint256 tokenId) public view returns (string memory) {
uint256 rand = random(string(abi.encodePacked("SECOND_WORD", Strings.toString(tokenId))));
rand = rand % secondWords.length;
return secondWords[rand];
}
function pickRandomThirdWord(uint256 tokenId) public view returns (string memory) {
uint256 rand = random(string(abi.encodePacked("THIRD_WORD", Strings.toString(tokenId))));
rand = rand % thirdWords.length;
return thirdWords[rand];
}
function random(string memory input) internal pure returns (uint256) {
return uint256(keccak256(abi.encodePacked(input)));
}
function makeAnEpicNFT() public {
uint256 newItemId = _tokenIds.current();
string memory first = pickRandomFirstWord(newItemId);
string memory second = pickRandomSecondWord(newItemId);
string memory third = pickRandomThirdWord(newItemId);
string memory combinedWord = string(abi.encodePacked(first, second, third));
string memory finalSvg = string(abi.encodePacked(baseSvg, combinedWord, "</text></svg>"));
// Get all the JSON metadata in place and base64 encode it.
// Json转换为Base64
string memory json = Base64.encode(
bytes(
string(
abi.encodePacked(
'{"name": "',
// We set the title of our NFT as the generated word.
combinedWord,
'", "description": "A highly acclaimed collection of squares.", "image": "data:image/svg+xml;base64,',
// We add data:image/svg+xml;base64 and then append our base64 encode our svg.
// Svg转换为Base64
Base64.encode(bytes(finalSvg)),
'"}'
)
)
)
);
// Just like before, we prepend data:application/json;base64, to our data.
string memory finalTokenUri = string(
abi.encodePacked("data:application/json;base64,", json)
);
console.log("\n--------------------");
console.log(finalTokenUri);
console.log("--------------------\n");
_safeMint(msg.sender, newItemId);
// Update your URI!!!
_setTokenURI(newItemId, finalTokenUri);
_tokenIds.increment();
console.log("An NFT w/ ID %s has been minted to %s", newItemId, msg.sender);
}
}
npx hardhat run scripts/run.js
查看从控制台打印的finalTokenUri,将得到的finalTokenUri粘贴进🔗 NFT preview进行Debug,这比起经过OS Test查看更加方便。 查看无误后可以部署在Rinkeby并通过OS Test查看:
npx hardhat run scripts/deploy.js --network rinkeby
🔗 Replit是一款基于网页的IDE,允许你在web端构建web应用。 注册完成后点击🔗 nft-starter-project后Fork repo进自己的库,并单击Run尝试运行。
需要下载MetaMask并登陆钱包。 为了让我们的网站与区块链通信,我们需要以某种方式将我们的钱包与它连接起来。一旦我们将钱包连接到我们的网站,我们的网站将有权代表我们调用智能合约。 如果我们登陆一个MetaMask,它就会自动在我们的window下注入一个名为ethereum的Object,并能够通过它完成一系列操作。 在Replit的src -> App.jsx下进行代码编写来测试我们是否成功拿到了ethereum:
import React, { useEffect } from "react";
import './styles/App.css';
import twitterLogo from './assets/twitter-logo.svg';
// Constants
const TWITTER_HANDLE = '_buildspace';
const TWITTER_LINK = `https://twitter.com/${TWITTER_HANDLE}`;
const OPENSEA_LINK = '';
const TOTAL_MINT_COUNT = 50;
const App = () => {
const checkIfWalletIsConnected = () => {
/*
* First make sure we have access to window.ethereum
*/
const { ethereum } = window;
if (!ethereum) {
console.log("Make sure you have metamask!");
return;
} else {
console.log("We have the ethereum object", ethereum);
}
}
// Render Methods
const renderNotConnectedContainer = () => (
<button className="cta-button connect-wallet-button">
Connect to Wallet
</button>
);
/*
* This runs our function when the page loads.
*/
useEffect(() => {
checkIfWalletIsConnected();
}, [])
return (
<div className="App">
<div className="container">
<div className="header-container">
<p className="header gradient-text">My NFT Collection</p>
<p className="sub-text">
Each unique. Each beautiful. Discover your NFT today.
</p>
{/* Add your render method here */}
{renderNotConnectedContainer()}
</div>
<div className="footer-container">
<img alt="Twitter Logo" className="twitter-logo" src={twitterLogo} />
<a
className="footer-text"
href={TWITTER_LINK}
target="_blank"
rel="noreferrer"
>{`built on @${TWITTER_HANDLE}`}</a>
</div>
</div>
</div>
);
};
export default App;
在Replit IDE的右上角选择分享按钮样式的open in a new tab,打开新的独立页面并fn + F12呼出控制台查看是否打印出的语句:
We have the ethereum object
若没打印请确保目前浏览器的Meta Mask处于可用状态。 接下来,我们需要实际检查我们是否被授权实际访问用户的钱包。一旦我们有权限,我们就能调用智能合约。 修改App.jsx:
import React, { useEffect, useState } from "react";
import './styles/App.css';
import twitterLogo from './assets/twitter-logo.svg';
// Constants
const TWITTER_HANDLE = '_buildspace';
const TWITTER_LINK = `https://twitter.com/${TWITTER_HANDLE}`;
const OPENSEA_LINK = '';
const TOTAL_MINT_COUNT = 50;
const App = () => {
/*
* Just a state variable we use to store our user's public wallet. Don't forget to import useState.
*/
const [currentAccount, setCurrentAccount] = useState("");
/*
* Gotta make sure this is async.
*/
const checkIfWalletIsConnected = async () => {
const { ethereum } = window;
if (!ethereum) {
console.log("Make sure you have metamask!");
return;
} else {
console.log("We have the ethereum object", ethereum);
}
/*
* Check if we're authorized to access the user's wallet
*/
const accounts = await ethereum.request({ method: 'eth_accounts' });
/*
* User can have multiple authorized accounts, we grab the first one if its there!
*/
if (accounts.length !== 0) {
const account = accounts[0];
console.log("Found an authorized account:", account);
setCurrentAccount(account);
} else {
console.log("No authorized account found");
}
}
// Render Methods
const renderNotConnectedContainer = () => (
<button className="cta-button connect-wallet-button">
Connect to Wallet
</button>
);
useEffect(() => {
checkIfWalletIsConnected();
}, [])
return (
<div className="App">
<div className="container">
<div className="header-container">
<p className="header gradient-text">My NFT Collection</p>
<p className="sub-text">
Each unique. Each beautiful. Discover your NFT today.
</p>
{renderNotConnectedContainer()}
</div>
<div className="footer-container">
<img alt="Twitter Logo" className="twitter-logo" src={twitterLogo} />
<a
className="footer-text"
href={TWITTER_LINK}
target="_blank"
rel="noreferrer"
>{`built on @${TWITTER_HANDLE}`}</a>
</div>
</div>
</div>
);
};
export default App;
运行上述代码时,控制台里输出的是:
No authorized account found
因为我们还没有向Meta Mask询问并获取权限,需要修改Connect to Wallet按钮的逻辑。 获取权限的方法为:
const accounts = await ethereum.request({ method: "eth_requestAccounts" });
App.jsx修改为:
import React, { useEffect, useState } from "react";
import './styles/App.css';
import twitterLogo from './assets/twitter-logo.svg';
const TWITTER_HANDLE = '_buildspace';
const TWITTER_LINK = `https://twitter.com/${TWITTER_HANDLE}`;
const OPENSEA_LINK = '';
const TOTAL_MINT_COUNT = 50;
const App = () => {
const [currentAccount, setCurrentAccount] = useState("");
const checkIfWalletIsConnected = async () => {
const { ethereum } = window;
if (!ethereum) {
console.log("Make sure you have metamask!");
return;
} else {
console.log("We have the ethereum object", ethereum);
}
const accounts = await ethereum.request({ method: 'eth_accounts' });
if (accounts.length !== 0) {
const account = accounts[0];
console.log("Found an authorized account:", account);
setCurrentAccount(account);
} else {
console.log("No authorized account found");
}
}
/*
* Implement your connectWallet method here
*/
const connectWallet = async () => {
try {
const { ethereum } = window;
if (!ethereum) {
alert("Get MetaMask!");
return;
}
/*
* Fancy method to request access to account.
*/
const accounts = await ethereum.request({ method: "eth_requestAccounts" });
/*
* Boom! This should print out public address once we authorize Metamask.
*/
console.log("Connected", accounts[0]);
setCurrentAccount(accounts[0]);
} catch (error) {
console.log(error);
}
}
// Render Methods
const renderNotConnectedContainer = () => (
<button onClick={connectWallet} className="cta-button connect-wallet-button">
Connect to Wallet
</button>
);
useEffect(() => {
checkIfWalletIsConnected();
}, [])
/*
* Added a conditional render! We don't want to show Connect to Wallet if we're already connected :).
*/
return (
<div className="App">
<div className="container">
<div className="header-container">
<p className="header gradient-text">My NFT Collection</p>
<p className="sub-text">
Each unique. Each beautiful. Discover your NFT today.
</p>
{currentAccount === "" ? (
renderNotConnectedContainer()
) : (
<button onClick={null} className="cta-button connect-wallet-button">
Mint NFT
</button>
)}
</div>
<div className="footer-container">
<img alt="Twitter Logo" className="twitter-logo" src={twitterLogo} />
<a
className="footer-text"
href={TWITTER_LINK}
target="_blank"
rel="noreferrer"
>{`built on @${TWITTER_HANDLE}`}</a>
</div>
</div>
</div>
);
};
export default App;
点击按钮后获取address成功:
Found an authorized account: 0x7e44639c5daa727f5035cf59cb....
我们的合约中有makeAnEpicNFT()方法,这个方法允许我们Mint NFT,现在要做的就是从Web App去call这个方法,代码逻辑(目前还有报错)如下所示:
import { ethers } from "ethers";
const askContractToMintNft = async () => {
const CONTRACT_ADDRESS = "INSERT_YOUR_DEPLOYED_RINKEBY_CONTRACT_ADDRESS";
try {
const { ethereum } = window;
if (ethereum) {
const provider = new ethers.providers.Web3Provider(ethereum);
const signer = provider.getSigner();
const connectedContract = new ethers.Contract(CONTRACT_ADDRESS, myEpicNft.abi, signer);
console.log("Going to pop wallet now to pay gas...")
let nftTxn = await connectedContract.makeAnEpicNFT();
console.log("Mining...please wait.")
await nftTxn.wait();
console.log(`Mined, see transaction: https://rinkeby.etherscan.io/tx/${nftTxn.hash}`);
} else {
console.log("Ethereum object doesn't exist!");
}
} catch (error) {
console.log(error)
}
}
其中引入的🔗 ethers是一个支持前端与智能合约链接的库,其中provider是我们与以太坊节点进行实际通信使用的;connectedContract与实际合约进行连接,这需要我们提供合约地址、abi文件(见后文的3.9章)以及签名。
import { ethers } from "ethers";
const provider = new ethers.providers.Web3Provider(ethereum);
const signer = provider.getSigner();
const connectedContract = new ethers.Contract(CONTRACT_ADDRESS, myEpicNft.abi, signer);
得到connectedContract连接后,可以去call合约的makeAnEpicNFT()方法即可:
console.log("Going to pop wallet now to pay gas...")
let nftTxn = await connectedContract.makeAnEpicNFT();
console.log("Mining...please wait.")
await nftTxn.wait();
console.log(`Mined, see transaction: https://rinkeby.etherscan.io/tx/${nftTxn.hash}`);
最后是该方法的调用,每当有人按下Mint Now按钮的时候,去调用上述逻辑askContractToMintNft():
return (
{currentAccount === ""
? renderNotConnectedContainer()
: (
/** Add askContractToMintNft Action for the onClick event **/
<button onClick={askContractToMintNft} className="cta-button connect-wallet-button">
Mint NFT
</button>
)
}
);
每次build智能合约时,在./artifacts/contracts/Greeter.sol文件夹下都有两个JSON,选取不带dbg的那个复制到web app的src/util目录下。并import到App.jsx:
import myEpicNft from './utils/MyEpicNFT.json';
每次更新合约都需要更新你的ABI File与CONTRACT_ADDRESS。 现在可以重新部署你的合约并修改前端的ABI File与CONTRACT_ADDRESS。以我的合约为例,前端代码如下:
import React, { useEffect, useState } from "react";
import './styles/App.css';
import twitterLogo from './assets/twitter-logo.svg';
import { ethers } from "ethers";
import myEpicNft from './utils/MyEpicNFT.json';
const TWITTER_HANDLE = '_buildspace';
const TWITTER_LINK = `https://twitter.com/${TWITTER_HANDLE}`;
const OPENSEA_LINK = '';
const TOTAL_MINT_COUNT = 50;
const App = () => {
const [currentAccount, setCurrentAccount] = useState("");
const checkIfWalletIsConnected = async () => {
const { ethereum } = window;
if (!ethereum) {
console.log("Make sure you have metamask!");
return;
} else {
console.log("We have the ethereum object", ethereum);
}
const accounts = await ethereum.request({ method: 'eth_accounts' });
if (accounts.length !== 0) {
const account = accounts[0];
console.log("Found an authorized account:", account);
setCurrentAccount(account);
} else {
console.log("No authorized account found");
}
}
/*
* Implement your connectWallet method here
*/
const connectWallet = async () => {
try {
const { ethereum } = window;
if (!ethereum) {
alert("Get MetaMask!");
return;
}
/*
* Fancy method to request access to account.
*/
const accounts = await ethereum.request({ method: "eth_requestAccounts" });
/*
* Boom! This should print out public address once we authorize Metamask.
*/
console.log("Connected", accounts[0]);
setCurrentAccount(accounts[0]);
} catch (error) {
console.log(error);
}
}
const askContractToMintNft = async () => {
const CONTRACT_ADDRESS = "0xAdF931Efff7F2579E13b97b83969582DCD5E0c69";
try {
const { ethereum } = window;
if (ethereum) {
const provider = new ethers.providers.Web3Provider(ethereum);
const signer = provider.getSigner();
const connectedContract = new ethers.Contract(CONTRACT_ADDRESS, myEpicNft.abi, signer);
console.log("Going to pop wallet now to pay gas...")
let nftTxn = await connectedContract.makeAnEpicNFT();
console.log("Mining...please wait.")
await nftTxn.wait();
console.log(`Mined, see transaction: https://rinkeby.etherscan.io/tx/${nftTxn.hash}`);
} else {
console.log("Ethereum object doesn't exist!");
}
} catch (error) {
console.log(error)
}
}
// Render Methods
const renderNotConnectedContainer = () => (
<button onClick={connectWallet} className="cta-button connect-wallet-button">
Connect to Wallet
</button>
);
useEffect(() => {
checkIfWalletIsConnected();
}, [])
/*
* Added a conditional render! We don't want to show Connect to Wallet if we're already connected :).
*/
return (
<div className="App">
<div className="container">
<div className="header-container">
<p className="header gradient-text">My NFT Collection</p>
<p className="sub-text">
Each unique. Each beautiful. Discover your NFT today.
</p>
{currentAccount === "" ? (
renderNotConnectedContainer()
) : (
<button onClick={askContractToMintNft} className="cta-button connect-wallet-button">
Mint NFT
</button>
)}
</div>
<div className="footer-container">
<img alt="Twitter Logo" className="twitter-logo" src={twitterLogo} />
<a
className="footer-text"
href={TWITTER_LINK}
target="_blank"
rel="noreferrer"
>{`built on @${TWITTER_HANDLE}`}</a>
</div>
</div>
</div>
);
};
export default App;
之后链接钱包至Rinkeby链后点击Mint Now后交gas即可Mint,可以在控制台看到如🔗 Transaction Details一样的交易记录。
利用Events将Token ID传到前端并拼接成Mint的NFT的OS链接。 在智能合约的随机字符组合完成后添加:
event NewEpicNFTMinted(address sender, uint256 tokenId);
并在合约的最后一行添加:
emit NewEpicNFTMinted(msg.sender, newItemId);
事实上Event类似于合约抛出的消息,更多信息可以参考🔗 Event。 更新智能合约,记得需要同时更新ADDRESS及ABI File! 在前端添加逻辑以显示OS链接:
connectedContract.on("NewEpicNFTMinted", (from, tokenId) => {
console.log(from, tokenId.toNumber())
alert(`Hey there! We've minted your NFT. It may be blank right now. It can take a max of 10 min to show up on OpenSea. Here's the link: <https://testnets.opensea.io/assets/${CONTRACT_ADDRESS}/${tokenId.toNumber()}>`)
});
所有的代码可以查看🔗 Github
如果上传到Github,不要上传你的hardhat配置文件与你的私钥到你的repo。 如果NFT较大,存储在以太坊成本太高,可以尝试了解🔗 IPFS。使用它非常简单,你所需要做的就是将你的NFT上传到IPFS,然后使用它在合同中给你的唯一内容ID哈希,而不是Imgur URL或Svg数据。例如,将NFT上传至🔗 Pinata,获取CID并拼接IPFS地址:
https://cloudflare-ipfs.com/ipfs/INSERT_YOUR_CID_HERE
在智能合约中修改Token URI为:
_setTokenURI(newItemId, "ipfs://INSERT_YOUR_CID_HERE")
有一点,随机生成的Svg无法直接通过合约传递给IPFS。 请记住,NFT最终只是一个链接到一些元数据的JSON文件。您可以将这个JSON文件放到IPFS上。你也可以把NFT数据本身(比如图像、视频等)放到IPFS上。不要把它复杂化。
现在合约在etherscan的Contract显示为为字节码,🔗 可以查询自己的合约链接 安装@nomiclabs/hardhat-etherscan:
npm i -D @nomiclabs/hardhat-etherscan
在🔗 etherscan注册一个账户并拿取API,填入./hardhat.config.js(不要将密钥告诉别人):
require('@nomiclabs/hardhat-waffle');
require("@nomiclabs/hardhat-etherscan");
require("dotenv").config({ path: ".env" });
module.exports = {
solidity: '0.8.1',
networks: {
rinkeby: {
url: process.env.ALCHEMY_API_KEY_URL,
accounts: [process.env.RINKEBY_PRIVATE_KEY],
},
},
etherscan: {
// Your API key for Etherscan
// Obtain one at https://etherscan.io/
apiKey: "xxxx...xxxx",
}
};
填好之后运行(修改为自己的合约地址):
npx hardhat verify YOUR_CONTRACT_ADDRESS --network rinkeby
// such us
npx hardhat verify 0x218C8cE07001df5c4045BADd78696603925A7d03 --network rinkeby
成功后原先的字节码会变为sol代码。

