在上一篇中,我们学习了如何在 stylus 使用 rust 编写 ERC20合约,并且部署到了Arbitrum Sepolia ,今天我们继续学习,如何在 stylus 中使用 rust 实现 ERC721 合约,OK, 直接开干!
关于环境准备,请参考上一章节《Arbitrum Stylus 合约实战 :Rust 实现 ERC20》
1. ERC721 标准
下面是 eip erc-721 协议 官方规定必须实现的接口,erc721 我还会出一期 solidity 的教程,会讲到代码以及如何去定义元数据,如何存储到 ipfs上面,以及eip 协议系列博客,这里我们就不展开讲了:
pragma solidity ^0.4.20;
/// @title ERC-721 Non-Fungible Token Standard
/// @dev See https://eips.ethereum.org/EIPS/eip-721
/// Note: the ERC-165 identifier for this interface is 0x80ac58cd.
interface ERC721 /* is ERC165 */ {
/// @dev This emits when ownership of any NFT changes by any mechanism.
/// This event emits when NFTs are created (`from` == 0) and destroyed
/// (`to` == 0). Exception: during contract creation, any number of NFTs
/// may be created and assigned without emitting Transfer. At the time of
/// any transfer, the approved address for that NFT (if any) is reset to none.
event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId);
/// @dev This emits when the approved address for an NFT is changed or
/// reaffirmed. The zero address indicates there is no approved address.
/// When a Transfer event emits, this also indicates that the approved
/// address for that NFT (if any) is reset to none.
event Approval(address indexed _owner, address indexed _approved, uint256 indexed _tokenId);
/// @dev This emits when an operator is enabled or disabled for an owner.
/// The operator can manage all NFTs of the owner.
event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved);
/// @notice Count all NFTs assigned to an owner
/// @dev NFTs assigned to the zero address are considered invalid, and this
/// function throws for queries about the zero address.
/// @param _owner An address for whom to query the balance
/// @return The number of NFTs owned by `_owner`, possibly zero
function balanceOf(address _owner) external view returns (uint256);
/// @notice Find the owner of an NFT
/// @dev NFTs assigned to zero address are considered invalid, and queries
/// about them do throw.
/// @param _tokenId The identifier for an NFT
/// @return The address of the owner of the NFT
function ownerOf(uint256 _tokenId) external view returns (address);
/// @notice Transfers the ownership of an NFT from one address to another address
/// @dev Throws unless `msg.sender` is the current owner, an authorized
/// operator, or the approved address for this NFT. Throws if `_from` is
/// not the current owner. Throws if `_to` is the zero address. Throws if
/// `_tokenId` is not a valid NFT. When transfer is complete, this function
/// checks if `_to` is a smart contract (code size > 0). If so, it calls
/// `onERC721Received` on `_to` and throws if the return value is not
/// `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`.
/// @param _from The current owner of the NFT
/// @param _to The new owner
/// @param _tokenId The NFT to transfer
/// @param data Additional data with no specified format, sent in call to `_to`
function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes data) external payable;
/// @notice Transfers the ownership of an NFT from one address to another address
/// @dev This works identically to the other function with an extra data parameter,
/// except this function just sets data to "".
/// @param _from The current owner of the NFT
/// @param _to The new owner
/// @param _tokenId The NFT to transfer
function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable;
/// @notice Transfer ownership of an NFT -- THE CALLER IS RESPONSIBLE
/// TO CONFIRM THAT `_to` IS CAPABLE OF RECEIVING NFTS OR ELSE
/// THEY MAY BE PERMANENTLY LOST
/// @dev Throws unless `msg.sender` is the current owner, an authorized
/// operator, or the approved address for this NFT. Throws if `_from` is
/// not the current owner. Throws if `_to` is the zero address. Throws if
/// `_tokenId` is not a valid NFT.
/// @param _from The current owner of the NFT
/// @param _to The new owner
/// @param _tokenId The NFT to transfer
function transferFrom(address _from, address _to, uint256 _tokenId) external payable;
/// @notice Change or reaffirm the approved address for an NFT
/// @dev The zero address indicates there is no approved address.
/// Throws unless `msg.sender` is the current NFT owner, or an authorized
/// operator of the current owner.
/// @param _approved The new approved NFT controller
/// @param _tokenId The NFT to approve
function approve(address _approved, uint256 _tokenId) external payable;
/// @notice Enable or disable approval for a third party ("operator") to manage
/// all of `msg.sender`'s assets
/// @dev Emits the ApprovalForAll event. The contract MUST allow
/// multiple operators per owner.
/// @param _operator Address to add to the set of authorized operators
/// @param _approved True if the operator is approved, false to revoke approval
function setApprovalForAll(address _operator, bool _approved) external;
/// @notice Get the approved address for a single NFT
/// @dev Throws if `_tokenId` is not a valid NFT.
/// @param _tokenId The NFT to find the approved address for
/// @return The approved address for this NFT, or the zero address if there is none
function getApproved(uint256 _tokenId) external view returns (address);
/// @notice Query if an address is an authorized operator for another address
/// @param _owner The address that owns the NFTs
/// @param _operator The address that acts on behalf of the owner
/// @return True if `_operator` is an approved operator for `_owner`, false otherwise
function isApprovedForAll(address _owner, address _operator) external view returns (bool);
}
interface ERC165 {
/// @notice Query if a contract implements an interface
/// @param interfaceID The interface identifier, as specified in ERC-165
/// @dev Interface identification is specified in ERC-165. This function
/// uses less than 30,000 gas.
/// @return `true` if the contract implements `interfaceID` and
/// `interfaceID` is not 0xffffffff, `false` otherwise
function supportsInterface(bytes4 interfaceID) external view returns (bool);
}
2. 创建 stylus 项目
1. 在项目目录下,执行:
cargo stylus new stylus_nft
2. 修改 rust-toolchain.toml 中的版本为 1.81.0,见上图, 修改 Cargo.toml 中的name 为你自定义的name,或者是你的项目名,因为创建项目的时候是拉取的官方的模板,然后main.rs 中的也跟着更改,见下图
3. 在src 目录下新建erc721.rs 文件,并且输入下面内容,重要的行我基本都加上了注释:
use alloc::{string::String, vec, vec::Vec};
use alloy_primitives::{Address, FixedBytes, U256};
use alloy_sol_types::sol;
use core::{borrow::BorrowMut, marker::PhantomData};
use stylus_sdk::{abi::Bytes, evm, msg, prelude::*};
// 定义 ERC-721 所需的参数 trait
pub trait Erc721Params {
// NFT 的名称,常量
const NAME: &'static str;
// NFT 的符号,常量
const SYMBOL: &'static str;
// 获取指定 token_id 的 URI
fn token_uri(token_id: U256) -> String;
}
// 定义 ERC-721 合约的存储结构
sol_storage! {
pub struct Erc721<T: Erc721Params> {
// token_id 到拥有者地址的映射
mapping(uint256 => address) owners;
// 地址到余额的映射
mapping(address => uint256) balances;
// token_id 到授权用户地址的映射
mapping(uint256 => address) token_approvals;
// 拥有者地址到操作者地址的授权映射
mapping(address => mapping(address => bool)) operator_approvals;
// 总供应量
uint256 total_supply;
// 用于支持 Erc721Params 的 PhantomData
PhantomData<T> phantom;
}
}
// 定义事件和 Solidity 错误类型
sol! {
// 转账事件
event Transfer(address indexed from, address indexed to, uint256 indexed token_id);
// 授权事件
event Approval(address indexed owner, address indexed approved, uint256 indexed token_id);
// 批量授权事件
event ApprovalForAll(address indexed owner, address indexed operator, bool approved);
// token_id 未被铸造或已被销毁
error InvalidTokenId(uint256 token_id);
// 指定地址不是 token_id 的拥有者
error NotOwner(address from, uint256 token_id, address real_owner);
// 指定地址无权操作 token_id
error NotApproved(address owner, address spender, uint256 token_id);
// 尝试向零地址转账
error TransferToZero(uint256 token_id);
// 接收者拒绝接收 token_id
error ReceiverRefused(address receiver, uint256 token_id, bytes4 returned);
}
// 定义 ERC-721 错误枚举
#[derive(SolidityError)]
pub enum Erc721Error {
InvalidTokenId(InvalidTokenId),
NotOwner(NotOwner),
NotApproved(NotApproved),
TransferToZero(TransferToZero),
ReceiverRefused(ReceiverRefused),
}
// 定义 IERC721TokenReceiver 接口
sol_interface! {
// 用于调用实现 IERC721TokenReceiver 的合约的 onERC721Received 方法
interface IERC721TokenReceiver {
function onERC721Received(address operator, address from, uint256 token_id, bytes data) external returns(bytes4);
}
}
// 定义 onERC721Received 方法的选择器常量
const ERC721_TOKEN_RECEIVER_ID: u32 = 0x150b7a02;
// 实现 ERC-721 内部方法
impl<T: Erc721Params> Erc721<T> {
// 检查 msg::sender 是否有权操作指定 token
fn require_authorized_to_spend(
&self,
from: Address,
token_id: U256,
) -> Result<(), Erc721Error> {
// 获取 token_id 的拥有者
let owner = self.owner_of(token_id)?;
// 验证 from 是否为拥有者
if from != owner {
return Err(Erc721Error::NotOwner(NotOwner {
from,
token_id,
real_owner: owner,
}));
}
// 如果调用者是拥有者,直接返回
if msg::sender() == owner {
return Ok(());
}
// 检查调用者是否为拥有者的操作者
if self.operator_approvals.getter(owner).get(msg::sender()) {
return Ok(());
}
// 检查调用者是否被授权操作此 token
if msg::sender() == self.token_approvals.get(token_id) {
return Ok(());
}
// 如果无授权,返回错误
Err(Erc721Error::NotApproved(NotApproved {
owner,
spender: msg::sender(),
token_id,
}))
}
// 执行 token 转账操作
pub fn transfer(
&mut self,
token_id: U256,
from: Address,
to: Address,
) -> Result<(), Erc721Error> {
// 获取 token_id 的拥有者
let mut owner = self.owners.setter(token_id);
let previous_owner = owner.get();
// 验证 from 是否为拥有者
if previous_owner != from {
return Err(Erc721Error::NotOwner(NotOwner {
from,
token_id,
real_owner: previous_owner,
}));
}
// 更新 token 的拥有者
owner.set(to);
// 减少 from 的余额
let mut from_balance = self.balances.setter(from);
let balance = from_balance.get() - U256::from(1);
from_balance.set(balance);
// 增加 to 的余额
let mut to_balance = self.balances.setter(to);
let balance = to_balance.get() + U256::from(1);
to_balance.set(balance);
// 清除 token 的授权记录
self.token_approvals.delete(token_id);
// 记录转账事件
evm::log(Transfer { from, to, token_id });
Ok(())
}
// 如果接收者是合约,调用 onERC721Received 方法
fn call_receiver<S: TopLevelStorage>(
storage: &mut S,
token_id: U256,
from: Address,
to: Address,
data: Vec<u8>,
) -> Result<(), Erc721Error> {
// 检查接收者是否为合约
if to.has_code() {
// 创建接收者接口实例
let receiver = IERC721TokenReceiver::new(to);
// 调用 onERC721Received 方法
let received = receiver
.on_erc_721_received(&mut *storage, msg::sender(), from, token_id, data.into())
.map_err(|_e| {
Erc721Error::ReceiverRefused(ReceiverRefused {
receiver: receiver.address,
token_id,
returned: alloy_primitives::FixedBytes(0_u32.to_be_bytes()),
})
})?
.0;
// 验证返回的选择器是否正确
if u32::from_be_bytes(received) != ERC721_TOKEN_RECEIVER_ID {
return Err(Erc721Error::ReceiverRefused(ReceiverRefused {
receiver: receiver.address,
token_id,
returned: alloy_primitives::FixedBytes(received),
}));
}
}
Ok(())
}
// 执行安全转账并调用 onERC721Received
pub fn safe_transfer<S: TopLevelStorage + BorrowMut<Self>>(
storage: &mut S,
token_id: U256,
from: Address,
to: Address,
data: Vec<u8>,
) -> Result<(), Erc721Error> {
// 执行转账
storage.borrow_mut().transfer(token_id, from, to)?;
// 调用接收者检查
Self::call_receiver(storage, token_id, from, to, data)
}
// 铸造新 token 并转账给 to
pub fn mint(&mut self, to: Address) -> Result<(), Erc721Error> {
// 获取当前总供应量作为新 token_id
let new_token_id = self.total_supply.get();
// 增加总供应量
self.total_supply.set(new_token_id + U256::from(1u8));
// 执行转账,从零地址到接收者
self.transfer(new_token_id, Address::default(), to)?;
Ok(())
}
// 销毁指定 token
pub fn burn(&mut self, from: Address, token_id: U256) -> Result<(), Erc721Error> {
// 执行转账到零地址
self.transfer(token_id, from, Address::default())?;
Ok(())
}
}
// 实现 ERC-721 外部方法
#[public]
impl<T: Erc721Params> Erc721<T> {
// 获取 NFT 名称
pub fn name() -> Result<String, Erc721Error> {
Ok(T::NAME.into())
}
// 获取 NFT 符号
pub fn symbol() -> Result<String, Erc721Error> {
Ok(T::SYMBOL.into())
}
// 获取指定 token 的 URI
#[selector(name = "tokenURI")]
pub fn token_uri(&self, token_id: U256) -> Result<String, Erc721Error> {
// 确保 token 存在
self.owner_of(token_id)?;
Ok(T::token_uri(token_id))
}
// 获取指定地址的 NFT 余额
pub fn balance_of(&self, owner: Address) -> Result<U256, Erc721Error> {
Ok(self.balances.get(owner))
}
// 获取指定 token 的拥有者
pub fn owner_of(&self, token_id: U256) -> Result<Address, Erc721Error> {
// 获取 token 的拥有者
let owner = self.owners.get(token_id);
// 如果拥有者是零地址,token 无效
if owner.is_zero() {
return Err(Erc721Error::InvalidTokenId(InvalidTokenId { token_id }));
}
Ok(owner)
}
// 执行带数据的安全转账
#[selector(name = "safeTransferFrom")]
pub fn safe_transfer_from_with_data<S: TopLevelStorage + BorrowMut<Self>>(
storage: &mut S,
from: Address,
to: Address,
token_id: U256,
data: Bytes,
) -> Result<(), Erc721Error> {
// 禁止转账到零地址
if to.is_zero() {
return Err(Erc721Error::TransferToZero(TransferToZero { token_id }));
}
// 检查调用者是否有权限
storage
.borrow_mut()
.require_authorized_to_spend(from, token_id)?;
// 执行安全转账
Self::safe_transfer(storage, token_id, from, to, data.0)
}
// 执行不带数据的安全转账
#[selector(name = "safeTransferFrom")]
pub fn safe_transfer_from<S: TopLevelStorage + BorrowMut<Self>>(
storage: &mut S,
from: Address,
to: Address,
token_id: U256,
) -> Result<(), Erc721Error> {
// 调用带数据的安全转账,数据为空
Self::safe_transfer_from_with_data(storage, from, to, token_id, Bytes(vec![]))
}
// 执行普通转账
pub fn transfer_from(
&mut self,
from: Address,
to: Address,
token_id: U256,
) -> Result<(), Erc721Error> {
// 禁止转账到零地址
if to.is_zero() {
return Err(Erc721Error::TransferToZero(TransferToZero { token_id }));
}
// 检查调用者是否有权限
self.require_authorized_to_spend(from, token_id)?;
// 执行转账
self.transfer(token_id, from, to)?;
Ok(())
}
// 为指定 token 设置授权
pub fn approve(&mut self, approved: Address, token_id: U256) -> Result<(), Erc721Error> {
// 获取 token 的拥有者
let owner = self.owner_of(token_id)?;
// 验证调用者是否有权限
if msg::sender() != owner && !self.operator_approvals.getter(owner).get(msg::sender()) {
return Err(Erc721Error::NotApproved(NotApproved {
owner,
spender: msg::sender(),
token_id,
}));
}
// 设置授权
self.token_approvals.insert(token_id, approved);
// 记录授权事件
evm::log(Approval {
approved,
owner,
token_id,
});
Ok(())
}
// 设置批量授权
pub fn set_approval_for_all(
&mut self,
operator: Address,
approved: bool,
) -> Result<(), Erc721Error> {
// 获取调用者地址
let owner = msg::sender();
// 设置操作者授权
self.operator_approvals
.setter(owner)
.insert(operator, approved);
// 记录批量授权事件
evm::log(ApprovalForAll {
owner,
operator,
approved,
});
Ok(())
}
// 获取指定 token 的授权地址
pub fn get_approved(&mut self, token_id: U256) -> Result<Address, Erc721Error> {
Ok(self.token_approvals.get(token_id))
}
// 检查是否为所有者设置了操作者授权
pub fn is_approved_for_all(
&mut self,
owner: Address,
operator: Address,
) -> Result<bool, Erc721Error> {
Ok(self.operator_approvals.getter(owner).get(operator))
}
// 检查是否支持指定接口
pub fn supports_interface(interface: FixedBytes<4>) -> Result<bool, Erc721Error> {
// 将接口 ID 转换为字节数组
let interface_slice_array: [u8; 4] = interface.as_slice().try_into().unwrap();
// 特殊处理 ERC165 标准中的 0xffffffff
if u32::from_be_bytes(interface_slice_array) == 0xffffffff {
return Ok(false);
}
// 定义支持的接口 ID
const IERC165: u32 = 0x01ffc9a7;
const IERC721: u32 = 0x80ac58cd;
const IERC721_METADATA: u32 = 0x5b5e139f;
// 检查是否支持指定接口
Ok(matches!(
u32::from_be_bytes(interface_slice_array),
IERC165 | IERC721 | IERC721_METADATA
))
}
}
3. 在src 目录下的 lib.rs 文件中输入下面内容,注意,token_uri ,需要自己去 ipfs 上面存储 json 元数据,然后复制链接复制给token_uri:
// 如果未启用 export-abi 特性,仅作为 WASM 运行
#![cfg_attr(not(any(feature = "export-abi", test)), no_main)]
extern crate alloc;
// 引入模块和依赖
mod erc721;
use crate::erc721::{Erc721, Erc721Error, Erc721Params};
use alloy_primitives::{Address, U256};
// 引入 Stylus SDK 和 alloy 基本类型
use stylus_sdk::{msg, prelude::*};
// 定义 NFT 参数结构体
struct StylusNFTParams;
// 实现 Erc721Params trait
impl Erc721Params for StylusNFTParams {
// 定义 NFT 名称常量
const NAME: &'static str = "DOG";
// 定义 NFT 符号常量
const SYMBOL: &'static str = "DOG";
// 生成指定 token_id 的 URI
fn token_uri(token_id: U256) -> String {
format!("{}{}{}", "https://external-magenta-alpaca.myfilebase.com/ipfs/QmY47C6mUFEGPGF5muGTEcSD3MPspCSpT2EGJV8QvQGUnV", token_id, ".json")
}
}
// 定义合约入口点和存储结构
sol_storage! {
#[entrypoint]
struct StylusNFT {
// 允许 erc721 访问 StylusNFT 的存储并调用方法
#[borrow]
Erc721<StylusNFTParams> erc721;
}
}
// 实现 StylusNFT 的外部方法
#[public]
#[inherit(Erc721<StylusNFTParams>)]
impl StylusNFT {
// 铸造 NFT 给调用者
pub fn mint(&mut self) -> Result<(), Erc721Error> {
// 获取调用者地址
let minter = msg::sender();
// 调用 erc721 的 mint 方法
self.erc721.mint(minter)?;
Ok(())
}
// 铸造 NFT 给指定地址
pub fn mint_to(&mut self, to: Address) -> Result<(), Erc721Error> {
// 调用 erc721 的 mint 方法
self.erc721.mint(to)?;
Ok(())
}
// 销毁指定 NFT
pub fn burn(&mut self, token_id: U256) -> Result<(), Erc721Error> {
// 调用 erc721 的 burn 方法,验证调用者是否拥有 token
self.erc721.burn(msg::sender(), token_id)?;
Ok(())
}
// 获取总供应量
pub fn total_supply(&mut self) -> Result<U256, Erc721Error> {
// 获取 erc721 的总供应量
Ok(self.erc721.total_supply.get())
}
}
4. 后续如果有什么编译报错,请参考我的配置文件,因为可能你创建项目的时候,版本更新了:
[package]
name = "stylus_erc721_example"
version = "0.1.11"
edition = "2021"
license = "MIT OR Apache-2.0"
homepage = "https://github.com/OffchainLabs/stylus-hello-world"
repository = "https://github.com/OffchainLabs/stylus-hello-world"
keywords = ["arbitrum", "ethereum", "stylus", "alloy"]
description = "stylus erc721 example"
[dependencies]
alloy-primitives = "=0.8.20"
alloy-sol-types = "=0.8.20"
mini-alloc = "0.9.0"
stylus-sdk = "0.9.0"
hex = "0.4.3"
dotenv = "0.15.0"
[dev-dependencies]
alloy-primitives = { version = "=0.8.20", features = ["sha3-keccak"] }
tokio = { version = "1.12.0", features = ["full"] }
ethers = "2.0"
eyre = "0.6.8"
stylus-sdk = { version = "0.9.0", features = ["stylus-test"] }
[features]
export-abi = ["stylus-sdk/export-abi"]
debug = ["stylus-sdk/debug"]
[[bin]]
name = "stylus_erc721_example"
path = "src/main.rs"
[lib]
crate-type = ["lib", "cdylib"]
[profile.release]
codegen-units = 1
strip = true
lto = true
panic = "abort"
# If you need to reduce the binary size, it is advisable to try other
# optimization levels, such as "s" and "z"
opt-level = 3
3. 编译与链上验证
1. 编译与链上验证,执行 :
cargo stylus check -e https://sepolia-rollup.arbitrum.io/rpc
2. 估算部署合约所需的 gas,依次执行:
export ARB_RPC_URL=https://sepolia-rollup.arbitrum.io/rpc
export PRIVATE_KEY=你的私钥
cargo stylus deploy --endpoint=$ARB_RPC_URL --private-key=$PRIVATE_KEY --estimate-gas
3. 链上部署
执行部署命令:
cargo stylus deploy --endpoint=$ARB_RPC_URL --private-key=$PRIVATE_KEY
可以看到这里我们部署成功,合约地址与 hash:
deployed code at address: 0xb326ed79eb80f475f2aba0df0edeabc54b5a07b8
deployment tx hash: 0xf73b6bff3ed6253d2420226eccb54b26f7e37e5f37c55df60b62f3c1a19e995b
4. 执行交互
我们使用 foundry cast 来进行交互,先把所需的环境变量导出,这样方便操作:
export NFT=你部署的NFT合约地址
export USER=你的钱包地址
1. 检查 NFT 合约的账户余额和所有者
cast call --rpc-url $ARB_RPC_URL $NFT "balanceOf(address) (uint256)" $USER
这个时候刚部署合约,没有余额,所以结果是0
2. 检查对应 NFT 的所有者,我们这里查看 token_id = 0 的所有者
cast call --rpc-url $ARB_RPC_URL $NFT "ownerOf(uint256) (address)" 0
可以看到这里报错了,因为我们还没有mint 任何的NFT, 在代码里面有判断,如果找不到对应的所有者,将返回一个错误
3. 我们来给自己 mint 一个NFT
OK mint 一个 NFT 成功!
4. 我们再来验证一下前面的步骤:
可以看到,余额已经变成了1,并且 token_id = 0 的所有者也输出了,在浏览器也能看到地址已经成功mint了一个NFT。 到这里就完成了一个完整的erc721的合约,从代码到部署,再到验证,如果你也跟我一样走到了最后,那么恭喜你,给自己一点鼓励吧,你是最棒的!
5. 总结
代码仓库:stylus_nft
今天我们学习了如何在 stylus 中编写erc721合约,也学会了如何去部署已经验证,希望大家多练习,多思考,重复巩固知识,思考推动进步,好了,今天就到这里啦,我是红烧6,关注我(主页有v),带你遨游web3