DEV Community

Swatantra goswami
Swatantra goswami

Posted on • Edited on

Approve USDT/USDC token allowance

  approve: async ({
      from,
      contractAddress,
      privateKey,
      stakingProviderName,
      amountInWei,
      nonce,
      estimateGas,
      maxPriorityFeePerGas,
      gasFee,
      feesType,
    }) => {
      try {
        const {currentAllowance, firstTrx, secondTrx} = await retryFunc(
          async evmProvider => {
            const walletSigner = new ethers.Wallet(privateKey, evmProvider);

            const tokenContract = new ethers.Contract(
              contractAddress,
              localErc20ABI,
              walletSigner,
            );

            const {stakingProviderAddress} =
              await EvmStakingProvider.getStakingAddress({
                contractAddress,
                stakingProviderName,
              });

            const allowance = await tokenContract.allowance(
              from,
              stakingProviderAddress,
            );
            console.log('Current allowance:', allowance.toString());

            if (allowance >= amountInWei) {
              console.log('Already approved');
              return true;
            }

            let finalEstimateGas = BigInt(estimateGas);
            if (typeof finalEstimateGas !== 'bigint') {
              finalEstimateGas = await tokenContract[
                'transfer(address,uint256)'
              ].estimateGas(stakingProviderAddress, amountInWei);
            }
            let finalGasPrice = gasFee;
            let finalMaxPriorityFeePerGas = maxPriorityFeePerGas;
            if (typeof finalGasPrice !== 'bigint') {
              const gasFeeData = await getEtherGasPrice(feesType, evmProvider);
              finalGasPrice = gasFeeData?.gasPrice;
              finalMaxPriorityFeePerGas = gasFeeData?.maxPriorityFeePerGas;
            }
            let options = {
              type: 2,
              // gasLimit: 100000n, // NOTE: this needs to revert before commit (for pending transaction)
              gasLimit: finalEstimateGas, // 100000
              // maxFeePerGas: 1n, // NOTE: this needs to revert before commit
              maxFeePerGas: finalGasPrice,
              maxPriorityFeePerGas: validatePriorityFee(
                finalMaxPriorityFeePerGas,
                finalGasPrice,
              ),
              nonce,
            };
            if (isEip1559NotSupported(chain_name)) {
              delete options.type;
              delete options.maxPriorityFeePerGas;
              delete options.maxFeePerGas;
              options.gasPrice = finalGasPrice;
            }

            const resetTrx = await tokenContract.approve.populateTransaction(
              stakingProviderAddress,
              0n,
              options,
            );
            const approveTx = await tokenContract.approve.populateTransaction(
              stakingProviderAddress,
              amountInWei,
              allowance > 0n
                ? {...options, nonce: options.nonce + 1} // reset will be sent → use N+1
                : options,
            );
            return {
              firstTrx: resetTrx,
              secondTrx: approveTx,
              currentAllowance: allowance,
            };
          },
          null,
        );
        let trx1, trx2;
        if (currentAllowance > 0n) {
          console.log('Resetting allowance to 0...');
          const approveHash = await createSendTransaction(
            new ethers.Wallet(privateKey),
            firstTrx,
          );
          const approveTxHash = approveHash.hash || approveHash;

          console.log('Approve first submitted:', approveTxHash);
          trx1 = await EVMChain(
            chain_name,
            undefined,
            customRpcUrl,
          ).waitForConfirmation({
            transaction: approveHash,
            interval: 5000,
            retries: 15,
          });

          console.log('Approved first confirmed', trx1);
        }

        const approveHash = await createSendTransaction(
          new ethers.Wallet(privateKey),
          secondTrx,
        );

        const approveTxHash = approveHash.hash || approveHash;

        console.log('Approve second submitted:', approveTxHash);
        trx2 = await EVMChain(
          chain_name,
          undefined,
          customRpcUrl,
        ).waitForConfirmation({
          transaction: approveHash,
          interval: 5000,
          retries: 15,
        });

        console.log('Approved second confirmed', trx2);
        return {
          confirmTransaction: trx2,
          transaction1: trx1,
          transaction2: trx2,
        };
      } catch (error) {
        console.error('Error in approve transaction', error);
        throw new Error(error);
      }
    },
    getEstimateFeForAllowanceApprove: async ({
      isFetchNonce,
      existingNonce,
      from,
      contractAddress,
      stakingProviderName,
      amountInWei,
      feesType,
      nonce,
      allowance,
    }) =>
      retryFunc(async evmProvider => {
        const tokenContract = new ethers.Contract(
          contractAddress,
          erc20Abi,
          evmProvider,
        );
        const {stakingProviderAddress} =
          await EvmStakingProvider.getStakingAddress({
            contractAddress,
            stakingProviderName,
          });
        let estimateGas;
        try {
          estimateGas = await tokenContract.approve.estimateGas(
            stakingProviderAddress,
            amountInWei,
            {from},
          );
        } catch (error) {
          estimateGas = 60000n;
        }

        // When an existing non-zero allowance must be reset first, the second tx
        // writes 0 → amountInWei (SSTORE ~20,000 gas) instead of the estimated
        // non-zero → non-zero case (~5,000 gas), so add the difference.
        try {
          const currentAllowance = await tokenContract.allowance(
            from,
            stakingProviderAddress,
          );
          if (currentAllowance > 0n && currentAllowance < amountInWei) {
            estimateGas += 20000n;
          }
        } catch (_) {}

        const finalEstimateGas = (estimateGas * 110n) / 100n;
        let currentNonce;
        if (isFetchNonce || existingNonce == null) {
          currentNonce = await EVMChain(chain_name, undefined).getNonce({
            customRpcUrl,
            address: from,
          });
        } else if (existingNonce) {
          currentNonce = existingNonce;
        }
        const obj = await calculateTotalFees({
          feesType,
          evmProvider,
          fromAddress: from,
          toAddress: stakingProviderAddress,
          estimateGas: finalEstimateGas,
          isFetchNonce: nonce == null,
          existingNonce: currentNonce,
        });
        return {
          ...obj,
          allowance,
          fee: allowance ? obj.fee * 2 : obj.fee,
        };
      }, null),
    readAllowance: async ({
      from,
      contractAddress,
      stakingProviderName,
      amountInWei,
    }) =>
      retryFunc(async evmProvider => {
        const tokenContract = new ethers.Contract(
          contractAddress,
          erc20Abi,
          evmProvider,
        );
        const {stakingProviderAddress} =
          await EvmStakingProvider.getStakingAddress({
            contractAddress,
            stakingProviderName,
          });
        const allowance = await tokenContract.allowance(
          from,
          stakingProviderAddress,
        );
        const isApproved = allowance >= amountInWei;
        const needsReset = !isApproved && allowance > 0n;
        const required = isApproved ? 0n : amountInWei - allowance;
        return {
          allowance,
          isApproved,
          needsReset,
          required,
        };
      }, null),
Enter fullscreen mode Exit fullscreen mode

Top comments (0)