Skip to main content

How to Build a Token Swap DApp

Learn how to build a token swapping DApp (a simple Matcha.xyz) using the Swap API This dApp aggregates liquidity across the greater DEX ecosystem surfaces the best price to the user to the user.


Have you ever gone onto your favorite token trading dapp to trade ETH for DAI and wondered how did it find the best price for you?

Matcha.xyz

It's more than likely that it uses a liquidity aggregator which sources all the possible prices across off-chain (e.g. Market Makers, Orderbooks) and on-chain (e.g. DEXs, AMMs) and routes the best price for the user.

In this tutorial, we will learn how to use the Swap API which allows users to fetch available quotes across the liquidity supply and uses smart order routing to split up a transaction across decentralized exchange networks to be filled with the lowest slippage possible while minimizing transaction costs.

This is the same endpoint that is behind swaps in major wallets and exchanges such as MetaMask, Coinbase wallet, Zapper, and many more.

Note that we won’t need to write any smart contracts to find and settle the trade! Instead, the Swap API allows web3 developers to easily tap into the 0x Protocol smart contracts which takes care of all the logic used to settle a trade, allowing web developers to focus on building the best trade experience.

By the end of this tutorial, you will learn how to do the following:

  • Understand why Liquidity Aggregation is important
  • Query and display a ERC20 token list
  • Use the Swap API
  • Set a Token Allowance
  • Build a Simple Token Swap DApp that connects to MetaMask using web3.js

Video tutorial version here:

Prerequisites​

To prepare for the rest of this tutorial, you need to have:

The following is not required, but extremely useful:

Here is the github repo if you ever need help.

Now let's get started building our swap dapp!

Part 1. Walk-through Starter Code​

The first thing you'll need to do is to clone the project starter code. We will be building on this starter UI to add in token trading functionality.

info

πŸ“£ Starting in June 2023, all API requests without an API key will return an error. Legacy code examples may contain requests without API keys. Follow this guide for how to get a live API key and use it for any 0x products.

git clone git clone https://github.com/0xProject/swap-demo-tutorial/tree/main/swap-demo-tutorial-part-1
cd swap-demo-tutorial-part-1

Open up the project in your favorite IDE and launch a local development server. If you are using Visual Studio Code, one way to do this is to install the Live Server from extensions on the left-hand panel. Then, right click on a index.html file from Explorer Window and click on Open with Live Server to open the DEX Aggregator skeleton.

VSCode Extension VSCode Extension VSCode Extension

A couple key features to call out (code found in index.html):

  1. Sign-in with MetaMask button - When this app is complete, clicking this button will enable the user to connect to their MetaMask wallet and enable the "Swap" button.
  2. Swap Box 3. SELECT A TOKEN - Currently these just change color when a cursor hovers over them; by the end, users will be able to click and display a list of available tokens to swap. 4. amount - This is an input form that allows user to input a number
  3. Estimated Gas - The swap endpoint will return back estimated gas for the swap to go through. We will display that here.
  4. Swap Button - As mentioned above, the "Swap" button is currently disabled, but we will enable it when the user has signed into MetaMask.

Take a look around these elements in index.js taking note of their ids, class well as their corresponding styling in style.css.

Part 2. Connect to MetaMask​

Now let's enable the user to connect to MetaMask.

First download MetaMask on your browser. MetaMask is a crypto wallet that allows you to interact with blockchain apps. It will hold our funds for us that we will use for the trade. In this app, we only want users to have the ability to carry out a swap if they have a MetaMask wallet installed.

Next, index.js, create a connect() function and connect it to the login_button element. Please read the comments to understand how we check that MetaMask is connected:

async  function  connect() {
/** MetaMask injects a global API into websites visited by its users at `window.ethereum`. This API allows websites to request users' Ethereum accounts, read data from blockchains the user is connected to, and suggest that the user sign messages and transactions. The presence of the provider object indicates an Ethereum user. Read more: https://ethereum.stackexchange.com/a/68294/85979**/

// Check if MetaMask is installed, if it is, try connecting to an account
if (typeof window.ethereum !== "undefined") {
try {
console.log("connecting");
// Requests that the user provides an Ethereum address to be identified by. The request causes a MetaMask popup to appear. Read more: https://docs.metamask.io/guide/rpc-api.html#eth-requestaccounts
await ethereum.request({ method: "eth_requestAccounts" });
} catch (error) {
console.log(error);
}
// If connected, change button to "Connected"
document.getElementById("login_button").innerHTML = "Connected";
// If connected, enable "Swap" button
document.getElementById("swap_button").disabled = false;
}
// Ask user to install MetaMask if it's not detected
else {
document.getElementById("login_button").innerHTML =
"Please install MetaMask";
}
}
// Call the connect function when the login_button is clicked
document.getElementById("login_button").onclick = connect;

