Section: Asynchronous Programming

Post Q&A related to the ‘Asynchronous Programming’ section of the course

@zsolt-nagy @thecil

I finished the videos on the 1Inch API call and got the 1inch API call to work locally.

Question:
When testing Catch with a bad URL, I noticed that the error is caught but PromiseState = ‘fulfilled’.

  • Shouldn’t the PromiseState = ‘rejected’?

  • How should we write this code in order to get ‘rejected’ when the Promise fails?

Here is a screen shot and code

async function parseTokens() {
  try { //Bad url, test catch
    const response = await fetch('https://api.BAD1inch.exchange/v3.0/1/Tokens')
    const tokens = await response.json()

    //1inch JSON hierarchy requires going 2 levels deep to get the value objects
    const tokenList = Object.values(tokens.tokens)

    //build the html <li>s
    const listItems = tokenList
      .filter((token) => token.decimals === 6)
      .map(
        (token) =>
          `<li>${token.symbol}, ${token.name}, decimals: ${token.decimals}, address: ${token.address}</li>`
      )
    
    //print the tokens to the browser
    const ul_tokens = document.querySelector('.tokens')
    ul_tokens.innerHTML = `${listItems.join('')}`
  } catch (e) {

    //TODO: on error, the PromisesState = 'fulfilled'. Why??
    console.log(`error: ${e}`)
  }
}

Nevermind. I researched this more and found the answer on MDN (ofc):
https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch

“The Promise returned from fetch() won’t reject on HTTP error status even if the response is an HTTP 404 or 500. Instead, as soon as the server responds with headers, the Promise will resolve normally (with the ok property of the response set to false if the response isn’t in the range 200–299), and it will only reject on network failure or if anything prevented the request from completing.”

@zsolt-nagy @thecil

Help :slight_smile:. I’ve been working on the 1inch quoting assignment for many days and I’m almost there. However, I have run into blockers that I have not been able to figure out.

Blockers/Questions

  1. I am putting the fetchQuote() response object in a global ‘quote’ variable, which I can read just fine from console.log in the browser. However, inside displayQuoteInfo(), the ‘quote’ variable is undefined so I am unable to get the property values on order to paint the Quote Info Box. See screenshot below

  2. I have seven bootstrap functions that need to run sequentially after the browser loads. How is that done? I have tried putting the functions inside window.onload() but that did not work. I also tried DOMContentLoaded/ready pattern and that did not work either. Not sure what else to try.

  3. Currently, I am pasting each function manually, sequentially into the browser console to test. Very tedious. I am ready to try out a test runner. Which do you recommend? I am looking here:
    https://2020.stateofjs.com/en-US/technologies/testing/

Open to any other suggestions. I definitely could benefit from a code review.

Thanks much!

Screenshot related to #1 above:

GitHub Link: https://github.com/ren132710/erc20Quote

js code:

const rank = 50
let geckoTokens = []
let oneInchTokens = []
let topERC20Tokens = []
let tickers = {}
//TODO: #1 quote variable seems receives fetch response object only if type is not assigned. Not sure why
//TODO: #2 However, subsequently quote is undefined when called by the displayQuoteInfo()
//TODO: although works fine from the browser console.log()
//TODO: See screenshot
//let quote = {}
let quote

/*
TODO: #3 How to get API and Bootstrap functions to run sequentially after the document loads?
TODO: I tried window.load and cannot get these function to successfully populate the global variables

TODO: #4 Testing. Currently, I am pasting each function manually, sequentially into the browser console to test.
TODO: Very tedious
TODO: I want to try a javascript test runner. Which test runner is recommended?
TODO: I am looking here: https://2020.stateofjs.com/en-US/technologies/testing/

These api and bootstrap functions need to fire after the initial page load:
fetchGeckoTokens()
fetchOneInchTokens()
generateTopERC20Tokens()
generateTickers()
populateTickerLists()
setDefaultTickers()
displayDefaultQuote()
*/

window.onload = function () {
  console.log('page is fully loaded')
  fetchGeckoTokens()
  fetchOneInchTokens()
  generateTopERC20Tokens()
  generateTickers()
  populateTickerLists()
  setDefaultTickers()
  displayDefaultQuote()
}

