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

第 14 章 SDK 上传流程拆解

writeBlobwriteFiles 是高层 API,适合脚本和服务端任务。浏览器钱包、断点续传、任务队列和大文件上传通常需要拆开流程:

  1. encode:编码文件,计算 blob ID。
  2. register:构造注册交易,把 blob 元数据写到 Sui。
  3. upload:把数据上传到 storage nodes 或 upload relay。
  4. certify:构造认证交易,提交可用性证明。
  5. listFiles:对 writeFilesFlow,列出 quilt 里的文件。

SDK 提供 writeBlobFlowwriteFilesFlow。它们让你控制每一步什么时候执行、由谁签名,以及如何保存进度。

服务端完整 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);

executeRegisterexecuteCertify 会用 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);

writeFileswriteFilesFlow 当前会写成 quilt。读取时可以按 quilt ID 获取文件集合,也可以保存每个文件的 ID 供 HTTP aggregator 单独下载。

状态机建议

不要在数据库里只保存 blobId。生产上传任务至少保存:

  • uploadId:业务上传任务 ID。
  • owner:支付和拥有 Blob 对象的地址。
  • sourceSha256:原始文件校验。
  • blobIdencode 后得到。
  • lastStep:SDK 返回的 step 原文。
  • registerDigest:注册交易 digest。
  • certifyDigest:认证交易 digest。
  • statusencodingregistereduploadingcertifiedfailed

这样排查问题时可以区分链上失败、relay 失败、storage node 写入失败和业务回调失败。