Now when you open index.html with the LiveServer, and click the "Sign-In with MetaMask" button, that button should automatically update to "Connected" as show below:

Connected to MetaMask

If MetaMask is not, the text will change to "Please install MetaMask": Please install MetaMask

Great! Now our app can detect when a user has a wallet connect.

Final Code for Part 2

git clone https://github.com/0xProject/swap-demo-tutorial/tree/main/swap-demo-tutorial-part-2

Part 3. Create a Modal for the Token List​

Now let's take a look at how to create a token list when a user clicks on "SELECT A TOKEN". We can use Bootstrap's Modal component to help with this.

Copy and paste the example Modal code from here, and paste it below the last </div>. You can remove the save and close buttons. Also, make sure to add the id="token_modal" so we can reference this modal later. The first line of the modal should look like:

<div  class="modal"  id="token_modal"  tabindex="-1"  role="dialog">

Your index.html should now look like this:

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

<title>Javascript Test</title>
<script src="https://cdn.jsdelivr.net/npm/web3@latest/dist/web3.min.js"></script>
<script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
<script src="https://unpkg.com/moralis/dist/moralis.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js" integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6" crossorigin="anonymous"></script>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
<link rel="stylesheet" href="./style.css">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<a class="navbar-brand" href="#">My DEX Aggregator</a>
<ul class="navbar-nav ml-auto">
<li class="nav-item">
<button id="login_button" class="btn btn-outline-primary my-2 my-sm-0" type="submit">Sign in with MetaMask</button>
</li>
</nav>
<div class ="container">
<div class="row">
<div class="col col-md-6 offset-md-3" id="window">
<h4>Swap</h4>
<div id="form">
<div class="swapbox">
<div class="swapbox_select token_select" id="from_token_select">
SELECT A TOKEN
</div>
<div class="swapbox_select">
<input class="number form-control" placeholder="amount" id="from_amount">
</div>
</div>
<div class="swapbox">
<div class="swapbox_select token_select" id="to_token_select">
SELECT A TOKEN
</div>
<div class="swapbox_select">
<input class="number form-control" placeholder="amount" id="to_amount">
</div>
</div>
<div class="gas_estimate_label">Estimated Gas: <span id="gas_estimate"></span></div>
<button disabled class="btn btn-large btn-primary btn-block" id="swap_button">Swap</button>
</div>
</div>
</div>
</div>
<!-- Add the new modal body here. Note we added id="token_modal" and updated the modal-title -->
<div class="modal" id="token_modal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Select a Token</h5>
<button id="modal_close" type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p>Modal body text goes here.</p>
</div>
</div>
</div>
</div>
<script src="./index.js" type="text/javascript"></script>
</body>
</html>

Now, we need a way to open our modal when we click SELECT A TOKEN, the html element with the id='from_token_select' .

In index.js, let's create a function called openModal() that opens the modal when that element is clicked. Add the following line:

document.getElementById("from_token_select").onclick = openModal;

Next, create the openModal() function:

function  openModal(){
document.getElementById("token_modal").style.display = "block";
}

Now, when you click "SELECT A TOKEN", this modal appears:

Token Modal

There's no way to close the modal now, so let's add that. The modal already has a modal_close element in index.html, this is the X in the top right corner. We will connect it to a function that closes the modal.

In index.js, create a function called closeModal() that closes the modal when that element is clicked. Add the following line:

document.getElementById("modal_close").onclick = closeModal;

Next, create the closeModal() function:

function  closeModal(){
document.getElementById("token_modal").style.display = "none";
}

Final Code for Part 3

git clone https://github.com/0xProject/swap-demo-tutorial/tree/main/swap-demo-tutorial-part-3

Part 4. Fetch and Display Token List from CoinGecko API​

We currently have a modal, but nothing inside. Let's retrieve a list of ERC20 tokens to populate the modal, so that a user can select a token to trade.

Uniswap has the Token Lists project, a standard for creating lists of ERC20 tokens to filter out high quality, legitimate tokens from scams, fakes, and duplicates. Read more about the importance of token lists here.

