博客开发NFT功能
作为一位朋克程序员,必须为自己的博客系统安排一款NFT
特别声明
本文档只限于技术交流学习,不涉及任何价值交易,所有内容都在测试环境中部署,不具有任何经济价值。
简介
NFT是一种非同质化代币,看到这个解释理解起来有点困难,通俗的说,NFT可以理解为一种不可以分割的物质,你可以理解为NFT具有原子性,不能再次进行分割(最小单位)。在网络上NFT通常与一张图片联系在一起,这是一种表现形式,你也可以使用一段视频来表示NFT。
本质
NFT 是一种 ERC721标准代币,在理解概念之前,我们先来了解ERC20代币,ERC20代币有以下特性:
- name-代币名称,例如 Decentraland
- symbol-代币标识符,可以理解为缩写:例如:mana
- decimals 精度,我们可以理解为有几位小数,通常为 :18,那么我们可以理解为小数点后面可以精确到第18位,例如:0.000000000000000001
- totalSupply 发行总量,例如:1000000000, 发行10亿
- ....(其它属性和方法不重要,有兴趣可以前往 EIP20提案进行了解)
上述属性中,decimals 是重点内容,我们可以将区块链理解为一个分布式的大账本,所有参与其中的节点(运行区块链提供服务的节点)都维护一份相同的账本,ERC20代币可以理解为一种积分标准,例如我们发行时间海绵博客积分, 账本中将会记录以下内容:
- 李雷 -> 2.5 积分
- 韩梅梅 -> 5.5 积分
- ....
ERC20表示的是某类数字余额的概念,这种余额有精度概念,可以将 1 分割成 0.8 + 0.2 进行转移使用。
ERC721则是另外一种概念,它表示的是一种不可分割的概念,它有以下特性:
- name-代币名称,例如 CryptoPunks
- symbol-代币标识符,可以理解为缩写:例如:punk
- tokenId-uint256类型数字(32位的整数)
- .....
ERC721没有decimals属性,增加了tokenId属性,因此不能表示余额的概念,但是可以表示你对某个事物的所有权,例如,使用TokenId属性与游戏中的某个道具进行绑定,那么TokenId就代表了特定物品,TokenId不会重复,我们要确保TokenId所对应的物品也不能重复。
小结
- ERC20可以理解为余额的概念,可以进行拆封使用
- ERC721可以理解为不可分割物品,每个TokenId对应的物品都是独一无二的,当然你可以按照自己的理解进行使用
博客NFT
回归正题,在博客系统中,将NFT中的TokenId与图片一一对应,我们将发行一万个NFT,每张图片各不相同(艺术细胞有限,我们参考CryptPunks图片进行开发),约定部署在 Rinkeby 环境(ETH的测试环境,主要是没钱,ETH太贵了)中,使用NFT需要开发两个功能:
- NFT 合约(基于Soliditiy),记录谁拥有了该NFT
- 后端使用web3j 连接 ETH Rinkeby 环境进行交易交互
NFT合约开发
目前有多种框架都可以实现快速开发Solidity合约,HardHat和TruffleSuite是两个最热门的框架,提供了完整的开发环境,包括本地运行模拟链,开发、调试、编译、部署等功能,有兴趣的可以前往观看文档了解更多,Truffle发展的较早,接触的较多,在这里使用 Truffle框架进行开发,同时引入了OpenZeppelin Contracts 合约库简化开发
Truffle 工具的安装和使用不在此介绍,如果感兴趣可以在博客留言,看情况写介绍文档(truffle配合npm完全类似于前端项目开发)。
安装完成后使用 truffle init,初始化项目,同时使用引入依赖:@openzeppelin/contracts: ^4.5.0,编写NFT.sol合约:(这里使用了OpenZeppelin的合约库,因此不需要写很多代码)
/// 指定 Solidity开发版本
pragma solidity ^0.8.0;
引入openzeppelin 依赖
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
/// NFT合约,继承了 Ownable(权限控制),ERC721(实现了ERC721标准合约)
contract TimestampSpongeNFT is Ownable, ERC721 {
/// 合约构造器
constructor() ERC721("TimeStamp Sponge NFT", "TSN") {}
/// 批量发布TokenId
function mintNFT(address to, uint256[] calldata tokenIds) external onlyOwner {
for (uint256 i = 0; i < tokenIds.length; i++) {
_mint(to, tokenIds[i]);
}
}
// 发布单TokenId
function mintNFT(address to, uint256 tokenId) external onlyOwner {
_mint(to, tokenId);
}
/// 批量发布TokenId
function mintNFT(address[] calldata tos, uint256[] calldata tokenIds) external onlyOwner {
require(tos.length == tokenIds.length, "ILLEGAL_ARGUMENTS");
for (uint256 i = 0; i < tokenIds.length; i++) {
_mint(tos[i], tokenIds[i]);
}
}
}
部署脚本
在migrations/2_deploy.js中编写部署脚本:
///引入NFT合约
const TimestampSpongeNFT = artifacts.require('TimestampSpongeNFT');
module.exports = async (deployer, network) => {
if (network == 'rinkeby') {
/// 部署NFT
await deployer.deploy(TimestampSpongeNFT);
}
}
部署合约需要消耗Gas,因此需要指定账户信息,在truffle-config.js中配置账户,配置信息如下:
const HDWalletProvider = require('@truffle/hdwallet-provider');
module.exports = {
networks: {
配置
rinkebyeth: {
provider: () => new HDWalletProvider({
/// 这里配置私钥信息
privateKeys: ['xxxxxxxx'],
/// 这里配置rpc地址
providerOrUrl: 'https://rinkeby.infura.io/v3/{infura网站提供的projectId}'
}),
/// 网络id 代表 特定区块链id,这里的4代币 ETH 的测试网络 Rinkeby 区块链
network_id: 4,
gasPrice: 20000000000,
gas: 9000000,
confirmations: 1,
networkCheckTimeout: 20000,
timeoutBlocks: 200,
skipDryRun: true
}
},
// Set default mocha options here, use special reporters etc.
mocha: {
timeout: 100000,
useColors: true
},
// Configure your compilers
compilers: {
solc: {
version: "0.8.11",
settings: {
optimizer: {
enabled: true,
runs: 200
},
}
}
},
plugins: [
'truffle-contract-size',
'truffle-plugin-verify'
],
/// Etherscan 区块浏览器账号,用于 合约代码验证
api_keys: {
etherscan: 'MY_API_KEY',
bscscan: 'MY_API_KEY',
snowtrace: 'MY_API_KEY',
polygonscan: 'MY_API_KEY',
ftmscan: 'MY_API_KEY',
hecoinfo: 'MY_API_KEY',
moonscan: 'MY_API_KEY'
},
};
编译合约
使用npx truffle compile对合约进行编译,日志信息如下:
blog.hzchendou.com:nft 时间博客$ npx truffle compile
Compiling your contracts...
===========================
> Compiling ./contracts/NFT.sol
> Compiling @openzeppelin/contracts/access/Ownable.sol
> Compiling @openzeppelin/contracts/token/ERC721/ERC721.sol
> Compiling @openzeppelin/contracts/token/ERC721/IERC721.sol
> Compiling @openzeppelin/contracts/token/ERC721/IERC721Receiver.sol
> Compiling @openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol
> Compiling @openzeppelin/contracts/utils/Address.sol
> Compiling @openzeppelin/contracts/utils/Context.sol
> Compiling @openzeppelin/contracts/utils/Strings.sol
> Compiling @openzeppelin/contracts/utils/introspection/ERC165.sol
> Compiling @openzeppelin/contracts/utils/introspection/IERC165.sol
> Artifacts written to /repo/时间博客/nft/build/contracts
> Compiled successfully using:
- solc: 0.8.11+commit.d7f03943.Emscripten.clang
部署合约
部署合约之前需要确保你的账户余额不少于0.003 ETH,你可以在Rinkeby水龙头网站领取免费 ETH 地址 (不一定能领取成功,我就被坑了好多次,提交之后没有任何转账信息)
使用npx truffle migrate --network rinkeby可以将编译完成的合约部署到Rinkeby网络中,很遗憾的是我这里一直提示网络问题,因此只能使用另外一种方式进行部署(部署方式有很多种,本质上都是将编译后的代码提交到区块网络中,发起转账交易。创建合约比较特殊,接受地址为空),我们采用MetaMask来完成部署交易,使用发送交易方法:
let params = [{
/// 部署合约的账户
"from": "0xxxxxxxxxxxxxxxxxxxxxx",
/// 在 build/contracts/TimestampSpongeNFT.json文件中的 bytecode 属性
"data": "xxxxxx"
}]
ethereum.request({
/// 发送交易请求
method: 'eth_sendTransaction',
params: params,
/// 部署合约的账户
from: '0xxxxxxxxxxxxxxxxxxxxxx',
})
.then(function (result) {
console.log('result', result)
})
.catch(function (reason) {
console.log('reason', reason)
})
合约部署完成:交易信息
部署完成后需要进行验证才能在浏览器中查看到源码信息,在 Truffle 中引入 truffle-plugin-verify 依赖,使用 命令 npx truffle run verify TimestampSpongeNFT@0xxxxxxxxxxxxxx --network rinkeby进行验证,完成后就可以在浏览器中看到合约源码,本合约地址
后端开发Web3
Web3j是一个RPC客户端,实现了ETH客户端提供的API方法,可以进行远程调用,不建议自行搭建ETH客户端,比较耗费资源。infura网站提供ETH网络服务,有一定的免费额度(注意网络环境,本项目使用测试网络Rinkeby),可以满足个人用户开发使用。在项目中引入依赖:
<dependency>
<groupId>org.web3j</groupId>
<artifactId>core</artifactId>
<version>4.8.7</version>
</dependency>
本项目使用4.8.7版本,支持EIP-1559(优化交易费用提案),注意`com.squareup.okhttp3`的使用版本,如果报错建议使用okhttps:4.9.0, 转账核心方法如下:
public String sendMintTransaction(String address, Long tokenId) throws IOException {
/// 获取账号已经发起的交易数量,这个用于指定这是账号的哪一笔交易,依据nonce可以实现交易信息替换
BigInteger nonce = getNonce();
/// 创建交易信息,address 是获得 NFT 的ETH账户地址,contractAddress是合约地址
RawTransaction transaction = createMintNFTTransaction(address, contractAddress, tokenId, nonce);
///进行交易信息签名
byte[] transactionBytes = TransactionEncoder.signMessage(transaction, credentials);
String hexValue = Numeric.toHexString(transactionBytes);
发送交易信息
EthSendTransaction ethSendTransaction =
web3j.ethSendRawTransaction(hexValue).send();
/// 交易Hash,可以根据Hash在浏览器中查看交易信息
String transactionHash = ethSendTransaction.getTransactionHash();
return transactionHash;
}
private static RawTransaction createMintNFTTransaction(String to, String contractAddress, Long tokenId, BigInteger nonce) {
/// 对调用函数信息进行编码
org.web3j.abi.datatypes.Function function = new org.web3j.abi.datatypes.Function(
FUNC_mintNFT,
Arrays.<Type>asList(new org.web3j.abi.datatypes.Address(to),
new org.web3j.abi.datatypes.generated.Uint256(BigInteger.valueOf(tokenId))),
Collections.<TypeReference<?>>emptyList());
DefaultFunctionEncoder encoder = new DefaultFunctionEncoder();
String data = encoder.encodeFunction(function);
/// 区块链id,4代表的是Rinkeby测试链
long chainId = 4L;
/// 这里是手动设置gas使用上限,稳妥的方式是依据调用函数去查看Gas使用上限
BigInteger gasLimit = BigInteger.valueOf(106230L);
转账金额,这里是NFT转账,不需要转账ETH,设置为 0
BigInteger value = BigInteger.ZERO;
/// maxPriorityFeePerGas ,这个是给矿工的小费,我们设置为 1GETH,稳妥的方式是依据最近100个交易计算
/// maxFeePerGas 这个是最大 Gas价格(矿工小费 + 基础费用,基础费用是算法计算的),
/// maxFeePerGas - maxPriorityFeePerGas - BaseGas 剩余的费用将会进行退还(不需要深入研究,了解即可)
return RawTransaction.createTransaction(chainId, nonce, gasLimit, contractAddress, value, data, maxPriorityFeePerGas, maxFeePerGas);
}
至此完成功能开发,前端界面的开发就不介绍了,这里没有使用MetaMask钱包功能。
博客NFT
博客提供了10000个NFT,地址:地址,功能分为两部分:
- 在网站获取 NFT
- 将网站 NFT 转账到 自己 ETH账户中
领取NFT
点击领取按钮获取NFT(所有的NFT都是代码自动生成确保独一无二,领取需要登录,微信扫码进行登录),然后就可以在我的NFT中查看领取到的NFT信息:
链上转账
完成领取后,并没有在区块链上生成任何转账信息,你需要在点击链上转账按钮,在弹出框里填写你的ETH地址信息,后台会将你领取的NFT转账到你的ETH账户,完成后你就可以在脸上查看到转账信息,嘿嘿,我已经完成我的NFT领取啦, 你也快来试试吧,先到先得噢,转账信息: 交易信息
总结
参考文档
联系方式
技术更新换代速度很快,我们无法在有限时间掌握全部知识,但我们可以在他人的基础上进行快速学习,学习也是枯燥无味的,加入我们学习牛人经验:
点击:加群讨论
更多推荐
所有评论(0)