Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

构建 NFT 市场

本节将所有 Kiosk 相关知识整合,设计一个完整的 NFT 市场。我们将从合约设计到前端集成思路,展示如何构建一个支持上架、购买和版税收取的去中心化 NFT 市场。

市场架构

基于 Kiosk 标准的市场架构:

┌────────────────────────────────────────────┐
│                 前端 dApp                   │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐ │
│  │ 浏览市场  │  │ 上架 NFT │  │ 购买 NFT │ │
│  └────┬─────┘  └────┬─────┘  └────┬─────┘ │
│       │             │             │        │
├───────┼─────────────┼─────────────┼────────┤
│       │        TypeScript SDK     │        │
│       │        KioskClient        │        │
├───────┼─────────────┼─────────────┼────────┤
│       ▼             ▼             ▼        │
│  ┌─────────┐  ┌──────────┐  ┌──────────┐  │
│  │ 索引器   │  │ 卖家     │  │ 买家     │  │
│  │ 查询     │  │ Kiosk    │  │ Kiosk    │  │
│  └─────────┘  └──────────┘  └──────────┘  │
│                     │                      │
│              TransferPolicy                │
│          (版税 + 锁定 + 个人Kiosk)          │
└────────────────────────────────────────────┘

合约设计

NFT 定义

module marketplace::sword;

use std::string::String;
use sui::display;
use sui::package;

public struct Sword has key, store {
    id: UID,
    name: String,
    damage: u64,
    special_effects: vector<String>,
}

public struct SWORD() has drop;

fun init(otw: SWORD, ctx: &mut TxContext) {
    let publisher = package::claim(otw, ctx);

    // 设置 Display
    let mut d = display::new<Sword>(&publisher, ctx);
    d.add(b"name".to_string(), b"{name}".to_string());
    d.add(
        b"image_url".to_string(),
        b"https://mygame.com/swords/{name}.png".to_string(),
    );
    d.add(
        b"description".to_string(),
        b"A sword with {damage} damage".to_string(),
    );
    d.update_version();

    transfer::public_transfer(publisher, ctx.sender());
    transfer::public_transfer(d, ctx.sender());
}

public fun mint(
    name: String,
    damage: u64,
    special_effects: vector<String>,
    ctx: &mut TxContext,
): Sword {
    Sword {
        id: object::new(ctx),
        name,
        damage,
        special_effects,
    }
}

public fun name(self: &Sword): &String { &self.name }
public fun damage(self: &Sword): u64 { self.damage }

TransferPolicy 配置

Sui Framework 只提供 transfer_policy::add_rule 等原语,不包含现成的 sui::royalty_rulesui::kiosk_lock_rule。版税等规则需要自行实现(或依赖 Kiosk 生态包)。下面示例在包内实现一个简单的版税规则并创建 Policy:

// 包内自定义版税规则(基于 transfer_policy::add_rule)
module marketplace::royalty_rule;

use sui::coin::{Self, Coin};
use sui::sui::SUI;
use sui::transfer_policy::{Self as policy, TransferPolicy, TransferPolicyCap, TransferRequest};

const MAX_BP: u16 = 10_000;

public struct Rule has drop {}
public struct Config has store, drop { amount_bp: u16 }

public fun add<T: key + store>(
    policy: &mut TransferPolicy<T>,
    cap: &TransferPolicyCap<T>,
    amount_bp: u16,
) {
    assert!(amount_bp <= MAX_BP, 0);
    policy::add_rule(Rule {}, policy, cap, Config { amount_bp })
}

public fun pay<T: key + store>(
    policy: &mut TransferPolicy<T>,
    request: &mut TransferRequest<T>,
    payment: &mut Coin<SUI>,
    ctx: &mut TxContext,
) {
    let paid = policy::paid(request);
    let config = policy::get_rule(Rule {}, policy);
    let amount = ((paid as u128) * (config.amount_bp as u128) / (MAX_BP as u128)) as u64;
    assert!(coin::value(payment) >= amount, 1);
    let fee = coin::split(payment, amount, ctx);
    policy::add_to_balance(Rule {}, policy, fee);
    policy::add_receipt(Rule {}, request)
}
module marketplace::policy_setup;

use sui::transfer_policy::{Self, TransferPolicy, TransferPolicyCap};
use sui::package::Publisher;
use marketplace::sword::Sword;
use marketplace::royalty_rule;

public fun create_policy_with_royalty(
    publisher: &Publisher,
    royalty_bps: u16,
    _min_royalty: u64,
    ctx: &mut TxContext,
) {
    let (mut policy, cap) = transfer_policy::new<Sword>(publisher, ctx);

    royalty_rule::add(&mut policy, &cap, royalty_bps);

    transfer::public_share_object(policy);
    transfer::public_transfer(cap, ctx.sender());
}

前端集成

初始化 KioskClient