The CoinGecko list is one of the more robust ones, so we will use this query https://tokens.coingecko.com/uniswap/all.json. I recommend you directly run that in your browser to see what is returned back. You should see a JSON object containing each token's name, logoURI, symbol, and decimals. We will use all these parameters as we build this dapp.

CoinGecko

Now, we want to load the token list before the modal is actually called up; otherwise, the user has to wait for it to load. We can do so by loading it in the init() function which is initialized when the page is first loaded up.

In index.js, create an init() function. Inside, add the following fetch to the CoinGecko API. Also be sure to call it from index.js, for example, I've added it above our onclick calls at the bottom of the index.js file:

async function init(){
console.log("initializing");
let response = await fetch('https://tokens.coingecko.com/uniswap/all.json');
let tokenListJSON = await response.json();
console.log("listing available tokens: ", tokenListJSON);
}
...

// Add init() call
init();

document.getElementById("login_button").onclick = connect;
document.getElementById("from_token_select").onclick = openModal;
document.getElementById("to_token_select").onclick = openModal;
document.getElementById("modal_close").onclick = closeModal;

Now, when you serve your dapp, and inspect it, you should see the token list json object printed out. If this is your first time using Chrome's Inspect DevTool, checkout this article to learn more about it.

Inspect modal

Notice that the token list JSON Object that's returned contains a tokens key, which contains an Array of 4954 JSON objects. We will pull the information from here to populate our token list.

tokens array

Now, rather than creating our token list directly inside init(), we will create a new function listAvailableTokens() to create a list of just what we need - token address, symbol, image, decimals - and call it from init().

Add the following to listAvailableTokens() inside index.js. Make sure to read the comments:

async function listAvailableTokens(){
console.log("initializing");
let response = await fetch('https://tokens.coingecko.com/uniswap/all.json');
let tokenListJSON = await response.json();
console.log("listing available tokens: ", tokenListJSON);
tokens = tokenListJSON.tokens
console.log("tokens:", tokens);

// Create a token list for the modal
let parent = document.getElementById("token_list");
// Loop through all the tokens inside the token list JSON object
for (const i in tokens){
// Create a row for each token in the list
let div = document.createElement("div");
div.className = "token_row";
// For each row, display the token image and symbol
let html = `
<img class="token_list_img" src="${tokens[i].logoURI}">
<span class="token_list_text">${tokens[i].symbol}</span>
`;
div.innerHTML = html;
parent.appendChild(div);
}
}

Also, let's replace the modal body dummy text with the "token_list" element we've just created:

 <div class="modal" id="token_modal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Select a Token</h5>
<button id="modal_close" type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
< !-- Replace the modal text with token_list -->
<div id="token_list"></div>
</div>
</div>
</div>
</div>

Lastly, add this styling for the modal to style.css so that the token list doesn't flow indefinitely, and there's a scrollbar:

.modal-body{
height: 500px;
overflow: scroll;
}

Now your app should look like this when a user clicks "SELECT A TOKEN" and the modal appears:

select token modal

info

You won’t find ETH in this list because it’s not actually an ERC20 token. Needs to be wrapped, WETH. Learn more about WETH. Some apps have a curated list rather than showing all options.

Final Code for Part 4

git clone https://github.com/0xProject/swap-demo-tutorial/tree/main/swap-demo-tutorial-part-4

Part 5. Display Selected Token Image and Symbol in Swap Box​

Now that we have a list of tokens for the user to select, we want to display the selected token image and symbol in the swap box.

First, create a selectToken() function. Call it from the bottom of listAvailableTokens() using an anonymous function when one of the div rows that indicates a token is clicked:

async function listAvailableTokens(){
// ... the rest of the function
div.innerHTML = html;
// selectToken() will be called when a token is clicked
div.onclick = () => {
selectToken(tokens[i]);
};
parent.appendChild(div);
}
}

Before we create the selectToken() function, we need a way to track which side of the token selector we are on. Are on the from-side or the to-side?

To track this, create a global currentTrade object and currentSelectSide variable at the very top of the code in index.js:

// top of index.js
let currentTrade = {};
let currentSelectSide;

Now that we have this global object, we should track whether the user is on the from-side or the to-side depending on which token selector is clicked. Set up this anonymous function that passes "from" when the "from_token_select" is selected at the bottom of index.js:

