Beacon wallet

The interface with the Tezos wallet

Beacon is a Tezos toolkit for wallet pairing. It allows wallet developers to integrate with dApps and dApps developers to give more choices to the user.

In order to implement the connect wallet button, and basic blockchain connectivity, we will start our first provider: WalletProvider

Add a providers folder in ./components, and create ./components/providers/WalletProvider.tsx

The imports that will be required:

import React, {
  createContext,
  ReactNode,
  useCallback,
  useContext,
  useEffect,
  useMemo,
  useState,
} from "react"
import { AccountInfo, NetworkType, ColorMode } from "@airgap/beacon-types"
import { TezosToolkit } from "@taquito/taquito"
import { BeaconWallet } from "@taquito/beacon-wallet"
import { set_binder_tezos_toolkit } from "@completium/dapp-ts"

In all our providers, we will define context props, that will represent the properties that will be made available to children using our context. We will then export a useContext shorthand, and the provider itself.

Our wallet provider will provide the following:

interface WalletContextProps {
  connect: () => Promise<void>
  disconnect: () => Promise<void>
  getBalance: () => Promise<void>
  account?: AccountInfo
  wallet?: BeaconWallet
  Tezos?: TezosToolkit
  balance: number
}

The properties are self-explanatory:

  • connect will request a wallet connection using Beacon

  • disconnect will reset the connection

  • getBalance will trigger a refresh of the balance (wallet tez balance)

  • account provides the wallet address, among other things

  • wallet and Tezos are Beacon/Taquito objects that will be used internally to connect the dots

Now that we have the props, let's define our context, with an empty props instance, and prepare the useContext shorthand:

const WalletContext = createContext<WalletContextProps>({
  connect: async () => {},
  disconnect: async () => {},
  getBalance: async () => {},
  balance: 0,
})

const useWalletContext = () => useContext(WalletContext)

The fun part, implementing the provider itself: it will be a React component:

const WalletProvider = ({ children }: { children: ReactNode }) => {
    ...
}

Inside the component, we define the state that will be exposed to the children:

const [Tezos, setTezos] = useState<TezosToolkit>()
const [wallet, setWallet] = useState<BeaconWallet | undefined>()
const [account, setAccount] = useState<AccountInfo>()
const [balance, setBalance] = useState<number>(0)

Tezos toolkit

In React component lifecycle, to update the state when the component loads or when a dependency is updated, we use an effect:

useEffect(() => {
  if (!Tezos) {
    // create Taquito's Tezos toolkit instance and connect it to our RPC
    const Tezos = new TezosToolkit(
      process.env.NEXT_PUBLIC_TEZOS_RPC ?? "http://localhost:20000"
    )
    
    // instantiate the BeaconWallet object
    const beacon = new BeaconWallet({
      name: "TZombies",
      preferredNetwork: (process.env.NEXT_PUBLIC_NETWORK ||
        "ghostnet") as NetworkType,
      colorMode: ColorMode.DARK,
    })
    
    // link the Tezos toolkit with Beacon
    Tezos.setWalletProvider(beacon)
    
    // link the Completium SDK to our toolkit
    set_binder_tezos_toolkit(Tezos)
    
    // a returning user may already have a connected wallet
    beacon.client.getActiveAccount().then(setAccount)
    
    // set the state
    setTezos(Tezos)
    setWallet(beacon)
  }
}, [Tezos])

Let's stop here for a moment and analyse what is going on. This is an important part of the Tezos specifics.

  • First, we create a TezosToolkit instance (from taquito). It will connect to the given node RPC

  • Then we create a BeaconWallet instance and we connect them, as per their documentation

  • We also need to connect the Completium SDK to our TezosToolkit instance

  • If there is already an account connected (a returning user) then the connection will be restored

  • Finally, we set the state of our provider with the account, the toolkit and Beacon

Note that this is only done once, after the page has loaded.

Connect/disconnect

We can now implement the connect and disconnect callbacks:

const connect = useCallback(async () => {
  try {
    await wallet?.requestPermissions({
      network: {
        type: (process.env.NEXT_PUBLIC_NETWORK ?? "ghostnet") as NetworkType,
        rpcUrl: process.env.NEXT_PUBLIC_TEZOS_RPC,
      },
    })
    const active = await wallet?.client.getActiveAccount()
    console.log(active)
    setAccount(active)
  } catch (e) {
    console.error(e)
  }
}, [wallet])

const disconnect = useCallback(async () => {
  await wallet?.clearActiveAccount()
  await wallet?.disconnect()
  setAccount(undefined)
  setBalance(0)
}, [wallet])

Balance update

The following fetches the current account's balance from the Tezos node, and is also triggered when the component loads.

const getBalance = useCallback(async () => {
  if (!Tezos || !account) {
    return
  }
  const balance = await Tezos.tz.getBalance(account.address)
  setBalance(balance.dividedBy(1_000_000).toNumber())
}, [Tezos, account])

useEffect(() => {
  getBalance()
}, [getBalance])

Wrap up

Let's wrap all this state and these methods into a memo that we will pass down to the provider's children:

const value: WalletContextProps = useMemo(
  () => ({
    connect,
    disconnect,
    getBalance,
    account,
    wallet,
    Tezos,
    balance,
  }),
  [connect, disconnect, getBalance, account, wallet, Tezos, balance]
)

return (
  <WalletContext.Provider value={value}>{children}</WalletContext.Provider>
)

At the end of your file, be sure to append:

export { WalletProvider, useWalletContext }

That's it for the first provider. We just need to add it to the app hierarchy in ./pages/_app.tsx such that the App function returns:

<ThemeProvider theme={theme}>
  <CssBaseline />
  <WalletProvider>
    <NavBar />
    <Container sx={{ mt: 12 }}>
      <Component {...pageProps} />
    </Container>
  </WalletProvider>
</ThemeProvider>

This will cause Visual Studio Code to complain that WalletProvider is undefined. This is because the file requires the following import:

import { WalletProvider } from "../components/providers/WalletProvider"

For the remainder of the tutorial, imports for providers and components such as these may be ommitted and it will be left to the reader to include them where necessary.

The context is now usable in any child with:

const walletContext = useWalletContext()

Last updated