0

I am calling a smart contract function to get some status which is the same as calling an API. And, I need to check if status.fulfilled===true before returning it to front-end. To do this I need to call the API every second to return the result as soon as possible. It usually takes 5-20 seconds for it to be fulfilled.

Here is how i tried to do it:

   async function getStatus(requestId) {
    try {
      await Moralis.enableWeb3({ provider: 'metamask' });
      const options = {
        contractAddress: coinFlipAddress,
        functionName: 'getStatus',
        abi: coinFlipABI,
        params: { requestId },
      };
      var status = await Moralis.executeFunction(options);
      console.log(status);
      if (status.fulfilled) {
        console.log('fulfilled');
        return status;
      } else {
        setTimeout(async () => {
          return await getStatus(requestId);
        }, 1000);
      }
    } catch (err) {
      console.log(err);
      return { error: err };
    }
  }

This keeps calling the getStatus function recursively until status.fulfilled===trueand console.log('fulfilled'); also logs when it is fulfilled, but it doesn't return it to where It is first initialized.

  const handleFlip = async (choice) => {
    setCurrentChoice(null);
    setMetamaskInProgress(true);
    const transaction = await flip(choice, amount);
    setMetamaskInProgress(false);
    setCurrentChoice(choices[choice]);
    setFlipping(true);
    setResult('Spinning');
    const requestId = waitForConfirmation(transaction);
    const result = await getStatus(requestId); //This is the initial call to getStatus()
    console.log('RESULT ' + result);
    if (result) {
      setFlipping(false);
      setSide(result.hasWon ? (choice === '0' ? 'Heads' : 'Tails') : choice === '0' ? 'Tails' : 'Heads');
      setResult(result.hasWon ? 'You have won!' : 'You have lost :(');
    }
  };

What am I doing wrong? Also, could this recursive calls create any problems with memory? If yes, do you have any suggestions to handle this case differently?

3 Answers3

0

You cannot return from a setTimeout callback. You'll need to promisify that, wait, and return afterwards:

async function getStatus(requestId) {
  try {
    await Moralis.enableWeb3({ provider: 'metamask' });
    const options = {
      contractAddress: coinFlipAddress,
      functionName: 'getStatus',
      abi: coinFlipABI,
      params: { requestId },
    };
    var status = await Moralis.executeFunction(options);
    console.log(status);
    if (status.fulfilled) {
      console.log('fulfilled');
      return status;
    } else {
      await new Promise(resolve => {
        setTimeout(resolve, 1000);
      });
      return getStatus(requestId);
    }
  } catch (err) {
    console.log(err);
    return { error: err };
  }
}
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
0

I would have done something like this :

const MAX_RETRIES = 10; //maximum retries
const REQUEST_DELAY = 1000; //delay between requests (milliseconds)

// JS Implementation of the wide known `sleep` function
const sleep = (time) => new Promise(res => setTimeout(res, time, "done."));

/**
 * Retrieve the status
 * @param {string} requestId 
 */
const getStatus = async (requestId) => {
    try {
      await Moralis.enableWeb3({ provider: 'metamask' }); //Guessing you need to call this only once
      const options = {
        contractAddress: coinFlipAddress,
        functionName: 'getStatus',
        abi: coinFlipABI,
        params: { requestId },
      };

      let retries = 0;
      while(retries < MAX_RETRIES) {
        let status = await Moralis.executeFunction(options); // check status
        console.log('attemtp %d | status %s', retries, status);
        if (status.fulfilled) {
          return status 
        }
        await sleep(REQUEST_DELAY);
        retries++;
      }
      throw new Error('Unable to retrieve status in time');
    } catch (error) {
      console.error('Error while fetching status', error);
      throw error;
    }
}

Few notes here :

I took the constants out of the function for more clarity, use the while to loop for the number of retries.

Used a widely known 'sleep' method to create a delay between requests, and throw an error whenever something's not supposed to happen, happens (up to you to edit according to your needs).

Finally I used arrow functions for simplicity of use, know it's up to you ;)

Sebastien H.
  • 6,818
  • 2
  • 28
  • 36
-2

You could try this change maxRetry and spaceBetweenRetry as you wish

  async function getStatus(requestId) {
    return new Promise(async (resolve, reject) => {
    try {
      let maxRetry = 10; //how many times you want to retry
      let spaceBetweenRetry = 1000; // sleep between retries in ms
      await Moralis.enableWeb3({ provider: 'metamask' }); //Guessing you need to call this only once
      const options = {
        contractAddress: coinFlipAddress,
        functionName: 'getStatus',
        abi: coinFlipABI,
        params: { requestId },
      };
      for (let index = 0; index < maxRetry; index++) {
        var status = await Moralis.executeFunction(options); // check status
        console.log(status);
        if (status.fulfilled) {
          resolve(status) //return the promise if fullfilled.
        }
        await new Promise((r) => setTimeout(r, spaceBetweenRetry)); // sleep for spaceBetweenRetry ms
      }
    } catch (err) {
      reject(err)
    }
  })}
  • 1
    Thank you very much, this is the way to go. There is one issue: It loops until the max retry even if fulfilled is true. Shouldn't it stop looping after resolve? – Bengican Altunsu Feb 13 '23 at 13:37
  • Avoid the [`Promise` constructor antipattern](https://stackoverflow.com/q/23803743/1048572?What-is-the-promise-construction-antipattern-and-how-to-avoid-it)! There's [no reason to pass an `async function` as the executor to `new Promise`](https://stackoverflow.com/q/43036229/1048572)! – Bergi Feb 13 '23 at 13:46
  • agreed with @Bergi, you should split your code, and use async/await instead, otherwise your code will become harder to read/maintain over time – Sebastien H. Feb 13 '23 at 14:02
  • @SebastienH., Bergi Thank you for your comments. I read the links Bergi attached Could you give me some hints how to convert this example to suggested pattern. Instead of using Promise Constructor should I just use .then with API call? – Bengican Altunsu Feb 13 '23 at 14:25
  • @BengicanAltunsu Just see my answer. You can also loop instead of doing recursive calls, doesn't matter, but just get rid of the `new Promise(async (resolve, reject) => {` wrapper and use `return`. – Bergi Feb 13 '23 at 15:04
  • @BengicanAltunsu just posted an answer, hope it helps – Sebastien H. Feb 15 '23 at 09:55