NFT-集合

原文地址: NFT-Collection

翻译: JulySong

现在是你推出自己的 NFT 收藏的时候了 - Crypto Devs.

要求

  • 应该只存在 20 个 Crypto Dev NFT,并且每个都应该是唯一的。
  • 用户应该能够通过一笔交易仅铸造 1 个 NFT。
  • 白名单用户应在实际销售前有 5 分钟的预售期,保证每笔交易获得 1 NFT。
  • 你的 NFT 收藏应该有一个网站。

让我们开始建造 🚀

先决条件

理论

  • 什么是不可替代代币?可替代意味着相同或可互换,例如 Eth 是可替代的。考虑到这一点,NFT 是独一无二的。每一个都是不同的。每个令牌都有独特的特征和价值。它们都可以相互区分并且不可互换,例如独特的艺术

  • 什么是 ERC-721?ERC-721 是一个开放标准,描述了如何在 EVM(以太坊虚拟机)兼容的区块链上构建不可替代的代币;它是不可替代代币的标准接口;它有一套规则,可以很容易地使用 NFT。在继续之前,先看看ERC721支持的所有功能

建造

喜欢视频?

如果您想从视频中学习,我们的 YouTube 上有本教程的录音。单击下面的屏幕截图观看视频,或继续阅读教程!

视频 1
视频 2

智能合约

  • 我们还将使用 Openzeppelin 的 Ownable.sol 来帮助您管理Ownership合同

    • 默认情况下,Ownable 合约的所有者是部署它的帐户,这通常正是您想要的。
    • Ownable 还可以让您:
      • 将所有权从所有者帐户转移到新帐户,以及
      • rrenounceOwnership 让所有者放弃此管理特权,这是集中管理初始阶段结束后的常见模式。
  • 我们还将使用 ERC721 的扩展,称为ERC721 Enumerable

    • ERC721 Enumerable 可帮助您跟踪合约中的所有 tokenIds 以及给定合约的地址持有的 tokensIds。
    • 在继续之前,请先看看它实现的功能

为了构建智能合约,我们将使用Hardhat。Hardhat 是一个以太坊开发环境和框架,专为 Solidity 中的全栈开发而设计。简单来说,您可以编写智能合约、部署它们、运行测试和调试代码。

  • 要设置 Hardhat 项目,请打开终端并执行以下命令
1
2
3
4
5
6
mkdir NFT-Collection
cd NFT-Collection
mkdir hardhat-tutorial
cd hardhat-tutorial
npm init --yes
npm install --save-dev hardhat
  • 在安装 Hardhat 的同一目录中运行:
1
npx hardhat
1
2
3
4
- Select Create a Javascript project
- Press enter for the already specified Hardhat Project root
- Press enter for the question on if you want to add a .gitignore
- Press enter for Do you want to install this sample project's dependencies with npm (@nomicfoundation/hardhat-toolbox)?

现在你有一个 hardhat 项目准备好了!

如果您在 Windows 上,请执行此额外步骤并安装这些库:)

1
npm install --save-dev @nomicfoundation/hardhat-toolbox
1
npm install @openzeppelin/contracts
  • 我们将需要调用您为之前级别部署的地址Whitelist Contract,以检查列入白名单的地址并授予他们预售访问权限。因为我们只需要调用 mapping(address => bool) public whitelistedAddresses; 我们可以为这个映射创建一个 Whitelist contract 带有函数的接口,这样我们就可以节省气体,因为我们不需要继承和部署整个Whitelist Contract,而只需要它的一部分。

  • 在目录中创建一个新文件contracts并调用它IWhitelist.sol

1
2
3
4
5
6
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

interface IWhitelist {
function whitelistedAddresses(address) external view returns (bool);
}
  • 现在让我们在目录中创建一个新文件contracts并调用它CryptoDevs.sol
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "./IWhitelist.sol";

