简体   繁体   中英

Web Application doesn't communicate correctly with Ethereum smart contract deployed through Truffle

I coded this really simple smart contract in Solidity which allows users to add todo tasks to their personal list, to fetch their list of todos, and so on.

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];
    }
}

This works exactly as intended when deployed through Truffle and tested in the Truffle(develop) terminal:

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 ]
]

However, when I try to call these contract's functions from a web-app, it seems like there are some kind of communication errors with the local blockchain provided by Truffle.

Of course I've installed Metamask in my browser and I've connected it to http://127.0.0.1:9545 (as Truffle tells me to do upon executing the truffle develop command). I've also imported the private phrase provided by Truffle, so that I could access the 10 test addresses on that local network.

I've also found the contract's address and ABI in the build/contracts directory and set up a simple front end in 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>
  );
}

The call to getTasks() always returns an empty array, even if I add a task through the terminal with the same address that's currently in use on Metamask, while the call to addTask() doesn't store anything in the smart contracts's map. The call to these two functions don't cause any errors or warnings to appear in the browser's console. However, the call to changeTaskState() does cause two errors do be displayed:

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)

I've also tried to use Ganache, instead of Truffle's built-in local blockchain, and I even tried to change browser, but nothing seems to work. I've also checked whether Metamask was actually connected to the webapp and sure enough it was. What am I missing here?

There are two main ways to interact with a smart contract. A call (read-only, free) and a transaction (read-write, requires gas fees).

Your react code uses the .call() method for addTask() and changeTaskState() , which doesn't allow writing in the contract storage.


Since you're working with MetaMask, you should use the Ethereum Provider API and submit a request to MM that will ask the user to confirm the transaction.

So instead of the props.contract.methods.addTask(content).call() you can get the contents of the data field and then generate the transaction request.

const data = props.contract.methods.addTask(content).encodeABI();

ethereum.request({
    method: 'eth_sendTransaction',
    params: [{
        from: senderAddress,
        to: contractAddress,
        data: data
    }],
});

Note: You can set the senderAddress after connecting to MM .

Another way is to use the web3 .send() method, but this requires passing the private key of the sender to web3 (bad idea in a frontend JS app) or unlocked provider account (local providers usually have few unlocked accounts, but production ones don't).

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM