[SOLVED] Need to Approve More Than Once

import { useWeb3Contract } from "react-moralis"
import {
    wethAbi,
    wethAddress,
    mutualsAbi,
    mutualsAddress,
    achieverAbi,
    achieverAddress,
} from "../../constants"
import { Form } from "web3uikit"
import { ethers } from "ethers"
import { useState } from "react"
export default function MutualsSwapForm() {
    const { runContractFunction } = useWeb3Contract()
    let achieverApproveOptions = {
        abi: achieverAbi,
        contractAddress: achieverAddress,
        functionName: "approve",
    }

    let wethApproveOptions = {
        abi: wethAbi,
        contractAddress: wethAddress,
        functionName: "approve",
    }

    let swapOptions = {
        abi: mutualsAbi,
        contractAddress: mutualsAddress,
        functionName: "swap",
    }

    const [swapResult, setSwapResult] = useState("0")

    const { runContractFunction: getReserve0 } = useWeb3Contract({
        abi: mutualsAbi,
        contractAddress: mutualsAddress,
        functionName: "reserve0",
        params: {},
    })

    const { runContractFunction: getReserve1 } = useWeb3Contract({
        abi: mutualsAbi,
        contractAddress: mutualsAddress,
        functionName: "reserve1",
        params: {},
    })

    async function handleSwapSubmit(data) {
        console.table({ data })
        let selectVal = document.querySelector("#swap-form select").value
        let approveOptions
        const amountToApprove = document.querySelector("#input_1").value

        if (selectVal == "WETH") {
            wethApproveOptions.params = {
                wad: ethers.utils.parseUnits(amountToApprove, "ether").toString(),
                guy: mutualsAddress,
            }
            approveOptions = wethApproveOptions
        } else {
            achieverApproveOptions.params = {
                amount: ethers.utils.parseUnits(amountToApprove, "ether").toString(),
                spender: mutualsAddress,
            }
            approveOptions = achieverApproveOptions
        }

        console.log("Approving...")
        console.log(approveOptions, "a")
        const tx = await runContractFunction({
            params: approveOptions,
            onError: (error) => console.log(error),
            onSuccess: () => {
                handleApproveSuccess(
                    approveOptions.contractAddress,
                    ethers.utils.parseUnits(amountToApprove, "ether").toString()
                )
            },
        })
    }

    async function handleApproveSuccess(contractAddress, amountToSwapFormatted) {
        swapOptions.params = {
            _amountIn: amountToSwapFormatted,
            _tokenIn: contractAddress,
        }
        console.log({ swapOptions })
        console.log(`Swapping ${swapOptions.params._amountIn} ...`)
        const tx = await runContractFunction({
            params: swapOptions,
            onError: (error) => console.log(error),
        })
        console.log({ tx })
        await tx.wait(1)
        console.log("Transaction has confirmed by 1 block")
    }

    async function handleSwapChanged(data) {
        if (data.target.tagName == "SELECT") {
            let swapAmount = document.querySelector("label[for=input_1]")
            swapAmount.textContent = `Amount to swap (in ${data.target.value})`
        } else {
            let tokenSelected = document.querySelector("#swap-form select").value
            let array =
                tokenSelected == "WETH"
                    ? [
                          ethers.utils.formatEther(
                              await getReserve0({ onError: (error) => console.log(error) })
                          ),
                          ethers.utils.formatEther(
                              await getReserve1({ onError: (error) => console.log(error) })
                          ),
                      ]
                    : [
                          ethers.utils.formatEther(
                              await getReserve1({ onError: (error) => console.log(error) })
                          ),
                          ethers.utils.formatEther(
                              await getReserve0({ onError: (error) => console.log(error) })
                          ),
                      ]
            // console.log(await getReserve0())
            let [reserveIn, reserveOut] = array
            let amountInWithFee = (data.target.value * 997) / 1000
            let amountOut = (+reserveOut * amountInWithFee) / (+reserveIn + amountInWithFee)
            setSwapResult(amountOut)
        }
    }

    return (
        <div className="shadow-2xl rounded-xl p-4">
            <Form
                onSubmit={handleSwapSubmit}
                onChange={handleSwapChanged}
                id="swap-form"
                data={[
                    {
                        selectOptions: [
                            {
                                id: "ACH",
                                label: "ACH",
                            },
                            {
                                id: "WETH",
                                label: "WETH",
                            },
                        ],
                        name: "Select token to swap",
                        type: "select",
                        value: "",
                    },
                    {
                        inputWidth: "50%",
                        name: "Amount to swap",
                        type: "number",
                        value: "",
                        key: "amountToSwap",
                        validation: { required: true },
                    },
                    {
                        value: `Corresponding Token Out: ${swapResult}`,
                        type: "box",
                        key: "swap-result",
                    },
                ]}
                title="Let's swap!"
            ></Form>
        </div>
    )
}

Whenever this swap button was submitted, the approve function would always need to invoke at least twice before the onSuccess could bring you to the swap function. On the first approve, the following error will be thrown on my console:

Object
MutualsSwapForm.js?2915:69 Approving...
MutualsSwapForm.js?2915:70 
{abi: Array(16), contractAddress: '0xc778417E063141139Fce010982780140Aa0cD5Ab', functionName: 'approve', params: {…}}
 'a'
MutualsSwapForm.js?2915:88 
{swapOptions: {…}}
MutualsSwapForm.js?2915:89 Swapping 25000000000000000 ...
inpage.js:1 MetaMask - RPC Error: execution reverted 
{code: -32000, message: 'execution reverted'}
MutualsSwapForm.js?2915:92 Error: cannot estimate gas; transaction may fail or may require manual gas limit [ See: https://links.ethers.org/v5-errors-UNPREDICTABLE_GAS_LIMIT ] (error={"code":-32000,"message":"execution reverted"}, method="estimateGas", transaction={"from":"0x8C6bB2BD30f4e28dCD0f6faF6B2d315f67155Abc","to":"0xb41fBe2e34700Eea625204c172d050dAF1EDc14D","data":"0xd004f0f7000000000000000000000000c778417e063141139fce010982780140aa0cd5ab0000000000000000000000000000000000000000000000000058d15e17628000","accessList":null}, code=UNPREDICTABLE_GAS_LIMIT, version=providers/5.6.0)
    at Logger.makeError (index.js?dd68:219:1)
    at Logger.throwError (index.js?dd68:228:1)
    at checkError (json-rpc-provider.js?8679:73:1)
    at Web3Provider.eval (json-rpc-provider.js?8679:490:1)
    at Generator.throw (<anonymous>)
    at rejected (json-rpc-provider.js?8679:6:42)
MutualsSwapForm.js?2915:94 
{tx: undefined}
MutualsSwapForm.js?2915:95 Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'wait')
    at _callee$ (MutualsSwapForm.js?2915:95:18)
    at tryCatch (runtime.js?ecd4:45:16)
    at Generator.invoke [as _invoke] (runtime.js?ecd4:274:1)
    at prototype.<computed> [as next] (runtime.js?ecd4:97:1)
    at asyncGeneratorStep (MutualsSwapForm.js?2915:1:1)
    at _next (MutualsSwapForm.js?2915:1:1)
client.js?374c:7 [Violation] Added non-passive event listener to a scroll-blocking 'touchstart' event. Consider marking event handler as 'passive' to make the page more responsive. See https://www.chromestatus.com/feature/5745543795965952


At first, I thought this could be the gas issue. But then I realized the approve operation actually went successful on the etherscan and also on the Metamask. And usually, refreshing the page and re-entering the same input and resubmitting the form would make the two-step transaction (approve, swap) go through smoothly:

Object
MutualsSwapForm.js?2915:69 Approving...
MutualsSwapForm.js?2915:70 
{abi: Array(16), contractAddress: '0xc778417E063141139Fce010982780140Aa0cD5Ab', functionName: 'approve', params: {…}}
 'a'
MutualsSwapForm.js?2915:88 
{swapOptions: {…}}
MutualsSwapForm.js?2915:89 Swapping 25000000000000000 ...
MutualsSwapForm.js?2915:94 
{tx: {…}}
MutualsSwapForm.js?2915:96 Transaction has confirmed by 1 block

