概述
approval-controller
负责管理所有需要用户审批的请求。它提供了一个统一的接口来处理各种类型的审批流程,包括交易签名、权限授予、连接请求等。
主要功能
- 请求管理:添加、接受、拒绝审批请求
- 流程控制:管理多步骤审批流程
- 速率限制:防止重复请求和滥用
- 结果处理:处理审批后的成功/失败结果
- 状态管理:维护审批请求的状态和计数
架构图
graph TD
subgraph "Requester"
R["请求方(其他控制器/功能模块)"] --> M["RestrictedMessenger"]
end
M -->|add / addAndShow / has / accept / reject / updateRequestState / clear / startFlow / endFlow / setFlowLoadingText / showSuccess / showError| AC["ApprovalController"]
subgraph "ApprovalController"
S["State\n- pendingApprovals\n- pendingApprovalCount\n- approvalFlows"]
I["内存索引\n- #approvals: id → callbacks\n- #origins: origin → (type → count)\n- typesExcludedFromRateLimiting"]
AC --> S
AC --> I
AC --> UI
UI["UI"] --> AC
end
subgraph "Add Flow"
A1["参数校验\n#validateAddParams"] --> A2["限流检查\n(origin + type)"]
A2 --> A3["缓存 Promise 解析器\n#approvals.set(id, { resolve, reject })"]
A3 --> A4["计数 +1\n#addPendingApprovalOrigin"]
A4 --> A5["落库到状态\n#addToStore → pendingApprovals / count"]
end
AC -->|add / addAndShow| A1
subgraph "Accept Flow"
C1["定位请求与回调\nget(id), #getCallbacks"] --> C2["waitForResult?"]
C2 -->|否| C3["立即 resolve 调用方"]
C2 -->|是且expectsResult| C4["返回 resultCallbacks 给创建方"]
C2 -->|是但不支持| C5["抛出 ApprovalRequestNoResultSupportError"]
C3 --> C6["删除请求\n#delete(id)"]
C4 --> C7["创建方完成后 success()/error()\n最终 resolve/reject"]
C7 --> C6
end
AC -->|accept| C1
subgraph "Reject Flow"
R1["获取回调\n#getCallbacks"] --> R2["删除请求\n#delete(id)"] --> R3["reject Promise"]
end
AC -->|reject| R1
subgraph "Clear Flow"
CL1["遍历所有 id: reject(id, error)"] --> CL2["清空 #origins"]
CL2 --> CL3["重置 pendingApprovals / count"]
end
AC -->|clear| CL1
subgraph "Flow API"
F1["startFlow(id?, loadingText?, show?)"] --> F2["setFlowLoadingText(id, text)"]
F1 --> F3["success()/error() → #result()\n(内部发起结果页请求)"]
F3 --> F4["endFlow(id)(栈顶校验,LIFO)"]
end
AC -->|startFlow / setFlowLoadingText / success / error / endFlow| F1
核心状态模型
控制器维护三部分状态,均通过 this.update
原子更新:
pendingApprovals: Record<string, ApprovalRequest>
:所有待批请求(以id
为 key)pendingApprovalCount: number
:待批总数(便于订阅 UI)approvalFlows: ApprovalFlowState[]
:流程栈,用于"成功/错误"等结果页展示
同时有两个内存结构(非持久化)用于控制行为:
#approvals: Map<string, ApprovalCallbacks>
:每个请求的 Promise 解析器(resolve/reject)#origins: Map<origin, Map<type, count>>
:按来源与类型的限流计数
请求实体 ApprovalRequest
包含:
id
:请求唯一标识origin
:来源(如 'metamask' 或 dapp host)type
:请求类型(字符串)time
:发起时间戳requestData
:附加的只读数据requestState
:可变状态(UI 可能更新)expectsResult: boolean
:请求方是否需要"延迟结果回传"(见下)
去重与限流策略
- 默认:同一
origin
下,同一type
同时只能有一个挂起的请求。再次发起会抛错:- 错误文案:
Request of type '${type}' already pending for origin ${origin}. Please wait.
- 错误文案:
- 例外:
typesExcludedFromRateLimiting
列表中的类型不做限流(可并行多个)。
实现点:
- 调用
add/addAndShowApprovalRequest
时,内部#add
会检查#origins
;不符合策略直接抛错。 #addPendingApprovalOrigin
/#delete
会递增/递减计数。
消息系统绑定
构造器会注册一组 action handler,供外界调用(例如扩展的脚本/其他控制器通过 messenger 调用):
:clearRequests
→clear
:addRequest
→addAndShowApprovalRequest
(可选展示 UI),或add
:hasRequest
→has
:acceptRequest
→accept
:rejectRequest
→reject
:updateRequestState
→updateRequestState
:startFlow
/:endFlow
/:setFlowLoadingText
→ 流程控制(成功/错误页)
请求全生命周期
- 发起
add(opts)
:仅创建请求并返回 Promise(创建方持有)addAndShowApprovalRequest(opts)
:创建请求并触发 UI 显示(返回 Promise)- 这两者本质都调用了内部
#add
,区别是是否立即showApprovalRequest()
(UI 侧展示)
- UI 展示
- UI 订阅
pendingApprovals
和pendingApprovalCount
来渲染待批项 - UI 点击"允许/拒绝"时调用
accept(id, value, options)
或reject(id, error)
- 结束
accept
:- 立即删除请求或延迟删除(取决于
options
) - 如果
options.waitForResult = true
:- 且请求的
expectsResult = true
:创建方期望收到"延迟结果回调" - 控制器把一组
resultCallbacks
(success(error)
)通过AddResult
回传给请求创建方 - 创建方拿到回调后,在其后续业务完成时再回调,控制器才最终 resolve 外部 Promise
- 且请求的
- 如果
options.waitForResult = false
:控制器立即 resolve 外部 Promise
- 立即删除请求或延迟删除(取决于
reject
:删除请求并 reject 外部 Promiseclear
:将所有挂起请求全部reject
,常用于全局关闭/切换场景
- 结果页(可选)
startFlow({ id?, loadingText?, show? })
→ 新开一个流程(默认会触发 UI 展示)success(opts)
/error(opts)
→ 展示结果页(本质是内部再发起一个ORIGIN_METAMASK
的结果请求)endFlow({ id })
:结束流程(后进先出,校验 id 必须为栈顶流程)
关键方法解读
add
- 校验参数合法性(
#validateAddParams
) - 做限流校验(除非 type 允许并发)
- 创建 Promise,缓存 resolve/reject 至
#approvals
- 落库:
#addToStore
更新pendingApprovals
和pendingApprovalCount
addAndShowApprovalRequest
还会触发showApprovalRequest()
,让 UI 显示
返回值有两种形态:
expectsResult = true
:Promise<AddResult>
,包含resultCallbacks
(用于延迟"二段确认")- 否则:
Promise<unknown>
,值为accept
时传入的value
js
function addAndShowApprovalRequest(opts) -> Promise<unknown | AddResult> {
const promise = #add(
opts.origin,
opts.type,
opts.id,
opts.requestData,
opts.requestState,
opts.expectsResult,
)
// 触发 UI 展示(由宿主传入)
#showApprovalRequest()
return promise
}
function add(opts) -> Promise<unknown | AddResult> {
return #add(
opts.origin,
opts.type,
opts.id,
opts.requestData,
opts.requestState,
opts.expectsResult,
)
}
function #add(origin, type, id = nanoid(), requestData?, requestState?, expectsResult?) {
#validateAddParams(id, origin, type, requestData, requestState)
// 限流:同一 origin+type 默认只能挂 1 个(除非该 type 在豁免列表)
if (!typesExcludedFromRateLimiting.includes(type) && has({ origin, type })) {
throw resourceUnavailable(`Request of type '${type}' already pending for origin ${origin}. Please wait.`)
}
return new Promise((resolve, reject) => {
// 1) 记住 promise 的解析器
#approvals.set(id, { resolve, reject })
// 2) 计入限流索引
#addPendingApprovalOrigin(origin, type)
// 3) 落库到控制器状态,便于 UI 渲染
#addToStore(id, origin, type, requestData, requestState, expectsResult)
})
}
accept
- 读取
ApprovalRequest
并获取该 id 对应的 Promise 解析器 - 根据
options
决定是否先删除pendingApprovals
waitForResult = true
:- 若
approval.expectsResult !== true
→ 抛ApprovalRequestNoResultSupportError
- 把
resultCallbacks
传回给创建方;等待创建方在后续业务结束后调用success/error
- 创建方回调后,
accept
返回的 Promise 才最终 resolve
- 若
waitForResult = false
:立即 resolve- 最后确保请求已删除(若之前未删)
常见用法:
- "两阶段确认":发起方希望 UI 同意后还要"再等业务执行结果"(如发起链上交易后确认上链成功才真正完成)
js
function accept(id, value?, options?: { waitForResult?, deleteAfterResult? }) -> Promise<{ value?: unknown }> {
const approval = get(id) // 读取请求体(若不存在会抛错)
const callbacks = #getCallbacks(id) // 取出 promise 解析器
let deleted = false
// 若不需要等"二段结果",或不指定 deleteAfterResult -> 立即删除
if (!options?.deleteAfterResult || !options?.waitForResult) {
#delete(id)
deleted = true
}
return new Promise((resolve, reject) => {
const resultCallbacks = { // "二段结果"回调,由创建方在后续执行
success: (finalVal?) => resolve({ value: finalVal }),
error: (err) => reject(err),
}
// 需要等待结果,但请求未声明 expectsResult -> 抛不支持错误
if (options?.waitForResult && !approval.expectsResult) {
reject(new ApprovalRequestNoResultSupportError(id))
return
}
// 生成返回给创建方的值:
// - expectsResult=true:返回 { value, resultCallbacks }
// - 否则直接返回 value
const resolveValue = approval.expectsResult
? { value, resultCallbacks: (options?.waitForResult ? resultCallbacks : undefined) }
: value
// 通知创建方:UI 已同意(第一阶段)
callbacks.resolve(resolveValue)
// 不等待结果则立即 resolve 调用方
if (!options?.waitForResult) {
resolve({ value: undefined })
}
}).finally(() => {
// 如果之前没删,这里兜底删除
if (!deleted) #delete(id)
})
}
reject
- 删除请求并
reject
外部 Promise
js
function reject(id, error) {
const callbacks = #getCallbacks(id) // 若不存在抛 ApprovalRequestNotFoundError
#delete(id) // 从状态与索引中删除
callbacks.reject(error) // 拒绝 promise
}
has / get / getApprovalCount / getTotalApprovalCount
has
:查询某 id,或某 origin/type 组合是否存在待批get
:按 id 取请求体getApprovalCount
:按 origin、type 或二者组合统计getTotalApprovalCount
:全量统计
js
function get(id) -> ApprovalRequest | undefined {
return state.pendingApprovals[id]
}
function has({ id?, origin?, type? } = {}) -> boolean {
if (id) return #approvals.has(id)
if (origin && type) return Boolean(#origins.get(origin)?.get(type))
if (origin) return #origins.has(origin)
if (type) return Object.values(state.pendingApprovals).some(a => a.type === type)
throw new Error('Must specify a valid combination of id, origin, and type.')
}
function getApprovalCount({ origin?, type? } = {}) -> number {
if (!origin && !type) throw new Error('Must specify origin, type, or both.')
if (origin && type) return #origins.get(origin)?.get(type) || 0
if (origin) {
// 累加该 origin 的全部 type 数量
return sum(Array.from(#origins.get(origin)?.values() ?? []))
}
// 仅按 type 统计
return count(Object.values(state.pendingApprovals).filter(a => a.type === type))
}
function getTotalApprovalCount() -> number {
return state.pendingApprovalCount
}
流程(Flow)系统
Flow 是一个轻量的"页面流程栈",典型用于结果页(成功/错误)的展示与收尾。
startFlow({ id?, loadingText?, show? })
:开启流程,默认show=true
会触发 UI 显示setFlowLoadingText({ id, loadingText })
:更新提示文案success(opts)
/error(opts)
:展示成功/错误页(内部通过#result
再发起一个结果型批准请求)endFlow({ id })
:结束流程。若传入 id 不是栈顶,会抛EndInvalidFlowError
,避免意外打断其他流程
注意:
- Flow 与普通批准请求的生命周期层级相互独立,但常一起使用(比如一个交易批准→成功页)
js
function startFlow({ id = nanoid(), loadingText = null, show = true } = {}) -> { id, loadingText } {
update(draft => draft.approvalFlows.push({ id, loadingText }))
if (show !== false) #showApprovalRequest()
return { id, loadingText }
}
function endFlow({ id }) {
if (state.approvalFlows.length === 0) throw new NoApprovalFlowsError()
const current = state.approvalFlows[state.approvalFlows.length - 1]
if (id !== current.id) throw new EndInvalidFlowError(id, state.approvalFlows.map(f => f.id))
update(draft => { draft.approvalFlows.pop() })
}
function setFlowLoadingText({ id, loadingText }) {
const idx = state.approvalFlows.findIndex(f => f.id === id)
if (idx === -1) throw new MissingApprovalFlowError(id)
update(draft => { draft.approvalFlows[idx].loadingText = loadingText })
}
推荐用法与最佳实践
-
简单批准(一次性):
- 发起方:
addAndShowApprovalRequest
(或add
并自行触发 UI) - UI:根据
pendingApprovals
展示,调用accept(id)
或reject(id)
- 发起方:
await
返回值,继续后续逻辑
- 发起方:
-
两阶段批准(等待外部结果):
- 发起方:
addAndShowApprovalRequest({ expectsResult: true })
,得到AddResult
- UI:
accept(id, value, { waitForResult: true })
- 发起方:拿到
resultCallbacks
后执行业务;业务成功调用resultCallbacks.success(data)
,失败调用resultCallbacks.error(error)
- 发起方:
-
结果页:
const { id } = startFlow({ loadingText: 'Processing...' })
- 成功:
await success({ message: 'Done', flowToEnd: id })
;失败:await error({ error: 'Failed', flowToEnd: id })
-
去重豁免:
- 某些类型(如系统级提示)可加入
typesExcludedFromRateLimiting
,允许同源并发请求
- 某些类型(如系统级提示)可加入
-
清理与中断:
- 场景切换(如网络切换/锁屏)可调用
clear(rpcErrors.internal('...'))
统一中断
- 场景切换(如网络切换/锁屏)可调用
端到端示例(两阶段批准)
发起方模块:
ts
// 创建请求,要求"二段确认"
const addResult = await messenger.call('ApprovalController:addRequest', {
origin: 'my-feature',
type: 'do-something',
requestData: { payload },
expectsResult: true,
}, true /* shouldShowRequest */);
// UI 同意后,这里才会拿到 resultCallbacks。现在执行业务:
try {
const result = await doHeavyWork(payload); // 比如链上操作
addResult.resultCallbacks?.success(result); // 触发最终 resolve
} catch (e) {
addResult.resultCallbacks?.error(e as Error); // 触发最终 reject
}
UI 模块:
ts
// 展示 pendingApprovals,用户点"同意"
await messenger.call('ApprovalController:acceptRequest', id, { ok: true }, {
waitForResult: true, // 告诉控制器:外部要等"二段确认"
deleteAfterResult: true, // 二段完成后再删除
});
// 或者"拒绝"
messenger.call('ApprovalController:rejectRequest', id, rpcErrors.userRejectedRequest());
小结
ApprovalController
将"发起请求---展示---同意/拒绝---(可选)二段结果---结束"完整闭环收口为简洁一致的 API。- 通过
expectsResult/waitForResult
精准控制 Promise 的"何时 resolve",支持复杂链路的"二段确认"。 - 内置去重限流、结果页流程栈、统一清理等能力,适用于浏览器扩展的多模块协作与复杂 UX 场景。
学习交流请添加vx: gh313061
下期预告:构建账户控制器(AccountsController)