Create Publication

We are looking for publications that demonstrate building dApps or smart contracts!
See the full list of Gitcoin bounties that are eligible for rewards.

Tutorial Thumbnail
Beginner · 15 minutes or less

Redux Example: Connect to Algorand Wallet via WalletConnect

dApps are often developed with a frontend framework like React or Vue.js. To let users interact with dApps using their wallet, users need to connect them. Every frontend component of the app should have access to the connected wallet’s information, such as the account balance. These components should also be able to submit transactions to the user’s wallet for review.

In this tutorial, we will go through how to connect to the Algorand Wallet using WalletConnect, and how to let every frontend component access the WalletConnect instance with the use of Redux and React Context.

You can download the source code for this application from the demo github repository.

You can try out the live demo app here.

Requirements

  • IDE, e.g. VSCode
  • NodeJS installed (latest version)
  • JavaScript package manager - either yarn or npm which comes with NodeJS.
  • Other dependencies are listed in package.json. Install them with yarn install or npm install.

Background

Connecting your Algorand mobile wallet to a dApp is simple. You only need to scan a QR code using the Algorand wallet app. This smooth user experience is made possible by the integration with WalletConnect. As developers, we are interested in knowing how we can capture the account data provided by the connected wallet, and how we can interact with the wallet on our dApp.

Algorand has provided a proof-of-concept example for us to see what interactions are possible between the React frontend and the connected wallet. The example is good for experiments yet doesn’t deal with how to share the wallet state across many components. Let’s learn how to do that with the help of Redux and React Context.

Steps

1. Create a React App

First of all, we need a brand new React project. The demo project was created using the create-react-app command. You can learn more about this command here.

First, create react app from a template:

npx create-react-app algorand-wallet-walletconnect-redux

Next, we need to install the Redux toolkit separately.

yarn add @reduxjs/toolkit

Note

If you prefer to start with a template that includes Redux, you can run this instead:
npx create-react-app my-app --template redux

If you would like to start with a TypeScript template, run this:
npx create-react-app my-app --template redux-typescript

After creating our project from a template, let’s install some important packages for interacting with the Algorand blockchain and WalletConnect.

yarn add @walletconnect/client algorand-walletconnect-qrcode-modal algosdk @json-rpc-tools/utils

For the rest of the dependencies listed on package.json, you can run the following command if it is under “dependencies”.

yarn add package-name

Run the following command if it is under “devDependencies”.

yarn add package-name --dev

Note

Installing everything manually will take some time. Alternatively, you can clone the repository.

git clone https://github.com/fionnachan/algorand-wallet-walletconnect-redux.git

You can install all dependencies by running this command.

yarn install

Now, run the app for local development. It will automatically update when there are any changes.

yarn start

Let’s start coding!

2. Basic Layout

We need to create a basic layout for our app with multiple components. Here we are using the frontend component library Evergreen React UI, you can choose to use another library as you wish, e.g. Material UI.

src/App.tsx

...
const App: React.FC = () => {
  const { isModalOpen } = useAppSelector((state) => state.application);
  const dispatch = useAppDispatch();
  const connector = useContext(ConnectContext);

  ...

  return (
    <div>
      <div className="site-layout">
        <SiteHeader />
        <SiteBody />
        <div className="footer">
          Made with 💖 by{" "}
          <a href="https://github.com/fionnachan" target="_blank" rel="noreferrer">
            @fionnachan
          </a>
        </div>
        <Dialog
          isShown={isModalOpen}
          title="Connect to a wallet"
          hasFooter={false}
          onCloseComplete={() => dispatch(setIsModalOpen(false))}
        >
          <Button className="wallet-button" onClick={connect}>
            <img className="wallet-icon" src={algowallet} alt="Algorand wallet" />
            <span>Algorand Wallet</span>
          </Button>
        </Dialog>
      </div>
    </div>
  );
};

export default App;

The code here should be pretty straightforward. Both useAppDispatch and useAppSelector are the typed versions of the main React-Redux hooks we will be using in our React components. useSelector accepts a selector function which extracts a state value from the Redux store. You can read more about useSelector here.

useDispatch is a proxy of the store.dispatch method in our Redux store. We can pass in a reducer function to dispatch an action which will update the Redux state. You can read more about useDispatch here.

Translating this into a React useState hook equivalent, useSelector allows us to access the state value, and useDispatch allows us to set the state value by calling a reducer function.

useContext is a hook to access a React context in any component. More on that soon.