/*
 * API Calls
 */

async function fetchGeckoTokens() {
  try {
    const response = await fetch(
      `https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&order=market_cap_desc&per_page=${rank}&page=1`
    )
    const tokens = await response.json()
    geckoTokens = tokens

    //put tokens in PromiseResult
    return tokens
  } catch (e) {
    console.log(`error: ${e}`)
  }
}

async function fetchOneInchTokens() {
  try {
    const response = await fetch('https://api.1inch.exchange/v3.0/1/Tokens')
    const tokens = await response.json()

    //1inch JSON hierarchy requires going 2 levels deep to get the value objects
    const tokenList = Object.values(tokens.tokens)
    oneInchTokens = tokenList

    //put tokenList in PromiseResult
    return tokenList
  } catch (e) {
    console.log(`error: ${e}`)
  }
}

/*
 * Defaults for testing
 * fromTokenAddress : "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" //ETH
 * toTokenAddress : "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48" //USDC
 * fromCurrencyUnit : "100000000000000000"
 * fetchQuote('0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', '100000000000000000')
 */

async function fetchQuote(fromTokenAddress, toTokenAddress, fromCurrencyUnit) {
  try {
    const response = await fetch(
      `https://api.1inch.io/v4.0/1/quote?fromTokenAddress=${fromTokenAddress}&toTokenAddress=${toTokenAddress}&amount=${fromCurrencyUnit}`
    )
    const quoteObj = await response.json()
    quote = quoteObj

    //put quoteObj in PromiseResult
    return quoteObj
  } catch (e) {
    console.log(`error: ${e}`)
  }
}

/*
 * Page Bootstrap Functions
 */

function generateTopERC20Tokens() {
  let arr = []

  //cool! destructuring works! :-)
  geckoTokens.forEach(({ symbol }) => {
    //.find() is case sensitive, 1inch symbols are upper case, so toUpperCase()
    let geckoSymbolUpper = symbol.toUpperCase()

    //gotta have BTC, so substitute WBTC
    if (geckoSymbolUpper === 'BTC') {
      geckoSymbolUpper = 'WBTC'
    }

    //if 1inch matches Gecko, push to array
    const foundToken = oneInchTokens.find(({ symbol }) => {
      return symbol === geckoSymbolUpper
    })
    if (!(foundToken == null)) {
      arr.push(foundToken)
    }
  })
  topERC20Tokens = arr
  return
}

function generateTickers() {
  let arr = []
  topERC20Tokens.forEach(({ symbol }) => {
    let ticker = symbol
    arr.push(ticker)
  })
  tickers = arr
  return
}

function populateTickerLists() {
  const fromTicker = document.querySelector('#fromTicker')
  const toTicker = document.querySelector('#toTicker')
  let optionsArr = tickers.map((ticker) => {
    return `<option value='${ticker}'>${ticker}</option>`
  })
  const optionsList = optionsArr.join('')
  fromTicker.innerHTML = optionsList
  toTicker.innerHTML = optionsList
  return
}

/*
 * Let's present a default quote, so
 * set ETH as the default fromTicker
 * set USDC as the default to toTicker
 */

function setDefaultTickers() {
  const fromTickerDefault = document.querySelector(
    "#fromTicker > [value = 'ETH']"
  )
  //empty '' sets the attribute to true
  fromTickerDefault.setAttribute('selected', '')

  const toTickerDefault = document.querySelector("#toTicker > [value = 'USDC']")
  toTickerDefault.setAttribute('selected', '')
}

function displayDefaultQuote() {
  getQuote()
  return
}

/*
 * Quote Functions
 */

function getQuote() {
  let isQuoteSuccessful = false
  let tickers = getTickerSelection()
  let fromTokenAddress = getTokenAddress(tickers.fromTicker).toString()
  console.log(fromTokenAddress)
  let fromCurrencyUnit = getFromCurrencyUnit(tickers.fromTicker).toString()
  console.log(fromCurrencyUnit)
  let toTokenAddress = getTokenAddress(tickers.toTicker).toString()
  console.log(toTokenAddress)
  //let response = fetchQuote(fromTokenAddress, toTokenAddress, fromCurrencyUnit)
  fetchQuote(fromTokenAddress, toTokenAddress, fromCurrencyUnit)
  // console.log(response)
  // quote = response
  //displayQuoteInfo(quote)
  displayQuoteInfo()

  isQuoteSuccessful = true
  return isQuoteSuccessful
}