contract CryptoDevs is ERC721Enumerable, Ownable {
/**
* @dev _baseTokenURI 用于计算 {tokenURI}。如果设置,每个
* 令牌的结果 URI 将是 `baseURI` 和 `tokenId` 的串联。
*/
string _baseTokenURI;

// _price 是一个 Crypto Dev NFT 的价格
uint256 public _price = 0.01 ether;

// _paused 用于在紧急情况下暂停合约
bool public _paused;

// CryptoDev 的最大数量
uint256 public maxTokenIds = 20;

// 铸造的 tokenIds 总数
uint256 public tokenIds;

// 白名单合约实例
IWhitelist whitelist;

// 布尔值,用于跟踪预售是否开始
bool public presaleStarted;

// 预售结束时间的时间戳
uint256 public presaleEnded;

modifier onlyWhenNotPaused {
require(!_paused, "Contract currently paused");
_;
}

/**
* @dev ERC721 构造函数接受一个 `name` 和一个 `symbol` 到令牌集合。
* 在我们的例子中,名称是“Crypto Devs”,符号是“CD”。
* Crypto Devs 的构造函数接受 baseURI 来为集合设置 _baseTokenURI。
* 它还初始化一个白名单接口的实例。
*/
constructor (string memory baseURI, address whitelistContract) ERC721("Crypto Devs", "CD") {
_baseTokenURI = baseURI;
whitelist = IWhitelist(whitelistContract);
}

/**
* @dev startPresale 开始预售白名单地址
*/
function startPresale() public onlyOwner {
presaleStarted = true;
// 将 presaleEnded 时间设置为当前时间戳 + 5 分钟
// Solidity 有很酷的时间戳语法(秒、分钟、小时、天、年)
presaleEnded = block.timestamp + 5 minutes;
}

/**
* @dev presaleMint 允许用户在预售期间为每笔交易铸造一个 NFT。
*/
function presaleMint() public payable onlyWhenNotPaused {
require(presaleStarted && block.timestamp < presaleEnded, "Presale is not running");
require(whitelist.whitelistedAddresses(msg.sender), "You are not whitelisted");
require(tokenIds < maxTokenIds, "Exceeded maximum Crypto Devs supply");
require(msg.value >= _price, "Ether sent is not correct");
tokenIds += 1;
// _safeMint 是 _mint 函数的更安全版本,因为它确保
// 如果要铸造的地址是合约,那么它知道如何处理 ERC721 代币
// 如果要铸造的地址不是合约,它的工作方式与 _mint
_safeMint(msg.sender, tokenIds);
}

/**
* @dev mint 允许用户在预售结束后为每笔交易铸造 1 个 NFT。
*/
function mint() public payable onlyWhenNotPaused {
require(presaleStarted && block.timestamp >= presaleEnded, "Presale has not ended yet");
require(tokenIds < maxTokenIds, "Exceed maximum Crypto Devs supply");
require(msg.value >= _price, "Ether sent is not correct");
tokenIds += 1;
_safeMint(msg.sender, tokenIds);
}

/**
* @dev _baseURI 覆盖 Openzeppelin 的 ERC721 实现,默认情况下
* 返回 baseURI 的空字符串
*/
function _baseURI() internal view virtual override returns (string memory) {
return _baseTokenURI;
}

/**
* @dev setPaused 使合约暂停或取消暂停
*/
function setPaused(bool val) public onlyOwner {
_paused = val;
}

/**
* @devdraw 将合约中的所有以太币 * 发送给合约的所有者
*/
function withdraw() public onlyOwner {
address _owner = owner();
uint256 amount = address(this).balance;
(bool sent, ) = _owner.call{value: amount}("");
require(sent, "Failed to send Ether");
}

// 接收以太币的函数。msg.data 必须为空
receive() external payable {}

// 当 msg.data 不为空时调用 fallback 函数
fallback() external payable {}
}
  • 现在我们将安装dotenv包以便能够导入 env 文件并在我们的配置中使用它。打开指向hardhat-tutorial目录的终端并执行此命令
1
npm install dotenv
  • 现在在hardhat-tutorial文件夹中创建一个.env文件并添加以下行,使用注释中的说明获取您的 Alchemy API 密钥 URL 和 RINKEBY 私钥。确保您获得 rinkeby 私钥的帐户由 Rinkeby 以太币提供资金。
1
2
3
4
5
6
7
8
9
// 转到 https://www.alchemyapi.io,注册,
// 在其仪表板中创建一个新应用程序并选择网络为 Rinkeby,并将“add-the-alchemy-key-url-here”替换为其密钥 url
ALCHEMY_API_KEY_URL="add-the-alchemy-key-url-here"

