diff --git a/public/content/developers/tutorials/creating-a-wagmi-ui-for-your-contract/index.md b/public/content/developers/tutorials/creating-a-wagmi-ui-for-your-contract/index.md index e4cd858de94..4613c5a35a3 100644 --- a/public/content/developers/tutorials/creating-a-wagmi-ui-for-your-contract/index.md +++ b/public/content/developers/tutorials/creating-a-wagmi-ui-for-your-contract/index.md @@ -9,50 +9,47 @@ lang: en sidebarDepth: 3 --- -You found a feature we need in the Ethereum ecosystem. You wrote the smart contracts to implement it, and maybe even some related code that runs offchain. This is great! Unfortunately, without a user interface you aren't going to have any users, and the last time you wrote a web site people used dial-up modems and JavaScript was new. +You found a feature we need in the Ethereum ecosystem. You wrote the smart contracts to implement it, and maybe even some related code that runs offchain. This is great! Unfortunately, without a user interface you aren't going to have any users, and the last time you wrote a website people used dial-up modems and JavaScript was new. -This article is for you. I assume you know programming, and maybe a bit of JavaScript and HTML, but that your user interface skills are rusty and out of date. Together we will go over a simple modern application so you'll see how it's done these days. +This article is for you. I assume you know programming, and maybe a bit of JavaScript and HTML, but that your user interface skills are rusty and outdated. Together we will go over a simple modern application so you'll see how it's done these days. ## Why is this important {#why-important} -In theory, you could just have people use [Etherscan](https://holesky.etherscan.io/address/0x432d810484add7454ddb3b5311f0ac2e95cecea8#writeContract) or [Blockscout](https://eth-holesky.blockscout.com/address/0x432d810484AdD7454ddb3b5311f0Ac2E95CeceA8?tab=write_contract) to interact with your contracts. That will be great for the experienced Ethereans. But we are trying to serve [another billion people](https://blog.ethereum.org/2021/05/07/ethereum-for-the-next-billion). This won't happen without a great user experience, and a friendly user interface is a big part of that. +In theory, you could just have people use [Etherscan](https://sepolia.etherscan.io/address/0xC87506C66c7896366b9E988FE0aA5B6dDE77CFfA#readContract) or [Blockscout](https://eth-sepolia.blockscout.com/address/0xC87506C66c7896366b9E988FE0aA5B6dDE77CFfA?tab=read_write_contract) to interact with your contracts. That is great for the experienced Ethereans. But we are trying to serve [another billion people](https://blog.ethereum.org/2021/05/07/ethereum-for-the-next-billion). This won't happen without a great user experience, and a friendly user interface is a big part of that. ## Greeter application {#greeter-app} -There is a lot of theory behind for a modern UI works, and [a lot of good sites](https://react.dev/learn/thinking-in-react) [that explain it](https://wagmi.sh/core/getting-started). Instead of repeating the fine work done by those sites, I'm going to assume you prefer to learn by doing and start with an application you can play with. You still need the theory to get things done, and we'll get to it - we'll just go source file by source file, and discuss things as we get to them. +There is a lot of theory behind how modern UI works, and [a lot of good sites](https://react.dev/learn/thinking-in-react) [that explain it](https://wagmi.sh/core/getting-started). Instead of repeating the fine work done by those sites, I'm going to assume you prefer to learn by doing and start with an application you can play with. You still need the theory to get things done, and we'll get to it - we'll just go source file by source file, and discuss things as we get to them. ### Installation {#installation} -1. If necessary, add [the Holesky blockchain](https://chainlist.org/?search=holesky&testnets=true) to your wallet and [get test ETH](https://www.holeskyfaucet.io/). +1. The application uses the [Sepolia](https://sepolia.dev/) test network. If necessary, [get Sepolia test ETH](/developers/docs/networks/#sepolia) and [add Sepolia to your wallet](https://chainlist.org/chain/11155111). -1. Clone the github repository. +2. Clone the GitHub repository and install the necessary packages. ```sh - git clone https://github.com/qbzzt/20230801-modern-ui.git + git clone https://github.com/qbzzt/260301-modern-ui-web3.git + cd 260301-modern-ui-web3 + npm install ``` -1. Install the necessary packages. +3. The application uses free access points, which have performance limitations. If you want to use a [Node as a service](/developers/docs/nodes-and-clients/nodes-as-a-service/) provider, replace the URLs in [`src/wagmi.ts`](#wagmi-ts). - ```sh - cd 20230801-modern-ui - pnpm install - ``` - -1. Start the application. +4. Start the application. ```sh - pnpm dev + npm run dev ``` -1. Browse to the URL shown by the application. In most cases, that is [http://localhost:5173/](http://localhost:5173/). +5. Browse to the URL shown by the application. In most cases, that is [http://localhost:5173/](http://localhost:5173/). -1. You can see the contract source code, a slightly modified version of Hardhat's Greeter, [on a blockchain explorer](https://eth-holesky.blockscout.com/address/0x432d810484AdD7454ddb3b5311f0Ac2E95CeceA8?tab=contract). +6. You can see the contract source code, a modified version of Hardhat's Greeter, [on a blockchain explorer](https://eth-sepolia.blockscout.com/address/0xC87506C66c7896366b9E988FE0aA5B6dDE77CFfA?tab=contract_code). ### File walk through {#file-walk-through} #### `index.html` {#index-html} -This file is standard HTML boilerplate except for this line, which imports the script file. +This file is a standard HTML boilerplate except for this line, which imports the script file. ```html @@ -60,25 +57,43 @@ This file is standard HTML boilerplate except for this line, which imports the s #### `src/main.tsx` {#main-tsx} -The file extension tells us that this file is a [React component](https://www.w3schools.com/react/react_components.asp) written in [TypeScript](https://www.typescriptlang.org/), an extension of JavaScript that supports [type checking](https://en.wikipedia.org/wiki/Type_system#Type_checking). TypeScript is compiled into JavaScript, so we can use it for client-side execution. +The file extension indicates that this is a [React component](https://www.w3schools.com/react/react_components.asp) written in [TypeScript](https://www.typescriptlang.org/), an extension of JavaScript that supports [type checking](https://en.wikipedia.org/wiki/Type_system#Type_checking). TypeScript is compiled to JavaScript, so we can use it on the client side. + +This file is mostly explained in case you are interested. Usually you do not modify this file, but [`src/App.tsx`](#app-tsx) and the files it imports. ```tsx -import '@rainbow-me/rainbowkit/styles.css' -import { RainbowKitProvider } from '@rainbow-me/rainbowkit' -import * as React from 'react' -import * as ReactDOM from 'react-dom/client' -import { WagmiConfig } from 'wagmi' -import { chains, config } from './wagmi' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import React from 'react' +import ReactDOM from 'react-dom/client' +import { WagmiProvider } from 'wagmi' ``` Import the library code we need. ```tsx -import { App } from './App' +import App from './App.tsx' ``` Import the React component that implements the application (see below). +```tsx +import { config } from './wagmi.ts' +``` + +Import the [wagmi](https://wagmi.sh/) configuration, which includes the blockchain configuration. + +```tsx +const queryClient = new QueryClient() +``` + +Creates a new instance of [React Query’s](https://tanstack.com/query/latest/docs/framework/react/overview) cache manager. This object will store: + +- Cached RPC calls +- Contract reads +- Background refetching state + +We need the cache manager because wagmi v3 uses React Query internally. + ```tsx ReactDOM.createRoot(document.getElementById('root')!).render( ``` @@ -92,16 +107,16 @@ Create the root React component. The parameter to `render` is [JSX](https://www. The application is going inside [a `React.StrictMode` component](https://react.dev/reference/react/StrictMode). This component tells the React library to insert additional debugging checks, which is useful during development. ```tsx - + ``` -The application is also inside [a `WagmiConfig` component](https://wagmi.sh/react/api/WagmiProvider). [The wagmi (we are going to make it) library](https://wagmi.sh/) connects the React UI definitions with [the viem library](https://viem.sh/) for writing an Ethereum decentralized application. +The application is also inside [a `WagmiProvider` component](https://wagmi.sh/react/api/WagmiProvider). [The wagmi (we are going to make it) library](https://wagmi.sh/) connects the React UI definitions with [the viem library](https://viem.sh/) for writing an Ethereum decentralized application. ```tsx - + ``` -And finally, [a `RainbowKitProvider` component](https://www.rainbowkit.com/). This component handles logging on and the communication between the wallet and the application. +And finally, add a React Query provider so any application component can use cached queries. ```tsx @@ -110,8 +125,8 @@ And finally, [a `RainbowKitProvider` component](https://www.rainbowkit.com/). Th Now we can have the component for the application, which actually implements the UI. The `/>` at the end of the component tells React that this component doesn't have any definitions inside it, as per the XML standard. ```tsx - - + + , ) ``` @@ -121,67 +136,186 @@ Of course, we have to close off the other components. #### `src/App.tsx` {#app-tsx} ```tsx -import { ConnectButton } from '@rainbow-me/rainbowkit' -import { useAccount } from 'wagmi' -import { Greeter } from './components/Greeter' +import { + useConnect, + useConnection, + useDisconnect, + useSwitchChain +} from 'wagmi' + +import { useEffect } from 'react' +import { Greeter } from './Greeter' +``` + +Import the libraries we need, as well as [the `Greeter` component](#greeter-tsx). + +```tsx +const SEPOLIA_CHAIN_ID = 11155111 +``` + +The Sepolia chain ID. -export function App() { +``` +function App() { ``` -This is the standard way to create a React component - define a function that is called every time it needs to be rendered. This function typically has some TypeScript or JavaScript code on top, followed by a `return` statement that returns the JSX code. +This is the standard way to create a React component: define a function that is called whenever it needs to be rendered. This function typically contains TypeScript or JavaScript code, followed by a `return` statement that returns the JSX code. ```tsx - const { isConnected } = useAccount() + const connection = useConnection() ``` -Here we use [`useAccount`](https://wagmi.sh/react/api/hooks/useAccount) to check if we are connected to a blockchain through a wallet or not. +Use [`useConnection`](https://wagmi.sh/react/api/hooks/useConnection) to get information related to the current connection, such as the address and `chainId`. -By convention, in React functions called `use...` are [hooks](https://www.w3schools.com/react/react_hooks.asp) that return some kind of data. When you use such hooks, not only does your component get the data, but when that data changes the component is re-rendered with the updated information. +By convention, in React functions called `use...` are [hooks](https://www.w3schools.com/react/react_hooks.asp). These functions don't just return data to the component; they also ensure it is re-rendered (the component function is executed again, and its output replaces the previous one in the HTML) when that data changes. + +```tsx + const { connectors, connect, status, error } = useConnect() +``` + +Use [`useConnect`](https://wagmi.sh/react/api/hooks/useConnect) to get information about the wallet connection. + +```tsx + const { disconnect } = useDisconnect() +``` + +[This hook](https://wagmi.sh/react/api/hooks/useDisconnect) gives us the function to disconnect from the wallet. + +```tsx + const { switchChain } = useSwitchChain() +``` + +[This hook](https://wagmi.sh/react/api/hooks/useSwitchChain) lets us switch chains. + +```tsx + useEffect(() => { +``` + +The React hook [`useEffect`](https://react.dev/reference/react/useEffect) lets you run a function whenever the value of a variable changes to synchronize an external system. + +```tsx + if (connection.status === 'connected' && + connection.chainId !== SEPOLIA_CHAIN_ID + ) { + switchChain({ chainId: SEPOLIA_CHAIN_ID }) + } +``` + +If we are connected, but not to the Sepolia blockchain, switch to Sepolia. + +```tsx + }, [connection.status, connection.chainId]) +``` + +Rerun the function every time either the connection status or the connection chainId changes. ```tsx return ( <> ``` -The JSX of a React component _has_ to return one component. When we have multiple components and we don't have anything that wraps up "naturally" we use an empty component (`<> ... `) to make them into a single component. +The JSX of a React component _must_ return a single HTML component. When we have multiple components and don't need a container to wrap them all, we use an empty component (`<> ... `) to combine them into a single component. ```tsx -

