How I made a Uniswap interface from scratch
Intro
With the goal of allowing users to easily make swaps between different coins and deploy or remove liquidity on the Autonity network with a UI, rather than using scripts, I decided to make an interface for Uniswap contracts deployed on the network.
The official Uniswap interface proved difficult to fork for the private network, due to its very expansive codebase and problems connecting to a wallet. As I wanted to have a smaller codebase that I understood top to bottom, I decided to write my own simpler application, with the help of my awesome intern Matt.
We used ReactJS for the project, with the EthersJS module to connect to the blockchain via metamask in the browser, and Material-UI for the frontend. As it was a static site, we used github pages to host the application.
This first blog describes the code for the swap part of the application. I’ll first go through the functions needed to connect and make calls and transactions with the Ethereum blockchain backend, and secondly explain the React frontend, which makes extensive use of React hooks and Material-UI. This is written with the expectation that the reader already has a good understanding of ReactJS, as well as a knowledge of how Uniswap V2 automated market makers (AMMs) work (see this article). I won’t explain generic React components such as the NavBar, or the redirect page for when an Ethereum wallet can’t be found, as this blog will be long enough without that!
Checkout the interface here, and the raw code at the repo here.
Please note, this blog describes the state of the interface as of this commit. It has since been updated to have support for multiple networks, and displays different default coins depending on the network, as well as a few other small changes.
Ethereum Functions
Connecting to the blockchain
These functions are needed to connect to the blockchain via a wallet in the browser, to fetch information from the blockchain via calls, and to use contracts on the network to make swaps via transactions.
My original plan was to use the javascript module Web3 to connect to the blockchain with metamask in the browser, but metamask dropped support for it in January 2021, so I instead used EthersJS. EthersJS is a much smaller module than Web3, which means your application will load faster.
In the file ethereumFunctions.js
, the first function is getProvider
, which connects to the Ethereum provider (metamask or another wallet) in the browser, then the function getSigner
, which is used to sign transactions. Using the EthersJS Contract class, are functions that return contract objects for the Router, Weth and Factory contracts that I had deployed onto the blockchain previously. For these the Contract class took the address, ABI of the deployed smart contract and the EthersJS signer as parameters.
Also defined is the function getAccount
, which prompts the user to select accounts to use from the connected wallet.
ethereumFunctions.js
These functions were all imported into the CoinSwapper/CoinSwapper.js
file, and used to set their corresponding state variables using React.useState
hooks, from line 70:
CoinSwapper/CoinSwapper.js
ERC20 token functions
The next two functions in the ethereumFunctions.js
file make calls to the network to fetch information on the token addresses chosen or provided by the user.
doesTokenExist
does a check to make sure the address provided corresponds to a token deployed on the blockchain:
ethereumFunctions.js
getBalanceAndSymbol
checks if a provided address is the address of the Weth contract on the blockchain, in which case it returns the AUT (the native coin of the Autonity blockchain, equivalent to ETH) balance of the user. Otherwise it returns the ERC20 token balance of the user, along with the symbol of the token:
This function checks if the address is the Weth address by comparing it to COINS.AUTONITY.address
, an array of the default tokens exported from constants/coins.js
.
The swap function
The swapTokens
function in ethereumFunctions.js
makes a transaction to the Router contract on the blockchain to perform one of three different swaps (AUT to token, token to AUT, token to token), depending on the token addresses provided to it, address1
and address2
.
- If
address1
is the address of the Weth contract, it calls the Router functionswapExactETHForTokens
- If
address2
is the address of the Weth contract, it calls the Router functionswapExactTokensForETH
- If neither
address1
oraddress2
is the address of the Weth contract, theswapTokens
function calls the Router functionswapExactTokensForTokens
The function getAmountOut
is used to get a preview of a swap. It calls the Router function getAmountsOut
with the amount of the first token and an array of the addresses of the tokens to be swapped as parameters. It returns the amount out of the second token.
The reserves function
Finally, the GetReserves
function in ethereumFunctions.js
returns the liquidity pool reserves for a given pair of tokens, as well as the liquidity token balance for the user. Internally this function calls another function fetchReserves
, which fetches the reserves by making a call to the pair contract then making sure the reserves are returned in the right order.
React Frontend
The frontend of the application makes extensive use of Material-UI components such as Grid
, Container
, Paper
, Typography
, as well as various buttons and more. Rather than explaining the workings of every component in the application, I will aim to give a high level overview, which will make the most sense if you are reading through the code at the same time. If you are not familiar with Material-UI I recommend reading up on some of their great documentation here.
The file CoinSwapper.js
exports a function CoinSwapper
, which returns the React component used to select tokens and make swaps. This function itself makes use of some other custom React components, which I will explain first before going through the internal functions and hooks that make the CoinSwapper
component work.
Coin Dialog component
The CoinDialog
component renders the menu of coins that opens upon clicking one of the ‘SELECT’ buttons. This extends the React Dialog
component, which renders a window that opens in front of the rest of the app, used to ask for a decision. It is defined in CoinSwapper/CoinDialog.js
Inside the CoinDialog
component, there is first a modified version of Material-UI's DialogTitle
component, with the addition of a close button which on click calls the exit
function that closes CoinDialog
.
Next there is a React TextField
component, which enables the user to paste the address of a token to be used. On change, this sets the state variable address
to the user's input.
The next part is a mapping that maps each of the default tokens in Constants/coins
to a custom CoinButton
component (see below), that when clicked calls the exit
function that closes CoinDialog
returning the address of the selected tokens.
Finally, there is the Enter
button, which on click calls the submit
function, which checks a token exists with the address in the TextField
with the Ethereum function doesTokenExist
, before calling exit
to close CoinDialog
returning the address.
CoinSwapper/CoinDialog.js
This component, as well as all the others, makes use of the Material-UI makeStyles
function for the styling, a CSS in JS solution that is easy to use with other Material-UI components and allows for theme nesting and dynamic styles.
Coin Button component
The CoinButton
component, defined in CoinSwapper/CoinButton.js
, extends the Material-UI ButtonBase
component, which contains text of the token symbol (coinAbbr) and token name (coinName), which are passed in via props.
CoinSwapper/CoinButton.js
Coin Field component
The CoinField
component, defined in CoinSwapper/CoinField
, renders the input bar with a "SELECT" button used to select each token and input an amount for swapping. The component is relatively simple and consists of Material-UI components Fab
(floating action button) and InputBase
(a text field), both wrapped in Grid
components for spacing. The relative properties for the Fab
and InputBase
components are passed into the CoinField
component via props.
CoinSwapper/CoinField.js
Loading button
The LoadingButton
component, defined in Components/LoadingButton.js
, renders the ‘SWAP’ button on the bottom of the main CoinSwapper
component. It extends the Material-UI Button
component, so it will be disabled depending on the valid
prop, and displays a spinning loading icon on click until the swap transaction is complete.
Components/LoadingButton.js
Coin Swapper Function
The Return statement
The main function of the application, CoinSwapper
, in CoinSwapper/CoinSwapper.js
returns a boxed in component that contains two CoinDialog
components, which only open when one of two CoinField
components is selected. Next there are Material-UI Typography
components which display the balances and reserves of the two selected tokens.
Finally there is the LoadingButton
component at the bottom, and a short instructional paragraph with a link to the AUT faucet.
CoinSwapper/CoinSwapper.js
When the LoadingButton
is clicked, it calls the internal function swap
(see below), which then calls the swapTokens
Ethereum function with the state variables coin1.address
, coin2.address
and field1Value
as arguments. This will first prompt the user to allow the spending of coin 1 with a metamask notification, then with another notification prompt the user to confirm the transaction. When that is done, the values in field1 and field2 will reset.
State variables
The return statement above references several state variables that keep track of the state of the application. These are:
dialog1Open
— Keeps a record of weather the first dialog window is opendialog2Open
— Keeps a record of weather the first dialog window is opencoin1
— array of address, symbol, balance for coin1coin2
— array of address, symbol, balance for coin2reserves
— Stores the liquidity reserves in the pool for coin1 and coin2.field1Value
— Stores the user’s input in field1 (the value of coin1 to be swapped)field2Value
— Stores the user’s input in field2 (the value of coin2 to be swapped)loading
— boolean to control the loading button
These are defined with React.useState
hooks from line 83:
CoinSwapper/CoinSwapper.js
These state variables are used in the following functions, and also used to display information to the user in the prior mentioned return statement.
Internal functions
Also referenced in the return statement above are several internal functions, these are:
switchFields
handleChange
formatBalance
formatReserve
isButtonEnabled
onToken1Selected
onToken2Selected
swap
switchFields
switches the top and bottom coins. This is called when users hit the swap button or select the opposite token in the dialog (e.g. if coin1 is TokenA and the user selects TokenB when choosing coin2):
handleChange
is a common internal function used in ReactJS, which takes an HTML event, pulls the data out, and puts it into a state variable. Here it used to set the field1 value when the value in the first coinFied
changes:
formatBalance
/ formatReserve
turn the account's balance / reserves into something nice and readable:
isButtonEnabled
determines whether the button should be enabled or not:
onToken1Selected
/ onToken2Selected
are called when the dialog window for coin1 / coin2 exits, and sets the relevant state variables:
swap
calls the swapTokens
Ethereum function to make the swap, then resets necessary state variables:
Line 16 in the swap function uses enqueueSnackbar
, a component from the node module Notistack. Notistack is a great library for making temporary notifications. Check the repo here.
useEffect hooks
Finally, in CoinSwapper/CoinSwapper.js
, there are four useEffect
hooks, which are used to keep the application up to date with the latest changes. The lambda (code) within each hook is run when one of the dependencies changes. The dependencies are defined in the array of variables passed to the function after the lambda expression.
The first useEffect
is called when either of the state variables coin1.address
or coin2.address
change. This means that when the user selects a different coin to convert between, or the coins are swapped, the new reserves will be calculated:
The second hook is called when any of the state variables field1Value
coin1.address
or coin2.address
change. It attempts to calculate and set the state variable field2Value
. This means that if the user enters a new value into the conversion box or the conversion rate changes, the value in the output box will change:
The third hook creates a timeout that will run every ~10 seconds, its role is to check if the user’s balance has updated and changed. This allows them to see when a transaction completes by looking at the balance output:
The final hook will run when the component first mounts. It is used to set the account:
Conclusion
The first section of this blog covered the Ethereum functions needed to make calls and transactions to the blockchain. The next section went through the custom components, internal functions and hooks needed to make the main swap functionality work.
I have yet to mention any of the remove/deploy liquidity functionality. That is the topic of the next blog, here.
To tip the author, you can use this ethereum address: 0xe7EeDB184E63Fe049EebA79EfeAc72939cB1461D