document.getElementById("from_token_select").onclick = () => {
openModal("from");
};

And update openModal() with the following code:

// index.js
function openModal(side){
// Store whether the user has selected a token on the from or to side
currentSelectSide = side;
document.getElementById("token_modal").style.display = "block";
}

Now, create aselectToken() function and add the following code:

function  selectToken(token) {
// When a token is selected, automatically close the modal
closeModal();
// Track which side of the trade we are on - from/to
currentTrade[currentSelectSide] = token;
// Log the selected token
console.log("currentTrade:" , currentTrade);
}

Run the program and check that currentTrade is logging properly.

currentTrade

Now to display the image and token symbols.

First, add in "from_token_img" , "from_token_text" , "to_token_img" , and"to_token_text" into the index.html: Create a renderInterface() underneath selectToken() and call it from within selectToken().

< !-- Replace the SELECT A TOKEN text -->
< !-- From token -->
<div class="swapbox_select token_select" id="from_token_select">
<img class="token_img" id="from_token_img">
<span id="from_token_text"></span>
</div>

< !-- To token -->
<div class="swapbox_select token_select" id="to_token_select">
<img class="token_img" id="to_token_img">
<span id="to_token_text"></span>
</div>

Inside renderInterface(), we will set the from/to token images and symbol text by calling the associated elements in index.html. Recall that logoURI and symbol are both returned by CoinGeckoAPI:

function selectToken(token) {
closeModal();
currentTrade[currentSelectSide] = token;
console.log("currentTrade:" , currentTrade);
renderInterface();
}

// Function to display the image and token symbols
function renderInterface(){
if (currentTrade.from) {
console.log(currentTrade.from)
// Set the from token image
document.getElementById("from_token_img").src = currentTrade.from.logoURI;
// Set the from token symbol text
document.getElementById("from_token_text").innerHTML = currentTrade.from.symbol;
}
if (currentTrade.to) {
// Set the to token image
document.getElementById("to_token_img").src = currentTrade.to.logoURI;
// Set the to token symbol text
document.getElementById("to_token_text").innerHTML = currentTrade.to.symbol;
}
}

Lastly, add renderInterface() at the end ofselectToken() function:

function  selectToken(token) {
closeModal();
currentTrade[currentSelectSide] = token;
console.log("currentTrade:" , currentTrade);
// Display token image and symbol in swapbox
}

Users can now select both from and to tokens as well as input the amount they would like to trade in our DApp!

Final Code for Part 5

git clone https://github.com/0xProject/swap-demo-tutorial/tree/main/swap-demo-tutorial-part-5

Part 6. Get Price​

Now that users can select their tokens and set the token amount to trade, let's help them find the best price!

One functionality that's common in DeFi trading dapps that's really intuitive is automatically generating a price when the amount is inputted and the user's cursor leaves focus on the amount box. Check it out here in Matcha.xyz.

The DAI price is automatically generated once the WETH price is entered. To do this, we can listen for a Javascript event called onblur. Use it here when the user leaves focus from the "from_amount" element:

Matcha.xyz

// at the bottom of index.js add this
document.getElementById("from_amount").onblur = getPrice;

/price vs /quote Endpoints

Now we need to create a function called getPrice(). Inside, we will be calling the GET /swap/v1/price endpoint.

/price is nearly identical to /quote, but with a few key differences. /price does not return a transaction that can be submitted on-chain; it simply provides us the same information. Think of it as the "read-only" version of /quote".

This is also important because /quote returns back an order in which a Market Maker has committed their assets to. So if we ping /quote too much when we really are just asking for a price and not ready to submit an order, then this can clog up the system!

Here is an example of a GET HTTP /price request. The call is almost identical to /quote: https://api.0x.org/swap/v1/price?sellToken=ETH&buyToken=DAI&sellAmount=1000000000000000000

tip

This API endpoint is using the Ethereum (mainnet) as indicated by https://api.0x.org. Here is a list of all API endpoints for the supported networks.

To use this in our index.js, we need to import the qs module. Add the require statement at the top of the file:

const  qs = require('qs');

Browserify to Use Node Modules in Browser

Now, because we've installed a module using require, our browser will throw a fit if we try to load our index.html now. In order fix this, install Browserify by running this in the terminal:

