Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/multi chain #615

Merged
merged 34 commits into from
Nov 30, 2023
Merged

Feat/multi chain #615

merged 34 commits into from
Nov 30, 2023

Conversation

sverps
Copy link
Collaborator

@sverps sverps commented Nov 20, 2023

Fixes #599

Updates scaffold.config.ts to allow configuring multiple target networks like so:

const scaffoldConfig = {
  targetNetworks: [chains.hardhat, chains.sepolia],
  ...rest
}

The first value is the default and will be the network selected upon launching the app.

  • Switching can be done from the top menu
  • Faucet only shown for local network
  • When multiple networks configured, pollingInterval will for all networks be the one from the config (if anyone has an idea to keep polling interval to 4000 while on hardhat, while at the same time use a slower polling for other networks, let me know)
  • Works for external wallets as well as for burner wallet

multi-network-1
multi-network-2
multi-network-3

…foldConfigContext for configuredNetwork variable
… useScaffoldConfig hook instead of getTargetNetwork().id
…om useScaffoldConfig hook instead of getTargetNetwork()
… from useScaffoldConfig hook instead of getTargetNetwork()
…om useScaffoldConfig hook instead of getTargetNetwork()
…rom useScaffoldConfig hook instead of getTargetNetwork()
…ScaffoldConfig hook instead of getTargetNetwork(), Refactored RainbowKitCustomConnectButton.tsx to add check for when burner wallet is used
@carletex
Copy link
Member

This is looking amazing! Great job @sverps. Left a few review comments/open questions.

When multiple networks configured, pollingInterval will for all networks be the one from the config (if anyone has an idea to keep polling interval to 4000 while on hardhat, while at the same time use a slower polling for other networks, let me know)

I think this is ok. Live apps shouldn't have hardhat/foundry configured, so you'll only want that on your dev env (where you can tweak the file for dev purposes)

Also, thank you @blahkheart!! Sorry for the miscommunication with the PRs. As @sverps said, we'll give you attribution on merging, since this PR took some inspiration from yours.

@rin-st
Copy link
Member

rin-st commented Nov 27, 2023

Great job! Apart from todos and unresolved fixes current version is lgtm

@sverps
Copy link
Collaborator Author

sverps commented Nov 27, 2023

