浏览器插件钱包(二) - 处理DApp请求
前言:紧接上一篇,我们已经能够在Dapp上检测到我们的钱包插件了,那么接下来我们就需要处理来自于DApp的请求,以便更丰富的交互。
本系列将分为多次文章展示,敬请期待,也可以到github 仓库地址查看最新的代码更新。
请求失败
在上一篇文章的代码运行下,刷新网页会得到一个报错 ,如图:
并且点击connect按钮会报错,如图:

这是因为我们没有提供一个可以使用的provider给DApp,本篇文章主要就是解释如何提供provider。
架构图
首先我们需要理解chrome extension的content.js和注入到页面的injected.js以及background.js之间的关系:

- 首先我们需要三个主体:Content, injected provider, Background
- 使用Content将injected provider注入到页面声明eip6963的provider,并且监听provider发出的事件
- Content将监听的事件发送到Background进行处理,然后将处理的结果发送给injected provider
完成这个图上的消息传递是整个系统中的基础,完成之后我们就能够打通Dapp与插件钱包的消息传递以及交互。
主要代码片段
ContentProvider.ts
ts
export class ContentProvider {
constructor() {
// 监听来自页面的消息
window.addEventListener('message', this.handlePageMessage);
// 监听来自扩展的消息
chrome.runtime.onMessage.addListener(this.handleExtensionMessage);
}
// 处理来自页面的消息
private handlePageMessage = (event: MessageEvent) => {
// 忽略来自其他源的消息
if (event.source !== window) return;
const data = event.data;
// 检查消息是否来自我们的注入脚本
if (data && typeof data === 'object' && data.type === `${MESSAGE_PREFIX}${MessageType.REQUEST}`) {
// 解析请求
const request = data.payload as JsonRpcRequest;
// 发送请求到扩展的后台脚本
chrome.runtime.sendMessage(
{ type: `${MESSAGE_PREFIX}${MessageType.REQUEST}`, payload: request },
(response: JsonRpcResponse) => {
// 将响应发送回页面
window.postMessage(
{
type: `${MESSAGE_PREFIX}${MessageType.RESPONSE}`,
payload: response
},
'*'
);
}
);
}
};
// 处理来自扩展的消息
private handleExtensionMessage = (
message: any,
sender: chrome.runtime.MessageSender,
sendResponse: (response: any) => void
) => {
// 检查消息类型
if (message && typeof message === 'object' && message.type === `${MESSAGE_PREFIX}${MessageType.RESPONSE}`) {
// 将响应发送回页面
window.postMessage(message, '*');
return true; // 保持消息通道打开以进行异步响应
}
return false;
};
// 清理方法
public cleanup() {
window.removeEventListener('message', this.handlePageMessage);
// 注意:Chrome 扩展 API 不提供 removeListener 的标准方式
}
}
injectedProvider.ts
ts
class PageProvider {
private requestIdCounter = 0;
private pendingRequests = new Map<number | string, (response: any) => void>();
constructor() {
// 添加消息监听器,用于接收来自 content script 的响应
window.addEventListener('message', this.handleContentScriptMessage);
}
// 处理来自 content script 的消息
private handleContentScriptMessage = (event: MessageEvent) => {
// 安全检查:确保消息来自当前窗口
if (event.source !== window) return;
const data = event.data;
// 验证消息格式
if (
data &&
typeof data === 'object' &&
data.type === `${MESSAGE_PREFIX}${MessageType.RESPONSE}` &&
data.payload
) {
const response = data.payload;
const callback = this.pendingRequests.get(response.id);
if (callback) {
callback(response);
this.pendingRequests.delete(response.id);
}
}
};
request = (args: any): Promise<any> => {
return new Promise((resolve, reject) => {
const id = this.requestIdCounter++;
// 创建 JSON-RPC 请求
const request = {
id,
jsonrpc: '2.0',
method: args.method,
params: args.params
};
// 存储回调
this.pendingRequests.set(id, (response) => {
if (response.error) {
reject(new Error(response.error.message));
} else {
resolve(response.result);
}
});
// 发送消息到 content script
window.postMessage(
{
type: `${MESSAGE_PREFIX}${MessageType.REQUEST}`,
payload: request
},
'*'
);
});
}
// 当 PageProvider 销毁时清理监听器
disconnect() {
window.removeEventListener('message', this.handleContentScriptMessage);
}
}
content.ts
ts
import { defineContentScript } from 'wxt/sandbox';
import contentProvider from './provider';
export default defineContentScript({
matches: ['<all_urls>'],
runAt: 'document_start',
main() {
// 注册 content provider
contentProvider;
// Inject the script into the page
const injectScript = () => {
try {
const script = document.createElement('script');
script.src = chrome.runtime.getURL('injected.js');
script.onload = () => script.remove();
(document.head || document.documentElement).appendChild(script);
} catch (error) {
console.error('Error injecting script:', error);
}
};
// Execute injection when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', injectScript);
} else {
injectScript();
}
},
});
background.ts
ts
import { defineBackground } from 'wxt/sandbox';
import { MESSAGE_PREFIX, MessageType } from '../../constants';
// 处理 RPC 请求
async function handleRpcRequest(request: any) {
try {
// 根据请求的方法执行相应的操作
switch (request.method) {
case 'eth_accounts':
return ['0x0000000000000000000000000000000000000000'];
case 'eth_chainId':
return '0x1'; // Ethereum Mainnet
case 'net_version':
return '1'; // Ethereum Mainnet
case 'eth_requestAccounts':
// 这里应该触发钱包 UI 来请求用户授权
// 现在我们简单返回一个固定地址
return ['0x0000000000000000000000000000000000000000'];
default:
throw new Error(`Method not supported: ${request.method}`);
}
} catch (error) {
console.error('RPC request error:', error);
throw error;
}
}
export default defineBackground({
main() {
// 监听来自 content script 的消息
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
// 检查消息是否来自我们的 content script
if (
message &&
typeof message === 'object' &&
message.type === `${MESSAGE_PREFIX}${MessageType.REQUEST}`
) {
const request = message.payload;
// 处理 RPC 请求
handleRpcRequest(request)
.then(result => {
// 发送成功响应
sendResponse({
id: request.id,
jsonrpc: request.jsonrpc,
result
});
})
.catch(error => {
// 发送错误响应
sendResponse({
id: request.id,
jsonrpc: request.jsonrpc,
error: {
code: -32603, // Internal error
message: error.message || 'Internal error'
}
});
});
// 返回 true 表示我们将异步发送响应
return true;
}
});
},
});
完成之后我们就能与Dapp进行交互了,也不会报错了,只会报错我们目前不支持的方法,因为目前我们还没有一个钱包所以,下一篇文章《浏览器插件钱包(三) - 创建钱包》敬请期待。
下期预告
《浏览器插件钱包(三) - 创建钱包》
求打赏
文章总结不易,欢迎各位打赏,打赏越快,更新越快,谢谢~