web3.py实现NFT合约全流程

NFT(Non-Fungible Token,非同质化代币)是一种基于区块链技术的数字资产,其核心特点是唯一性、不可分割性和可验证性。以下是详细解释:

1. 定义与核心特性

2. 常见应用场景

3. 工作原理

4. 优势与争议

5. 简单类比

想象你拥有一幅名画,但它是数字形式的(如一张JPEG图片)。通过NFT技术,你可以:

总结

NFT是数字世界中的“唯一凭证”,它重新定义了数字资产的所有权与交易方式,为创作者、收藏家和投资者提供了新机遇,但同时也需警惕市场风险与伦理问题。

NFT 核心架构图解

在开始代码之前,我们需要建立正确的心理模型。NFT 的本质不仅仅是一张图片,它是一个 “指向数据的权属证明”


阶段一:元数据 (Metadata) —— NFT 的灵魂

在铸造(Mint)之前,你需要准备好 NFT 的内容。为了去中心化,我们通常不把图片放在中心化服务器(AWS/阿里云),而是放在 IPFS

1. 标准 Metadata 格式 (JSON)

这是 OpenSea 和主流市场通用的标准:

{
  "name": "CyberPunk #2077",
  "description": "这是一个来自未来的赛博朋克战士。",
  "image": "ipfs://QmYourImageHash...", 
  "attributes": [
    { "trait_type": "Background", "value": "Neon City" },
    { "trait_type": "Weapon", "value": "Katana" },
    { "trait_type": "Level", "value": 5 }
  ]
}

💡 关键点:合约里存的只是上面这个 JSON 文件的链接(TokenURI),而不是 JSON 内容本身。


阶段二:Mint (铸造) —— 资产诞生

铸造是将数据写入区块链的过程。我们将使用 Solidity 和 OpenZeppelin 库(行业安全标准)。

Solidity 合约示例

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Counters.sol";

contract ProNFT is ERC721URIStorage, Ownable {
    using Counters for Counters.Counter;
    Counters.Counter private _tokenIds;

    constructor() ERC721("ProfessionalNFT", "PNFT") {}

    // 铸造函数
    function mintNFT(address recipient, string memory tokenURI)
        public
        onlyOwner
        returns (uint256)
    {
        _tokenIds.increment();
        uint256 newItemId = _tokenIds.current();

        // 1. 铸造给指定地址
        _safeMint(recipient, newItemId);
        
        // 2. 设置元数据链接
        _setTokenURI(newItemId, tokenURI);

        return newItemId;
    }
}

阶段三:Approve (授权) —— 交易的前提

这是新手最容易困惑的地方。
为什么卖 NFT 时需要先“授权”?

因为智能合约(如 OpenSea 的合约)不能直接拿走你钱包里的 NFT。你必须先执行 approve 操作,告诉 NFT 合约:“我允许 OpenSea 支配我的这个 Token”。

两种授权方式:

  1. 单个授权 (approve): 只允许操作特定的 Token ID。
  2. 全员授权 (setApprovalForAll): 允许操作该系列下你拥有的所有 NFT(省 Gas,OpenSea 常用此法)。

Solidity 接口定义

// 允许 operator 地址挪动你的 tokenId
function approve(address operator, uint256 tokenId) external;

// 允许 operator 挪动你所有的 NFT
function setApprovalForAll(address operator, bool _approved) external;

阶段四:Transfer (转移) —— 所有权变更

转移有两种方式,但在开发中强烈建议只使用 safeTransferFrom

# 假设这是从 Alice 转给 Bob
tx = contract.functions.safeTransferFrom(
    alice_address, # From
    bob_address,   # To
    1              # Token ID
).transact({"from": alice_address})

阶段五:Marketplace (交易) —— 买卖逻辑

NFT 本身没有“价格”属性。交易功能是由市场合约(Marketplace Contract)实现的。

简易市场交易逻辑(原子化交易):

  1. Listing (挂单): 卖家授权市场合约,并设定价格(存储在市场合约的数据结构中)。
  2. Buying (购买): 买家调用市场合约的 buy 函数并附带 ETH。
  3. Settlement (结算): 市场合约同时做两件事:
    • 把 ETH 转给卖家。
    • 把 NFT 从卖家转给买家(利用之前的授权)。