import { SuiGrpcClient } from "@mysten/sui/grpc";
import { KioskClient, Network } from "@mysten/kiosk";

const suiClient = new SuiGrpcClient({
  network: "testnet",
  baseUrl: "https://fullnode.testnet.sui.io:443",
});

const kioskClient = new KioskClient({
  client: suiClient,
  network: Network.TESTNET,
});

创建 Kiosk

import { KioskTransaction } from "@mysten/kiosk";
import { Transaction } from "@mysten/sui/transactions";

async function createUserKiosk(signer: Keypair) {
  const tx = new Transaction();
  const kioskTx = new KioskTransaction({
    transaction: tx,
    kioskClient,
  });

  kioskTx.create();
  kioskTx.finalize();

  const result = await suiClient.signAndExecuteTransaction({
    transaction: tx,
    signer,
  });
  if (result.$kind === 'FailedTransaction') {
    throw new Error(result.FailedTransaction.status.error?.message ?? 'Transaction failed');
  }
  await suiClient.waitForTransaction({ digest: result.Transaction.digest });
  return result;
}

上架 NFT

async function listNFT(
  signer: Keypair,
  kioskCap: KioskOwnerCap,
  swordId: string,
  price: bigint,
) {
  const tx = new Transaction();
  const kioskTx = new KioskTransaction({
    transaction: tx,
    kioskClient,
    kioskCap,
  });

  kioskTx.list({
    itemType: `${PACKAGE_ID}::sword::Sword`,
    itemId: swordId,
    price,
  });

  kioskTx.finalize();

  const result = await suiClient.signAndExecuteTransaction({
    transaction: tx,
    signer,
  });
  if (result.$kind === 'FailedTransaction') {
    throw new Error(result.FailedTransaction.status.error?.message ?? 'Transaction failed');
  }
  await suiClient.waitForTransaction({ digest: result.Transaction.digest });
  return result;
}

购买 NFT

async function purchaseNFT(
  signer: Keypair,
  buyerKioskCap: KioskOwnerCap,
  swordId: string,
  sellerKioskId: string,
  price: bigint,
) {
  const tx = new Transaction();
  const kioskTx = new KioskTransaction({
    transaction: tx,
    kioskClient,
    kioskCap: buyerKioskCap,
  });

  await kioskTx.purchase({
    itemType: `${PACKAGE_ID}::sword::Sword`,
    itemId: swordId,
    price,
    sellerKiosk: sellerKioskId,
  });

  kioskTx.finalize();

  const result = await suiClient.signAndExecuteTransaction({
    transaction: tx,
    signer,
  });
  if (result.$kind === 'FailedTransaction') {
    throw new Error(result.FailedTransaction.status.error?.message ?? 'Transaction failed');
  }
  await suiClient.waitForTransaction({ digest: result.Transaction.digest });
  return result;
}

查询上架 NFT

async function getListedItems(kioskId: string) {
  const { items } = await kioskClient.getKiosk({
    id: kioskId,
    options: {
      withListingPrices: true,
      withKioskFields: true,
    },
  });

  return items
    .filter((item) => item.listing !== undefined)
    .map((item) => ({
      id: item.objectId,
      type: item.type,
      price: item.listing?.price,
    }));
}

提取收益

async function withdrawProfits(signer: Keypair, kioskCap: KioskOwnerCap) {
  const tx = new Transaction();
  const kioskTx = new KioskTransaction({
    transaction: tx,
    kioskClient,
    kioskCap,
  });

  kioskTx.withdraw(tx.object(kioskCap.kioskId));
  kioskTx.finalize();

  const result = await suiClient.signAndExecuteTransaction({
    transaction: tx,
    signer,
  });
  if (result.$kind === 'FailedTransaction') {
    throw new Error(result.FailedTransaction.status.error?.message ?? 'Transaction failed');
  }
  await suiClient.waitForTransaction({ digest: result.Transaction.digest });
  return result;
}

市场功能清单

一个完整的 NFT 市场通常包含:

功能合约层前端层
铸造 NFTMove mint 函数Mint 表单页面
创建 Kioskkiosk::new用户注册时自动创建
上架kiosk::place_and_list价格设定表单
购买kiosk::purchase + Policy 满足购买按钮 + 钱包签名
下架kiosk::delist管理面板
提取收益kiosk::withdraw收益提取按钮
浏览索引器 + RPC列表页 + 详情页
版税TransferPolicy自动收取

小结

  • 基于 Kiosk 的 NFT 市场是去中心化的——每个用户拥有自己的商店
  • 合约层负责 NFT 定义、Display、TransferPolicy 配置
  • 前端通过 TypeScript SDK 的 KioskClientKioskTransaction 交互
  • TransferPolicy 的规则(版税、锁定等)自动在购买过程中执行
  • SDK 提供了自动解析 Policy 并生成满足逻辑的能力,简化开发