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

第 15 章 前端与后端集成模式

Walrus 应用通常不是只选 HTTP API 或只选 SDK,而是按读写责任组合:

  • 公开下载:aggregator HTTP API,加 CDN 或多 aggregator fallback。
  • 后端托管上传:publisher HTTP API,服务端统一付费和治理。
  • 用户自付上传:TypeScript SDK,钱包签名,必要时接 upload relay。
  • 批量小文件:SDK writeFiles 或 HTTP quilt。
  • 浏览器大文件:SDK flow + upload relay + 持久化进度。

本章给出几种可以直接落地的集成方式。

模式一:后端代理 publisher

适合内容平台、管理员上传、CI 发布、AI 任务产物归档。用户不直接接触 Sui/WAL,费用由服务端账户或 publisher 承担。

sequenceDiagram
  participant UI as Web UI
  participant API as App Backend
  participant Pub as Walrus Publisher
  participant Agg as Walrus Aggregator

  UI->>API: POST /api/uploads(file)
  API->>API: 鉴权、限额、扫描、记录任务
  API->>Pub: PUT /v1/blobs?epochs=...
  Pub-->>API: blobId / objectId
  API-->>UI: blobId / downloadUrl
  UI->>Agg: GET /v1/blobs/{blobId}

后端接口示例:

export async function uploadToWalrus(file: Uint8Array, contentType: string) {
  const url = new URL('/v1/blobs', process.env.PUBLISHER);
  url.searchParams.set('epochs', '5');
  url.searchParams.set('permanent', 'true');

  const response = await fetch(url, {
    method: 'PUT',
    headers: {
      'content-type': contentType,
    },
    body: file,
  });

  if (!response.ok) {
    throw new Error(`publisher failed: ${response.status} ${await response.text()}`);
  }

  const result = await response.json();
  const blobId = result.newlyCreated?.blobObject?.blobId ?? result.alreadyCertified?.blobId;
  const objectId = result.newlyCreated?.blobObject?.id;

  if (!blobId) {
    throw new Error(`publisher response missing blobId: ${JSON.stringify(result)}`);
  }

  return { blobId, objectId, raw: result };
}

这个模式最容易运营,但 publisher 是写入信任边界。你需要限制文件大小、内容类型、用户额度和保存周期,避免开放接口被刷成存储账单。

模式二:用户钱包直写

适合 NFT 元数据、用户拥有的数据资产、去中心化网盘、付费数据集。用户钱包签名,用户地址拥有 Blob 对象。

sequenceDiagram
  participant UI as Browser
  participant Wallet as Sui Wallet
  participant Relay as Upload Relay
  participant Sui as Sui

  UI->>UI: encode file
  UI->>Wallet: sign register tx
  Wallet->>Sui: execute register
  UI->>Relay: upload blob bytes/slivers
  UI->>Wallet: sign certify tx
  Wallet->>Sui: execute certify

关键点:

  • 不要把私钥放进浏览器代码;使用钱包适配器签名。
  • 注册和认证分成两个用户动作,避免钱包弹窗被浏览器拦截。
  • 大文件使用 upload relay,减少浏览器直接访问 storage nodes 的请求数量。
  • 保存 lastStep 到 IndexedDB 或后端,用户刷新页面后可以恢复。

Vite WASM 配置

Walrus SDK 需要 WASM 编解码。Vite 中导入 WASM URL 后传给 walrus()

import { walrus } from '@mysten/walrus';
import { SuiGrpcClient } from '@mysten/sui/grpc';
import walrusWasmUrl from '@mysten/walrus-wasm/web/walrus_wasm_bg.wasm?url';

export const client = new SuiGrpcClient({
  network: 'testnet',
  baseUrl: 'https://fullnode.testnet.sui.io:443',
}).$extend(
  walrus({
    wasmUrl: walrusWasmUrl,
    uploadRelay: {
      host: 'https://upload-relay.testnet.walrus.space',
      sendTip: {
        max: 1_000,
      },
    },
  }),
);

如果构建系统不能解析 ?url,可以自托管 WASM 文件,或固定 CDN 版本:

walrus({
  wasmUrl: 'https://unpkg.com/@mysten/walrus-wasm@0.2.0/web/walrus_wasm_bg.wasm',
});

不要在生产代码里使用 @latest CDN URL。书中固定版本是为了可复现。

