diff --git a/apps/base-docs/base-camp/assets/images/reading-and-displaying-data/issues-console-log.png b/apps/base-docs/base-camp/assets/images/reading-and-displaying-data/issues-console-log.png new file mode 100644 index 0000000000..b3c637483b Binary files /dev/null and b/apps/base-docs/base-camp/assets/images/reading-and-displaying-data/issues-console-log.png differ diff --git a/apps/base-docs/base-camp/assets/images/reading-and-displaying-data/missing-data.png b/apps/base-docs/base-camp/assets/images/reading-and-displaying-data/missing-data.png new file mode 100644 index 0000000000..cd44b91296 Binary files /dev/null and b/apps/base-docs/base-camp/assets/images/reading-and-displaying-data/missing-data.png differ diff --git a/apps/base-docs/base-camp/docs/frontend-setup/building-an-onchain-app.md b/apps/base-docs/base-camp/docs/frontend-setup/building-an-onchain-app.md new file mode 100644 index 0000000000..1fd165408d --- /dev/null +++ b/apps/base-docs/base-camp/docs/frontend-setup/building-an-onchain-app.md @@ -0,0 +1,216 @@ +--- +title: Building an Onchain App +description: Learn step-by-step how to turn a regular template app into an onchain app with a wallet connection. +hide_table_of_contents: false +--- + +While it's convenient and fast to start from a template, the template may not fit your needs. Whether you prefer a different stack, or have already started building the traditional web components of your app, it's common to need to manually add onchain libraries to get your app working. + +In this guide, we'll build the beginnings of an app similar to the one created by the [RainbowKit] quick start, but we'll do it piece by piece. You can follow along, and swap out any of our library choices with the ones you prefer. + +--- + +## Objectives + +By the end of this guide you should be able to: + +- Identify the role of a wallet aggregator in an onchain app +- Debate the pros and cons of using a template +- Add a wallet connection to a standard template app + +--- + +## Creating the Traditional App + +Start by running the [Next.js] script to create a Next.js 13 app: + +```bash +npx create-next-app@13 --use-yarn +``` + +:::info +We're using specific versions of Next.js, wagmi, and viem to ensure things work as expected. It's generally a good idea to give new major versions a few months to settle, and in the fast-moving world of onchain apps, this is doubly true. +::: + +This script will accept `.`, if you want to add the project to the root of a folder you've already created. Otherwise, name your project. Select each option in the generation script as you see fit. We recommend the following selections: + +- Use Typescript?: Yes +- Use ESLint?: Yes +- Use Tailwind?: Your preference +- Use `src/` directory?: Yes +- Use App Router?: **No** +- Customize the default import alias?: No + +:::caution +For now, we recommend that you **DO NOT** use App Router. If you do, you'll need to debug some additional config and dependency issues that are out of the scope of this guide. +::: + +:::info + +The default Next.js script installs [Tailwind]. [RainbowKit]'s does not. + +::: + +Run your app with `yarn dev` to make sure it generated correctly. + +### Manually Installing RainbowKit, Wagmi, and Viem + +The [quick start] guide for RainbowKit also contains step-by-step instructions for manual install. We'll be following an adjusted version here. Most of the setup is actually for configuring [wagmi], which sits on top of [viem] and makes it much easier to write React that interacts with the blockchain. + +Start by installing the dependencies. Once again, we'll use specific versions to ensure compatibility. Update your `package.json` dependencies to include: + +```json + "@rainbow-me/rainbowkit": "^1.3.0", + "viem": "^1.19.7", + "wagmi": "^1.4.5" +``` + +Then run: + +```bash +yarn install +``` + +:::info +Onchain libraries and packages tend to require very current versions of Node. If you're not already using it, you may want to install [nvm]. +::: + +## Adding Imports, Connectors, Config + +In Next.js 13 with the pages router, the root of your app is found in `src/pages/_app_.tsx`, if you followed the recommended setup options. As we want the blockchain provider context to be available for the entire app, we'll add it here. + +### Imports + +Start with the imports: + +```typescript +import '@rainbow-me/rainbowkit/styles.css'; +import { getDefaultWallets, RainbowKitProvider } from '@rainbow-me/rainbowkit'; +import { configureChains, createConfig, WagmiConfig } from 'wagmi'; +import { mainnet, base, baseGoerli } from 'wagmi/chains'; +import { publicProvider } from 'wagmi/providers/public'; +``` + +:::caution +If you're adapting this guide to a different set of libraries or platforms, you may need to import `styles.css` differently. You'll know this is the case if you get ugly text at the bottom of the page instead of a nice modal when you click the connect button. +::: + +### Connectors + +Now, we'll configure the chains, wallet connectors, and providers for your app. We'll use the [`publicProvider`] for now, to get started. See our guide on [Connecting to the Blockchain] for more information on blockchain providers. + +```typescript +const { chains, publicClient } = configureChains([mainnet, base, baseGoerli], [publicProvider()]); +``` + +The default connectors provided by RainbowKit automatically enable the most popular wallets, so we'll add that next: + +```typescript +const { connectors } = getDefaultWallets({ + appName: 'My RainbowKit App', + projectId: 'YOUR_PROJECT_ID', + chains, +}); +``` + +You'll need a `projectId` from [Wallet Connect Cloud], which you can get for free on their site. Make sure to insert it in the appropriate place. + +```danger +Remember, everything on the frontend is public! Be sure to configure the allowlist for your WalletConnect id! +``` + +### Config + +Finally, add the config for wagmi: + +```typescript +const wagmiConfig = createConfig({ + autoConnect: true, + connectors, + publicClient, +}); +``` + +Setting `autoConnect` to `true` will allow your app to automatically reconnect users once they connect for the first time. Most people want this. + +## Wrapping Context Providers + +You can now wrap your app with the context providers for RainbowKit and wagmi. This will make your connection to the blockchain available throughout your entire app without needing to pass anything through props. + +```typescript +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + + + {children} + + + + ); +} +``` + +## Adding the Connect Button + +You're now ready to add your connect button. You can do this anywhere in your app, thanks to the `RainbowKitProvider`. Common practice would be to place the button in your app's header. Since the Next.js template doesn't have one, we'll just add it to the top of the automatically generated page, rather than spending time implementing React components. + +Open up `index.tsx`, and import the `ConnectButton`: + +```typescript +import { ConnectButton } from '@rainbow-me/rainbowkit'; +``` + +Then, simply add the `ConnectButton` component at the top of the first `
`: + +```typescript +// This function has been simplified to save space. +export default function Home() { + return ( +
+
+ +

+ Get started by editing  + src/pages/index.tsx +

