Ethereum Developer Tooling for Creating Better Web3 Dapps

Blocknative Ethereum Web3

How to connect wallets and add real-time transaction notifications and customizable alerts to any front-end framework with Web3 Onboard

Are you building a dapp and looking for an easy and reliable web3 developer tooling to add a connect wallet button, support multiple accounts, and push transaction notifications to your users? With Web3 Onboard’s Core package, any developer can quickly set up their dapp to connect EVM (Ethereum Virtual Machine) compatible wallets, make transactions and sign contracts on any front-end framework or Javascript code base. Web3 Onboard also allows for a full range of customizations, styling and theming that makes the process of onboarding users look and feel natural and native in any dapp (decentralized application) environment.

This article we will cover the Web3 Onboard Core package and the API that is exposed. We will cover:

  • How to add a “connect wallet” button to your dapp
  • How to connect multiple wallets and accounts on your dapp
  • How to send transactions from your dapp
  • How to add real-time transaction notifications to your dapp so users can monitor their transaction through the mempool
  • How to add customizable alerts to your dapp
  • How to add pre-flight notifications (i.e. waiting for confirmation, insufficient funds, etc) to your dapp

The best part about the Web3 Onboard Core package is it is completely framework agnostic meaning it can be used within any front-end Javascript framework (Svelte, React, Next, Angular, Vue, etc), Typescript or Vanilla JS application. If you prefer to use React, Web3 Onboard has a React Hooks package or, if you are a Vue developer, check out the Vue Package.

In this article, we will use Svelte as our front-end framework but the same API usage and patterns can be used in any front-end Javascript library as the Core package is framework agnostic.

Accept injected wallets on your dapp

Injected wallets are wallets that connect to users' browsers, such as Metamask or Coinbase Wallet. You will most likely want to add this ability to your dapp. With this module, Injected Wallets are automatically detected on both desktop or within the specified wallet’s mobile app by Web3 Onboard. Full documentation for injected-wallets can be found in our official docs here.

To start accepting injected wallets, let's get started by installing a few packages using yarn or npm.

**NPM**
`npm i @web3-onboard/core @web3-onboard/injected-wallets ethers`
 
**Yarn**
`yarn add @web3-onboard/core @web3-onboard/injected-wallets ethers`

Initializing Web3 Onboard

Next we will initialize Web3-Onboard using the `Onboard` function provided by the @web3-onboard/core package. This can be done in a service file or directly in the entry file of your project. For this example we are using Svelte so we will have the initialization code in the App.svelte file.

For this example we will only add the Ethereum Mainnet and Rinkeby Testnet chains but any EVM chain can be added to the chains array (Polygon, BNB - Binance, Fantom, Arbitrum, Optimism, etc.).

To get started we will execute the `injectedModule()` function that we get from @web3-onboard/injected-wallet which will initialize the module and will allow Web3 Onboard to look for injected wallets on page load or button click. If we wanted to expand on the wallets accepted by your dapp Web3 Onboard supports many hardware and SDK wallets as well which can be added in the same way. See a full list here.

**Note - With injected wallets only wallets that are installed on the browser or used within the specified wallet’s mobile app will be displayed.

Next we will want to head to https://infura.io/ and sign up for a free Infura API key. This will allow us to retrieve balances and make RPC calls.

Full documentation around initializing Web3 Onboard can be found here.

import Onboard from '@web3-onboard/core'
import injectedModule from '@web3-onboard/injected-wallets'
 
const injected = injectedModule()
 
const infuraKey = ''
 
// initialize Onboard
const onboard = Onboard({
 wallets: [injected],
 chains: [
   {
     id: '0x1',
     token: 'ETH',
     label: 'Ethereum',
     rpcUrl: 'https://mainnet.infura.io/v3/${infuraKey}'
   },
   {
     id: '0x4',
     token: 'rETH',
     label: 'Rinkeby',
     rpcUrl: 'https://rinkeby.infura.io/v3/${infuraKey}'
   }
 ]
})

That’s it for initializing! Now let’s connect a wallet!

How to create a Connect Wallet button on your dapp

Now that Web3 Onboard is initialized and set up to look for and connect to injected wallets we can add code to connect wallets and make the wallet’s information available to the dapp. For this we will use the `connectWallet()` function on the initialized onboard object.

async function connectWallet() {
 const wallets = await onboard.connectWallet()
 console.log(wallets)
}
 
connectWallet()


<script>
 import Onboard from '@web3-onboard/core'
 import injectedModule from '@web3-onboard/injected-wallets'
 
 const injected = injectedModule()
 
const infuraKey = ''
 
 const onboard = Onboard({
   wallets: [
     injected
   ],
   chains: [
     {
       id: '0x1',
       token: 'ETH',
       label: 'Ethereum',
       rpcUrl: 'https://mainnet.infura.io/v3/${infuraKey}'
     },
     {
       id: 4,
       token: 'rETH',
       label: 'Rinkeby',
       rpcUrl: 'https://rinkeby.infura.io/v3/${infuraKey}'
     }
   ]
 })
 
