From 1a8623d4106d39a938c3f62f7bf4a79e410cc0d5 Mon Sep 17 00:00:00 2001 From: sanam-umer Date: Wed, 7 Feb 2024 10:25:11 +0530 Subject: [PATCH] added --- packages/nextjs/.env.example | 13 + packages/nextjs/.eslintignore | 11 + packages/nextjs/.eslintrc.json | 15 + packages/nextjs/.gitignore | 39 ++ packages/nextjs/.npmrc | 1 + packages/nextjs/.prettierrc.json | 8 + packages/nextjs/components/Footer.tsx | 100 +++ packages/nextjs/components/Header.tsx | 108 +++ packages/nextjs/components/MetaHeader.tsx | 52 ++ packages/nextjs/components/SwitchTheme.tsx | 31 + .../components/assets/BuidlGuidlLogo.tsx | 18 + packages/nextjs/components/assets/Spinner.tsx | 23 + .../blockexplorer/AddressCodeTab.tsx | 25 + .../blockexplorer/AddressLogsTab.tsx | 21 + .../blockexplorer/AddressStorageTab.tsx | 59 ++ .../blockexplorer/PaginationButton.tsx | 39 ++ .../components/blockexplorer/SearchBar.tsx | 47 ++ .../blockexplorer/TransactionHash.tsx | 37 + .../blockexplorer/TransactionsTable.tsx | 71 ++ .../nextjs/components/blockexplorer/index.tsx | 7 + .../components/scaffold-eth/Address.tsx | 129 ++++ .../components/scaffold-eth/Balance.tsx | 55 ++ .../components/scaffold-eth/BlockieAvatar.tsx | 15 + .../scaffold-eth/Contract/ContractInput.tsx | 45 ++ .../Contract/ContractReadMethods.tsx | 42 ++ .../scaffold-eth/Contract/ContractUI.tsx | 102 +++ .../Contract/ContractVariables.tsx | 49 ++ .../Contract/ContractWriteMethods.tsx | 48 ++ .../scaffold-eth/Contract/DisplayVariable.tsx | 69 ++ .../Contract/InheritanceTooltip.tsx | 14 + .../Contract/ReadOnlyFunctionForm.tsx | 82 +++ .../scaffold-eth/Contract/TxReceipt.tsx | 48 ++ .../Contract/WriteOnlyFunctionForm.tsx | 134 ++++ .../scaffold-eth/Contract/index.tsx | 8 + .../scaffold-eth/Contract/utilsContract.tsx | 82 +++ .../scaffold-eth/Contract/utilsDisplay.tsx | 56 ++ .../nextjs/components/scaffold-eth/Faucet.tsx | 130 ++++ .../scaffold-eth/FaucetBuildBear.tsx | 174 +++++ .../components/scaffold-eth/FaucetButton.tsx | 69 ++ .../scaffold-eth/Input/AddressInput.tsx | 90 +++ .../scaffold-eth/Input/Bytes32Input.tsx | 30 + .../scaffold-eth/Input/BytesInput.tsx | 27 + .../scaffold-eth/Input/EtherInput.tsx | 112 +++ .../scaffold-eth/Input/InputBase.tsx | 49 ++ .../scaffold-eth/Input/IntegerInput.tsx | 64 ++ .../components/scaffold-eth/Input/index.ts | 7 + .../components/scaffold-eth/Input/utils.ts | 111 +++ .../AddressInfoDropdown.tsx | 132 ++++ .../AddressQRCodeModal.tsx | 32 + .../NetworkOptions.tsx | 47 ++ .../WrongNetworkDropdown.tsx | 32 + .../RainbowKitCustomConnectButton/index.tsx | 64 ++ .../nextjs/components/scaffold-eth/index.tsx | 9 + .../nextjs/contracts/deployedContracts.ts | 654 ++++++++++++++++++ .../nextjs/contracts/externalContracts.ts | 15 + packages/nextjs/hooks/scaffold-eth/index.ts | 16 + .../hooks/scaffold-eth/useAccountBalance.ts | 35 + .../hooks/scaffold-eth/useAnimationConfig.ts | 20 + .../hooks/scaffold-eth/useAutoConnect.ts | 85 +++ .../hooks/scaffold-eth/useBurnerWallet.ts | 141 ++++ .../hooks/scaffold-eth/useContractLogs.ts | 37 + .../scaffold-eth/useDeployedContractInfo.ts | 46 ++ .../hooks/scaffold-eth/useFetchBlocks.ts | 133 ++++ .../scaffold-eth/useNativeCurrencyPrice.ts | 35 + .../hooks/scaffold-eth/useNetworkColor.ts | 20 + .../hooks/scaffold-eth/useOutsideClick.ts | 21 + .../hooks/scaffold-eth/useScaffoldContract.ts | 48 ++ .../scaffold-eth/useScaffoldContractRead.ts | 48 ++ .../scaffold-eth/useScaffoldContractWrite.ts | 102 +++ .../scaffold-eth/useScaffoldEventHistory.ts | 184 +++++ .../useScaffoldEventSubscriber.ts | 38 + .../hooks/scaffold-eth/useTargetNetwork.ts | 26 + .../hooks/scaffold-eth/useTransactor.tsx | 106 +++ packages/nextjs/next-env.d.ts | 5 + packages/nextjs/next.config.js | 18 + packages/nextjs/package.json | 58 ++ packages/nextjs/pages/_app.tsx | 64 ++ .../pages/blockexplorer/address/[address].tsx | 195 ++++++ packages/nextjs/pages/blockexplorer/index.tsx | 62 ++ .../blockexplorer/transaction/[txHash].tsx | 149 ++++ packages/nextjs/pages/debug.tsx | 151 ++++ packages/nextjs/pages/index.tsx | 82 +++ packages/nextjs/postcss.config.js | 6 + packages/nextjs/public/favicon.png | Bin 0 -> 5745 bytes packages/nextjs/public/logo.svg | 10 + packages/nextjs/public/manifest.json | 5 + packages/nextjs/public/thumbnail.jpg | Bin 0 -> 19855 bytes packages/nextjs/sandbox.json | 9 + packages/nextjs/scaffold.config.ts | 72 ++ packages/nextjs/services/store/store.ts | 26 + .../web3/wagmi-burner/BurnerConnector.ts | 156 +++++ .../wagmi-burner/BurnerConnectorErrors.ts | 23 + .../web3/wagmi-burner/BurnerConnectorTypes.ts | 10 + .../web3/wagmi-burner/burnerWalletConfig.ts | 41 ++ packages/nextjs/services/web3/wagmiConfig.tsx | 8 + .../nextjs/services/web3/wagmiConnectors.tsx | 76 ++ packages/nextjs/styles/globals.css | 32 + packages/nextjs/tailwind.config.js | 86 +++ packages/nextjs/tsconfig.json | 23 + packages/nextjs/types/abitype/abi.d.ts | 13 + packages/nextjs/types/utils.ts | 3 + packages/nextjs/utils/scaffold-eth/block.ts | 17 + packages/nextjs/utils/scaffold-eth/common.ts | 3 + .../nextjs/utils/scaffold-eth/contract.ts | 274 ++++++++ .../utils/scaffold-eth/contractNames.ts | 7 + .../nextjs/utils/scaffold-eth/decodeTxData.ts | 59 ++ .../scaffold-eth/fetchPriceFromUniswap.ts | 71 ++ packages/nextjs/utils/scaffold-eth/index.ts | 5 + .../nextjs/utils/scaffold-eth/networks.ts | 113 +++ .../utils/scaffold-eth/notification.tsx | 91 +++ 110 files changed, 6715 insertions(+) create mode 100644 packages/nextjs/.env.example create mode 100644 packages/nextjs/.eslintignore create mode 100644 packages/nextjs/.eslintrc.json create mode 100644 packages/nextjs/.gitignore create mode 100644 packages/nextjs/.npmrc create mode 100644 packages/nextjs/.prettierrc.json create mode 100644 packages/nextjs/components/Footer.tsx create mode 100644 packages/nextjs/components/Header.tsx create mode 100644 packages/nextjs/components/MetaHeader.tsx create mode 100644 packages/nextjs/components/SwitchTheme.tsx create mode 100644 packages/nextjs/components/assets/BuidlGuidlLogo.tsx create mode 100644 packages/nextjs/components/assets/Spinner.tsx create mode 100644 packages/nextjs/components/blockexplorer/AddressCodeTab.tsx create mode 100644 packages/nextjs/components/blockexplorer/AddressLogsTab.tsx create mode 100644 packages/nextjs/components/blockexplorer/AddressStorageTab.tsx create mode 100644 packages/nextjs/components/blockexplorer/PaginationButton.tsx create mode 100644 packages/nextjs/components/blockexplorer/SearchBar.tsx create mode 100644 packages/nextjs/components/blockexplorer/TransactionHash.tsx create mode 100644 packages/nextjs/components/blockexplorer/TransactionsTable.tsx create mode 100644 packages/nextjs/components/blockexplorer/index.tsx create mode 100644 packages/nextjs/components/scaffold-eth/Address.tsx create mode 100644 packages/nextjs/components/scaffold-eth/Balance.tsx create mode 100644 packages/nextjs/components/scaffold-eth/BlockieAvatar.tsx create mode 100644 packages/nextjs/components/scaffold-eth/Contract/ContractInput.tsx create mode 100644 packages/nextjs/components/scaffold-eth/Contract/ContractReadMethods.tsx create mode 100644 packages/nextjs/components/scaffold-eth/Contract/ContractUI.tsx create mode 100644 packages/nextjs/components/scaffold-eth/Contract/ContractVariables.tsx create mode 100644 packages/nextjs/components/scaffold-eth/Contract/ContractWriteMethods.tsx create mode 100644 packages/nextjs/components/scaffold-eth/Contract/DisplayVariable.tsx create mode 100644 packages/nextjs/components/scaffold-eth/Contract/InheritanceTooltip.tsx create mode 100644 packages/nextjs/components/scaffold-eth/Contract/ReadOnlyFunctionForm.tsx create mode 100644 packages/nextjs/components/scaffold-eth/Contract/TxReceipt.tsx create mode 100644 packages/nextjs/components/scaffold-eth/Contract/WriteOnlyFunctionForm.tsx create mode 100644 packages/nextjs/components/scaffold-eth/Contract/index.tsx create mode 100644 packages/nextjs/components/scaffold-eth/Contract/utilsContract.tsx create mode 100644 packages/nextjs/components/scaffold-eth/Contract/utilsDisplay.tsx create mode 100644 packages/nextjs/components/scaffold-eth/Faucet.tsx create mode 100644 packages/nextjs/components/scaffold-eth/FaucetBuildBear.tsx create mode 100644 packages/nextjs/components/scaffold-eth/FaucetButton.tsx create mode 100644 packages/nextjs/components/scaffold-eth/Input/AddressInput.tsx create mode 100644 packages/nextjs/components/scaffold-eth/Input/Bytes32Input.tsx create mode 100644 packages/nextjs/components/scaffold-eth/Input/BytesInput.tsx create mode 100644 packages/nextjs/components/scaffold-eth/Input/EtherInput.tsx create mode 100644 packages/nextjs/components/scaffold-eth/Input/InputBase.tsx create mode 100644 packages/nextjs/components/scaffold-eth/Input/IntegerInput.tsx create mode 100644 packages/nextjs/components/scaffold-eth/Input/index.ts create mode 100644 packages/nextjs/components/scaffold-eth/Input/utils.ts create mode 100644 packages/nextjs/components/scaffold-eth/RainbowKitCustomConnectButton/AddressInfoDropdown.tsx create mode 100644 packages/nextjs/components/scaffold-eth/RainbowKitCustomConnectButton/AddressQRCodeModal.tsx create mode 100644 packages/nextjs/components/scaffold-eth/RainbowKitCustomConnectButton/NetworkOptions.tsx create mode 100644 packages/nextjs/components/scaffold-eth/RainbowKitCustomConnectButton/WrongNetworkDropdown.tsx create mode 100644 packages/nextjs/components/scaffold-eth/RainbowKitCustomConnectButton/index.tsx create mode 100644 packages/nextjs/components/scaffold-eth/index.tsx create mode 100644 packages/nextjs/contracts/deployedContracts.ts create mode 100644 packages/nextjs/contracts/externalContracts.ts create mode 100644 packages/nextjs/hooks/scaffold-eth/index.ts create mode 100644 packages/nextjs/hooks/scaffold-eth/useAccountBalance.ts create mode 100644 packages/nextjs/hooks/scaffold-eth/useAnimationConfig.ts create mode 100644 packages/nextjs/hooks/scaffold-eth/useAutoConnect.ts create mode 100644 packages/nextjs/hooks/scaffold-eth/useBurnerWallet.ts create mode 100644 packages/nextjs/hooks/scaffold-eth/useContractLogs.ts create mode 100644 packages/nextjs/hooks/scaffold-eth/useDeployedContractInfo.ts create mode 100644 packages/nextjs/hooks/scaffold-eth/useFetchBlocks.ts create mode 100644 packages/nextjs/hooks/scaffold-eth/useNativeCurrencyPrice.ts create mode 100644 packages/nextjs/hooks/scaffold-eth/useNetworkColor.ts create mode 100644 packages/nextjs/hooks/scaffold-eth/useOutsideClick.ts create mode 100644 packages/nextjs/hooks/scaffold-eth/useScaffoldContract.ts create mode 100644 packages/nextjs/hooks/scaffold-eth/useScaffoldContractRead.ts create mode 100644 packages/nextjs/hooks/scaffold-eth/useScaffoldContractWrite.ts create mode 100644 packages/nextjs/hooks/scaffold-eth/useScaffoldEventHistory.ts create mode 100644 packages/nextjs/hooks/scaffold-eth/useScaffoldEventSubscriber.ts create mode 100644 packages/nextjs/hooks/scaffold-eth/useTargetNetwork.ts create mode 100644 packages/nextjs/hooks/scaffold-eth/useTransactor.tsx create mode 100644 packages/nextjs/next-env.d.ts create mode 100644 packages/nextjs/next.config.js create mode 100644 packages/nextjs/package.json create mode 100644 packages/nextjs/pages/_app.tsx create mode 100644 packages/nextjs/pages/blockexplorer/address/[address].tsx create mode 100644 packages/nextjs/pages/blockexplorer/index.tsx create mode 100644 packages/nextjs/pages/blockexplorer/transaction/[txHash].tsx create mode 100644 packages/nextjs/pages/debug.tsx create mode 100644 packages/nextjs/pages/index.tsx create mode 100644 packages/nextjs/postcss.config.js create mode 100644 packages/nextjs/public/favicon.png create mode 100644 packages/nextjs/public/logo.svg create mode 100644 packages/nextjs/public/manifest.json create mode 100644 packages/nextjs/public/thumbnail.jpg create mode 100644 packages/nextjs/sandbox.json create mode 100644 packages/nextjs/scaffold.config.ts create mode 100644 packages/nextjs/services/store/store.ts create mode 100644 packages/nextjs/services/web3/wagmi-burner/BurnerConnector.ts create mode 100644 packages/nextjs/services/web3/wagmi-burner/BurnerConnectorErrors.ts create mode 100644 packages/nextjs/services/web3/wagmi-burner/BurnerConnectorTypes.ts create mode 100644 packages/nextjs/services/web3/wagmi-burner/burnerWalletConfig.ts create mode 100644 packages/nextjs/services/web3/wagmiConfig.tsx create mode 100644 packages/nextjs/services/web3/wagmiConnectors.tsx create mode 100644 packages/nextjs/styles/globals.css create mode 100644 packages/nextjs/tailwind.config.js create mode 100644 packages/nextjs/tsconfig.json create mode 100644 packages/nextjs/types/abitype/abi.d.ts create mode 100644 packages/nextjs/types/utils.ts create mode 100644 packages/nextjs/utils/scaffold-eth/block.ts create mode 100644 packages/nextjs/utils/scaffold-eth/common.ts create mode 100644 packages/nextjs/utils/scaffold-eth/contract.ts create mode 100644 packages/nextjs/utils/scaffold-eth/contractNames.ts create mode 100644 packages/nextjs/utils/scaffold-eth/decodeTxData.ts create mode 100644 packages/nextjs/utils/scaffold-eth/fetchPriceFromUniswap.ts create mode 100644 packages/nextjs/utils/scaffold-eth/index.ts create mode 100644 packages/nextjs/utils/scaffold-eth/networks.ts create mode 100644 packages/nextjs/utils/scaffold-eth/notification.tsx diff --git a/packages/nextjs/.env.example b/packages/nextjs/.env.example new file mode 100644 index 0000000..c8d03d7 --- /dev/null +++ b/packages/nextjs/.env.example @@ -0,0 +1,13 @@ +# Template for NextJS environment variables. + +# For local development, copy this file, rename it to .env.local, and fill in the values. +# When deploying live, you'll need to store the vars in Vercel/System config. + +# If not set, we provide default values (check `scaffold.config.ts`) so developers can start prototyping out of the box, +# but we recommend getting your own API Keys for Production Apps. + +# To access the values stored in this env file you can use: process.env.VARIABLENAME +# You'll need to prefix the variables names with NEXT_PUBLIC_ if you want to access them on the client side. +# More info: https://nextjs.org/docs/pages/building-your-application/configuring/environment-variables +NEXT_PUBLIC_ALCHEMY_API_KEY= +NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID= diff --git a/packages/nextjs/.eslintignore b/packages/nextjs/.eslintignore new file mode 100644 index 0000000..2ea0b64 --- /dev/null +++ b/packages/nextjs/.eslintignore @@ -0,0 +1,11 @@ +# folders +.next +node_modules/ +# files +**/*.less +**/*.css +**/*.scss +**/*.json +**/*.png +**/*.svg +**/generated/**/* diff --git a/packages/nextjs/.eslintrc.json b/packages/nextjs/.eslintrc.json new file mode 100644 index 0000000..c120c8c --- /dev/null +++ b/packages/nextjs/.eslintrc.json @@ -0,0 +1,15 @@ +{ + "parser": "@typescript-eslint/parser", + "extends": ["next/core-web-vitals", "plugin:prettier/recommended", "plugin:@typescript-eslint/recommended"], + "rules": { + "@typescript-eslint/no-unused-vars": ["error"], + "@typescript-eslint/no-explicit-any": ["off"], + "@typescript-eslint/ban-ts-comment": ["off"], + "prettier/prettier": [ + "warn", + { + "endOfLine": "auto" + } + ] + } +} diff --git a/packages/nextjs/.gitignore b/packages/nextjs/.gitignore new file mode 100644 index 0000000..9dbfdd5 --- /dev/null +++ b/packages/nextjs/.gitignore @@ -0,0 +1,39 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# local env files +.env.local +.env.development.local +.env.test.local +.env.production.local +.env + +# vercel +.vercel + +# typescript +*.tsbuildinfo \ No newline at end of file diff --git a/packages/nextjs/.npmrc b/packages/nextjs/.npmrc new file mode 100644 index 0000000..aff8a32 --- /dev/null +++ b/packages/nextjs/.npmrc @@ -0,0 +1 @@ +strict-peer-dependencies = false diff --git a/packages/nextjs/.prettierrc.json b/packages/nextjs/.prettierrc.json new file mode 100644 index 0000000..36fa5f3 --- /dev/null +++ b/packages/nextjs/.prettierrc.json @@ -0,0 +1,8 @@ +{ + "arrowParens": "avoid", + "printWidth": 120, + "tabWidth": 2, + "trailingComma": "all", + "importOrder": ["^react$", "^next/(.*)$", "", "^@heroicons/(.*)$", "^~~/(.*)$"], + "importOrderSortSpecifiers": true +} diff --git a/packages/nextjs/components/Footer.tsx b/packages/nextjs/components/Footer.tsx new file mode 100644 index 0000000..903ab88 --- /dev/null +++ b/packages/nextjs/components/Footer.tsx @@ -0,0 +1,100 @@ +import React from "react"; +import Link from "next/link"; +import buildbearSandbox from "../sandbox.json"; +import { hardhat } from "viem/chains"; +import { CurrencyDollarIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline"; +import { HeartIcon } from "@heroicons/react/24/outline"; +import { SwitchTheme } from "~~/components/SwitchTheme"; +import { Faucet, FaucetBuildBear } from "~~/components/scaffold-eth"; +import { useTargetNetwork } from "~~/hooks/scaffold-eth/useTargetNetwork"; +import { useGlobalState } from "~~/services/store/store"; + +/** + * Site footer + */ +export const Footer = () => { + const nativeCurrencyPrice = useGlobalState(state => state.nativeCurrencyPrice); + const { targetNetwork } = useTargetNetwork(); + const isLocalNetwork = targetNetwork.id === hardhat.id; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const isBuildBear = targetNetwork.name! === "BuildBear"; + + return ( +
+
+
+
+ {nativeCurrencyPrice > 0 && ( +
+
+ + {nativeCurrencyPrice} +
+
+ )} + {isLocalNetwork && ( + <> + + + + Block Explorer + + + )} + {isBuildBear && ( + <> + + + + Block Explorer + + + )} +
+ +
+
+
+ +
+
+ ); +}; diff --git a/packages/nextjs/components/Header.tsx b/packages/nextjs/components/Header.tsx new file mode 100644 index 0000000..461bade --- /dev/null +++ b/packages/nextjs/components/Header.tsx @@ -0,0 +1,108 @@ +import React, { useCallback, useRef, useState } from "react"; +import Image from "next/image"; +import Link from "next/link"; +import { useRouter } from "next/router"; +import { Bars3Icon, BugAntIcon } from "@heroicons/react/24/outline"; +import { FaucetButton, RainbowKitCustomConnectButton } from "~~/components/scaffold-eth"; +import { useOutsideClick } from "~~/hooks/scaffold-eth"; + +type HeaderMenuLink = { + label: string; + href: string; + icon?: React.ReactNode; +}; + +export const menuLinks: HeaderMenuLink[] = [ + { + label: "Home", + href: "/", + }, + { + label: "Debug Contracts", + href: "/debug", + icon: , + }, +]; + +export const HeaderMenuLinks = () => { + const router = useRouter(); + + return ( + <> + {menuLinks.map(({ label, href, icon }) => { + const isActive = router.pathname === href; + return ( +
  • + + {icon} + {label} + +
  • + ); + })} + + ); +}; + +/** + * Site header + */ +export const Header = () => { + const [isDrawerOpen, setIsDrawerOpen] = useState(false); + const burgerMenuRef = useRef(null); + useOutsideClick( + burgerMenuRef, + useCallback(() => setIsDrawerOpen(false), []), + ); + + return ( +
    +
    +
    + + {isDrawerOpen && ( +
      { + setIsDrawerOpen(false); + }} + > + +
    + )} +
    + +
    + SE2 logo +
    +
    + Scaffold-ETH x BuildBear + Ethereum dev stack +
    + +
      + +
    +
    +
    + + +
    +
    + ); +}; diff --git a/packages/nextjs/components/MetaHeader.tsx b/packages/nextjs/components/MetaHeader.tsx new file mode 100644 index 0000000..d348fa6 --- /dev/null +++ b/packages/nextjs/components/MetaHeader.tsx @@ -0,0 +1,52 @@ +import React from "react"; +import Head from "next/head"; + +type MetaHeaderProps = { + title?: string; + description?: string; + image?: string; + twitterCard?: string; + children?: React.ReactNode; +}; + +// Images must have an absolute path to work properly on Twitter. +// We try to get it dynamically from Vercel, but we default to relative path. +const baseUrl = process.env.NEXT_PUBLIC_VERCEL_URL ? `https://${process.env.NEXT_PUBLIC_VERCEL_URL}/` : "/"; + +export const MetaHeader = ({ + title = "Se-2 x Buildbear", + description = "Built with 🏗 Scaffold-ETH 2 & BuildBear", + image = "thumbnail.jpg", + twitterCard = "summary_large_image", + children, +}: MetaHeaderProps) => { + const imageUrl = baseUrl + image; + + return ( + + {title && ( + <> + {title} + + + + )} + {description && ( + <> + + + + + )} + {image && ( + <> + + + + )} + {twitterCard && } + + {children} + + ); +}; diff --git a/packages/nextjs/components/SwitchTheme.tsx b/packages/nextjs/components/SwitchTheme.tsx new file mode 100644 index 0000000..4752e35 --- /dev/null +++ b/packages/nextjs/components/SwitchTheme.tsx @@ -0,0 +1,31 @@ +import { useEffect } from "react"; +import { useDarkMode, useIsMounted } from "usehooks-ts"; +import { MoonIcon, SunIcon } from "@heroicons/react/24/outline"; + +export const SwitchTheme = ({ className }: { className?: string }) => { + const { isDarkMode, toggle } = useDarkMode(); + const isMounted = useIsMounted(); + + useEffect(() => { + const body = document.body; + body.setAttribute("data-theme", isDarkMode ? "scaffoldEthDark" : "scaffoldEth"); + }, [isDarkMode]); + + return ( +
    + + {isMounted() && ( + + )} +
    + ); +}; diff --git a/packages/nextjs/components/assets/BuidlGuidlLogo.tsx b/packages/nextjs/components/assets/BuidlGuidlLogo.tsx new file mode 100644 index 0000000..af46b02 --- /dev/null +++ b/packages/nextjs/components/assets/BuidlGuidlLogo.tsx @@ -0,0 +1,18 @@ +export const BuidlGuidlLogo = ({ className }: { className: string }) => { + return ( + + + + ); +}; diff --git a/packages/nextjs/components/assets/Spinner.tsx b/packages/nextjs/components/assets/Spinner.tsx new file mode 100644 index 0000000..01f103c --- /dev/null +++ b/packages/nextjs/components/assets/Spinner.tsx @@ -0,0 +1,23 @@ +export const Spinner = ({ width, height }: { width?: string; height?: string }) => { + return ( + + ); +}; diff --git a/packages/nextjs/components/blockexplorer/AddressCodeTab.tsx b/packages/nextjs/components/blockexplorer/AddressCodeTab.tsx new file mode 100644 index 0000000..ff57c43 --- /dev/null +++ b/packages/nextjs/components/blockexplorer/AddressCodeTab.tsx @@ -0,0 +1,25 @@ +type AddressCodeTabProps = { + bytecode: string; + assembly: string; +}; + +export const AddressCodeTab = ({ bytecode, assembly }: AddressCodeTabProps) => { + const formattedAssembly = assembly.split(" ").join("\n"); + + return ( +
    + Bytecode +
    +
    +          {bytecode}
    +        
    +
    + Opcodes +
    +
    +          {formattedAssembly}
    +        
    +
    +
    + ); +}; diff --git a/packages/nextjs/components/blockexplorer/AddressLogsTab.tsx b/packages/nextjs/components/blockexplorer/AddressLogsTab.tsx new file mode 100644 index 0000000..9d2ab0e --- /dev/null +++ b/packages/nextjs/components/blockexplorer/AddressLogsTab.tsx @@ -0,0 +1,21 @@ +import { Address } from "viem"; +import { useContractLogs } from "~~/hooks/scaffold-eth"; +import { replacer } from "~~/utils/scaffold-eth/common"; + +export const AddressLogsTab = ({ address }: { address: Address }) => { + const contractLogs = useContractLogs(address); + + return ( +
    +
    +
    +          {contractLogs.map((log, i) => (
    +            
    + Log: {JSON.stringify(log, replacer, 2)} +
    + ))} +
    +
    +
    + ); +}; diff --git a/packages/nextjs/components/blockexplorer/AddressStorageTab.tsx b/packages/nextjs/components/blockexplorer/AddressStorageTab.tsx new file mode 100644 index 0000000..f9f4d4f --- /dev/null +++ b/packages/nextjs/components/blockexplorer/AddressStorageTab.tsx @@ -0,0 +1,59 @@ +import { useEffect, useState } from "react"; +import { createPublicClient, http, toHex } from "viem"; +import { hardhat } from "viem/chains"; + +const publicClient = createPublicClient({ + chain: hardhat, + transport: http(), +}); + +export const AddressStorageTab = ({ address }: { address: string }) => { + const [storage, setStorage] = useState([]); + + useEffect(() => { + const fetchStorage = async () => { + try { + const storageData = []; + let idx = 0; + + while (true) { + const storageAtPosition = await publicClient.getStorageAt({ + address: address, + slot: toHex(idx), + }); + + if (storageAtPosition === "0x" + "0".repeat(64)) break; + + if (storageAtPosition) { + storageData.push(storageAtPosition); + } + + idx++; + } + setStorage(storageData); + } catch (error) { + console.error("Failed to fetch storage:", error); + } + }; + + fetchStorage(); + }, [address]); + + return ( +
    + {storage.length > 0 ? ( +
    +
    +            {storage.map((data, i) => (
    +              
    + Storage Slot {i}: {data} +
    + ))} +
    +
    + ) : ( +
    This contract does not have any variables.
    + )} +
    + ); +}; diff --git a/packages/nextjs/components/blockexplorer/PaginationButton.tsx b/packages/nextjs/components/blockexplorer/PaginationButton.tsx new file mode 100644 index 0000000..77aefbc --- /dev/null +++ b/packages/nextjs/components/blockexplorer/PaginationButton.tsx @@ -0,0 +1,39 @@ +import { ArrowLeftIcon, ArrowRightIcon } from "@heroicons/react/24/outline"; + +type PaginationButtonProps = { + currentPage: number; + totalItems: number; + setCurrentPage: (page: number) => void; +}; + +const ITEMS_PER_PAGE = 20; + +export const PaginationButton = ({ currentPage, totalItems, setCurrentPage }: PaginationButtonProps) => { + const isPrevButtonDisabled = currentPage === 0; + const isNextButtonDisabled = currentPage + 1 >= Math.ceil(totalItems / ITEMS_PER_PAGE); + + const prevButtonClass = isPrevButtonDisabled ? "bg-gray-200 cursor-default" : "btn btn-primary"; + const nextButtonClass = isNextButtonDisabled ? "bg-gray-200 cursor-default" : "btn btn-primary"; + + if (isNextButtonDisabled && isPrevButtonDisabled) return null; + + return ( +
    + + Page {currentPage + 1} + +
    + ); +}; diff --git a/packages/nextjs/components/blockexplorer/SearchBar.tsx b/packages/nextjs/components/blockexplorer/SearchBar.tsx new file mode 100644 index 0000000..abbf691 --- /dev/null +++ b/packages/nextjs/components/blockexplorer/SearchBar.tsx @@ -0,0 +1,47 @@ +import { useState } from "react"; +import { useRouter } from "next/router"; +import { isAddress, isHex } from "viem"; +import { hardhat } from "viem/chains"; +import { usePublicClient } from "wagmi"; + +export const SearchBar = () => { + const [searchInput, setSearchInput] = useState(""); + const router = useRouter(); + + const client = usePublicClient({ chainId: hardhat.id }); + + const handleSearch = async (event: React.FormEvent) => { + event.preventDefault(); + if (isHex(searchInput)) { + try { + const tx = await client.getTransaction({ hash: searchInput }); + if (tx) { + router.push(`/blockexplorer/transaction/${searchInput}`); + return; + } + } catch (error) { + console.error("Failed to fetch transaction:", error); + } + } + + if (isAddress(searchInput)) { + router.push(`/blockexplorer/address/${searchInput}`); + return; + } + }; + + return ( +
    + setSearchInput(e.target.value)} + /> + +
    + ); +}; diff --git a/packages/nextjs/components/blockexplorer/TransactionHash.tsx b/packages/nextjs/components/blockexplorer/TransactionHash.tsx new file mode 100644 index 0000000..5d36151 --- /dev/null +++ b/packages/nextjs/components/blockexplorer/TransactionHash.tsx @@ -0,0 +1,37 @@ +import { useState } from "react"; +import Link from "next/link"; +import { CopyToClipboard } from "react-copy-to-clipboard"; +import { CheckCircleIcon, DocumentDuplicateIcon } from "@heroicons/react/24/outline"; + +export const TransactionHash = ({ hash }: { hash: string }) => { + const [addressCopied, setAddressCopied] = useState(false); + + return ( +
    + + {hash?.substring(0, 6)}...{hash?.substring(hash.length - 4)} + + {addressCopied ? ( +
    + ); +}; diff --git a/packages/nextjs/components/blockexplorer/TransactionsTable.tsx b/packages/nextjs/components/blockexplorer/TransactionsTable.tsx new file mode 100644 index 0000000..e462dc9 --- /dev/null +++ b/packages/nextjs/components/blockexplorer/TransactionsTable.tsx @@ -0,0 +1,71 @@ +import { formatEther } from "viem"; +import { TransactionHash } from "~~/components/blockexplorer/TransactionHash"; +import { Address } from "~~/components/scaffold-eth"; +import { useTargetNetwork } from "~~/hooks/scaffold-eth/useTargetNetwork"; +import { TransactionWithFunction } from "~~/utils/scaffold-eth"; +import { TransactionsTableProps } from "~~/utils/scaffold-eth/"; + +export const TransactionsTable = ({ blocks, transactionReceipts }: TransactionsTableProps) => { + const { targetNetwork } = useTargetNetwork(); + + return ( +
    +
    + + + + + + + + + + + + + + {blocks.map(block => + (block.transactions as TransactionWithFunction[]).map(tx => { + const receipt = transactionReceipts[tx.hash]; + const timeMined = new Date(Number(block.timestamp) * 1000).toLocaleString(); + const functionCalled = tx.input.substring(0, 10); + + return ( + + + + + + + + + + ); + }), + )} + +
    Transaction HashFunction CalledBlock NumberTime MinedFromToValue ({targetNetwork.nativeCurrency.symbol})
    + + + {tx.functionName === "0x" ? "" : {tx.functionName}} + {functionCalled !== "0x" && ( + {functionCalled} + )} + {block.number?.toString()}{timeMined} +
    +
    + {!receipt?.contractAddress ? ( + tx.to &&
    + ) : ( +
    +
    + (Contract Creation) +
    + )} +
    + {formatEther(tx.value)} {targetNetwork.nativeCurrency.symbol} +
    +
    +
    + ); +}; diff --git a/packages/nextjs/components/blockexplorer/index.tsx b/packages/nextjs/components/blockexplorer/index.tsx new file mode 100644 index 0000000..bc0a716 --- /dev/null +++ b/packages/nextjs/components/blockexplorer/index.tsx @@ -0,0 +1,7 @@ +export * from "./AddressCodeTab"; +export * from "./AddressLogsTab"; +export * from "./AddressStorageTab"; +export * from "./PaginationButton"; +export * from "./SearchBar"; +export * from "./TransactionHash"; +export * from "./TransactionsTable"; diff --git a/packages/nextjs/components/scaffold-eth/Address.tsx b/packages/nextjs/components/scaffold-eth/Address.tsx new file mode 100644 index 0000000..057d706 --- /dev/null +++ b/packages/nextjs/components/scaffold-eth/Address.tsx @@ -0,0 +1,129 @@ +import { useEffect, useState } from "react"; +import Link from "next/link"; +import { CopyToClipboard } from "react-copy-to-clipboard"; +import { isAddress } from "viem"; +import { hardhat } from "viem/chains"; +import { useEnsAvatar, useEnsName } from "wagmi"; +import { CheckCircleIcon, DocumentDuplicateIcon } from "@heroicons/react/24/outline"; +import { BlockieAvatar } from "~~/components/scaffold-eth"; +import { useTargetNetwork } from "~~/hooks/scaffold-eth/useTargetNetwork"; +import { getBlockExplorerAddressLink } from "~~/utils/scaffold-eth"; + +type AddressProps = { + address?: string; + disableAddressLink?: boolean; + format?: "short" | "long"; + size?: "xs" | "sm" | "base" | "lg" | "xl" | "2xl" | "3xl"; +}; + +const blockieSizeMap = { + xs: 6, + sm: 7, + base: 8, + lg: 9, + xl: 10, + "2xl": 12, + "3xl": 15, +}; + +/** + * Displays an address (or ENS) with a Blockie image and option to copy address. + */ +export const Address = ({ address, disableAddressLink, format, size = "base" }: AddressProps) => { + const [ens, setEns] = useState(); + const [ensAvatar, setEnsAvatar] = useState(); + const [addressCopied, setAddressCopied] = useState(false); + + const { targetNetwork } = useTargetNetwork(); + + const { data: fetchedEns } = useEnsName({ address, enabled: isAddress(address ?? ""), chainId: 1 }); + const { data: fetchedEnsAvatar } = useEnsAvatar({ + name: fetchedEns, + enabled: Boolean(fetchedEns), + chainId: 1, + cacheTime: 30_000, + }); + + // We need to apply this pattern to avoid Hydration errors. + useEffect(() => { + setEns(fetchedEns); + }, [fetchedEns]); + + useEffect(() => { + setEnsAvatar(fetchedEnsAvatar); + }, [fetchedEnsAvatar]); + + // Skeleton UI + if (!address) { + return ( +
    +
    +
    +
    +
    +
    + ); + } + + if (!isAddress(address)) { + return Wrong address; + } + + const blockExplorerAddressLink = getBlockExplorerAddressLink(targetNetwork, address); + let displayAddress = address?.slice(0, 5) + "..." + address?.slice(-4); + + if (ens) { + displayAddress = ens; + } else if (format === "long") { + displayAddress = address; + } + + return ( +
    +
    + +
    + {disableAddressLink ? ( + {displayAddress} + ) : targetNetwork.id === hardhat.id ? ( + + {displayAddress} + + ) : ( + + {displayAddress} + + )} + {addressCopied ? ( +
    + ); +}; diff --git a/packages/nextjs/components/scaffold-eth/Balance.tsx b/packages/nextjs/components/scaffold-eth/Balance.tsx new file mode 100644 index 0000000..66bc172 --- /dev/null +++ b/packages/nextjs/components/scaffold-eth/Balance.tsx @@ -0,0 +1,55 @@ +import { useAccountBalance } from "~~/hooks/scaffold-eth"; +import { useTargetNetwork } from "~~/hooks/scaffold-eth/useTargetNetwork"; + +type BalanceProps = { + address?: string; + className?: string; +}; + +/** + * Display (ETH & USD) balance of an ETH address. + */ +export const Balance = ({ address, className = "" }: BalanceProps) => { + const { targetNetwork } = useTargetNetwork(); + const { balance, price, isError, isLoading, onToggleBalance, isEthBalance } = useAccountBalance(address); + + if (!address || isLoading || balance === null) { + return ( +
    +
    +
    +
    +
    +
    + ); + } + + if (isError) { + return ( +
    +
    Error
    +
    + ); + } + + return ( + + ); +}; diff --git a/packages/nextjs/components/scaffold-eth/BlockieAvatar.tsx b/packages/nextjs/components/scaffold-eth/BlockieAvatar.tsx new file mode 100644 index 0000000..9c1ea4d --- /dev/null +++ b/packages/nextjs/components/scaffold-eth/BlockieAvatar.tsx @@ -0,0 +1,15 @@ +import { AvatarComponent } from "@rainbow-me/rainbowkit"; +import { blo } from "blo"; + +// Custom Avatar for RainbowKit +export const BlockieAvatar: AvatarComponent = ({ address, ensImage, size }) => ( + // Don't want to use nextJS Image here (and adding remote patterns for the URL) + // eslint-disable-next-line @next/next/no-img-element + {`${address} +); diff --git a/packages/nextjs/components/scaffold-eth/Contract/ContractInput.tsx b/packages/nextjs/components/scaffold-eth/Contract/ContractInput.tsx new file mode 100644 index 0000000..396f4a9 --- /dev/null +++ b/packages/nextjs/components/scaffold-eth/Contract/ContractInput.tsx @@ -0,0 +1,45 @@ +import { Dispatch, SetStateAction } from "react"; +import { AbiParameter } from "abitype"; +import { + AddressInput, + Bytes32Input, + BytesInput, + InputBase, + IntegerInput, + IntegerVariant, +} from "~~/components/scaffold-eth"; + +type ContractInputProps = { + setForm: Dispatch>>; + form: Record | undefined; + stateObjectKey: string; + paramType: AbiParameter; +}; + +/** + * Generic Input component to handle input's based on their function param type + */ +export const ContractInput = ({ setForm, form, stateObjectKey, paramType }: ContractInputProps) => { + const inputProps = { + name: stateObjectKey, + value: form?.[stateObjectKey], + placeholder: paramType.name ? `${paramType.type} ${paramType.name}` : paramType.type, + onChange: (value: any) => { + setForm(form => ({ ...form, [stateObjectKey]: value })); + }, + }; + + if (paramType.type === "address") { + return ; + } else if (paramType.type === "bytes32") { + return ; + } else if (paramType.type === "bytes") { + return ; + } else if (paramType.type === "string") { + return ; + } else if (paramType.type.includes("int") && !paramType.type.includes("[")) { + return ; + } + + return ; +}; diff --git a/packages/nextjs/components/scaffold-eth/Contract/ContractReadMethods.tsx b/packages/nextjs/components/scaffold-eth/Contract/ContractReadMethods.tsx new file mode 100644 index 0000000..13eae1c --- /dev/null +++ b/packages/nextjs/components/scaffold-eth/Contract/ContractReadMethods.tsx @@ -0,0 +1,42 @@ +import { ReadOnlyFunctionForm } from "./ReadOnlyFunctionForm"; +import { Abi, AbiFunction } from "abitype"; +import { Contract, ContractName, GenericContract, InheritedFunctions } from "~~/utils/scaffold-eth/contract"; + +export const ContractReadMethods = ({ deployedContractData }: { deployedContractData: Contract }) => { + if (!deployedContractData) { + return null; + } + + const functionsToDisplay = ( + ((deployedContractData.abi || []) as Abi).filter(part => part.type === "function") as AbiFunction[] + ) + .filter(fn => { + const isQueryableWithParams = + (fn.stateMutability === "view" || fn.stateMutability === "pure") && fn.inputs.length > 0; + return isQueryableWithParams; + }) + .map(fn => { + return { + fn, + inheritedFrom: ((deployedContractData as GenericContract)?.inheritedFunctions as InheritedFunctions)?.[fn.name], + }; + }) + .sort((a, b) => (b.inheritedFrom ? b.inheritedFrom.localeCompare(a.inheritedFrom) : 1)); + + if (!functionsToDisplay.length) { + return <>No read methods; + } + + return ( + <> + {functionsToDisplay.map(({ fn, inheritedFrom }) => ( + + ))} + + ); +}; diff --git a/packages/nextjs/components/scaffold-eth/Contract/ContractUI.tsx b/packages/nextjs/components/scaffold-eth/Contract/ContractUI.tsx new file mode 100644 index 0000000..84869de --- /dev/null +++ b/packages/nextjs/components/scaffold-eth/Contract/ContractUI.tsx @@ -0,0 +1,102 @@ +import { useReducer } from "react"; +import { ContractReadMethods } from "./ContractReadMethods"; +import { ContractVariables } from "./ContractVariables"; +import { ContractWriteMethods } from "./ContractWriteMethods"; +import { Spinner } from "~~/components/assets/Spinner"; +import { Address, Balance } from "~~/components/scaffold-eth"; +import { useDeployedContractInfo, useNetworkColor } from "~~/hooks/scaffold-eth"; +import { useTargetNetwork } from "~~/hooks/scaffold-eth/useTargetNetwork"; +import { ContractName } from "~~/utils/scaffold-eth/contract"; + +type ContractUIProps = { + contractName: ContractName; + className?: string; +}; + +/** + * UI component to interface with deployed contracts. + **/ +export const ContractUI = ({ contractName, className = "" }: ContractUIProps) => { + const [refreshDisplayVariables, triggerRefreshDisplayVariables] = useReducer(value => !value, false); + const { targetNetwork } = useTargetNetwork(); + const { data: deployedContractData, isLoading: deployedContractLoading } = useDeployedContractInfo(contractName); + const networkColor = useNetworkColor(); + + if (deployedContractLoading) { + return ( +
    + +
    + ); + } + + if (!deployedContractData) { + return ( +

    + {`No contract found by the name of "${contractName}" on chain "${targetNetwork.name}"!`} +

    + ); + } + + return ( +
    +
    +
    +
    +
    +
    + {contractName} +
    +
    + Balance: + +
    +
    +
    + {targetNetwork && ( +

    + Network:{" "} + {targetNetwork.name} +

    + )} +
    +
    + +
    +
    +
    +
    +
    +
    +
    +

    Read

    +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    +

    Write

    +
    +
    +
    + +
    +
    +
    +
    +
    +
    + ); +}; diff --git a/packages/nextjs/components/scaffold-eth/Contract/ContractVariables.tsx b/packages/nextjs/components/scaffold-eth/Contract/ContractVariables.tsx new file mode 100644 index 0000000..0e625cc --- /dev/null +++ b/packages/nextjs/components/scaffold-eth/Contract/ContractVariables.tsx @@ -0,0 +1,49 @@ +import { DisplayVariable } from "./DisplayVariable"; +import { Abi, AbiFunction } from "abitype"; +import { Contract, ContractName, GenericContract, InheritedFunctions } from "~~/utils/scaffold-eth/contract"; + +export const ContractVariables = ({ + refreshDisplayVariables, + deployedContractData, +}: { + refreshDisplayVariables: boolean; + deployedContractData: Contract; +}) => { + if (!deployedContractData) { + return null; + } + + const functionsToDisplay = ( + (deployedContractData.abi as Abi).filter(part => part.type === "function") as AbiFunction[] + ) + .filter(fn => { + const isQueryableWithNoParams = + (fn.stateMutability === "view" || fn.stateMutability === "pure") && fn.inputs.length === 0; + return isQueryableWithNoParams; + }) + .map(fn => { + return { + fn, + inheritedFrom: ((deployedContractData as GenericContract)?.inheritedFunctions as InheritedFunctions)?.[fn.name], + }; + }) + .sort((a, b) => (b.inheritedFrom ? b.inheritedFrom.localeCompare(a.inheritedFrom) : 1)); + + if (!functionsToDisplay.length) { + return <>No contract variables; + } + + return ( + <> + {functionsToDisplay.map(({ fn, inheritedFrom }) => ( + + ))} + + ); +}; diff --git a/packages/nextjs/components/scaffold-eth/Contract/ContractWriteMethods.tsx b/packages/nextjs/components/scaffold-eth/Contract/ContractWriteMethods.tsx new file mode 100644 index 0000000..e42baf3 --- /dev/null +++ b/packages/nextjs/components/scaffold-eth/Contract/ContractWriteMethods.tsx @@ -0,0 +1,48 @@ +import { WriteOnlyFunctionForm } from "./WriteOnlyFunctionForm"; +import { Abi, AbiFunction } from "abitype"; +import { Contract, ContractName, GenericContract, InheritedFunctions } from "~~/utils/scaffold-eth/contract"; + +export const ContractWriteMethods = ({ + onChange, + deployedContractData, +}: { + onChange: () => void; + deployedContractData: Contract; +}) => { + if (!deployedContractData) { + return null; + } + + const functionsToDisplay = ( + (deployedContractData.abi as Abi).filter(part => part.type === "function") as AbiFunction[] + ) + .filter(fn => { + const isWriteableFunction = fn.stateMutability !== "view" && fn.stateMutability !== "pure"; + return isWriteableFunction; + }) + .map(fn => { + return { + fn, + inheritedFrom: ((deployedContractData as GenericContract)?.inheritedFunctions as InheritedFunctions)?.[fn.name], + }; + }) + .sort((a, b) => (b.inheritedFrom ? b.inheritedFrom.localeCompare(a.inheritedFrom) : 1)); + + if (!functionsToDisplay.length) { + return <>No write methods; + } + + return ( + <> + {functionsToDisplay.map(({ fn, inheritedFrom }, idx) => ( + + ))} + + ); +}; diff --git a/packages/nextjs/components/scaffold-eth/Contract/DisplayVariable.tsx b/packages/nextjs/components/scaffold-eth/Contract/DisplayVariable.tsx new file mode 100644 index 0000000..0604a9d --- /dev/null +++ b/packages/nextjs/components/scaffold-eth/Contract/DisplayVariable.tsx @@ -0,0 +1,69 @@ +import { useEffect } from "react"; +import { InheritanceTooltip } from "./InheritanceTooltip"; +import { Abi, AbiFunction } from "abitype"; +import { Address } from "viem"; +import { useContractRead } from "wagmi"; +import { ArrowPathIcon } from "@heroicons/react/24/outline"; +import { displayTxResult } from "~~/components/scaffold-eth"; +import { useAnimationConfig } from "~~/hooks/scaffold-eth"; +import { notification } from "~~/utils/scaffold-eth"; + +type DisplayVariableProps = { + contractAddress: Address; + abiFunction: AbiFunction; + refreshDisplayVariables: boolean; + inheritedFrom?: string; +}; + +export const DisplayVariable = ({ + contractAddress, + abiFunction, + refreshDisplayVariables, + inheritedFrom, +}: DisplayVariableProps) => { + const { + data: result, + isFetching, + refetch, + } = useContractRead({ + address: contractAddress, + functionName: abiFunction.name, + abi: [abiFunction] as Abi, + onError: error => { + notification.error(error.message); + }, + }); + + const { showAnimation } = useAnimationConfig(result); + + useEffect(() => { + refetch(); + }, [refetch, refreshDisplayVariables]); + + return ( +
    +
    +

    {abiFunction.name}

    + + +
    +
    +
    +
    + {displayTxResult(result)} +
    +
    +
    +
    + ); +}; diff --git a/packages/nextjs/components/scaffold-eth/Contract/InheritanceTooltip.tsx b/packages/nextjs/components/scaffold-eth/Contract/InheritanceTooltip.tsx new file mode 100644 index 0000000..9825520 --- /dev/null +++ b/packages/nextjs/components/scaffold-eth/Contract/InheritanceTooltip.tsx @@ -0,0 +1,14 @@ +import { InformationCircleIcon } from "@heroicons/react/20/solid"; + +export const InheritanceTooltip = ({ inheritedFrom }: { inheritedFrom?: string }) => ( + <> + {inheritedFrom && ( + + + )} + +); diff --git a/packages/nextjs/components/scaffold-eth/Contract/ReadOnlyFunctionForm.tsx b/packages/nextjs/components/scaffold-eth/Contract/ReadOnlyFunctionForm.tsx new file mode 100644 index 0000000..2965b6e --- /dev/null +++ b/packages/nextjs/components/scaffold-eth/Contract/ReadOnlyFunctionForm.tsx @@ -0,0 +1,82 @@ +import { useState } from "react"; +import { InheritanceTooltip } from "./InheritanceTooltip"; +import { Abi, AbiFunction } from "abitype"; +import { Address } from "viem"; +import { useContractRead } from "wagmi"; +import { + ContractInput, + displayTxResult, + getFunctionInputKey, + getInitialFormState, + getParsedContractFunctionArgs, +} from "~~/components/scaffold-eth"; +import { notification } from "~~/utils/scaffold-eth"; + +type ReadOnlyFunctionFormProps = { + contractAddress: Address; + abiFunction: AbiFunction; + inheritedFrom?: string; +}; + +export const ReadOnlyFunctionForm = ({ contractAddress, abiFunction, inheritedFrom }: ReadOnlyFunctionFormProps) => { + const [form, setForm] = useState>(() => getInitialFormState(abiFunction)); + const [result, setResult] = useState(); + + const { isFetching, refetch } = useContractRead({ + address: contractAddress, + functionName: abiFunction.name, + abi: [abiFunction] as Abi, + args: getParsedContractFunctionArgs(form), + enabled: false, + onError: (error: any) => { + notification.error(error.message); + }, + }); + + const inputElements = abiFunction.inputs.map((input, inputIndex) => { + const key = getFunctionInputKey(abiFunction.name, input, inputIndex); + return ( + { + setResult(undefined); + setForm(updatedFormValue); + }} + form={form} + stateObjectKey={key} + paramType={input} + /> + ); + }); + + return ( +
    +

    + {abiFunction.name} + +

    + {inputElements} +
    +
    + {result !== null && result !== undefined && ( +
    +

    Result:

    +
    {displayTxResult(result)}
    +
    + )} +
    + +
    +
    + ); +}; diff --git a/packages/nextjs/components/scaffold-eth/Contract/TxReceipt.tsx b/packages/nextjs/components/scaffold-eth/Contract/TxReceipt.tsx new file mode 100644 index 0000000..6308ed2 --- /dev/null +++ b/packages/nextjs/components/scaffold-eth/Contract/TxReceipt.tsx @@ -0,0 +1,48 @@ +import { useState } from "react"; +import { CopyToClipboard } from "react-copy-to-clipboard"; +import { TransactionReceipt } from "viem"; +import { CheckCircleIcon, DocumentDuplicateIcon } from "@heroicons/react/24/outline"; +import { displayTxResult } from "~~/components/scaffold-eth"; + +export const TxReceipt = ( + txResult: string | number | bigint | Record | TransactionReceipt | undefined, +) => { + const [txResultCopied, setTxResultCopied] = useState(false); + + return ( +
    +
    + {txResultCopied ? ( +
    +
    + +
    + Transaction Receipt +
    +
    +
    {displayTxResult(txResult)}
    +
    +
    +
    + ); +}; diff --git a/packages/nextjs/components/scaffold-eth/Contract/WriteOnlyFunctionForm.tsx b/packages/nextjs/components/scaffold-eth/Contract/WriteOnlyFunctionForm.tsx new file mode 100644 index 0000000..b86ce0f --- /dev/null +++ b/packages/nextjs/components/scaffold-eth/Contract/WriteOnlyFunctionForm.tsx @@ -0,0 +1,134 @@ +import { useEffect, useState } from "react"; +import { InheritanceTooltip } from "./InheritanceTooltip"; +import { Abi, AbiFunction } from "abitype"; +import { Address, TransactionReceipt } from "viem"; +import { useContractWrite, useNetwork, useWaitForTransaction } from "wagmi"; +import { + ContractInput, + IntegerInput, + TxReceipt, + getFunctionInputKey, + getInitialFormState, + getParsedContractFunctionArgs, + getParsedError, +} from "~~/components/scaffold-eth"; +import { useTransactor } from "~~/hooks/scaffold-eth"; +import { useTargetNetwork } from "~~/hooks/scaffold-eth/useTargetNetwork"; +import { notification } from "~~/utils/scaffold-eth"; + +type WriteOnlyFunctionFormProps = { + abiFunction: AbiFunction; + onChange: () => void; + contractAddress: Address; + inheritedFrom?: string; +}; + +export const WriteOnlyFunctionForm = ({ + abiFunction, + onChange, + contractAddress, + inheritedFrom, +}: WriteOnlyFunctionFormProps) => { + const [form, setForm] = useState>(() => getInitialFormState(abiFunction)); + const [txValue, setTxValue] = useState(""); + const { chain } = useNetwork(); + const writeTxn = useTransactor(); + const { targetNetwork } = useTargetNetwork(); + const writeDisabled = !chain || chain?.id !== targetNetwork.id; + + const { + data: result, + isLoading, + writeAsync, + } = useContractWrite({ + address: contractAddress, + functionName: abiFunction.name, + abi: [abiFunction] as Abi, + args: getParsedContractFunctionArgs(form), + }); + + const handleWrite = async () => { + if (writeAsync) { + try { + const makeWriteWithParams = () => writeAsync({ value: BigInt(txValue) }); + await writeTxn(makeWriteWithParams); + onChange(); + } catch (e: any) { + const message = getParsedError(e); + notification.error(message); + } + } + }; + + const [displayedTxResult, setDisplayedTxResult] = useState(); + const { data: txResult } = useWaitForTransaction({ + hash: result?.hash, + }); + useEffect(() => { + setDisplayedTxResult(txResult); + }, [txResult]); + + // TODO use `useMemo` to optimize also update in ReadOnlyFunctionForm + const inputs = abiFunction.inputs.map((input, inputIndex) => { + const key = getFunctionInputKey(abiFunction.name, input, inputIndex); + return ( + { + setDisplayedTxResult(undefined); + setForm(updatedFormValue); + }} + form={form} + stateObjectKey={key} + paramType={input} + /> + ); + }); + const zeroInputs = inputs.length === 0 && abiFunction.stateMutability !== "payable"; + + return ( +
    +
    +

    + {abiFunction.name} + +

    + {inputs} + {abiFunction.stateMutability === "payable" ? ( + { + setDisplayedTxResult(undefined); + setTxValue(updatedTxValue); + }} + placeholder="value (wei)" + /> + ) : null} +
    + {!zeroInputs && ( +
    + {displayedTxResult ? : null} +
    + )} +
    + +
    +
    +
    + {zeroInputs && txResult ? ( +
    + +
    + ) : null} +
    + ); +}; diff --git a/packages/nextjs/components/scaffold-eth/Contract/index.tsx b/packages/nextjs/components/scaffold-eth/Contract/index.tsx new file mode 100644 index 0000000..83833d8 --- /dev/null +++ b/packages/nextjs/components/scaffold-eth/Contract/index.tsx @@ -0,0 +1,8 @@ +export * from "./ContractInput"; +export * from "./ContractUI"; +export * from "./DisplayVariable"; +export * from "./ReadOnlyFunctionForm"; +export * from "./TxReceipt"; +export * from "./utilsContract"; +export * from "./utilsDisplay"; +export * from "./WriteOnlyFunctionForm"; diff --git a/packages/nextjs/components/scaffold-eth/Contract/utilsContract.tsx b/packages/nextjs/components/scaffold-eth/Contract/utilsContract.tsx new file mode 100644 index 0000000..e31941e --- /dev/null +++ b/packages/nextjs/components/scaffold-eth/Contract/utilsContract.tsx @@ -0,0 +1,82 @@ +import { AbiFunction, AbiParameter } from "abitype"; +import { BaseError as BaseViemError } from "viem"; + +/** + * Generates a key based on function metadata + * @param {string} functionName + * @param {AbiParameter} input - object containing function name and input type corresponding to index + * @param {number} inputIndex + * @returns {string} key + */ +const getFunctionInputKey = (functionName: string, input: AbiParameter, inputIndex: number): string => { + const name = input?.name || `input_${inputIndex}_`; + return functionName + "_" + name + "_" + input.internalType + "_" + input.type; +}; + +/** + * Parses an error to get a displayable string + * @param e - error object + * @returns {string} parsed error string + */ +const getParsedError = (e: any | BaseViemError): string => { + let message = e.message ?? "An unknown error occurred"; + + if (e instanceof BaseViemError) { + if (e.details) { + message = e.details; + } else if (e.shortMessage) { + message = e.shortMessage; + } else if (e.message) { + message = e.message; + } else if (e.name) { + message = e.name; + } + } + + return message; +}; + +// This regex is used to identify array types in the form of `type[size]` +const ARRAY_TYPE_REGEX = /\[.*\]$/; + +/** + * Parses form input with array support + * @param {Record} form - form object containing key value pairs + * @returns parsed error string + */ +const getParsedContractFunctionArgs = (form: Record) => { + const keys = Object.keys(form); + const parsedArguments = keys.map(key => { + try { + const keySplitArray = key.split("_"); + const baseTypeOfArg = keySplitArray[keySplitArray.length - 1]; + let valueOfArg = form[key]; + + if (ARRAY_TYPE_REGEX.test(baseTypeOfArg) || baseTypeOfArg === "tuple") { + valueOfArg = JSON.parse(valueOfArg); + } else if (baseTypeOfArg === "bool") { + if (["true", "1", "0x1", "0x01", "0x0001"].includes(valueOfArg)) { + valueOfArg = 1; + } else { + valueOfArg = 0; + } + } + return valueOfArg; + } catch (error: any) { + // ignore error, it will be handled when sending/reading from a function + } + }); + return parsedArguments; +}; + +const getInitialFormState = (abiFunction: AbiFunction) => { + const initialForm: Record = {}; + if (!abiFunction.inputs) return initialForm; + abiFunction.inputs.forEach((input, inputIndex) => { + const key = getFunctionInputKey(abiFunction.name, input, inputIndex); + initialForm[key] = ""; + }); + return initialForm; +}; + +export { getFunctionInputKey, getInitialFormState, getParsedContractFunctionArgs, getParsedError }; diff --git a/packages/nextjs/components/scaffold-eth/Contract/utilsDisplay.tsx b/packages/nextjs/components/scaffold-eth/Contract/utilsDisplay.tsx new file mode 100644 index 0000000..5fcf310 --- /dev/null +++ b/packages/nextjs/components/scaffold-eth/Contract/utilsDisplay.tsx @@ -0,0 +1,56 @@ +import { ReactElement } from "react"; +import { TransactionBase, TransactionReceipt, formatEther } from "viem"; +import { Address } from "~~/components/scaffold-eth"; +import { replacer } from "~~/utils/scaffold-eth/common"; + +type DisplayContent = + | string + | number + | bigint + | Record + | TransactionBase + | TransactionReceipt + | undefined + | unknown; + +export const displayTxResult = ( + displayContent: DisplayContent | DisplayContent[], + asText = false, +): string | ReactElement | number => { + if (displayContent == null) { + return ""; + } + + if (typeof displayContent === "bigint") { + try { + const asNumber = Number(displayContent); + if (asNumber <= Number.MAX_SAFE_INTEGER && asNumber >= Number.MIN_SAFE_INTEGER) { + return asNumber; + } else { + return "Ξ" + formatEther(displayContent); + } + } catch (e) { + return "Ξ" + formatEther(displayContent); + } + } + + if (typeof displayContent === "string" && displayContent.indexOf("0x") === 0 && displayContent.length === 42) { + return asText ? displayContent :
    ; + } + + if (Array.isArray(displayContent)) { + const mostReadable = (v: DisplayContent) => + ["number", "boolean"].includes(typeof v) ? v : displayTxResultAsText(v); + const displayable = JSON.stringify(displayContent.map(mostReadable), replacer); + + return asText ? ( + displayable + ) : ( + {displayable.replaceAll(",", ",\n")} + ); + } + + return JSON.stringify(displayContent, replacer, 2); +}; + +const displayTxResultAsText = (displayContent: DisplayContent) => displayTxResult(displayContent, true); diff --git a/packages/nextjs/components/scaffold-eth/Faucet.tsx b/packages/nextjs/components/scaffold-eth/Faucet.tsx new file mode 100644 index 0000000..a21c9fa --- /dev/null +++ b/packages/nextjs/components/scaffold-eth/Faucet.tsx @@ -0,0 +1,130 @@ +import { useEffect, useState } from "react"; +import { Address as AddressType, createWalletClient, http, parseEther } from "viem"; +import { hardhat } from "viem/chains"; +import { useNetwork } from "wagmi"; +import { BanknotesIcon } from "@heroicons/react/24/outline"; +import { Address, AddressInput, Balance, EtherInput, getParsedError } from "~~/components/scaffold-eth"; +import { useTransactor } from "~~/hooks/scaffold-eth"; +import { notification } from "~~/utils/scaffold-eth"; + +// Account index to use from generated hardhat accounts. +const FAUCET_ACCOUNT_INDEX = 0; + +const localWalletClient = createWalletClient({ + chain: hardhat, + transport: http(), +}); + +/** + * Faucet modal which lets you send ETH to any address. + */ +export const Faucet = () => { + const [loading, setLoading] = useState(false); + const [inputAddress, setInputAddress] = useState(); + const [faucetAddress, setFaucetAddress] = useState(); + const [sendValue, setSendValue] = useState(""); + + const { chain: ConnectedChain } = useNetwork(); + + const faucetTxn = useTransactor(localWalletClient); + + useEffect(() => { + const getFaucetAddress = async () => { + try { + const accounts = await localWalletClient.getAddresses(); + setFaucetAddress(accounts[FAUCET_ACCOUNT_INDEX]); + } catch (error) { + notification.error( + <> +

    Cannot connect to local provider

    +

    + - Did you forget to run yarn chain ? +

    +

    + - Or you can change targetNetwork in{" "} + scaffold.config.ts +

    + , + ); + console.error("⚡️ ~ file: Faucet.tsx:getFaucetAddress ~ error", error); + } + }; + getFaucetAddress(); + }, []); + + const sendETH = async () => { + if (!faucetAddress) { + return; + } + try { + setLoading(true); + await faucetTxn({ + to: inputAddress, + value: parseEther(sendValue as `${number}`), + account: faucetAddress, + chain: hardhat, + }); + setLoading(false); + setInputAddress(undefined); + setSendValue(""); + } catch (error) { + const parsedError = getParsedError(error); + console.error("⚡️ ~ file: Faucet.tsx:sendETH ~ error", error); + notification.error(parsedError); + setLoading(false); + } + }; + + // Render only on local chain + if (ConnectedChain?.id !== hardhat.id) { + return null; + } + + return ( +
    + + + +
    + ); +}; diff --git a/packages/nextjs/components/scaffold-eth/FaucetBuildBear.tsx b/packages/nextjs/components/scaffold-eth/FaucetBuildBear.tsx new file mode 100644 index 0000000..3cb2e4f --- /dev/null +++ b/packages/nextjs/components/scaffold-eth/FaucetBuildBear.tsx @@ -0,0 +1,174 @@ +import { useState } from "react"; +import { bbSupportedERC20Tokens } from "../../../buildbear/constants.mjs"; +import buildBearSandbox from "../../sandbox.json"; +import axios from "axios"; +import { Address as AddressType } from "viem"; +import { useNetwork } from "wagmi"; +import { BanknotesIcon } from "@heroicons/react/24/outline"; +import { AddressInput, EtherInput, getParsedError } from "~~/components/scaffold-eth"; +import { notification } from "~~/utils/scaffold-eth"; + +// Account index to use from generated hardhat accounts. + +/** + * Faucet modal which lets you send ETH to any address. + */ +export const FaucetBuildBear = () => { + const [loading, setLoading] = useState(false); + const [inputAddress, setInputAddress] = useState(); + const [erc20inputAddress, setErc20InputAddress] = useState(); + const [sendValue, setSendValue] = useState(""); + const [sendErc20Value, setSendErc20Value] = useState(""); + const [erc20TokenAddress, setErc20TokenAddress] = useState(null); + + const updateTokenAddress = (newValue: any) => { + setErc20TokenAddress(newValue.target.value); + console.log(newValue.target.value); + }; + const erc20Tokens = buildBearSandbox ? bbSupportedERC20Tokens[buildBearSandbox.forkingChainId] : {}; + const erc20Options = Object.keys(erc20Tokens).map(tokenSymbol => ({ + value: erc20Tokens[tokenSymbol]?.address, + label: tokenSymbol, + })); + + useNetwork(); + + const nativefaucet = async () => { + try { + const data = { + jsonrpc: "2.0", + id: 1, + method: "buildbear_nativeFaucet", + params: [ + { + address: inputAddress, + balance: sendValue, + }, + ], + }; + const config = { + method: "post", + url: buildBearSandbox.rpcUrl, + data, + }; + await axios(config); + notification.success("Minted Native Tokens Successfully"); + } catch (error) { + const parsedError = getParsedError(error); + console.error("⚡️ ~ file: Faucet.tsx:sendETH ~ error", error); + notification.error(parsedError); + setLoading(false); + } + }; + const erc20faucet = async () => { + try { + const data = { + jsonrpc: "2.0", + id: 1, + method: "buildbear_ERC20Faucet", + params: [ + { + address: erc20inputAddress, + balance: sendErc20Value, + token: erc20TokenAddress, + }, + ], + }; + const config = { + method: "post", + url: buildBearSandbox.rpcUrl, + data, + }; + await axios(config); + notification.success("Minted ERC20 Tokens Successfully"); + } catch (error) { + const parsedError = getParsedError(error); + console.error("⚡️ ~ file: Faucet.tsx:sendETH ~ error", error); + notification.error(parsedError); + setLoading(false); + } + }; + + return ( +
    + + + +
    + ); +}; diff --git a/packages/nextjs/components/scaffold-eth/FaucetButton.tsx b/packages/nextjs/components/scaffold-eth/FaucetButton.tsx new file mode 100644 index 0000000..b9538bb --- /dev/null +++ b/packages/nextjs/components/scaffold-eth/FaucetButton.tsx @@ -0,0 +1,69 @@ +import { useState } from "react"; +import { createWalletClient, http, parseEther } from "viem"; +import { hardhat } from "viem/chains"; +import { useAccount, useNetwork } from "wagmi"; +import { BanknotesIcon } from "@heroicons/react/24/outline"; +import { useAccountBalance, useTransactor } from "~~/hooks/scaffold-eth"; + +// Number of ETH faucet sends to an address +const NUM_OF_ETH = "1"; +const FAUCET_ADDRESS = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"; + +const localWalletClient = createWalletClient({ + chain: hardhat, + transport: http(), +}); + +/** + * FaucetButton button which lets you grab eth. + */ +export const FaucetButton = () => { + const { address } = useAccount(); + const { balance } = useAccountBalance(address); + + const { chain: ConnectedChain } = useNetwork(); + + const [loading, setLoading] = useState(false); + + const faucetTxn = useTransactor(localWalletClient); + + const sendETH = async () => { + try { + setLoading(true); + await faucetTxn({ + chain: hardhat, + account: FAUCET_ADDRESS, + to: address, + value: parseEther(NUM_OF_ETH), + }); + setLoading(false); + } catch (error) { + console.error("⚡️ ~ file: FaucetButton.tsx:sendETH ~ error", error); + setLoading(false); + } + }; + + // Render only on local chain + if (ConnectedChain?.id !== hardhat.id) { + return null; + } + + return ( +
    + +
    + ); +}; diff --git a/packages/nextjs/components/scaffold-eth/Input/AddressInput.tsx b/packages/nextjs/components/scaffold-eth/Input/AddressInput.tsx new file mode 100644 index 0000000..7bfcd16 --- /dev/null +++ b/packages/nextjs/components/scaffold-eth/Input/AddressInput.tsx @@ -0,0 +1,90 @@ +import { useCallback, useEffect, useState } from "react"; +import { blo } from "blo"; +import { useDebounce } from "usehooks-ts"; +import { Address, isAddress } from "viem"; +import { useEnsAddress, useEnsAvatar, useEnsName } from "wagmi"; +import { CommonInputProps, InputBase, isENS } from "~~/components/scaffold-eth"; + +/** + * Address input with ENS name resolution + */ +export const AddressInput = ({ value, name, placeholder, onChange, disabled }: CommonInputProps
    ) => { + // Debounce the input to keep clean RPC calls when resolving ENS names + // If the input is an address, we don't need to debounce it + const _debouncedValue = useDebounce(value, 500); + const debouncedValue = isAddress(value) ? value : _debouncedValue; + const isDebouncedValueLive = debouncedValue === value; + + // If the user changes the input after an ENS name is already resolved, we want to remove the stale result + const settledValue = isDebouncedValueLive ? debouncedValue : undefined; + + const { data: ensAddress, isLoading: isEnsAddressLoading } = useEnsAddress({ + name: settledValue, + enabled: isENS(debouncedValue), + chainId: 1, + cacheTime: 30_000, + }); + + const [enteredEnsName, setEnteredEnsName] = useState(); + const { data: ensName, isLoading: isEnsNameLoading } = useEnsName({ + address: settledValue, + enabled: isAddress(debouncedValue), + chainId: 1, + cacheTime: 30_000, + }); + + const { data: ensAvatar } = useEnsAvatar({ + name: ensName, + enabled: Boolean(ensName), + chainId: 1, + cacheTime: 30_000, + }); + + // ens => address + useEffect(() => { + if (!ensAddress) return; + + // ENS resolved successfully + setEnteredEnsName(debouncedValue); + onChange(ensAddress); + }, [ensAddress, onChange, debouncedValue]); + + const handleChange = useCallback( + (newValue: Address) => { + setEnteredEnsName(undefined); + onChange(newValue); + }, + [onChange], + ); + + return ( + + name={name} + placeholder={placeholder} + error={ensAddress === null} + value={value} + onChange={handleChange} + disabled={isEnsAddressLoading || isEnsNameLoading || disabled} + prefix={ + ensName && ( +
    + {ensAvatar ? ( + + { + // eslint-disable-next-line + {`${ensAddress} + } + + ) : null} + {enteredEnsName ?? ensName} +
    + ) + } + suffix={ + // Don't want to use nextJS Image here (and adding remote patterns for the URL) + // eslint-disable-next-line @next/next/no-img-element + value && + } + /> + ); +}; diff --git a/packages/nextjs/components/scaffold-eth/Input/Bytes32Input.tsx b/packages/nextjs/components/scaffold-eth/Input/Bytes32Input.tsx new file mode 100644 index 0000000..f8098be --- /dev/null +++ b/packages/nextjs/components/scaffold-eth/Input/Bytes32Input.tsx @@ -0,0 +1,30 @@ +import { useCallback } from "react"; +import { hexToString, isHex, stringToHex } from "viem"; +import { CommonInputProps, InputBase } from "~~/components/scaffold-eth"; + +export const Bytes32Input = ({ value, onChange, name, placeholder, disabled }: CommonInputProps) => { + const convertStringToBytes32 = useCallback(() => { + if (!value) { + return; + } + onChange(isHex(value) ? hexToString(value, { size: 32 }) : stringToHex(value, { size: 32 })); + }, [onChange, value]); + + return ( + + # + + } + /> + ); +}; diff --git a/packages/nextjs/components/scaffold-eth/Input/BytesInput.tsx b/packages/nextjs/components/scaffold-eth/Input/BytesInput.tsx new file mode 100644 index 0000000..695e650 --- /dev/null +++ b/packages/nextjs/components/scaffold-eth/Input/BytesInput.tsx @@ -0,0 +1,27 @@ +import { useCallback } from "react"; +import { bytesToString, isHex, toBytes, toHex } from "viem"; +import { CommonInputProps, InputBase } from "~~/components/scaffold-eth"; + +export const BytesInput = ({ value, onChange, name, placeholder, disabled }: CommonInputProps) => { + const convertStringToBytes = useCallback(() => { + onChange(isHex(value) ? bytesToString(toBytes(value)) : toHex(toBytes(value))); + }, [onChange, value]); + + return ( + + # + + } + /> + ); +}; diff --git a/packages/nextjs/components/scaffold-eth/Input/EtherInput.tsx b/packages/nextjs/components/scaffold-eth/Input/EtherInput.tsx new file mode 100644 index 0000000..9ac76ec --- /dev/null +++ b/packages/nextjs/components/scaffold-eth/Input/EtherInput.tsx @@ -0,0 +1,112 @@ +import { useMemo, useState } from "react"; +import { ArrowsRightLeftIcon } from "@heroicons/react/24/outline"; +import { CommonInputProps, InputBase, SIGNED_NUMBER_REGEX } from "~~/components/scaffold-eth"; +import { useGlobalState } from "~~/services/store/store"; + +const MAX_DECIMALS_USD = 2; + +function etherValueToDisplayValue(usdMode: boolean, etherValue: string, nativeCurrencyPrice: number) { + if (usdMode && nativeCurrencyPrice) { + const parsedEthValue = parseFloat(etherValue); + if (Number.isNaN(parsedEthValue)) { + return etherValue; + } else { + // We need to round the value rather than use toFixed, + // since otherwise a user would not be able to modify the decimal value + return ( + Math.round(parsedEthValue * nativeCurrencyPrice * 10 ** MAX_DECIMALS_USD) / + 10 ** MAX_DECIMALS_USD + ).toString(); + } + } else { + return etherValue; + } +} + +function displayValueToEtherValue(usdMode: boolean, displayValue: string, nativeCurrencyPrice: number) { + if (usdMode && nativeCurrencyPrice) { + const parsedDisplayValue = parseFloat(displayValue); + if (Number.isNaN(parsedDisplayValue)) { + // Invalid number. + return displayValue; + } else { + // Compute the ETH value if a valid number. + return (parsedDisplayValue / nativeCurrencyPrice).toString(); + } + } else { + return displayValue; + } +} + +/** + * Input for ETH amount with USD conversion. + * + * onChange will always be called with the value in ETH + */ +export const EtherInput = ({ value, name, placeholder, onChange, disabled }: CommonInputProps) => { + const [transitoryDisplayValue, setTransitoryDisplayValue] = useState(); + const nativeCurrencyPrice = useGlobalState(state => state.nativeCurrencyPrice); + const [usdMode, setUSDMode] = useState(false); + + // The displayValue is derived from the ether value that is controlled outside of the component + // In usdMode, it is converted to its usd value, in regular mode it is unaltered + const displayValue = useMemo(() => { + const newDisplayValue = etherValueToDisplayValue(usdMode, value, nativeCurrencyPrice); + if (transitoryDisplayValue && parseFloat(newDisplayValue) === parseFloat(transitoryDisplayValue)) { + return transitoryDisplayValue; + } + // Clear any transitory display values that might be set + setTransitoryDisplayValue(undefined); + return newDisplayValue; + }, [nativeCurrencyPrice, transitoryDisplayValue, usdMode, value]); + + const handleChangeNumber = (newValue: string) => { + if (newValue && !SIGNED_NUMBER_REGEX.test(newValue)) { + return; + } + + // Following condition is a fix to prevent usdMode from experiencing different display values + // than what the user entered. This can happen due to floating point rounding errors that are introduced in the back and forth conversion + if (usdMode) { + const decimals = newValue.split(".")[1]; + if (decimals && decimals.length > MAX_DECIMALS_USD) { + return; + } + } + + // Since the display value is a derived state (calculated from the ether value), usdMode would not allow introducing a decimal point. + // This condition handles a transitory state for a display value with a trailing decimal sign + if (newValue.endsWith(".") || newValue.endsWith(".0")) { + setTransitoryDisplayValue(newValue); + } else { + setTransitoryDisplayValue(undefined); + } + + const newEthValue = displayValueToEtherValue(usdMode, newValue, nativeCurrencyPrice); + onChange(newEthValue); + }; + + const toggleMode = () => { + setUSDMode(!usdMode); + }; + + return ( + {usdMode ? "$" : "Ξ"}} + suffix={ + + } + /> + ); +}; diff --git a/packages/nextjs/components/scaffold-eth/Input/InputBase.tsx b/packages/nextjs/components/scaffold-eth/Input/InputBase.tsx new file mode 100644 index 0000000..62be760 --- /dev/null +++ b/packages/nextjs/components/scaffold-eth/Input/InputBase.tsx @@ -0,0 +1,49 @@ +import { ChangeEvent, ReactNode, useCallback } from "react"; +import { CommonInputProps } from "~~/components/scaffold-eth"; + +type InputBaseProps = CommonInputProps & { + error?: boolean; + prefix?: ReactNode; + suffix?: ReactNode; +}; + +export const InputBase = string } | undefined = string>({ + name, + value, + onChange, + placeholder, + error, + disabled, + prefix, + suffix, +}: InputBaseProps) => { + let modifier = ""; + if (error) { + modifier = "border-error"; + } else if (disabled) { + modifier = "border-disabled bg-base-300"; + } + + const handleChange = useCallback( + (e: ChangeEvent) => { + onChange(e.target.value as unknown as T); + }, + [onChange], + ); + + return ( +
    + {prefix} + + {suffix} +
    + ); +}; diff --git a/packages/nextjs/components/scaffold-eth/Input/IntegerInput.tsx b/packages/nextjs/components/scaffold-eth/Input/IntegerInput.tsx new file mode 100644 index 0000000..f9b4361 --- /dev/null +++ b/packages/nextjs/components/scaffold-eth/Input/IntegerInput.tsx @@ -0,0 +1,64 @@ +import { useCallback, useEffect, useState } from "react"; +import { CommonInputProps, InputBase, IntegerVariant, isValidInteger } from "~~/components/scaffold-eth"; + +type IntegerInputProps = CommonInputProps & { + variant?: IntegerVariant; + disableMultiplyBy1e18?: boolean; +}; + +export const IntegerInput = ({ + value, + onChange, + name, + placeholder, + disabled, + variant = IntegerVariant.UINT256, + disableMultiplyBy1e18 = false, +}: IntegerInputProps) => { + const [inputError, setInputError] = useState(false); + const multiplyBy1e18 = useCallback(() => { + if (!value) { + return; + } + if (typeof value === "bigint") { + return onChange(value * 10n ** 18n); + } + return onChange(BigInt(Math.round(Number(value) * 10 ** 18))); + }, [onChange, value]); + + useEffect(() => { + if (isValidInteger(variant, value, false)) { + setInputError(false); + } else { + setInputError(true); + } + }, [value, variant]); + + return ( + + + + ) + } + /> + ); +}; diff --git a/packages/nextjs/components/scaffold-eth/Input/index.ts b/packages/nextjs/components/scaffold-eth/Input/index.ts new file mode 100644 index 0000000..d48a8b3 --- /dev/null +++ b/packages/nextjs/components/scaffold-eth/Input/index.ts @@ -0,0 +1,7 @@ +export * from "./AddressInput"; +export * from "./Bytes32Input"; +export * from "./BytesInput"; +export * from "./EtherInput"; +export * from "./InputBase"; +export * from "./IntegerInput"; +export * from "./utils"; diff --git a/packages/nextjs/components/scaffold-eth/Input/utils.ts b/packages/nextjs/components/scaffold-eth/Input/utils.ts new file mode 100644 index 0000000..481b5d1 --- /dev/null +++ b/packages/nextjs/components/scaffold-eth/Input/utils.ts @@ -0,0 +1,111 @@ +export type CommonInputProps = { + value: T; + onChange: (newValue: T) => void; + name?: string; + placeholder?: string; + disabled?: boolean; +}; + +export enum IntegerVariant { + UINT8 = "uint8", + UINT16 = "uint16", + UINT24 = "uint24", + UINT32 = "uint32", + UINT40 = "uint40", + UINT48 = "uint48", + UINT56 = "uint56", + UINT64 = "uint64", + UINT72 = "uint72", + UINT80 = "uint80", + UINT88 = "uint88", + UINT96 = "uint96", + UINT104 = "uint104", + UINT112 = "uint112", + UINT120 = "uint120", + UINT128 = "uint128", + UINT136 = "uint136", + UINT144 = "uint144", + UINT152 = "uint152", + UINT160 = "uint160", + UINT168 = "uint168", + UINT176 = "uint176", + UINT184 = "uint184", + UINT192 = "uint192", + UINT200 = "uint200", + UINT208 = "uint208", + UINT216 = "uint216", + UINT224 = "uint224", + UINT232 = "uint232", + UINT240 = "uint240", + UINT248 = "uint248", + UINT256 = "uint256", + INT8 = "int8", + INT16 = "int16", + INT24 = "int24", + INT32 = "int32", + INT40 = "int40", + INT48 = "int48", + INT56 = "int56", + INT64 = "int64", + INT72 = "int72", + INT80 = "int80", + INT88 = "int88", + INT96 = "int96", + INT104 = "int104", + INT112 = "int112", + INT120 = "int120", + INT128 = "int128", + INT136 = "int136", + INT144 = "int144", + INT152 = "int152", + INT160 = "int160", + INT168 = "int168", + INT176 = "int176", + INT184 = "int184", + INT192 = "int192", + INT200 = "int200", + INT208 = "int208", + INT216 = "int216", + INT224 = "int224", + INT232 = "int232", + INT240 = "int240", + INT248 = "int248", + INT256 = "int256", +} + +export const SIGNED_NUMBER_REGEX = /^-?\d+\.?\d*$/; +export const UNSIGNED_NUMBER_REGEX = /^\.?\d+\.?\d*$/; + +export const isValidInteger = (dataType: IntegerVariant, value: bigint | string, strict = true) => { + const isSigned = dataType.startsWith("i"); + const bitcount = Number(dataType.substring(isSigned ? 3 : 4)); + + let valueAsBigInt; + try { + valueAsBigInt = BigInt(value); + } catch (e) {} + if (typeof valueAsBigInt !== "bigint") { + if (strict) { + return false; + } + if (!value || typeof value !== "string") { + return true; + } + return isSigned ? SIGNED_NUMBER_REGEX.test(value) || value === "-" : UNSIGNED_NUMBER_REGEX.test(value); + } else if (!isSigned && valueAsBigInt < 0) { + return false; + } + const hexString = valueAsBigInt.toString(16); + const significantHexDigits = hexString.match(/.*x0*(.*)$/)?.[1] ?? ""; + if ( + significantHexDigits.length * 4 > bitcount || + (isSigned && significantHexDigits.length * 4 === bitcount && parseInt(significantHexDigits.slice(-1)?.[0], 16) < 8) + ) { + return false; + } + return true; +}; + +// Treat any dot-separated string as a potential ENS name +const ensRegex = /.+\..+/; +export const isENS = (address = "") => ensRegex.test(address); diff --git a/packages/nextjs/components/scaffold-eth/RainbowKitCustomConnectButton/AddressInfoDropdown.tsx b/packages/nextjs/components/scaffold-eth/RainbowKitCustomConnectButton/AddressInfoDropdown.tsx new file mode 100644 index 0000000..101c60b --- /dev/null +++ b/packages/nextjs/components/scaffold-eth/RainbowKitCustomConnectButton/AddressInfoDropdown.tsx @@ -0,0 +1,132 @@ +import { useRef, useState } from "react"; +import { NetworkOptions } from "./NetworkOptions"; +import CopyToClipboard from "react-copy-to-clipboard"; +import { useDisconnect } from "wagmi"; +import { + ArrowLeftOnRectangleIcon, + ArrowTopRightOnSquareIcon, + ArrowsRightLeftIcon, + CheckCircleIcon, + ChevronDownIcon, + DocumentDuplicateIcon, + QrCodeIcon, +} from "@heroicons/react/24/outline"; +import { BlockieAvatar } from "~~/components/scaffold-eth"; +import { useOutsideClick } from "~~/hooks/scaffold-eth"; +import { getTargetNetworks } from "~~/utils/scaffold-eth"; + +const allowedNetworks = getTargetNetworks(); + +type AddressInfoDropdownProps = { + address: string; + blockExplorerAddressLink: string | undefined; + displayName: string; + ensAvatar?: string; +}; + +export const AddressInfoDropdown = ({ + address, + ensAvatar, + displayName, + blockExplorerAddressLink, +}: AddressInfoDropdownProps) => { + const { disconnect } = useDisconnect(); + + const [addressCopied, setAddressCopied] = useState(false); + + const [selectingNetwork, setSelectingNetwork] = useState(false); + const dropdownRef = useRef(null); + const closeDropdown = () => { + setSelectingNetwork(false); + dropdownRef.current?.removeAttribute("open"); + }; + useOutsideClick(dropdownRef, closeDropdown); + + return ( + <> +
    + + + {displayName} + + +
      +
    +
    + + ); +}; diff --git a/packages/nextjs/components/scaffold-eth/RainbowKitCustomConnectButton/AddressQRCodeModal.tsx b/packages/nextjs/components/scaffold-eth/RainbowKitCustomConnectButton/AddressQRCodeModal.tsx new file mode 100644 index 0000000..c6e00b7 --- /dev/null +++ b/packages/nextjs/components/scaffold-eth/RainbowKitCustomConnectButton/AddressQRCodeModal.tsx @@ -0,0 +1,32 @@ +import { QRCodeSVG } from "qrcode.react"; +import { Address } from "~~/components/scaffold-eth"; + +interface IAddressQRCodeModal { + address: string; + modalId: string; +} + +export const AddressQRCodeModal: React.FC = ({ address, modalId }) => { + return ( + <> +
    + + +
    + + ); +}; diff --git a/packages/nextjs/components/scaffold-eth/RainbowKitCustomConnectButton/NetworkOptions.tsx b/packages/nextjs/components/scaffold-eth/RainbowKitCustomConnectButton/NetworkOptions.tsx new file mode 100644 index 0000000..bd0e1bc --- /dev/null +++ b/packages/nextjs/components/scaffold-eth/RainbowKitCustomConnectButton/NetworkOptions.tsx @@ -0,0 +1,47 @@ +import { useDarkMode } from "usehooks-ts"; +import { useNetwork, useSwitchNetwork } from "wagmi"; +import { ArrowsRightLeftIcon } from "@heroicons/react/24/solid"; +import { getNetworkColor } from "~~/hooks/scaffold-eth"; +import { getTargetNetworks } from "~~/utils/scaffold-eth"; + +const allowedNetworks = getTargetNetworks(); + +type NetworkOptionsProps = { + hidden?: boolean; +}; + +export const NetworkOptions = ({ hidden = false }: NetworkOptionsProps) => { + const { isDarkMode } = useDarkMode(); + const { switchNetwork } = useSwitchNetwork(); + const { chain } = useNetwork(); + + return ( + <> + {allowedNetworks + .filter(allowedNetwork => allowedNetwork.id !== chain?.id) + .map(allowedNetwork => ( +
  • + +
  • + ))} + + ); +}; diff --git a/packages/nextjs/components/scaffold-eth/RainbowKitCustomConnectButton/WrongNetworkDropdown.tsx b/packages/nextjs/components/scaffold-eth/RainbowKitCustomConnectButton/WrongNetworkDropdown.tsx new file mode 100644 index 0000000..f9f0fd8 --- /dev/null +++ b/packages/nextjs/components/scaffold-eth/RainbowKitCustomConnectButton/WrongNetworkDropdown.tsx @@ -0,0 +1,32 @@ +import { NetworkOptions } from "./NetworkOptions"; +import { useDisconnect } from "wagmi"; +import { ArrowLeftOnRectangleIcon, ChevronDownIcon } from "@heroicons/react/24/outline"; + +export const WrongNetworkDropdown = () => { + const { disconnect } = useDisconnect(); + + return ( +
    + +
      + +
    • + +
    • +
    +
    + ); +}; diff --git a/packages/nextjs/components/scaffold-eth/RainbowKitCustomConnectButton/index.tsx b/packages/nextjs/components/scaffold-eth/RainbowKitCustomConnectButton/index.tsx new file mode 100644 index 0000000..02efcc8 --- /dev/null +++ b/packages/nextjs/components/scaffold-eth/RainbowKitCustomConnectButton/index.tsx @@ -0,0 +1,64 @@ +import { Balance } from "../Balance"; +import { AddressInfoDropdown } from "./AddressInfoDropdown"; +import { AddressQRCodeModal } from "./AddressQRCodeModal"; +import { WrongNetworkDropdown } from "./WrongNetworkDropdown"; +import { ConnectButton } from "@rainbow-me/rainbowkit"; +import { useAutoConnect, useNetworkColor } from "~~/hooks/scaffold-eth"; +import { useTargetNetwork } from "~~/hooks/scaffold-eth/useTargetNetwork"; +import { getBlockExplorerAddressLink } from "~~/utils/scaffold-eth"; + +/** + * Custom Wagmi Connect Button (watch balance + custom design) + */ +export const RainbowKitCustomConnectButton = () => { + useAutoConnect(); + const networkColor = useNetworkColor(); + const { targetNetwork } = useTargetNetwork(); + + return ( + + {({ account, chain, openConnectModal, mounted }) => { + const connected = mounted && account && chain; + const blockExplorerAddressLink = account + ? getBlockExplorerAddressLink(targetNetwork, account.address) + : undefined; + + return ( + <> + {(() => { + if (!connected) { + return ( + + ); + } + + if (chain.unsupported || chain.id !== targetNetwork.id) { + return ; + } + + return ( + <> +
    + + + {chain.name} + +
    + + + + ); + })()} + + ); + }} +
    + ); +}; diff --git a/packages/nextjs/components/scaffold-eth/index.tsx b/packages/nextjs/components/scaffold-eth/index.tsx new file mode 100644 index 0000000..05faf73 --- /dev/null +++ b/packages/nextjs/components/scaffold-eth/index.tsx @@ -0,0 +1,9 @@ +export * from "./Address"; +export * from "./Balance"; +export * from "./BlockieAvatar"; +export * from "./Contract"; +export * from "./Faucet"; +export * from "./FaucetBuildBear"; +export * from "./FaucetButton"; +export * from "./Input"; +export * from "./RainbowKitCustomConnectButton"; diff --git a/packages/nextjs/contracts/deployedContracts.ts b/packages/nextjs/contracts/deployedContracts.ts new file mode 100644 index 0000000..16e5025 --- /dev/null +++ b/packages/nextjs/contracts/deployedContracts.ts @@ -0,0 +1,654 @@ +/** + * This file is autogenerated by Scaffold-ETH. + * You should not edit it manually or your changes might be overwritten. + */ +import { GenericContractsDeclaration } from "~~/utils/scaffold-eth/contract"; + +const deployedContracts = { + 14499: { + Aave: { + address: "0x883a1E3784f7d91d023632b336b846962394285A", + abi: [ + { + inputs: [ + { + internalType: "address", + name: "asset", + type: "address", + }, + { + internalType: "uint256", + name: "amount", + type: "uint256", + }, + { + internalType: "uint256", + name: "interestRateMode", + type: "uint256", + }, + { + internalType: "uint16", + name: "referralCode", + type: "uint16", + }, + { + internalType: "address", + name: "onBehalfOf", + type: "address", + }, + ], + name: "borrow", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "pool", + outputs: [ + { + internalType: "contract IPool", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "asset", + type: "address", + }, + { + internalType: "uint256", + name: "amount", + type: "uint256", + }, + { + internalType: "uint256", + name: "interestRateMode", + type: "uint256", + }, + ], + name: "repay", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "token", + type: "address", + }, + { + internalType: "uint256", + name: "amount", + type: "uint256", + }, + ], + name: "supply", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "asset", + type: "address", + }, + { + internalType: "uint256", + name: "amount", + type: "uint256", + }, + { + internalType: "address", + name: "to", + type: "address", + }, + ], + name: "withdraw", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "nonpayable", + type: "function", + }, + ], + inheritedFunctions: {}, + }, + Attack: { + address: "0x95698C72492Ab4a06d926d9E947142665A7237E9", + abi: [ + { + inputs: [ + { + internalType: "contract Bank", + name: "_bank", + type: "address", + }, + ], + stateMutability: "nonpayable", + type: "constructor", + }, + { + stateMutability: "payable", + type: "fallback", + }, + { + inputs: [], + name: "attack", + outputs: [], + stateMutability: "payable", + type: "function", + }, + { + inputs: [], + name: "getBalance", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + ], + inheritedFunctions: {}, + }, + Bank: { + address: "0x872DBd049B5058aFE26966f0e157CE6A906c1d87", + abi: [ + { + inputs: [ + { + internalType: "contract Logger", + name: "_logger", + type: "address", + }, + ], + stateMutability: "nonpayable", + type: "constructor", + }, + { + inputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + name: "balances", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "deposit", + outputs: [], + stateMutability: "payable", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "_amount", + type: "uint256", + }, + ], + name: "withdraw", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + ], + inheritedFunctions: {}, + }, + HoneyPot: { + address: "0xaD8045407fEd8507c206d80619E8a102f7e8B033", + abi: [ + { + inputs: [ + { + internalType: "string", + name: "_a", + type: "string", + }, + { + internalType: "string", + name: "_b", + type: "string", + }, + ], + name: "equal", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "pure", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "_caller", + type: "address", + }, + { + internalType: "uint256", + name: "_amount", + type: "uint256", + }, + { + internalType: "string", + name: "_action", + type: "string", + }, + ], + name: "log", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + ], + inheritedFunctions: {}, + }, + HoneypotExample: { + address: "0xB08027782615630e03c1654e1fA51dD5a19CdB97", + abi: [ + { + inputs: [], + stateMutability: "nonpayable", + type: "constructor", + }, + { + inputs: [], + name: "claimPrize", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "deposit", + outputs: [], + stateMutability: "payable", + type: "function", + }, + { + inputs: [], + name: "getBalance", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "getOwner", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "amount", + type: "uint256", + }, + ], + name: "withdraw", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + ], + inheritedFunctions: {}, + }, + Logger: { + address: "0xC4e11fFC646f699C5F43D05e436c7ace3339cc11", + abi: [ + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "address", + name: "caller", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "amount", + type: "uint256", + }, + { + indexed: false, + internalType: "string", + name: "action", + type: "string", + }, + ], + name: "Log", + type: "event", + }, + { + inputs: [ + { + internalType: "address", + name: "_caller", + type: "address", + }, + { + internalType: "uint256", + name: "_amount", + type: "uint256", + }, + { + internalType: "string", + name: "_action", + type: "string", + }, + ], + name: "log", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + ], + inheritedFunctions: {}, + }, + Swap: { + address: "0x1dc470A85F5B262c1ad03644C22Fa269da438a70", + abi: [ + { + inputs: [ + { + internalType: "contract ISwapRouter", + name: "_swapRouter", + type: "address", + }, + ], + stateMutability: "nonpayable", + type: "constructor", + }, + { + inputs: [ + { + internalType: "address", + name: "token0", + type: "address", + }, + { + internalType: "address", + name: "token1", + type: "address", + }, + { + internalType: "uint256", + name: "amountIn", + type: "uint256", + }, + { + internalType: "uint24", + name: "poolFee", + type: "uint24", + }, + ], + name: "swapExactInputSingle", + outputs: [ + { + internalType: "uint256", + name: "amountOut", + type: "uint256", + }, + ], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "token0", + type: "address", + }, + { + internalType: "address", + name: "token1", + type: "address", + }, + { + internalType: "uint256", + name: "amountOut", + type: "uint256", + }, + { + internalType: "uint256", + name: "amountInMaximum", + type: "uint256", + }, + { + internalType: "uint24", + name: "poolFee", + type: "uint24", + }, + ], + name: "swapExactOutputSingle", + outputs: [ + { + internalType: "uint256", + name: "amountIn", + type: "uint256", + }, + ], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "swapRouter", + outputs: [ + { + internalType: "contract ISwapRouter", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + ], + inheritedFunctions: {}, + }, + YourContract: { + address: "0x956bD0D93aE81855Ae853b97f9B3c8AB866678d6", + 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 deployedContracts satisfies GenericContractsDeclaration; diff --git a/packages/nextjs/contracts/externalContracts.ts b/packages/nextjs/contracts/externalContracts.ts new file mode 100644 index 0000000..f287290 --- /dev/null +++ b/packages/nextjs/contracts/externalContracts.ts @@ -0,0 +1,15 @@ +import { GenericContractsDeclaration } from "~~/utils/scaffold-eth/contract"; + +/** + * @example + * const externalContracts = { + * 1: { + * DAI: { + * address: "0x...", + * abi: [...], + * } + * } as const; + */ +const externalContracts = {} as const; + +export default externalContracts satisfies GenericContractsDeclaration; diff --git a/packages/nextjs/hooks/scaffold-eth/index.ts b/packages/nextjs/hooks/scaffold-eth/index.ts new file mode 100644 index 0000000..10862ff --- /dev/null +++ b/packages/nextjs/hooks/scaffold-eth/index.ts @@ -0,0 +1,16 @@ +export * from "./useAccountBalance"; +export * from "./useAnimationConfig"; +export * from "./useBurnerWallet"; +export * from "./useDeployedContractInfo"; +export * from "./useNativeCurrencyPrice"; +export * from "./useNetworkColor"; +export * from "./useOutsideClick"; +export * from "./useScaffoldContract"; +export * from "./useScaffoldContractRead"; +export * from "./useScaffoldContractWrite"; +export * from "./useScaffoldEventSubscriber"; +export * from "./useScaffoldEventHistory"; +export * from "./useTransactor"; +export * from "./useFetchBlocks"; +export * from "./useContractLogs"; +export * from "./useAutoConnect"; diff --git a/packages/nextjs/hooks/scaffold-eth/useAccountBalance.ts b/packages/nextjs/hooks/scaffold-eth/useAccountBalance.ts new file mode 100644 index 0000000..61946f6 --- /dev/null +++ b/packages/nextjs/hooks/scaffold-eth/useAccountBalance.ts @@ -0,0 +1,35 @@ +import { useCallback, useEffect, useState } from "react"; +import { useTargetNetwork } from "./useTargetNetwork"; +import { useBalance } from "wagmi"; +import { useGlobalState } from "~~/services/store/store"; + +export function useAccountBalance(address?: string) { + const [isEthBalance, setIsEthBalance] = useState(true); + const [balance, setBalance] = useState(null); + const price = useGlobalState(state => state.nativeCurrencyPrice); + const { targetNetwork } = useTargetNetwork(); + + const { + data: fetchedBalanceData, + isError, + isLoading, + } = useBalance({ + address, + watch: true, + chainId: targetNetwork.id, + }); + + const onToggleBalance = useCallback(() => { + if (price > 0) { + setIsEthBalance(!isEthBalance); + } + }, [isEthBalance, price]); + + useEffect(() => { + if (fetchedBalanceData?.formatted) { + setBalance(Number(fetchedBalanceData.formatted)); + } + }, [fetchedBalanceData, targetNetwork]); + + return { balance, price, isError, isLoading, onToggleBalance, isEthBalance }; +} diff --git a/packages/nextjs/hooks/scaffold-eth/useAnimationConfig.ts b/packages/nextjs/hooks/scaffold-eth/useAnimationConfig.ts new file mode 100644 index 0000000..e0044fd --- /dev/null +++ b/packages/nextjs/hooks/scaffold-eth/useAnimationConfig.ts @@ -0,0 +1,20 @@ +import { useEffect, useState } from "react"; + +const ANIMATION_TIME = 2000; + +export function useAnimationConfig(data: any) { + const [showAnimation, setShowAnimation] = useState(false); + const [prevData, setPrevData] = useState(); + + useEffect(() => { + if (prevData !== undefined && prevData !== data) { + setShowAnimation(true); + setTimeout(() => setShowAnimation(false), ANIMATION_TIME); + } + setPrevData(data); + }, [data, prevData]); + + return { + showAnimation, + }; +} diff --git a/packages/nextjs/hooks/scaffold-eth/useAutoConnect.ts b/packages/nextjs/hooks/scaffold-eth/useAutoConnect.ts new file mode 100644 index 0000000..36260f2 --- /dev/null +++ b/packages/nextjs/hooks/scaffold-eth/useAutoConnect.ts @@ -0,0 +1,85 @@ +import { useEffect } from "react"; +import { useEffectOnce, useLocalStorage, useReadLocalStorage } from "usehooks-ts"; +import { Chain, hardhat } from "viem/chains"; +import { Connector, useAccount, useConnect } from "wagmi"; +import scaffoldConfig from "~~/scaffold.config"; +import { burnerWalletId } from "~~/services/web3/wagmi-burner/BurnerConnector"; +import { getTargetNetworks } from "~~/utils/scaffold-eth"; + +const SCAFFOLD_WALLET_STROAGE_KEY = "scaffoldEth2.wallet"; +const WAGMI_WALLET_STORAGE_KEY = "wagmi.wallet"; + +// ID of the SAFE connector instance +const SAFE_ID = "safe"; + +/** + * This function will get the initial wallet connector (if any), the app will connect to + * @param initialNetwork + * @param previousWalletId + * @param connectors + * @returns + */ +const getInitialConnector = ( + initialNetwork: Chain, + previousWalletId: string, + connectors: Connector[], +): { connector: Connector | undefined; chainId?: number } | undefined => { + // Look for the SAFE connector instance and connect to it instantly if loaded in SAFE frame + const safeConnectorInstance = connectors.find(connector => connector.id === SAFE_ID && connector.ready); + + if (safeConnectorInstance) { + return { connector: safeConnectorInstance }; + } + + const allowBurner = scaffoldConfig.onlyLocalBurnerWallet ? initialNetwork.id === hardhat.id : true; + + if (!previousWalletId) { + // The user was not connected to a wallet + if (allowBurner && scaffoldConfig.walletAutoConnect) { + const connector = connectors.find(f => f.id === burnerWalletId); + return { connector, chainId: initialNetwork.id }; + } + } else { + // the user was connected to wallet + if (scaffoldConfig.walletAutoConnect) { + if (previousWalletId === burnerWalletId && !allowBurner) { + return; + } + + const connector = connectors.find(f => f.id === previousWalletId); + return { connector }; + } + } + + return undefined; +}; + +/** + * Automatically connect to a wallet/connector based on config and prior wallet + */ +export const useAutoConnect = (): void => { + const wagmiWalletValue = useReadLocalStorage(WAGMI_WALLET_STORAGE_KEY); + const [walletId, setWalletId] = useLocalStorage(SCAFFOLD_WALLET_STROAGE_KEY, wagmiWalletValue ?? ""); + const connectState = useConnect(); + const accountState = useAccount(); + + useEffect(() => { + if (accountState.isConnected) { + // user is connected, set walletName + setWalletId(accountState.connector?.id ?? ""); + } else { + // user has disconnected, reset walletName + window.localStorage.setItem(WAGMI_WALLET_STORAGE_KEY, JSON.stringify("")); + setWalletId(""); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [accountState.isConnected, accountState.connector?.name]); + + useEffectOnce(() => { + const initialConnector = getInitialConnector(getTargetNetworks()[0], walletId, connectState.connectors); + + if (initialConnector?.connector) { + connectState.connect({ connector: initialConnector.connector, chainId: initialConnector.chainId }); + } + }); +}; diff --git a/packages/nextjs/hooks/scaffold-eth/useBurnerWallet.ts b/packages/nextjs/hooks/scaffold-eth/useBurnerWallet.ts new file mode 100644 index 0000000..bb75706 --- /dev/null +++ b/packages/nextjs/hooks/scaffold-eth/useBurnerWallet.ts @@ -0,0 +1,141 @@ +import { useCallback, useEffect, useRef, useState } from "react"; +import { useLocalStorage } from "usehooks-ts"; +import { Chain, Hex, HttpTransport, PrivateKeyAccount, createWalletClient, http } from "viem"; +import { generatePrivateKey, privateKeyToAccount } from "viem/accounts"; +import { WalletClient, usePublicClient } from "wagmi"; + +const burnerStorageKey = "scaffoldEth2.burnerWallet.sk"; + +/** + * Checks if the private key is valid + */ +const isValidSk = (pk: Hex | string | undefined | null): boolean => { + return pk?.length === 64 || pk?.length === 66; +}; + +/** + * If no burner is found in localstorage, we will generate a random private key + */ +const newDefaultPrivateKey = generatePrivateKey(); + +/** + * Save the current burner private key to local storage + */ +export const saveBurnerSK = (privateKey: Hex): void => { + if (typeof window != "undefined" && window != null) { + window?.localStorage?.setItem(burnerStorageKey, privateKey); + } +}; + +/** + * Gets the current burner private key from local storage + */ +export const loadBurnerSK = (): Hex => { + let currentSk: Hex = "0x"; + if (typeof window != "undefined" && window != null) { + currentSk = (window?.localStorage?.getItem?.(burnerStorageKey)?.replaceAll('"', "") ?? "0x") as Hex; + } + + if (!!currentSk && isValidSk(currentSk)) { + return currentSk; + } else { + saveBurnerSK(newDefaultPrivateKey); + return newDefaultPrivateKey; + } +}; + +type BurnerAccount = { + walletClient: WalletClient | undefined; + account: PrivateKeyAccount | undefined; + // creates a new burner account + generateNewBurner: () => void; + // explicitly save burner to storage + saveBurner: () => void; +}; + +/** + * Creates a burner wallet + */ +export const useBurnerWallet = (): BurnerAccount => { + const [burnerSk, setBurnerSk] = useLocalStorage(burnerStorageKey, newDefaultPrivateKey); + + const publicClient = usePublicClient(); + const [walletClient, setWalletClient] = useState>(); + const [generatedPrivateKey, setGeneratedPrivateKey] = useState("0x"); + const [account, setAccount] = useState(); + const isCreatingNewBurnerRef = useRef(false); + + const saveBurner = useCallback(() => { + setBurnerSk(generatedPrivateKey); + }, [setBurnerSk, generatedPrivateKey]); + + const generateNewBurner = useCallback(() => { + if (publicClient && !isCreatingNewBurnerRef.current) { + console.log("🔑 Create new burner wallet..."); + isCreatingNewBurnerRef.current = true; + + const randomPrivateKey = generatePrivateKey(); + const randomAccount = privateKeyToAccount(randomPrivateKey); + + const client = createWalletClient({ + chain: publicClient.chain, + account: randomAccount, + transport: http(), + }); + + setWalletClient(client); + setGeneratedPrivateKey(randomPrivateKey); + setAccount(randomAccount); + + setBurnerSk(() => { + console.log("🔥 Saving new burner wallet"); + isCreatingNewBurnerRef.current = false; + return randomPrivateKey; + }); + return client; + } else { + console.log("⚠ Could not create burner wallet"); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [publicClient.chain.id]); + + /** + * Load wallet with burnerSk + * connect and set wallet, once we have burnerSk and valid provider + */ + useEffect(() => { + if (burnerSk && publicClient.chain.id) { + let wallet: WalletClient | undefined = undefined; + if (isValidSk(burnerSk)) { + const randomAccount = privateKeyToAccount(burnerSk); + + wallet = createWalletClient({ + chain: publicClient.chain, + account: randomAccount, + transport: http(), + }); + + setGeneratedPrivateKey(burnerSk); + setAccount(randomAccount); + } else { + wallet = generateNewBurner(); + } + + if (wallet == null) { + throw "Error: Could not create burner wallet"; + } + + setWalletClient(wallet); + saveBurner(); + } + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [burnerSk, publicClient.chain.id]); + + return { + walletClient, + account, + generateNewBurner, + saveBurner, + }; +}; diff --git a/packages/nextjs/hooks/scaffold-eth/useContractLogs.ts b/packages/nextjs/hooks/scaffold-eth/useContractLogs.ts new file mode 100644 index 0000000..a5cf7d0 --- /dev/null +++ b/packages/nextjs/hooks/scaffold-eth/useContractLogs.ts @@ -0,0 +1,37 @@ +import { useEffect, useState } from "react"; +import { Address, Log } from "viem"; +import { usePublicClient } from "wagmi"; + +export const useContractLogs = (address: Address) => { + const [logs, setLogs] = useState([]); + const client = usePublicClient(); + + useEffect(() => { + const fetchLogs = async () => { + try { + const existingLogs = await client.getLogs({ + address: address, + fromBlock: 0n, + toBlock: "latest", + }); + setLogs(existingLogs); + } catch (error) { + console.error("Failed to fetch logs:", error); + } + }; + fetchLogs(); + + return client.watchBlockNumber({ + onBlockNumber: async (blockNumber, prevBlockNumber) => { + const newLogs = await client.getLogs({ + address: address, + fromBlock: prevBlockNumber, + toBlock: "latest", + }); + setLogs(prevLogs => [...prevLogs, ...newLogs]); + }, + }); + }, [address, client]); + + return logs; +}; diff --git a/packages/nextjs/hooks/scaffold-eth/useDeployedContractInfo.ts b/packages/nextjs/hooks/scaffold-eth/useDeployedContractInfo.ts new file mode 100644 index 0000000..69b961f --- /dev/null +++ b/packages/nextjs/hooks/scaffold-eth/useDeployedContractInfo.ts @@ -0,0 +1,46 @@ +import { useEffect, useState } from "react"; +import { useTargetNetwork } from "./useTargetNetwork"; +import { useIsMounted } from "usehooks-ts"; +import { usePublicClient } from "wagmi"; +import { Contract, ContractCodeStatus, ContractName, contracts } from "~~/utils/scaffold-eth/contract"; + +/** + * Gets the matching contract info from the contracts file generated by `yarn deploy` + * @param contractName - name of deployed contract + */ +export const useDeployedContractInfo = (contractName: TContractName) => { + const isMounted = useIsMounted(); + const { targetNetwork } = useTargetNetwork(); + const deployedContract = contracts?.[targetNetwork.id]?.[contractName as ContractName] as Contract; + const [status, setStatus] = useState(ContractCodeStatus.LOADING); + const publicClient = usePublicClient({ chainId: targetNetwork.id }); + + useEffect(() => { + const checkContractDeployment = async () => { + if (!deployedContract) { + setStatus(ContractCodeStatus.NOT_FOUND); + return; + } + const code = await publicClient.getBytecode({ + address: deployedContract.address, + }); + + if (!isMounted()) { + return; + } + // If contract code is `0x` => no contract deployed on that address + if (code === "0x") { + setStatus(ContractCodeStatus.NOT_FOUND); + return; + } + setStatus(ContractCodeStatus.DEPLOYED); + }; + + checkContractDeployment(); + }, [isMounted, contractName, deployedContract, publicClient]); + + return { + data: status === ContractCodeStatus.DEPLOYED ? deployedContract : undefined, + isLoading: status === ContractCodeStatus.LOADING, + }; +}; diff --git a/packages/nextjs/hooks/scaffold-eth/useFetchBlocks.ts b/packages/nextjs/hooks/scaffold-eth/useFetchBlocks.ts new file mode 100644 index 0000000..7e490ad --- /dev/null +++ b/packages/nextjs/hooks/scaffold-eth/useFetchBlocks.ts @@ -0,0 +1,133 @@ +import { useCallback, useEffect, useState } from "react"; +import { + Block, + Hash, + Transaction, + TransactionReceipt, + createTestClient, + publicActions, + walletActions, + webSocket, +} from "viem"; +import { hardhat } from "viem/chains"; +import { decodeTransactionData } from "~~/utils/scaffold-eth"; + +const BLOCKS_PER_PAGE = 20; + +export const testClient = createTestClient({ + chain: hardhat, + mode: "hardhat", + transport: webSocket("ws://127.0.0.1:8545"), +}) + .extend(publicActions) + .extend(walletActions); + +export const useFetchBlocks = () => { + const [blocks, setBlocks] = useState([]); + const [transactionReceipts, setTransactionReceipts] = useState<{ + [key: string]: TransactionReceipt; + }>({}); + const [currentPage, setCurrentPage] = useState(0); + const [totalBlocks, setTotalBlocks] = useState(0n); + const [error, setError] = useState(null); + + const fetchBlocks = useCallback(async () => { + setError(null); + + try { + const blockNumber = await testClient.getBlockNumber(); + setTotalBlocks(blockNumber); + + const startingBlock = blockNumber - BigInt(currentPage * BLOCKS_PER_PAGE); + const blockNumbersToFetch = Array.from( + { length: Number(BLOCKS_PER_PAGE < startingBlock + 1n ? BLOCKS_PER_PAGE : startingBlock + 1n) }, + (_, i) => startingBlock - BigInt(i), + ); + + const blocksWithTransactions = blockNumbersToFetch.map(async blockNumber => { + try { + return testClient.getBlock({ blockNumber, includeTransactions: true }); + } catch (err) { + setError(err instanceof Error ? err : new Error("An error occurred.")); + throw err; + } + }); + const fetchedBlocks = await Promise.all(blocksWithTransactions); + + fetchedBlocks.forEach(block => { + block.transactions.forEach(tx => decodeTransactionData(tx as Transaction)); + }); + + const txReceipts = await Promise.all( + fetchedBlocks.flatMap(block => + block.transactions.map(async tx => { + try { + const receipt = await testClient.getTransactionReceipt({ hash: (tx as Transaction).hash }); + return { [(tx as Transaction).hash]: receipt }; + } catch (err) { + setError(err instanceof Error ? err : new Error("An error occurred.")); + throw err; + } + }), + ), + ); + + setBlocks(fetchedBlocks); + setTransactionReceipts(prevReceipts => ({ ...prevReceipts, ...Object.assign({}, ...txReceipts) })); + } catch (err) { + setError(err instanceof Error ? err : new Error("An error occurred.")); + } + }, [currentPage]); + + useEffect(() => { + fetchBlocks(); + }, [fetchBlocks]); + + useEffect(() => { + const handleNewBlock = async (newBlock: any) => { + try { + if (currentPage === 0) { + if (newBlock.transactions.length > 0) { + const transactionsDetails = await Promise.all( + newBlock.transactions.map((txHash: string) => testClient.getTransaction({ hash: txHash as Hash })), + ); + newBlock.transactions = transactionsDetails; + } + + newBlock.transactions.forEach((tx: Transaction) => decodeTransactionData(tx as Transaction)); + + const receipts = await Promise.all( + newBlock.transactions.map(async (tx: Transaction) => { + try { + const receipt = await testClient.getTransactionReceipt({ hash: (tx as Transaction).hash }); + return { [(tx as Transaction).hash]: receipt }; + } catch (err) { + setError(err instanceof Error ? err : new Error("An error occurred fetching receipt.")); + throw err; + } + }), + ); + + setBlocks(prevBlocks => [newBlock, ...prevBlocks.slice(0, BLOCKS_PER_PAGE - 1)]); + setTransactionReceipts(prevReceipts => ({ ...prevReceipts, ...Object.assign({}, ...receipts) })); + } + if (newBlock.number) { + setTotalBlocks(newBlock.number); + } + } catch (err) { + setError(err instanceof Error ? err : new Error("An error occurred.")); + } + }; + + return testClient.watchBlocks({ onBlock: handleNewBlock, includeTransactions: true }); + }, [currentPage]); + + return { + blocks, + transactionReceipts, + currentPage, + totalBlocks, + setCurrentPage, + error, + }; +}; diff --git a/packages/nextjs/hooks/scaffold-eth/useNativeCurrencyPrice.ts b/packages/nextjs/hooks/scaffold-eth/useNativeCurrencyPrice.ts new file mode 100644 index 0000000..b81e781 --- /dev/null +++ b/packages/nextjs/hooks/scaffold-eth/useNativeCurrencyPrice.ts @@ -0,0 +1,35 @@ +import { useEffect, useState } from "react"; +import { useTargetNetwork } from "./useTargetNetwork"; +import { useInterval } from "usehooks-ts"; +import scaffoldConfig from "~~/scaffold.config"; +import { fetchPriceFromUniswap } from "~~/utils/scaffold-eth"; + +const enablePolling = false; + +/** + * Get the price of Native Currency based on Native Token/DAI trading pair from Uniswap SDK + * @returns nativeCurrencyPrice: number + */ +export const useNativeCurrencyPrice = () => { + const { targetNetwork } = useTargetNetwork(); + const [nativeCurrencyPrice, setNativeCurrencyPrice] = useState(0); + + // Get the price of ETH from Uniswap on mount + useEffect(() => { + (async () => { + const price = await fetchPriceFromUniswap(targetNetwork); + setNativeCurrencyPrice(price); + })(); + }, [targetNetwork]); + + // Get the price of ETH from Uniswap at a given interval + useInterval( + async () => { + const price = await fetchPriceFromUniswap(targetNetwork); + setNativeCurrencyPrice(price); + }, + enablePolling ? scaffoldConfig.pollingInterval : null, + ); + + return nativeCurrencyPrice; +}; diff --git a/packages/nextjs/hooks/scaffold-eth/useNetworkColor.ts b/packages/nextjs/hooks/scaffold-eth/useNetworkColor.ts new file mode 100644 index 0000000..3e7e4fb --- /dev/null +++ b/packages/nextjs/hooks/scaffold-eth/useNetworkColor.ts @@ -0,0 +1,20 @@ +import { useTargetNetwork } from "./useTargetNetwork"; +import { useDarkMode } from "usehooks-ts"; +import { ChainWithAttributes } from "~~/utils/scaffold-eth"; + +export const DEFAULT_NETWORK_COLOR: [string, string] = ["#666666", "#bbbbbb"]; + +export function getNetworkColor(network: ChainWithAttributes, isDarkMode: boolean) { + const colorConfig = network.color ?? DEFAULT_NETWORK_COLOR; + return Array.isArray(colorConfig) ? (isDarkMode ? colorConfig[1] : colorConfig[0]) : colorConfig; +} + +/** + * Gets the color of the target network + */ +export const useNetworkColor = () => { + const { isDarkMode } = useDarkMode(); + const { targetNetwork } = useTargetNetwork(); + + return getNetworkColor(targetNetwork, isDarkMode); +}; diff --git a/packages/nextjs/hooks/scaffold-eth/useOutsideClick.ts b/packages/nextjs/hooks/scaffold-eth/useOutsideClick.ts new file mode 100644 index 0000000..28a44b5 --- /dev/null +++ b/packages/nextjs/hooks/scaffold-eth/useOutsideClick.ts @@ -0,0 +1,21 @@ +import React, { useEffect } from "react"; + +/** + * Check if a click was made outside the passed ref + */ +export const useOutsideClick = (ref: React.RefObject, callback: { (): void }) => { + useEffect(() => { + function handleOutsideClick(event: MouseEvent) { + if (!(event.target instanceof Element)) { + return; + } + + if (ref.current && !ref.current.contains(event.target)) { + callback(); + } + } + + document.addEventListener("click", handleOutsideClick); + return () => document.removeEventListener("click", handleOutsideClick); + }, [ref, callback]); +}; diff --git a/packages/nextjs/hooks/scaffold-eth/useScaffoldContract.ts b/packages/nextjs/hooks/scaffold-eth/useScaffoldContract.ts new file mode 100644 index 0000000..5dd782a --- /dev/null +++ b/packages/nextjs/hooks/scaffold-eth/useScaffoldContract.ts @@ -0,0 +1,48 @@ +import { Account, Address, Chain, Transport, getContract } from "viem"; +import { PublicClient, usePublicClient } from "wagmi"; +import { GetWalletClientResult } from "wagmi/actions"; +import { useDeployedContractInfo } from "~~/hooks/scaffold-eth"; +import { Contract, ContractName } from "~~/utils/scaffold-eth/contract"; + +/** + * Gets a deployed contract by contract name and returns a contract instance + * @param config - The config settings + * @param config.contractName - Deployed contract name + * @param config.walletClient - An viem wallet client instance (optional) + */ +export const useScaffoldContract = < + TContractName extends ContractName, + TWalletClient extends Exclude | undefined, +>({ + contractName, + walletClient, +}: { + contractName: TContractName; + walletClient?: TWalletClient | null; +}) => { + const { data: deployedContractData, isLoading: deployedContractLoading } = useDeployedContractInfo(contractName); + const publicClient = usePublicClient(); + + let contract = undefined; + if (deployedContractData) { + contract = getContract< + Transport, + Address, + Contract["abi"], + Chain, + Account, + PublicClient, + TWalletClient + >({ + address: deployedContractData.address, + abi: deployedContractData.abi as Contract["abi"], + walletClient: walletClient ? walletClient : undefined, + publicClient, + }); + } + + return { + data: contract, + isLoading: deployedContractLoading, + }; +}; diff --git a/packages/nextjs/hooks/scaffold-eth/useScaffoldContractRead.ts b/packages/nextjs/hooks/scaffold-eth/useScaffoldContractRead.ts new file mode 100644 index 0000000..55cb76d --- /dev/null +++ b/packages/nextjs/hooks/scaffold-eth/useScaffoldContractRead.ts @@ -0,0 +1,48 @@ +import { useTargetNetwork } from "./useTargetNetwork"; +import type { ExtractAbiFunctionNames } from "abitype"; +import { useContractRead } from "wagmi"; +import { useDeployedContractInfo } from "~~/hooks/scaffold-eth"; +import { + AbiFunctionReturnType, + ContractAbi, + ContractName, + UseScaffoldReadConfig, +} from "~~/utils/scaffold-eth/contract"; + +/** + * Wrapper for wagmi's useContractRead hook which automatically loads (by name) + * the contract ABI and address from the deployed contracts + * @param config - The config settings, including extra wagmi configuration + * @param config.contractName - deployed contract name + * @param config.functionName - name of the function to be called + * @param config.args - args to be passed to the function call + */ +export const useScaffoldContractRead = < + TContractName extends ContractName, + TFunctionName extends ExtractAbiFunctionNames, "pure" | "view">, +>({ + contractName, + functionName, + args, + ...readConfig +}: UseScaffoldReadConfig) => { + const { data: deployedContract } = useDeployedContractInfo(contractName); + const { targetNetwork } = useTargetNetwork(); + + return useContractRead({ + chainId: targetNetwork.id, + functionName, + address: deployedContract?.address, + abi: deployedContract?.abi, + watch: true, + args, + enabled: !Array.isArray(args) || !args.some(arg => arg === undefined), + ...(readConfig as any), + }) as Omit, "data" | "refetch"> & { + data: AbiFunctionReturnType | undefined; + refetch: (options?: { + throwOnError: boolean; + cancelRefetch: boolean; + }) => Promise>; + }; +}; diff --git a/packages/nextjs/hooks/scaffold-eth/useScaffoldContractWrite.ts b/packages/nextjs/hooks/scaffold-eth/useScaffoldContractWrite.ts new file mode 100644 index 0000000..8c09582 --- /dev/null +++ b/packages/nextjs/hooks/scaffold-eth/useScaffoldContractWrite.ts @@ -0,0 +1,102 @@ +import { useState } from "react"; +import { useTargetNetwork } from "./useTargetNetwork"; +import { Abi, ExtractAbiFunctionNames } from "abitype"; +import { useContractWrite, useNetwork } from "wagmi"; +import { getParsedError } from "~~/components/scaffold-eth"; +import { useDeployedContractInfo, useTransactor } from "~~/hooks/scaffold-eth"; +import { notification } from "~~/utils/scaffold-eth"; +import { ContractAbi, ContractName, UseScaffoldWriteConfig } from "~~/utils/scaffold-eth/contract"; + +type UpdatedArgs = Parameters>["writeAsync"]>[0]; + +/** + * Wrapper for wagmi's useContractWrite hook (with config prepared by usePrepareContractWrite hook) + * which automatically loads (by name) the contract ABI and address from the deployed contracts + * @param config - The config settings, including extra wagmi configuration + * @param config.contractName - deployed contract name + * @param config.functionName - name of the function to be called + * @param config.args - arguments for the function + * @param config.value - value in ETH that will be sent with transaction + */ +export const useScaffoldContractWrite = < + TContractName extends ContractName, + TFunctionName extends ExtractAbiFunctionNames, "nonpayable" | "payable">, +>({ + contractName, + functionName, + args, + value, + onBlockConfirmation, + blockConfirmations, + ...writeConfig +}: UseScaffoldWriteConfig) => { + const { data: deployedContractData } = useDeployedContractInfo(contractName); + const { chain } = useNetwork(); + const writeTx = useTransactor(); + const [isMining, setIsMining] = useState(false); + const { targetNetwork } = useTargetNetwork(); + + const wagmiContractWrite = useContractWrite({ + chainId: targetNetwork.id, + address: deployedContractData?.address, + abi: deployedContractData?.abi as Abi, + functionName: functionName as any, + args: args as unknown[], + value: value, + ...writeConfig, + }); + + const sendContractWriteTx = async ({ + args: newArgs, + value: newValue, + ...otherConfig + }: { + args?: UseScaffoldWriteConfig["args"]; + value?: UseScaffoldWriteConfig["value"]; + } & UpdatedArgs = {}) => { + if (!deployedContractData) { + notification.error("Target Contract is not deployed, did you forget to run `yarn deploy`?"); + return; + } + if (!chain?.id) { + notification.error("Please connect your wallet"); + return; + } + if (chain?.id !== targetNetwork.id) { + notification.error("You are on the wrong network"); + return; + } + + if (wagmiContractWrite.writeAsync) { + try { + setIsMining(true); + const writeTxResult = await writeTx( + () => + wagmiContractWrite.writeAsync({ + args: newArgs ?? args, + value: newValue ?? value, + ...otherConfig, + }), + { onBlockConfirmation, blockConfirmations }, + ); + + return writeTxResult; + } catch (e: any) { + const message = getParsedError(e); + notification.error(message); + } finally { + setIsMining(false); + } + } else { + notification.error("Contract writer error. Try again."); + return; + } + }; + + return { + ...wagmiContractWrite, + isMining, + // Overwrite wagmi's write async + writeAsync: sendContractWriteTx, + }; +}; diff --git a/packages/nextjs/hooks/scaffold-eth/useScaffoldEventHistory.ts b/packages/nextjs/hooks/scaffold-eth/useScaffoldEventHistory.ts new file mode 100644 index 0000000..a8c2b3a --- /dev/null +++ b/packages/nextjs/hooks/scaffold-eth/useScaffoldEventHistory.ts @@ -0,0 +1,184 @@ +import { useEffect, useMemo, useState } from "react"; +import { useTargetNetwork } from "./useTargetNetwork"; +import { Abi, AbiEvent, ExtractAbiEventNames } from "abitype"; +import { useInterval } from "usehooks-ts"; +import { Hash } from "viem"; +import * as chains from "viem/chains"; +import { usePublicClient } from "wagmi"; +import { useDeployedContractInfo } from "~~/hooks/scaffold-eth"; +import scaffoldConfig from "~~/scaffold.config"; +import { replacer } from "~~/utils/scaffold-eth/common"; +import { + ContractAbi, + ContractName, + UseScaffoldEventHistoryConfig, + UseScaffoldEventHistoryData, +} from "~~/utils/scaffold-eth/contract"; + +/** + * Reads events from a deployed contract + * @param config - The config settings + * @param config.contractName - deployed contract name + * @param config.eventName - name of the event to listen for + * @param config.fromBlock - the block number to start reading events from + * @param config.filters - filters to be applied to the event (parameterName: value) + * @param config.blockData - if set to true it will return the block data for each event (default: false) + * @param config.transactionData - if set to true it will return the transaction data for each event (default: false) + * @param config.receiptData - if set to true it will return the receipt data for each event (default: false) + * @param config.watch - if set to true, the events will be updated every pollingInterval milliseconds set at scaffoldConfig (default: false) + * @param config.enabled - set this to false to disable the hook from running (default: true) + */ +export const useScaffoldEventHistory = < + TContractName extends ContractName, + TEventName extends ExtractAbiEventNames>, + TBlockData extends boolean = false, + TTransactionData extends boolean = false, + TReceiptData extends boolean = false, +>({ + contractName, + eventName, + fromBlock, + filters, + blockData, + transactionData, + receiptData, + watch, + enabled = true, +}: UseScaffoldEventHistoryConfig) => { + const [events, setEvents] = useState(); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(); + const [fromBlockUpdated, setFromBlockUpdated] = useState(fromBlock); + + const { data: deployedContractData, isLoading: deployedContractLoading } = useDeployedContractInfo(contractName); + const publicClient = usePublicClient(); + const { targetNetwork } = useTargetNetwork(); + + const readEvents = async (fromBlock?: bigint) => { + setIsLoading(true); + try { + if (!deployedContractData) { + throw new Error("Contract not found"); + } + + if (!enabled) { + throw new Error("Hook disabled"); + } + + const event = (deployedContractData.abi as Abi).find( + part => part.type === "event" && part.name === eventName, + ) as AbiEvent; + + const blockNumber = await publicClient.getBlockNumber({ cacheTime: 0 }); + + if ((fromBlock && blockNumber >= fromBlock) || blockNumber >= fromBlockUpdated) { + const logs = await publicClient.getLogs({ + address: deployedContractData?.address, + event, + args: filters as any, // TODO: check if it works and fix type + fromBlock: fromBlock || fromBlockUpdated, + toBlock: blockNumber, + }); + setFromBlockUpdated(blockNumber + 1n); + + const newEvents = []; + for (let i = logs.length - 1; i >= 0; i--) { + newEvents.push({ + log: logs[i], + args: logs[i].args, + block: + blockData && logs[i].blockHash === null + ? null + : await publicClient.getBlock({ blockHash: logs[i].blockHash as Hash }), + transaction: + transactionData && logs[i].transactionHash !== null + ? await publicClient.getTransaction({ hash: logs[i].transactionHash as Hash }) + : null, + receipt: + receiptData && logs[i].transactionHash !== null + ? await publicClient.getTransactionReceipt({ hash: logs[i].transactionHash as Hash }) + : null, + }); + } + if (events && typeof fromBlock === "undefined") { + setEvents([...newEvents, ...events]); + } else { + setEvents(newEvents); + } + setError(undefined); + } + } catch (e: any) { + console.error(e); + setEvents(undefined); + setError(e); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + readEvents(fromBlock); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [fromBlock, enabled]); + + useEffect(() => { + if (!deployedContractLoading) { + readEvents(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ + publicClient, + contractName, + eventName, + deployedContractLoading, + deployedContractData?.address, + deployedContractData, + // eslint-disable-next-line react-hooks/exhaustive-deps + JSON.stringify(filters, replacer), + blockData, + transactionData, + receiptData, + ]); + + useEffect(() => { + // Reset the internal state when target network or fromBlock changed + setEvents([]); + setFromBlockUpdated(fromBlock); + setError(undefined); + }, [fromBlock, targetNetwork.id]); + + useInterval( + async () => { + if (!deployedContractLoading) { + readEvents(); + } + }, + watch ? (targetNetwork.id !== chains.hardhat.id ? scaffoldConfig.pollingInterval : 4_000) : null, + ); + + const eventHistoryData = useMemo( + () => + events?.map(addIndexedArgsToEvent) as UseScaffoldEventHistoryData< + TContractName, + TEventName, + TBlockData, + TTransactionData, + TReceiptData + >, + [events], + ); + + return { + data: eventHistoryData, + isLoading: isLoading, + error: error, + }; +}; + +export const addIndexedArgsToEvent = (event: any) => { + if (event.args && !Array.isArray(event.args)) { + return { ...event, args: { ...event.args, ...Object.values(event.args) } }; + } + + return event; +}; diff --git a/packages/nextjs/hooks/scaffold-eth/useScaffoldEventSubscriber.ts b/packages/nextjs/hooks/scaffold-eth/useScaffoldEventSubscriber.ts new file mode 100644 index 0000000..66fb3e4 --- /dev/null +++ b/packages/nextjs/hooks/scaffold-eth/useScaffoldEventSubscriber.ts @@ -0,0 +1,38 @@ +import { useTargetNetwork } from "./useTargetNetwork"; +import { Abi, ExtractAbiEventNames } from "abitype"; +import { Log } from "viem"; +import { useContractEvent } from "wagmi"; +import { addIndexedArgsToEvent, useDeployedContractInfo } from "~~/hooks/scaffold-eth"; +import { ContractAbi, ContractName, UseScaffoldEventConfig } from "~~/utils/scaffold-eth/contract"; + +/** + * Wrapper for wagmi's useContractEvent which automatically loads (by name) + * the contract ABI and address from the deployed contracts. + * @param config - The config settings + * @param config.contractName - deployed contract name + * @param config.eventName - name of the event to listen for + * @param config.listener - the callback that receives events. If only interested in 1 event, call `unwatch` inside of the listener + */ +export const useScaffoldEventSubscriber = < + TContractName extends ContractName, + TEventName extends ExtractAbiEventNames>, +>({ + contractName, + eventName, + listener, +}: UseScaffoldEventConfig) => { + const { data: deployedContractData } = useDeployedContractInfo(contractName); + const { targetNetwork } = useTargetNetwork(); + + const addInexedArgsToLogs = (logs: Log[]) => logs.map(addIndexedArgsToEvent); + const listenerWithIndexedArgs = (logs: Log[]) => + listener(addInexedArgsToLogs(logs) as Parameters[0]); + + return useContractEvent({ + address: deployedContractData?.address, + abi: deployedContractData?.abi as Abi, + chainId: targetNetwork.id, + listener: listenerWithIndexedArgs, + eventName, + }); +}; diff --git a/packages/nextjs/hooks/scaffold-eth/useTargetNetwork.ts b/packages/nextjs/hooks/scaffold-eth/useTargetNetwork.ts new file mode 100644 index 0000000..2ce7653 --- /dev/null +++ b/packages/nextjs/hooks/scaffold-eth/useTargetNetwork.ts @@ -0,0 +1,26 @@ +import { useEffect } from "react"; +import { useNetwork } from "wagmi"; +import scaffoldConfig from "~~/scaffold.config"; +import { useGlobalState } from "~~/services/store/store"; +import { ChainWithAttributes } from "~~/utils/scaffold-eth"; +import { NETWORKS_EXTRA_DATA } from "~~/utils/scaffold-eth"; + +export function useTargetNetwork(): { targetNetwork: ChainWithAttributes } { + const { chain } = useNetwork(); + const targetNetwork = useGlobalState(({ targetNetwork }) => targetNetwork); + const setTargetNetwork = useGlobalState(({ setTargetNetwork }) => setTargetNetwork); + + useEffect(() => { + const newSelectedNetwork = scaffoldConfig.targetNetworks.find(targetNetwork => targetNetwork.id === chain?.id); + if (newSelectedNetwork && newSelectedNetwork.id !== targetNetwork.id) { + setTargetNetwork(newSelectedNetwork); + } + }, [chain?.id, setTargetNetwork, targetNetwork.id]); + + return { + targetNetwork: { + ...targetNetwork, + ...NETWORKS_EXTRA_DATA[targetNetwork.id], + }, + }; +} diff --git a/packages/nextjs/hooks/scaffold-eth/useTransactor.tsx b/packages/nextjs/hooks/scaffold-eth/useTransactor.tsx new file mode 100644 index 0000000..efd7444 --- /dev/null +++ b/packages/nextjs/hooks/scaffold-eth/useTransactor.tsx @@ -0,0 +1,106 @@ +import { WriteContractResult, getPublicClient } from "@wagmi/core"; +import { Hash, SendTransactionParameters, TransactionReceipt, WalletClient } from "viem"; +import { useWalletClient } from "wagmi"; +import { getParsedError } from "~~/components/scaffold-eth"; +import { getBlockExplorerTxLink, notification } from "~~/utils/scaffold-eth"; + +type TransactionFunc = ( + tx: (() => Promise) | (() => Promise) | SendTransactionParameters, + options?: { + onBlockConfirmation?: (txnReceipt: TransactionReceipt) => void; + blockConfirmations?: number; + }, +) => Promise; + +/** + * Custom notification content for TXs. + */ +const TxnNotification = ({ message, blockExplorerLink }: { message: string; blockExplorerLink?: string }) => { + return ( +
    +

    {message}

    + {blockExplorerLink && blockExplorerLink.length > 0 ? ( + + check out transaction + + ) : null} +
    + ); +}; + +/** + * @description Runs Transaction passed in to returned function showing UI feedback. + * @param _walletClient + * @returns function that takes a transaction and returns a promise of the transaction hash + */ +export const useTransactor = (_walletClient?: WalletClient): TransactionFunc => { + let walletClient = _walletClient; + const { data } = useWalletClient(); + if (walletClient === undefined && data) { + walletClient = data; + } + + const result: TransactionFunc = async (tx, options) => { + if (!walletClient) { + notification.error("Cannot access account"); + console.error("⚡️ ~ file: useTransactor.tsx ~ error"); + return; + } + + let notificationId = null; + let transactionHash: Awaited["hash"] | undefined = undefined; + try { + const network = await walletClient.getChainId(); + // Get full transaction from public client + const publicClient = getPublicClient(); + + notificationId = notification.loading(); + if (typeof tx === "function") { + // Tx is already prepared by the caller + const result = await tx(); + if (typeof result === "string") { + transactionHash = result; + } else { + transactionHash = result.hash; + } + } else if (tx != null) { + transactionHash = await walletClient.sendTransaction(tx); + } else { + throw new Error("Incorrect transaction passed to transactor"); + } + notification.remove(notificationId); + + const blockExplorerTxURL = network ? getBlockExplorerTxLink(network, transactionHash) : ""; + + notificationId = notification.loading( + , + ); + + const transactionReceipt = await publicClient.waitForTransactionReceipt({ + hash: transactionHash, + confirmations: options?.blockConfirmations, + }); + notification.remove(notificationId); + + notification.success( + , + { + icon: "🎉", + }, + ); + + if (options?.onBlockConfirmation) options.onBlockConfirmation(transactionReceipt); + } catch (error: any) { + if (notificationId) { + notification.remove(notificationId); + } + console.error("⚡️ ~ file: useTransactor.ts ~ error", error); + const message = getParsedError(error); + notification.error(message); + } + + return transactionHash; + }; + + return result; +}; diff --git a/packages/nextjs/next-env.d.ts b/packages/nextjs/next-env.d.ts new file mode 100644 index 0000000..4f11a03 --- /dev/null +++ b/packages/nextjs/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/packages/nextjs/next.config.js b/packages/nextjs/next.config.js new file mode 100644 index 0000000..3fb3552 --- /dev/null +++ b/packages/nextjs/next.config.js @@ -0,0 +1,18 @@ +// @ts-check + +/** @type {import('next').NextConfig} */ +const nextConfig = { + reactStrictMode: true, + typescript: { + ignoreBuildErrors: process.env.NEXT_PUBLIC_IGNORE_BUILD_ERROR === "true", + }, + eslint: { + ignoreDuringBuilds: process.env.NEXT_PUBLIC_IGNORE_BUILD_ERROR === "true", + }, + webpack: config => { + config.resolve.fallback = { fs: false, net: false, tls: false }; + return config; + }, +}; + +module.exports = nextConfig; diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json new file mode 100644 index 0000000..124714b --- /dev/null +++ b/packages/nextjs/package.json @@ -0,0 +1,58 @@ +{ + "name": "@se-2/nextjs", + "private": true, + "version": "0.1.0", + "scripts": { + "dev": "next dev", + "start": "next dev", + "build": "next build", + "serve": "next start", + "lint": "next lint", + "format": "prettier --write . '!(node_modules|.next|contracts)/**/*'", + "check-types": "tsc --noEmit --incremental", + "vercel": "vercel", + "vercel:yolo": "vercel --build-env NEXT_PUBLIC_IGNORE_BUILD_ERROR=true" + }, + "dependencies": { + "@ethersproject/providers": "^5.7.2", + "@heroicons/react": "^2.0.11", + "@openzeppelin/contracts": "^4.8.1", + "@rainbow-me/rainbowkit": "1.3.0", + "@uniswap/sdk-core": "^4.0.1", + "@uniswap/v2-sdk": "^3.0.1", + "axios": "^1.3.4", + "blo": "^1.0.1", + "daisyui": "^3.5.1", + "next": "^13.1.6", + "nextjs-progressbar": "^0.0.16", + "qrcode.react": "^3.1.0", + "react": "^18.2.0", + "react-copy-to-clipboard": "^5.1.0", + "react-dom": "^18.2.0", + "react-hot-toast": "^2.4.0", + "use-debounce": "^8.0.4", + "usehooks-ts": "^2.7.2", + "viem": "1.19.9", + "wagmi": "1.4.7", + "zustand": "^4.1.2" + }, + "devDependencies": { + "@trivago/prettier-plugin-sort-imports": "^4.1.1", + "@types/node": "^17.0.35", + "@types/react": "^18.0.9", + "@types/react-copy-to-clipboard": "^5.0.4", + "@typescript-eslint/eslint-plugin": "^5.39.0", + "autoprefixer": "^10.4.12", + "eslint": "^8.15.0", + "eslint-config-next": "^13.1.6", + "eslint-config-prettier": "^8.5.0", + "eslint-plugin-prettier": "^4.2.1", + "ethers": "^5.7.1", + "postcss": "^8.4.16", + "prettier": "^2.8.4", + "tailwindcss": "^3.3.3", + "type-fest": "^4.6.0", + "typescript": "^5.1.6", + "vercel": "^28.15.1" + } +} diff --git a/packages/nextjs/pages/_app.tsx b/packages/nextjs/pages/_app.tsx new file mode 100644 index 0000000..86e4fd0 --- /dev/null +++ b/packages/nextjs/pages/_app.tsx @@ -0,0 +1,64 @@ +import { useEffect, useState } from "react"; +import type { AppProps } from "next/app"; +import { RainbowKitProvider, darkTheme, lightTheme } from "@rainbow-me/rainbowkit"; +import "@rainbow-me/rainbowkit/styles.css"; +import NextNProgress from "nextjs-progressbar"; +import { Toaster } from "react-hot-toast"; +import { useDarkMode } from "usehooks-ts"; +import { WagmiConfig } from "wagmi"; +import { Footer } from "~~/components/Footer"; +import { Header } from "~~/components/Header"; +import { BlockieAvatar } from "~~/components/scaffold-eth"; +import { useNativeCurrencyPrice } from "~~/hooks/scaffold-eth"; +import { useGlobalState } from "~~/services/store/store"; +import { wagmiConfig } from "~~/services/web3/wagmiConfig"; +import { appChains } from "~~/services/web3/wagmiConnectors"; +import "~~/styles/globals.css"; + +const ScaffoldEthApp = ({ Component, pageProps }: AppProps) => { + const price = useNativeCurrencyPrice(); + const setNativeCurrencyPrice = useGlobalState(state => state.setNativeCurrencyPrice); + + useEffect(() => { + if (price > 0) { + setNativeCurrencyPrice(price); + } + }, [setNativeCurrencyPrice, price]); + + return ( + <> +
    +
    +
    + +
    +
    +
    + + + ); +}; + +const ScaffoldEthAppWithProviders = (props: AppProps) => { + // This variable is required for initial client side rendering of correct theme for RainbowKit + const [isDarkTheme, setIsDarkTheme] = useState(true); + const { isDarkMode } = useDarkMode(); + useEffect(() => { + setIsDarkTheme(isDarkMode); + }, [isDarkMode]); + + return ( + + + + + + + ); +}; + +export default ScaffoldEthAppWithProviders; diff --git a/packages/nextjs/pages/blockexplorer/address/[address].tsx b/packages/nextjs/pages/blockexplorer/address/[address].tsx new file mode 100644 index 0000000..aea9bf0 --- /dev/null +++ b/packages/nextjs/pages/blockexplorer/address/[address].tsx @@ -0,0 +1,195 @@ +import { useEffect, useState } from "react"; +import { useRouter } from "next/router"; +import fs from "fs"; +import { GetServerSideProps } from "next"; +import path from "path"; +import { createPublicClient, http } from "viem"; +import { hardhat } from "viem/chains"; +import { + AddressCodeTab, + AddressLogsTab, + AddressStorageTab, + PaginationButton, + TransactionsTable, +} from "~~/components/blockexplorer/"; +import { Address, Balance } from "~~/components/scaffold-eth"; +import deployedContracts from "~~/contracts/deployedContracts"; +import { useFetchBlocks } from "~~/hooks/scaffold-eth"; +import { GenericContractsDeclaration } from "~~/utils/scaffold-eth/contract"; + +type AddressCodeTabProps = { + bytecode: string; + assembly: string; +}; + +type PageProps = { + address: string; + contractData: AddressCodeTabProps | null; +}; + +const publicClient = createPublicClient({ + chain: hardhat, + transport: http(), +}); + +const AddressPage = ({ address, contractData }: PageProps) => { + const router = useRouter(); + const { blocks, transactionReceipts, currentPage, totalBlocks, setCurrentPage } = useFetchBlocks(); + const [activeTab, setActiveTab] = useState("transactions"); + const [isContract, setIsContract] = useState(false); + + useEffect(() => { + const checkIsContract = async () => { + const contractCode = await publicClient.getBytecode({ address: address }); + setIsContract(contractCode !== undefined && contractCode !== "0x"); + }; + + checkIsContract(); + }, [address]); + + const filteredBlocks = blocks.filter(block => + block.transactions.some(tx => { + if (typeof tx === "string") { + return false; + } + return tx.from.toLowerCase() === address.toLowerCase() || tx.to?.toLowerCase() === address.toLowerCase(); + }), + ); + + return ( +
    +
    + +
    +
    +
    +
    +
    +
    +
    +
    + Balance: + +
    +
    +
    +
    +
    +
    + {isContract && ( +
    + + + + +
    + )} + {activeTab === "transactions" && ( +
    + + +
    + )} + {activeTab === "code" && contractData && ( + + )} + {activeTab === "storage" && } + {activeTab === "logs" && } +
    + ); +}; + +export default AddressPage; + +async function fetchByteCodeAndAssembly(buildInfoDirectory: string, contractPath: string) { + const buildInfoFiles = fs.readdirSync(buildInfoDirectory); + let bytecode = ""; + let assembly = ""; + + for (let i = 0; i < buildInfoFiles.length; i++) { + const filePath = path.join(buildInfoDirectory, buildInfoFiles[i]); + + const buildInfo = JSON.parse(fs.readFileSync(filePath, "utf8")); + + if (buildInfo.output.contracts[contractPath]) { + for (const contract in buildInfo.output.contracts[contractPath]) { + bytecode = buildInfo.output.contracts[contractPath][contract].evm.bytecode.object; + assembly = buildInfo.output.contracts[contractPath][contract].evm.bytecode.opcodes; + break; + } + } + + if (bytecode && assembly) { + break; + } + } + + return { bytecode, assembly }; +} + +export const getServerSideProps: GetServerSideProps = async context => { + const address = (context.params?.address as string).toLowerCase(); + const contracts = deployedContracts as GenericContractsDeclaration | null; + const chainId = hardhat.id; + let contractPath = ""; + + const buildInfoDirectory = path.join( + __dirname, + "..", + "..", + "..", + "..", + "..", + "..", + "hardhat", + "artifacts", + "build-info", + ); + + if (!fs.existsSync(buildInfoDirectory)) { + throw new Error(`Directory ${buildInfoDirectory} not found.`); + } + + const deployedContractsOnChain = contracts ? contracts[chainId] : {}; + for (const [contractName, contractInfo] of Object.entries(deployedContractsOnChain)) { + if (contractInfo.address.toLowerCase() === address) { + contractPath = `contracts/${contractName}.sol`; + break; + } + } + + if (!contractPath) { + // No contract found at this address + return { props: { address, contractData: null } }; + } + + const { bytecode, assembly } = await fetchByteCodeAndAssembly(buildInfoDirectory, contractPath); + + return { props: { address, contractData: { bytecode, assembly } } }; +}; diff --git a/packages/nextjs/pages/blockexplorer/index.tsx b/packages/nextjs/pages/blockexplorer/index.tsx new file mode 100644 index 0000000..a5ee43c --- /dev/null +++ b/packages/nextjs/pages/blockexplorer/index.tsx @@ -0,0 +1,62 @@ +import { useEffect } from "react"; +import type { NextPage } from "next"; +import { hardhat } from "viem/chains"; +import { PaginationButton } from "~~/components/blockexplorer/PaginationButton"; +import { SearchBar } from "~~/components/blockexplorer/SearchBar"; +import { TransactionsTable } from "~~/components/blockexplorer/TransactionsTable"; +import { useFetchBlocks } from "~~/hooks/scaffold-eth"; +import { useTargetNetwork } from "~~/hooks/scaffold-eth/useTargetNetwork"; +import { notification } from "~~/utils/scaffold-eth"; + +const Blockexplorer: NextPage = () => { + const { blocks, transactionReceipts, currentPage, totalBlocks, setCurrentPage, error } = useFetchBlocks(); + const { targetNetwork } = useTargetNetwork(); + + useEffect(() => { + if (targetNetwork.id === hardhat.id && error) { + notification.error( + <> +

    Cannot connect to local provider

    +

    + - Did you forget to run yarn chain ? +

    +

    + - Or you can change targetNetwork in{" "} + scaffold.config.ts +

    + , + ); + } + + if (targetNetwork.id !== hardhat.id) { + notification.error( + <> +

    + targeNetwork is not localhost +

    +

    + - You are on {targetNetwork.name} .This + block explorer is only for localhost. +

    +

    + - You can use{" "} + + {targetNetwork.blockExplorers?.default.name} + {" "} + instead +

    + , + ); + } + }, [error, targetNetwork]); + + return ( +
    + + + +
    + ); +}; + +export default Blockexplorer; diff --git a/packages/nextjs/pages/blockexplorer/transaction/[txHash].tsx b/packages/nextjs/pages/blockexplorer/transaction/[txHash].tsx new file mode 100644 index 0000000..5a03c2c --- /dev/null +++ b/packages/nextjs/pages/blockexplorer/transaction/[txHash].tsx @@ -0,0 +1,149 @@ +import { useEffect, useState } from "react"; +import { useRouter } from "next/router"; +import type { NextPage } from "next"; +import { Transaction, TransactionReceipt, formatEther, formatUnits } from "viem"; +import { hardhat } from "viem/chains"; +import { usePublicClient } from "wagmi"; +import { Address } from "~~/components/scaffold-eth"; +import { useTargetNetwork } from "~~/hooks/scaffold-eth/useTargetNetwork"; +import { decodeTransactionData, getFunctionDetails } from "~~/utils/scaffold-eth"; +import { replacer } from "~~/utils/scaffold-eth/common"; + +const TransactionPage: NextPage = () => { + const client = usePublicClient({ chainId: hardhat.id }); + + const router = useRouter(); + const { txHash } = router.query as { txHash?: `0x${string}` }; + const [transaction, setTransaction] = useState(); + const [receipt, setReceipt] = useState(); + const [functionCalled, setFunctionCalled] = useState(); + + const { targetNetwork } = useTargetNetwork(); + + useEffect(() => { + if (txHash) { + const fetchTransaction = async () => { + const tx = await client.getTransaction({ hash: txHash }); + const receipt = await client.getTransactionReceipt({ hash: txHash }); + + const transactionWithDecodedData = decodeTransactionData(tx); + setTransaction(transactionWithDecodedData); + setReceipt(receipt); + + const functionCalled = transactionWithDecodedData.input.substring(0, 10); + setFunctionCalled(functionCalled); + }; + + fetchTransaction(); + } + }, [client, txHash]); + + return ( +
    + + {transaction ? ( +
    +

    Transaction Details

    {" "} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + Transaction Hash: + {transaction.hash}
    + Block Number: + {Number(transaction.blockNumber)}
    + From: + +
    +
    + To: + + {!receipt?.contractAddress ? ( + transaction.to &&
    + ) : ( + + Contract Creation: +
    + + )} +
    + Value: + + {formatEther(transaction.value)} {targetNetwork.nativeCurrency.symbol} +
    + Function called: + +
    + {functionCalled === "0x" ? ( + "This transaction did not call any function." + ) : ( + <> + {getFunctionDetails(transaction)} + {functionCalled} + + )} +
    +
    + Gas Price: + {formatUnits(transaction.gasPrice || 0n, 9)} Gwei
    + Data: + +