function getTickerSelection() {
  let from = document.querySelector('#fromTicker').value
  let to = document.querySelector('#toTicker').value
  return {
    fromTicker: from,
    toTicker: to,
  }
}

function getTokenAddress(ticker) {
  const foundToken = topERC20Tokens.find((item) => {
    return item.symbol === ticker
  })
  return foundToken.address
}

function getFromCurrencyUnit(ticker) {
  const foundToken = topERC20Tokens.find((item) => {
    return item.symbol === ticker
  })
  let unit = 1
  let decimals = foundToken.decimals
  let fromCurrencyUnit = zeroPad(unit, decimals)
  return fromCurrencyUnit
}

function displayQuoteInfo() {
  const spanFromLabel = document.querySelector('#fromLabel')
  const spanToTokenAmount = document.querySelector('#toTokenAmount')
  const spanToLabel = document.querySelector('#toLabel')
  const spanFromTokenAmount = document.querySelector('#fromTokenAmount')
  const spanEstimatedGas = document.querySelector('#estimatedGas')

  //TODO: Fails here ****************
  //TODO: quote returns 'undefined', however console.log(quote) in the browser DOES return the quote object??
  console.log(quote)
  spanFromLabel.innerText = `1 ${quote.fromToken.symbol} costs approximately`
  spanToTokenAmount.innerText = `${quote.toTokenAmount} ${quote.toToken.symbol}`

  spanToLabel.innerText = `1 ${quote.toToken.symbol} costs approximately`

  //TODO: Let's see if doing math on a string will fail
  spanFromTokenAmount.innerText = `${1 / quote.toTokenAmount} ${
    quote.fromToken.symbol
  }`

  spanEstimatedGas.innerText = `${quote.estimatedGas} GWEI`
}

function zeroPad(num, decimals) {
  return num.toString().padEnd(decimals + 1, '0')
}

1 Like

Hey @pivot, hope you are great.

I have downloaded your github repo, run the index.html on my browser (using VS live server) but i think the code is outdated, i copy/paste the JS code you provided here but still not getting the same error than you.

Maybe i just have to add something else? The fetchOneInchTokens function it does return the list of tokens that should be used.

Carlos Z

1 Like

Hi @thecil…thanks for the response and for downloading the code.

Yeah…to get the error in the screen shot you actually have to copy and paste sequentially, one-by-one, each of the bootstrap functions into the console. Currently, that is the only way I am able to build the page. Hence, my question about how to get the page to load automatically.

Now, after watching more videos, I realize that it’s because I don’t have the asynchronous programming set up correctly. I wanted to do the assignment first without seeing the solution. I think I need to chain the bootstrap functions together inside an async function() in some way, with await before each function.

The bootstrap functions are currently listed inside the window.onload function, which is the last approach I tried, and as you saw, does not work.

I’m going to watch the rest of the videos to understand Zsolt’s solution and do some more reading on MDN. Then I will try to refactor and get it to work.

However, open to your thoughts/hints regarding best approach.

Also your thoughts about my other questions…such as recommended javascript testing frameworks. Again, I am looking here: https://2020.stateofjs.com/en-US/technologies/testing/

Thanks!

PS: I commented out the window.onload function and pushed to Github. Download latest and then you can manually build the page by pasting each of the bootstrap functions into the browser console one-by-one, as I describe above. Then you will get the error.

1 Like

I might advice Mocha, which also will help you with Solidity smart contracts unit tests, but keep in mind this, if you are learning programming, i advice you to go slow, step by step with the course, try not to diversify your learning curve that much because you might end confused with too many concepts.

You are going quite well with the course, but try not to loose yourself into the programming learning forest :nerd_face:

I will try your updated repo and get back to you again, any other issue you face, just upgrade the repo and will check together.

Carlos Z

1 Like

