# Payment Kit Cloudflare Workers: 本地开发环境搭建与链上交互修复

## 总览

本次工作围绕两个目标展开：

1. **搭建 CF Workers 本地开发环境**，实现 payment-kit + blocklet-service 双 worker 协同运行，支持端到端支付测试。
2. **修复链上交互在 Workers 环境中的阻塞问题**，确保一次性支付和订阅支付正常完成。

### 核心发现与修复

| # | 问题 | 根因 | 修复 | 影响 |
|---|------|------|------|------|
| 1 | Worker hang：所有链上相关的 HTTP 请求永远 pending | axios 的 Node.js HTTP adapter 依赖 `node:http`（在 bundle 中被 shim 成空壳） | esbuild plugin 将 axios http adapter 重定向到 fetch adapter | 线上 + 本地 |
| 2 | 订阅支付 onConnect 超时（>8s） | delegation 链上查询串行累积延迟 6-10s | Promise.all / Promise.allSettled 并发化 | 线上 + 本地 |
| 3 | `encodeTransferV3Tx` 在 workerd 下 hang | `@ocap/client` 的 `getContext()` 内部 `setTimeout(resolve, 0)` 在 timer shim 下不触发 | 提前 `await client.getContext()` warm up 缓存 | 线上 + 本地 |
| 4 | 支付成功页前端崩溃 | vendor status API 返回 `vendors: null`，前端 `.map()` 报错 | null 防御 | 线上 + 本地 |
| 5 | webhook 发送在 CF Workers 下失败 | `componentApi` shim 缺少 `request()` 方法 | 补充 `request()` 实现 | 线上 + 本地 |
| 6 | 无迁移历史时每次订阅都查链 | `getMigratedFromList` 空结果不缓存（`length > 0` 才缓存） | 空数组也缓存，加 5 分钟 TTL | 线上 + 本地 |

### 改动文件一览

| 文件 | 类型 | 影响范围 |
|------|------|---------|
| `cloudflare/run-build.js` | 构建配置：新增 axiosFetchAdapterPlugin | 线上 + 本地 |
| `cloudflare/shims/axios-http-adapter.ts` | 新建：axios fetch adapter re-export | 线上 + 本地 |
| `cloudflare/shims/blocklet-sdk/component-api.ts` | 补充 `request()` 方法 | 线上 + 本地 |
| `api/src/libs/wallet-migration.ts` | 并发优化 + 缓存修复 + 自检 bug 修复 | 线上 + 本地 |
| `api/src/routes/connect/pay.ts` | getContext warm-up | 线上 + 本地 |
| `packages/payment-react-headless/src/checkout/hooks/useSubmit.ts` | vendors null 防御 | 线上 + 本地 |
| `packages/react/src/checkout-v2/views/success-view.tsx` | vendors null 防御 | 线上 + 本地 |
| `cloudflare/wrangler.dev.toml` | 本地开发配置（已 gitignore） | 仅本地 |
| `cloudflare/.dev.vars` | 本地密钥（已 gitignore） | 仅本地 |

---

## 一、本地开发环境搭建

### 1.1 架构

```
Browser/Wallet
      │
      ▼
┌─────────────────────────────────────────┐
│  cloudflared tunnel                      │
│  test-payment-tunnel.ofind.cn → :8787   │
└─────────────────┬───────────────────────┘
                  │
      ┌───────────┴───────────┐
      ▼                       ▼
┌──────────────┐    ┌──────────────────┐
│ payment-kit  │◄──►│ blocklet-service │
│ :8787        │    │ :8788            │
│ (wrangler)   │    │ (wrangler)       │
├──────────────┤    ├──────────────────┤
│ D1 (local)   │    │ D1 (local)       │
│ KV (local)   │    │ settings 表      │
│ Assets       │    │ users/sessions   │
└──────────────┘    └──────────────────┘
```

### 1.2 启动步骤

```bash
# 1. 启动 blocklet-service（DID 认证）
cd did/services/blocklet-service
npx wrangler dev --port 8788 --local

# 2. 启动 payment-kit
cd payment-kit/blocklets/core
npx wrangler dev -c cloudflare/wrangler.dev.toml --local --port 8787

# 3. 启动 tunnel（可选，手机钱包测试需要）
cloudflared tunnel --config /tmp/cf-tunnel-config.yml --protocol http2 run did-dev
```

### 1.3 关键配置

**`cloudflare/wrangler.dev.toml`** — 本地开发专用：
- 无 `[build]` 段，直接使用预构建的 `dist/worker.js`
- D1/KV 使用本地模式（假 ID）
- `AUTH_SERVICE` service binding 连接本地 blocklet-service
- `APP_PID` 必须是从 `APP_SK` 推导出的合法 DID（否则前端 `getDIDMotifInfo` 崩溃，钱包 JWT 验签失败）

**身份一致性要求**：
- payment-kit 和 blocklet-service 共用同一个 `APP_SK`
- `APP_PID` = 从 SK 推导的 DID
- blocklet-service D1 的 `settings` 表中需注册该 DID 的 `app:sk`
- `__blocklet__.js` 返回的 `appPid`、`did`、`appPk` 三者必须一致