// 将此私钥替换为您的 RINKEBY 帐户私钥
// 要从 Metamask 导出您的私钥,请打开 Metamask 并
// 转到 Account Details > Export Private Key
// 请注意永远不要将真实的 Ether 放入测试帐户
RINKEBY_PRIVATE_KEY="add-the-rinkeby-private-key-here"
  • 让我们将合约部署到rinkeby网络。创建一个新文件,或者替换在 scripts 文件夹下默认文件 deploy.js

  • 现在我们将编写一些代码来在deploy.js文件中部署合约。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
const { ethers } = require("hardhat");
require("dotenv").config({ path: ".env" });
const { WHITELIST_CONTRACT_ADDRESS, METADATA_URL } = require("../constants");

async function main() {
// 您在上一个模块中部署的白名单合约的地址
const whitelistContract = WHITELIST_CONTRACT_ADDRESS;
// 我们可以从中提取 Crypto Dev NFT 元数据的 URL
const metadataURL = METADATA_URL;
/*
ethers.js 中的 ContractFactory 是用于部署新智能合约的抽象,
因此这里的 cryptoDevsContract 是我们的 CryptoDevs 合约实例的工厂。
*/
const cryptoDevsContract = await ethers.getContractFactory("CryptoDevs");

// 部署合约
const deployedCryptoDevsContract = await cryptoDevsContract.deploy(
metadataURL,
whitelistContract
);

// 打印部署合约的地址
console.log(
"Crypto Devs Contract Address:",
deployedCryptoDevsContract.address
);
}

// 调用main函数,如果有错误就catch
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
  • 如您所见,deploy.js需要一些常量。让我们在hardhat-tutorial文件夹下创建一个名为constants的文件夹

  • 现在在constants文件夹中添加一个index.js文件,并将以下行添加到文件中。将“address-of-the-whitelist-contract”替换为您在上一教程中部署的白名单合约的地址。对于 Metadata_URL,只需复制已提供的示例。我们将在教程中进一步替换它。

1
2
3
4
5
6
// 您部署的白名单合约
const WHITELIST_CONTRACT_ADDRESS = "address-of-the-whitelist-contract";
// 为 Crypto Dev NFT 提取元数据的 URL
const METADATA_URL = "https://nft-collection-sneh1999.vercel.app/api/";

module.exports = { WHITELIST_CONTRACT_ADDRESS, METADATA_URL };
  • 现在打开 hardhat.config.js 文件,我们将在rinkeby此处添加网络,以便我们可以将合约部署到 rinkeby。用下面给出的行替换hardhat.config.js文件中的所有行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
require("@nomicfoundation/hardhat-toolbox");
require("dotenv").config({ path: ".env" });

const ALCHEMY_API_KEY_URL = process.env.ALCHEMY_API_KEY_URL;

const RINKEBY_PRIVATE_KEY = process.env.RINKEBY_PRIVATE_KEY;

module.exports = {
solidity: "0.8.4",
networks: {
rinkeby: {
url: ALCHEMY_API_KEY_URL,
accounts: [RINKEBY_PRIVATE_KEY],
},
},
};
  • 编译合约,打开一个指向hardhat-tutorial目录的终端并执行这个命令
1
npx hardhat compile
  • 要部署,请打开指向hardhat-tutorial目录的终端并执行此命令
1
npx hardhat run scripts/deploy.js --network rinkeby
  • 将打印在终端上的 Crypto Devs Contract Address 保存在记事本中,您将在教程中进一步使用它。

网站

  • 为了开发网站,我们将使用ReactNext Js.React 是一个用于制作网站的 javascript 框架,Next Js 构建在 React 之上。

  • 首先,您需要创建一个新 next 应用程序。您的文件夹结构应该类似于

1
2
3
- NFT-Collection
- hardhat-tutorial
- my-app
  • 要创建my-app,请在终端指向 NFT-Collection 文件夹并键入
1
npx create-next-app@latest

并按下enter所有问题

  • 现在运行应用程序,在终端中执行这些命令
