文章目录
原文链接:https://trufflesuite.com/guides/rentable-nft/
编写可rentable(可出租的) NFT 智能合约
概述
在本指南中,我们将涵盖 ERC-4907可出租 NFT 标准是什么,以及我们如何使用Truffle实现一个示例!
欢迎观看我们与来自 Double Protocol 的 Jesse Luong 的在 YouTube 上的直播录像,他是 ERC-4907标准的创建者,对该标准、 GameFi 和元宇宙的影响进行更深入的解释和探索!
ERC-4907是什么?
基于 NFT 的NFT 租赁已经有越来越多的用例——例如,元宇宙中的虚拟土地或游戏中的 NFT 资产。在上一篇文章中,我们了解到 ERC 是应用程序级别的标准,它为合约和 dapps 建立了一个共享接口,以便可靠地相互交互。ERC-4907通过分离使用者user
和所有者owner
的概念来实现 NFT 租赁。这使我们能够识别 NFT 上的授权角色。也就是说,user
有能力使用 NFT,但没有权限出售它。此外,还引入了一个expires
函数,这样user
只有临时访问权限才能使用 NFT。
ERC-4907里有什么?
接口如下:
interface IERC4907 {
// Logged when the user of a NFT is changed or expires is changed
/// @notice Emitted when the `user` of an NFT or the `expires` of the `user` is changed
/// The zero address for user indicates that there is no user address
event UpdateUser(uint256 indexed tokenId, address indexed user, uint64 expires);
/// @notice set the user and expires of a NFT
/// @dev The zero address indicates there is no user
/// Throws if `tokenId` is not valid NFT
/// @param user The new user of the NFT
/// @param expires UNIX timestamp, The new user could use the NFT before expires
function setUser(uint256 tokenId, address user, uint64 expires) external;
/// @notice Get the user address of an NFT
/// @dev The zero address indicates that there is no user or the user is expired
/// @param tokenId The NFT to get the user address for
/// @return The user address for this NFT
function userOf(uint256 tokenId) external view returns(address);
/// @notice Get the user expires of an NFT
/// @dev The zero value indicates that there is no user
/// @param tokenId The NFT to get the user expires for
/// @return The user expires for this NFT
function userExpires(uint256 tokenId) external view returns(uint256);
}
解释:
- userOf(uint256 tokenId) 函数可以作为
pure
或view
函数实现。 - userExpires(uint256 tokenId) 函数可以作为
pure
或view
函数实现。 - setUser(uint256 tokenId, address user, uint64 expires) 函数可以作为
public
或external
函数实现。 - UpdateUser 事件必须在用户地址更改或用户过期时发出。
- 当用0xad092b5c 调用时,SupportsInterface 方法必须返回 true。
让我们写一个 ERC-4907
让我们开始写一个可租用的 NFT 吧!您可以在这里找到完整的代码。我们将导入 Open Zeppelin 的合同,这些合约提供了安全的、事先编写的 ERC 实现,我们的合约可以直接继承!
请注意,我们将不会涉及 ERC-721标准的基础知识。你可以在Infura 博客了解,原文详细介绍了它是什么以及如何实现它。
下载安装所需工具
你需要安装:
- Node.js, v12 或更高版本
- truffle
- ganache UI 或者ganache CLI
创建一个 Infura 帐户和项目
要将你的 DApp 连接到以太网和测试网,你需要一个 Infura 帐户。在这里注册一个帐户。
然后登录,创建一个项目! 我们将其命名为 rentable-nft
,并从下拉列表中选择 Web3API
注册 MetaMask 钱包
要在浏览器中与你的 DApp 交互,你需要一个 MetaMask 钱包。
下载VS Code
您可以随意使用任何您想要的 IDE,但是我们强烈推荐使用 VS Code!您可以使用 Truffle 扩展运行本教程的大部分内容来创建、构建和部署智能契约,而不需要使用 CLI!你可以在这里了解更多。
获取测试网eth
为了部署到公共测试网,你需要一些测试 Eth 来支付你的gas费用!Paradigm 有一个很棒的 MultiFaucet,它可以同时在8个不同的网络上存入资金。
配置项目
Truffle 可以为您的项目搭建脚手架,并添加示例合约和测试。我们将在一个名为 rentable-nft
的文件夹中构建项目。
truffle init rentable-nft
cd rentable-nft
truffle create contract RentablePets
truffle create contract IERC4907
truffle create contract ERC4907
truffle create test TestRentablePets
执行完成后,项目结构如下:
rentable-nft
├── contracts
│ ├── ERC4907.sol
│ ├── IERC4907.sol
│ └── RentablePets.sol
├── migrations
│ └── 1_deploy_contracts.js
├── test
│ └── test_rentable_pets.js
└── truffle-config.js
编写 ERC-4907接口
现在,让我们添加在 EIP 中定义的接口函数。为此,搜索找到IERC4907.sol
。然后,我们只需复制和粘贴 EIP 上指定的内容!如下:
// SPDX-License-Identifier: MIT
pragma solidity >=0.4.22 <0.9.0;
interface IERC4907 {
// Logged when the user of a NFT is changed or expires is changed
/// @notice Emitted when the `user` of an NFT or the `expires` of the `user` is changed
/// The zero address for user indicates that there is no user address
event UpdateUser(uint256 indexed tokenId, address indexed user, uint64 expires);
/// @notice set the user and expires of a NFT
/// @dev The zero address indicates there is no user
/// Throws if `tokenId` is not valid NFT
/// @param user The new user of the NFT
/// @param expires UNIX timestamp, The new user could use the NFT before expires
function setUser(uint256 tokenId, address user, uint64 expires) external;
/// @notice Get the user address of an NFT
/// @dev The zero address indicates that there is no user or the user is expired
/// @param tokenId The NFT to get the user address for
/// @return The user address for this NFT
function userOf(uint256 tokenId) external view returns(address);
/// @notice Get the user expires of an NFT
/// @dev The zero value indicates that there is no user
/// @param tokenId The NFT to get the user expires for
/// @return The user expires for this NFT
function userExpires(uint256 tokenId) external view returns(uint256);
}
一旦你创建了这个文件,你应该不需要再次动它。
编写 ERC-4907智能合约
现在,让我们写一个 ERC-4907智能合约,继承 OpenZeppelin 的 ERC-721URIStorage
合约:
npm i @openzeppelin/contracts@4.8.0
ERC-721的基础知识在这个 Infura 博客中有所介绍。我们选择使用 ERC721URIStorage
,这样就不必使用静态元数据文件来填充 tokenURI。目前为止,我们导入了刚才自己创建的接口和 OpenZeppelin 的 ERC721URIStorage 实现,并让我们的 ERC4907智能合约继承它们的属性,如下:
// SPDX-License-Identifier: MIT
pragma solidity >=0.4.22 <0.9.0;
import "@openzeppelin/contracts/token/ERC721/ERC721URIStorage.sol";
import "./IERC4907.sol";
contract ERC4907 is ERC721URIStorage, IERC4907 {
constructor() public {
}
}
然后,我们修改构造函数,以便在部署契约时接受 NFT 集合名称和符号(symbol )。
contract ERC4907 is ERC721, IERC4907 {
constructor(string memory _name, string memory _symbol) ERC721(_name, _symbol){
}
}
在开始实现 IERC4907中定义的函数之前,让我们设置两个状态变量 UserInfo 和 _ users,以帮助定义和存储用户的概念。
contract ERC4907 is ERC721URIStorage, IERC4907 {
struct UserInfo {
address user; // address of user role
uint64 expires; // unix timestamp, user expires
}
mapping(uint256 => UserInfo) internal _users;
- UserInfo 存储用户的地址和租赁到期日期
- _users 映射NFT的tokenId到user (rentee) (承租人)
最后,让我们开始实现接口函数!
setUser
此函数只能由 NFT 的所有者调用。它允许所有者指定谁将是 NFT 的承租人。用户现在有 NFT 在他们的钱包,但不能执行任何动作,如烧毁或转移。将这个函数添加到 ERC4907.sol 文件中:
/// @notice set the user and expires of a NFT
/// @dev The zero address indicates there is no user
/// Throws if `tokenId` is not valid NFT
/// @param user The new user of the NFT
/// @param expires UNIX timestamp, The new user could use the NFT before expires
function setUser(uint256 tokenId, address user, uint64 expires) public virtual override {
require(_isApprovedOrOwner(msg.sender, tokenId),"ERC721: transfer caller is not owner nor approved");
UserInfo storage info = _users[tokenId];
info.user = user;
info.expires = expires;
emit UpdateUser(tokenId, user, expires);
}
这个函数将用租用者的地址和租用期将过期的块时间戳更新 UserInfo
结构。我们使用从 ERC721继承的函数 _ isApprovedOrOwner 来表明只有所有者有权决定谁可以使用 NFT。最后,我们将发出在 IERC4907中定义的 UpdateUser 事件,以便在设置新用户时传递相关信息。
userOf
接下来,我们希望能够确定谁是 NFT 的当前用户。将 userOf 添加到你的合约中:
/// @notice Get the user address of an NFT
/// @dev The zero address indicates that there is no user or the user is expired
/// @param tokenId The NFT to get the user address for
/// @return The user address for this NFT
function userOf(uint256 tokenId)
public
view
virtual
override
returns (address)
{
if (uint256(_users[tokenId].expires) >= block.timestamp) {
return _users[tokenId].user;
} else {
return address(0);
}
}
该函数接受 tokenId 作为参数,如果该tokenId仍在被租用,则返回用户地址。否则,零地址表示 NFT 没有被租用。
userExpires
添加 userExires 功能,以便 dapps 可以检索特定 NFT 的到期日期信息:
/// @notice Get the user expires of an NFT
/// @dev The zero value indicates that there is no user
/// @param tokenId The NFT to get the user expires for
/// @return The user expires for this NFT
function userExpires(uint256 tokenId) public view virtual override returns(uint256){
return _users[tokenId].expires;
}
如果 tokenId 不存在,那么将返回具有默认值的 UserInfo。在这种情况下,用户地址的默认值是 address (0) ,而 expires (uint64)的默认值是0。
supportsInterface
为了让 dapp 知道我们的 NFT 是否可以租用,它需要能够检查 interfaceId
!为此,重写 EIP-165标准中定义的 SupportsInterface 函数。
/// @dev See {IERC165-supportsInterface}.
function supportsInterface(bytes4 interfaceId)
public
view
virtual
override
returns (bool)
{
return
interfaceId == type(IERC4907).interfaceId ||
super.supportsInterface(interfaceId);
}
_beforeTokenTransfer
这是我们将实现的最后一个函数!当令牌被转移(即所有者更改)或烧毁时,我们也希望删除租赁信息。请注意,这种行为是从 OpenZeppelin 的 ERC721实现继承而来的。我们将覆盖 ERC721中的 _beforeTokenTransfer
,以添加以下功能:
function _beforeTokenTransfer(
address from,
address to,
uint256 tokenId,
uint256 batchSize
) internal virtual override {
super._beforeTokenTransfer(from, to, tokenId, batchSize);
if (from != to && _users[tokenId].user != address(0)) {
delete _users[tokenId];
emit UpdateUser(tokenId, address(0), 0);
}
}
为了从映射中删除 UserInfo,我们希望确保所有权转移,并且存在 UserInfo。一旦验证,我们就可以删除并发出 UserInfo 已更新的事件!
请注意,这取决于您,合约编写者,来决定这是否是您所期望的令牌转移和销毁的行为方式。您可能会选择忽略这一点,并说即使所有权发生变化,租用者仍然保持其用户状态!
最终合约如下:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "./IERC4907.sol";
contract ERC4907 is ERC721URIStorage, IERC4907 {
struct UserInfo {
address user; // address of user role
uint64 expires; // unix timestamp, user expires
}
mapping(uint256 => UserInfo) internal _users;
constructor(string memory name_, string memory symbol_) ERC721(name_, symbol_) {}
/// @notice set the user and expires of a NFT
/// @dev The zero address indicates there is no user
/// Throws if `tokenId` is not valid NFT
/// @param user The new user of the NFT
/// @param expires UNIX timestamp, The new user could use the NFT before expires
function setUser(
uint256 tokenId,
address user,
uint64 expires
) public virtual override {
require(
_isApprovedOrOwner(msg.sender, tokenId),
"ERC721: transfer caller is not owner nor approved"
);
UserInfo storage info = _users[tokenId];
info.user = user;
info.expires = expires;
emit UpdateUser(tokenId, user, expires);
}
/// @notice Get the user address of an NFT
/// @dev The zero address indicates that there is no user or the user is expired
/// @param tokenId The NFT to get the user address for
/// @return The user address for this NFT
function userOf(uint256 tokenId)
public
view
virtual
override
returns (address)
{
if (uint256(_users[tokenId].expires) >= block.timestamp) {
return _users[tokenId].user;
} else {
return address(0);
}
}
/// @notice Get the user expires of an NFT
/// @dev The zero value indicates that there is no user
/// @param tokenId The NFT to get the user expires for
/// @return The user expires for this NFT
function userExpires(uint256 tokenId)
public
view
virtual
override
returns (uint256)
{
return _users[tokenId].expires;
}
/// @dev See {IERC165-supportsInterface}.
function supportsInterface(bytes4 interfaceId)
public
view
virtual
override
returns (bool)
{
return
interfaceId == type(IERC4907).interfaceId ||
super.supportsInterface(interfaceId);
}
function _beforeTokenTransfer(
address from,
address to,
uint256 tokenId
) internal virtual override {
super._beforeTokenTransfer(from, to, tokenId);
if (from != to && _users[tokenId].user != address(0)) {
delete _users[tokenId];
emit UpdateUser(tokenId, address(0), 0);
}
}
}
编写可出租宠物智能合约
最后,我们可以编写一个利用刚刚实现的 ERC4907合约的 NFT。我们遵循与前面相同的 NFT 格式。您可以通过这些来获得更深入的了解。我们暴露burn
功能,以便测试。如果您不希望您的 NFT 可以转移,请不要使用此方法!
合约如下:
// SPDX-License-Identifier: MIT
pragma solidity >=0.4.22 <0.9.0;
import "./ERC4907.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
contract RentablePets is ERC4907 {
using Counters for Counters.Counter;
Counters.Counter private _tokenIds;
constructor() ERC4907("RentablePets", "RP") {}
function mint(string memory _tokenURI) public {
_tokenIds.increment();
uint256 newTokenId = _tokenIds.current();
_safeMint(msg.sender, newTokenId);
_setTokenURI(newTokenId, _tokenURI);
}
function burn(uint256 tokenId) public {
_burn(tokenId);
}
}
启动本地区块链
为了部署和测试我们的智能合约,我们需要修改migrations/1_deploy_contracts.js
:
const RentablePets = artifacts.require("RentablePets");
module.exports = function (deployer) {
deployer.deploy(RentablePets);
};
接下来,让我们启动一个本地 Ganache 实例。有多种方法可以做到这一点: 通过 VS Code 扩展、 Ganache CLI 和 Ganche 图形用户界面。每个Ganche版本都有自己的优点,您可以在这里查看 Ganache v7版本的特性。
在本教程中,我们将使用 GUI。打开它,创建一个工作区,然后点击保存(随时添加您的项目来使用 Ganache UI 的一些功能特性) !
这会在 http://127.0.0.1:7545
创建一个正在运行的 Ganache 实例。
接下来,在 truffle-config. js 中取消注释的开发网络,并将端口号修改为7545以匹配。
development: {
host: "127.0.0.1", // Localhost (default: none)
port: 7545, // Standard Ethereum port (default: none)
network_id: "*", // Any network (default: none)
}
测试你的智能合约
如果您想在不编写完整测试用例的情况下动态测试智能合约,可以通过truffle develop
或truffle console
来完成。点击这里了解更多信息。
出于本教程的目的,我们将继续编写一个 Javascript 测试。请注意,使用 Truffle,您可以选择使用 Javascript、 Typecript 或 Solidy 编写测试。
我们想测试以下功能:
- RentablePets 是 ERC721和 ERC49072
- 除了所有者,其他人不能调用 setUser
- 所有者可以正确地调用 setUser
- burn 将正确删除用户信息
作为测试的一部分,我们希望确保正确地发出事件,as well as our require statement failing correctly。OpenZeppelin 有一些非常好的测试辅助工具,我们将使用它们。下载:
npm install --save-dev @openzeppelin/test-helpers
完整的测试如下:
require("@openzeppelin/test-helpers/configure")({
provider: web3.currentProvider,
singletons: {
abstraction: "truffle",
},
});
const { constants, expectRevert, expectEvent } = require('@openzeppelin/test-helpers');
const RentablePets = artifacts.require("RentablePets");
contract("RentablePets", function (accounts) {
it("should support the ERC721 and ERC4907 standards", async () => {
const rentablePetsInstance = await RentablePets.deployed();
const ERC721InterfaceId = "0x80ac58cd";
const ERC4907InterfaceId = "0xad092b5c";
var isERC721 = await rentablePetsInstance.supportsInterface(ERC721InterfaceId);
var isER4907 = await rentablePetsInstance.supportsInterface(ERC4907InterfaceId);
assert.equal(isERC721, true, "RentablePets is not an ERC721");
assert.equal(isER4907, true, "RentablePets is not an ERC4907");
});
it("should not set UserInfo if not the owner", async () => {
const rentablePetsInstance = await RentablePets.deployed();
const expirationDatePast = 1660252958; // Aug 8 2022
await rentablePetsInstance.mint("fakeURI");
// Failed require in function
await expectRevert(rentablePetsInstance.setUser(1, accounts[1], expirationDatePast, {from: accounts[1]}), "ERC721: transfer caller is not owner nor approved");
// Assert no UserInfo for NFT
var user = await rentablePetsInstance.userOf.call(1);
var date = await rentablePetsInstance.userExpires.call(1);
assert.equal(user, constants.ZERO_ADDRESS, "NFT user is not zero address");
assert.equal(date, 0, "NFT expiration date is not 0");
});
it("should return the correct UserInfo", async () => {
const rentablePetsInstance = await RentablePets.deployed();
const expirationDatePast = 1660252958; // Aug 8 2022
const expirationDateFuture = 4121727755; // Aug 11 2100
await rentablePetsInstance.mint("fakeURI");
await rentablePetsInstance.mint("fakeURI");
// Set and get UserInfo
var expiredTx = await rentablePetsInstance.setUser(2, accounts[1], expirationDatePast)
var unexpiredTx = await rentablePetsInstance.setUser(3, accounts[2], expirationDateFuture)
var expiredNFTUser = await rentablePetsInstance.userOf.call(2);
var expiredNFTDate = await rentablePetsInstance.userExpires.call(2);
var unexpireNFTUser = await rentablePetsInstance.userOf.call(3);
var unexpiredNFTDate = await rentablePetsInstance.userExpires.call(3);
// Assert UserInfo and event transmission
assert.equal(expiredNFTUser, constants.ZERO_ADDRESS, "Expired NFT has wrong user");
assert.equal(expiredNFTDate, expirationDatePast, "Expired NFT has wrong expiration date");
expectEvent(expiredTx, "UpdateUser", { tokenId: "2", user: accounts[1], expires: expirationDatePast.toString()});
assert.equal(unexpireNFTUser, accounts[2], "Expired NFT has wrong user");
assert.equal(unexpiredNFTDate, expirationDateFuture, "Expired NFT has wrong expiration date");
expectEvent(unexpiredTx, "UpdateUser", { tokenId: "3", user: accounts[2], expires: expirationDateFuture.toString()});
// Burn NFT
unexpiredTx = await rentablePetsInstance.burn(3);
// Assert UserInfo was deleted
unexpireNFTUser = await rentablePetsInstance.userOf.call(3);
unexpiredNFTDate = await rentablePetsInstance.userExpires.call(3);
assert.equal(unexpireNFTUser, constants.ZERO_ADDRESS, "NFT user is not zero address");
assert.equal(unexpiredNFTDate, 0, "NFT expiration date is not 0");
expectEvent(unexpiredTx, "UpdateUser", { tokenId: "3", user: constants.ZERO_ADDRESS, expires: "0"});
});
});
这里有一个特殊的点要调用: 为了测试当 msg.sender 不是 owner 时 setUser 是否失败,我们可以通过添加 param 中的额外命令来伪造发送方
rentablePetsInstance.setUser(1, accounts[1], expirationDatePast, {from: accounts[1]})
如果您遇到问题测试,使用 Truffle 调试器是非常有帮助的!
创建一个 NFT 并在您的移动钱包或 OpenSea 中查看它
如果您想为自己创建一个 NFT,并在您的移动 MetaMask 钱包中查看它,那么您需要将您的合约部署到公共测试网或 主网。要做到这一点,你需要从你的 Infura 项目和你的 MetaMask 钱包密钥中获取你的 Infura 项目 API。在文件夹的根目录中,添加一个.env
文件,我们将在其中放入该信息。
警告: 不要公开或提交此文件。我们建议将
.env
添加到.gitignore
文件中。
MNEMONIC="YOUR SECRET KEY"
INFURA_API_KEY="YOUR INFURA_API_KEY"
然后,在 truffle-config.js
的顶部,添加以下代码以获取该信息:
require('dotenv').config();
const mnemonic = process.env["MNEMONIC"];
const infuraApiKey = process.env["INFURA_API_KEY"];
const HDWalletProvider = require('@truffle/hdwallet-provider');
最后,将 Goerli 网络添加到 module.exports
下的 networks
列表中:
goerli: {
provider: () => new HDWalletProvider(mnemonic, `https://goerli.infura.io/v3/${infuraApiKey}`),
network_id: 5, // Goerli's network id
chain_id: 5, // Goerli's chain id
gas: 5500000, // Gas limit used for deploys.
confirmations: 2, // # of confirmations to wait between deployments. (default: 0)
timeoutBlocks: 200, // # of blocks before a deployment times out (minimum/default: 50)
skipDryRun: true // Skip dry run before migrations? (default: false for public nets)
}
最终的 truffle-config. js 如下:
require('dotenv').config();
const mnemonic = process.env["MNEMONIC"];
const infuraApiKey = process.env["INFURA_API_KEY"];
const HDWalletProvider = require('@truffle/hdwallet-provider');
module.exports = {
networks: {
development: {
host: "127.0.0.1", // Localhost (default: none)
port: 7545, // Standard Ethereum port (default: none)
network_id: "*", // Any network (default: none)
},
goerli: {
provider: () => new HDWalletProvider(mnemonic, `https://goerli.infura.io/v3/${infuraApiKey}`),
network_id: 5, // Goerli's network id
chain_id: 5, // Goerli's chain id
gas: 5500000, // Gas limit used for deploys.
confirmations: 2, // # of confirmations to wait between deployments. (default: 0)
timeoutBlocks: 200, // # of blocks before a deployment times out (minimum/default: 50)
skipDryRun: true // Skip dry run before migrations? (default: false for public nets)
}
},
// Set default mocha options here, use special reporters, etc.
mocha: {
// timeout: 100000
},
// Configure your compilers
compilers: {
solc: {
version: "0.8.15", // Fetch exact version from solc-bin (default: truffle's version)
}
},
};
然后,我们需要安装 dotenv
和@truffle/hdwallet-provider
的开发依赖项!最后执行 truffle migrate --network goerli
部署。
npm i --save-dev dotenv
npm i --save-dev @truffle/hdwallet-provider
truffle migrate --network goerli
为了快速地与 goerli 网络交互,我们可以使用 truffle console --network goerli
,并调用适当的合约函数。我们已经将一些元数据pinned到 IPFS 中,以便您将其用作 tokenURI: IPFS://bafybeiffapvkruv2vwtomswzxiaxdgm2dflet2cxmh6t4ixrgaezumbw4。它应该看起来有点像这样:
truffle migrate --network goerli
truffle(goerli)> const contract = await RentablePets.deployed()
undefined
truffle(goerli)> await contract.mintNFT("YOUR ADDRESS", "ipfs://bafybeiffapvkruv2vwtomswqzxiaxdgm2dflet2cxmh6t4ixrgaezumbw4")
如果你想使用你自己的元数据,有很多方法可以做到这一点——使用 Truffle 或 Infura。
要在您的移动钱包上查看您的 NFT,手机打开 MetaMask ,切换到 Goerli 网络,并打开 NFT 选项卡!要在 OpenSea 上查看,您必须部署到 mainnet 或 Polygon。否则,如果你将合约部署到 rinkeby,你可以在 https://testnets.opensea.io/
上查看。注意到合并后 Rinkby 将被弃用。
如果你不想在 Infura 项目中监视你的交易,你也可以通过 Truffle Dashboard 部署,它允许你通过 MetaMask 部署和签署交易——因此永远不会泄露你的私钥!要做到这一点,只需运行:
truffle dashboard
truffle migrate --network dashboard
truffle console --network dashboard
未来扩展
恭喜你!你写了一份可租用的 NFT 合约!寻找一个更深入的指南上传您的元数据到 IPFS!要获得更详细的代码演练,请观看 YouTube 上的直播。在未来版本的 Web3 Unleached 中,我们能够将其集成到一个完整的 DApp 中。也就是说,NFT 租赁市场将使用 ERC-4907可租赁标准和 ERC-2981版税标准。
如果你想讨论这个内容,对你想看到的内容提出建议或者问一些关于这个系列的问题,在这里讨论。如果你想展示你建立了什么以及想加入Unleashed社区,加入我们的Discord!最后,别忘了在 Twitter 上关注我们关于 Truffle 的最新消息。