Increasing gas limit on the approve method will not change the above pattern of operation. Could anyone help look into this issue? Thanks.

Whenever this swap button was submitted, the approve function would always need to invoke at least twice before the onSuccess could bring you to the swap function.

And usually, refreshing the page and re-entering the same input and resubmitting the form would make the two-step transaction (approve, swap) go through smoothly:

Can you clear up where this issue is reproduced - are you saying that only sometimes it happens (based on you saying if you reload the page, the process works as it should)? The form or app state would be reset in the first instance as well (when you get the execution reverted error).

console.log("Approving...")
console.log(approveOptions, "a")
const tx = await runContractFunction({

Is the execution reverted error happening at this stage? Maybe try a different RPC URL in your wallet.

    const tx = await runContractFunction({
        params: swapOptions,
        onError: (error) => console.log(error),
        })
        console.log({ tx })
        await tx.wait(1)

The error reverted at tx.wait(1)

I’ve tried switching to goerli instead of rinkeby. The same error happened, i.e., the error was thrown always the first attempt, and subsequently refreshing the page with the same input submission will work as it should. The form or app state would NOT be reset in the first instance (when I get the execution reverted error).

The form or app state would NOT be reset in the first instance (when I get the execution reverted error).

Going to the form in the first place (or reloading the page/app) means the app state is not reset? I don’t understand. I’m not seeing how using the same submission or trying again with the same values would then make it work, may be an issue with your form. Try making the transaction first with just hardcoded values.

Also try runContractFunction without onError, only the params.

Thanks for the suggestions. I’ve tried removing onError but it didn’t work. While hard coding it, I found out the error was actually due to a delay in allowance making it insufficient fund to do the swap. I managed to resolve it by setting a timeout for 10 seconds to make sure the allowance is updated first. Is there a better way? I have another stake form needing two approvals which need 15 seconds of timeout. During the timeout, metamask would disappear and then reappear after the timeout.

const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
    async function handleApproveSuccess(contractAddress, amountToSwapFormatted) {
        await sleep(9999)
        swapOptions.params = {
            _amountIn: amountToSwapFormatted,
            _tokenIn: contractAddress,
        }
        console.log({ swapOptions })
        console.log(`Swapping ${swapOptions.params._amountIn} ...`)
        const tx = await runContractFunction({
            params: swapOptions,
        })
        console.log({ tx })
        await tx.wait(1)
        console.log("Transaction has confirmed by 1 block")
    }

I see now, I was still under the impression the error was coming from your approve. Waiting for the approve to be confirmed should be enough; you may be set a higher confirmation number in wait().

Alternatively as a backup you could make a call to check for allowance so that the swap at least won’t fail in this way.

Thanks much. Setting up an await tx.wait(1) for approve works but I will have to omit the onSucess which makes the Metamask disappear for a good 10 plus seconds before popping up again like the timeout thing. Maybe I will have to implement a circular loading just so that the user would know the transaction is not over yet…

console.log("Approving...")
        console.log(approveOptions, "a")
        const tx = await runContractFunction({
            params: approveOptions,
            onError: (error) => console.log(error),
        })
        await tx.wait(1)
        handleApproveSuccess(approveOptions.contractAddress, formattedAmount)

If anyone knows how to keep the onSuccess while impleting a wait for approve, please let me know. Thanks.

1 Like

Maybe I will have to implement a circular loading just so that the user would know the transaction is not over yet…

You can set up this indicator in between initiating the transaction and waiting. This is something you would ideally do anyway since both transactions are done in the same step (and the second relies on the first).

Absolutely. This forum is incredibly constructive. The solutions and suggestions always point me to the true source of errors.

Btw, I did try your second alternative but it didn’t work as intended because the await result would still return zero allowance if insufficient time has been allotted to it. Not sure if a ternary could take care of it. If it could it would be better off since a block of wait on the approve transaction is either an overkill or insufficient amidst a busy network.

1 Like