# SDK 1.26.3 → 1.30.9 升级完整回顾

**时间**：2026-04-20 ~ 2026-04-22
**涉及仓库**：`payment-kit` + `abt-wallet`
**主要目标**：升级 `@arcblock/*` / `@ocap/*` 到 1.30.9，并在可行时切到 CBOR 编码

---

## 目录

1. [升级目标与产出](#一升级目标与产出)
2. [遇到的坑（按层级分类）](#二遇到的坑按层级分类)
3. [每个问题的成因与解决](#三每个问题的成因与解决)
4. [如果重新做一次：完整 Checklist](#四如果重新做一次完整-checklist)
5. [尚未解决的事项](#五尚未解决的事项)

---

## 一、升级目标与产出

### 起点
- `@ocap/*`、`@arcblock/*` 在 1.26.3 系列（protobuf 原生，无 CBOR）
- payment-kit 已迁移到 Cloudflare Workers 但 SDK 未升
- abt-wallet 仍使用 1.26.3 SDK

### 1.30.9 带来的主要变化
| 变化 | 意义 | 对消费者影响 |
|---|---|---|
| `@ocap/client` 默认 → CBOR | wire 格式切换 | 必须改用 `/legacy` subpath 或接受 CBOR |
| `@arcblock/did-util` 拆 `/cbor` `/protobuf` 两个 subpath | 按需引入，减小 bundle | 要手动选对子路径才省体积 |
| `@ocap/asset/mint/client` 加入 CBOR-only 版本 | 同上 | 同上 |
| `@scure/bip32` 替换 `@wangshijun/hdkey` | 移除 elliptic 依赖链 | **seed 严格 16-64 字节**（break change）|
| `@noble/curves` v2 替换 elliptic | 安全审计 + bundle -45% | API 可能不同 |
| Joi → Valibot schema 校验 | 更快、更小 | 某些 key-order 敏感路径要改 |
| `@ocap/asset/mint/server`、`@arcblock/did-util` 顶层入口 | 需要显式 `encoding` 参数 | 旧代码会 break |
| itx address derivation 跟 `client.txEncoding` 对齐 | 修复编码不一致 bug | 纯 UPSIDE |

### 最终产出
- `payment-kit/feat/cf-workers-migration`（legacy 分支）：1.30.9 + `/legacy` wire 格式 + Node 后端 pnpm override pin 到 1.29.27
- `payment-kit/feat/cbor-migration-pk`（CBOR 分支）：1.30.9 + CBOR wire 格式，bundle 2.76 MB / gzip 819 KiB（比 legacy -21% raw / -11% gzip，google-protobuf 完全清除）
- `abt-wallet/upgrade-sdk-to-1.30.9-dual-decode`：4.19.x + dual-decode 能力（WIP）
- 两个 smoke test 脚本沉淀：`tools/sdk-v3-smoke.ts`、`tools/l2-wallet-chain-smoke.ts`

---

## 二、遇到的坑（按层级分类）

### A. 依赖管理层（6 个）

| # | 问题 | 难度 | 首次触发时间 |
|---|---|---|---|
| A1 | pnpm 不认 `resolutions` 字段（yarn-only），顶层 1.30.9 强制无效 | 高 | Phase A 后期 |
| A2 | `@blocklet/sdk@1.17.11` 内嵌 `@arcblock/did-connect-js@1.29.8`，pnpm 扁平化后它 `require('@ocap/client')` 命中顶层 1.30.9，**Node 后端悄无声息切到 CBOR** | **极高** | 用户主动追问发现 |
| A3 | `@arcblock/did-connect-js` 发布版本最高仅到 1.30.1，无法对齐 1.30.9 | 中 | Phase A 规划 |
| A4 | `@wangshijun/hdkey` → `@scure/bip32` 严格 seed 长度，旧钱包 `fromAppDid('')` 炸 `seed length must be between 128 and 512 bits; got 152` | 高 | abt-wallet 编译后首次登录 |
| A5 | 同一 npm 树下两个 webpack 副本（react-scripts 5.76.2 vs top-level 5.106.2），plugin 实例跨版本 `.tap()` 失败 | 极高 | 钱包 build |
| A6 | nested packages 的 @ocap/client 无法通过 resolution 强制 pin，需 `pnpm.overrides` 的 `>` 语法显式隔离 | 高 | A2 的解决过程中 |

### B. webpack / bundler 打包层（5 个）

| # | 问题 | 难度 |
|---|---|---|
| B1 | `@ocap/message/esm/patch.mjs` 的 Rolldown `__require(createRequire(import.meta.url))` 在浏览器得到 noop，炸 "Cannot read properties of undefined (reading 'Map')" | 极高 |
| B2 | CRA 5 `ModuleScopePlugin` 阻止所有 alias 指向 `src/` 之外的文件 | 高 |
| B3 | CRA webpack 规则里 `oneOf` 的 file-loader fallback 把 `.cjs` 当静态资源，`require('@ocap/mcrypto')` 返回 URL 字符串而不是模块 → `mcrypto.Hasher === undefined` | **极高** |
| B4 | webpack ESM↔CJS interop：CJS consumer `require('@ocap/mcrypto')` 经 ESM 路径加载时，命名导出 `.Hasher` 在某些场景下丢失 | 高 |
| B5 | CRA 默认不编译 `.cjs` 文件（babel-loader test 仅 `/\.(m?js|tsx?)$/`），需显式加 rule | 中 |

### C. SDK 运行时层（4 个）

| # | 问题 | 难度 |
|---|---|---|
| C1 | `@ocap/client` 默认 entry 即使在 CBOR 模式下，仍静态引入 `@ocap/proto/gen/*_pb.js`（1.2 MB protobuf schema），bundle 未按 commit message 宣传那样瘦身 | 高 |
| C2 | `@ocap/client/encode` 的 JSDoc 写 "protobuf 二进制" 但实际代码 `require('@ocap/message/cbor')` 输出 CBOR，文档与实现不符 | 中 |
| C3 | `@arcblock/did-util` 顶层入口会无条件拉进 `protobuf.mjs` → `@ocap/proto/runtime`，即使消费者只用到 `toStakeAddress` 等通用工具 | 高 |
| C4 | `@ocap/asset` 顶层入口同理会拉 `mint/server.mjs`（dual-encoding）→ 再拉 `@arcblock/did-util` 顶层 | 高 |

### D. 应用代码层（4 个）

| # | 问题 | 难度 |
|---|---|---|
| D1 | `@ocap/client/legacy` wire 格式下 tx bytes 是 protobuf，CBOR 钱包无法解；反之 CBOR bytes 也被 legacy 钱包拒 → `INVALID_CHECK: Unmatched end-group tag` | 中（理解后）|
| D2 | 1.30.9 `toTokenAddress(payload)` 等函数签名加 `encoding` 参数为必填，不传报 `Expected 2 arguments, but got 1` | 中 |
| D3 | 1.30.9 `preMintFromFactory({...})` 要求 `encoding` 字段，原来的调用处不传会 type check 失败 | 中 |
| D4 | payment-kit 多个业务文件用顶层 `@arcblock/did-util` / `@ocap/asset`，变相带进整条 protobuf 链 | 中（追踪到之后）|

### E. 体验层（非 bug 但影响调试）（3 个）

| # | 问题 |
|---|---|
| E1 | payment-kit 服务端的 `INVALID_CHECK` / `INSUFFICIENT_FUND` 错误未结构化透传给钱包，钱包仅看到 "jwt response empty or invalid"，完全丢失诊断信息 |
| E2 | `new Error('INVALID_CHECK', 'actual reason')` —— 第二参数被静默丢弃，钱包日志只剩错误码 |
| E3 | wrangler dev 热重载有时对 ESM `import path` 大改动不敏感，需重启才生效 |

---

## 三、典型问题深度分析

> 二节共记录 22 个问题；本节挑其中**耗时最长、最具代表性的 7 个**做深度解析，其余在"解决"栏里用一行概述。

### A1: pnpm 不认 `resolutions`

**成因**：pnpm 只识别 `pnpm.overrides`，顶层 package.json 的 `resolutions` 是 yarn 字段。
**症状**：所有"强制 1.30.9"的意图全部无效，nested 依赖照样装旧版本。
**解决**：把需要的 override 迁到 `pnpm.overrides`，支持 `A>B` 语法定点覆盖。
**代码位置**：`payment-kit/package.json`

### A2: @blocklet/sdk 嵌套 did-connect-js 偷偷用 CBOR

**成因**：
```
@blocklet/sdk@1.17.11
  └── node_modules/@arcblock/did-connect-js@1.29.8 (pinned, 不是 ^)
        └── require('@ocap/client')   # pnpm 扁平化后命中顶层 1.30.9 (CBOR!)
```
**症状**：Node 后端 `blocklet dev` 悄悄发 CBOR 字节，与 master 钱包的 protobuf 解码器不兼容。**此 bug 在我们以为"Node 侧跟 CF 不一样"时隐藏了数小时**。
**解决**：
```json
"pnpm": {
  "overrides": {
    "@arcblock/did-connect-js>@ocap/client": "1.29.27"
  }
}
```
pnpm 装了一份 1.29.27 在 `@blocklet/sdk/node_modules/@ocap/client`，Node resolution 向上查找时命中。顶层和 CF Workers 的 1.30.9 完全不受影响。

### A4: @scure/bip32 seed 长度

**成因**：旧 `@wangshijun/hdkey` 宽容地接受任意长度 seed；`@scure/bip32` 严格 16-64 字节。wallet 的 `fromAppDid('', root.base64Seed, ...)` 里 `base64Seed` 是 base64 编码的字符串，直接当 bytes 传会得到 152 字节。
**症状**：`HDKey: seed length must be between 128 and 512 bits; got 152`
**解决**：
```js
const seedLike = (root?.base64Seed || '').slice(0, 64);
const newDID = fromAppDid('', seedLike, DID_TYPE_ARCBLOCK, 0);
```
**副作用 ⚠️**：现有用户的 wallet DID **会变**（seed 换算路径不同）。升级前必须评估用户数据迁移。

### A5: Dual webpack

**成因**：
- `react-scripts` 内 `node_modules/webpack@5.76.2`
- `node_modules/webpack@5.106.2`（被某个 dev dep 引入）

`config-overrides.js` 的 `require('webpack')` 命中 5.106，但 compiler 创建用 5.76。`IgnorePlugin` 实例在 apply 时调用 `compiler.hooks.normalModuleFactory.beforeResolve.tap(...)` 这个 hook 在两个版本间 shape 不同。
**症状**：`Cannot read properties of undefined (reading 'tap')`
**解决**：
```js
const webpack = require('react-scripts/node_modules/webpack');
```
显式锁到 react-scripts 自带的 webpack。

### B1: Rolldown __require shim

**成因**：1.30.x 部分 `@ocap/*` 包用 Rolldown 打包，其 ESM runtime 里 `__require = createRequire(import.meta.url)` 在 Node 里 OK，在 webpack 浏览器 bundle 里 createRequire 被 stub 成 noop。
**症状**：`@ocap/message/esm/patch.mjs` 模块 init 时 `__require('google-protobuf').Map` 炸。
**解决**：`mocks/rolldown-runtime-shim.js` + `webpack.NormalModuleReplacementPlugin` 按正则替换 `_virtual/rolldown_runtime.mjs`：
```js
const __require = (id) => {
  const loader = __requireModuleMap[id];
  if (!loader) throw new Error(...);
  return loader();
};
```
静态 switch 因为 webpack 只能 bundle 它能静态分析的模块。

### B3 + B5: `.cjs` 当静态资源

**成因**：
1. CRA 的 `oneOf` 最后一条是 file-loader fallback，match 所有未识别扩展名（包括 `.cjs`）。
2. babel-loader 的 test 是 `/\.(m?js|tsx?)$/`，**不含 `.cjs`**。
3. 结果 `.cjs` 文件被当作静态资源，`require('.../index.cjs')` 返回 URL 字符串。

**症状**：`mcrypto_1.Hasher === undefined`（因为 mcrypto_1 其实是一个路径字符串）。

**解决**：webpack rule 注入到 `oneOf` 顶部：
```js
for (const rule of config.module.rules) {
  if (Array.isArray(rule.oneOf)) {
    rule.oneOf.unshift({
      test: /[\\/]node_modules[\\/]@ocap[\\/].*\.cjs$/,
      type: 'javascript/auto',
    });
    break;
  }
}
```
**关键点**：scope **只限 `@ocap/*`**，否则会把 axon 等 Node-only CJS 也拉进来，触发 `Can't resolve 'net'`。

### C1 + D4: Bundle 没瘦身的真相

**Phase 2 commit message 宣称 "drops ~300KB google-protobuf"**，实测 `cfa2fb51` 只改了两个点，protobuf 还在 bundle 里，**实际只省了 5 KiB gzip**。

**根因追踪（耗时最长的 bug）**：payment-kit 业务代码 6 处从顶层入口 import：
```
api/src/libs/util.ts              → import { toStakeAddress } from '@arcblock/did-util'
api/src/libs/wallet-migration.ts  → import { toDelegateAddress, toStakeAddress }
api/src/integrations/arcblock/stake.ts
api/src/integrations/arcblock/token.ts
api/src/routes/connect/shared.ts
api/src/integrations/arcblock/nft.ts  → '@ocap/asset'（非 mint/client）
```

`@arcblock/did-util` 顶层无条件 `import './protobuf.mjs'` → `@ocap/proto/lib/runtime.js` → 拖入 1.2 MB `*_pb.js` + 246 KB google-protobuf。

**解决**：改 6 个 import 到 `/cbor` / `/mint/client` 子路径。
**收益**：raw -735 KB (-21%)、gzip -103 KiB (-11%)。

**本质教训**：**CBOR 不自动瘦身 bundle**。切 wire 格式只是第一步，业务代码必须主动用 CBOR-only 子路径才能真省。

---

## 四、如果重新做一次：完整 Checklist

### Phase 0：前置调研（1-2 天）

- [ ] **读一遍 upstream CHANGELOG**，特别关注：
  - 签名 / 接口变化（`encoding` 参数是否必填？）
  - 依赖链路变化（`@wangshijun/hdkey` → `@scure/bip32` 这类替换）
  - 新 subpath 是否已经存在（`/cbor`、`/mint/client`）
- [ ] **画出完整依赖树**：`pnpm why @ocap/client`，找所有嵌套版本。标记谁 pin 了哪个。
- [ ] **确认包管理器**：pnpm 还是 yarn？这决定 override 语法。
- [ ] **列举自己业务代码对顶层 import 的使用**：`grep -rn "from '@arcblock/did-util'" src/`、`from '@ocap/asset'`、`from '@ocap/client'`。这些都是潜在的 "顶层 aggregate entry → 肥胖" 点。
- [ ] **准备"legacy 中间态"作为回退点**：切 wire 格式前，先让 SDK 升级在 legacy/protobuf 下稳定。

### Phase 1：SDK 升级（1-2 天）

- [ ] bump `package.json`，`pnpm install`
- [ ] **如果有 pnpm + 嵌套依赖**：先跑 `pnpm why @ocap/client | @ocap/message` 看扁平化后的版本，可能需要 `pnpm.overrides` 定点隔离
- [ ] **写 L1 smoke 脚本**（Node 不经 bundler 直接测 SDK），验证：
  - 新 SDK 的 `encode*Tx` / `sign*Tx` / `multiSign*Tx` 全部注册
  - 常见 tx 类型（V2、V3、rollup）编码/解码 roundtrip 成功
- [ ] **写 L2 smoke 脚本**（mirror 业务代码调用模式），避免先 build bundle 才发现 SDK 用法变了
- [ ] 对付 build-time 问题：
  - [ ] Rolldown shim（如果包用 Rolldown ESM）
  - [ ] `.cjs` 加入 JS 解析 rule
  - [ ] webpack 版本对齐（dual webpack 时锁到 react-scripts 自带版本）
  - [ ] CRA ModuleScopePlugin 视情况 remove
- [ ] 对付 breaking changes：
  - [ ] 搜所有 `fromAppDid` / `fromMasterSeed` 用法，检查 seed 长度
  - [ ] 搜所有 `toXxxAddress(payload)` / `preMintFromFactory({...})`，加 `encoding` 参数
  - [ ] 搜所有 Joi schema，确认 Valibot key-order 敏感点

### Phase 2：legacy 中间态（半天）

- [ ] 把所有 `@ocap/client` 改 `@ocap/client/legacy`，继续 protobuf wire
- [ ] **部署 legacy 1.30.9 到 staging**，先让它跑通跟 master 钱包的 E2E
- [ ] 这个中间态作为"SDK 升级独立变量"的验证点：如果这一步不通，回退容易

### Phase 3：CBOR 迁移（1-2 天，独立分支）

- [ ] 新分支
- [ ] `@ocap/client/legacy` → `@ocap/client` 默认
- [ ] did-connect-auth 的 txEncoder 换 `@ocap/client/encode` 的 `createTxEncoder`
- [ ] **pre-warm `fetchContext(chainHost)`** 在 `initAuth` 时调一次，避免首次请求 8s timeout
- [ ] **搜索所有 `@arcblock/did-util` 顶层 import，改 `/cbor`**（这一步是 bundle 瘦身的真正来源）
- [ ] **同理 `@ocap/asset` → `/mint/client`**
- [ ] 对应的 tsconfig `paths` 可能要加 `.d.mts` 映射（看 moduleResolution）
- [ ] Wallet 端必须有 dual-decode 能力（检查 CBOR tag `0xd9d9f7`，按需选 CBOR/protobuf 解码器）

### Phase 4：验证

- [ ] **bundle 体积实测**：用 esbuild meta.json 看 top 包，**不要信 commit message**
- [ ] **Node 后端单独测**：`blocklet dev` 能起来、pre-start tsc 过、能跟 master 钱包通
- [ ] **CF Workers 单独测**：`wrangler dev` 起来，扫 QR 能走通支付流
- [ ] **扫 QR 测实际支付**：要一笔真的上链 tx hash 作为"迁移成功"证据
- [ ] **错误路径也测**：用 balance 不足的钱包扫，看错误是否合理透传（当前 payment-kit 有 error-surface bug）

### Phase 5：收尾

- [ ] 两个分支都 push 到 origin，作为"legacy 回退线"和"CBOR 前进线"并存
- [ ] 旧的诊断脚本（sdk-v3-smoke、l2-wallet-chain-smoke）归档到 `tools/`，下次升级复用
- [ ] 写这份回顾文档（or update 本文档），记录坑和学到的东西

---

## 五、尚未解决的事项

| 项目 | 状态 | 影响 |
|---|---|---|
| **abt-wallet V3 multi-sign "o is not a function"** | 未定位 | 浏览器扩展里 V3 多签走不通，原因可能是 webpack tree-shake 或 Rolldown shim 未覆盖。L1 smoke 证明不是 SDK bug |
| **payment-kit 错误透传** | CBOR 分支已部分修复（commit `715ff9ae`：structured chain error parsing）；legacy 分支待合入 | 链拒 tx 时服务端返回破损 JWT，钱包只看到 "jwt empty" |
| **`new Error('CODE', 'reason')` 参数丢失** | 已知 bug | 钱包十余处 throw 点丢 reason，调试困难 |
| **钱包跨账户智能调度** | 产品 roadmap | 余额不够时应提示内部转账，而非 hard error |
| **`.mts` type 解析（CBOR 分支）** | tsconfig 未更新 | `blocklet dev` 的 pre-start tsc 报 6 处 "Cannot find module"，不影响 esbuild |
| **wallet DID 因 seed slice(0,64) 改变** | 未测 | 1.26.3 → 1.30.9 升级后，老用户的 wallet DID 可能变。**生产升级前必须做数据迁移评估** |
| **官方 @blocklet/sdk 升级到 1.30.x** | 等上游 | 现在用 pnpm override 定点 pin 1.29.27，等 SDK 官方发布 1.18.x 对齐 1.30.9 再彻底解决 |

---

## 附录：关键 commit 索引

### payment-kit
```
feat/cf-workers-migration (legacy 主线)
  e5cb9fdb  test(tools): SDK + wallet-chain smoke scripts
  e09e404b  fix(deps): pin did-connect-js's @ocap/client to 1.29.27
  e89b73a9  fix(api): close TS type gaps after 1.30.9
  f3aef135  fix(cloudflare): enable CORS on /__blocklet__.js
  54c98f71  fix(cloudflare): adapt payment to 1.30.9 wallet compat
  b6d8837a  chore(deps): bump @arcblock/@ocap SDK to ^1.30.9

feat/cbor-migration-pk (CBOR 线)
  715ff9ae  feat(api): structured chain error parsing for payment flow  ← 部分修 E1
  93028445  fix(api): annotate implicit-any params in payment.ts
  6727a62a  perf(cloudflare): drop google-protobuf from bundle (-720KB)
  2c6d2ddd  fix(cloudflare): pre-warm chain context cache at initAuth
  cfa2fb51  feat(cloudflare): Phase 2 — CBOR default
```

### abt-wallet
```
feat/cbor-migration-design (早期激进尝试，含审计文档)
  11953f9f  docs(audit): honest Phase 1 change review
  9ceaf3a8  chore(release): 4.20.0 — dual-mode CBOR migration
  57668a5a  fix(wallet-did): slice encrypted seed to 64 bytes  ← 关键修复

feat/sdk-upgrade-1.30-legacy (我的 Phase A 尝试)
  73eb0a2e  fix(build): force @ocap/mcrypto to CJS entry
  c33ef404  fix(build): pin webpack in config-overrides
  36c9401b  fix(chain): pin @ocap/client imports to /legacy

upgrade-sdk-to-1.30.9-dual-decode (用户自建的 WIP 分支)
  667d6d49  WIP: Stage 1 upgrade SDK 1.26.3 -> 1.30.9 (legacy only)
```