Hi @thecil…OK. thanks for the advice. I hear you. Stay grounded. It’s a hard balance! :slight_smile:

  1. I started out with ETH 101 and ETH 201 (201 I still need to finish), and then decided I really need/want to understand and become reasonably competent at the frontend before heading back to Smart Contracts, since at some point we need to interface with users.

  2. And I’m glad I did that. I didn’t realize how versatile and powerful javascript is. I’m guessing javascript is a must know language in the Web3 world…regardless of favorite chain.

  3. I am following Zsolt’s course but trying to dig a bit deeper on the topics he introduces, which Zsolt encourages.

  4. Today, I spent most of the day on MDN and YouTube digging deeper into callbacks, Promises and Asynchronous programming. Now I realize that the core problem with my solution is that my page initialization is synchronous, not asynchronous. Also, getQuote() should be asynchronous and there should be an await on fetchQuote(). I believe this is what is causing the error in the screen shot.

  5. So…I need to do a bunch of refactoring and, man, do I wish I had tests to back me up :). I am a big believer in TDD (I’ve worked in the software world, but on the business/product/consulting side) and aim to learn how to code this way until it becomes habitual.

  6. RE: Mocha. OK. Solidity friendly makes sense. I was using Hardhat and Chai with my ETH 201 tests, and found those test to be a pleasure to write. I’m assuming I can use Mocha and Chai with Javascript? Can you recommend any tutorials?

Finally, I think I understand the changes I need to make in order to get the my solution to initialize and make the error above go away. Let me take a crack at it. If I still run into issues, I will ping you.

Either way, when I’m done I would love a code review :slight_smile:

Thanks much and cheers!

1 Like

Hey @thecil…I got page initialization to work properly. I understand asynchronous programming now :slight_smile: haha

Here is the Github link: https://github.com/ren132710/erc20Quote

When you have a moment, please take a look and let me know your thoughts, comments, suggestions for improvement. Best!

I’m getting the CORS error (inconsistently). How do we avoid this error?

1 Like

Im not getting the same error, i download your repo, start a live server and check if the console trigger any error.

Mainly that error comes from a bad request, could be just an error in the request, but it does not appear all the time.

Carlos Z

1 Like

@thecil. OK, thanks much for checking it out.

Hey @zsolt-nagy…when you have a moment, would you please do a quick review of my solution to the Fetch() quote assignment and let me know if you spot anything particularly amateurish?

Here’s the link: https://github.com/ren132710/erc20Quote

In particular, I have questions about whether I am doing the Try/Catch error handling correctly. For instance:

  • On line #116, do I need a .catch() at the end of the .then() initialization chain, or would that be redundant?

  • Should there also be a Try/Catch inside the getQuote() async function (line 158), or is that not needed since it calls fetchQuote() which has the Try/Catch.

Any general comments and suggestions would be appreciated.

Thanks!

I will address your two questions.

Line 116: given fetchTopTickers may fail e.g. due to network outage or server error, a catch is always desirable at the end to inform the end user on why they can’t get data.

Line 158: it depends. If you have a specific error that you want to locally handle in that function, you can add a try-catch locally. However, if one generic .then().then().catch() handler is sufficient for all errors, then that catch call should also catch errors thrown inside your getQuote function.

1 Like

Hello, thank you for the prompt reply. I did not find this post. Could you pass me the link? Because if it is not open and I have to create it, I did not find how to do it. Thank you very much

1 Like

Hello. For the API exercise, I have problems with having the coins in the dropdown menu. Can someone tell me what the problem is please? Below are my codes:

HTML

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Top 10 coins exchange</title>
    <link rel="stylesheet" href="./APIex2.css">
</head>
<body>
  <form action="#" method="POST" class="exchange-form">
    <div class="form-row">  
        <label>
        From:
            <select name="from-token"></select>>
        </label>
    </div>
    <div class="form-row">
        <label>
        To: 
            <select name="to-token"></select>>
        </label>
    </div>
    <div class="form-row">
        <button type="submit" class="js-submit-quote">Get Quote</button>
    </div>
  </form>
  <script src="./APIex2.js"></script>

    <p>conversion rate: </p>
    <p>estimated gas: </p>
</body>
</html>

JS

async function getTop10Tokens() {
    const response = await fetch('https://api.coinpaprika.com/v1/coins');
    const tokens = await response.json();

    return tokens
            .filter(token => token.rank >= 1 && token.rank <= 10)
            .map(token => token.symbol);
}