npm install -g browserify`

Install the [qs module] module with npm:

npm i qs

Now recursively bundle up all the required modules starting at main.js into a single file called bundle.js with the browserify command (Note: You might need to explicitly state the paths for ./index.js and ./bundle.js as specific to your setup).

browserify index.js --standalone bundle -o bundle.js
info

Moving forward, make sure to re-run the Browerify command to generate an updated bundle.js whenever you need to serve the index.html

And make sure to update the src of the script in index.html from src=./index.js to src=./bundles.js:

< !-- Make sure your script now sources from the correct file --> 
<script src="./bundle.js" type="text/javascript"></script>

Build getPrice() Function

Inside index.js, create getPrice(). I will break down the different sections in this function and then show a complete function code snippet at the end.

We add an if statement because we only want to run the /price query if both the from and to tokens have been selected as well as a from token amount has been filled in.

We also get the amount inputted from the user and multiply it by 10 to the power the number of decimals of the from token. For example, if the user inputted they want to trade 1 WETH, WETH has 18 decimal places. The smallest unit of WETH is wei. So the amount they want to trade is (1 x 10 to the power of 18) wei. You can double-check decimals places by looking on EtherScan under Profile Summary here.

async  function  getPrice(){
console.log("Getting Price");
// Only fetch price if from token, to token, and from token amount have been filled in
if (!currentTrade.from || !currentTrade.to || !document.getElementById("from_amount").value) return;
// The amount is calculated from the smallest base unit of the token. We get this by multiplying the (from amount) x (10 to the power of the number of decimal places)
let amount = Number(document.getElementById("from_amount").value * 10 ** currentTrade.from.decimals);
...
}

Next, inside getPrice() we will set our params and fill them into our price quote:

async  function  getPrice(){
...
const params = {
sellToken: currentTrade.from.address,
buyToken: currentTrade.to.address,
sellAmount: amount,
}

const headers = {'0x-api-key: [api-key]'}; // This is a placeholder. Get your live API key from the 0x Dashboard (https://dashboard.0x.org/apps)

// Fetch the swap price.
const response = await fetch(
`https://api.0x.org/swap/v1/price?${qs.stringify(params)}`, { headers }
);
...
}

And lastly, once the response is returned, we will parse out the JSON. The JSON object contains the key/value pairs for buyAmount and estimatedGas which we can directly populate into the "to_amount" and "gas_estimate" html elements appropriately in the UI:

async  function  getPrice(){
...
// Await and parse the JSON response
swapPriceJSON = await response.json();
console.log("Price: ", swapPriceJSON);
// Use the returned values to populate the buy Amount and the estimated gas in the UI
document.getElementById("to_amount").value = swapPriceJSON.buyAmount / (10 ** currentTrade.to.decimals);
document.getElementById("gas_estimate").innerHTML = swapPriceJSON.estimatedGas;
...
}

Now in order to run it, make sure to rerun the Browerify command to generate an updated bundle.js.

Your project should now auto-populate the to-amount and Estimated Gas like this!

estimated gas

Final Code for Part 6

git clone https://github.com/0xProject/swap-demo-tutorial/tree/main/swap-demo-tutorial-part-6

Part 7. Get Quote​

Now for getting Quote setup!

Here is the gameplan for what's left:

  • βœ… Enable the "Swap" button only when MetaMask is connect
  • βšͺ Use the users's MetaMask account address to fetch a quote
  • βšͺ Set a token allowance
  • βšͺ Perform the swap

We have completed the first step in Part (2. Connect to MetaMask) above. Let's create a function to fetch the quote.

getQuote() - Use the Users's MetaMask Account Address to Fetch a Quote

This function will be very similar to getPrice(), the only difference is is that we will pass in a takerAddress, which is the address which will fill the quote. In our case, this is our MetaMask account. You can read more about the takerAddress parameter here.

I will copy and paste the getPrice() code and made the following modifications into getQuote() to pass in and set the takerAddress:

// index.js