In the above code snippet, setIsModalOpen is a reducer function. We will have a look at our reducer functions for WalletConnect later.

Our connect function has three parts.

const connect = async () => {
  if (connector.connected) return;
  if (connector.pending) return QRCodeModal.open(connector.uri, null);
  await connector.createSession();
};

First we check whether the connection between our dApp and the wallet is already established, if so there is no use connecting again.
If it isn’t we check if the connection between the dApp and the WalletConnect bridge - the websocket that lets a dApp and a wallet communicate - exists. If the dApp is already connected to the bridge we need to give the wallet the connection uri so it too can connect. This is done by opening the QRCodeModal with the connector.uri.
Lastly if the dApp is neither connected to the wallet nor to the bridge we call createSession()

src/store/connector.ts

import WalletConnect from "@walletconnect/client";
import { createContext } from "react";
import QRCodeModal from "algorand-walletconnect-qrcode-modal";

const connectProps = {
  bridge: "https://bridge.walletconnect.org",
  qrcodeModal: QRCodeModal,
};
export const connector = new WalletConnect(connectProps);
export const ConnectContext = createContext(connector);

This file is where we are instantiating our WalletConnect class and creating the React Context. A context is a nifty way to share data across many components without having to systematically pass it as props.

Our top-level App component is then sandwiched in the Redux store and the React context in src/index.tsx:

const renderApp = () =>
  ReactDOM.render(
    <React.StrictMode>
      <Provider store={store}>
        <ConnectContext.Provider value={connector}>
          <App />
        </ConnectContext.Provider>
      </Provider>
    </React.StrictMode>,
    document.getElementById("root"),
  );

As a result our WalletConnect object will be accessible in every component with the following:

const connector = useContext(ConnectContext);

Note

A keen observer could say the connector object was already available without the context since we are exporting it from connector.ts
In our case passing the connector in a context ensures the components update if the WalletConnect instance mutates. We can also imagine advanced scenarios where the context would not hold one but an array of connectors as well as a function to instantiate new WalletConnect objects, for example to manage multiple wallets at once.

3. Creating the Site Header

We would like our users to click on a “Connect to a wallet” button to show the QR code for connection. In this tutorial, clicking the button on the header will bring up a modal - or dialog - to allow future integration with MyAlgo wallet and AlgoSigner.

EditorImages/2021/11/19 11:03/demopopup.png

src/components/SiteHeader/index.tsx
This file is pretty long, so we will break it down into several parts.

1. Imports

Among the first few imports are some helper functions. They are taken from this Algorand Wallet React app example. As for the long list of imports from the /features/ folder, they are our selector and reducer functions to be used with useAppSelector and useAppDispatch.

import React, { useContext, useEffect } from "react";
import { Button, Select } from "evergreen-ui";
import { ellipseAddress, formatBigNumWithDecimals } from "../../helpers/utilities";
import { IAssetData } from "../../helpers/types";
import {
  reset,
  onSessionUpdate,
  getAccountAssets,
  switchChain,
  selectAssets,
} from "../../features/walletConnectSlice";
import { setIsModalOpen } from "../../features/applicationSlice";
import { ChainType } from "../../helpers/api";
import { useAppDispatch, useAppSelector } from "../../store/hooks";
import { ConnectContext } from "../../store/connector";

2. Set up selectors and a helper object

Right after creating our React component, we set up the constants we need.

const SiteHeader: React.FC = () => {
  const { fetching: loading, address, chain } = useAppSelector((state) => state.walletConnect);
  const assets = useAppSelector(selectAssets);
  const dispatch = useAppDispatch();
  const connector = useContext(ConnectContext);
  ...
}

3. Update state values in stores

connector represents the WalletConnect object. If we are connected to a wallet we dispatch the account address to the store. Then we subscribe to emitted events and perform actions accordingly. Finally we are returning a cleanup function.

  useEffect(() => {
    // Check if connection is already established
    if (connector.connected) {
      const { accounts } = connector;
      dispatch(onSessionUpdate(accounts));
    }

    // Subscribe to connection events
    console.log("%cin subscribeToEvents", "background: yellow");
    connector.on("connect", (error, payload) => {
      console.log("%cOn connect", "background: yellow");
      if (error) {
        throw error;
      }
      const { accounts } = payload.params[0];
      dispatch(onSessionUpdate(accounts));
      dispatch(setIsModalOpen(false));
    });

    connector.on("session_update", (error, payload) => {
      console.log("%cOn session_update", "background: yellow");
      if (error) {
        throw error;
      }
      const { accounts } = payload.params[0];
      dispatch(onSessionUpdate(accounts));
    });

    connector.on("disconnect", (error, payload) => {
      console.log("%cOn disconnect", "background: yellow");
      if (error) {
        throw error;
      }
      dispatch(reset());
    });

    return () => {
      console.log("%cin unsubscribeFromEvents", "background: yellow");
      connector.off("connect");
      connector.off("session_update");
      connector.off("disconnect");
    };
  }, [dispatch, connector]);

