第 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-type、content-disposition、content-language、content-encoding、content-location 和 link。如果你在构建下载服务,优先保存对象 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。
- 应用要把
register、upload、certify分成多个用户交互。 - 你要自己选择 upload relay、storage node 请求策略或恢复上传进度。
- 你要在前端直接读取 blob,并接受 SDK 读取会发起大量 storage node 请求的成本。
实践中常见组合是:公开读走 aggregator/CDN,用户付费写走 SDK,后端托管写走 publisher。