scaffold-eth / scaffold-eth-2

Open source forkable Ethereum dev stack
https://scaffoldeth.io
MIT License
1.19k stars 745 forks source link

rewrite useScaffoldEventHistory hook #869

Closed technophile-04 closed 4 weeks ago

technophile-04 commented 1 month ago

Description:

NOTE: Don't use #633 its bit outdated checkout To Test section for reproducible :

Rewrote useScaffoldEventHistory with useInfiniteQuery from tanstack-query.

Our present logic felt like a bit try-hard solution to get it working also introducing lots of imperative logic and also have a bug example #633.

I have been diving deep in to tanstack-query recently and also had #633 in back of mind for a while, and useInfiniteQuery seems like a perfect solution for this with some additional advantage:

  1. Solves #633

  2. Cleans up the code nicely

  3. feature like refetch, it will allow people to imperatively fetch events again after a write is performed(without needing for watch) :

    Example : ```tsx const { data: eventHistory, refetch: refetchAllEvents } = useScaffoldEventHistory({ contractName: "YourContract", eventName: "GreetingChange", filters: { greetingSetter: connectedAddress }, fromBlock: 0n, }); // ... ```

    Testing steps :

  4. Switch to main branch.

2. Copy this in externalContract.ts : ```ts import { GenericContractsDeclaration } from "~~/utils/scaffold-eth/contract"; const externalContracts = { 11155111: { YourContract: { address: "0xE009aea21af005e6B531B5f4a8f909C64A0c596d", abi: [ { inputs: [ { internalType: "address", name: "_owner", type: "address", }, ], stateMutability: "nonpayable", type: "constructor", }, { anonymous: false, inputs: [ { indexed: true, internalType: "address", name: "greetingSetter", type: "address", }, { indexed: false, internalType: "string", name: "newGreeting", type: "string", }, { indexed: false, internalType: "bool", name: "premium", type: "bool", }, { indexed: false, internalType: "uint256", name: "value", type: "uint256", }, ], name: "GreetingChange", type: "event", }, { inputs: [], name: "greeting", outputs: [ { internalType: "string", name: "", type: "string", }, ], stateMutability: "view", type: "function", }, { inputs: [], name: "owner", outputs: [ { internalType: "address", name: "", type: "address", }, ], stateMutability: "view", type: "function", }, { inputs: [], name: "premium", outputs: [ { internalType: "bool", name: "", type: "bool", }, ], stateMutability: "view", type: "function", }, { inputs: [ { internalType: "string", name: "_newGreeting", type: "string", }, ], name: "setGreeting", outputs: [], stateMutability: "payable", type: "function", }, { inputs: [], name: "totalCounter", outputs: [ { internalType: "uint256", name: "", type: "uint256", }, ], stateMutability: "view", type: "function", }, { inputs: [ { internalType: "address", name: "", type: "address", }, ], name: "userGreetingCounter", outputs: [ { internalType: "uint256", name: "", type: "uint256", }, ], stateMutability: "view", type: "function", }, { inputs: [], name: "withdraw", outputs: [], stateMutability: "nonpayable", type: "function", }, { stateMutability: "payable", type: "receive", }, ], inheritedFunctions: {}, }, }, 84532: { YourContract: { address: "0x8806fc80A0274Eda6a45E2944f6bB6E6Bb635831", abi: [ { inputs: [ { internalType: "address", name: "_owner", type: "address", }, ], stateMutability: "nonpayable", type: "constructor", }, { anonymous: false, inputs: [ { indexed: true, internalType: "address", name: "greetingSetter", type: "address", }, { indexed: false, internalType: "string", name: "newGreeting", type: "string", }, { indexed: false, internalType: "bool", name: "premium", type: "bool", }, { indexed: false, internalType: "uint256", name: "value", type: "uint256", }, ], name: "GreetingChange", type: "event", }, { inputs: [], name: "greeting", outputs: [ { internalType: "string", name: "", type: "string", }, ], stateMutability: "view", type: "function", }, { inputs: [], name: "owner", outputs: [ { internalType: "address", name: "", type: "address", }, ], stateMutability: "view", type: "function", }, { inputs: [], name: "premium", outputs: [ { internalType: "bool", name: "", type: "bool", }, ], stateMutability: "view", type: "function", }, { inputs: [ { internalType: "string", name: "_newGreeting", type: "string", }, ], name: "setGreeting", outputs: [], stateMutability: "payable", type: "function", }, { inputs: [], name: "totalCounter", outputs: [ { internalType: "uint256", name: "", type: "uint256", }, ], stateMutability: "view", type: "function", }, { inputs: [ { internalType: "address", name: "", type: "address", }, ], name: "userGreetingCounter", outputs: [ { internalType: "uint256", name: "", type: "uint256", }, ], stateMutability: "view", type: "function", }, { inputs: [], name: "withdraw", outputs: [], stateMutability: "nonpayable", type: "function", }, { stateMutability: "payable", type: "receive", }, ], inheritedFunctions: {}, }, }, } as const; export default externalContracts satisfies GenericContractsDeclaration; ```
3. Copy this in `app/page.tsx` : ```tsx "use client"; import type { NextPage } from "next"; import { Address } from "~~/components/scaffold-eth"; import { useScaffoldEventHistory } from "~~/hooks/scaffold-eth"; const Home: NextPage = () => { const { data: eventHistory } = useScaffoldEventHistory({ contractName: "YourContract", eventName: "GreetingChange", fromBlock: 4738147n, }); return ( <>

Welcome to Scaffold-ETH 2

{/* head */} {eventHistory?.map((event, index) => ( ))}
Gretting Setter greeting
{index + 1}
{event.args.newGreeting}
); }; export default Home; ```
  1. Update scaffold.config.ts#targetNetworks with [chains.sepolia, chains.baseSepolia].

  2. yarn start.

  3. Connect wallet and switch network to baseSepolia you will see that instead of showing just 3 events it appended the events to previously fetched events from sepolia

    Demo: bug video : https://github.com/scaffold-eth/scaffold-eth-2/assets/80153681/ce9b3704-f366-4d53-b8da-3ea9dc5bd877
  4. Now switch to this branch(no need to stash above changes, you could directly switch to this PR branch) and try test point 6 again it should work as expected and solve #633.

    Demo: bug is solved : https://github.com/scaffold-eth/scaffold-eth-2/assets/80153681/90ee1d6e-cab7-410e-939a-15974acae309