If either the chain or the address is changed, we would do an asynchronous call to retrieve the account’s Algo and ASA balance.

  useEffect(() => {
    // Retrieve assets info
    if (address?.length > 0) {
      console.log("chain: ", chain);
      dispatch(getAccountAssets({ chain, address }));
    }
  }, [dispatch, address, chain]);

4. The DOM

The following contains a Select dropdown for switching chains, a button for opening the modal, the amount of Algos the address has, the address, and a disconnect button.

EditorImages/2021/11/19 14:14/demositeheader.png

const SiteHeader: React.FC = () => {
  ...
  return (
    <div className="site-layout-background site-header">
      <div className="site-header-inner">
        <div>
          <span>Connected to </span>
          <Select
            value={chain}
            onChange={(event) => dispatch(switchChain(event.target.value as ChainType))}
          >
            <option value={ChainType.TestNet}>Testnet</option>
            <option value={ChainType.MainNet}>Mainnet</option>
          </Select>
        </div>
        {!address ? (
          <Button onClick={() => dispatch(setIsModalOpen(true))}>Connect Wallet</Button>
        ) : (
          <div className="header-address-info">
            {!loading && (
              <span>
                {formatBigNumWithDecimals(nativeCurrency.amount, nativeCurrency.decimals)}{" "}
                {nativeCurrency.unitName || "units"}
              </span>
            )}
            <span className="header-account">{ellipseAddress(address)}</span>
            <Button
              className="disconnect-button"
              onClick={() => connector.killSession().catch((err) => console.error(err.message))}
            >
              Disconnect
            </Button>
          </div>
        )}
      </div>
    </div>
  );
};

export default SiteHeader;

4. Create the Site Body and the Asset Rows

EditorImages/2021/11/19 14:15/demositebody.png

In our app’s body, we are going to show all the assets of the account. These files are pretty clear to read given that you have a basic understanding of React.

src/components/SiteBody/index.tsx

import React from "react";
import { selectAssets } from "../../features/walletConnectSlice";
import { useAppSelector } from "../../store/hooks";
import AccountAssets from "../AccountAssets";
import LoadingIcon from "../LoadingIcon";

const SiteBody: React.FC = () => {
  const loading = useAppSelector((state) => state.walletConnect.fetching);
  const assets = useAppSelector(selectAssets);

  return (
    <div className="site-body">
      <div className="site-body-inner">
        {loading ? <LoadingIcon /> : <AccountAssets assets={assets} />}
      </div>
    </div>
  );
};

export default SiteBody;

src/components/AccountAssets.tsx

import AssetRow from "./AssetRow";
import { IAssetData } from "../helpers/types";

const AccountAssets = ({ assets }: { assets: IAssetData[] }) => {
  const nativeCurrency = assets.find((asset) => asset.id === 0)!;
  const tokens = assets.filter((asset) => asset.id !== 0);
  return (
    <div>
      <h2>Account Balance</h2>
      <AssetRow key={nativeCurrency.id} asset={nativeCurrency} />
      {tokens.map((token) => (
        <AssetRow key={token.id} asset={token} />
      ))}
    </div>
  );
};

export default AccountAssets;

src/components/AssetRow.tsx

import Icon from "./Icon";
import ASAIcon from "./ASAIcon";
import algo from "../assets/algo.svg";
import { formatBigNumWithDecimals } from "../helpers/utilities";
import { IAssetData } from "../helpers/types";

const AssetRow = ({ asset }: { asset: IAssetData }) => (
  <div className="asset-row">
    <div className="asset-info">
      {asset.id === 0 ? <Icon src={algo} /> : <ASAIcon assetID={asset.id} />}
      <span>{asset.name}</span>
    </div>
    <div>
      <div>
        {`${formatBigNumWithDecimals(asset.amount as bigint, asset.decimals)} ${
          asset.unitName || "units"
        }`}
      </div>
    </div>
  </div>
);

export default AssetRow;

6. Create the Reducer functions for the wallet

src/features/walletConnectSlice.ts
As this is the longest file we will go through, we will also study it in several sections.