核心 Solidity 代码片段

// 这是一个简化的市场购买函数
function buyNFT(address nftContract, uint256 tokenId) external payable {
    // 1. 获取上架信息
    Listing memory item = listings[nftContract][tokenId];
    require(item.price > 0, "Not for sale");
    require(msg.value >= item.price, "Insufficient funds");

    // 2. 结算资金 (买家 -> 卖家)
    payable(item.seller).transfer(item.price);

    // 3. 转移 NFT (卖家 -> 买家)
    // 注意:这里市场合约调用 NFT 合约,将 NFT 从 Seller 账户拉取并给 Buyer
    IERC721(nftContract).safeTransferFrom(item.seller, msg.sender, tokenId);
    
    // 4. 清除上架状态
    delete listings[nftContract][tokenId];
}

阶段六:Swap (交换) —— 以物易物

Swap 是指 NFT 换 NFT,或者 NFT 换 ERC20 Token。去中心化交换的核心是互信。我们不需要互信,我们需要一个Swap 合约作为中介。

流程:

  1. User A 授权 Swap 合约操作 NFT A。
  2. User B 授权 Swap 合约操作 NFT B。
  3. 任意一方调用 swap 函数,合约同时执行两个 transferFrom
function swap(
    address nftA_Contract, uint256 idA, address ownerA,
    address nftB_Contract, uint256 idB, address ownerB
) external {
    // 这是一个原子操作,要么全成功,要么全失败
    IERC721(nftA_Contract).transferFrom(ownerA, ownerB, idA);
    IERC721(nftB_Contract).transferFrom(ownerB, ownerA, idB);
}

实战演练:完整的 Python 交互脚本

先部署一个 MyNFT 合约。使用 Python (web3.py) 走完铸造 -> 授权 -> 转移全流程的代码。

1. 环境准备

你需要安装 web3 库:
pip install web3

from web3 import Web3

# 1. 连接区块链 (这里使用本地test节点)
w3 = Web3(Web3.EthereumTesterProvider())

if not w3.is_connected():
    raise Exception("无法连接到区块链节点")

# 模拟的三个用户地址 (使用测试节点的前三个账户)
admin = w3.eth.accounts[0] # 铸造者
alice = w3.eth.accounts[1] # 主要持有人
bob = w3.eth.accounts[2]   # 市场/操作员

还需要一个NFT合约,代码如下:

// SPDX-License-Identifier: MIT
// Compatible with OpenZeppelin Contracts ^5.5.0
pragma solidity ^0.8.27;

import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import {ERC721Burnable} from "@openzeppelin/contracts/token/ERC721/extensions/ERC721Burnable.sol";
import {ERC721URIStorage} from "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";

contract MyNFT is ERC721, ERC721URIStorage, ERC721Burnable, Ownable {
    uint256 private _nextTokenId;

    constructor(address initialOwner)
        ERC721("MyNFT", "MT")
        Ownable(initialOwner)
    {}

    function mint(address to, string memory uri)
        public
        onlyOwner
        returns (uint256)
    {
        uint256 tokenId = _nextTokenId++;
        _safeMint(to, tokenId);
        _setTokenURI(tokenId, uri);
        return tokenId;
    }

    // The following functions are overrides required by Solidity.

    function tokenURI(uint256 tokenId)
        public
        view
        override(ERC721, ERC721URIStorage)
        returns (string memory)
    {
        return super.tokenURI(tokenId);
    }

    function supportsInterface(bytes4 interfaceId)
        public
        view
        override(ERC721, ERC721URIStorage)
        returns (bool)
    {
        return super.supportsInterface(interfaceId);
    }
}
# 先部署合约,然后获取合约地址和ABI
from solcx import install_solc, set_solc_version
from solcx import compile_files

install_solc(version='0.8.30')
set_solc_version('0.8.30')