Testing watch mode:

Just to make sure we don't break anything and watch mode also works perfectly, you revert all the above changes keep everything clean switch to this branch. Run three magical command yarn chain, yarn deploy and yarn start after that copy this in app/page.tsx

Example : ```tsx "use client"; import { useState } from "react"; import type { NextPage } from "next"; import { useAccount } from "wagmi"; import { Address, InputBase } from "~~/components/scaffold-eth"; import { useScaffoldEventHistory, useScaffoldWriteContract } from "~~/hooks/scaffold-eth"; const Home: NextPage = () => { const { address: connectedAddress } = useAccount(); const [newGreetings, setNewGreetings] = useState(""); const { data: eventHistory, status, isFetchingNewEvent, } = useScaffoldEventHistory({ contractName: "YourContract", eventName: "GreetingChange", filters: { greetingSetter: connectedAddress }, fromBlock: 0n, watch: true, }); const { writeContractAsync: writeYourContractAsync } = useScaffoldWriteContract("YourContract"); return ( <>

Welcome to Scaffold-ETH 2

{/* head */} {status === "pending" && ( )} {eventHistory?.map((event, index) => ( ))}
Gretting Setter greeting
{index + 1}
{event?.args.newGreeting}
); }; export default Home; ```

Set the greeting on home page and see it updating


Also types should also work as expected and consuming it still remains same, but we need to document some new return values like refetch, isFetchingNewEvents in SE-2 docs

rin-st commented 4 weeks ago

Great changes! And thank you for testing steps! 🙏

rin-st commented 4 weeks ago

https://github.com/scaffold-eth/scaffold-eth-2/pull/869/files#diff-0efe8962fd8674c8a04310503b57e5e0ff36eb7016ae11764e1cc83472bc7646R63

As I understand there's no polling interval anymore

upd. My bad, it works as expected

rin-st commented 4 weeks ago

This should possibly be added to recipes in docs

 <button
      className="btn btn-primary"
      onClick={async () => {
        try {
          await writeYourContractAsync({
            functionName: "setGreeting",
            args: [newGreetings],
          });
         // refetch events again so that UI is reactive and shows the latest event
          await refetchAllEvents();
        } catch (e) {
          console.error("Error setting greeting:", e);
        }
      }}
    >
  Set Greeting
</button>
technophile-04 commented 4 weeks ago

Thanks Rinat and Damu review, updated it with suggested changes