Greeter

- +

Connection

+
+ status: {connection.status} +
+ addresses: {JSON.stringify(connection.addresses)} +
+ chainId: {connection.chainId} +
``` -We get [the `ConnectButton` component](https://www.rainbowkit.com/docs/connect-button) from RainbowKit. When we are not connected, it gives us a `Connect Wallet` button that opens a modal that explains wallets and lets you choose which one you use. When we are connected, it displays the blockchain we use, our account address, and our ETH balance. We can use these displays to switch network or to disconnect. +Provide information about the current connection. Within JSX, `{}` means to evaluate the expression as JavaScript. ```tsx - {isConnected && ( + {connection.status === 'connected' && ( ``` -When we need to insert actual JavaScript (or TypeScript that will be compiled to JavaScript) into a JSX, we use brackets (`{}`). - -The syntax `a && b` is short for [`a ? b : a`](https://www.w3schools.com/react/react_es6_ternary.asp). That is, if `a` is true it evaluates to `b` and otherwise it evaluates to `a` (which can be `false`, `0`, etc). This is an easy way to tell React that a component should only be displayed if a certain condition is fulfilled. +The syntax `{ && } means "if the condition is `true`, evaluate to the value; if it isn't, evaluate to `false`". -In this case, we only want to show the user `Greeter` if the user is connected to a blockchain. +This is the standard way to put if statements inside JSX. ```tsx +
+
+``` + +JSX follows the XML standard, which is stricter than HTML. If a tag does not have a corresponding end tag, it _must_ have a slash (`/`) at the end to terminate it. + +Here we have two such tags, `` (which actually contains the HTML code that talks to the contract) and [`
` for a horizontal line](https://www.w3schools.com/tags/tag_hr.asp). + +```tsx + +
+ )} +``` + +If the user clicks this button, call the `disconnect` function. + +```tsx + {connection.status !== 'connected' && ( +``` + +If we are _not_ connected, show the necessary options to connect to the wallet. + +```tsx +
+

Connect

+ {connectors.map((connector) => ( +``` + +In `connectors` we have a list of connectors. We use [`map`](https://www.w3schools.com/jsref/jsref_map.asp) to turn it into a list of JSX buttons to display. + +```tsx + + ))} +``` + +The connector buttons. + +```tsx +
{status}
+
{error?.message}
+
)} - - ) -} ``` -#### `src/components/Greeter.tsx` {#greeter-tsx} +Provide additional information. The expression syntax `?.` tells JavaScript that if the variable is defined, evaluate to that field. If the variable is not defined, then this expression evaluates to `undefined`. + +The expression `error.message`, when there is no error, would raise an exception. Using `error?.message` lets us avoid this issue. + +#### `src/Greeter.tsx` {#greeter-tsx} -This file contains most of the UI functionality. It includes definitions that would normally be in multiple files, but as this is a tutorial the program is optimized for being easy to understand the first time, rather than performance or ease of maintenance. +This file contains most of the UI functionality. It includes definitions that would normally be in multiple files, but as this is a tutorial, the program is optimized for being easy to understand the first time, rather than performance or ease of maintenance. ```tsx -import { useState, ChangeEventHandler } from 'react' -import { useNetwork, +import { + useState, + useEffect, + } from 'react' +import { useChainId, + useAccount, useReadContract, - usePrepareContractWrite, - useContractWrite, - useContractEvent - } from 'wagmi' + useWriteContract, + useWatchContractEvent, + useSimulateContract + } from 'wagmi' ``` We use these library functions. Again, they are explained below where they are used. @@ -194,14 +328,16 @@ import { AddressType } from 'abitype' ```tsx let greeterABI = [ - . - . - . + { "type": "function", "name": "greet", ... }, + { "type": "function", "name": "setGreeting", ... }, + { "type": "event", "name": "SetGreeting", ... }, ] as const // greeterABI ``` The ABI for the `Greeter` contract. -If you are developing the contracts and UI at the same time you'd normally put them in the same repository and use the ABI generated by the Solidity compiler as a file in your application. However, this is not necessary here because the contract is already developed and not going to change. +If you are developing the contracts and UI at the same time, you'd normally put them in the same repository and use the ABI generated by the Solidity compiler as a file in your application. However, this is not necessary here because the contract is already developed and will not change. + +We use [`as const`](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-4.html#const-assertions) to tell TypeScript that this is a _real_ constant. Normally, when you specify in JavaScript `const x = {"a": 1}`, you can change the value in `x`, you just can't assign to it. ```tsx type AddressPerBlockchainType = { @@ -209,38 +345,57 @@ type AddressPerBlockchainType = { } ``` -TypeScript is strongly typed. We use this definition to specify the address in which the `Greeter` contract is deployed on different chains. The key is a number (the chainId), and the value is an `AddressType` (an address). +TypeScript is strongly typed. We use this definition to specify the address where the `Greeter` contract is deployed across different chains. The key is a number (the chainId), and the value is an `AddressType` (an address). ```tsx -const contractAddrs: AddressPerBlockchainType = { - // Holesky - 17000: '0x432d810484AdD7454ddb3b5311f0Ac2E95CeceA8', - +const contractAddrs : AddressPerBlockchainType = { // Sepolia - 11155111: '0x7143d5c190F048C8d19fe325b748b081903E3BF0' + 11155111: '0xC87506C66c7896366b9E988FE0aA5B6dDE77CFfA' } ``` -The address of the contract on the two supported networks: [Holesky](https://eth-holesky.blockscout.com/address/0x432d810484AdD7454ddb3b5311f0Ac2E95CeceA8?tab=contact_code) and [Sepolia](https://eth-sepolia.blockscout.com/address/0x7143d5c190F048C8d19fe325b748b081903E3BF0?tab=contact_code). +The address of the contract on [Sepolia](https://eth-sepolia.blockscout.com/address/0xC87506C66c7896366b9E988FE0aA5B6dDE77CFfA?tab=contract). + +##### `Timer` component {#timer-component} -Note: There is actually a third definition, for Redstone Holesky, it will be explained below. +The `Timer` component shows the number of seconds since a given time. This is important for usability purposes. When users do something, they expect an immediate reaction. In blockchains, this is often impossible because nothing happens until a transaction is placed in a block. One solution is to show how long it has been since the user performed the action, so the user can decide whether the time required is reasonable. ```tsx -type ShowObjectAttrsType = { - name: string, - object: any +type TimerProps = { + lastUpdate: Date } ``` -This type is used as a parameter to the `ShowObject` component (explained later). It includes the name of the object and its value, which are displayed for debugging purposes. +The `Timer` component takes one parameter, `lastUpdate`, which is the time of the last action. + +```tsx +const Timer = ({ lastUpdate }: TimerProps) => { + const [_, setNow] = useState(new Date()) +``` + +We need to have state (a variable tied to the component) and update it for the component to work correctly. But we never need to read it, so don't bother to do a variable. + +```tsx + useEffect(() => { + const id = setInterval(() => setNow(new Date()), 1000) + return () => clearInterval(id) + }, []) +``` + +The [`setInterval`](https://www.w3schools.com/jsref/met_win_setinterval.asp) function lets us schedule a function to run periodically. In this case, every second. The function calls `setNow` to update the state, so the `Timer` component will be re-rendered. We wrap this inside [`useEffect`](https://react.dev/reference/react/useEffect) with an empty dependency list so it'll happen just once, rather than each time the component is rendered. ```tsx -type ShowGreetingAttrsType = { - greeting: string | undefined + const secondsSinceUpdate = Math.floor( + (Date.now() - lastUpdate.getTime()) / 1000 + ) + + return ( + {secondsSinceUpdate} seconds ago + ) } ``` -At any moment in time we may either know what the greeting is (because we read it from the blockchain) or not know (because we haven't received it yet). So it is useful to have a type that can be either a string or nothing. +Calculate the number of seconds since the last update and return it. ##### `Greeter` component {#greeter-component} @@ -248,35 +403,34 @@ At any moment in time we may either know what the greeting is (because we read i const Greeter = () => { ``` -Finally, we get the define the component. +Finally, we get to define the component. ```tsx - const { chain } = useNetwork() + const chainId = useChainId() + const account = useAccount() ``` -Information about the chain we are using, courtesy of [wagmi](https://wagmi.sh/react/hooks/useNetwork). -Because this is a hook (`use...`), every time this information changes the component gets redrawn. +Information about the chain and account we are using, courtesy of [wagmi](https://wagmi.sh/). Because this is a hook (`use...`), the component is re-rendered whenever this information changes. ```tsx - const greeterAddr = chain && contractAddrs[chain.id] + const greeterAddr = chainId && contractAddrs[chainId] ``` -The address of the Greeter contract, which varies by chain (and which is `undefined` if we don't have chain information or we are on a chain without that contract). +The address of the Greeter contract, which is `undefined` if we don't have chain information, or we are on a chain without that contract. ```tsx const readResults = useReadContract({ address: greeterAddr, abi: greeterABI, - functionName: "greet" , // No arguments - watch: true + functionName: "greet", // No arguments }) ``` -[The `useReadContract` hook](https://wagmi.sh/react/api/hooks/useReadContract) reads information from a contract. You can see exactly what information it returns expand `readResults` in the UI. In this case we want it to keep looking so we'll be informed when the greeting changes. - -**Note:** We could listen to [`setGreeting` events](https://eth-holesky.blockscout.com/address/0x432d810484AdD7454ddb3b5311f0Ac2E95CeceA8?tab=logs) to know when the greeting changes and update that way. However, while it may be more efficient, it will not apply in all cases. When the user switches to a different chain the greeting also changes, but that change is not accompanied by an event. We could have one part of the code listening for events and another to identify chain changes, but that would be more complicated than just setting [the `watch` parameter](https://wagmi.sh/react/api/hooks/useReadContract#watch-optional). +[The `useReadContract` hook](https://wagmi.sh/react/api/hooks/useReadContract) calls the `greet` function of [the contract](https://eth-sepolia.blockscout.com/address/0xC87506C66c7896366b9E988FE0aA5B6dDE77CFfA?tab=contract). ```tsx + const [ currentGreeting, setCurrentGreeting ] = + useState("Please wait while we fetch the greeting from the blockchain...") const [ newGreeting, setNewGreeting ] = useState("") ``` @@ -290,277 +444,302 @@ The `useState` hook returns a list with two values: In this case, we are using a state variable for the new greeting the user wants to set. ```tsx - const greetingChange : ChangeEventHandler = (evt) => - setNewGreeting(evt.target.value) + const [ lastSetterAddress, setLastSetterAddress ] = useState("") ``` -This is the event handler for when the new greeting input field changes. The type, [`ChangeEventHandler`](https://react-typescript-cheatsheet.netlify.app/docs/basic/getting-started/forms_and_events/), specifies that this is handler for a value change of an HTML input element. The `` part is used because this is a [generic type](https://www.w3schools.com/typescript/typescript_basic_generics.php). +If multiple users are using the same contract at the same time, they might overwrite each other's greetings. This would look to the users as if the application is malfunctioning. If the application shows who last set the greeting, the user will know it was someone else and that the application is working correctly. ```tsx - const preparedTx = usePrepareContractWrite({ - address: greeterAddr, - abi: greeterABI, - functionName: 'setGreeting', - args: [ newGreeting ] - }) - const workingTx = useContractWrite(preparedTx.config) + const [ status, setStatus ] = useState("") + const [ statusTime, setStatusTime ] = useState(new Date()) ``` -This is the process to submit a blockchain transaction from the client perspective: +Users like to see that their actions have an immediate effect. However, on a blockchain, this is not the case. These state variables let us at least display something to users so they'll know their action is in progress. -1. Send the transaction to a node in the blockchain using [`eth_estimateGas`](https://docs.alchemy.com/reference/eth-estimategas). -2. Wait for a response from the node. -3. When the response is received, ask the user to sign the transaction through the wallet. This step _has_ to happen after the node response is received because the user is shown the gas cost of the transaction before signing it. -4. Wait for the user for approve. -5. Send the transaction again, this time using [`eth_sendRawTransaction`](https://docs.alchemy.com/reference/eth-sendrawtransaction). +```tsx + useEffect(() => { + if (readResults.data) { + setCurrentGreeting(readResults.data) + setStatus("Greeting fetched from blockchain") + } + }, [readResults.data]) +``` + +If `readResults` above changes the data and it's not set to a false value (`undefined`, for example), update the current greeting to the one read from the blockchain. Also, update the status. -Step 2 is likely to take a perceptible amount of time, during which users would wonder if their command was really received by the user interface and why they aren't being asked to sign the transaction already. That makes for bad user experience (UX). +```tsx + useWatchContractEvent({ + address: greeterAddr, + abi: greeterABI, + eventName: 'SetGreeting', + chainId, +``` -The solution is to use [prepare hooks](https://wagmi.sh/react/prepare-hooks). Every time that a parameter changes, immediately send the node the `eth_estimateGas` request. Then, when the user actually wants to send the transaction (in this case by pressing **Update greeting**), the gas cost is known and the user can see the wallet page immediately. +Listen to `SetGreeting` events. ```tsx - return ( + enabled: !!greeterAddr, ``` -Now we can finally create the actual HTML to return. +`!!` means that if the value is `false`, or a value that evaluates as false, such as `undefined`, `0`, or an empty string, the expression overall is `false`. For any other value, it is `true`. It's a way to convert values to booleans, because if there is no `greeterAddr`, we don't want to listen to events. ```tsx - <> -

Greeter

- { - !readResults.isError && !readResults.isLoading && - - } -
+ onLogs: logs => { + const greetingFromContract = logs[0].args.greeting + setCurrentGreeting(greetingFromContract) + setLastSetterAddress(logs[0].args.sender) + updateStatus("Greeting updated by event") + }, + }) ``` -Create a `ShowGreeting` component (explained below), but only if the greeting was read successfully from the blockchain. +When we see logs (which happens when we see a new event), it means that the greeting has been modified. In that case, we can update `currentGreeting` and `lastSetterAddress` to the new values. Also, we want to update the status display. ```tsx - + const updateStatus = (newStatus: string) => { + setStatus(newStatus) + setStatusTime(new Date()) + } ``` -This is the input text field where the user can set a new greeting. Every time the user presses a key, we call `greetingChange` which calls `setNewGreeting`. As `setNewGreeting` comes from the `useState` hook, it causes the `Greeter` component to be rendered again. This means that: +When we update the status we want to do two things: -- We need to specify `value` to keep the value of the new greeting, because otherwise it would turn back into the default, the empty string. -- `usePrepareContractWrite` is called every time `newGreeting` changes, which means it is always going to have the latest `newGreeting` in the prepared transaction. +1. Update the status string (`status`) +2. Update the time of last status update (`statusTime`) to now. ```tsx - + const greetingChange = (evt) => + setNewGreeting(evt.target.value) ``` -If there is no `workingTx.write` then we are still waiting for information necessary for sending the greeting update, so the button is disabled. If there is a `workingTx.write` value then that is the function to call to send the transaction. +This is the event handler for changes to the new greeting input field. We could specify the type of the `evt` parameter, but TypeScript is a type optional language. As this function is called only once, in an HTML event handler, I don't think it is necessary. ```tsx -
- - - - - ) -} + const { writeContractAsync } = useWriteContract() +``` + +The function to write to a contract. It is similar to [`writeContracts`](https://wagmi.sh/core/api/actions/writeContracts#writecontracts), but enables better status updates. + +```tsx + const simulation = useSimulateContract({ + address: greeterAddr, + abi: greeterABI, + functionName: 'setGreeting', + args: [newGreeting], + account: account.address + }) ``` -Finally, to help you see what we're doing, show the three objects we use: +This is the process to submit a blockchain transaction from the client perspective: -- `readResults` -- `preparedTx` -- `workingTx` +1. Send the transaction to a node in the blockchain using [`eth_estimateGas`](https://docs.alchemy.com/reference/eth-estimategas). +2. Wait for a response from the node. +3. When the response is received, ask the user to sign the transaction through the wallet. This step _has_ to happen after the node response is received because the user is shown the gas cost of the transaction before signing it. +4. Wait for the user to approve. +5. Send the transaction again, this time using [`eth_sendRawTransaction`](https://docs.alchemy.com/reference/eth-sendrawtransaction). -##### `ShowGreeting` component {#showgreeting-component} +Step 2 is likely to take a perceptible amount of time, during which users may wonder whether their command was received by the user interface and why they aren't being asked to sign the transaction yet. That creates a poor user experience (UX). -This component shows +One solution is to send out `eth_estimateGas` every time that a parameter changes. Then, when the user actually wants to send the transaction (in this case by pressing **Update greeting**), the gas cost is known, and the user can see the wallet page immediately. ```tsx -const ShowGreeting = (attrs : ShowGreetingAttrsType) => { + return ( ``` -A component function receives a parameter with all the attributes of the component. +Now we can finally create the actual HTML to return. ```tsx - return {attrs.greeting} -} + <> +

Greeter

+ {currentGreeting} ``` -##### `ShowObject` component {#showobject-component} - -For information purposes, we use the `ShowObject` component to show the important objects (`readResults` for reading the greeting and `preparedTx` and `workingTx` for transactions we create). +Show the current greeting. ```tsx -const ShowObject = (attrs: ShowObjectAttrsType ) => { - const keys = Object.keys(attrs.object) - const funs = keys.filter(k => typeof attrs.object[k] == "function") - return <> -
+ {lastSetterAddress && ( +

Last updated by { + lastSetterAddress === account.address ? "you" : lastSetterAddress + }

+ )} ``` -We don't want to clutter the UI with all the information, so to make it possible to view them or close them, we use a [`details`](https://www.w3schools.com/tags/tag_details.asp) tag. +If we know who set the greeting last, display that information. `Greeter` does not keep track of this information, and we don't want to look back for `SetGreeting` events, so we only get it once the greeting is changed while we are running. ```tsx - {attrs.name} -
-        {JSON.stringify(attrs.object, null, 2)}
+      
+ +
``` -Most of the fields are displayed using [`JSON.stringify`](https://www.w3schools.com/js/js_json_stringify.asp). +This is the input text field where the user can set a new greeting. Every time the user presses a key, we call `greetingChange`, which calls `setNewGreeting`. Since `setNewGreeting` comes from `useState`, it causes the `Greeter` component to be re-rendered. This means that: + +- We need to specify `value` to keep the value of the new greeting, because otherwise it would turn back into the default, the empty string. +- `simulation` is also updated every time `newGreeting` changes, which means that we'll get a simulation with the correct greeting. This could be relevant because the gas cost depends on the size of the call data, which depends on the length of the string. ```tsx -
- { funs.length > 0 && - <> - Functions: -
    + + ``` -React requires tags in the [DOM Tree](https://www.w3schools.com/js/js_htmldom.asp) to have distinct identifiers. This means that children of the same tag (in this case, [the unordered list](https://www.w3schools.com/tags/tag_ul.asp)), need different `key` attributes. +`writeContractAsync` only returns after the transaction is actually sent. This lets us show the user how long the transaction has been waiting to be included in the blockchain. ```tsx -
- - } -
- +

Status: {status}

+

Updated

+ + ) } ``` -End the various HTML tags. - -##### The final `export` {#the-final-export} +Show the status and how long it has been since it was updated. -```tsx -export { Greeter } +``` +export {Greeter} ``` -The `Greeter` component is the one we need to export for the application. +Export the component. #### `src/wagmi.ts` {#wagmi-ts} -Finally, various definitions related to WAGMI are in `src/wagmi.ts`. I am not going to explain everything here, because most of it is boilerplate you are unlikely to need to change. - -The code here isn't exactly the same as [on github](https://github.com/qbzzt/20230801-modern-ui/blob/main/src/wagmi.ts) because later in the article we add another chain ([Redstone Holesky](https://redstone.xyz/docs/network-info)). +Finally, various definitions related to wagmi are in `src/wagmi.ts`. I am not going to explain everything here, because most of it is boilerplate you are unlikely to need to change. ```ts -import { getDefaultWallets } from '@rainbow-me/rainbowkit' -import { configureChains, createConfig } from 'wagmi' -import { holesky, sepolia } from 'wagmi/chains' +import { http, webSocket, createConfig, fallback } from 'wagmi' +import { sepolia } from 'wagmi/chains' +import { injected } from 'wagmi/connectors' + +export const config = createConfig({ + chains: [sepolia], ``` -Import the blockchains the application supports. You can see the list of supported chains [in the viem github](https://github.com/wagmi-dev/viem/tree/main/src/chains/definitions). +The wagmi configuration includes the chains supported by this application. You can see the [list of available chains](https://wagmi.sh/core/api/chains). ```ts -import { publicProvider } from 'wagmi/providers/public' - -const walletConnectProjectId = 'c96e690bb92b6311e8e9b2a6a22df575' + connectors: [ + injected(), + ], ``` -To be able to use [WalletConnect](https://walletconnect.com/) you need a project ID for your application. You can get it [on cloud.walletconnect.com](https://cloud.walletconnect.com/sign-in). +[This connector](https://wagmi.sh/core/api/connectors/injected) lets us talk to a wallet installed in the browser. ```ts -const { chains, publicClient, webSocketPublicClient } = configureChains( - [ holesky, sepolia ], - [ - publicProvider(), - ], -) + transports: { + [sepolia.id]: http() +``` -const { connectors } = getDefaultWallets({ - appName: 'My wagmi + RainbowKit App', - chains, - projectId: walletConnectProjectId, -}) +The default HTTP endpoint that comes with Viem is good enough. If we want a different URL, we can use `http("https:// hostname ")` or `webSocket("wss:// hostname ")`. -export const config = createConfig({ - autoConnect: true, - connectors, - publicClient, - webSocketPublicClient, +```ts + }, + multiInjectedProviderDiscovery: false, }) - -export { chains } ``` -### Adding another blockchain {#add-blockchain} - -These days there are a lot of [L2 scaling solution](/layer-2/), and you might want to support some that viem does not support yet. To do it, you modify `src/wagmi.ts`. These instructions explain how to add [Redstone Holesky](https://redstone.xyz/docs/network-info). - -1. Import the `defineChain` type from viem. - - ```ts - import { defineChain } from 'viem' - ``` - -1. Add the network definition. - - ```ts - const redstoneHolesky = defineChain({ - id: 17_001, - name: 'Redstone Holesky', - network: 'redstone-holesky', - nativeCurrency: { - decimals: 18, - name: 'Ether', - symbol: 'ETH', - }, - rpcUrls: { - default: { - http: ['https://rpc.holesky.redstone.xyz'], - webSocket: ['wss://rpc.holesky.redstone.xyz/ws'], - }, - public: { - http: ['https://rpc.holesky.redstone.xyz'], - webSocket: ['wss://rpc.holesky.redstone.xyz/ws'], - }, - }, - blockExplorers: { - default: { name: 'Explorer', url: 'https://explorer.holesky.redstone.xyz' }, - }, - }) - ``` - -1. Add the new chain to the `configureChains` call. +## Adding another blockchain {#add-blockchain} + +These days there are a lot of [L2 scaling solutions](https://ethereum.org/layer-2/), and you might want to support some that viem does not support yet. To do it, you modify `src/wagmi.ts`. These instructions explain how to add [Optimism Sepolia](https://chainlist.org/chain/11155420). + +1. Edit `src/wagmi.ts` + + A. Import the `defineChain` type from viem. + + ```ts + import { defineChain } from 'viem' + ``` + + B. Add the network definition. You don't really need to do this for Optimism Sepolia, [it is already in `viem`](https://github.com/wevm/viem/blob/main/src/chains/definitions/optimismSepolia.ts), but this way you learn how to add a blockchain that is not in `viem`. + + ```ts + const optimismSepolia = defineChain({ + id: 11_155_420, + name: 'OP Sepolia', + nativeCurrency: { name: 'Sepolia Ether', symbol: 'ETH', decimals: 18 }, + rpcUrls: { + default: { + http: ['https://sepolia.optimism.io'], + webSocket: ['wss://optimism-sepolia.drpc.org'], + }, + }, + blockExplorers: { + default: { + name: 'Blockscout', + url: 'https://optimism-sepolia.blockscout.com', + apiUrl: 'https://optimism-sepolia.blockscout.com/api', + } + }, + }) + ``` + + C. Add the new chain to the `createConfig` call. + + ```ts + export const config = createConfig({ + chains: [sepolia, optimismSepolia], + connectors: [ + injected(), + ], + transports: { + [optimismSepolia.id]: http(), + [sepolia.id]: http() + }, + multiInjectedProviderDiscovery: false, + }) + ``` + +2. Edit `src/App.tsx` to comment out the automatic switch to Sepolia. On a production system, you'd probably show buttons with links to each of the blockchains you support. - ```ts - const { chains, publicClient, webSocketPublicClient } = configureChains( - [ holesky, sepolia, redstoneHolesky ], - [ publicProvider(), ], - ) - ``` + ```ts + /* + useEffect(() => { + if (connection.status === 'connected' && + connection.chainId !== SEPOLIA_CHAIN_ID + ) { + switchChain({ chainId: SEPOLIA_CHAIN_ID }) + } + }, [connection.status, connection.chainId]) + */ + ``` -1. Ensure that the application knows the address for your contracts on the new network. In this case, we modify `src/components/Greeter.tsx`: +3. Edit `src/Greeter.tsx` to ensure that the application knows the address for your contracts on the new network. ```ts - const contractAddrs : AddressPerBlockchainType = { - // Holesky - 17000: '0x432d810484AdD7454ddb3b5311f0Ac2E95CeceA8', - - // Redstone Holesky - 17001: '0x4919517f82a1B89a32392E1BF72ec827ba9986D3', + const contractAddrs: AddressPerBlockchainType = { + // Optimism Sepolia + 11155420: "0x4dd85791923E9294E934271522f63875EAe5806f", // Sepolia - 11155111: '0x7143d5c190F048C8d19fe325b748b081903E3BF0' + 11155111: "0x7143d5c190F048C8d19fe325b748b081903E3BF0", } ``` +4. In your browser. + + A. Browse to [ChainList](https://chainlist.org/chain/11155420?testnets=true) and click one of buttons on the right side of the table to add the chain to your wallet. + + B. In the application, **Disconnect** and then reconnect to change the blockchain. There are nicer ways to handle this, but they'd require application changes. + ## Conclusion {#conclusion} Of course, you don't really care about providing a user interface for `Greeter`. You want to create a user interface for your own contracts. To create your own application, run these steps: @@ -568,18 +747,17 @@ Of course, you don't really care about providing a user interface for `Greeter`. 1. Specify to create a wagmi application. ```sh copy - pnpm create wagmi + npm create wagmi ``` -1. Name the application. +2. Type `y` to proceed. -1. Select **React** framework. +3. Name the application. -1. Select the **Vite** variant. +4. Select **React** framework. -1. You can [add Rainbow kit](https://www.rainbowkit.com/docs/installation#manual-setup). +5. Select the **Vite** variant. Now go and make your contracts usable for the wide world. [See here for more of my work](https://cryptodocguy.pro/). -