### 1.4 数据准备

从 staging D1 导出数据并导入本地：

```bash
# 导出
npx wrangler d1 export payment-kit-staging --remote -c cloudflare/wrangler.local.toml --output /tmp/staging-dump.sql

# 提取 INSERT 语句，按表导入
grep '^INSERT INTO.*products' /tmp/staging-dump.sql > /tmp/staging-products.sql
npx wrangler d1 execute payment-kit-local --local -c cloudflare/wrangler.dev.toml --file /tmp/staging-products.sql
```

---

## 二、axios fetch adapter 替换

### 2.1 问题

axios 在 Node.js 环境下默认使用 `http` adapter，依赖 `node:http` 和 `follow-redirects`。在 Worker bundle 中，这两个模块被 esbuild alias 替换为空壳 shim，导致 `http.request()` 永远不回调 → Worker hang → 500。

### 2.2 修复

在 `run-build.js` 中新增 esbuild plugin `axiosFetchAdapterPlugin`：
- 拦截 `axios/lib/adapters/http.js` 导入，重定向到 `fetch.js`
- 拦截 `follow-redirects` 和 `proxy-from-env`（仅被 http adapter 使用）指向 noop

### 2.3 风险评估（已自检）

**自检结论：无风险。** 全项目扫描确认所有 axios 调用仅使用 fetch 兼容选项（`timeout`、`baseURL`、`params`、`headers`）。

附带发现：exchange-rate providers 中 `error.code === 'ECONNABORTED'` 等 Node.js 错误码检查，fetch adapter 不产生这些错误码，会 fallback 到通用 branch。不影响功能，仅错误信息不够精确。

---

## 三、delegation 链上查询并发优化

### 3.1 问题

`getDelegationAddressWithFallback` 串行执行多次链上 RPC，总延迟 6-10s，超过钱包 8s 超时。

### 3.2 修复

- Step 2（getDelegateState）与 Step 3（getMigratedFromList）改为 `Promise.all` 并发
- Step 3 内部多个 migratedFrom 地址查询改为 `Promise.allSettled` 并发
- 总延迟从 ~8s 降到 ~2-4s

### 3.3 自检发现与修复

| 问题 | 影响 | 修复 |
|------|------|------|
| `currentStateResult.error` 被无条件赋值覆盖 | 失败时错误信息丢失 | 赋值时保留 `error` 字段 |
| `results.indexOf(result)` 获取索引 | O(n) 且不符合惯例 | 改为 `for (let i = 0; ...)` |
| `getMigratedFromList` 空结果不缓存 | 最常见场景每次都查链 | 空数组也缓存，加 5 分钟 TTL 防止迁移后缓存过期 |

### 3.4 风险评估（已自检）

| 风险项 | 自检结果 |
|--------|---------|
| 短路逻辑弱化（step 2 命中时 step 3 已发出） | 影响极小，仅多一次 getAccountState |
| 链节点并发压力 | migratedFrom 典型 0-3 个，无需 pLimit |
| 错误传播变化 | 语义与原来一致（原来也 catch 继续） |
| 空缓存 TTL 过期后重新查链 | 5 分钟刷新一次，平衡了性能和数据新鲜度 |

---

## 四、getContext warm-up

在 `pay.ts` 的 `onAuth` 中提前 `await client.getContext()` 让缓存 ready，避免 `encodeTransferV3Tx` 内部的 `setTimeout(resolve, 0)` 在 workerd timer shim 下 hang。

风险：几乎无。有内部缓存，通常在 `onConnect` 阶段已 warm up。

---

## 五、componentApi.request() shim 补充

`queues/webhook.ts` 调用 `componentApi.request()` 发送 webhook 通知，但原 shim 只实现了 `post()` 和 `get()`。补充 `request()` 方法，支持自定义 method、headers、timeout（通过 AbortController）。

---

## 六、前端 vendors null 防御

- `useSubmit.ts`：确保 `vendors` 始终为数组
- `success-view.tsx`：`.map()` 前加 `|| []` 防御

无风险，纯防御性代码。

---

## 七、自检覆盖范围

### 已完成的自检

| 检查项 | 方法 | 结论 |
|--------|------|------|
| axios fetch adapter 兼容性 | 全项目 grep 所有 axios 调用和选项 | **无风险**：无 Node.js 特有选项使用 |
| migratedFrom 并发数量 | 源码分析 + 测试数据 | **低风险**：典型 0-3 个 |
| Promise.allSettled 索引正确性 | 代码审查 | **已修复**：`indexOf` 改为直接索引 |
| currentStateResult.error 丢失 | 代码审查 | **已修复**：保留 error 字段 |
| 空缓存无 TTL | 逻辑分析 | **已修复**：加 5 分钟 TTL |
| componentApi shim 完整性 | grep 所有 componentApi 方法调用 | **已修复**：补充 `request()` |

### 需要仕军 review 的内容

以下改动涉及链上交互的核心逻辑，建议仕军重点审查：

#### 1. `wallet-migration.ts` — 并发化改动的业务正确性