+
+
+ ); +} +``` + +Run your app with `yarn dev`, and you should be able to use the RainbowKit connect button to connect with your wallet and switch between networks. + +You use the [Connect Button] props to modify its properties, or you can [customize the connect button] extensively. Some users dislike having the connect button display their token balance. Try disabling it with: + +```typescript + +``` + +--- + +## Conclusion + +In this guide, you've learned how to assemble your onchain app from several pieces. You can use this knowledge to integrate a wallet connection with an existing site, or adjust the stack to meet your preferences. Finally, you've learned how to insert and customize the connect button. + +If you're looking to quickly bootstrap a simple app, you can always use a script, such as the RainbowKit [quit start]. If you're looking for a robust start for a consumer application, check out our [Build Onchain Apps] template! + +--- + +[RainbowKit]: https://www.rainbowkit.com/ +[wagmi]: https://wagmi.sh/ +[viem]: https://viem.sh/ +[quick start]: https://www.rainbowkit.com/docs/installation +[Next.js]: https://nextjs.org/ +[Tailwind]: https://tailwindcss.com/ +[nvm]: https://github.com/nvm-sh/nvm +[WalletConnect]: https://cloud.walletconnect.com/ +[Connecting to the Blockchain]: https://docs.base.org/connecting-to-the-blockchain/overview +[Wallet Connect Cloud]: https://cloud.walletconnect.com/ +[`publicProvider`]: https://wagmi.sh/react/providers/public +[Connect Button]: https://www.rainbowkit.com/docs/connect-button +[customize the connect button]: https://www.rainbowkit.com/docs/custom-connect-button +[Build Onchain Apps]: https://github.com/coinbase/build-onchain-apps diff --git a/apps/base-docs/base-camp/docs/frontend-setup/wallet-connectors.md b/apps/base-docs/base-camp/docs/frontend-setup/wallet-connectors.md new file mode 100644 index 0000000000..a0e428b950 --- /dev/null +++ b/apps/base-docs/base-camp/docs/frontend-setup/wallet-connectors.md @@ -0,0 +1,67 @@ +--- +title: Wallet Connectors +description: Learn about how wallet connector libraries aggregate wallets and make it easier to connect to them from your app. +hide_table_of_contents: false +--- + +One of the most intimidating tasks when building an onchain app is making that initial connection between your users' wallets, and your app. Initial research often surfaces a bewildering number of wallets, each with their own SDKs, and own methods to manage the connection. Luckily, you don't actually need to manage all of this on your own. There are a number of libraries specialized in creating a smooth and beautiful user experience to facilitate this connection. + +--- + +## Objectives + +By the end of this guide you should be able to: + +- Identify the role of a wallet aggregator in an onchain app +- Debate the pros and cons of using a template +- Scaffold a new onchain app with RainbowKit + +--- + +## Connecting to the Blockchain + +One of the many challenging tasks of building a frontend that can interface with your smart contracts is managing the user's connection between your onchain app and their [EOA] wallet. Not only is there an ever-growing suite of different wallets, but users can (and probably should!) use several different addresses within the same wallet app. + +[Rainbowkit] is one of several options that makes this a little bit easier by serving as an aggregator of wallets, and handling some of the details of connecting them. Alternatives include [ConnectKit], and [Dynamic], which are both excellent choices as well. + +Each of these include customizable UI/UX components for inviting the user to connect, displaying connection status, and selecting which wallet they wish to use. + +### Using the Quick Start + +If you're just trying to get up and running as quickly as possible, you can use RainbowKit's [quick start] script to scaffold an app from their template, with a single command. If you're using Yarn: + +```bash +yarn create @rainbow-me/rainbowkit +``` + +:::info + +The script doesn't accept `.` as a project name, so you'll want to run this script in your `src` directory, or wherever you keep your projects. It will create a folder with the same name as your project, and install the project files inside. + +::: + +Once it's done, simply run the app with: + +```bash +yarn run dev +``` + +Using the script is fast, but it does mean less choice. In this case, it builds the app on top of [Next.js], which is great if you want to use it, but not helpful if you prefer to work from a different framework, such as [Create React App], or [Remix]. The script also doesn't help you if you want to add an onchain integration to an existing site. + +--- + +## Conclusion + +In this article, you've learned how libraries such as [Rainbowkit], [ConnectKit], and [Dynamic], aggregate wallets and make it easier for you to connect your app to your users' wallet of choice. You've also learned how you can use a template to quickly create the foundation of your app. Finally, you've learned that the cost of using a template is that it does make some choices for you. + +--- + +[RainbowKit]: https://www.rainbowkit.com/ +[wagmi]: https://wagmi.sh/ +[wallet]: https://ethereum.org/en/developers/docs/accounts/ +[ConnectKit]: https://ethereum.org/en/developers/docs/accounts/ +[Dynamic]: https://www.dynamic.xyz/ +[quick start]: https://www.rainbowkit.com/docs/installation +[Next.js]: https://nextjs.org/ +[Create React App]: https://create-react-app.dev/ +[Remix]: https://remix.run/ diff --git a/apps/base-docs/base-camp/docs/reading-and-displaying-data/configuring-useContractRead.md b/apps/base-docs/base-camp/docs/reading-and-displaying-data/configuring-useContractRead.md new file mode 100644 index 0000000000..0dccfb0d2a --- /dev/null +++ b/apps/base-docs/base-camp/docs/reading-and-displaying-data/configuring-useContractRead.md @@ -0,0 +1,276 @@ +--- +title: Configuring `useContractRead` +description: Configure the properties of the `useContractRead` hook. +hide_table_of_contents: false +--- + +The [`useContractRead`] hook has a number of configurable properties that will allow you to adapt it to your needs. You can [`watch`], for updates, though not for free. Once those updates are retrieved you can use the hook to automatically run an update function to set state for React, or handle any other logic you want to trigger when the data on the blockchain changes. + +--- + +## Objectives + +By the end of this guide you should be able to: + +- Enable the `watch` feature of `useContractRead` to automatically fetch updates from the blockchain +- Describe the costs of using the `watch` feature, and methods to reduce those costs +- Configure arguments to be passed with a call to a `pure` or `view` smart contract function +- Call an instance of `useContractRead` on demand +- Utilize `isLoading` and `isFetching` to improve user experience + +--- + +## Fetching Updates from the Blockchain + +We'll continue with the project you've been building and last updated while learning about the [`useContractRead` hook]. + +Once the excitement of your accomplishment of finally reading from your own contract subsides, try using BaseScan to add another issue, or vote on an existing issue. You'll notice that your frontend does **not** update. There are a few ways to handle this. + +### The Watch Feature + +The easiest, is to turn on the [`watch`] feature. Update your hook to set it to `true`: + +```typescript +const { isError: issuesIsError, isLoading: issuesIsLoading } = useContractRead({ + address: contractData.address as `0x${string}`, + abi: contractData.abi, + functionName: 'getAllIssues', + watch: true, + onSettled(data, error) { + if (data) { + const issuesList = data as Issue[]; + // console.log(issuesList); + setIssues(issuesList); + } + }, +}); +``` + +Try adding a new issue and it will automatically appear on the list, although it may take more time than you are used to. Blockchain is still slower than the web. + +It works! Unfortunately, you can't really stop here, unless you're working on a hackathon prototype or a very early stage demo. The catch is that `wagmi` has a default [`pollingInterval`] of 4 seconds, so having this `watch` causes it to call `eth_blocknumber` and `eth_call`, to call your `getAllIssuesFunction` over and over and over again. + +If you were to take the obvious approach of adding a `useContractRead` for every function you wanted data from, and set it to `watch`, things would quickly get out of hand. A single open web page with 15 functions watched in this way will hit rate limiting in as short as an hour. + +:::info + +Don't do this, either use multicall via [`useContractReads`], or consolidate your `view`s into a single function that fetches all the data you need in one call. + +::: + +Luckily, you have options to control these calls a little better. + +### Pausing On Blur + +Once quick improvement is to simply stop watching the blockchain if the website doesn't have focus. To see this in action, add a state variable to count how many times the function has settled, and one for if the page is focused. You'll also need to set up event listeners to set the state of the latter when the page is focused or blurred. + +```typescript +const [timesCalled, setTimesCalled] = useState(0); +const [pageIsFocused, setPageIsFocused] = useState(false); + +useEffect(() => { + const onFocus = () => setPageIsFocused(true); + const onBlur = () => setPageIsFocused(false); + + window.addEventListener('focus', onFocus); + window.addEventListener('blur', onBlur); + + return () => { + window.removeEventListener('focus', onFocus); + window.removeEventListener('blur', onBlur); + }; +}, []); +``` + +Then, set `watch` to `pageIsFocused`, and increment your counter in `onSettled` + +```typescript +const { isError: issuesIsError, isLoading: issuesIsLoading } = useContractRead({ + address: contractData.address as `0x${string}`, + abi: contractData.abi, + functionName: 'getAllIssues', + watch: pageIsFocused, + onSettled(data, error) { + if (data) { + const issuesList = data as Issue[]; + setIssues(issuesList); + + setTimesCalled(timesCalled + 1); + } + }, +}); +``` + +Finally, surface your counter in the component. + +```typescript +return ( +
+

Number of times called

+

{timesCalled.toString()}

+

{'Has focus: ' + pageIsFocused}

+

All Issues

+
{renderIssues()}
+
+); +``` + +Now, when you watch the page, the count will go up every four seconds. When you switch to another tab or window, the counter will pause until you switch back. + +### Adjusting the Polling Rate + +You likely need to share timely updates with your users, but how timely do those updates need to be to meet the requirements of your app? If you're doing instant messaging, 4 seconds may even be too long (though any faster is running into the speed blocks are added in most L2s). + +A more robust DAO is going to have a voting period of at least a day or two, so those users probably don't need to see that there is a new issue within 4 seconds of it hitting the chain. + +Adjust your [`pollingInterval`] by setting it in `configureChains` in `_app.tsx`: + +```typescript +const { chains, publicClient, webSocketPublicClient } = configureChains( + [baseGoerli], + [ + // other providers... + publicProvider(), + ], + { pollingInterval: 30_000 }, +); +``` + +Setting it to 30 seconds, or 30,000 milliseconds, will reduce your API calls dramatically, without negatively impacting members of the DAO. + +### Updating on Demand + +You can use a similar system to call your update function on demand. First, add a button, a handler for that button, and a state variable for it to set: + +```typescript +const [triggerRead, setTriggerRead] = useState(false); + +const handleTriggerRead = () => { + setTriggerRead(true); +}; +``` + +```typescript +return ( +
+ +

Number of times called

+

{timesCalled.toString()}

+

{'Has focus: ' + pageIsFocused}

+

All Issues

+
{renderIssues()}
+
+); +``` + +Finally, set `watch` to equal `triggerRead`, instead of `pageIsFocused`, and reset `triggerRead` in `onSettled`. + +```typescript +const { isError: issuesIsError, isLoading: issuesIsLoading } = useContractRead({ + address: contractData.address as `0x${string}`, + abi: contractData.abi, + functionName: 'getAllIssues', + watch: triggerRead, + onSettled(data, error) { + if (data) { + const issuesList = data as Issue[]; + setIssues(issuesList); + + setTimesCalled(timesCalled + 1); + setTriggerRead(false); + } + }, +}); +``` + +Now, when the user clicks the button, the hook will call the read function a single time, then set `watch` back to false. + +--- + +## Setting UI Elements + +You can use the "is" return values to set UI elements depending on the status of the hook as it attempts to call a function on the blockchain. + +```typescript +// Wagmi useContractRead return values +{ + data?: Result + error?: Error + isIdle: boolean + isLoading: boolean + isFetching: boolean + isSuccess: boolean + isError: boolean + isFetched: boolean + isFetchedAfterMount: boolean + isRefetching: boolean + refetch: (options: { + throwOnError: boolean + cancelRefetch: boolean + }) => Promise + status: 'idle' | 'error' | 'loading' | 'success' +} +``` + +Try to modify your button to provide feedback to the user that the function has been called. + +```typescript +// Bad code example, do not use + +``` + +The above code won't break anything, but nothing will appear to happen. This happens because `isLoading` is only `true` in circumstances where data is loading for the first time, but no data is present. You could use this to show a spinning wheel in place of the list of issues. + +Instead, try decomposing `isFetching` in your `useContractRead`. This property is true while data is being fetched, even if data has already been loaded once. + +```typescript +// Imperfect code example, do not use + +``` + +You'll probably see the button flicker very quickly since the call doesn't take very long. For a production app, you'd need to add additional handling to smooth out the experience. + +--- + +## Passing Arguments + +Arguments are passed into a `useContractRead` hook by adding an array of arguments, in order, to the `args` property. Common practice is to use React state variables set by UI elements to enable the arguments to be set and modified. For example, you might create a drop-down to set `issueNumber`, then fetch that issue with: + +```typescript +// Incomplete code stub +const [issueNumber, setIssueNumber] = useState(0); + +const { isError: getIssueIsError, isLoading: getIssueIsLoading } = useContractRead({ + address: contractData.address as `0x${string}`, + abi: contractData.abi, + functionName: 'getIssue', + args: [issueNumber], + enabled: issueNumber != 0, + watch: getIssueTriggered, + onSettled(data, error) { + // Set state... + // Reset triggers + }, +}); +``` + +Here, we've set the `enabled` flag to be `false` until an id is selected, which will prevent the hook from running with a bad `issueNumber` and generating an error. We've also mocked out a similar system as above to turn `watch` on and off, perhaps in the handler for selecting the `issueNumber`. + +--- + +## Conclusion + +In this guide, you've learned how to use the `watch` feature of `useContractRead` to enable your frontend to see updates to your smart contract. You've also learned the costs of doing so, and some strategies for mitigation. You've learned how to pass arguments to your functions. Finally, you've learned how to use the properties returned by `useContractRead` to adjust your UI to improve the experience for your users. + +--- + +[wagmi]: https://wagmi.sh/ +[`useContractRead`]: https://wagmi.sh/react/hooks/useContractRead +[`useContractRead` hook]: ./useContractRead +[`watch`]: https://wagmi.sh/react/hooks/useContractRead#watch-optional +[`pollingInterval`]: https://wagmi.sh/core/providers/configuring-chains#pollinginterval-optional +[`useContractReads`]: https://wagmi.sh/react/hooks/useContractReads diff --git a/apps/base-docs/base-camp/docs/reading-and-displaying-data/useAccount.md b/apps/base-docs/base-camp/docs/reading-and-displaying-data/useAccount.md new file mode 100644 index 0000000000..99f6bc027e --- /dev/null +++ b/apps/base-docs/base-camp/docs/reading-and-displaying-data/useAccount.md @@ -0,0 +1,195 @@ +--- +title: The `useAccount` Hook +description: Learn how to access information about the connected user's wallet. +hide_table_of_contents: false +--- + +[wagmi] is a library that provides React hooks that trade a somewhat complex setup process for a great developer experience when building a frontend around the constraints and quirks of onchain building. One of the hooks, `useAccount`, provides access to information about your users' wallet and connection information. + +You can use this for connection-status-based rendering, to enable or disable controls or views based on address, and many other useful tasks. + +--- + +## Objectives + +By the end of this guide you should be able to: + +- Implement the `useAccount`` hook to show the user's address, connection state, network, and balance +- Implement an `isMounted` hook to prevent hydration errors + +--- + +## Displaying Connection Information + +We'll be working from an app generated by RainbowKit's [quick start]. Either open the one you created when we were exploring [Wallet Connectors], or create a new one for this project. + +Either way, change the list of chains to only include `baseGoerli` as the network option. You don't want to accidentally spend real money while developing! + +```typescript +const { chains, publicClient, webSocketPublicClient } = configureChains( + [baseGoerli], + [ + // other providers... + publicProvider(), + ], +); +``` + +### The `useAccount` Hook + +The [`useAccount`] hook allows you to access account and connection data from within any of your components. + +Add a folder for `components` and a file called `ConnectionWindow.tsx` in that folder. Add the below component to the file, and replace the boilerplate text in `index.tsx` with an instance of it. + +```typescript +// ConnectionWindow.tsx +export function ConnectionWindow() { + return ( +
+

Connection Status

+
+ ); +} +``` + +```typescript +// index.tsx +import { ConnectButton } from '@rainbow-me/rainbowkit'; +import type { NextPage } from 'next'; +import Head from 'next/head'; +import styles from '../styles/Home.module.css'; +import { ConnectionWindow } from '../components/ConnectionWindow'; + +const Home: NextPage = () => { + return ( +
+
+ + +
+
+ ); +}; + +export default Home; +``` + +For the purposes of this exercise, open `styles/Home.module.css` and **delete or comment out** `.main`. Doing so will move the content to the top of the page, which will prevent the RainbowKit modal from blocking our ability to see changes. + +Return to `ConnectionWindow.tsx` and add the `useAccount` hook to the top, where you'd add any state variables. The general pattern for wagmi hooks is you decompose the properties you want to use from a function call of the name of the hook. For some, you'll add a config object to that call, but it's not needed for this one. + +```typescript +import { useAccount } from 'wagmi'; + +export function ConnectionWindow() { + const { address, isConnected, isConnecting, isDisconnected } = useAccount(); + + return ( +
+

Connection Status

+
+ ); +} +``` + +You can see all the options in the docs: + +```typescript +{ + address?: string + connector?: Connector + isConnecting: boolean + isReconnecting: boolean + isConnected: boolean + isDisconnected: boolean + status: 'connecting' | 'reconnecting' | 'connected' | 'disconnected' +} +``` + +Update your `
` to show the address of the connected wallet: + +```Typescript +
+