compiled_sol = compile_files([
    "contracts/MyNFT.sol"
], import_remappings=["@openzeppelin=openzeppelin-contracts"])
contract_id, contract_interface = compiled_sol.popitem()
# get bytecode / bin
bytecode = contract_interface['bin']
# get abi
abi = contract_interface['abi']

MyNFTContract = w3.eth.contract(abi=abi, bytecode=bytecode)
tx_hash = MyNFTContract.constructor(admin).transact({'from': admin})
tx_receipt = w3.eth.wait_for_transaction_receipt(tx_hash)
nft_contract = w3.eth.contract(address=tx_receipt.contractAddress, abi=abi)
import base64
import json

print("\n--- NFT ERC721 全流程模拟开始 ---")
print(f"Admin (铸造者): {admin}")
print(f"Alice (持有人):  {alice}")
print(f"Bob (操作员):    {bob}\n")

# ----------------------------------------------------
# 辅助函数
# ----------------------------------------------------

def encode_metadata_json(metadata_dict):
    """将 NFT Metadata 转成 base64 data URI"""
    json_str = json.dumps(metadata_dict)
    encoded = base64.b64encode(json_str.encode()).decode()
    return f"data:application/json;base64,{encoded}"

def transact_and_wait(func, from_addr):
    """发送交易并等待确认"""
    tx_hash = func.transact({"from": from_addr})
    return w3.eth.wait_for_transaction_receipt(tx_hash)

def check_status(step_name, token_id=None):
    """打印当前状态的辅助函数"""
    print(f"\n--- {step_name} ---")
    print(f"Alice Balance: {nft_contract.functions.balanceOf(alice).call()}")
    print(f"Bob Balance:   {nft_contract.functions.balanceOf(bob).call()}")

    if token_id is not None:
        try:
            owner = nft_contract.functions.ownerOf(token_id).call()
            uri = nft_contract.functions.tokenURI(token_id).call()
            approved = nft_contract.functions.getApproved(token_id).call()
            print(f"Token {token_id} Owner:  {owner}")
            print(f"Token {token_id} URI:    {uri}")
            print(f"Token {token_id} Approved: {approved}")
        except:
            print(f"Token {token_id} 不存在或已被销毁。")

    print(f"Alice 授权 Bob: {nft_contract.functions.isApprovedForAll(alice, bob).call()}")

# ----------------------------------------------------
# 步骤 1: 检查 name / symbol
# ----------------------------------------------------
print("name:", nft_contract.functions.name().call())
print("symbol:", nft_contract.functions.symbol().call())

# ----------------------------------------------------
# 步骤 2: Admin 铸造 Token 1 给 Alice
# ----------------------------------------------------
nft_metadata = {
    "name": "CyberPunk #2077",
    "description": "来自未来的赛博朋克战士装备。",
    "image": "ipfs://QmImageHash...",
    "attributes": [
        {"trait_type": "Background", "value": "Neon City"},
        {"trait_type": "Weapon",     "value": "Katana"},
        {"trait_type": "Level",      "value": 5}
    ]
}

token_uri = encode_metadata_json(nft_metadata)

transact_and_wait(
    nft_contract.functions.mint(alice, token_uri),
    admin
)
check_status("2. Admin 铸造 Token ID 1 给 Alice", 0)  # tokenId = 0 (从 0 开始)

# ----------------------------------------------------
# 步骤 3: Alice 授权 Bob 操作 Token ID 0
# ----------------------------------------------------
transact_and_wait(
    nft_contract.functions.approve(bob, 0),
    alice
)
check_status("3. Alice 授权 Bob 操作 Token 0", 0)

# ----------------------------------------------------
# 步骤 4: Bob 转移 Token 0 给自己
# ----------------------------------------------------
transact_and_wait(
    nft_contract.functions.transferFrom(alice, bob, 0),
    bob
)
check_status("4. Bob 将 Token 0 转给自己", 0)

# ----------------------------------------------------
# 步骤 5: Bob 授权 Admin 为全局操作员
# ----------------------------------------------------
transact_and_wait(
    nft_contract.functions.setApprovalForAll(admin, True),
    bob
)
check_status("5. Bob 设置 Admin 为全局 Operator", 0)