// Function to get a quote using /swap/v1/quote. We will pass in the user's MetaMask account to use as the takerAddress
async function getQuote(account){
console.log("Getting Quote");

if (!currentTrade.from || !currentTrade.to || !document.getElementById("from_amount").value) return;
let amount = Number(document.getElementById("from_amount").value * 10 ** currentTrade.from.decimals);

const params = {
sellToken: currentTrade.from.address,
buyToken: currentTrade.to.address,
sellAmount: amount,
// Set takerAddress to account
takerAddress: account,
}

const headers = {'0x-api-key: [api-key]'}; // This is a placeholder. Get your live API key from the 0x Dashboard (https://dashboard.0x.org/apps)

// Fetch the swap quote.
const response = await fetch(
`https://api.0x.org/swap/v1/quote?${qs.stringify(params)}`, {headers}
);

swapQuoteJSON = await response.json();
console.log("Quote: ", swapQuoteJSON);

document.getElementById("to_amount").value = swapQuoteJSON.buyAmount / (10 ** currentTrade.to.decimals);
document.getElementById("gas_estimate").innerHTML = swapQuoteJSON.estimatedGas;

return swapQuoteJSON;
}

Step 2 done!

  • βœ… Enable the "Swap" button only when MetaMask is connect
  • βœ… Use the users's MetaMask account address to fetch a quote
  • βšͺ Set a token allowance
  • βšͺ Perform the swap

Now let's take a look at setting a token allowance

Final Code for Part 7

git clone https://github.com/0xProject/swap-demo-tutorial/tree/main/swap-demo-tutorial-part-7

Part 8. Set a Token Allowance​

Set a Token Allowance A token allowance is required if you want a third-party to move funds on your behalf. In short, you are allowing them to move your tokens.

In our case, we would like the 0x Exchange Proxy smart contract to trade our ERC20 tokens for us, so we will need to approve an allowance (a certain amount) for this contract to move a certain amount of our ERC20 tokens on our behalf.

What's needed to do this:

  • (i) Connect with the ERC20 token's approve() method using a web3 object
  • (ii) Set the approval amount to maxApproval
  • (iii) Use approve() to give our allowanceTarget an allowance for a max amount

(i) Connect with the ERC20 Token's approve() Method Using a web3 Object

