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

第 12 章 HTTP API:Publisher 与 Aggregator

Walrus 的 HTTP API 把常用读写操作拆成两个角色:

  • Publisher:接收原始文件或字节,代替客户端完成编码、上传 sliver、收集证明和链上写入。
  • Aggregator:按 blob ID、对象 ID 或 quilt patch ID 读取数据,重建 blob 后用普通 HTTP 响应返回原始字节。

如果你在做 Web 服务、后端批处理、CI 上传或内容分发,优先使用 HTTP API。它的调用模型最简单,也最容易接入已有网关、缓存、鉴权和日志系统。TypeScript SDK 更适合需要用户自己签名、自付存储费,或需要直接控制写入流程的应用。

准备端点

本章示例使用环境变量保存端点:

export PUBLISHER="https://publisher.walrus-testnet.walrus.space"
export AGGREGATOR="https://aggregator.walrus-testnet.walrus.space"

生产环境建议把这些端点放在服务端配置里,不要让前端直接依赖单个自运营节点。你可以为读路径配置多个 aggregator 或 CDN 缓存,写路径则根据业务选择可信 publisher、自运营 publisher,或改用第 13-15 章的 SDK 用户签名流程。

上传字符串

最小写入请求是一个 PUT /v1/blobs

curl -X PUT "$PUBLISHER/v1/blobs" \
  -H "content-type: text/plain; charset=utf-8" \
  -d "hello walrus"

未指定 epochs 时,publisher 默认存储 1 个 epoch。测试可以这样做,业务数据应显式指定保存周期。

curl -X PUT "$PUBLISHER/v1/blobs?epochs=5" \
  -H "content-type: text/plain; charset=utf-8" \
  -d "hello walrus"

响应有两类常见形态。第一次写入新内容时,返回 newlyCreated,其中 blobObject.blobId 是读取用的 blob ID,blobObject.id 是链上 Blob 对象 ID:

{
  "newlyCreated": {
    "blobObject": {
      "id": "0x...",
      "blobId": "M4hsZGQ1oCktdzegB6HnI6Mi28S2nqOPHxK-W7_4BUk",
      "size": 17,
      "certifiedEpoch": 34,
      "storage": {
        "startEpoch": 34,
        "endEpoch": 39
      },
      "deletable": true
    },
    "cost": 132300
  }
}

如果同样内容已经有满足期限的 certified blob,publisher 可能返回 alreadyCertified

{
  "alreadyCertified": {
    "blobId": "M4hsZGQ1oCktdzegB6HnI6Mi28S2nqOPHxK-W7_4BUk",
    "event": {
      "txDigest": "4XQHFa9S324wTzYHF3vsBSwpUZuLpmwTHYMFv9nsttSs",
      "eventSeq": "0"
    },
    "endEpoch": 39
  }
}

应用代码不要假设只会出现一种字段。应统一提取 blob ID:

function getBlobId(result) {
  return result.newlyCreated?.blobObject?.blobId ?? result.alreadyCertified?.blobId;
}

上传文件

文件上传使用同一个接口,只是请求体改为文件字节:

curl -X PUT "$PUBLISHER/v1/blobs?epochs=5&deletable=true" \
  --upload-file ./photo.jpg

deletable=true 表示 Blob 对象所有者可以在过期前删除该 blob。permanent=true 表示在过期前不可删除。Walrus 官方 HTTP API 当前说明中,新写入 blob 默认是 deletable;面向长期引用、审计、模型产物和公开资产时,建议显式传 permanent=true,避免读者误判可用性。

curl -X PUT "$PUBLISHER/v1/blobs?epochs=10&permanent=true" \
  --upload-file ./release.tar.zst

如果希望 Blob 对象发送给另一个 Sui 地址,使用 send_object_to

curl -X PUT "$PUBLISHER/v1/blobs?epochs=5&send_object_to=$SUI_ADDRESS" \
  --upload-file ./report.pdf

这个参数只影响链上 Blob 对象的接收地址,不会改变 blob ID。blob ID 由内容决定,相同内容会得到相同 blob ID。

下载 blob

按 blob ID 读取:

curl "$AGGREGATOR/v1/blobs/<BLOB_ID>" -o ./download.bin

直接打印文本:

curl "$AGGREGATOR/v1/blobs/<BLOB_ID>"

按链上对象 ID 读取:

curl "$AGGREGATOR/v1/blobs/by-object-id/<OBJECT_ID>" -o ./download.bin

