Web 应用程序无法与通过 Truffle 部署的以太坊智能合约正确通信
Web Application doesn't communicate correctly with Ethereum smart contract deployed through Truffle
我用 Solidity 编写了这个非常简单的智能合约,它允许用户将待办事项添加到他们的个人列表中,获取他们的待办事项列表,等等。
pragma solidity ^0.8.0;
contract ToDo {
struct Task {
string content;
bool completed;
}
mapping(address => Task[]) private tasks;
function addTask(string memory content) public {
tasks[msg.sender].push(Task(content, false));
}
function changeTaskState(uint256 taskId) public {
tasks[msg.sender][taskId].completed = !tasks[msg.sender][taskId].completed;
}
function editTaskContent(uint256 taskId, string memory content) public {
tasks[msg.sender][taskId].content = content;
}
function getTasks() public view returns(Task[] memory) {
return tasks[msg.sender];
}
}
当通过 Truffle 部署并在 Truffle(develop)
终端中测试时,这完全符合预期:
truffle(develop)> const todo = await ToDo.deployed()
undefined
truffle(develop)> todo.getTasks()
[]
truffle(develop)> todo.addTask("Hello, world!")
{
tx: '0x7e607352c1ab8f6532c5b43e282eb20f29d5bfa451dfbb873bac3506df00cb1a',
receipt: {
transactionHash: '0x7e607352c1ab8f6532c5b43e282eb20f29d5bfa451dfbb873bac3506df00cb1a',
transactionIndex: 0,
blockHash: '0x98b361190eadf1905c3e15b5054aa4ace8eaa33a2b4d35898f78e2165ea996a2',
blockNumber: 5,
from: '0x3455100c0b0617afbf0f53db5e5c07366e20791b',
to: '0x645a78fe8eb3529291ba63a8e420d26c7baf61a0',
gasUsed: 66634,
cumulativeGasUsed: 66634,
contractAddress: null,
logs: [],
status: true,
logsBloom: '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
rawLogs: []
},
logs: []
}
truffle(develop)> todo.changeTaskState(0)
{
tx: '0xddb313978411cd3f1429f1eb61b9bbde816e3a874d765aa5588a69508d226b44',
receipt: {
transactionHash: '0xddb313978411cd3f1429f1eb61b9bbde816e3a874d765aa5588a69508d226b44',
transactionIndex: 0,
blockHash: '0xbae43abf22ca06de65a41e3e54493ad944f4b997b12a3ed407bc5f778d00a3be',
blockNumber: 6,
from: '0x3455100c0b0617afbf0f53db5e5c07366e20791b',
to: '0x645a78fe8eb3529291ba63a8e420d26c7baf61a0',
gasUsed: 45320,
cumulativeGasUsed: 45320,
contractAddress: null,
logs: [],
status: true,
logsBloom: '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
rawLogs: []
},
logs: []
}
truffle(develop)> todo.getTasks()
[
[ 'Hello, world!', true, content: 'Hello, world!', completed: true ]
]
但是,当我尝试从网络应用程序调用这些合约的功能时,似乎与 Truffle 提供的本地区块链存在某种通信错误。
当然,我已经在浏览器中安装了 Metamask,并将其连接到 http://127.0.0.1:9545(正如 Truffle 在执行 truffle develop
命令时告诉我的那样)。我还导入了 Truffle 提供的私有短语,这样我就可以访问该本地网络上的 10 个测试地址。
我也在build/contracts
目录下找到了合约的地址和ABI,并在React中搭建了一个简单的前端
import Web3 from 'web3';
import React, { useState, useEffect } from "react";
const TODO_ABI =
[
{
"inputs": [
{
"internalType": "string",
"name": "content",
"type": "string"
}
],
"name": "addTask",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "taskId",
"type": "uint256"
}
],
"name": "changeTaskState",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "taskId",
"type": "uint256"
},
{
"internalType": "string",
"name": "content",
"type": "string"
}
],
"name": "editTaskContent",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "getTasks",
"outputs": [
{
"components": [
{
"internalType": "string",
"name": "content",
"type": "string"
},
{
"internalType": "bool",
"name": "completed",
"type": "bool"
}
],
"internalType": "struct ToDo.Task[]",
"name": "",
"type": "tuple[]"
}
],
"stateMutability": "view",
"type": "function",
"constant": true
}
];
const TODO_ADDRESS = "0x645a78fe8eb3529291ba63a8e420d26c7baf61a0";
function ChangeTaskStateButton(props) {
return (
<button onClick={ () => props.contract.methods.changeTaskState(props.id).call() }>{ props.state }</button>
);
}
function Task(props) {
return (
<li>
{ props.content } | <ChangeTaskStateButton contract={ props.contract } id={ props.id } state={ props.completed ? "Completed" : "Pending "}></ChangeTaskStateButton>
</li>
);
}
function TasksList(props) {
let tasks = [];
const tasksData = props.tasks;
for(let i = 0; i < tasksData.length; i++) {
tasks.push(<Task id={i} content={ tasksData[i].content } completed={ tasksData[i].completed } contract={ props.contract }></Task>);
}
return (
<div>
<ul>
{ tasks }
</ul>
</div>
);
}
function TaskForm(props) {
const [content, setContent] = useState("");
const handleSubmit = (event) => {
event.preventDefault();
props.contract.methods.addTask(content).call()
.then(() => props.setTasks(props.tasks.concat({content: content, completed: false})));
};
const handleChange = (event) => {
setContent(event.target.value);
};
return(
<form onSubmit={ handleSubmit }>
<input type="text" onChange={ handleChange }></input>
<button type="submit">Submit</button>
</form>
);
}
function App() {
const [web3] = useState(new Web3(Web3.givenProvider || "http://localhost:9545"));
const [contract] = useState(new web3.eth.Contract(TODO_ABI, TODO_ADDRESS));
const [tasks, setTasks] = useState([]);
useEffect(() => {
contract.methods.getTasks().call()
.then(tasks => {
setTasks(tasks);
});
}, [contract.methods]);
return (
<div>
<TaskForm contract={contract} setTasks={setTasks} tasks={tasks}></TaskForm>
<TasksList tasks={tasks} contract={contract}></TasksList>
</div>
);
}
对getTasks()
的调用总是returns一个空数组,即使我通过终端添加了一个任务,其地址与当前在Metamask上使用的地址相同,而对[=18的调用=] 不在智能合约的地图中存储任何内容。调用这两个函数不会导致浏览器控制台中出现任何错误或警告。但是,调用 changeTaskState()
确实会导致显示两个错误:
inpage.js:1 MetaMask - RPC Error: Internal JSON-RPC error.
{code: -32603, message: "Internal JSON-RPC error.", data: {…}}
code: -32603
data: {message: "VM Exception while processing transaction: revert", code: -32000, data: {…}}
message: "Internal JSON-RPC error."
__proto__: Object
index.js:50 Uncaught (in promise) Error: Internal JSON-RPC error.
{
"message": "VM Exception while processing transaction: revert",
"code": -32000,
"data": {
"0x359c33ac64b2b3eb0096b40b2d225679d4212f40fc86ef938af49fcc47159f2c": {
"error": "revert",
"program_counter": 994,
"return": "0x4e487b710000000000000000000000000000000000000000000000000000000000000032"
},
"stack": "RuntimeError: VM Exception while processing transaction: revert\n at Function.RuntimeError.fromResults (C:\Users\gianm\AppData\Roaming\npm\node_modules\truffle\build\webpack:\node_modules\ganache-core\lib\utils\runtimeerror.js:94:1)\n at C:\Users\gianm\AppData\Roaming\npm\node_modules\truffle\build\webpack:\node_modules\ganache-core\lib\blockchain_double.js:568:1",
"name": "RuntimeError"
}
}
at Object._fireError (index.js:50)
at sendTxCallback (index.js:540)
at cb (util.js:689)
at callbackifyOnRejected (util.js:666)
at Item.push../node_modules/process/browser.js.Item.run (browser.js:153)
at drainQueue (browser.js:123)
我也尝试过使用 Ganache,而不是 Truffle 内置的本地区块链,我什至尝试过更换浏览器,但似乎没有任何效果。我还检查了 Metamask 是否真的连接到 webapp 并且确实如此。我在这里错过了什么?
与智能合约交互的主要方式有两种。 A call (read-only, free) and a transaction(读写,需要 gas 费)。
您的 React 代码对 addTask()
和 changeTaskState()
使用 .call()
方法,这不允许写入合约存储。
由于您使用的是 MetaMask,因此您应该使用 Ethereum Provider API 并向 MM 提交 request,要求用户确认交易。
因此,您可以获取 data
字段的内容,然后生成交易请求,而不是 props.contract.methods.addTask(content).call()
。
const data = props.contract.methods.addTask(content).encodeABI();
ethereum.request({
method: 'eth_sendTransaction',
params: [{
from: senderAddress,
to: contractAddress,
data: data
}],
});
注意:可以在connecting to MM之后设置senderAddress
。
另一种方法是使用 web3 .send()
方法,但这需要将发件人的私钥传递给 web3
(前端 JS 应用程序中的坏主意)或解锁提供者帐户(本地提供者通常很少有未锁定的帐户,但生产帐户则没有)。
我用 Solidity 编写了这个非常简单的智能合约,它允许用户将待办事项添加到他们的个人列表中,获取他们的待办事项列表,等等。
pragma solidity ^0.8.0;
contract ToDo {
struct Task {
string content;
bool completed;
}
mapping(address => Task[]) private tasks;
function addTask(string memory content) public {
tasks[msg.sender].push(Task(content, false));
}
function changeTaskState(uint256 taskId) public {
tasks[msg.sender][taskId].completed = !tasks[msg.sender][taskId].completed;
}
function editTaskContent(uint256 taskId, string memory content) public {
tasks[msg.sender][taskId].content = content;
}
function getTasks() public view returns(Task[] memory) {
return tasks[msg.sender];
}
}
当通过 Truffle 部署并在 Truffle(develop)
终端中测试时,这完全符合预期:
truffle(develop)> const todo = await ToDo.deployed()
undefined
truffle(develop)> todo.getTasks()
[]
truffle(develop)> todo.addTask("Hello, world!")
{
tx: '0x7e607352c1ab8f6532c5b43e282eb20f29d5bfa451dfbb873bac3506df00cb1a',
receipt: {
transactionHash: '0x7e607352c1ab8f6532c5b43e282eb20f29d5bfa451dfbb873bac3506df00cb1a',
transactionIndex: 0,
blockHash: '0x98b361190eadf1905c3e15b5054aa4ace8eaa33a2b4d35898f78e2165ea996a2',
blockNumber: 5,
from: '0x3455100c0b0617afbf0f53db5e5c07366e20791b',
to: '0x645a78fe8eb3529291ba63a8e420d26c7baf61a0',
gasUsed: 66634,
cumulativeGasUsed: 66634,
contractAddress: null,
logs: [],
status: true,
logsBloom: '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
rawLogs: []
},
logs: []
}
truffle(develop)> todo.changeTaskState(0)
{
tx: '0xddb313978411cd3f1429f1eb61b9bbde816e3a874d765aa5588a69508d226b44',
receipt: {
transactionHash: '0xddb313978411cd3f1429f1eb61b9bbde816e3a874d765aa5588a69508d226b44',
transactionIndex: 0,
blockHash: '0xbae43abf22ca06de65a41e3e54493ad944f4b997b12a3ed407bc5f778d00a3be',
blockNumber: 6,
from: '0x3455100c0b0617afbf0f53db5e5c07366e20791b',
to: '0x645a78fe8eb3529291ba63a8e420d26c7baf61a0',
gasUsed: 45320,
cumulativeGasUsed: 45320,
contractAddress: null,
logs: [],
status: true,
logsBloom: '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
rawLogs: []
},
logs: []
}
truffle(develop)> todo.getTasks()
[
[ 'Hello, world!', true, content: 'Hello, world!', completed: true ]
]
但是,当我尝试从网络应用程序调用这些合约的功能时,似乎与 Truffle 提供的本地区块链存在某种通信错误。
当然,我已经在浏览器中安装了 Metamask,并将其连接到 http://127.0.0.1:9545(正如 Truffle 在执行 truffle develop
命令时告诉我的那样)。我还导入了 Truffle 提供的私有短语,这样我就可以访问该本地网络上的 10 个测试地址。
我也在build/contracts
目录下找到了合约的地址和ABI,并在React中搭建了一个简单的前端
import Web3 from 'web3';
import React, { useState, useEffect } from "react";
const TODO_ABI =
[
{
"inputs": [
{
"internalType": "string",
"name": "content",
"type": "string"
}
],
"name": "addTask",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "taskId",
"type": "uint256"
}
],
"name": "changeTaskState",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "taskId",
"type": "uint256"
},
{
"internalType": "string",
"name": "content",
"type": "string"
}
],
"name": "editTaskContent",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "getTasks",
"outputs": [
{
"components": [
{
"internalType": "string",
"name": "content",
"type": "string"
},
{
"internalType": "bool",
"name": "completed",
"type": "bool"
}
],
"internalType": "struct ToDo.Task[]",
"name": "",
"type": "tuple[]"
}
],
"stateMutability": "view",
"type": "function",
"constant": true
}
];
const TODO_ADDRESS = "0x645a78fe8eb3529291ba63a8e420d26c7baf61a0";
function ChangeTaskStateButton(props) {
return (
<button onClick={ () => props.contract.methods.changeTaskState(props.id).call() }>{ props.state }</button>
);
}
function Task(props) {
return (
<li>
{ props.content } | <ChangeTaskStateButton contract={ props.contract } id={ props.id } state={ props.completed ? "Completed" : "Pending "}></ChangeTaskStateButton>
</li>
);
}
function TasksList(props) {
let tasks = [];
const tasksData = props.tasks;
for(let i = 0; i < tasksData.length; i++) {
tasks.push(<Task id={i} content={ tasksData[i].content } completed={ tasksData[i].completed } contract={ props.contract }></Task>);
}
return (
<div>
<ul>
{ tasks }
</ul>
</div>
);
}
function TaskForm(props) {
const [content, setContent] = useState("");
const handleSubmit = (event) => {
event.preventDefault();
props.contract.methods.addTask(content).call()
.then(() => props.setTasks(props.tasks.concat({content: content, completed: false})));
};
const handleChange = (event) => {
setContent(event.target.value);
};
return(
<form onSubmit={ handleSubmit }>
<input type="text" onChange={ handleChange }></input>
<button type="submit">Submit</button>
</form>
);
}
function App() {
const [web3] = useState(new Web3(Web3.givenProvider || "http://localhost:9545"));
const [contract] = useState(new web3.eth.Contract(TODO_ABI, TODO_ADDRESS));
const [tasks, setTasks] = useState([]);
useEffect(() => {
contract.methods.getTasks().call()
.then(tasks => {
setTasks(tasks);
});
}, [contract.methods]);
return (
<div>
<TaskForm contract={contract} setTasks={setTasks} tasks={tasks}></TaskForm>
<TasksList tasks={tasks} contract={contract}></TasksList>
</div>
);
}
对getTasks()
的调用总是returns一个空数组,即使我通过终端添加了一个任务,其地址与当前在Metamask上使用的地址相同,而对[=18的调用=] 不在智能合约的地图中存储任何内容。调用这两个函数不会导致浏览器控制台中出现任何错误或警告。但是,调用 changeTaskState()
确实会导致显示两个错误:
inpage.js:1 MetaMask - RPC Error: Internal JSON-RPC error.
{code: -32603, message: "Internal JSON-RPC error.", data: {…}}
code: -32603
data: {message: "VM Exception while processing transaction: revert", code: -32000, data: {…}}
message: "Internal JSON-RPC error."
__proto__: Object
index.js:50 Uncaught (in promise) Error: Internal JSON-RPC error.
{
"message": "VM Exception while processing transaction: revert",
"code": -32000,
"data": {
"0x359c33ac64b2b3eb0096b40b2d225679d4212f40fc86ef938af49fcc47159f2c": {
"error": "revert",
"program_counter": 994,
"return": "0x4e487b710000000000000000000000000000000000000000000000000000000000000032"
},
"stack": "RuntimeError: VM Exception while processing transaction: revert\n at Function.RuntimeError.fromResults (C:\Users\gianm\AppData\Roaming\npm\node_modules\truffle\build\webpack:\node_modules\ganache-core\lib\utils\runtimeerror.js:94:1)\n at C:\Users\gianm\AppData\Roaming\npm\node_modules\truffle\build\webpack:\node_modules\ganache-core\lib\blockchain_double.js:568:1",
"name": "RuntimeError"
}
}
at Object._fireError (index.js:50)
at sendTxCallback (index.js:540)
at cb (util.js:689)
at callbackifyOnRejected (util.js:666)
at Item.push../node_modules/process/browser.js.Item.run (browser.js:153)
at drainQueue (browser.js:123)
我也尝试过使用 Ganache,而不是 Truffle 内置的本地区块链,我什至尝试过更换浏览器,但似乎没有任何效果。我还检查了 Metamask 是否真的连接到 webapp 并且确实如此。我在这里错过了什么?
与智能合约交互的主要方式有两种。 A call (read-only, free) and a transaction(读写,需要 gas 费)。
您的 React 代码对 addTask()
和 changeTaskState()
使用 .call()
方法,这不允许写入合约存储。
由于您使用的是 MetaMask,因此您应该使用 Ethereum Provider API 并向 MM 提交 request,要求用户确认交易。
因此,您可以获取 data
字段的内容,然后生成交易请求,而不是 props.contract.methods.addTask(content).call()
。
const data = props.contract.methods.addTask(content).encodeABI();
ethereum.request({
method: 'eth_sendTransaction',
params: [{
from: senderAddress,
to: contractAddress,
data: data
}],
});
注意:可以在connecting to MM之后设置senderAddress
。
另一种方法是使用 web3 .send()
方法,但这需要将发件人的私钥传递给 web3
(前端 JS 应用程序中的坏主意)或解锁提供者帐户(本地提供者通常很少有未锁定的帐户,但生产帐户则没有)。