Skip to main content
ArcBlock Community

使用 NFT 作为 Dapp 配置的实践指南

shenxiuqiang
Support
discussiondocstutorial

image.png

基于 ArcBlock NFT 的去中心化配置管理模式。

背景帖子:

目录

  • 概述
  • 为什么使用 NFT 存储配置
  • 设计模式与架构
  • 数据结构设计
  • 技术实现
  • 使用指南
  • 最佳实践
  • GLofter 案例:Hub Protocol Config

概述

什么是 NFT 配置模式

NFT 配置模式是一种将 Dapp(去中心化应用)的配置信息存储在链上 NFT(非同质化代币)中的设计模式。通过将配置作为链上资产,实现了配置的透明化、可验证性和可追溯性。

核心优势

与传统配置管理方式相比,NFT 配置模式的核心优势在于:所有 Dapp 和节点从同一个链上 NFT 读取配置,确保了配置的统一性和公平性,所有参与者使用相同的协议参数;配置变更需要通过链上交易完成,虽然 NFT 拥有者可以修改配置,但所有变更都公开透明,任何人都可以查询和验证配置的真实性;所有配置变更都永久记录在区块链上,保证了可追溯性。

适用场景

NFT 配置模式特别适用于需要所有参与方使用统一配置的场景。例如,协议参数配置、治理规则配置、定价和费率规则配置、权限和等级配置、功能开关配置等。通过统一配置源,避免了不同节点或用户看到不同配置而导致的不公平问题。

为什么使用 NFT 存储配置

传统配置管理的局限性

传统的中心化应用中,配置通常存储在服务器端的配置文件、数据库或环境变量中。运营方可以随时修改这些配置,用户无法验证配置的真实性,也无法追溯配置的变更历史。在去中心化应用中,这种配置管理方式存在明显的信任问题。

区块链作为配置载体的优势

将配置存储在区块链上的 NFT 中,所有 Dapp 和节点从同一个链上 NFT 读取配置,确保配置的统一性和公平性。这意味着所有参与者使用相同的协议参数,不会因为不同节点使用不同配置而导致不公平的情况。

配置数据公开可查,通过 GraphQL API 可以直接查询链上 NFT 数据,在前端界面展示配置信息,用户可以随时查看。虽然 NFT 拥有者可以修改配置,但配置变更通过链上交易完成,所有变更都公开透明,所有节点都能看到。

所有配置变更都记录在区块链上,每次变更都有对应的交易哈希,可以查询变更的区块高度和时间戳,也可以对比不同版本的配置差异。通过 NFT 的 data 字段存储版本信息(version 和 updatedAt),可以追踪配置的演进历史。

设计模式与架构

核心设计原则

配置数据应该完全公开,任何人都可以查询和验证。需要注意的是,配置 NFT 中不应该包含敏感信息(如私钥、密钥等),只存储协议参数和业务规则。

配置 NFT 设置为 readonly: false 和 transferrable: false,允许通过治理流程更新配置,但配置 NFT 不可被转移,确保配置地址固定。所有配置变更必须通过链上交易,变更记录永久保存。所有 Dapp 和节点从同一个 NFT 地址读取配置,确保了配置的统一性和公平性。

未来可以通过 DAO(去中心化自治组织)治理机制来管理配置更新。社区成员可以提交提案(如提高 capacity 配置的 base 数值、调整 tiers 限制等),经过投票通过后,由授权的治理合约或多签钱包执行配置更新交易。这样既保证了配置更新的去中心化和民主化,又确保了只有经过社区共识的变更才会生效。

配置 NFT 的 display 字段包含 SVG 内容,用于可视化展示配置信息。SVG 使用模板引擎(如 Mustache)动态填充数据,提供直观的图形界面。利用 NFT 标准提供的统一接口,可以通过 getAssetState 查询 NFT 状态,通过 updateAsset 更新 NFT 数据。

架构设计

