第 14 章 SDK 上传流程拆解
writeBlob 和 writeFiles 是高层 API,适合脚本和服务端任务。浏览器钱包、断点续传、任务队列和大文件上传通常需要拆开流程:
encode:编码文件,计算 blob ID。register:构造注册交易,把 blob 元数据写到 Sui。upload:把数据上传到 storage nodes 或 upload relay。certify:构造认证交易,提交可用性证明。listFiles:对writeFilesFlow,列出 quilt 里的文件。
SDK 提供 writeBlobFlow 和 writeFilesFlow。它们让你控制每一步什么时候执行、由谁签名,以及如何保存进度。
服务端完整 flow
服务端持有 signer 时,可以使用 run():
import { readFile, writeFile } from 'node:fs/promises';
import { createWalrusClient, loadKeypair } from './client.js';
const blob = await readFile('./dataset.jsonl');
const client = createWalrusClient();
const signer = loadKeypair();
const flow = client.walrus.writeBlobFlow({ blob });
let lastStep;
for await (const step of flow.run({
signer,
epochs: 3,
deletable: true,
})) {
lastStep = step;
await writeFile('walrus-upload-step.json', JSON.stringify(step, null, 2));
console.log(step);
}
console.log('last step', lastStep);
run() 是异步迭代器,每完成一步就产出一个 step。生产服务应把 step 写入数据库,而不是只写本地文件。任务重启后把最后一步传给 resume:
const saved = JSON.parse(await readFile('walrus-upload-step.json', 'utf8'));
const flow = client.walrus.writeBlobFlow({
blob,
resume: saved,
});
for await (const step of flow.run({ signer, epochs: 3, deletable: true })) {
await saveStep(uploadId, step);
}
SDK 会跳过已完成步骤,并只补传缺失数据。这个特性对长文件和不稳定网络非常关键。
手动执行 register/upload/certify
如果你要在每一步之间更新业务状态,可以调用显式方法:
const flow = client.walrus.writeBlobFlow({ blob });
const encoded = await flow.encode();
console.log('blob id', encoded.blobId);
const registered = await flow.executeRegister({
signer,
owner: signer.toSuiAddress(),
epochs: 3,
deletable: true,
});
await flow.upload({
digest: registered.txDigest,
});
const certified = await flow.executeCertify({ signer });
console.log(certified);
executeRegister 和 executeCertify 会用 signer 直接签名并执行交易。它们适合 Node 后端和 CLI。
浏览器钱包为什么要拆步
浏览器钱包签名通常会打开弹窗。浏览器只允许用户点击触发的窗口稳定弹出;如果你在一个长异步链里自动调用第二次签名,弹窗可能被拦截。
所以浏览器里推荐:
- 文件选择后立即
encode,不需要签名。 - 用户点击“注册”按钮时创建
register交易并调用钱包签名。 - 注册交易成功后执行
upload。 - 用户点击“认证”按钮时创建
certify交易并调用钱包签名。 - 认证成功后
listFiles或保存 blob ID。
核心代码如下:
import { WalrusFile } from '@mysten/walrus';
const flow = client.walrus.writeFilesFlow({
files: [
WalrusFile.from({
contents: new Uint8Array(await selectedFile.arrayBuffer()),
identifier: selectedFile.name,
tags: {
'content-type': selectedFile.type || 'application/octet-stream',
},
}),
],
});
await flow.encode();
async function handleRegister() {
const tx = flow.register({
epochs: 3,
owner: currentAccount.address,
deletable: true,
});
const result = await signAndExecuteTransaction({ transaction: tx });
if (result.$kind === 'FailedTransaction') {
throw new Error(result.FailedTransaction.status.error?.message);
}
await flow.upload({ digest: result.Transaction.digest });
}
async function handleCertify() {
const tx = flow.certify();
const result = await signAndExecuteTransaction({ transaction: tx });
if (result.$kind === 'FailedTransaction') {
throw new Error(result.FailedTransaction.status.error?.message);
}
const files = await flow.listFiles();
console.log(files);
}
真实 UI 不要把这些步骤隐藏成一个按钮。用户需要理解为什么会签两次:第一次是注册存储,第二次是认证可用性。
Upload relay
SDK 直接写 storage nodes 需要大量请求。Upload relay 把上传 slivers 的工作交给一个服务端,客户端仍然负责链上资产和签名。
配置方式:
import { walrus } from '@mysten/walrus';
import { SuiGrpcClient } from '@mysten/sui/grpc';
const client = new SuiGrpcClient({
network: 'testnet',
baseUrl: 'https://fullnode.testnet.sui.io:443',
}).$extend(
walrus({
uploadRelay: {
host: 'https://upload-relay.testnet.walrus.space',
sendTip: {
max: 1_000,
},
},
}),
);
sendTip.max 的单位是 MIST。SDK 会读取 relay 的 /v1/tip-config 并在上限内支付 tip。你也可以手动配置固定 tip:
uploadRelay: {
host: 'https://upload-relay.testnet.walrus.space',
sendTip: {
address: '0x...',
kind: {
const: 105,
},
},
}
线性 tip:
uploadRelay: {
host: 'https://upload-relay.testnet.walrus.space',
sendTip: {
address: '0x...',
kind: {
linear: {
base: 105,
perEncodedKib: 10,
},
},
},
}
前端大文件上传优先考虑 upload relay。它减少浏览器请求数量,但不是免费信任:relay 可以要求 tip,也可能限流或不可用。应用需要显示失败状态,并允许用户重试或切换 relay。
writeFilesFlow 与 quilt
多文件拆步写入使用 writeFilesFlow:
const flow = client.walrus.writeFilesFlow({
files: [
WalrusFile.from({
contents: new TextEncoder().encode('# README\n'),
identifier: 'README.md',
tags: {
'content-type': 'text/markdown',
},
}),
WalrusFile.from({
contents: JSON.stringify({ version: 1 }),
identifier: 'manifest.json',
tags: {
'content-type': 'application/json',
},
}),
],
});
await flow.encode();
const reg = await flow.executeRegister({
signer,
owner: signer.toSuiAddress(),
epochs: 5,
deletable: true,
});
await flow.upload({ digest: reg.txDigest });
await flow.executeCertify({ signer });
const files = await flow.listFiles();
console.log(files);
writeFiles 和 writeFilesFlow 当前会写成 quilt。读取时可以按 quilt ID 获取文件集合,也可以保存每个文件的 ID 供 HTTP aggregator 单独下载。
状态机建议
不要在数据库里只保存 blobId。生产上传任务至少保存:
uploadId:业务上传任务 ID。owner:支付和拥有 Blob 对象的地址。sourceSha256:原始文件校验。blobId:encode后得到。lastStep:SDK 返回的 step 原文。registerDigest:注册交易 digest。certifyDigest:认证交易 digest。status:encoding、registered、uploading、certified、failed。
这样排查问题时可以区分链上失败、relay 失败、storage node 写入失败和业务回调失败。