async function getTickerData(tickerList) {
    const response = await fetch('https://api.1inch.exchange/v3.0/56/tokens');
    const tokens = await response.json();
    const tokenList = Object.values(tokens.tokens);

    return tokenList.filter(token => tickerList.includes(token.symbol));
}

function renderForm(tokens) {
    const options = tokens.map(token =>
        `<option value="${tokens.decimals}-${token.address}">${token.name} (${token.symbol})</option>`);
    document.querySelector('[name=from-token]').innerHTML = options;
    document.querySelector('[name=to-token]').innerHTML = options;
    document.querySelector('.js-submit-quote').removeAttribute('disabled');
}

async function formSubmitted(event) {
    event.preventDefault();
    const fromToken = document.querySelector('[name-from-token]').value;
    const toToken = document.querySelector('[name-to-token]').value;
    const [fromDecimals, fromAddress] = fromToken.split('-');
    const [toDecimals, toAddress] = toToken.split('-');
    const fromUnit = 10 ** fromDecimals;
    const decimalRatio = 10 ** (fromDecimals - toDecimals);

    const url = `https://api.1inch.io/v4.0/56/quote?fromTokenAddress=${fromAddress}&toTokenAddress=${toAddress}&amount=10000000000000000`

    try {
        const response = await fetch(url);
        const quote = await response.json();
        const exchange_rate = Number(quote.toTokenAmount) / Number(quote.fromTokenAmount) * decimalRatio;;
        document.querySelector('.js-quote-container').innerHTML = 
            <p>1 ${quote.fromToken.symbol} = ${exchange_rate} ${quote.toToken.symbol}</p>
            <p>Gas fee: ${quote.estimatedGas}</p>
        ;
    } catch(e) {
        document.querySelector('.js-quote-container').innerHTML = `The conversion didn't succeed.`;
    }
}

document
    .querySelector('.js-submit-quote')
    .addEventListener('click', formSubmitted);

getTop10Tokens()
    .then(getTickerData)
    .then(renderForm);

CSS

*{
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    background-color: antiquewhite; 
}

.exchange-form {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    margin-left: 4rem;
}

.form-row {
    margin: 0.25rem;
}

Also, may I know why there are red curly underlines for my two

lines? I can’t find what the problem is.

Thanks!

Also, can someone explain the lines:

const fromUnit = 10 ** fromDecimals;
const decimalRatio = 10 ** (fromDecimals - toDecimals);

and

const exchange_rate = Number(quote.toTokenAmount) / Number(quote.fromTokenAmount) * decimalRatio;

Does the latter means exchange rate is equals to (amount of To token) / (amount of From token) X Decimal Ratio? I don’t understand why this is the equation of exchange rate. Isn’t the exchange rate equals to (price of From token) / (price of To token)? Is this something I should stress about?

Hi everybody, I’m following the videos and in “Map filter” section, I tried to call the parseTokens() function but appears this message. Error: ReferenceError: Cannot access ‘listItems’ before initialization. Why occurs that?

async function parseTokens() {
        try {
            let response = await fetch('https://api.1inch.exchange/v3.0/1/tokens');
            let tokens = await response.json();
            let tokenList = Object.values(tokens.tokens);
            let listItems = tokenList.map(token => {
                `<li>${token.name} (${token.symbol}): ${token.address}</li>`;
                document.body.innerHTML += `<ul>${listItems.join('')}</ul>`;
            })
        } catch(e) {
            console.log(`Error: ${e}`);
        }
 }

This codeline should be outside the listItems function body. Because you must initialize its content first, then apply any change.

async function parseTokens() {
    try {
        let response = await fetch('https://api.1inch.exchange/v3.0/1/tokens');
        let tokens = await response.json();
        let tokenList = Object.values(tokens.tokens);
        console.log(tokenList)
        let listItems = tokenList.map(token => `<li>${token.name} (${token.symbol}): ${token.address}</li>`);
        document.body.innerHTML += `<ul>${listItems.join('')}</ul>`;
    } catch(e) {
        console.log(`Error: ${e}`);
    }
}

Carlos Z

oh Thank you very much !! you’re right