javascript
┌─────────────────────────────────────────────────────────────┐
│                      Dapp 应用层                             │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐      │
│  │  Hub 应用    │  │  Studio 应用 │  │  其他客户端  │      │
│  └──────┬───────┘  └──────┬───────┘  └──────┬───────┘      │
│         │                 │                  │              │
┌─────────▼─────────────────▼──────────────────▼──────────────┐
│                  配置服务层                                  │
│  ┌──────────────────────────────────────────────────────┐  │
│  │  配置服务 (hub-protocol-config.service.ts)           │  │
│  │  - 从 NFT 读取配置数据                               │  │
│  │  - 解析和验证配置结构                                │  │
│  └──────────────────────────────────────────────────────┘  │
┌─────────▼───────────────────────────────────────────────────┐
│                  OCAP GraphQL API 层                        │
│  ┌──────────────────────────────────────────────────────┐  │
│  │  client.getAssetState({ address })                   │  │
│  │  client.updateAsset({ address, data, wallet })       │  │
│  └──────────────────────────────────────────────────────┘  │
┌─────────▼───────────────────────────────────────────────────┐
│                    区块链网络层                              │
│  ┌──────────────────────────────────────────────────────┐  │
│  │           配置 NFT (HUB_PROTOCOL_CONFIG)              │  │
│  │  ┌────────────────────────────────────────────────┐  │  │
│  │  │  data.value: { tiers, capacity, version, ... } │  │  │
│  │  │  display.content: <svg>...</svg>               │  │  │
│  │  │  readonly: false, transferrable: false         │  │  │
│  │  └────────────────────────────────────────────────┘  │  │
│  └──────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────┘

配置生命周期

javascript
创建 NFT
    │
    ├─→ 设置初始配置数据
    ├─→ 生成 SVG 模板
    ├─→ 上链创建 NFT
    │
    ├─→ [应用读取配置]
    │      │
    │      ├─→ 通过环境变量获取 NFT 地址
    │      ├─→ 调用 OCAP API 查询 NFT
    │      ├─→ 解析配置数据
    │      └─→ 在应用中使用配置
    │
    └─→ [更新配置]
           │
           ├─→ [简单更新] 直接使用授权钱包更新
           │      ├─→ 构建新配置数据
           │      ├─→ 更新版本号和更新时间
           │      ├─→ 提交链上更新交易
           │      └─→ 应用自动读取新配置
           │
           └─→ [DAO 治理更新]
                  ├─→ 社区成员提交提案(如提高 capacity.base、调整 tiers 限制)
                  ├─→ 社区投票表决
                  ├─→ 提案通过后,由治理合约或多签钱包执行更新
                  ├─→ 提交链上更新交易
                  └─→ 应用自动读取新配置

数据结构设计

通用配置数据结构

NFT 配置的数据结构应该遵循几个基本原则:配置数据要包含足够的元数据(版本、更新时间),支持未来添加新的配置项而不破坏兼容性,使用明确的类型定义(通过 TypeScript 接口)保证类型安全。

典型的配置数据结构如下:

javascript
interface ConfigData {
  // 业务配置(根据应用需求定义)
  // 例如:订阅等级、定价规则、功能开关等
  [key: string]: unknown;

  // 元数据(必须字段)
  version: string; // 语义化版本号,如 "1.0.0"
  updatedAt: string; // ISO 8601 时间戳
}

配置数据示例

以协议配置为例:

javascript
interface ProtocolConfig {
  // 业务配置:订阅等级配置
  tiers: Record<
    string,
    {
      maxAlbums: number;
      maxPhotos: number;
      maxServices: number;
      maxExhibitions: number;
      maxPhotographers: number;
    }
  >;

  // 业务配置:容量计算公式
  capacity: {
    curve: "log";
    params: {
      base: number; // β:截距参数
      a: number; // α:对数增长系数
      max: number; // Max:最大容量上限
    };
  };

  // 元数据
  version: string;
  updatedAt: string;
}

SVG 显示模板

配置 NFT 的 display 字段存储 SVG 模板,使用模板引擎(如 Mustache)实现动态数据填充。SVG 模板示例:

javascript
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 315">
  <!-- 使用 Mustache 变量引用配置数据 -->
  <text>{{data.version}}</text>
  <text>{{data.capacity.params.base}}</text>
  <text>{{data.tiers.basic.maxAlbums}}</text>
</svg>

渲染时,将真实配置数据填充到模板中:

javascript
const template = assetState.display.content; // SVG 模板
const data = { data: configData }; // 配置数据
const renderedSvg = Mustache.render(template, data); // 渲染后的 SVG

技术实现

1. NFT 创建

使用 ArcBlock OCAP Client 创建配置 NFT:

javascript
import GraphQLClient from "@ocap/client";

const client = new GraphQLClient(chainHost);

// 构建配置数据
const configData = {
  // 业务配置...
  version: "1.0.0",
  updatedAt: new Date().toISOString(),
};

// 生成 SVG 模板(包含 Mustache 变量)
const svgTemplate = generateSVGTemplate(configData);

// 创建 NFT
const [hash, assetAddress] = await client.createAsset({
  moniker: "ConfigNFT",
  readonly: false, // 允许更新
  transferrable: false, // 不可转移
  data: {
    type: "json",
    value: configData, // 真实配置数据
  },
  display: {
    type: "svg",
    content: svgTemplate, // SVG 模板
  },
  tags: ["Config", "Protocol"],
  wallet: wallet,
});

2. 配置读取

应用启动时或需要时读取配置:

javascript
// 从环境变量获取 NFT 地址
const configNFTAddress = process.env.CONFIG_NFT_ADDRESS;

// 查询 NFT 状态
const { state: assetState } = await client.getAssetState({
  address: configNFTAddress,
});

// 解析配置数据
const configData = JSON.parse(assetState.data.value);

// 使用配置
console.log(configData.version);
console.log(configData.tiers);

3. 配置更新

对于简单的配置更新,可以直接使用授权钱包执行更新。对于需要社区治理的配置更新(如提高 capacity 配置的 base 数值、调整 tiers 限制等),可以通过 DAO 治理机制来实现:社区成员提交提案,持有治理代币的社区成员对提案进行投票,提案通过后由授权的治理合约或多签钱包执行配置更新交易。

javascript
// 获取当前配置
const { state: assetState } = await client.getAssetState({
  address: configNFTAddress,
});

const currentConfig = JSON.parse(assetState.data.value);

// 构建新配置
const newConfig = {
  ...currentConfig,
  // 更新配置项...
  version: "1.1.0", // 更新版本号
  updatedAt: new Date().toISOString(), // 更新时间
};

// 提交更新交易
const hash = await client.updateAsset({
  address: configNFTAddress,
  data: {
    type: "json",
    value: newConfig,
  },
  wallet: wallet,
});

4. SVG 渲染

渲染 SVG 用于前端展示:

javascript
import Mustache from "mustache";

// 获取 SVG 模板
const svgTemplate = assetState.display.content;

// 获取配置数据
const configData = JSON.parse(assetState.data.value);

// 渲染 SVG
const renderedSvg = Mustache.render(svgTemplate, {
  data: configData,
});

// 转换为 base64 data URI
const base64Svg = Buffer.from(renderedSvg).toString("base64");
const dataUri = `data:image/svg+xml;base64,${base64Svg}`;

5. 环境变量配置

在应用中通过环境变量配置 NFT 地址:

javascript
// .env
CONFIG_NFT_ADDRESS = zjdeNciWrvMmZqCa5spW182WeRzb4ApKWKwC;

// 应用代码
const configNFTAddress = process.env.CONFIG_NFT_ADDRESS;
if (!configNFTAddress) {
  throw new Error("CONFIG_NFT_ADDRESS environment variable is required");
}

使用指南

创建配置 NFT

步骤 1:准备配置数据

定义配置数据结构和初始值:

javascript
interface MyConfig {
  // 业务配置
  featureFlags: {
    enableNewFeature: boolean;
    maxUsers: number;
  };
  pricing: {
    basePrice: number;
    discountRate: number;
  };
  // 元数据
  version: string;
  updatedAt: string;
}

const initialConfig: MyConfig = {
  featureFlags: {
    enableNewFeature: true,
    maxUsers: 1000,
  },
  pricing: {
    basePrice: 10,
    discountRate: 0.1,
  },
  version: "1.0.0",
  updatedAt: new Date().toISOString(),
};
步骤 2:生成 SVG 模板

创建 SVG 模板用于可视化展示:

javascript
function generateSVGTemplate(config: MyConfig): string {
  return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 300">
    <rect width="500" height="300" fill="#1a1f3a"/>
    <text x="250" y="50" text-anchor="middle" fill="#fff" font-size="20">
      Configuration v{{data.version}}
    </text>
    <text x="50" y="100" fill="#ccc">Base Price: {{data.pricing.basePrice}}</text>
    <text x="50" y="130" fill="#ccc">Max Users: {{data.featureFlags.maxUsers}}</text>
    <text x="50" y="250" fill="#999" font-size="10">
      Updated: {{data.updatedAt}}
    </text>
  </svg>`;
}
步骤 3:创建 NFT

使用 OCAP Client 创建 NFT:

javascript
const [hash, assetAddress] = await client.createAsset({
  moniker: "MyAppConfig",
  readonly: false,
  transferrable: false,
  data: {
    type: "json",
    value: initialConfig,
  },
  display: {
    type: "svg",
    content: generateSVGTemplate(initialConfig),
  },
  tags: ["Config"],
  wallet: wallet,
});

console.log("Config NFT created:");
console.log("Address:", assetAddress);
console.log("Transaction hash:", hash);
步骤 4:配置环境变量

将 NFT 地址配置到环境变量:

javascript
# .env
CONFIG_NFT_ADDRESS=<assetAddress>

读取配置

在应用中读取配置:

javascript
// config.service.ts
import GraphQLClient from "@ocap/client";
import env from "./env";

const client = new GraphQLClient(env.chainHost);

export async function getConfig(): Promise<MyConfig> {
  const address = env.configNFTAddress;
  if (!address) {
    throw new Error("CONFIG_NFT_ADDRESS is not set");
  }

  const { state: assetState } = await client.getAssetState({ address });
  if (!assetState?.data?.value) {
    throw new Error(`Config NFT not found: ${address}`);
  }

  const configData =
    typeof assetState.data.value === "string"
      ? JSON.parse(assetState.data.value)
      : assetState.data.value;

  return configData as MyConfig;
}

更新配置

更新配置需要持有创建 NFT 的钱包私钥:

javascript
export async function updateConfig(
  newConfig: Partial<MyConfig>,
  wallet: Wallet,
): Promise<string> {
  // 获取当前配置
  const currentConfig = await getConfig();

  // 合并新配置
  const updatedConfig: MyConfig = {
    ...currentConfig,
    ...newConfig,
    version: incrementVersion(currentConfig.version), // 版本号递增
    updatedAt: new Date().toISOString(),
  };

  // 提交更新交易
  const hash = await client.updateAsset({
    address: env.configNFTAddress!,
    data: {
      type: "json",
      value: updatedConfig,
    },
    wallet: wallet,
  });

  return hash;
}

在前端展示配置

使用 React 组件展示:

javascript
// ConfigCard.tsx
import { useEffect, useState } from "react";
import { getConfig } from "@/services/config.service";

export function ConfigCard() {
  const [config, setConfig] = useState<MyConfig | null>(null);
  const [svg, setSvg] = useState<string | null>(null);

  useEffect(() => {
    async function loadConfig() {
      const configData = await getConfig();
      setConfig(configData);

      // 渲染 SVG
      const { state: assetState } = await client.getAssetState({
        address: process.env.CONFIG_NFT_ADDRESS!,
      });
      const svgTemplate = assetState.display.content;
      const renderedSvg = Mustache.render(svgTemplate, {
        data: configData,
      });
      const base64Svg = Buffer.from(renderedSvg).toString("base64");
      setSvg(`data:image/svg+xml;base64,${base64Svg}`);
    }

    loadConfig();
  }, []);

  if (!config) return <div>Loading...</div>;

  return (
    <div>
      <h2>Configuration v{config.version}</h2>
      {svg && <img src={svg} alt="Config visualization" />}
      <div>
        <p>Base Price: {config.pricing.basePrice}</p>
        <p>Max Users: {config.featureFlags.maxUsers}</p>
        <p>Updated: {config.updatedAt}</p>
      </div>
    </div>
  );
}

最佳实践

1. 配置设计原则

配置 NFT 中的数据应该是可以公开的协议参数和业务规则,不应包含私钥、密钥等敏感信息,也不应包含用户数据和内部实现细节。

保持数据结构稳定很重要。使用语义化版本号管理配置变更,新增字段时考虑向后兼容,删除字段时升级主版本号。在应用代码中为配置项提供合理的默认值,避免 NFT 读取失败时应用崩溃。

2. 版本管理

遵循语义化版本规范(Semantic Versioning):主版本号用于不兼容的 API 修改,次版本号用于向下兼容的功能性新增,修订号用于向下兼容的问题修正。

版本更新策略:修改配置值(如调整价格)通常为 Patch,添加新配置项通常为 Minor,删除配置项或改变结构通常为 Major。在配置注释或变更日志中记录变更原因、影响范围和迁移指南(如有),有助于后续维护和理解配置演进。

3. 安全性考虑

配置 NFT 的创建和更新需要私钥签名,私钥应安全存储,使用环境变量或密钥管理服务,不应将私钥提交到代码仓库。

配置 NFT 设置为 transferrable: false 防止被转移。对于重要配置,建议使用多签钱包或 DAO 治理机制来管理配置更新权限,而不是依赖单一私钥。这样可以防止单点故障,同时通过社区投票确保配置变更的合理性和合法性。

更新配置前要验证数据的完整性和正确性,使用 TypeScript 类型检查确保数据结构正确,验证数值范围(如价格不应为负数)。

4. 错误处理和性能优化

当配置 NFT 读取失败时,应用应该使用默认配置继续运行,记录错误日志,并向用户显示友好的错误提示,实现优雅降级。

在内存中缓存配置数据可以减少链上查询,需要设置合理的缓存过期时间,并提供手动刷新缓存的机制。应用启动时读取一次配置并缓存,使用定时任务定期刷新配置,或者提供配置变更通知机制(如 WebSocket)来减少链上查询。

对于 SVG 渲染,可以在前端缓存渲染后的 SVG,使用 CDN 加速 SVG 资源加载,或者考虑将 SVG 转换为 PNG 格式以提高兼容性。

GLofter 案例:Hub Protocol Config

GLofter 项目中的 Hub Protocol Config NFT 是一个完整的 NFT 配置实践案例。

业务背景

GLofter Hub 是一个去中心化聚合平台,需要公开透明的协议配置,包括订阅等级配置(tiers)和容量计算公式。

配置数据结构

javascript
interface HubProtocolConfig {
  tiers: Record<
    "basic" | "pro" | "premium" | "enterprise",
    {
      maxAlbums: number;
      maxPhotos: number;
      maxServices: number;
      maxExhibitions: number;
      maxPhotographers: number;
    }
  >;
  capacity: {
    curve: "log";
    params: {
      base: number; // β:截距
      a: number; // α:对数增长系数
      max: number; // 最大容量上限
    };
  };
  version: string;
  updatedAt: string;
}

容量计算公式

使用的容量计算公式为:

javascript
C(stake) = min(β + α · log(stake + 1), Max)

其中:C(stake) 是根据质押数量计算的容量,stake 是质押的 ABT 代币数量,β(base)是截距参数(当前值为 0),α(a)是对数增长系数(当前值约为 1442.695),Max 是最大容量上限(当前值为 10000)。

实现细节

GLofter 项目提供了完整的 NFT 创建和更新脚本,支持创建、更新、生成 SVG 预览和渲染 SVG 等操作。配置读取服务从链上 NFT 获取配置数据,前端组件展示配置信息,包括 SVG 可视化展示和配置详情表格。

总结

NFT 配置模式为 Dapp 提供了一种透明、可验证、可追溯的配置管理方案。通过将配置存储在链上 NFT 中,所有 Dapp 和节点从同一个配置源读取数据,确保了配置的统一性和公平性;配置变更通过链上交易完成,所有变更都公开透明,任何人都可以查询和验证;所有配置变更都记录在链上,实现了可追溯性;通过 SVG 图形化展示配置信息,实现了可视化;利用 NFT 标准提供的统一接口,实现了标准化存储和查询。

这种模式不仅适用于 GLofter 项目,也可以作为其他 Dapp 配置管理的通用方案。通过统一配置源和区块链的透明性,实现了一种既公平又灵活的配置管理机制。

Reply