</script>
 
<style>
 button {
   width: 14rem;
   margin: 8px;
 }
</style>
 
<main>
 <button on:click={() => onboard.connectWallet()}>Connect Wallet</button>
</main>

With this code we should have a very simple webpage with a button that looks like this:

basic-connect-wallet-buttonTo connect a wallet the user will need an extension wallet downloaded and installed on their browser. For more information on customizing this part of the onboarding experience checkout the section Add a custom message to your dapp if a user is not using a supported wallet in my other article. The rest of the article will assume you have at least one browser extension wallet installed and understand the process of adding wallet extension to your browser. For the purposes of this article I have installed MetaMask, Binance Chain Wallet and Coinbase wallet for demonstration purposes.

Verify by connecting a wallet

To verify that your dapp is able to connect to injected wallets we will assume the user has an injected wallet installed (see above for more info on customizing this part of the experience). After clicking Connect we will see the following:

example-wallets

After clicking the MetaMask button we will be asked by the wallet if we can connect to the dapp, which we will confirm.

**Note - Web3 Onboard allows users to connect multiple accounts from multiple wallets. At this step the user must select all accounts they want to connect through the “Connect With MetaMask” confirmation. There is no programmatic way to change the selection after a user has connected aside from the user manually revoking site access from the connected sites through the wallet and re-connecting to the application.

metamask-wallet-balances

After confirming the selected accounts in the wallet we will see:

metamask-connection

And then, boom! We have a wallet connected!

metamask-connected-with-balanceIn the upper corner of your dapp you will see the Web3 Onboard Account Center component. This is enabled by default and gives the user more information on and ways to interact with their connected wallets. Upon clicking the Account Center you will see an expanded Account Center view with options to change chains, connect another wallet, disconnect all wallets and more dapp info. Learn more about the Account Center.

Without a DApp icon or logo added to the Web3 Onboard initialization `appMetaData ` object, Onboard will default to showing a question mark in the connection successful section and Account Center and will default to showing a Blocknative logo in the left panel of the Onboard connect modal. To customize these to fit your DApp add your icons and logo to the `appMetaData` property within the initialization.

For the remainder of this article we will add the `appMetaData` property to the W3O initialization along with a Blocknative icon.

For more information on customizing the look and feel of Web3 Onboard through the `appMetaData` property please see our official documentation here.

How to connect multiple wallets or accounts to your dapp

Now that we have connected a wallet, we can make it easy for users to connect a second wallet.

To do this let's add the stream from the web3-onboard state and select ‘wallets’.

**Note - When using streams be sure to `unsubscribe` from the stream when updates are no longer needed to avoid memory leaks!

// Subscribe to wallet updates
 const wallets$ = onboard.state.select('wallets')
 const { unsubscribe } = wallets$.subscribe(update =>
   console.log('wallets update: ', update)
 )
 
 // unsubscribe when updates are no longer needed
 onDestroy(unsubscribe)

Let's also update the code to conditionally render a new button if a wallet is already connected. Along with more information on the wallet connected (balance/token, ENS if it exists, address and wallet icon) and a disconnect button.

<script>
 import Onboard from '@web3-onboard/core'
 import injectedModule from '@web3-onboard/injected-wallets'
 import { onDestroy } from 'svelte'
 
 import blocknativeIcon from './blocknative-icon'
 import blocknativeLogo from './blocknative-logo'
 
 const injected = injectedModule()
 
 const onboard = Onboard({
   wallets: [injected],
   chains: [
     {
       id: '0x1',
       token: 'ETH',
       label: 'Ethereum',
       rpcUrl: 'https://mainnet.infura.io/v3/17c1e1500e384acfb6a72c5d2e67742e'
     },
     {
       id: 4,
       token: 'rETH',
       label: 'Rinkeby',
       rpcUrl: 'https://rinkeby.infura.io/v3/17c1e1500e384acfb6a72c5d2e67742e'
     }
   ],
   appMetadata: {
     name: 'Blocknative',
     icon: blocknativeIcon,
     logo: blocknativeLogo,
     description: 'Demo app for Onboard V2',
     recommendedInjectedWallets: [
       { name: 'MetaMask', url: 'https://metamask.io' },
       { name: 'Coinbase', url: 'https://wallet.coinbase.com/' }
     ],
     agreement: {
       version: '1.0.0',
       termsUrl: 'https://www.blocknative.com/terms-conditions',
       privacyUrl: 'https://www.blocknative.com/privacy-policy'
     },
     gettingStartedGuide: 'https://blocknative.com',
     explore: 'https://blocknative.com'
   }
 })
 
 // Subscribe to wallet updates
 const wallets$ = onboard.state.select('wallets')
 const { unsubscribe } = wallets$.subscribe(update =>
   console.log('wallets update: ', update)
 )
 
 // unsubscribe when updates are no longer needed
 onDestroy(unsubscribe)
</script>
 
<style>
 button {
   width: 14rem;
   margin: 8px;
 }
</style>
 