# ----------------------------------------------------
# 步骤 6: Admin 再铸造 Token 1 给 Bob
# ----------------------------------------------------
transact_and_wait(
    nft_contract.functions.mint(bob, token_uri),
    admin
)
check_status("6. Admin 铸造 Token 1 给 Bob", 1)

# ----------------------------------------------------
# 步骤 7: Admin (operator) 安全转移 Token 1 给 Alice
# ----------------------------------------------------
transact_and_wait(
    nft_contract.functions.safeTransferFrom(bob, alice, 1),
    admin
)
check_status("7. Operator Admin 将 Token 1 转给 Alice", 1)

# ----------------------------------------------------
# 步骤 8: Alice 销毁 Token 1
# ----------------------------------------------------
transact_and_wait(
    nft_contract.functions.burn(1),
    alice
)
check_status("8. Alice 销毁 Token 1")

# ----------------------------------------------------
# 步骤 9: Bob 撤销 Admin 的操作员权限
# ----------------------------------------------------
transact_and_wait(
    nft_contract.functions.setApprovalForAll(admin, False),
    bob
)
check_status("9. Bob 撤销 Admin 的 Operator 权限", 0)

print("\n--- 完整 NFT 流程测试结束 ---")

--- NFT ERC721 全流程模拟开始 ---
Admin (铸造者): 0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf
Alice (持有人): 0x2B5AD5c4795c026514f8317c7a215E218DcCD6cF
Bob (操作员): 0x6813Eb9362372EEF6200f3b1dbC3f819671cBA69

name: MyNFT
symbol: MT

--- 2. Admin 铸造 Token ID 1 给 Alice ---
Alice Balance: 1
Bob Balance: 0
Token 0 Owner: 0x2B5AD5c4795c026514f8317c7a215E218DcCD6cF
Token 0 URI: data:application/json;base64,eyJuYW1lIjogIkN5YmVyUHVuayAjMjA3NyIsICJkZXNjcmlwdGlvbiI6ICJcdTY3NjVcdTgxZWFcdTY3MmFcdTY3NjVcdTc2ODRcdThkNWJcdTUzNWFcdTY3MGJcdTUxNGJcdTYyMThcdTU4ZWJcdTg4YzVcdTU5MDdcdTMwMDIiLCAiaW1hZ2UiOiAiaXBmczovL1FtSW1hZ2VIYXNoLi4uIiwgImF0dHJpYnV0ZXMiOiBbeyJ0cmFpdF90eXBlIjogIkJhY2tncm91bmQiLCAidmFsdWUiOiAiTmVvbiBDaXR5In0sIHsidHJhaXRfdHlwZSI6ICJXZWFwb24iLCAidmFsdWUiOiAiS2F0YW5hIn0sIHsidHJhaXRfdHlwZSI6ICJMZXZlbCIsICJ2YWx1ZSI6IDV9XX0=
Token 0 Approved: 0x0000000000000000000000000000000000000000
Alice 授权 Bob: False

--- 3. Alice 授权 Bob 操作 Token 0 ---
Alice Balance: 1
Bob Balance: 0
Token 0 Owner: 0x2B5AD5c4795c026514f8317c7a215E218DcCD6cF
Token 0 URI: data:application/json;base64,eyJuYW1lIjogIkN5YmVyUHVuayAjMjA3NyIsICJkZXNjcmlwdGlvbiI6ICJcdTY3NjVcdTgxZWFcdTY3MmFcdTY3NjVcdTc2ODRcdThkNWJcdTUzNWFcdTY3MGJcdTUxNGJcdTYyMThcdTU4ZWJcdTg4YzVcdTU5MDdcdTMwMDIiLCAiaW1hZ2UiOiAiaXBmczovL1FtSW1hZ2VIYXNoLi4uIiwgImF0dHJpYnV0ZXMiOiBbeyJ0cmFpdF90eXBlIjogIkJhY2tncm91bmQiLCAidmFsdWUiOiAiTmVvbiBDaXR5In0sIHsidHJhaXRfdHlwZSI6ICJXZWFwb24iLCAidmFsdWUiOiAiS2F0YW5hIn0sIHsidHJhaXRfdHlwZSI6ICJMZXZlbCIsICJ2YWx1ZSI6IDV9XX0=
Token 0 Approved: 0x6813Eb9362372EEF6200f3b1dbC3f819671cBA69
Alice 授权 Bob: False