1. Imports, TypeScript interface, and the inital state values

import { createAsyncThunk, createSelector, createSlice, PayloadAction } from "@reduxjs/toolkit";
import { apiGetAccountAssets, ChainType } from "../helpers/api";
import { IAssetData } from "../helpers/types";
import { RootState } from "../store";

interface WalletConnectState {
  chain: ChainType;
  accounts: string[];
  address: string;
  assets: IAssetData[];
  fetching: boolean;
}

const initialState = {
  accounts: [],
  address: "",
  assets: [
    {
      id: 0,
      amount: "0",
      creator: "",
      frozen: false,
      decimals: 6,
      name: "Algo",
      unitName: "Algo",
    },
  ],
  chain: ChainType.TestNet,
  fetching: false,
} as WalletConnectState;

2. createSlice

We use createSlice to generate action creators and action types by passing in the initial state and reducer functions for our WalletConnect related states. Learn more about createSlice here.

export const walletConnectSlice = createSlice({
  name: "walletConnect",
  initialState,
  reducers: {
    switchChain(state, action: PayloadAction<ChainType>) {
      state.chain = action.payload;
    },
    reset: (state) => ({ ...initialState, chain: state.chain }),
    onSessionUpdate: (state, action: PayloadAction<string[]>) => {
      state.accounts = action.payload;
      state.address = action.payload[0];
    },
  },
  extraReducers(builder) {
    builder.addCase(getAccountAssets.fulfilled, (state, action) => {
      state.fetching = false;
      state.assets = action.payload;
    });
    builder.addCase(getAccountAssets.pending, (state) => {
      state.fetching = true;
    });
  },
});

3. Handling an asynchronous call and saving its response as state value

This is the only asynchronous call we are performing in this tutorial. We will have to use createAsyncThunk to ensure the state value is updated at the right time of the promise lifecycle, i.e. the state value assets is updated when this call is successful and returns the response. This async thunk is used together with the extraReducers above. Learn more about createAsyncThunk here.

export const getAccountAssets = createAsyncThunk(
  "walletConnect/getAccountAssets",
  async ({ chain, address }: { chain: ChainType; address: string }) => {
    return await apiGetAccountAssets(chain, address);
  },
);

4. Selector function

Here we make a custom selector to convert the asset amounts from string back to BigInts. BigInts and all other non serializable objects should not be stored as is in the Redux store and were therefore converted to string in the api callback above.

export const selectAssets = createSelector(
  (state: RootState) => state.walletConnect.assets,
  (assets) => assets.map((a) => ({ ...a, amount: BigInt(a.amount) })),
);

5. Exports

We finally export the actions generated by createSlice to use in the rest of the code base.

export const { switchChain, reset, onSessionUpdate } = walletConnectSlice.actions;

export default walletConnectSlice.reducer;

5. Create the Reducer function for the modal

After going through the reducer functions for the wallet, you should be able to read this file without any difficulties. We are handling the open/close state of the connect wallet modal in this file.

import { createSlice, PayloadAction } from "@reduxjs/toolkit";

const initialState = {
  isModalOpen: false,
};

export const applicationSlice = createSlice({
  name: "application",
  initialState,
  reducers: {
    setIsModalOpen: (state, action: PayloadAction<boolean>) => {
      state.isModalOpen = action.payload;
    },
  },
});

export const { setIsModalOpen } = applicationSlice.actions;

export default applicationSlice.reducer;

7. Configure the Store

We need to set up the store in order to use it in our app. You can add some inital state values to preloadedState if needed. A common example for preloaded state would be the user’s UI preference, e.g. light/dark mode. Learn more about configureStore here.

import { configureStore } from "@reduxjs/toolkit";
import walletConnectReducer from "../features/walletConnectSlice";
import applicationReducer from "../features/applicationSlice";
import logger from "../features/logger";

const store = configureStore({
  reducer: {
    walletConnect: walletConnectReducer,
    application: applicationReducer,
  },
  preloadedState: {},
  middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(logger),
});

export type StoreGetSate = typeof store.getState;
export type RootState = ReturnType<StoreGetSate>;
export type AppDispatch = typeof store.dispatch;

export default store;

8. Play with your app

Now try adding more functions to the template, or integrate with MyAlgo wallet or AlgoSigner.

Demo

Check out the GitHub repository for this tutorial if you haven’t.

You can try out the live demo app here.

Warning

This tutorial is meant for educational purposes. It cannot be used in production without modification.