Connection Status

+
+

{"Address: " + address}

+
+
+``` + +Test it out by connecting and disconnecting with your wallet. You should see your full address when you are connected, and the address will be `undefined` when you are disconnected. + +### Connection Status Conditional Rendering + +It isn't very nice to display a value of `undefined` to the user, so let's use the connection status values for conditional rendering depending on if the user is disconnected, connected, or connecting. + +A common pattern is to use the conditional directly in the html return of a component or render function. For example, we could add a line to show that we're connecting as demonstrated: + +``` +
+

Connection Information

+
+ {isConnecting &&

Please click Connect in your wallet...

} +

{"Address: " + address}

+
+
+``` + +You're going to get an `Unhandled Runtime Error`. You can safely close the error window and ignore it for now, we'll fix it in a moment. + +Connect and disconnect your wallet a few times. The `isConnecting` state is true while the _Connect to website_ wallet UI is open. + +Use the `connected` property in the same way to only render the wallet address if there is a wallet connected. Similarly, use the `isDisconnected` property to show a message asking the user to connect. + +``` +
+

Connection Information

+
+ {isConnecting &&

Please click Connect in your wallet...

} + {isConnected &&

{"Address: " + address}

} + {isDisconnected &&

Please connect your wallet to use this app.

} +
+
+``` + +### Implementing the `useIsMounted` Hook + +Whenever you refresh your browser window, you're probably seen an `Unhandled Runtime Error` that content did not match. The error provides a link explaining why this [hydration error] occurred, but doesn't give you much to explain why the error is occurring here. + +What's happening is an interaction between the server-side rendering of Next.js, and wagmi's use of local storage caching to prevent a flickering of UI elements that can occur when connection state changes. As a result, the server and the client have different states for the first render cycle. + +You can fix this by adding a small hook to make sure the component is mounted before it tries to render the app. + +Open `_app.tsx` and import `useEffect` and `useState` from `'react'`. + +Create a state variable called `mounted`, and set it in the first render cycle to be `true`. Finally, use the same conditional rendering technique to only render the `` element if the app has mounted. + +```typescript +const [mounted, setMounted] = useState(false); +useEffect(() => setMounted(true), []); + +return ( + + + {mounted && } + + +); +``` + +With this fix in place, you should no longer get hydration errors! + +--- + +## Conclusion + +In this guide, you've learned how the `useAccount` hook gives you access to information about the user's connection status and wallet. It can be used in any part of your app that is wrapped by the wagmi context provider. You've also learned a technique for conditional rendering based on connection status. Finally, you've learned how the underlying mechanics of wagmi and Next.js conflict, and what you can do to resolve the resulting hydration error. + +--- + +[RainbowKit]: https://www.rainbowkit.com/ +[wagmi]: https://wagmi.sh/ +[quick start]: https://www.rainbowkit.com/docs/installation/ +[Wallet Connectors]: ../frontend-setup/wallet-connectors/ +[`useAccount`]: https://wagmi.sh/react/hooks/useAccount +[hydration error]: https://nextjs.org/docs/messages/react-hydration-error diff --git a/apps/base-docs/base-camp/docs/reading-and-displaying-data/useContractRead.md b/apps/base-docs/base-camp/docs/reading-and-displaying-data/useContractRead.md new file mode 100644 index 0000000000..1bb1cc1c0a --- /dev/null +++ b/apps/base-docs/base-camp/docs/reading-and-displaying-data/useContractRead.md @@ -0,0 +1,450 @@ +--- +title: The `userContractRead` Hook +description: Learn how to call view and pure functions from a smart contract. +hide_table_of_contents: false +--- + +The `useContractRead` hook is [wagmi]'s method of calling `pure` and `view` functions from your smart contracts. As with `useAccount`, `useContractRead` contains a number of helpful properties to enable you to manage displaying information to your users. + +--- + +## Objectives + +By the end of this guide you should be able to: + +- Implement wagmi's `useContractRead` hook to fetch data from a smart contract +- Convert data fetched from a smart contract to information displayed to the user +- Identify the caveats of reading data from automatically-generated getters + +--- + +## Contract Setup + +For this guide, we'll be continuing from the project you started for the [`useAccount` hook]. We'll work with an upgrade to the contract that you may have created if you completed the [ERC 20 Tokens Exercise]. See below for an example you can use if you don't already have your own! + +The contract creates a very simple DAO, in which users can create issues and vote for them, against them, or abstain. Anyone can `claim` 100 tokens. This is an insecure system for demonstration purposes, since it would be trivial to claim a large number of tokens with multiple wallets, then transfer them to a single address and use that to dominate voting. + +But it makes it much easier to test! + +:::caution + +If you're using your own contract, please redeploy it with the following `view` functions: + +```solidity +function numberOfIssues() public view returns(uint) { + return issues.length; +} + +function getAllIssues() public view returns(ReturnableIssue[] memory) { + ReturnableIssue[] memory allIssues = new ReturnableIssue[](issues.length); + + for(uint i = 0; i < issues.length; i++) { + allIssues[i] = getIssue(i); + } + + return allIssues; +} +``` + +**You also need to make the `getIssue` function `public`. The original spec called for it to be `external`.** + +::: + +### Create Demo Issues + +To start, you'll need to put some data into your contract so that you can read it from your frontend. Open [Goerli BaseScan], find your contract, connect with your wallet, and call the `claim` function. + +Add the following two issues: + +```text +_issueDesc: We should enable light mode by default. +_quorom: 2 +``` + +```text +_issueDesc: We should make inverted mouse controls the default selection. +_quorom: 2 +``` + +Switch to a **different wallet address**. Claim your tokens with the new address, and add one more issue: + +```text +_issueDesc: Two spaces, not four, not tabs! +_quorom: 2 +``` + +Call the `getIssue` function under the `Read Contract` tab to make sure all three are there. + +--- + +## Reading from your Smart Contract + +To be able to read from your deployed smart contract, you'll need two pieces of information: the address and [ABI]. These are used as parameters in the `useContractRead` hook. + +If you're using [Hardhat], both of these can be conveniently found in a json file in the `deployments/` folder, named after your contract. For example, our contract is called `FEWeightedVoting`, so the file is `deployments/base-goerli/FEWeightedVoting.json`. + +If you're using something else, it should produce a similar artifact, or separate artifacts with the [ABI] and address. If this is the case, make the adjustments you need when you import this data. + +Either way, add a folder called `deployments` and place a copy of the artifact file(s) inside. + +### Using the `useContractRead` Hook + +Add a file for a new component called `IssueList.tsx`. You can start with: + +```typescript +import { useContractRead } from 'wagmi'; + +export function IssueList() { + return ( +
+

All Issues

+
{/* TODO: List each issue */}
+
+ ); +} +``` + +You'll need to do some prepwork to enable Typescript to more easily interpret the data returned from your contract. Add an `interface` called `Issue` that matches with the `ReturnableIssue` type: + +```typescript +interface Issue { + voters: string[]; + issueDesc: string; + votesFor: bigint; + votesAgainst: bigint; + votesAbstain: bigint; + totalVotes: bigint; + quorum: bigint; + passed: boolean; + closed: boolean; +} +``` + +:::warning + +Be very careful here! `bigint` is the name of the type, `BigInt` is the name of the constructor for that type. If you incorrectly use the constructor as the type, much of your code will still work, but other parts will express very confusing bugs. + +::: + +Now, import `useState` and add a state variable to hold your list of `Issue`s. + +```typescript +const [issues, setIssues] = useState([]); +``` + +You'll also need to import your contract artifact: + +```typescript +import contractData from '../deployments/FEWeightedVoting.json'; +``` + +Finally, the moment you've been waiting for: Time to read from your contract! Add an instance of the [`useContractRead`] hook. It works similarly to the [`useAccount`] hook. Configure it with: + +```typescript +const { isError: issuesIsError, isLoading: issuesIsLoading } = useContractRead({ + address: contractData.address as `0x${string}`, + abi: contractData.abi, + functionName: 'getAllIssues', + onSettled(data, error) { + if (data) { + const issuesList = data as Issue[]; + console.log(issuesList); + setIssues(issuesList); + } + }, +}); +``` + +We'll break down the above, but first, let's see it in action. Add in instance of your new component to `index.tsx`: + +```typescript +
+ + + +
+``` + +Run your app, and you should see your list of issues fetched from the blockchain and displayed in the console! + +![Issues Console Log](../../assets/images/reading-and-displaying-data/issues-console-log.png) + +Breaking down the hook, you've: + +- Renamed the properties decomposed from `useContractRead` to be specific for our function. Doing so is helpful if you're going to read from more than one function in a file +- Configured the hook with the address and ABI for your contract +- Made use of the `onSettled` function, which is called either if there is an error, or fetching is completed, to set the list of `Issue`s in state. + +### Displaying the Data + +Now that you've got the data in state, you can display it via your component. One strategy to display a list of items is to compile a `ReactNode` array in a render function. + +```typescript +function renderIssues() { + return issues.map((issue) => ( +
+

{issue.issueDesc}

+

{'Voters: ' + issue.voters.toString()}

+

{'Votes For: ' + issue.votesFor.toString()}

+

{'Votes Against: ' + issue.votesAgainst.toString()}

+

{'Votes Abstain: ' + issue.votesAbstain.toString()}

+

{'Quorum: ' + issue.quorum.toString()}

+

{'Passed: ' + issue.passed}

+

{'Closed: ' + issue.closed}

+
+ )); +} +``` + +Then, call the render function in the return for your component: + +```typescript +return ( +
+

All Issues

+
{renderIssues()}
+
+); +``` + +You'll now see your list of issues rendered in the browser! Congrats, you've finally made a meaningful connection between your smart contract and your frontend! + +### A Caveat with Automatic Getters + +Remember how the Solidity compiler creates automatic getters for all of your public state variables? This feature is very helpful, but it can create bewildering results when you use it for `struct`s that contain `mapping`s. Remember, nesting mappings **cannot** be returned outside the blockchain. The `enumerableSet` protects you from this problem, because it has private variables inside it, which prevents setting `issues` as `public`. Had we instead used a mapping, we'd lose this protection: + +```solidity + // Code for demo only + struct Issue { + mapping(address => bool) voters; + string issueDesc; + uint votesFor; + uint votesAgainst; + uint votesAbstain; + uint totalVotes; + uint quorum; + bool passed; + bool closed; + } +``` + +To demonstrate, add a second `useContractRead` to fetch an individual issue using the getter: + +```typescript +// Bad code for example, do not use +const { isError: getOneIsError, isLoading: getOneIsLoading } = useContractRead({ + address: contractData.address as `0x${string}`, + abi: contractData.abi, + functionName: 'issues', + args: [1], + onSettled(data, error) { + console.log('Settled'); + if (data) { + console.log('Data', data); + const issueOne = data as Issue; + console.log('Issue One', issueOne); + } else { + console.log(error); + } + }, +}); +``` + +Everything appears to be working just fine, but how is `issueOne.desc` undefined? You can see it right there in the log! + +![Missing Data](../../assets/images/reading-and-displaying-data/missing-data.png) + +If you look closely, you'll see that `voters` is missing from the data in the logs. What's happening is that because the nested `mapping` cannot be returned outside the blockchain, it simply isn't. TypeScript then gets the `data` and does the best it can to cast it `as` an `Issue`. Since `voters` is missing, this will fail and it instead does the JavaScript trick of simply tacking on the extra properties onto the object. + +Take a look at the working example above where you retrieved the list. Notice that the keys in our `Issue` type are in that log, but are missing here. The assignment has failed, and all of the `Issue` properties are `null`. + +--- + +## Conclusion + +In this guide, you've learned how to use the `useContractRead` hook to call `pure` and `view` functions from your smart contracts. You then converted this data into React state and displayed it to the user. Finally, you explored a tricky edge case in which the presence of a nested `mapping` can cause a confusing bug when using the automatically-generated getter. + +--- + +## Simple DAO Contract Example + +Use this contract if you don't have your own from the [ERC 20 Tokens Exercise]. You can also use this if you want to cheat to get that badge. Doing so would be silly though! + +:::caution +If you use your own contract, redeploy it with the `numberOfIssues` and `getAllIssues` functions from the bottom of the contract below. We'll need this for our first pass solution for getting all the `Issues` in the contract. + +**You also need to make the `getIssue` function `public`. The original spec called for it to be `external`.** +::: + +```Solidity +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.17; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; + +contract FEWeightedVoting is ERC20 { + using EnumerableSet for EnumerableSet.AddressSet; + + mapping(address => bool) claimed; + uint public maxSupply = 1000000; + uint totalClaimed; + + uint constant claimAmount = 100; + + error TokensClaimed(); + error AllTokensClaimed(); + error NoTokensHeld(); + error QuorumTooHigh(uint); + error AlreadyVoted(); + error VotingClosed(); + + enum Vote { + AGAINST, + FOR, + ABSTAIN + } + + struct Issue { + EnumerableSet.AddressSet voters; + string issueDesc; + uint votesFor; + uint votesAgainst; + uint votesAbstain; + uint totalVotes; + uint quorum; + bool passed; + bool closed; + } + + // EnumerableSets are mappings and cannot be returned outside a contract + struct ReturnableIssue { + address[] voters; + string issueDesc; + uint votesFor; + uint votesAgainst; + uint votesAbstain; + uint totalVotes; + uint quorum; + bool passed; + bool closed; + } + + Issue[] issues; + + constructor( + string memory _name, + string memory _symbol + ) ERC20(_name, _symbol) { + // Burn Issue 0 + issues.push(); + } + + function claim() public { + if (claimed[msg.sender] == true) { + revert TokensClaimed(); + } + + if (totalSupply() >= maxSupply) { + revert AllTokensClaimed(); + } + + _mint(msg.sender, claimAmount); + claimed[msg.sender] = true; + } + + function createIssue( + string memory _issueDesc, + uint _quorum + ) public returns (uint) { + if (balanceOf(msg.sender) == 0) { + revert NoTokensHeld(); + } + + if (_quorum > totalSupply()) { + revert QuorumTooHigh(_quorum); + } + + Issue storage newIssue = issues.push(); + newIssue.issueDesc = _issueDesc; + newIssue.quorum = _quorum; + return issues.length - 1; + } + + function getIssue(uint _id) public view returns (ReturnableIssue memory) { + Issue storage issue = issues[_id]; + return + ReturnableIssue( + issue.voters.values(), + issue.issueDesc, + issue.votesFor, + issue.votesAgainst, + issue.votesAbstain, + issue.totalVotes, + issue.quorum, + issue.closed, + issue.passed + ); + } + + function vote(uint _issueId, Vote _vote) public { + Issue storage issue = issues[_issueId]; + if (issue.voters.contains(msg.sender)) { + revert AlreadyVoted(); + } + if (issue.closed) { + revert VotingClosed(); + } + issue.voters.add(msg.sender); + + if (_vote == Vote.FOR) { + issue.votesFor += balanceOf(msg.sender); + } else if (_vote == Vote.AGAINST) { + issue.votesAgainst += balanceOf(msg.sender); + } else if (_vote == Vote.ABSTAIN) { + issue.votesAbstain += balanceOf(msg.sender); + } else { + revert("Error..."); + } + + issue.totalVotes += balanceOf(msg.sender); + + if (issue.totalVotes >= issue.quorum) { + issue.closed = true; + if (issue.votesFor > issue.votesAgainst) { + issue.passed = true; + } + } + } + + function numberOfIssues() public view returns(uint) { + return issues.length; + } + + function getAllIssues() public view returns(ReturnableIssue[] memory) { + ReturnableIssue[] memory allIssues = new ReturnableIssue[](issues.length); + + for(uint i = 0; i < issues.length; i++) { + allIssues[i] = getIssue(i); + } + + return allIssues; + } +} +``` + +--- + +[RainbowKit]: https://www.rainbowkit.com/ +[wagmi]: https://wagmi.sh/ +[quick start]: https://www.rainbowkit.com/docs/installation/ +[Wallet Connectors]: ../frontend-setup/wallet-connectors/ +[`useAccount`]: https://wagmi.sh/react/hooks/useAccount +[hydration error]: https://nextjs.org/docs/messages/react-hydration-error +[ERC 20 Tokens Exercise]: https://docs.base.org/base-camp/docs/erc-20-token/erc-20-exercise +[Goerli BaseScan]: https://goerli.basescan.org/ +[`useAccount` hook]: ./useAccount +[Hardhat]: https://hardhat.org/ +[ABI]: https://docs.soliditylang.org/en/latest/abi-spec.html +[`useContractRead`]: https://wagmi.sh/react/hooks/useContractRead diff --git a/apps/base-docs/base-camp/docs/writing-to-contracts/useContractWrite.md b/apps/base-docs/base-camp/docs/writing-to-contracts/useContractWrite.md new file mode 100644 index 0000000000..6cc0759fe4 --- /dev/null +++ b/apps/base-docs/base-camp/docs/writing-to-contracts/useContractWrite.md @@ -0,0 +1,162 @@ +--- +title: The `useContractWrite` hook +description: Write to your smart contracts with the `useContractWrite` hook. +hide_table_of_contents: false +--- + +The [`useContractWrite`] hook allows you to call your `public` and `external` smart contract functions that write to state and create a permanent modification to the data on chain. + +--- + +## Objectives + +By the end of this guide you should be able to: + +- Implement wagmi's useContractWrite hook to send transactions to a smart contract +- Configure the options in useContractWrite +- Display the execution, success, or failure of a function with button state changes, and data display + +--- + +## Sending a Transaction to the Blockchain + +:::warning + +In this step-by-step, we're going to start with the [`useContractWrite`] hook. You probably won't want to use this in production. In the next step-by-step, we'll show you the [`usePrepareContractWrite`] hook, how it works with `useContractWrite`, and how you can use it to create a better user experience. + +Exploring them separately will highlight the functionality provided by the prepare hook. + +::: + +:::caution + +In this module, we'll extend the onchain app you build in the previous module, [Reading and Displaying Data]. + +::: + +You've built an app that can read from your Simple DAO smart contract, but so far, you've used Basescan to send transactions that call your write functions. You can use the [`useContractWrite`] hook in a similar way to call those functions directly from your app. + +### Setting up the Component + +Add a new component called `TokenInfo` to the project, and a state variable for `tokenBalance`. + +```typescript +import { useState } from 'react'; + +export function TokenInfo() { + const [tokenBalance, setTokenBalance] = useState(0); +} +``` + +### Reading the Token Balance + +You'll need to know how many tokens the user has to be able to make decisions on what UI controls to display, so start by adding a `useContractRead`. You don't have a function for this directly in your contract, but your contract inherits from the [OpenZeppelin ERC20] contract, which has a function called `balanceOf` that takes an address and returns the balance for that address. + +If you're not going to use any of the returns, such as `isLoading`, you can just call it without decomposing or assigning. You'll need the user's address, which you can conveniently get from the [`useAccount`] hook using the pattern below. + +```typescript: +useContractRead({ + address: contractData.address as `0x${string}`, + abi: contractData.abi, + functionName: "balanceOf", + args: [useAccount().address], + watch: true, // Don't do this in production + onSettled(data, error) { + if (data) { + setTokenBalance(data as bigint); + console.log(data); + } + if (error) { + console.log(error); + } + } +}); +``` + +:::caution + +Remember, this is an expensive method to watch for data to change on the blockchain. In this case, a more production-suitable solution might be to call `balanceOf` after the user has done something that might change the balance. + +::: + +Set the `return` for your component to display this balance to the user: + +```typescript +return ( +
+