--- 4. Bob 将 Token 0 转给自己 ---
Alice Balance: 0
Bob Balance: 1
Token 0 Owner: 0x6813Eb9362372EEF6200f3b1dbC3f819671cBA69
Token 0 URI: data:application/json;base64,eyJuYW1lIjogIkN5YmVyUHVuayAjMjA3NyIsICJkZXNjcmlwdGlvbiI6ICJcdTY3NjVcdTgxZWFcdTY3MmFcdTY3NjVcdTc2ODRcdThkNWJcdTUzNWFcdTY3MGJcdTUxNGJcdTYyMThcdTU4ZWJcdTg4YzVcdTU5MDdcdTMwMDIiLCAiaW1hZ2UiOiAiaXBmczovL1FtSW1hZ2VIYXNoLi4uIiwgImF0dHJpYnV0ZXMiOiBbeyJ0cmFpdF90eXBlIjogIkJhY2tncm91bmQiLCAidmFsdWUiOiAiTmVvbiBDaXR5In0sIHsidHJhaXRfdHlwZSI6ICJXZWFwb24iLCAidmFsdWUiOiAiS2F0YW5hIn0sIHsidHJhaXRfdHlwZSI6ICJMZXZlbCIsICJ2YWx1ZSI6IDV9XX0=
Token 0 Approved: 0x0000000000000000000000000000000000000000
Alice 授权 Bob: False

--- 5. Bob 设置 Admin 为全局 Operator ---
Alice Balance: 0
Bob Balance: 1
Token 0 Owner: 0x6813Eb9362372EEF6200f3b1dbC3f819671cBA69
Token 0 URI: data:application/json;base64,eyJuYW1lIjogIkN5YmVyUHVuayAjMjA3NyIsICJkZXNjcmlwdGlvbiI6ICJcdTY3NjVcdTgxZWFcdTY3MmFcdTY3NjVcdTc2ODRcdThkNWJcdTUzNWFcdTY3MGJcdTUxNGJcdTYyMThcdTU4ZWJcdTg4YzVcdTU5MDdcdTMwMDIiLCAiaW1hZ2UiOiAiaXBmczovL1FtSW1hZ2VIYXNoLi4uIiwgImF0dHJpYnV0ZXMiOiBbeyJ0cmFpdF90eXBlIjogIkJhY2tncm91bmQiLCAidmFsdWUiOiAiTmVvbiBDaXR5In0sIHsidHJhaXRfdHlwZSI6ICJXZWFwb24iLCAidmFsdWUiOiAiS2F0YW5hIn0sIHsidHJhaXRfdHlwZSI6ICJMZXZlbCIsICJ2YWx1ZSI6IDV9XX0=
Token 0 Approved: 0x0000000000000000000000000000000000000000
Alice 授权 Bob: False

--- 6. Admin 铸造 Token 1 给 Bob ---
Alice Balance: 0
Bob Balance: 2
Token 1 Owner: 0x6813Eb9362372EEF6200f3b1dbC3f819671cBA69
Token 1 URI: data:application/json;base64,eyJuYW1lIjogIkN5YmVyUHVuayAjMjA3NyIsICJkZXNjcmlwdGlvbiI6ICJcdTY3NjVcdTgxZWFcdTY3MmFcdTY3NjVcdTc2ODRcdThkNWJcdTUzNWFcdTY3MGJcdTUxNGJcdTYyMThcdTU4ZWJcdTg4YzVcdTU5MDdcdTMwMDIiLCAiaW1hZ2UiOiAiaXBmczovL1FtSW1hZ2VIYXNoLi4uIiwgImF0dHJpYnV0ZXMiOiBbeyJ0cmFpdF90eXBlIjogIkJhY2tncm91bmQiLCAidmFsdWUiOiAiTmVvbiBDaXR5In0sIHsidHJhaXRfdHlwZSI6ICJXZWFwb24iLCAidmFsdWUiOiAiS2F0YW5hIn0sIHsidHJhaXRfdHlwZSI6ICJMZXZlbCIsICJ2YWx1ZSI6IDV9XX0=
Token 1 Approved: 0x0000000000000000000000000000000000000000
Alice 授权 Bob: False

