5. JSON-RPC调用合约方法

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

JSON-RPC2.0协议详解

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. 编写调试代码

以太坊JSON-RPC API

合约代码
基于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。后续文章会进一步探索。

一. 系列视频
二. 系列文章

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

OneFlyPig

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值