All ERC20 tokens are required to implement the approve(address spender, uint256 amount) function. This function sets amount as the allowance of spender over the caller’s tokens (i.e. how many of these ERC20 tokens can the third-party move on the caller's behalf).

It returns a boolean value indicating whether the operation succeeded.

Read more about setting token allowances here.

As seen in approve(address spender, uint256 amount) function above, in order to call the token allowance, we need a couple of things:

  • spender address- This will be the 0x Exchange Proxy Smart Contract address. We can get this from the JSON response as the allowanceTarget parameter returned in our /quote query
  • amount - How much allowance do we want to give the contract? In this case, I will demo with the max amount (maxApproval) possible; however, I highly recommend setting only what you need if possible. Check out this example here for how to implement that (hint, weekly challenge, hint)
  • caller - The user's MetaMask account will be the caller address (aka takerAddress)

Build trySwap()

Let's start building out trySwap() and I will explain each piece along the way. I will post the full code at the end

First, let's get the takerAddress and pass it to getQuote(address) so we can get swapQuoteJSON back to use from the /quote request:

// index.js
async function trySwap(){
// The address, if any, of the most recently used account that the caller is permitted to access
let accounts = await ethereum.request({ method: "eth_accounts" });
let takerAddress = accounts[0];
// Log the the most recently used address in our MetaMask wallet
console.log("takerAddress: ", takerAddress);
// Pass this as the account param into getQuote() we built out earlier. This will return a JSON object trade order.
const swapQuoteJSON = await getQuote(takerAddress);
}

Now let's call the ERC20 token's approve() method. Since will need to interact with the ERC20's contract methods, we need to setup a web3 object.

In order for us to interact with an ERC20 contract's methods, need to create a web3 object, specifically web3.eth.Contract:

const ERC20TokenContract = new web3.eth.Contract(erc20abi, fromTokenAddress);

This object needs a erc20abi which is the json representation of an erc20 contract. The erc20abi is our blueprint for interacting with any token that follows the ERC20 standard. It is represented in JSON format. A quick search should pull up a erc20abi.json since it is a standard. I'm using this one.

The object also needs the specific token address we are interested in interacting with, in this case, it's the fromTokenAddrss because we want the third-party (i.e the 0x Smart Contract) to act on the tokens we want to trade from.

Add the following into trySwap():

// index.js
async function trySwap(){
...
// Setup the erc20abi in json format so we can interact with the approve method below
const erc20abi= [{ "inputs": [ { "internalType": "string", "name": "name", "type": "string" }, { "internalType": "string", "name": "symbol", "type": "string" }, { "internalType": "uint256", "name": "max_supply", "type": "uint256" } ], "stateMutability": "nonpayable", "type": "constructor" }, { "anonymous": false, "inputs": [ { "indexed": true, "internalType": "address", "name": "owner", "type": "address" }, { "indexed": true, "internalType": "address", "name": "spender", "type": "address" }, { "indexed": false, "internalType": "uint256", "name": "value", "type": "uint256" } ], "name": "Approval", "type": "event" }, { "anonymous": false, "inputs": [ { "indexed": true, "internalType": "address", "name": "from", "type": "address" }, { "indexed": true, "internalType": "address", "name": "to", "type": "address" }, { "indexed": false, "internalType": "uint256", "name": "value", "type": "uint256" } ], "name": "Transfer", "type": "event" }, { "inputs": [ { "internalType": "address", "name": "owner", "type": "address" }, { "internalType": "address", "name": "spender", "type": "address" } ], "name": "allowance", "outputs": [ { "internalType": "uint256", "name": "", "type": "uint256" } ], "stateMutability": "view", "type": "function" }, { "inputs": [ { "internalType": "address", "name": "spender", "type": "address" }, { "internalType": "uint256", "name": "amount", "type": "uint256" } ], "name": "approve", "outputs": [ { "internalType": "bool", "name": "", "type": "bool" } ], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ { "internalType": "address", "name": "account", "type": "address" } ], "name": "balanceOf", "outputs": [ { "internalType": "uint256", "name": "", "type": "uint256" } ], "stateMutability": "view", "type": "function" }, { "inputs": [ { "internalType": "uint256", "name": "amount", "type": "uint256" } ], "name": "burn", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ { "internalType": "address", "name": "account", "type": "address" }, { "internalType": "uint256", "name": "amount", "type": "uint256" } ], "name": "burnFrom", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [], "name": "decimals", "outputs": [ { "internalType": "uint8", "name": "", "type": "uint8" } ], "stateMutability": "view", "type": "function" }, { "inputs": [ { "internalType": "address", "name": "spender", "type": "address" }, { "internalType": "uint256", "name": "subtractedValue", "type": "uint256" } ], "name": "decreaseAllowance", "outputs": [ { "internalType": "bool", "name": "", "type": "bool" } ], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ { "internalType": "address", "name": "spender", "type": "address" }, { "internalType": "uint256", "name": "addedValue", "type": "uint256" } ], "name": "increaseAllowance", "outputs": [ { "internalType": "bool", "name": "", "type": "bool" } ], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [], "name": "name", "outputs": [ { "internalType": "string", "name": "", "type": "string" } ], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "symbol", "outputs": [ { "internalType": "string", "name": "", "type": "string" } ], "stateMutability": "view", "type": "function" }, { "inputs": [], "name": "totalSupply", "outputs": [ { "internalType": "uint256", "name": "", "type": "uint256" } ], "stateMutability": "view", "type": "function" }, { "inputs": [ { "internalType": "address", "name": "recipient", "type": "address" }, { "internalType": "uint256", "name": "amount", "type": "uint256" } ], "name": "transfer", "outputs": [ { "internalType": "bool", "name": "", "type": "bool" } ], "stateMutability": "nonpayable", "type": "function" }, { "inputs": [ { "internalType": "address", "name": "sender", "type": "address" }, { "internalType": "address", "name": "recipient", "type": "address" }, { "internalType": "uint256", "name": "amount", "type": "uint256" } ], "name": "transferFrom", "outputs": [ { "internalType": "bool", "name": "", "type": "bool" } ], "stateMutability": "nonpayable", "type": "function" }]
// Set up approval amount for the token we want to trade from
const fromTokenAddress = currentTrade.from.address;

// In order for us to interact with a ERC20 contract's method's, need to create a web3 object. This web3.eth.Contract object needs a erc20abi which we can get from any erc20 abi as well as the specific token address we are interested in interacting with, in this case, it's the fromTokenAddrss
// Read More: https://web3js.readthedocs.io/en/v1.2.11/web3-eth-contract.html#web3-eth-contract
const web3 = new Web3(Web3.givenProvider);
const ERC20TokenContract = new web3.eth.Contract(erc20abi, fromTokenAddress);
console.log("setup ERC20TokenContract: ", ERC20TokenContract);
...
}

(ii) Set the approval amount to maxApproval

In this example, I'll show you how to set the maximum approval amount. I'd recommend checking out this example to see how to set only what's needed.