1
2
cd my-app
npm run dev
  • 现在转到http://localhost:3000,您的应用程序应该正在运行 🤘

  • 现在让我们安装 Web3Modal 库(https://github.com/Web3Modal/web3modal)。Web3Modal 是一个易于使用的库,可帮助开发人员通过简单的可自定义配置在其应用程序中添加对多个提供程序的支持。默认情况下,Web3Modal 库支持注入的提供程序,例如(Metamask、Dapper、Gnosis Safe、Frame、Web3 浏览器等),您还可以轻松配置库以支持 Portis、Fortmatic、Squarelink、Torus、Authereum、D’CENT 钱包和 Arkane。打开指向my-app目录的终端并执行此命令

1
npm install web3modal
  • 在同一个终端也安装ethers.js
1
npm install ethers
  • 在您的公用文件夹中,下载此文件夹及其中的所有图像(下载链接)。确保下载的文件夹的名称是 cryptodevs

  • 现在转到样式文件夹并用以下代码替换Home.modules.css文件的所有内容,这将为您的 dapp 添加一些样式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
.main {
min-height: 90vh;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
font-family: "Courier New", Courier, monospace;
}

.footer {
display: flex;
padding: 2rem 0;
border-top: 1px solid #eaeaea;
justify-content: center;
align-items: center;
}

.image {
width: 70%;
height: 50%;
margin-left: 20%;
}

.title {
font-size: 2rem;
margin: 2rem 0;
}

.description {
line-height: 1;
margin: 2rem 0;
font-size: 1.2rem;
}

.button {
border-radius: 4px;
background-color: blue;
border: none;
color: #ffffff;
font-size: 15px;
padding: 20px;
width: 200px;
cursor: pointer;
margin-bottom: 2%;
}
@media (max-width: 1000px) {
.main {
width: 100%;
flex-direction: column;
justify-content: center;
align-items: center;
}
}
  • 打开 pages 文件夹下的 index.js 文件并粘贴以下代码,代码解释可以在评论中找到。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
import { Contract, providers, utils } from "ethers";
import Head from "next/head";
import React, { useEffect, useRef, useState } from "react";
import Web3Modal from "web3modal";
import { abi, NFT_CONTRACT_ADDRESS } from "../constants";
import styles from "../styles/Home.module.css";

export default function Home() {
// walletConnected 跟踪用户的钱包是否连接
const [walletConnected, setWalletConnected] = useState(false);
// presaleStarted 跟踪预售是否已经开始
const [presaleStarted, setPresaleStarted] = useState(false);
// presaleEnded 跟踪预售是否结束
const [presaleEnded, setPresaleEnded] = useState(false);
// 当我们等待交易被挖掘时,loading 设置为 true
const [loading, setLoading] = useState(false);
// 检查当前连接的 MetaMask 钱包是否是合约的所有者
const [isOwner, setIsOwner] = useState(false);
// tokenIdsMinted 跟踪已铸造的 tokenId 数量
const [tokenIdsMinted, setTokenIdsMinted] = useState("0");
// 创建对 Web3 模态(用于连接到 Metamask)的引用,只要页面打开,它就会持续存在
const web3ModalRef = useRef();

/**
* presaleMint:在预售期间铸造 NFT
*/
const presaleMint = async () => {
try {
// 我们需要一个签名者,因为这是一个“写入”交易。
const signer = await getProviderOrSigner(true);
// 创建一个带有签名者的合约的新实例,它允许更新方法
const whitelistContract = new Contract(
NFT_CONTRACT_ADDRESS,
abi,
signer
);
// 从合约中调用 presaleMint,只有列入白名单的地址才能铸币
const tx = await whitelistContract.presaleMint({
// value 表示一个加密开发者的成本,即“0.01”eth。
// 我们正在使用来自 ethers.js
value: utils.parseEther("0.01"),
});
setLoading(true);
// 等待交易被挖掘
await tx.wait();
setLoading(false);
window.alert("You successfully minted a Crypto Dev!");
} catch (err) {
console.error(err);
}
};

/**
* publicMint: 在预售后铸币 NFT
*/
const publicMint = async () => {
try {
// 我们需要一个签名者,因为这是一个“写”交易。
const signer = await getProviderOrSigner(true);
// 创建一个带有签名者的合约的新实例,它允许更新方法
const whitelistContract = new Contract(
NFT_CONTRACT_ADDRESS,
abi,
signer
);
// 从合约调用铸币厂来铸币加密货币开发者
const tx = await whitelistContract.mint({
// value 表示一个加密货币开发者的成本,即“0.01”eth。
// 我们正在使用来自 ethers.js 的 utils 库将 `0.01` 字符串解析为 ether
value: utils.parseEther("0.01"),
});
setLoading(true);
// 等待交易被挖掘
await tx.wait();
setLoading(false);
window.alert("You successfully minted a Crypto Dev!");
} catch (err) {
console.error(err);
}
};

/*
connectWallet: 连接 MetaMask 钱包
*/
const connectWallet = async () => {
try {
// 从 web3Modal 获取提供者,在我们的例子中是 MetaMask
// 第一次使用时提示用户连接他们的钱包
await getProviderOrSigner();
setWalletConnected(true);
} catch (err) {
console.error(err);
}
};

/**
* startPresale:开始 NFT 集合的预售
*/
const startPresale = async () => {
try {
// 我们需要一个签名者,因为这是一个“写”交易。
const signer = await getProviderOrSigner(true);
// 创建一个带有签名者的合约的新实例,它允许更新方法
const whitelistContract = new Contract(
NFT_CONTRACT_ADDRESS,
abi,
signer
);
// 从合约调用 startPresale
const tx = await whitelistContract.startPresale();
setLoading(true);
// 等待交易被挖掘
await tx.wait();
setLoading(false);
// 设置预售开始为真
await checkIfPresaleStarted();
} catch (err) {
console.error(err);
}
};

/**
* checkIfPresaleStarted:通过查询合约中的 `presaleStarted`
* 变量来检查预售是否已经开始
*/
const checkIfPresaleStarted = async () => {
try {
// 从 web3Modal 获取提供者,在我们的例子中是 MetaMask
// 这里不需要签名者,因为我们只是从区块链中读取状态
const provider = await getProviderOrSigner();
// 我们使用提供者连接到合约,所以我们将只有
// 对合约拥有只读权限
const nftContract = new Contract(NFT_CONTRACT_ADDRESS, abi, provider);
// 从合约中调用 presaleStarted
const _presaleStarted = await nftContract.presaleStarted();
if (!_presaleStarted) {
await getOwner();
}
setPresaleStarted(_presaleStarted);
return _presaleStarted;
} catch (err) {
console.error(err);
return false;
}
};

/**
* checkIfPresaleEnded: 通过查询合约中的 `presaleEnded`
* 变量检查预售是否结束
*/
const checkIfPresaleEnded = async () => {
try {
// 从 web3Modal 获取提供者,在我们的例子中是 MetaMask
// 这里不需要签名者,因为我们只是从区块链中读取状态
const provider = await getProviderOrSigner();
// 我们使用提供者连接到合约,所以我们将只有
// 对合约拥有只读权限
const nftContract = new Contract(NFT_CONTRACT_ADDRESS, abi, provider);
// 从合约中调用 presaleEnded
const _presaleEnded = await nftContract.presaleEnded();
// _presaleEnded 是一个大数字,所以我们使用 lt(小于函数) 而不是 `<`
// Date.now()/1000 返回当前时间(以秒为单位)
// 我们比较 _presaleEnded 时间戳是否小于当前时间
// 这意味着预售已经结束
const hasEnded = _presaleEnded.lt(Math.floor(Date.now() / 1000));
if (hasEnded) {
setPresaleEnded(true);
} else {
setPresaleEnded(false);
}
return hasEnded;
} catch (err) {
console.error(err);
return false;
}
};

/**
* getOwner: 调用合约来获取所有者
*/
const getOwner = async () => {
try {
// 从 web3Modal 获取提供者,在我们的例子中是 MetaMask
// 这里不需要签名者,因为我们只是从区块链中读取状态
const provider = await getProviderOrSigner();
// 我们使用提供者连接到合约,所以我们将只有
// 对合约拥有只读权限
const nftContract = new Contract(NFT_CONTRACT_ADDRESS, abi, provider);
// 从合约中调用 owner 函数
const _owner = await nftContract.owner();
// 我们现在将获取签名者以提取当前连接的 MetaMask 帐户的地址
const signer = await getProviderOrSigner(true);
// 获取与 MetaMask 连接的签名者关联的地址
const address = await signer.getAddress();
if (address.toLowerCase() === _owner.toLowerCase()) {
setIsOwner(true);
}
} catch (err) {
console.error(err.message);
}
};

/**
* getTokenIdsMinted: 获取已铸造的 tokenId 数量
*/
const getTokenIdsMinted = async () => {
try {
// 从 web3Modal 获取提供者,在我们的例子中是 MetaMask
// 这里不需要签名者,因为我们只是从区块链中读取状态
const provider = await getProviderOrSigner();
// 我们使用提供者连接到合约,所以我们将只有
// 对合约拥有只读权限
const nftContract = new Contract(NFT_CONTRACT_ADDRESS, abi, provider);
// 从合约中调用 tokenIds
const _tokenIds = await nftContract.tokenIds();
//_tokenIds 是一个“BigNumber”。我们需要将 Big Number 转换为字符串
setTokenIdsMinted(_tokenIds.toString());
} catch (err) {
console.error(err);
}
};

/**
* 返回代表以太坊 RPC 的 Provider 或 Signer 对象,带有或不带有
* 附加元掩码的签名功能
*
* 需要一个 `Provider` 来与区块链交互 - 读取交易、读取余额、读取状态等
*
* `Signer` 是一种特殊类型的 Provider,用于在需要对区块链进行`write` 交易的情况下,这涉及到连接的帐户
* 需要进行数字签名以授权正在发送的交易。Metamask 公开了一个 Signer API 以允许您的网站
* 使用 Signer 函数向用户请求签名。
*
* @param {*} needSigner - 如果需要签名者则为真,否则默认为假
*/
const getProviderOrSigner = async (needSigner = false) => {
// 连接到 Metamask
// 因为我们存储 `web3Modal` 作为参考,我们需要访问 `current` 值来访问底层对象
const provider = await web3ModalRef.current.connect();
const web3Provider = new providers.Web3Provider(provider);

// 如果用户没有连接到 Rinkeby 网络,让他们知道并抛出错误
const { chainId } = await web3Provider.getNetwork();
if (chainId !== 4) {
window.alert("Change the network to Rinkeby");
throw new Error("Change network to Rinkeby");
}

if (needSigner) {
const signer = web3Provider.getSigner();
return signer;
}
return web3Provider;
};

// useEffects 用于对网站状态的变化做出反应
// 函数调用末尾的数组表示什么状态变化会触发这个效果
// 在这种情况下,只要 `walletConnected` 的值发生变化 - 这个效果就会被称为
useEffect(() => {
// 如果钱包没有连接,则创建一个新的 Web3Modal 实例并连接 MetaMask 钱包
if (!walletConnected) {
// 通过将 Web3Modal 类设置为 `current` 将其分配给引用对象value
// 只要此页面打开,`current` 值就会一直保持
web3ModalRef.current = new Web3Modal({
network: "rinkeby",
providerOptions: {},
disableInjectedProvider: false,
});
connectWallet();

// 检查预售是否已经开始和结束
const _presaleStarted = checkIfPresaleStarted();
if (_presaleStarted) {
checkIfPresaleEnded();
}

getTokenIdsMinted();

// 设置每 5 秒调用一次的时间间隔,以检查预售是否结束
const presaleEndedInterval = setInterval(async function () {
const _presaleStarted = await checkIfPresaleStarted();
if (_presaleStarted) {
const _presaleEnded = await checkIfPresaleEnded();
if (_presaleEnded) {
clearInterval(presaleEndedInterval);
}
}
}, 5 * 1000);

// 设置间隔以获取每 5 秒生成的令牌 Id 的数量
setInterval(async function () {
await getTokenIdsMinted();
}, 5 * 1000);
}
}, [walletConnected]);

/*
renderButton: 根据 dapp 的状态返回一个按钮
*/
const renderButton = () => {
// 如果钱包没有连接,返回一个允许他们连接钱包的按钮
if (!walletConnected) {
return (
<button onClick={connectWallet} className={styles.button}>
Connect your wallet
</button>
);
}

// 如果我们当前正在等待某些东西,返回一个加载按钮
if (loading) {
return <button className={styles.button}>Loading...</button>;
}

// 如果连接的用户是所有者,并且预售还没有开始,允许他们开始预售
if (isOwner && !presaleStarted) {
return (
<button className={styles.button} onClick={startPresale}>
Start Presale!
</button>
);
}

// 如果连接的用户不是所有者但预售还没有开始,告诉他们
if (!presaleStarted) {
return (
<div>
<div className={styles.description}>Presale hasnt started!</div>
</div>
);
}

// 如果预售开始,但尚未结束,允许在预售期间进行铸币
if (presaleStarted && !presaleEnded) {
return (
<div>
<div className={styles.description}>
Presale has started!!! If your address is whitelisted, Mint a
Crypto Dev 🥳
</div>
<button className={styles.button} onClick={presaleMint}>
Presale Mint 🚀
</button>
</div>
);
}

// 如果预售开始并结束,则公开铸币的时间
if (presaleStarted && presaleEnded) {
return (
<button className={styles.button} onClick={publicMint}>
Public Mint 🚀
</button>
);
}
};

return (
<div>
<Head>
<title>Crypto Devs</title>
<meta name="description" content="Whitelist-Dapp" />
<link rel="icon" href="/favicon.ico" />
</Head>
<div className={styles.main}>
<div>
<h1 className={styles.title}>Welcome to Crypto Devs!</h1>
<div className={styles.description}>
Its an NFT collection for developers in Crypto.
</div>
<div className={styles.description}>
{tokenIdsMinted}/20 have been minted
</div>
{renderButton()}
</div>
<div>
<img className={styles.image} src="./cryptodevs/0.svg" />
</div>
</div>

<footer className={styles.footer}>
Made with &#10084; by Crypto Devs
</footer>
</div>
);
}
  • 现在在 my-app 文件夹下创建一个新文件夹并将其命名为constants.

  • 在 constants 文件夹中创建一个文件index.js,然后粘贴以下代码。

    • 替换"addres of your NFT contract"为您部署并保存到记事本的 CryptoDevs 合约的地址。
    • 替换---your abi---为您的 CryptoDevs 合约的 abi。要获取您的合同的 abi,​​ 请转到您的hardhat-tutorial/artifacts/contracts/CryptoDevs.sol文件夹并从您的CryptoDevs.json文件中获取标记在"abi"密钥下的数组。
1
2
export const abi =---your abi---
export const NFT_CONTRACT_ADDRESS = "address of your NFT contract"
  • 现在在指向my-app文件夹的终端中,执行
1
npm run dev

您的 Crypto Devs NFT dapp 现在应该可以正常运行了 🚀


推送到 github

确保在继续之前您已将所有代码推送到 github :)