{'Token Balance: ' + tokenBalance}

+
+); +``` + +Then, add the component to your app in `index.tsx`. + +```typescript +return ( +
+
+ + + + +
+); +``` + +Run the app and make sure you see the expected balance displayed on the page. + +### Setting up `useContractWrite` + +The [`useContractWrite`] hook is configured similarly to the [`useContractRead`] hook, with one important difference. You'll need to decompose the `write` property from the function call. This is a function that you can use to call your smart contract function whenever you'd like! + +```typescript +const { isLoading: claimIsLoading, write: claim } = useContractWrite({ + address: contractData.address as `0x${string}`, + abi: contractData.abi, + functionName: 'claim', + onSettled(error) { + if (error) { + alert('Unable to claim'); + } + }, +}); +``` + +Add an event handler function and a button. As with the `useContractWrite` hook, you can use `isLoading` and other state helpers to adjust your UI. The name of this one is a little misleading. `isLoading` will be `true` starting from the moment the transaction gets sent to the user's wallet. + +You can use this to nudge them to look to their wallet to complete the transaction: + +```typescript +const handleClaimClick = () => { + claim(); +}; + +return ( +
+

{'Token Balance: ' + tokenBalance}

+ +
+); +``` + +Try it out. Notice that the `alert` triggers without the wallet window popping up if you click the `Claim Tokens` button while connected with a wallet that already owns the tokens. The reason this happens is that viem, which underlies wagmi, runs a simulation of the transaction to estimate gas costs. If that simulation fails, it triggers the fail mechanism immediately, rather than allowing the app to send a bad transaction to the blockchain and cost the user gas for a call doomed to fail. + +You'll probably need to change to a new wallet or redeploy your contract a couple of times to complete your testing. Do that, and try out the call on a wallet with no tokens. Notice that the button is disabled and the text now prompts the user to look to their wallet to approve the transaction. + +--- + +## Conclusion + +In this step-by-step, you've learned how to use the [`useContractWrite`] hook to call your smart contract functions on demand. You've also tested methods to manage the UI/UX experience for your users, based on the state of the transaction, as well as its success or failure. + +--- + +[wagmi]: https://wagmi.sh/ +[`useContractWrite`]: https://wagmi.sh/react/hooks/useContractWrite +[`usePrepareContractWrite`]: https://wagmi.sh/react/prepare-hooks/usePrepareContractWrite +[Reading and Displaying Data]: ../reading-and-displaying-data/useAccount +[`useContractRead`]: https://wagmi.sh/react/hooks/useContractRead +[OpenZeppelin ERC20]: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20.sol +[`useAccount`]: https://wagmi.sh/react/hooks/useAccount diff --git a/apps/base-docs/base-camp/docs/writing-to-contracts/usePrepareContractWrite.md b/apps/base-docs/base-camp/docs/writing-to-contracts/usePrepareContractWrite.md new file mode 100644 index 0000000000..d0de460c0d --- /dev/null +++ b/apps/base-docs/base-camp/docs/writing-to-contracts/usePrepareContractWrite.md @@ -0,0 +1,99 @@ +--- +title: The `usePrepareContractWrite` hook +description: Improve your user experience with the `usePrepareContractWrite` hook. +hide_table_of_contents: false +--- + +The [`usePrepareContractWrite`] hook "Eagerly fetches the parameters required for sending a contract write transaction such as the gas estimate." What this means is that by using this hook, you can dramatically reduce the time it takes for the wallet confirmation to pop up for a transaction, and detect and respond to potential errors before the user tries to send a transaction. + +--- + +## Objectives + +By the end of this guide you should be able to: + +- Implement Wagmi's `usePrepareContractWRite` and `useContractWrite` to send transactions to a smart contract +- Configure the options in `usePrepareContractWrite` and `useContractWrite` +- Call a smart contract function on-demand using the write function from `useContractWrite`, with arguments and a value + +--- + +## Refining the Claim Component + +In the previous step-by-step, you used [`useContractWrite`] to set up a hook you can use to to call the `claim` function in your smart contract when the user clicks a button. The component works well enough, but it can take a long time for the wallet to pop up, particularly if there is network congestion. You also have no way of responding to a problem with the transaction inputs until after the user tries to initiate a transaction. + +### Using `usePrepareContractWrite` + +The `usePrepareContractWrite` is use with `useContractWrite`, which accepts it as an argument. It has most of the same properties as well. Modify your `TokenInfo` component to test it: + +```typescript +// Bad code for example. See below for fix. +const { config: claimConfig, isLoading: claimIsLoading } = usePrepareContractWrite({ + address: contractData.address as `0x${string}`, + abi: contractData.abi, + functionName: 'claim', + onSettled(error) { + if (error) { + alert('Unable to claim'); + } + }, +}); +const { write: claim } = useContractWrite(claimConfig); +``` + +You'll also need to update your handler to use the TypeScript pre-check feature, because the claim function will be briefly `undefined`. + +```typescript +const handleClaimClick = () => { + claim?.(); +}; +``` + +Reload the site and observe that the `alert` is triggered on load if you're signed in with an address that has already claimed tokens. You'll also see that the button is disabled, as though the user had clicked it and a transaction is loading in the wallet. + +### Making Adjustments + +The reason for this is a subtle difference in how `useContractRead` and `usePrepareContractRead` work. + +In the last step-by-step, you saw how viem runs a simulation of the transaction when the `write` function is called. `usePrepareContractRead` eagerly runs this simulation, updates status variables, and runs `onSettled` eagerly and proactively. + +You'll need to make some modifications for it to work. You can go back to the instance of `isLoading` you decomposed from `useContractWrite` and it will work as before. + +The `onSettled` function is now being triggered when the data for the call is _fetched_, not when the call has settled. As a result, it immediately generates the error, and triggers the `alert`. + +You can solve this a number of ways, including simply not rendering the button if the user has already claimed. You could also modify the code, and combine it with `isError`, to share this information to the user. + +```typescript +const { config: claimConfig, isError: claimIsError } = usePrepareContractWrite({ + address: contractData.address as `0x${string}`, + abi: contractData.abi, + functionName: 'claim', +}); +const { write: claim, isLoading: claimIsLoading } = useContractWrite(claimConfig); + +const handleClaimClick = () => { + claim?.(); +}; + +return ( +
+