We will use the BigNumber library to create a really large number. Then we se this to a maxApproval variable. Plug this in at the bottom of trySwap() in index.js

    // The max approval is set here. Using Bignumber to handle large numbers and account for overflow (https://github.com/MikeMcl/bignumber.js/)
const maxApproval = new BigNumber(2).pow(256).minus(1);
console.log("approval amount: ", maxApproval);

(iii) Use approve() to give our allowanceTarget an allowance for a max amount

Now that we can interact with the approve() method and the approval amount, let's use it to grant the allowanceTarget (i.e. the 0x Exchange Proxy smart contract address), an allowance to spend our ERC20 tokens. We can get the allowanceTarget address directly from the JSON object that's returned from our /quote response:

// index.js
async function trySwap(){
...
// Grant the allowance target (the 0x Exchange Proxy) an allowance to spend our tokens. Note that this is a txn that incurs fees.
const tx = await ERC20TokenContract.methods.approve(
swapQuoteJSON.allowanceTarget,
maxApproval,
)
.send({ from: takerAddress })
.then(tx => {
console.log("tx: ", tx)
});
...
}

Test It Out

Now give this a test run!

  • Select a from token (make sure your wallet has enough of that token; otherwise you will get an error)
  • Select a to token
  • Input a from amount (make sure your wallet has at least that amount; otherwise the quote won't carry through)
  • Connect your MetaMask wallet, the "Swap" button should be enabled
  • If you click "Swap" you should get a MetaMask pop-up asking if you approve the allowanceTarget, 0x Exchange Proxy contract address: 0xdef1c0ded9bec7f1a1670819833240f027b25eff !
danger

This transaction does incur fees. Read before you sign.

grant permission

Almost done!

  • βœ… Enable the "Swap" button only when MetaMask is connect
  • βœ… Use the users's MetaMask account address to fetch a quote
  • βœ… Set a token allowance
  • βšͺ Perform the swap

Final Code for Part 8

git clone https://github.com/0xProject/swap-demo-tutorial/tree/main/swap-demo-tutorial-part-8

Part 9: Perform the Swap​

Now for the last part, performing the swap. This part should be very straight-forward since what's passed back from /quote is a JSON object that's ready to be signed and submitted as a valid transaction on the blockchain.

Add this at the end of trySwap():

// index.js
async function trySwap(){
...
// Perform the swap
const receipt = await web3.eth.sendTransaction(swapQuoteJSON);
console.log("receipt: ", receipt);
}

The reason we can directly pass in the /quote response is because it contains all the necessary parameters for web3.eth.setTransaction() - from, to, value, gas, data, etc.

Now if you select a token pair (with a from token that you own), input a from amount, and connect your MetaMask wallet, and hit "Swap", you should get this popup asking if you'd like to make the trade! And with that, your app is complete!

perform the swap

  • βœ… Enable the "Swap" button only when MetaMask is connect
  • βœ… Use the users's MetaMask account address to fetch a quote
  • βœ… Set a token allowance
  • πŸ₯³ Perform the swap

If you go through the entire flow, you will be able to approve the token allowance and perform the swap and receive your newly swapped tokens inside your wallet!

You can also check your transaction on Etherscan!

check etherscan

Final Code for Part 9

git clone https://github.com/0xProject/swap-demo-tutorial/tree/main/swap-demo-tutorial-part-9

Learn More​

Challenges​

Now to take your dapp to the next level! Here are some challenges to try on your own to test your understanding! (Some hints offered in the YouTube video if you need help)

  • Show the percentage breakdown where a swap was sourced from using the sources response param (ex: the best price comes from 50% Uniswap, 50% Kyber)
  • Currently we set the token allowance is set to the max amount. Change this to be safer so the user only approves just the amount needed.
  • Calculate price when a user enters new β€œto” token (right now it only auto-calculates when a user enters a new "from" token)
  • Show estimated gas in $
  • Filter down the long tokens list
  • Allows users to switch chains and receive a proper quote (remember the tokenlist will change as well!)
info

This tutorial is part of Alchemy's Road to Web3 series, a 10-week, self-paced web3 developer program to help new blockchain devs go from beginner to advanced. Check out the original tutorial here.



In order to receive your Proof of Knowledge NFT for this week, share your experience working on the challenges in the Alchemy University Discord.



Once you're done with your challenge, tweet about it by tagging @AlchemyLearn, @0xProject, and the author @hey_its_jlin on Twitter!