So we tell people to pass in chain ID, and then constrain contractName for autocompletion based on the chain id passed by the user (if the user doesn't pass the chain we give him autocompletion for 0th chain). similar stuff we can do for other custom hooks, what do you think?

I gave it a go, and it got complex quite quickly. 🙈

If we just make it a generic type, then for it to have a default value, it can't be the first generic. So that implies users would need to specify the TContractName and TFunctionName as well, which is a bit weird and hard to explain.

When they do so, it will only do autocompletion based on the given chainId, but it's really only typescript, so internally the hook will continue to use whichever chain the user is actually on.

So in order to make those two things work, the hook should really accept an optional chainId param, but that needs to be propagated through all types all the way to the top. I started doing that, and things started breaking all over. In the end I was blocked by an error thrown by abitype's ExtractAbiFunctionNames that I didn't manage to resolve.

Maybe there's a way, but it started feeling like it wasn't really worth it... 😅

I think, for now, it is an acceptable trade-off if users have autocompletion based on the default chain. Multi-chain projects that use different contract names and functions on each chain are probably advanced enough that devs working on that will understand how to use wagmi directly, I suppose.

(In trying out this idea, I did find a small issue with how the scaffold config file type was defined, not sure why it didn't pop up earlier.)

@carletex
Copy link
Member

Thanks for raising the autocompletion issue @technophile-04 and @sverps for looking into it.

If Samuel says it gets too complex in TS, I 100% trust him haha

I think, for now, it is an acceptable trade-off if users have autocompletion based on the default chain. Multi-chain projects that use different contract names and functions on each chain are probably advanced enough that devs working on that will understand how to use wagmi directly, I suppose.

Unless there was a simple solution (which it seems there is not)... I agree with this!

(In trying out this idea, I did find a small issue with how the scaffold config file type was defined, not sure why it didn't pop up earlier.)

What is the issue? 🤓

@sverps
Copy link
Collaborator Author

sverps commented Nov 28, 2023

(In trying out this idea, I did find a small issue with how the scaffold config file type was defined, not sure why it didn't pop up earlier.)

What is the issue? 🤓

targetNetworks was typed as chains.Chain[], which meant creating a type that holds the first element like so type ConfiguredChainId = (typeof scaffoldConfig)["targetNetworks"][0]["id"] was actually equaling an intersection of the options.

In other words, setting target networks as [chains.hardhat, chains.sepolia] in the config turned the ConfiguredChainId type into 31337 | 11155111, rather than the expected 31337. And I got all sorts of errors in the contract.ts type file.

Fix was to turn the targetNetworks into a tuple (which necessitated a custom Tuple utility type, since the length can vary).

Hope that makes sense 😅

@technophile-04
Copy link
Collaborator

technophile-04 commented Nov 28, 2023

In other words, setting target networks as [chains.hardhat, chains.sepolia] in the config turned the ConfiguredChainId type into 31337 | 11155111, rather than the expected 31337. And I got all sorts of errors in the contract.ts type file.

Fix was to turn the targetNetworks into a tuple (which necessitated a custom Tuple utility type, since the length can vary).

Ohh this was strange ideally it shouldn't have happened right? Since we were using satisfies (which I thought worked similarly to as const) and we should have gotten literal 31337 instead of unioning it with 11155111.

I tried doing some research and found another approach without using a custom Tuple utility and keeping chains.Chain[] we can just do this assertion in the end as const satisfies ReadonlyDeep<ScaffoldConfig> where ReadOnlyDeep comes from type-fest (which we are already using )

Also with custom Tuple if we specify < 10 chains we get Source has 14 element(s) but target allows only 10. lol kind of edge case and we can increase MaxLength but maybe I feel an assertion approach with as const and ReadOnlyDeep is more generic


#615 (comment)

Tysm @sverps for exploring and great explainantion!! Completely agree that it is not in the scope of this PR and may require a lot of changes and brainstorming on changes to the custom hooks API design.

The reason I mentioned that point was there is a slight difference b/w cross chain vs multi-chain and people tend to use cross chain smart contracts more and probability of having different Abi's / name in cross-chain stuff is more, where one chain smart-contract holds more functionalities than others.

Will create a new issue with a more detailed explanation and there we can discuss what's the best approach 🙌

Also, I think we are already handling mult-chain stuff in this PR nicely 🙌 !!!

Tysm again Samuel !! Let's get the merge conflict fixed, I think we are almost there merging this 🙌

@sverps
Copy link
Collaborator Author

sverps commented Nov 28, 2023

I tried doing some research and found another approach without using a custom Tuple utility and keeping chains.Chain[] we can just do this assertion in the end as const satisfies ReadonlyDeep<ScaffoldConfig> where ReadOnlyDeep comes from type-fest (which we are already using )

Nice find, I'll try it out. Sounds like a better solution than the custom Tuple type (that is indeed limited to a certain max amount of chains)

@sverps
Copy link
Collaborator Author

sverps commented Nov 28, 2023

@technophile-04 @carletex @rin-st
Conflicts resolved and final change made. Let me know if I missed anything 😊

Copy link
Collaborator

@technophile-04 technophile-04 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome stuff nicely tested out and works like a charm, Tysm Samuel !!!


Just found two minor bugs : one with useScaffoldContract and other with useScaffoldEventHistory

Screen.Recording.2023-11-30.at.12.52.19.AM.mov

Regarding useScaffoldContract : If you look at the above video the read from the contract instance of useScaffoldContract gives undefined ideally it should have given value from 0th chain if the wallet is not connected (similar to what it's doing when we use useScaffoldContractRead)......I think the contract instance we are getting from useScaffoldContract is always undefined for some reason.

Regarding useScaffoldEvenHistory : It also kind of broken I think when you switch chain, if you look at the video when I switched from sepolia (which had 6 events ) to goerli (which had only 1 event) it showed me 7 I think it appended the 1 goerli event on top of sepolia.

Here are steps to reproduce this locally :

1 . Update targetNetwork : [chains.sepolia, chains.goerli]

2. Copy paste this in `externalContracts.ts` :
import { GenericContractsDeclaration } from "~~/utils/scaffold-eth/contract";

const externalContracts = {
  5: {
    MyContract: {
      address: "0x5bB28a827e250F6BF45D75809Ff21733e7c6704f",
      abi: [
        {
          inputs: [
            {
              internalType: "address",
              name: "_owner",
              type: "address",
            },
          ],
          stateMutability: "nonpayable",
          type: "constructor",
        },
        {
          anonymous: false,
          inputs: [
            {
              indexed: true,
              internalType: "address",
              name: "previousOwner",
              type: "address",
            },
            {
              indexed: true,
              internalType: "address",
              name: "newOwner",
              type: "address",
            },
          ],
          name: "OwnershipTransferred",
          type: "event",
        },
        {
          inputs: [],
          name: "increaseCounter",
          outputs: [],
          stateMutability: "payable",
          type: "function",
        },
        {
          inputs: [],
          name: "owner",
          outputs: [
            {
              internalType: "address",
              name: "",
              type: "address",
            },
          ],
          stateMutability: "view",
          type: "function",
        },
        {
          inputs: [],
          name: "renounceOwnership",
          outputs: [],
          stateMutability: "nonpayable",
          type: "function",
        },
        {
          inputs: [],
          name: "totalCounter",
          outputs: [
            {
              internalType: "uint256",
              name: "",
              type: "uint256",
            },
          ],
          stateMutability: "view",
          type: "function",
        },
        {
          inputs: [
            {
              internalType: "address",
              name: "newOwner",
              type: "address",
            },
          ],
          name: "transferOwnership",
          outputs: [],
          stateMutability: "nonpayable",
          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: {
        owner: "@openzeppelin/contracts/access/Ownable.sol",
        renounceOwnership: "@openzeppelin/contracts/access/Ownable.sol",
        transferOwnership: "@openzeppelin/contracts/access/Ownable.sol",
      },
    },
    YourContract: {
      address: "0x1a2de031A50Fe9918f0BCe71Cb060C8CA3911f55",
      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: {},
    },
  },
  11155111: {
    MyContract: {
      address: "0xd7036EDDDA3743Aa577DC24ca817f3F85099bDbD",
      abi: [
        {
          inputs: [
            {
              internalType: "address",
              name: "_owner",
              type: "address",
            },
          ],
          stateMutability: "nonpayable",
          type: "constructor",
        },
        {
          anonymous: false,
          inputs: [
            {
              indexed: true,
              internalType: "address",
              name: "previousOwner",
              type: "address",
            },
            {
              indexed: true,
              internalType: "address",
              name: "newOwner",
              type: "address",
            },
          ],
          name: "OwnershipTransferred",
          type: "event",
        },
        {
          inputs: [],
          name: "increaseCounter",
          outputs: [],
          stateMutability: "payable",
          type: "function",
        },
        {
          inputs: [],
          name: "owner",
          outputs: [
            {
              internalType: "address",
              name: "",
              type: "address",
            },
          ],
          stateMutability: "view",
          type: "function",
        },
        {
          inputs: [],
          name: "renounceOwnership",
          outputs: [],
          stateMutability: "nonpayable",
          type: "function",
        },
        {
          inputs: [],
          name: "totalCounter",
          outputs: [
            {
              internalType: "uint256",
              name: "",
              type: "uint256",
            },
          ],
          stateMutability: "view",
          type: "function",
        },
        {
          inputs: [
            {
              internalType: "address",
              name: "newOwner",
              type: "address",
            },
          ],
          name: "transferOwnership",
          outputs: [],
          stateMutability: "nonpayable",
          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: {
        owner: "@openzeppelin/contracts/access/Ownable.sol",
        renounceOwnership: "@openzeppelin/contracts/access/Ownable.sol",
        transferOwnership: "@openzeppelin/contracts/access/Ownable.sol",
      },
    },
    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: {},
    },
  },
} as const;

export default externalContracts satisfies GenericContractsDeclaration;
3 . Copy pasted this in index.tsx
import { useEffect, useState } from "react";
import type { NextPage } from "next";
import { parseEther } from "viem";
import { useAccount, useWalletClient } from "wagmi";
import { MetaHeader } from "~~/components/MetaHeader";
import { Address, InputBase } from "~~/components/scaffold-eth";
import {
  useScaffoldContract,
  useScaffoldContractRead,
  useScaffoldContractWrite,
  useScaffoldEventHistory,
} from "~~/hooks/scaffold-eth";

const Home: NextPage = () => {
  const { address: connectedAddress } = useAccount();

  const [inputGreetings, setInputGreetings] = useState("");

  const { writeAsync: writeSetGreetings } = useScaffoldContractWrite({
    contractName: "YourContract",
    functionName: "setGreeting",
    args: [inputGreetings],
    value: parseEther("0.01"),
  });

  const { data: totalCounter } = useScaffoldContractRead({
    contractName: "YourContract",
    functionName: "totalCounter",
    watch: true,
  });

  const { data: walletClient } = useWalletClient();
  const { data: YourContract } = useScaffoldContract({
    contractName: "YourContract",
    walletClient,
  });

  useEffect(() => {
    const fetchTotalCounter = async () => {
      console.log("YourContract", YourContract);
      const value = await YourContract?.read.totalCounter();
      console.log("totalCounter", value);
    };

    fetchTotalCounter();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  const { data: eventHistory } = useScaffoldEventHistory({
    contractName: "YourContract",
    eventName: "GreetingChange",
    filters: { greetingSetter: connectedAddress },
    fromBlock: 0n,
    watch: true,
  });
  console.log("eventHistory", eventHistory);

  return (
    <>
      <MetaHeader />
      <div className="flex items-center flex-col flex-grow pt-10">
        <div className="text-center mb-8">
          <span className="block text-2xl mb-2">The connected address is</span>
          <Address address={connectedAddress} />
        </div>
        <div className="px-5 space-y-4">
          <h1 className="text-center mb-4">
            <span className="block text-3xl mb-2">With Hooks</span>
          </h1>
          <h1 className="text-center mb-4">
            <span className="block text-2xl mb-2">The totalCounter is</span>
            <span className="block text-4xl font-bold">{totalCounter?.toString()}</span>
          </h1>
          <InputBase onChange={setInputGreetings} value={inputGreetings} placeholder="Set greetings" />
          <button className="btn btn-primary btn-sm ml-12 mb-2" onClick={() => writeSetGreetings()}>
            Set Greetings
          </button>
          <h1 className="text-center mt-8">
            <span className="block text-3xl mb-2">Imperative</span>
          </h1>
          <button
            className="btn btn-primary btn-sm"
            onClick={async () => {
              await YourContract?.write.setGreeting([inputGreetings], { value: parseEther("0.01") });
            }}
          >
            Set Imperative Greetings
          </button>
        </div>
      </div>
    </>
  );
};

export default Home;

If it get too complicated to handle maybe we can handle it in a different PR and even @damianmarti can help us with the issue with useScffoldEventHistory there 🙌


So the TODO's after this PR might be :

  • Placement of network switch button
  • Handle bugs for useScaffoldContract and useScaffoldEventHistory
  • Handle autocompletions even if there are different contract names

Thanks again Samuel and others for reviews 🙌

@sverps
Copy link
Collaborator Author

sverps commented Nov 29, 2023

Regarding useScaffoldContract : If you look at the above video the read from the contract instance of useScaffoldContract gives undefined ideally it should have given value from 0th chain if the wallet is not connected (similar to what it's doing when we use useScaffoldContractRead)......I think the contract instance we are getting from useScaffoldContract is always undefined for some reason.

useScaffoldContract works with a loading state (because internally in useDeployedContractInfo, it checks the bytecode to see if it is deployed), so the contract is indeed undefined when initializing. If you add YourContract to the dependency array of the useEffect, you'll see it reads the value properly as soon as the check resolves. (The same behavior occurs on our current main branch)

Regarding useScaffoldEvenHistory

Good find 👍
I fixed it by clearing the internal state when the targetNetwork changes

@technophile-04
Copy link
Collaborator

If you add YourContract to the dependency array of the useEffect, you'll see it reads the value properly as soon as the check resolves.

Ohh shit, my bad...yup it works nicely 🙌

I fixed it by clearing the internal state when the targetNetwork changes

Yup makes sense, but still sometimes it still works a bit weirdly :

Screen.Recording.2023-11-30.at.11.10.35.AM.mov

I think maybe it's worth handling in different PR nicely, will create an issue for it


Tysm all merging this 🙌 !!

@technophile-04 technophile-04 merged commit e7f6091 into main Nov 30, 2023
1 check passed
@technophile-04 technophile-04 deleted the feat/multi-chain branch November 30, 2023 05:54
@carletex
Copy link
Member

This is a great job @sverps. This feature was a long time coming!

Thanks all

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Allow multiple target networks
6 participants