按对象 ID 读取时,aggregator 可以根据 Blob 对象属性返回部分 HTTP 头,例如 content-typecontent-dispositioncontent-languagecontent-encodingcontent-locationlink。如果你在构建下载服务,优先保存对象 ID,因为它能承载更多链上元数据;如果你只需要内容寻址,保存 blob ID 就够。

一致性检查

Aggregator 默认执行与 CLI 一致的读取校验。特殊场景可以通过查询参数调整:

curl "$AGGREGATOR/v1/blobs/<BLOB_ID>?strict_consistency_check=true" \
  -o ./checked.bin

只在写入者可信、且你愿意牺牲校验换取性能时才跳过一致性检查:

curl "$AGGREGATOR/v1/blobs/<BLOB_ID>?skip_consistency_check=true" \
  -o ./fast.bin

默认不要跳过。Walrus 的价值在于可验证读取,应用层为了几百毫秒优化关闭校验,通常得不偿失。

Quilt:一次上传多个小文件

多个小文件应使用 quilt。Publisher 接收 multipart/form-data,每个字段名是 quilt 内 identifier:

curl -X PUT "$PUBLISHER/v1/quilts?epochs=5" \
  -F "readme=@README.md;type=text/markdown" \
  -F "logo=@logo.png;type=image/png"

带 Walrus 原生 tags:

curl -X PUT "$PUBLISHER/v1/quilts?epochs=5" \
  -F "readme=@README.md;type=text/markdown" \
  -F "logo=@logo.png;type=image/png" \
  -F '_metadata=[
    {"identifier":"readme","tags":{"content-type":"text/markdown"}},
    {"identifier":"logo","tags":{"content-type":"image/png"}}
  ]'

响应里的 blobStoreResult 描述整个 quilt blob,storedQuiltBlobs 给出每个文件的 quiltPatchId

{
  "blobStoreResult": {
    "newlyCreated": {
      "blobObject": {
        "blobId": "6XUOE-Q5-nAXHRifN6n9nomVDtHZQbGuAkW3PjlBuKo"
      }
    }
  },
  "storedQuiltBlobs": [
    {
      "identifier": "readme",
      "quiltPatchId": "6XUOE-Q5-nAXHRifN6n9nomVDtHZQbGuAkW3PjlBuKoBAQDQAA"
    }
  ]
}

读取 quilt 内单个文件有两种方式:

curl "$AGGREGATOR/v1/blobs/by-quilt-patch-id/<QUILT_PATCH_ID>" \
  -o ./readme.md
curl "$AGGREGATOR/v1/blobs/by-quilt-id/<QUILT_ID>/readme" \
  -o ./readme.md

当前 HTTP API 一次请求只取 quilt 内一个 blob;批量读取请在应用层并发请求,或使用第 13 章的 SDK getFiles 批量读取。

Node fetch 示例

examples/http 里有可运行脚本。核心上传逻辑如下:

import { readFile } from 'node:fs/promises';

const publisher = process.env.PUBLISHER;
const file = await readFile(process.argv[2]);

const url = new URL('/v1/blobs', publisher);
url.searchParams.set('epochs', process.env.EPOCHS ?? '3');
url.searchParams.set('deletable', 'true');

const response = await fetch(url, {
  method: 'PUT',
  body: file,
});

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

const result = await response.json();
console.log(JSON.stringify(result, null, 2));

下载逻辑就是 GET 后把响应体写入文件:

import { writeFile } from 'node:fs/promises';

const aggregator = process.env.AGGREGATOR;
const blobId = process.argv[2];
const out = process.argv[3] ?? 'blob.bin';

const response = await fetch(`${aggregator}/v1/blobs/${blobId}`);
if (!response.ok) {
  throw new Error(`aggregator failed: ${response.status} ${await response.text()}`);
}

await writeFile(out, new Uint8Array(await response.arrayBuffer()));

什么时候不用 HTTP API

HTTP publisher 会替用户完成链上动作,因此它天然是一个受信任写入服务。以下场景更适合 SDK:

  • 用户必须用自己的钱包签名并支付 SUI/WAL。
  • 应用要把 registeruploadcertify 分成多个用户交互。
  • 你要自己选择 upload relay、storage node 请求策略或恢复上传进度。
  • 你要在前端直接读取 blob,并接受 SDK 读取会发起大量 storage node 请求的成本。

实践中常见组合是:公开读走 aggregator/CDN,用户付费写走 SDK,后端托管写走 publisher。