trufflesuite / drizzle

Reactive Ethereum dapp UI suite
905 stars 235 forks source link

Listening multiple contracts events lets Drizzle Events produce multiple firing events , one for each Contract presents in the drizzleOptions files. #7

Open d0na opened 5 years ago

d0na commented 5 years ago

Using the code described in the tutorial DRIZZLE AND CONTRACT EVENTS I verified a strange behavior if I set up more than one contract (with related events to watch) inside the drizzleOptions.js.

It's seems in fact that the EVENT_FIRED by Drizzle are replicated one per each contracts set up in the drizzle_options.js file.

The steps below are the test case used to reproduce and show the behavior:

    ...
    "drizzle": "^1.4.0",
    "drizzle-react": "^1.3.0",
     ...
....
contract ComplexStorage {
    event StorageSet2(string _message);
     ...

..
constructor() public {
       StorageSet2("Not the same data stored successfully!");
       ...

The behavior: after submitted a new value will appears two toasts as shown in the figure below while I was expecting just one Toast corresponding to a single event triggered.

Actually drizzle fired the same event twice.

image

Is this behavior correct? If yes why?

cds-amal commented 5 years ago

@d0na That is not expected behavior. Thanks for reporting this! I'll update this thread with my findings.

cds-amal commented 5 years ago

@dona ~I think this a web3 1.0 issue and wanted to upgrade from web3 1.0.0-beta.35 to the latest version to test, but there are breaking changes in the beta releases. I'll code up a sample outside of drizzle to test this theory against the latest beta and take it from there.~

Edit: Seems to be an issue with MetaMask's provider

cds-amal commented 5 years ago

I created a vanilla JS test that loads two deployed contracts via web3 and registered as a listener for their events. I then updated a contract state via the drizzle-event-demo and saw the same issue (without the drizzle framework) for web3@1.0.0-beta.35, the version drizzle is pinned at, as well as beta.55, the latest.

This could be an issue with MetaMask provider and/or web3. I'll continue investigating.

Anyone interested can view the sample/gist here

Screen Shot 2019-05-28 at 11 23 51 PM

cds-amal commented 5 years ago

It may be related to MetaMask. I wrote a test app to see the behavior of interacting with web3 with MetaMask (regular chrome browser) and sans Metamask (incognito chrome browser). With metaMask, you can see the duplicate events fired.

The test loads two contracts, SimpleStorage and DimpleStorage (don't judge!) where a contract event is emitted whenever the storage value is set; events SimpleSet and DimpleNumber2 respectively.

The test repo is here with instructions in its readme.

Demo

This Video demonstrates the issue using repo and steps above

Duplicate contract events with MetaMask provider

EDITED: linked to reproduction repository

cds-amal commented 5 years ago

Raised an issue with MetaMask

d0na commented 5 years ago

@cds-amal many thanks for your work.

stale[bot] commented 5 years ago

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

swkim109 commented 4 years ago

Same problem in Drizzle suite version 1.5.0, here is my SimpleStorage sample:

pragma solidity ^0.5.8;

contract SimpleStorage {

    uint storedData;

    event Change(string message, uint indexed newVal);
    event Stored(uint newVal);
    event Message(string message);

    constructor (uint s) public {
        storedData = s;
    }

    function set(uint x) public {
        require(x < 5000, "Should be less than 5000");
        storedData = x;
        emit Change("set", x);
    }

    function get() public view returns (uint) {
        return storedData;
    }
}

drizzleOptions has 3 events.

const options = {
    web3: {
        block: false,
        fallback: {
            type: "ws",
            url: "ws://127.0.0.1:8545",
        },
    },
    contracts: [SimpleStorage],
    events: {
        SimpleStorage: ["Change", "Stored", "Message"],
    },
    polls: {
        accounts: 3000,
    },
};

Main.js for Provider

import React, { Component } from 'react'
import { Drizzle, generateStore, EventActions } from "@drizzle/store";
import { DrizzleContext } from "@drizzle/react-plugin";
import options from "./drizzleOptions";
import MyStorage from "./MyStorage";
import EventNotifier from "./EventNotifier";

const drizzleStore = generateStore({drizzleOptions: options, appMiddlewares: [EventNotifier]});
const drizzle = new Drizzle(options, drizzleStore);
//const drizzle = new Drizzle(options);

class Main extends Component {
    render () {
        return (
            <DrizzleContext.Provider drizzle={drizzle}>
                <MyStorage />
            </DrizzleContext.Provider>
        )
    }
}
export default Main;

MyStorage.js for Consumer


import React, { useState, useEffect, useContext } from 'react';
import { DrizzleContext } from "@drizzle/react-plugin";

const MyStorage = () => {

    const drizzleContext = useContext(DrizzleContext.Context);
    const { drizzle, drizzleState, initialized } = drizzleContext;
    return (
        <MyData drizzle={drizzle} drizzleState={drizzleState} />
    )
}

export default MyStorage;

function MyData(props) {

    const [web3, setWeb3] = useState(null);
    const [contracts, setContracts] = useState(null);

    const [val, setVal] = useState(0);
    const [storedData, setStoredData] = useState(0);

    useEffect(() => {
        setContracts(props.drizzle.contracts);
        setWeb3(props.drizzle.web3);

    })

    const handleSet = () => {
        if (val && !isNaN(val)) {
            contracts.SimpleStorage.methods.set.cacheSend(val);
        }
    }

    const handleChange = (e) => {
        if (e.target.value !== "") {
            setVal(e.target.value);
        }
    }

    return (
        <div style={{marginTop: "10px", marginLeft: "10px"}}>
            <input type={"text"} onChange={handleChange}/>
            <button onClick={handleSet}>Set</button>
            <p>{storedData}</p>
        </div>
    )

}

EventNotifier.js

import {EventActions} from "@drizzle/store";

const EventNotifier = store => next => action => {

    if (action.type === EventActions.EVENT_FIRED) {

        //console.log(action);
        const contract = action.name;
        const eventName = action.event.event;
        const newVal = action.event.returnValues.newVal;
        const display = `${contract}(${eventName}): ${newVal}`
        console.log(display);
    }
    return next(action);
}

export default EventNotifier;

Emitted only 1 event but EventNotifier says 3 Change events.

console

cds-amal commented 4 years ago

Hi @swkim109, Thank you for your detailed writeup, however this is a MetaMask issue, perhaps you can comment there and give it a +1

cds-amal commented 4 years ago

One workaround is to use a middleware to filter out consecutive duplicate error events until this is resolved upstream.

import { generateStore, EventActions } from '@drizzle/store'
import drizzleOptions from '../drizzleOptions'

// lastSeenEventId used as a work around for known MM events issue
const contractEventNotifier = lastSeenEventId => _ => next => action => {
  if (action.type === EventActions.EVENT_FIRED) {
    if (action.event.id !== lastSeenEventId) {
      lastSeenEventId = action.event.id
      const caller = action.event.returnValues.caller
      const value = action.event.returnValues.value
      const blockNumber = action.event.blockNumber
      const message = `value=[${value}] set by [${caller}] in block [${blockNumber}]`
      console.group('Event fired')
      console.log(action)
      console.log(message)
      console.groupEnd()
    }
  }
  return next(action)
}

// see issue:
// https://github.com/MetaMask/metamask-extension/issues/6668
const appMiddlewares = [ contractEventNotifier("lastSeenEventIdToGetAroundMetaMaskIssue6668") ]

export default generateStore({
  drizzleOptions,
  appMiddlewares,
  disableReduxDevTools: false  // enable ReduxDevTools!
})