Hardhat编写、编译、部署、测试Solidity ERC20合约 - 进阶篇 - JSON-RPC调用合约方法
1. Remix编写、编译、部署、测试Solidity ERC20合约 - 基础篇
2. Remix编写、编译、部署、测试Solidity ERC20合约 - 进阶篇
3. Hardhat编写、编译、部署、测试Solidity ERC20合约 - 基础篇
4. JSON-RPC调用区块链方法
5. JSON-RPC调用合约方法
6. JSON-RPC给合约账户同时转以太币和代币
7. web3.js调用区块链方法
8. web3.js调用合约方法
9. web3.js给合约账户同时转以太币和代币
10. Go-Ethereum调用区块链方法
11. Go-Ethereum调用合约方法
12. sendTransaction和sendRawTransaction区别
13. Metamask导入代币,转账ETH,转账代币 - 界面操作
14. Metamask连接hardhat本地节点
15. DAPP react界面-web3.js库-Metemask调用和显示-调用合约方法
16. web3js结合sepolia测试网
17. 总结
对比系列中的此篇文章
4. web3.js调用合约方法
8. JSON-RPC调用区块链方法
1. 以太坊通信协议
以太坊基于区块链,区块链是去中心化的,即没有中心服务端节点,或者说每个节点都是中心服务端节点。
去中心化应用程序DAPP通过JSON-RPC协议请求区块链节点数据,区块链节点通过JSON-RPC协议响应数据。
节点间通过JSON-RPC协议同步交易数据和区块数据。

本节中使用hardhat node作为以太坊节点,手写调试代码作为DAPP。后续会以sepolia测试网作为以太坊节点,web3.js等作为DAPP。

1.1 JSON-RPC
RPC即远程过程调用,一般用于多进程通信。
JSON-RPC是基于JSON格式来进行RPC通信。
TCP通信传输16进制字节流,接收端要按通信协议对字节流逐段截取,将字节数组转换为实际含义值,再进行条件分支流转。通信协议较低层,数据交互复杂。
HTTP一般为前后端应用系统使用,HTTP协议承载JSON格式数据,请求的url中包含了后端接口映射名,前端的请求可以直接访问到后端接口。应用层通信协议,数据交互简单。
JSON-RPC是应用层协议,可以基于HTTP、TCP等底层协议,来承载JSON格式数据。以太坊是基于http post。请求的url中的后端接口并不是实际要调用的接口,而是一个通用接口。实际要调用的接口名称在JSON格式的数据中。
1.1.1 请求对象
用于客户端调用服务端的方法,结构如下:
{
"jsonrpc": "2.0",
"method": "methodName",
"params": ["param1", "param2"],
"id": 1
}
jsonrpc:协议版本,固定为2.0。
method:调用的远程方法名。
params:方法参数,可以是数组或对象。
id:唯一标识请求的ID,用于匹配响应。可为数字、字符串或null。
示例:
{
"jsonrpc": "2.0",
"method": "add",
"params": [4, 5],
"id": 123
}
2.1.2 响应对象
用于服务端返回结果或错误信息,结构如下:
成功响应:
{
"jsonrpc": "2.0",
"result": 9,
"id": 123
}
jsonrpc:协议版本。
result:调用方法的返回结果。
id:与请求对象中的id一致,用于匹配响应。
2. 编写调试代码
合约代码
基于ERC20的智能合约
// SPDX-License-Identifier: MIT
// Compatible with OpenZeppelin Contracts ^5.4.0
pragma solidity ^0.8.27;
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract MyToken is ERC20 {
constructor() ERC20("MyToken", "MTK") payable {
_mint(msg.sender, 100 * 10 ** decimals());
}
fallback() external payable { }
receive() external payable { }
}
读操作用eth_call,写操作用eth_sendTransaction。
读操作,将method赋值给eth_call。合约地址放入to,方法名和参数放入data,赋值给params,组装jsonrpc。
读操作不消耗gas,从本地节点直接返回,不组装交易结构,不进行挖矿。所以不需要交易结构中的from、value、gaslimit、gasprice。
写操作消耗gas,广播到区块链上的节点,组装交易结构,进行挖矿。所以需要交易结构中的from、gaslimit、gasprice,默认不需要value。
查询代币名称的JSON-RPC结构:
{
jsonrpc: '2.0',
method: 'eth_call',
params: [{
to: contractAddress,
data: keccak256(Buffer.from('name()')).slice(0, 10)
}, 'latest'],
id: 1
}
查询余额的JSON-RPC结构:
{
jsonrpc: '2.0',
method: 'eth_call',
params: [{
to: contractAddress,
data: keccak256(Buffer.from('balanceOf(address)')).slice(0, 10) + accountAddr.toLowerCase().replace('0x', '').padStart(64, '0')
}, 'latest'],
id: 1
}
转账的JSON-RPC结构:
{
jsonrpc: '2.0',
method: 'eth_sendTransaction',
params: [{
from: sendAccountAddr,
to: contractAddr,
data: keccak256(Buffer.from('transfer(address,uint256)')).slice(0, 10) + recAccountAddr.toLowerCase().replace('0x', '').padStart(64, '0') + BigInt(value).toString(16).padStart(64, '0'),
gas: '0x300000'
}],
id: 1
}
// sendAccountAddr调用contractAddr合约中的transfer方法, 给recAccountAddr账户转value个MTK代币
await jsonrpc_write_contract('0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266', contractAddress, 'transfer(address,uint256)', '0x70997970C51812dc3A010C7d01b50e0d17dc79C8', 1000);
// JSON-RPC调用合约方法-写操作
async function jsonrpc_write_contract(sendAccountAddr, contractAddr, funcName, recAccountAddr, value) {
const name = await http('eth_sendTransaction', [{
from: sendAccountAddr,
to: contractAddr,
data: keccak256(Buffer.from(funcName)).slice(0, 10) + recAccountAddr.toLowerCase().replace('0x', '').padStart(64, '0') + BigInt(value).toString(16).padStart(64, '0'),
gas: '0x300000'
}]);
}
sendAccountAddr账户调用contractAddr合约中的transfer方法, 给recAccountAddr账户转value个MTK代币
funcName(recAccountAddr, value)是合约方法和参数,调用transfer(address,uint256),给’0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266’转账1000MTK。
from: sendAccountAddr,
to: contractAddr,是交易参数,消耗ETH。
const { ethers } = require("hardhat");
const { default: Web3 } = require('web3');
const { keccak256 } = require("ethers");
// 部署合约
async function deploy() {
// 获取合约工厂(这里期望存在名为 Token 的合约,位于 contracts/ 下)
// 注意:合约名需与 solidity 文件中合约名一致。
const myContract = await ethers.getContractFactory("MyToken");
const token = await myContract.deploy();
// 等待链上确认
await token.waitForDeployment();
const address = await token.getAddress();
console.log("实际合约地址:", address);
return address;
}
// JSON-RPC调用合约方法-读操作
async function jsonrpc_read_contract(contractAddress, funcName, accountAddr='') {
const name = await http('eth_call', [{
to: contractAddress,
// 拼接函数选择器和参数,函数取前四字节作为选择器,参数按ABI编码规则补齐64字节
data: keccak256(Buffer.from(funcName)).slice(0, 10) + (accountAddr!=''? accountAddr.toLowerCase().replace('0x', '').padStart(64, '0') : '')
}, 'latest']);
}
// JSON-RPC调用合约方法-写操作
async function jsonrpc_write_contract(sendAccountAddr, contractAddr, funcName, recAccountAddr, value) {
const name = await http('eth_sendTransaction', [{
from: sendAccountAddr,
to: contractAddr,
data: keccak256(Buffer.from(funcName)).slice(0, 10) + recAccountAddr.toLowerCase().replace('0x', '').padStart(64, '0') + BigInt(value).toString(16).padStart(64, '0'),
gas: '0x300000'
}]);
}
async function http(method, params) {
const response = await fetch('http://localhost:8545', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
jsonrpc: '2.0',
method,
params,
id: 1
})
});
const data = await response.json();
// console.log('JSON-RPC Raw Response:', data);
if(method == 'eth_call')
{
// 字符串类型返回值
if(data.result.length > 66)
console.log(method + ':', new Web3().eth.abi.decodeParameter('string', data.result));
// 数值类型返回值
else if(BigInt(data.result))
console.log(method + ':', BigInt(data.result) / BigInt(10**18));
}
else if(method == 'eth_sendTransaction')
console.log(method + ':', data.result);
}
async function jsonRpc(contractAddress) {
// 无参方法
await jsonrpc_read_contract(contractAddress, 'name()');
await jsonrpc_read_contract(contractAddress, 'symbol()');
// 有参方法
await jsonrpc_read_contract(contractAddress, 'balanceOf(address)', '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266');
await jsonrpc_read_contract(contractAddress, 'balanceOf(address)', '0x70997970C51812dc3A010C7d01b50e0d17dc79C8');
// sendAccountAddr调用contractAddr合约中的transfer方法, 给recAccountAddr账户转value个MTK代币
await jsonrpc_write_contract('0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266', contractAddress, 'transfer(address,uint256)', '0x70997970C51812dc3A010C7d01b50e0d17dc79C8', 10 * 10**18);
// 有参方法
await jsonrpc_read_contract(contractAddress, 'balanceOf(address)', '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266');
await jsonrpc_read_contract(contractAddress, 'balanceOf(address)', '0x70997970C51812dc3A010C7d01b50e0d17dc79C8');
}
async function main() {
var address = await deploy();
console.log("jsonRpc 调用合约方法:");
await jsonRpc(address);
}
main().then();
3. 安装依赖
npm install web3
4. 启动Hardhat本地节点
npx hardhat node

5. 调试
npx hardhat run ignition\modules\Mytoken.js --network localhost

hardhat node输出

5.思考
读操作没有输入from,hardhat node输出了from。是因为hardhat node输出日志时,用第一个账户地址作为from。后续文章会进一步探索。

1538

被折叠的 条评论
为什么被折叠?