**关注点**：
- 原来串行查询有隐含的优先级（stored → current → migrated），并发后 step 2 和 step 3 同时发出。如果 `current` 地址命中，`getMigratedFromList` 的链上请求已经发出但结果会被丢弃。确认这是否有副作用。
- `Promise.allSettled` 返回的结果顺序是否保证和 `migratedFrom` 输入顺序一致（根据 ES 规范是保证的，但建议确认）。
- 空缓存 TTL 5 分钟是否合理 — 应用迁移后需要等最多 5 分钟才能发现新的 migratedFrom。

#### 2. `pay.ts` — getContext warm-up 的必要性

**关注点**：
- `getContext()` 内部的 `setTimeout(resolve, 0)` 是 `@ocap/client` 的实现细节。如果上游升级移除了这个 setTimeout，warm-up 就变成冗余代码。建议确认是否有更根本的方式处理 timer shim 和 `getContext` 的交互。
- 线上 Cloudflare Workers（非 wrangler dev local）是否也受 `setTimeout(resolve, 0)` 影响，还是仅本地 workerd 有问题。

#### 3. axios fetch adapter — 对 exchange-rate providers 的影响

**关注点**：
- `coingecko-provider.ts`、`coinmarketcap-provider.ts`、`token-data-provider.ts` 中的错误处理依赖 `error.code === 'ECONNABORTED'` 等 Node.js 错误码。fetch adapter 下这些分支永远不会触发，错误会 fallback 到通用处理。确认通用处理的行为（重试策略、降级逻辑）是否足够。

#### 4. componentApi.request() shim — 与原始实现的差异

**关注点**：
- 原始 `@blocklet/sdk` 的 `componentApi` 会在请求头中自动添加签名（`x-component-sig` 等），shim 版本没有签名逻辑。webhook 接收方如果验证签名，请求会被拒绝。
- shim 的 `request()` 使用 `AbortController` 实现 timeout，与原始 axios 的 timeout 行为可能有细微差异（如连接超时 vs 响应超时）。

---

## 八、验证结果

| 场景 | 本地环境 | staging 线上 |
|------|---------|-------------|
| 一次性支付（链上） | ✅ | ✅ |
| 订阅支付（delegation + stake） | ✅ | ✅（手机钱包） |
| 前端 checkout 页面 | ✅ | ✅ |
| DID Connect 认证流程 | ✅ | ✅ |
| `__blocklet__.js` 身份一致性 | ✅ | ✅ |

Web 钱包扩展有少量非阻塞报错（EVM 余额查询超时、内部队列暂停），属于钱包端问题，不影响支付流程。

---

## 九、排查过程记录

### 使用的排查手段

| 手段 | 用途 | 效果 |
|------|------|------|
| `vite build --minify false` | 让前端错误堆栈可读 | 从 `cc`/`z7` 定位到 `getDIDMotifInfo`/`convert.hex.rgb` |
| `console.log` 关键路径插桩 | 确定 hang 的精确位置 | 定位到 `encodeTransferV3Tx` |
| `grep` bundle 分析 | 检查 `node:http`/`follow-redirects` | 确认 axios 用了错误的 adapter |
| D1 dev endpoint 查数据 | 检查 payment intent 状态 | 发现 `processing` 脏状态 |
| JWT base64 解码 | 检查身份字段 | 发现 appPid/appPk/did 不一致 |
| `@ocap/client` 源码阅读 | 追溯调用链 | 找到 `getContext` → `setTimeout` 根因 |
| wrangler 请求日志时序分析 | 分析并发模式 | 理解 workerd 并发限制 |
| 全项目 axios 调用扫描 | 自检 fetch adapter 兼容性 | 确认无 Node.js 特有选项使用 |

### 问题解决时间线

```
环境搭建
  ├─ wrangler.dev.toml + D1 migration + staging 数据导入
  ├─ blocklet-service instance 注册
  └─ cloudflared tunnel 建立

前端报错 "null is not iterable"
  └─ 根因：APP_PID 不是合法 DID → getDIDMotifInfo 返回 null

JWT 验签失败
  └─ 根因：appPk(blocklet-service) ≠ APP_SK(payment-kit) → 复用同一 SK

Worker hang (一次性支付)
  ├─ 初始判断：链上 RPC 超时 → 加 skip 逻辑（错误方向，已撤回）
  ├─ 最终根因：axios http adapter 被 shim 成空壳
  └─ 修复：esbuild plugin 重定向到 fetch adapter

Worker hang (订阅支付)
  ├─ 表现：onConnect 阶段 10s 超时
  ├─ 根因：delegation 查询串行 6-10s
  └─ 修复：Promise.all / Promise.allSettled 并发

自检
  ├─ axios 兼容性：全项目确认无风险
  ├─ migratedFrom 数量：确认典型 0-3 个
  ├─ Bug: currentStateResult.error 被覆盖 → 已修复
  ├─ Bug: results.indexOf(result) 不安全 → 已修复
  ├─ 优化: 空 migratedFrom 不缓存 → 已修复，加 TTL
  └─ Bug: componentApi.request() 缺失 → 已修复
```