<main>
 <div class="cta">
   {#if !$wallets$}
     <button on:click={() => onboard.connectWallet()}>Connect Wallet</button>
   {/if}
 
   {#if $wallets$}
     <button on:click={() => onboard.connectWallet()}
       >Connect Another Wallet</button>
   {/if}
 </div>
 
 {#if $wallets$}
   {#each $wallets$ as { icon, label, accounts, chains, provider }}
     <div class="connected-wallet">
       <div class="flex-centered" style="width: 10rem;">
         <div style="width: 2rem; height: 2rem">{@html icon}</div>
         <span>{label}</span>
       </div>
 
       <div>Chains: {JSON.stringify(chains, null, 2)}</div>
 
       {#each accounts as { address, ens, balance }}
         <div
           class="account-info"
           style="margin-top: 0.25rem; margin-bottom: 0.25rem; padding: 0.25rem; border: 1px solid gray;"
         >
           <div>Address: {address}</div>
           {#if balance}
             <div>Balances:</div>
             {#each Object.entries(balance) as [token, amount]}
               <div style="margin-left: 1rem;">{token}: {amount}</div>
             {/each}
           {/if}
 
           {#if ens}
             <div>ENS Name: {(ens && ens.name) || ''}</div>
           {/if}
         </div>
       {/each}
       <button
         style="margin-top: 0.5rem;"
         on:click={() => onboard.disconnectWallet({ label })}
       >
         Disconnect Wallet
       </button>
     </div>
   {/each}
 {/if}
</main>

With these changes we will see our original view but, once connected, will have a new button on our dapp to connect another wallet.

balance-wallet-interfaceBy clicking “Connect Another Wallet” we can then select Coinbase or Binance Smart Wallet.

metamask-coinbase-bnbchain-wallet-display

With our terminal printing:

wallet-info-terminal

Upon selecting Coinbase we will see the Account Center update and display Coinbase, as the most recently connected wallet becomes the Primary Wallet. This is also reflected by the console.log within the subscription printing the two wallets with the most recent having the 0 index position.

**Note - If we had two MetaMask accounts we could also connect both accounts although the trick here is they would have to be approved on the initial “Connect With MetaMask” confirmation.

metamask-account-selection

We now have two wallets connected to our dapp!

two-wallets-connectedWe can change the primary wallet or account by using the `setPrimaryWallet` function on the `onboard.state.actions` property.

// set the second wallet in the wallets array as the primary
onboard.state.actions.setPrimaryWallet(wallets[1])
 
// set the second wallet in the wallets array as the primary wallet
// as well as setting the third account in that wallet as the primary account
onboard.state.actions.setPrimaryWallet(
 wallets[1],
 wallets[1].accounts[2].address
)

How to execute transactions on your dapp

First we will import ethers from the ethers.js lib (ethers lib documentation). Ethers.js is a lightweight alternative to Web3.js and will allow us to create a new Ethereum (EVM chain) provider to execute transactions.

For this example we'll be using the `setChain` function on the onboard object to ensure the user is on Rinkeby Testnet (‘0x4’). We will also add a button to change chains to Rinkeby for demonstration purposes.

**Note - The chain ID can be a hex encoded string or number. Chain IDs and information can be found at https://chainlist.org/.

We will then create a function called `sendTransaction` that will ensure we are on Rinkeby and switch the user if not and create a new ethers provider.

const sendTransaction = async provider => {
   if (!toAddress) return console.error('No to address specified for transaction')
  
   await onboard.setChain({ chainId: '0x4' })
   const ethersProvider = new ethers.providers.Web3Provider(provider, 'any')
 
   const signer = ethersProvider.getSigner()
 
   const txn = await signer.sendTransaction({
     to: toAddress,
     value: 100000000000000
   })
 
   const receipt = await txn.wait()
   console.log(receipt)
 }

We will then add an input for the address and a button to send the transaction.

This gives us the final code seen below:

<script>
 import Onboard from '@web3-onboard/core'
 import injectedModule from '@web3-onboard/injected-wallets'
 import { onDestroy } from 'svelte'
 import { ethers } from 'ethers'
 
 import blocknativeIcon from './blocknative-icon'
 import blocknativeLogo from './blocknative-logo'
 
 const injected = injectedModule()
 
 const onboard = Onboard({
   wallets: [injected],
   chains: [
     {
       id: '0x1',
       token: 'ETH',
       label: 'Ethereum',
       rpcUrl: 'https://mainnet.infura.io/v3/17c1e1500e384acfb6a72c5d2e67742e'
     },
     {
       id: 4,
       token: 'rETH',
       label: 'Rinkeby',
       rpcUrl: 'https://rinkeby.infura.io/v3/17c1e1500e384acfb6a72c5d2e67742e'
     }
   ],
   appMetadata: {
     name: 'Blocknative',
     icon: blocknativeIcon,
     logo: blocknativeLogo,
     description: 'Demo app for Onboard V2',
     recommendedInjectedWallets: [
       { name: 'MetaMask', url: 'https://metamask.io' },
       { name: 'Coinbase', url: 'https://wallet.coinbase.com/' }
     ],
     agreement: {
       version: '1.0.0',
       termsUrl: 'https://www.blocknative.com/terms-conditions',
       privacyUrl: 'https://www.blocknative.com/privacy-policy'
     },
     gettingStartedGuide: 'https://blocknative.com',
     explore: 'https://blocknative.com'
   }
 })
 
 let toAddress
 const sendTransaction = async provider => {
   if (!toAddress)
     return console.error('No to address specified for transaction')
 
   await onboard.setChain({ chainId: '0x4' })
   const ethersProvider = new ethers.providers.Web3Provider(provider, 'any')
 
   const signer = ethersProvider.getSigner()
 
   const txn = await signer.sendTransaction({
     to: toAddress,
     value: 100000000000000
   })
 
   const receipt = await txn.wait()
   console.log(receipt)
 }
 
 // Subscribe to wallet updates
 const wallets$ = onboard.state.select('wallets')
 const { unsubscribe } = wallets$.subscribe(update =>
   console.log('wallets update: ', update)
 )
 
 // unsubscribe when updates are no longer needed
 onDestroy(unsubscribe)
</script>
 
<style>
 button {
   width: 14rem;
   margin: 8px;
 }
</style>
 
<main>
 <div class="cta">
   {#if !$wallets$}
     <button on:click={() => onboard.connectWallet()}>Connect Wallet</button>
   {/if}
 
   {#if $wallets$}
     <button on:click={() => onboard.connectWallet()}
       >Connect Another Wallet</button>
   {/if}
 </div>
 
 {#if $wallets$}
   <button on:click={() => onboard.setChain({ chainId: '0x4' })}
     >Set Chain to Rinkeby</button>
   {#each $wallets$ as { icon, label, accounts, chains, provider }}
     <div class="connected-wallet">
       <div class="flex-centered" style="width: 10rem;">
         <div style="width: 2rem; height: 2rem">{@html icon}</div>
         <span>{label}</span>
       </div>
 
       <div>Chains: {JSON.stringify(chains, null, 2)}</div>
 
       {#each accounts as { address, ens, balance }}
         <div
           class="account-info"
           style="margin-top: 0.25rem; margin-bottom: 0.25rem; padding: 0.25rem; border: 1px solid gray;"
         >
           <div>Address: {address}</div>
           {#if balance}
             <div>Balances:</div>
             {#each Object.entries(balance) as [token, amount]}
               <div style="margin-left: 1rem;">{token}: {amount}</div>
             {/each}
           {/if}
 
           {#if ens}
             <div>ENS Name: {(ens && ens.name) || ''}</div>
           {/if}
           <div>
             <input
               type="text"
               class="text-input"
               placeholder="0x..."
               bind:value={toAddress}
             />
             <button on:click={sendTransaction(provider)}>
               Send 0.0001 rETH
             </button>
           </div>
         </div>
       {/each}
       <button
         style="margin-top: 0.5rem;"
         on:click={() => onboard.disconnectWallet({ label })}
       >
         Disconnect Wallet
       </button>
     </div>
   {/each}
 {/if}
</main>

Rendering a UI that will look something like:

dapp-interface-wallet-connected

Now you can either copy an address from a second wallet/account or you can use the currently connected account using the Account Center (Click to expand, select menu in the address row - 3 vertical dots, and select “Copy Wallet Address”).

wallet-dapp-interface-copy-address

Paste the address in the address field and click send.

You will be prompted by your wallet to confirm with the gas price included and then your transaction is sent!

If you have included a Blocknative API key (get one for free here) in your initialization, Transaction Notifications will be turned on by default and the user will be updated on the transaction process every step of the way.

How to add transaction notifications to your dapp

Full documentation around Web3-Onboards notifications can be found at https://onboard.blocknative.com/docs/packages/core#options

Transaction UX in the blockchain/crypto space is highly challenging for user’s to say the least. With large values passed through seemingly invisible transactions it can be stressful for a user to know what happens after they click confirm in their wallet and even more challenging to onboard new-to-crypto users, until now. With Blocknative’s infrastructure we allow dapp developers to update user’s on the status of their transaction every step of the way.

Now that we have a simple dapp setup with Blocknative’s Web3 Onboard Core package for Ethereum and EVM Chains (Binance BNB, Polygon, Arbitrum, Optimism, Fantom, etc) we will look into enabling transaction notifications.

Web3 Onboard comes with an array of notification options right out of the box all of which can be customized using the Core package API.

First let’s look at getting setup with Transaction Notifications.

Initializing Notifications

To initialize notifications all a developer has to do is add their `apiKey` to the Onboard Initialization object. Blocknative API keys are completely free to use. If you don’t already have an API key, you can get one by signing up for a free Blocknative account at: https://explorer.blocknative.com/?signup=true

Let’s look at our initialization with that API key added:

init({
 wallets: [injected],
 chains: [
   {
     id: '0x1',
     token: 'ETH',
     label: 'Ethereum',
     rpcUrl: 'https://mainnet.infura.io/v3/${infuraKey}'
   },
   {
     id: '0x4',
     token: 'rETH',
     label: 'Rinkeby',
     rpcUrl: 'https://rinkeby.infura.io/v3/${infuraKey}'
   }
 ],
 apiKey: 'xxxxxx-bf21-42ec-a093-9d37e426xxxx'
})

That’s it!

You are now set up to receive free Transaction Notifications on any transactions on all supported chains.

Let’s take a look at how these are displayed in our UI:

First a “pending” message is displayed that will tell the user how much of what token went to which address. This notification will also include a transaction hash (a link can be added here to link the user to Etherscan for that specific transaction) and elapsed time.

no-address-full-dapp-interface
Once the transaction is confirmed a successful confirmation notification will be sent.

account-success-message
If an error were to arise along the transaction path, the user would also receive a notification of the specific error.

account-failed-to-send

Pretty cool right?!

Users finally get updates in a human readable format on what is happening to their transaction throughout its lifecycle.

That’s what developers get right out of the box by just adding a free Blocknative API key!

Many more notification customizations can be achieved through the notify configuration properties within the onboard initialization object. This includes positioning, enable/disable, adding links and setting the transaction handler - the powerhouse of the notification customizations. The transaction handler is a function that can modify any transaction based on the Ethereum event properties and return a notification object based on those specifications.

  • `Notification.message` - completely customize the message shown
  • `Notification.eventCode` - handle codes in your own way - see codes here under the notify prop [default en file here](src/i18n/en.json)
  • `Notification.type` - icon type displayed (see `NotificationType` below for options)
  • `Notification.autoDismiss` - time (in ms) after which the notification will be dismissed. If set to `0` the notification will remain on screen until the user dismisses the notification, refreshes the page or navigates away from the site with the notifications
  • `Notification.link` - add link to the transaction hash. For instance, a link to the transaction on etherscan
  • `Notification.onClick()` - onClick handler for when user clicks the notification element

An example of this in our sample onboard initialization could look like this:

init({
 wallets: [injected],
 chains: [
   {
     id: '0x1',
     token: 'ETH',
     label: 'Ethereum',
     rpcUrl: 'https://mainnet.infura.io/v3/${infuraKey}'
   },
   {
     id: '0x4',
     token: 'rETH',
     label: 'Rinkeby',
     rpcUrl: 'https://rinkeby.infura.io/v3/${infuraKey}'
   }
 ],
 apiKey: 'xxxxxx-bf21-42ec-a093-9d37e426xxxx'
 notify: {
   enabled: true,
   position: 'topRight',
   transactionHandler: transaction => {
     console.log({ transaction })
     if (transaction.eventCode === 'txPool') {
       return {
         type: 'hint',
         message: 'Your in the pool, hope you brought a towel!',
         autoDismiss: 0,
         link: 'https://ropsten.etherscan.io/tx/${transaction.hash}'
       }
     }
   }
 }
})

Notify can also be styled by using the CSS variables found below. These are setup to allow maximum customization with base styling variables setting the global theme (i.e. `--onboard-grey-600`) along with more precise component level styling variables available (`--notify-onboard-grey-600`) with the latter taking precedence if defined.

This `notify` initialization can also be split into `desktop` and `mobile` properties for more refined customizing of notifications depending on the device.

notify: {
   desktop: {
     enabled: true,
     position: 'topRight',
     transactionHandler: transaction => {
       console.log({ transaction })
     }
   },
   mobile: {
     enabled: true,
     position: 'bottomRight', // mobile defaults to top and bottom so the left/right is ignored in this case
     transactionHandler: transaction => {
       console.log({ transaction })
    }
 }

Adding custom alerts and pre-flight notifications to your dapp

Full documentation on the Notify API for Ethereum and EVM developers can be found https://onboard.blocknative.com/docs/packages/core#options

In this section I’ll walk you through adding customizable alerts and pre-flight notifications to your dapp for a better user experience.

Let’s start with the log within the transactionHandler seen above in the notify initialization object we will add to our onboard initialization. We see this console.log displaying the full, decoded ethereum transaction data from which we can customize our notifications:

terminal-wallet-info
With this information we can customize notifications however we see fit to notify our users. Below we will explore this further.

How to add custom notifications to your dapp

Next let’s dig into one of my favorites, the `customNotification` function on the `onboard.state.actions` object. With this Ethereum developers can send styled and slick looking notifications to their users at any time.

When executing the `customNotification` function two methods are returned, `dismiss` and `update`. Calling `dismiss` will remove the notification and calling `update` the notification can be updated with a new notification on any application trigger.

Let’s add a button to send a custom hint notification to the sample Ethereum Svelte demo we have going and check it out in action.

<button
 on:click={() => {
   const { update, dismiss } = onboard.state.actions.customNotification({
     eventCode: 'dbUpdate',
     type: 'hint',
     message:
       'Custom hint notification created by the dapp',
     onClick: () =>
       window.open('https://www.blocknative.com')
   })
   // Update your notification example below
   setTimeout(
     () =>
       update({
         eventCode: 'dbUpdateSuccess',
         message: 'Hint notification was resolved!',
         type: 'success',
         autoDismiss: 6000 // 6 seconds
       }),
     4000
   )
   setTimeout(
     () =>
       // use the dismiss method returned or add an autoDismiss prop to the notification
       dismiss(),
     10000
   )
 }}
>
 Custom Hint Notification
</button>

First you can see we are calling the `customNotification` function with a CustomNotification object (TypeScript object and all TS types are exported for the TypeScript developers out there!), which will create a notification on the UI with the properties set. This ends up producing a custom notification that looks like this:

custom-dapp-event-message

Now we can perform updates on the notification using the returned methods. In the on:click within our button code you can see we have a couple of `setTimeout` functions, one with 4 seconds and the other with 10. These are to simulate dapp driven actions.

The first setTimeout will call the `update` function after 4 seconds and this takes as a param a CustomNotification object and updates the previously shown notification, in this case, to a success notification. After 10 seconds the `dismiss` function is called which clears the notification.

hint-notification-resolved-1

Further customizing notifications

Now let’s checkout the `updateNotify` function available on the same `onboard.state.actions` API object we have been using up to this point.

This function allows devs to customize the setting of the Notifications component, this includes enabled/disabled, position and the transaction handler. This function takes a `Partial <Notify>` object which is the Notify initialization object that you can include any property optionally. This is the same approach used for positioning the AccountCenter as well.

onboard.state.actions.updateNotify({ position: 'topRight' })

How to add pre-flight notifications to your dapp for a better UX

Finally we will check out the `preflightNotifications` function on the `onboard.state.actions` API object.

With `preflightNotifications` Ethereum dapp developers can add further quality UX to transaction events!

These pre-flight events include:

  • `txRequest` : Alert user there is a transaction request awaiting confirmation by their wallet
  • `txAwaitingApproval` : A previous transaction is awaiting confirmation
  • `txConfirmReminder` : Reminder to confirm a transaction to continue - configurable with the `txApproveReminderTimeout` property; defaults to 15 seconds
  • `nsfFail` : The user has insufficient funds for transaction (requires `gasPrice`, `estimateGas`, `balance`, `txDetails.value`)
  • `txError` : General transaction error (requires `sendTransaction`)
  • `txSendFail` : The user rejected the transaction (requires `sendTransaction`)
  • `txUnderpriced` : The gas price for the transaction is too low (requires `sendTransaction`)

To get these added to your app we need to provide the `preflightNotification` function with a set of properties (`Partial` for TypeScript developers).

For this we will add one more button to send a transaction but this time we will execute the following code chunk:

const sendTransactionWithPreFlight = async (provider, balance) => {
   const balanceValue = Object.values(balance)[0]
   const ethersProvider = new ethers.providers.Web3Provider(provider, 'any')
 
   const signer = ethersProvider.getSigner()
   const txDetails = {
     to: toAddress,
     value: 100000000000000
   }
 
   const sendTransaction = () => {
     return signer.sendTransaction(txDetails).then(tx => tx.hash)
   }
 
   const gasPrice = () =>
     ethersProvider.getGasPrice().then(res => res.toString())
 
   const estimateGas = () => {
     return ethersProvider.estimateGas(txDetails).then(res => res.toString())
   }
 
   const transactionHash = await onboard.state.actions.preflightNotifications({
     sendTransaction,
     gasPrice,
     estimateGas,
     balance: balanceValue,
     txDetails: txDetails
   })
 
   console.log(transactionHash)
 }

We can see here that for full preflightNotification coverage we must pass the `sendTransaction`, `gasPrice` and `estimateGas` functions for the notification component to call along with `balance` and `txDetails`.

`preflightNotifications` then returns a transaction hash if all properties are submitted and the transaction is successfully received or the id of the notification if the extra params are not sent.

This will deliver messages like:

display-dapp-messaging-1
All Together Now

With all the above code snippets in place and utilizing all of the notification API actions we have an App.svelte file that looks like this:

<script>
 import Onboard from '@web3-onboard/core'
 import injectedModule from '@web3-onboard/injected-wallets'
 
 import { recoverAddress, arrayify, hashMessage } from 'ethers/lib/utils'
 import { ethers } from 'ethers'
 import { onDestroy } from 'svelte'
 import blocknativeIcon from './blocknative-icon'
 import blocknativeLogo from './blocknative-logo'
 
 let signMsg = 'Any string message'
 
 const injected = injectedModule({
   custom: [
     // include custom injected wallet modules here
   ],
   filter: {
     // mapping of wallet label to filter here
   }
 })
 
 const onboard = Onboard({
   wallets: [injected],
   chains: [
     {
       id: '0x1',
       token: 'ETH',
       label: 'Ethereum',
       rpcUrl: 'https://mainnet.infura.io/v3/17c1e1500e384acfb6a72c5d2e67742e'
     },
     {
       id: 4,
       token: 'rETH',
       label: 'Rinkeby',
       rpcUrl: 'https://rinkeby.infura.io/v3/17c1e1500e384acfb6a72c5d2e67742e'
     },
     {
       id: 137,
       token: 'MATIC',
       label: 'Polygon',
       rpcUrl: 'https://matic-mainnet.chainstacklabs.com'
     }
   ],
   appMetadata: {
     name: 'Blocknative',
     icon: blocknativeIcon,
     logo: blocknativeLogo,
     description: 'Demo app for Onboard V2',
     recommendedInjectedWallets: [
       { name: 'MetaMask', url: 'https://metamask.io' },
       { name: 'Coinbase', url: 'https://wallet.coinbase.com/' }
     ],
     agreement: {
       version: '1.0.0',
       termsUrl: 'https://www.blocknative.com/terms-conditions',
       privacyUrl: 'https://www.blocknative.com/privacy-policy'
     },
     gettingStartedGuide: 'https://blocknative.com',
     explore: 'https://blocknative.com'
   },
   // example customizing account center
   accountCenter: {
     desktop: {
       position: 'topRight',
       enabled: true,
       minimal: false
     }
   },
   // example customizing copy
   i18n: {
     en: {
       notify: {
         watched: {
           // "txConfirmed": "you paid a foo {formattedValue} {asset}!"
         }
       }
     }
   },
   notify: {
     desktop: {
       enabled: true,
       transactionHandler: transaction => {
         console.log({ transaction })
         // if (transaction.eventCode === 'txPool') {
         //   return {
         //     type: 'hint',
         //     message: 'Your in the pool, hope you brought a towel!',
         //     autoDismiss: 0,
         //     link: 'https://ropsten.etherscan.io/tx/${transaction.hash}'
         //   }
         // }
       },
       position: 'topRight'
     }
   },
   // Sign up for your free api key at www.Blocknative.com
   apiKey: 'xxxxxx-bf21-42ec-a093-9d37e426xxxx'
 })
 
 // Subscribe to wallet updates
 const wallets$ = onboard.state.select('wallets')
 const { unsubscribe } = wallets$.subscribe(update =>
   console.log('wallets update: ', update)
 )
 
 // unsubscribe when updates are no longer needed
 onDestroy(unsubscribe)
 
 let toAddress
 const sendTransaction = async provider => {
   const ethersProvider = new ethers.providers.Web3Provider(provider, 'any')
 
   const signer = ethersProvider.getSigner()
 
   const txn = await signer.sendTransaction({
     to: toAddress,
     value: 100000000000000
   })
 
   const receipt = await txn.wait()
   console.log(receipt)
 }
 
 const sendTransactionWithPreFlight = async (provider, balance) => {
   const balanceValue = Object.values(balance)[0]
   const ethersProvider = new ethers.providers.Web3Provider(provider, 'any')
 
   const signer = ethersProvider.getSigner()
   const txDetails = {
     to: toAddress,
     value: 100000000000000
   }
 
   const sendTransaction = () => {
     return signer.sendTransaction(txDetails).then(tx => tx.hash)
   }
 
   const gasPrice = () =>
     ethersProvider.getGasPrice().then(res => res.toString())
 
   const estimateGas = () => {
     return ethersProvider.estimateGas(txDetails).then(res => res.toString())
   }
 
   const transactionHash = await onboard.state.actions.preflightNotifications({
     sendTransaction,
     gasPrice,
     estimateGas,
     balance: balanceValue,
     txDetails: txDetails
   })
 
   console.log(transactionHash)
 }
 
 const signMessage = async (provider, address) => {
   const ethersProvider = new ethers.providers.Web3Provider(provider, 'any')
 
   const signer = ethersProvider?.getSigner()
   const addr = await signer?.getAddress()
   const signature = await signer?.signMessage(signMsg)
 
   const recoveredAddress = recoverAddress(
     arrayify(hashMessage(signMsg)),
     signature
   )
 
   if (recoveredAddress !== address) {
     console.error(
       "Signature failed. Recovered address doesn' match signing address."
     )
   }
 
   console.log({ signMsg, signature, recoveredAddress, addr })
 }
</script>
 
<style>
 button {
   width: 14rem;
   margin: 8px;
 }
 .connected-wallet {
   padding: 1rem;
   border-radius: 4px;
   margin: 0.5rem;
   border: 1px solid gray;
 }
 
 .flex-centered {
   display: flex;
   align-items: center;
 }
 
 .account-info div {
   overflow: hidden;
   white-space: nowrap;
   text-overflow: ellipsis;
 }
 
 .text-input {
   width: 18rem;
 }
 
 .notify-chain-container {
   display: flex;
   flex-wrap: wrap;
 }
 .switch-chain-container,
 .notify-action-container {
   display: flex;
   flex-direction: column;
   width: 15rem;
 }
</style>
 
<main>
 <div class="cta">
   <button on:click={() => onboard.connectWallet()}gt;Connect Wallet</button>
 
   {#if $wallets$}
     <button
       on:click={() => {
         // If not using notifications call this to update balances
         onboard.state.actions.updateBalances()
       }}>Update Wallet Balance</button>
     <div class="notify-chain-container">
       <div class="notify-action-container">
         <button
           on:click={() =>
             onboard.state.actions.customNotification({
               type: 'hint',
               message: 'This is a custom DApp hint',
               autoDismiss: 0
             })}>Send Hint Notification</button>
         <button
           on:click={() => {
             const { update, dismiss } =
               onboard.state.actions.customNotification({
                 type: 'pending',
                 message:
                   'This is a custom DApp pending notification to use however you want',
                 autoDismiss: 0
               })
             setTimeout(
               () =>
                 update({
                   eventCode: 'dbUpdateSuccess',
                   message: 'Updated status for custom notification',
                   type: 'success',
                   autoDismiss: 0
                 }),
               4000
             )
           }}>Send Success Notification</button>
       </div>
       <div class="switch-chain-container">
         <button on:click={() => onboard.setChain({ chainId: '0x1' })}
           >Set Chain to Mainnet</button>
         <button on:click={() => onboard.setChain({ chainId: '0x4' })}
           >Set Chain to Rinkeby</button>
         <button on:click={() => onboard.setChain({ chainId: '0x89' })}
           >Set Chain to Matic</button>
       </div>
     </div>
   {/if}
 </div>
 
 {#if $wallets$}
   {#each $wallets$ as { icon, label, accounts, chains, provider }}
     <div class="connected-wallet">
       <div class="flex-centered" style="width: 10rem;">
         <div style="width: 2rem; height: 2rem">{@html icon}</div>
         <span>{label}</span>
       </div>
 
       <div>Chains: {JSON.stringify(chains, null, 2)}</div>
 
       {#each accounts as { address, ens, balance }}
         <div
           class="account-info"
           style="margin-top: 0.25rem; margin-bottom: 0.25rem; padding: 0.25rem; border: 1px solid gray;"
         >
           <div>Address: {address}</div>
           {#if balance}
             <div>Balances:</div>
             {#each Object.entries(balance) as [token, amount]}
               <div style="margin-left: 1rem;">{token}: {amount}</div>
             {/each}
           {/if}
 
           {#if ens}
             <div>ENS Name: {(ens && ens.name) || ''}</div>
           {/if}
         </div>
         <div>
           <input
             id="sign-msg-input"
             type="text"
             class="text-input"
             placeholder="Message..."
             bind:value={signMsg}
           />
           <button on:click={signMessage(provider, address)}>
             Sign Message
           </button>
         </div>
 
         <div>
           <input
             type="text"
             class="text-input"
             placeholder="0x..."
             bind:value={toAddress}
           />
           <button on:click={sendTransaction(provider)}>
             Send Transaction
           </button>
         </div>
         <div>
           <input
             type="text"
             class="text-input"
             placeholder="0x..."
             bind:value={toAddress}
           />
           <button on:click={sendTransactionWithPreFlight(provider, balance)}>
             Send with Preflight Notifications
           </button>
         </div>
       {/each}
       <button
         style="margin-top: 0.5rem;"
         on:click={() => onboard.disconnectWallet({ label })}
       >
         Disconnect Wallet
       </button>
     </div>
   {/each}
 {/if}
</main>

And a final UI product that looks like:

final-ui-with-wallet-onboarded

Get started with Web3 Onboard today!

To see our full core library in action, check out the clonable code base here and easily get it running locally by running `yarn && yarn dev`. Or check out our live react demo utilizing react hooks here.

Developers can leverage Web3 Onboard notifications by signing up for a free Blocknative account today.

With built-in modules for more than 35 unique hardware and software wallets and transaction alerts on leading blockchain networks like Ethereum, Polygon, Gnosis Chain, Optimism, and Arbitrum, Web3 Onboard is the easiest, most effective way for developers to add multi-chain support for top web3 wallets like MetaMask, Ledger, Coinbase Wallet or GameStop.

We encourage all web3 developers to join our Discord community and let us know what new features or libraries you would like to see next to make developing with Web3 Onboard easier!

Helpful Links:

Master the Mempool today.

Blocknative's proven & powerful enterprise-grade infrastructure makes it easy for builders and traders to work with mempool data.

Start for free

Want to keep reading?

Good choice! We have more articles.

understanding-ethereum-validators-&-staking
Ethereum

Understanding Ethereum Validators & Staking

Now that the Ethereum Merge is complete, staking Ethereum represents the most important action a..

is-the-block-building-market-doomed-to-monopoly?
Ethereum

Is the Block Building Market Doomed to Monopoly?

PART I Thoughts on Relay diversity and neutrality The Ethereum network is a manifestation of..

what-is-proposer/builder-separation-on-ethereum?
Ethereum

What is Proposer/Builder Separation on Ethereum?

While the Ethereum Merge was significant due to introducing Proof-of-Stake, there was another major..

Connect with us. Build with us.

We love to connect with teams who are building with Blocknative. Tell us about your team and what you would like to learn.

"After first building our own infrastructure, we appreciate that mempool management is a difficult, expensive problem to solve at scale. That's why we partner with Blocknative to power the transaction notifications in our next-generation wallet."

Schedule a demo