--- 7. Operator Admin 将 Token 1 转给 Alice ---
Alice Balance: 1
Bob Balance: 1
Token 1 Owner: 0x2B5AD5c4795c026514f8317c7a215E218DcCD6cF
Token 1 URI: data:application/json;base64,eyJuYW1lIjogIkN5YmVyUHVuayAjMjA3NyIsICJkZXNjcmlwdGlvbiI6ICJcdTY3NjVcdTgxZWFcdTY3MmFcdTY3NjVcdTc2ODRcdThkNWJcdTUzNWFcdTY3MGJcdTUxNGJcdTYyMThcdTU4ZWJcdTg4YzVcdTU5MDdcdTMwMDIiLCAiaW1hZ2UiOiAiaXBmczovL1FtSW1hZ2VIYXNoLi4uIiwgImF0dHJpYnV0ZXMiOiBbeyJ0cmFpdF90eXBlIjogIkJhY2tncm91bmQiLCAidmFsdWUiOiAiTmVvbiBDaXR5In0sIHsidHJhaXRfdHlwZSI6ICJXZWFwb24iLCAidmFsdWUiOiAiS2F0YW5hIn0sIHsidHJhaXRfdHlwZSI6ICJMZXZlbCIsICJ2YWx1ZSI6IDV9XX0=
Token 1 Approved: 0x0000000000000000000000000000000000000000
Alice 授权 Bob: False

--- 8. Alice 销毁 Token 1 ---
Alice Balance: 0
Bob Balance: 1
Alice 授权 Bob: False

--- 9. Bob 撤销 Admin 的 Operator 权限 ---
Alice Balance: 0
Bob Balance: 1
Token 0 Owner: 0x6813Eb9362372EEF6200f3b1dbC3f819671cBA69
Token 0 URI: data:application/json;base64,eyJuYW1lIjogIkN5YmVyUHVuayAjMjA3NyIsICJkZXNjcmlwdGlvbiI6ICJcdTY3NjVcdTgxZWFcdTY3MmFcdTY3NjVcdTc2ODRcdThkNWJcdTUzNWFcdTY3MGJcdTUxNGJcdTYyMThcdTU4ZWJcdTg4YzVcdTU5MDdcdTMwMDIiLCAiaW1hZ2UiOiAiaXBmczovL1FtSW1hZ2VIYXNoLi4uIiwgImF0dHJpYnV0ZXMiOiBbeyJ0cmFpdF90eXBlIjogIkJhY2tncm91bmQiLCAidmFsdWUiOiAiTmVvbiBDaXR5In0sIHsidHJhaXRfdHlwZSI6ICJXZWFwb24iLCAidmFsdWUiOiAiS2F0YW5hIn0sIHsidHJhaXRfdHlwZSI6ICJMZXZlbCIsICJ2YWx1ZSI6IDV9XX0=
Token 0 Approved: 0x0000000000000000000000000000000000000000
Alice 授权 Bob: False

--- 完整 NFT 流程测试结束 ---

你现在已经有了一个完整的 NFT 交易流程

包含:

步骤 功能
1 查看 name / symbol
2 mint(内置 JSON metadata → base64)
3 approve(单授权)
4 transferFrom(被授权者转移)
5 setApprovalForAll(市场授权)
6 再次 mint
7 safeTransferFrom(操作员转移)
8 burn(销毁)
9 revoke operator(撤销授权)

补充:meta data通常是用 IPFS(nft.storage / pinata)或去中心化方案进行存储的,而不是直接用 base64 存在链上,这里为了简化演示才这么做的。IPFS介绍可以参考另一篇博客[]