# 钱包接入定位 · WALLET（E#6 / 门槛 G7）

> 范围：`FEATURE-MATRIX.md` E#6、`MAINNET-PUBLIC-READINESS.md` G7（钱包 / 市场接入）。
> 一句话：**FTO 自带市场与全流程，把浏览器钱包当作「可替换的 PSBT 签名器」**——不追某个尚未统一的钱包标准，而是定义一个极小的能力接口，探测所有满足它的钱包；新增钱包 = 加一行。

## 1. 定位（为什么是「自带市场 + 可替换」）

G7 的门槛是可选的：**标准被 UniSat 等识别** 或 **明确「自带市场」定位且可替换**。当前 Fractal 生态不存在单一浏览器钱包标准：

| 钱包 | Fractal | 浏览器接口 | 形态 |
| --- | --- | --- | --- |
| **UniSat**（参考实现） | ✅ 主网/测试网 | `window.unisat`（自有） | 专有，最完整 |
| **OKX Wallet** | ✅ | `window.okxwallet.bitcoin`（UniSat 形） | 与 UniSat 高度相似 |
| Magic Eden | 部分 | `window.magiceden[.bitcoin]` | 专有 |
| Xverse / Leather | 未确认 | `sats-connect` / `window.LeatherProvider` | 另一套 `request` 协议 |

因此 FTO 选择**明确自带市场**：订单、结算、流片（tape-out）、建厂/铸币/出题都在 FTO 自己的协议与页面里完成，钱包只负责**签名与付费**。签名器可替换 ⇒ 任何能 `signPsbt` 的浏览器钱包（或未来的硬件/托管签名器）都能接入，FTO 不被单一厂商锁定。这也与 G1「去托管」一致：服务端不持钥，签名面越小越好。

## 2. 能力接口（adapter 只认这些方法）

`fto/src/wallet.js`（纯逻辑，可 Node 测试）探测候选 provider，`fto/canvas/wallet.js`（浏览器胶水）统一为：

| 能力 | 方法 | 必需 |
| --- | --- | --- |
| 账户 | `requestAccounts()` \| `connect()` \| `getAccounts()` | ✅ |
| 签名 | `signPsbt(hex, { autoFinalized, toSignInputs })` | ✅ |
| 链 | `getChain()`（权威）\| `getNetwork()`（仅地址格式） | 可选 |
| 切链 | `switchChain(enum)` | 可选 |
| 挂单 | `signMessage(msg)`、`getPublicKey()` | 可选（挂单需要） |
| 资金 | `getUtxos()` | 可选（边端 `/api/utxo` 优先） |
| 事件 | `on("accountsChanged" / "chainChanged" / "networkChanged")` | 可选 |

页面只调用 adapter：`Wallet.detect()`、`Wallet.connect()`、`Wallet.sign(psbt, {address|toSignInputs})`、`Wallet.signMessage()`、`Wallet.switchToFractalTestnet()`、`Wallet.on()`、`Wallet.utxos()`。**页面不再直接碰 `window.unisat`**（由 `tools/wallet_check.mjs` 断言）。

**关键兼容点**：`getNetwork()` 在 Fractal 上只反映**地址格式**（测试网也用 `bc1`，同样返回 `livenet`），只有 `getChain().enum` 才能判断真实链。adapter 仅在 `enum` 明确为主网时告警，无法判断时**不打扰**用户（与改造前行为逐字一致）。

## 3. 探测顺序与多钱包选择

`PROVIDERS` 按偏好排序：`unisat → okx → magiceden → xverse → leather`。页面加载时 `mountSelector()`：**检测到 ≥ 2 个钱包**才在「连接钱包」按钮前插入一个下拉框（选择持久化在 `localStorage["fto.wallet"]`）；否则静默。`connect()` 顺序为：用户选择 → 上次保存 → 第一个**支持 `signPsbt`** 的钱包。状态行显示 `✓ 地址 · 钱包名 · 链`。

## 4. 接入清单 / 对接记录

| 钱包 | 状态 | 证据 |
| --- | --- | --- |
| **UniSat** | ✅ **线上真跑**（参考签名器） | G1 去托管流片：测试网 + 主网 + 线上边缘 + **线上浏览器钱包** 各 4/4（`signPsbt`，Fractal 测试网，客户端自持钥）；G9/R15「发」层线上真跑 12/0 | 
| **OKX Wallet** | ○ 接口兼容，**待真跑** | 能力探测通过（UniSat 形 `requestAccounts`/`signPsbt`/`getChain`）；尚未在测试网钱包实机跑通，接入清单按「待验证」记 |
| Magic Eden / Xverse / Leather | ○ 已登记，**未验证** | 已列入探测；`signPsbt`（或其 `sats-connect` 等价）未验证 |

> 说明：G7 门槛不要求每一个钱包都真跑，而是「明确自带市场定位 **且** 可替换」——本文件即定位与接入记录。**UniSat 作为参考实现已线上真跑**；其余按能力探测登记，接入方式见 §5。

## 5. 如何接入一个新钱包

1. 在 `fto/src/wallet.js` 的 `PROVIDERS` 加一行：`{ id, name, install, get: (g) => g.<global> }`（嵌套时用 `?.`）。
2. 若它的签名接口与 `signPsbt(hex, {toSignInputs})` 不同，在 `fto/canvas/wallet.js` 的 `connect()`/`sign()` 里为它加一层归一（例如 `sats-connect` 的 `request("signPsbt", …)`）——**只改 adapter，页面不动**。
3. 若需要新的链枚举/事件名，补 `normalizeChain()` / `on()`。
4. 加一个合成 provider 到 `tools/wallet_check.mjs`（模拟账户/签名/链/多钱包/无钱包），`node tools/verify_all.mjs` 全绿。
5. 有条件的，按 G1 线上真跑流程在测试网实机验证一次，并把结果补进 §4。

## 6. 安全与边界

- adapter **不接触私钥、不接触助记词**；只把 PSBT 交给钱包，回收签名后的 PSBT。服务端仍然 KEYLESS。
- 页面显示的地址即签名者；金额/收款人都由边端 `plan` + 链上真相层（`indexer.js`）决定，钱包签名不会改变协议规则。
- 选错链（主网）时状态行明确告警；线上边缘为**测试网**。
- 主网 `fail-closed` 守卫不变：`FTO_ALLOW_MAINNET=1` / `ALLOW_MAINNET_BROADCAST=0`；本项不改任何 wire/共识。

## 7. 与 G7 门槛的映射

| G7 要求 | 交付 |
| --- | --- |
| 自带市场（PSBT） | 订单/结算/流片/发层页面 + Worker，全部经 `signPsbt`（既有） |
| 标准识别 **或** 明确自带市场定位且可替换 | **本文件**（定位）+ `fto/src/wallet.js`（能力接口）+ 多钱包探测 |
| 接入清单 / 对接记录 | §4（UniSat 真跑；OKX/其余登记待验证） |
| 可替换 | 加一行 `PROVIDERS`；页面零改动；`wallet_check.mjs` 用合成 provider 证明 |

## 8. CI

- `node fto/tools/wallet_check.mjs` —— 纯函数覆盖：无钱包 / 单钱包(UniSat) / 多钱包选择与优先级 / `getNetwork` 不误判 / `toSignInputs`/`signedHexOf` 归一 / 页面不含直连 `window.unisat` / 页面 import 了 `./wallet.js`。
- 语法检查：`fto/src/wallet.js`、`fto/canvas/wallet.js`、`canvas/app.js`、`canvas/market.js`、`canvas/issue.js`。