部署你的 dApp

我们现在将部署您的 dApp,以便每个人都可以看到您的网站,并且您可以与所有 LearnWeb3 DAO 朋友分享它。

  • 转到https://vercel.com/ 并使用您的 GitHub 登录
  • 然后单击New Project按钮,然后选择您的 NFT-Collection 存储库
  • 在配置您的新项目时,Vercel 将允许您自定义您的Root Directory
  • 单击Edit旁边Root Directory并将其设置为my-app
  • 选择框架为Next.js
  • 单击部署

  • 现在,您可以通过转到仪表板、选择您的项目并domain从那里复制来查看您部署的网站!保存domain在记事本上,稍后您将需要它。

在 Opensea 上查看您的收藏

现在让您的收藏在 Opensea 上可用

为了使该集合在 Opensea 上可用,我们需要创建一个元数据端点。该端点将返回给定 NFT 的元数据tokenId

  • 打开您的my-app文件夹并在pages/api文件夹下创建一个名为的新文件[tokenId].js(确保名称也包含括号). 添加括号有助于next js中创建动态路由
  • 将以下行添加到[tokenId].js文件中。在here阅读有关添加next jsAPI 路由的信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export default function handler(req, res) {
// 从查询参数中获取 tokenId
const tokenId = req.query.tokenId;
// 由于所有图片都是在github上上传的,我们可以直接从github中提取图片。
const image_url =
"https://raw.githubusercontent.com/LearnWeb3DAO/NFT-Collection/main/my-app/public/cryptodevs/";
// api 正在为 Crypto Dev 发回元数据
// 为了使我们的集合与 Opensea 兼容,我们需要遵循一些元数据标准
// 当从 api 发回响应时
// 更多信息可以在这里找到:https://docs.opensea.io/docs/metadata-standards
res.status(200).json({
name: "Crypto Dev #" + tokenId,
description: "Crypto Dev is a collection of developers in crypto",
image: image_url + tokenId + ".svg",
});
}
1
npx hardhat run scripts/deploy.js --network rinkeby
  • 将新的 NFT 合约地址保存到记事本中。
  • 打开”my-app/constants”文件夹,在index.js文件中将旧的 NFT 合约地址替换为新的
  • 将所有代码推送到 github,等待 vercel 部署新代码。
  • 在 vercel 部署你的代码后,打开你的网站并铸造一个 NFT
  • 交易成功后,在浏览器中打开此链接,替换your-nft-contract-address为您的 NFT 合约地址(https://testnets.opensea.io/assets/your-nft-contract-address/1)
  • 你的 NFT 现在可以在 Opensea 上使用 🚀 🥳
  • 在 discord 上与所有人分享您的 Opensea 链接 :) 并传播快乐。