在web2开发中,前端与后端通信大多通过HTTP的方式,比如说用的最多的Axios.js, 是一个基于 Promise 的 HTTP 客户端库,主要用于在浏览器和 Node.js 环境中发送 HTTP 请求.但在 DApp(去中心化应用)中,通信方式发生了本质性的变化。
DApp 前端如何与合约通信?
我们以 ethers.js 为例,介绍通信流程。
步骤一,获取用户钱包授权
const provider = new ethers.BrowserProvider(window.ethereum);
await provider.send("eth_requestAccounts", []); // 请求连接钱包
const signer = await provider.getSigner(); // 获取用户签名器(用于发交易)
步骤二,加载合约实例
const contractAddress = '0xYourContractAddress';
const contractABI = [...] // 从部署后的合约 artifacts 中获取
const contract = new ethers.Contract(contractAddress, contractABI, signer);
步骤三:调用合约的读取方法(不会产生交易)
const name = await contract.name(); // 读取合约中的状态,不上链
console.log("Token Name:", name);
步骤四:调用合约的写入方法(会生成交易)
const tx = await contract.mint("0xRecipientAddress", 1); // 写操作,上链 await tx.wait(); // 等待交易打包确认 console.log("NFT minted");
接下来介绍一下ethers.js与合约通信的的源码解析,首先看Contract类https://github.com/ethers-io/ethers.js/blob/v6/lib.esm/contract/contract.js
export class BaseContract {
constructor(target, abi, runner, _deployTx) {
.....
return new Proxy(this, {
get: (target, prop, receiver) => {
.....
return target.getFunction(prop);
});
}
}
实例化一个合约,需要传入合约地址、abi以及singer,当一个合约实例执行合约的方法的时候,从源码可以看出实际执行的是 target.getFunction(prop),abi的处理在之前的文章有介绍过.
getFunction(key) {
if (typeof (key) !== "string") {
key = key.format();
}
const func = buildWrappedMethod(this, key);
return func;
}
执行buildWrappedMethod时,会根据fragment的constant来判断具体执行哪个function如果constant为true(即执行合约的function是view或者view),则执行staticCall
const fragment = getFragment(...args);
if (fragment.constant) {
return await staticCall(...args);
}
return await send(...args);
我们来看看staticCall的具体逻辑,
const staticCallResult = async function (...args) {
const runner = getRunner(contract.runner, "call");
assert(canCall(runner), "contract runner does not support calling", "UNSUPPORTED_OPERATION", { operation: "call" });
// tx的值为
// {
// data: "0x22f4596f",
// to:""0x5FbDB2315678afecb367f032d93F642f64180aa3""
// }
const tx = await populateTransaction(...args);
let result = "0x";
try {
// 执行合约的方法
// 在 ethers/src.ts/providers/abstract-provider.ts 文件中
// 它最终会发送一个 eth_call JSON-RPC
result = await runner.call(tx);
}
catch (error) {
if (isCallException(error) && error.data) {
throw contract.interface.makeError(error.data, tx);
}
throw error;
}
const fragment = getFragment(...args);
// 这里执行的是this.#abiCoder.decode(fragment.outputs, bytes);
return contract.interface.decodeFunctionResult(fragment, result);
};
最终是通过runner.call来发送JSON-RPC,runner就是provider