Arbitrum Stylus 合约实战 :Rust 实现 ERC721

发布于:2025-06-03 ⋅ 阅读:(18) ⋅ 点赞:(0)

在上一篇中,我们学习了如何在 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


网站公告

今日签到

点亮在社区的每一天
去签到