{'Token Balance: ' + tokenBalance}

+ +

{claimIsError ? 'Unable to claim tokens.' : 'Claim your tokens!'}

+
+); +``` + +--- + +## Conclusion + +In this step-by-step, you updated your app to use the `usePrepareContractWrite` hook to provide a speedier wallet interaction for your users. You've also learned how you can predict and respond to potential errors without the user needing to attempt to send a transaction. You could use this functionality to let them know a username is already taken, a bid amount is not large enough, or an item is no longer available. + +--- + +[wagmi]: https://wagmi.sh/ +[`useContractWrite`]: https://wagmi.sh/react/hooks/useContractWrite +[`usePrepareContractWrite`]: https://wagmi.sh/react/prepare-hooks/usePrepareContractWrite diff --git a/apps/base-docs/base-camp/sidebars.js b/apps/base-docs/base-camp/sidebars.js index 2cf141f653..87b7609046 100644 --- a/apps/base-docs/base-camp/sidebars.js +++ b/apps/base-docs/base-camp/sidebars.js @@ -753,6 +753,72 @@ const sidebars = { }, ], }, + { + type: 'category', + label: 'Onchain App Development (Frontend)', + collapsible: true, + items: [ + { + type: 'category', + label: 'Frontend Setup', + items: [ + { + type: 'doc', + id: 'docs/frontend-setup/wallet-connectors', + className: 'sidebar-coding', + }, + { + type: 'doc', + id: 'docs/frontend-setup/building-an-onchain-app', + className: 'sidebar-coding', + }, + ], + }, + { + type: 'link', + label: 'Connecting to the Blockchain', + href: 'https://docs.base.org/connecting-to-the-blockchain/overview', + className: 'sidebar-coding', + }, + { + type: 'category', + label: 'Reading and Displaying Data', + items: [ + { + type: 'doc', + id: 'docs/reading-and-displaying-data/useAccount', + className: 'sidebar-coding', + }, + { + type: 'doc', + id: 'docs/reading-and-displaying-data/useContractRead', + className: 'sidebar-coding', + }, + { + type: 'doc', + id: 'docs/reading-and-displaying-data/configuring-useContractRead', + className: 'sidebar-coding', + }, + ], + }, + { + type: 'category', + label: 'Writing to Contracts', + items: [ + { + type: 'doc', + id: 'docs/writing-to-contracts/useContractWrite', + className: 'sidebar-coding', + }, + { + type: 'doc', + id: 'docs/writing-to-contracts/usePrepareContractWrite', + className: 'sidebar-coding', + }, + ], + }, + ], + }, ], };