Next.js 配置

Next.js 服务端 API routes 或 server actions 使用 Walrus SDK 时,配置外部包:

// next.config.ts
import type { NextConfig } from 'next';

const nextConfig: NextConfig = {
  serverExternalPackages: ['@mysten/walrus', '@mysten/walrus-wasm'],
};

export default nextConfig;

如果你只在浏览器组件使用 SDK,仍然要确认 WASM 文件能被打包或从固定 URL 加载。服务端上传更推荐 HTTP publisher 或 Node SDK;浏览器上传更推荐钱包 flow。

模式三:服务端签名 SDK

适合你不想运行 publisher,但希望服务端自己直接控制 Walrus 写入。服务端保存 WALRUS_PRIVATE_KEY,用 SDK 写 blob:

const result = await client.walrus.writeBlob({
  blob: fileBytes,
  epochs: 7,
  deletable: false,
  signer,
  onStep: (step) => uploadStore.save(uploadId, step),
  resume: await uploadStore.load(uploadId),
});

相比 publisher,这个模式更灵活:

  • 可以接 upload relay,也可以直连 storage nodes。
  • 可以把 flow step 写进自己的任务表。
  • 可以统一处理 retry、审计和业务状态。

代价是服务端需要管理 Sui/WAL 余额、私钥安全和并发上传。

模式四:读路径多 aggregator fallback

Blob 是内容寻址的,读路径天然适合 fallback:

const aggregators = [
  'https://aggregator.walrus-testnet.walrus.space',
  'https://walrus.example.com',
];

export async function readWithFallback(blobId: string) {
  let lastError: unknown;

  for (const baseUrl of aggregators) {
    try {
      const response = await fetch(`${baseUrl}/v1/blobs/${blobId}`);
      if (!response.ok) {
        throw new Error(`${response.status} ${await response.text()}`);
      }
      return new Uint8Array(await response.arrayBuffer());
    } catch (error) {
      lastError = error;
    }
  }

  throw lastError;
}

公共资产可以把 aggregator 放在 CDN 后面。注意缓存键要包含完整 URL;如果使用 strict_consistency_checkskip_consistency_check,查询参数也是缓存键的一部分。

前端展示下载链接

最简单的下载 URL:

export function walrusDownloadUrl(blobId: string) {
  return `${import.meta.env.VITE_WALRUS_AGGREGATOR}/v1/blobs/${encodeURIComponent(blobId)}`;
}

对象 ID 下载:

export function walrusObjectDownloadUrl(objectId: string) {
  return `${import.meta.env.VITE_WALRUS_AGGREGATOR}/v1/blobs/by-object-id/${objectId}`;
}

Quilt patch 下载:

export function walrusPatchDownloadUrl(patchId: string) {
  return `${import.meta.env.VITE_WALRUS_AGGREGATOR}/v1/blobs/by-quilt-patch-id/${patchId}`;
}

浏览器会尝试推断内容类型,但 aggregator 会阻止危险可执行类型的嗅探。需要稳定文件名、下载行为和 MIME 时,用对象属性或自己的后端代理设置响应头。

安全边界

Walrus blob 是公开可发现的。不要直接上传密钥、未加密个人数据、访问令牌、内部日志或商业敏感数据。需要保密时先在应用层加密,再上传密文。

后端代理必须做:

  • 鉴权:谁能上传、能保存多久。
  • 限额:单文件大小、每日总量、并发数。
  • 内容治理:MIME 白名单、病毒扫描、业务审核。
  • 成本保护:按用户、项目或 API key 计量。
  • 结果校验:保存 blob ID、对象 ID、epoch、交易 digest 和响应原文。

前端钱包直写必须做:

  • 明确展示费用、epochs、deletable/permanent。
  • 拆分签名步骤。
  • 保存恢复状态。
  • 给出 aggregator 下载验证入口。

推荐默认架构

大多数产品可以从这个组合开始:

  • 管理员和后端任务上传:HTTP publisher。
  • 普通用户下载:HTTP aggregator + CDN。
  • 用户自拥有资产上传:SDK writeFilesFlow + upload relay。
  • 小文件批量:quilt。
  • 大文件任务:SDK flow + 后端任务表 + 重试。

这个架构把复杂度放在需要控制的地方,把普通读写交给 HTTP 接口,后续再按业务要求逐步替换成更底层的 SDK flow。