Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TypeError: fetch failed #124

Open
utsavempiric20 opened this issue Jan 17, 2025 · 0 comments
Open

TypeError: fetch failed #124

utsavempiric20 opened this issue Jan 17, 2025 · 0 comments

Comments

@utsavempiric20
Copy link

utsavempiric20 commented Jan 17, 2025

I face the TypeError: fetch failed when I execute this script and not always i face this error. But sometimes it occurs and stop my execution of script. so give me a solution about it.

My code:

import { NATIVE_MINT, TOKEN_2022_PROGRAM_ID } from "@solana/spl-token";
import axios from "axios";
import { API_URLS, parseTokenAccountResp } from "@raydium-io/raydium-sdk-v2";
import {
  VersionedTransaction,
  Transaction,
  sendAndConfirmTransaction,
  Connection,
  TransactionExpiredBlockheightExceededError,
} from "@solana/web3.js";
import { TOKEN_PROGRAM_ID } from "@raydium-io/raydium-sdk";
import { Keypair } from "@solana/web3.js";
import dotenv from "dotenv";
import usersModel from "../../models/usersModel";
import mongoose from "mongoose";
import { Logger } from "winston";
dotenv.config();
require("../../utils/errorHandler");

interface SwapCompute {
  id: string;
  success: true;
  version: "V0" | "V1";
  openTime?: undefined;
  msg: undefined;
  data: {
    swapType: "BaseIn" | "BaseOut";
    inputMint: string;
    inputAmount: string;
    outputMint: string;
    outputAmount: string;
    otherAmountThreshold: string;
    slippageBps: number;
    priceImpactPct: number;
    routePlan: {
      poolId: string;
      inputMint: string;
      outputMint: string;
      feeMint: string;
      feeRate: number;
      feeAmount: string;
    }[];
  };
}

let connection: Connection;
const MAX_RETRIES = 3;
const RETRY_DELAY = 2000;
const createConnection = async (): Promise<Connection> => {
  let attempt = 0;
  const url =
    process.env.MAINNET_RPC_URL_2 || "https://api.mainnet-beta.solana.com";

  connection = new Connection(url, { commitment: "confirmed" });

  const retryConnection = async () => {
    while (attempt < MAX_RETRIES) {
      try {
        await connection.getEpochInfo("finalized");
        console.log(`Successfully connected to ${url}`);
        return connection;
      } catch (error: any) {
        if (attempt >= MAX_RETRIES - 1) {
          console.error(
            `Error connecting to Solana RPC: ${JSON.stringify(error)}`
          );
          break;
        }

        if (error.message.includes("503")) {
          console.warn(
            `503 Error: Solana RPC is unavailable. Retrying... (${
              attempt + 1
            }/${MAX_RETRIES})`
          );
        } else {
          console.error(`Error: ${JSON.stringify(error)}. Retrying...`);
        }

        attempt++;
        await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY)); // Wait before retrying
      }
    }

    console.warn("Max retries reached. Proceeding with fallback connection...");
    return connection;
  };

  return retryConnection();
};

(async () => {
  try {
    await createConnection();
    console.log("Solana Connection Established");
  } catch (error) {
    console.error(
      "Failed to establish connection in raydiumSwap script:",
      error
    );
  }
})();

const fetchTokenAccountData = async (owner: Keypair) => {
  try {
    const solAccountResp = await connection.getAccountInfo(owner.publicKey);
    const tokenAccountResp = await connection.getTokenAccountsByOwner(
      owner.publicKey,
      { programId: TOKEN_PROGRAM_ID }
    );
    const token2022Req = await connection.getTokenAccountsByOwner(
      owner.publicKey,
      { programId: TOKEN_2022_PROGRAM_ID }
    );
    const tokenAccountData = parseTokenAccountResp({
      owner: owner.publicKey,
      solAccountResp,
      tokenAccountResp: {
        context: tokenAccountResp.context,
        value: [...tokenAccountResp.value, ...token2022Req.value],
      },
    });
    return tokenAccountData;
  } catch (error) {
    console.error("Error in fetchTokenAccountData:", error);
    throw new Error(
      "Failed to fetch token account data. Please try again later."
    );
  }
};

const retryWithExponentialBackoff = async (
  fn: Function,
  maxRetries: number,
  logger: Logger
) => {
  let attempt = 0;
  let delay = 1000;

  while (attempt < maxRetries) {
    try {
      return await fn();
    } catch (error: any) {
      if (
        (error.response && error.response.status === 503) ||
        error.message?.includes("503")
      ) {
        attempt++;
        logger.error(
          `Attempt ${attempt} failed due to 503 Service Unavailable. Retrying in ${
            delay / 1000
          }s...`
        );
        await new Promise((resolve) => setTimeout(resolve, delay));
        delay *= 2;
      } else {
        logger.error(`Error: ${JSON.stringify(error)}`);
        throw error;
      }
    }
  }
};

const attemptSwap = async (
  userId: mongoose.Schema.Types.ObjectId,
  tokenId: mongoose.Schema.Types.ObjectId,
  inputMint: string,
  outputMint: string,
  amount: number,
  inputTokenDecimals: number,
  privateKey: string,
  attempt: number,
  logger: Logger,
  slippage?: number
) => {
  try {
    const inputTokenTotalDecimals = 10 ** inputTokenDecimals;

    amount = Math.floor(Number(amount) * inputTokenTotalDecimals);
    const txVersion: string = "V0";
    const isV0Tx = txVersion === "V0";

    const owner = Keypair.fromSecretKey(new Uint8Array(JSON.parse(privateKey)));

    const { tokenAccounts } = await fetchTokenAccountData(owner);

    const inputTokenAcc = tokenAccounts.find(
      (a: any) => a.mint.toBase58() === inputMint
    )?.publicKey;
    const outputTokenAcc = tokenAccounts.find(
      (a: any) => a.mint.toBase58() === outputMint
    )?.publicKey;

    if (!inputTokenAcc) {
      logger.error(
        `User ${userId.toString()} don't have input token account for ${inputMint}`
      );
      return;
    }

    // get statistical transaction fee from api
    /**
     * vh: very high
     * h: high
     * m: medium
     */
    const { data } = await retryWithExponentialBackoff(
      () =>
        axios.get<{
          id: string;
          success: boolean;
          data: { default: { vh: number; h: number; m: number } };
        }>(`${API_URLS.BASE_HOST}${API_URLS.PRIORITY_FEE}`),
      3,
      logger
    );

    let PRIORITY_FEE: string;
    if (attempt === 1) {
      PRIORITY_FEE = String(data.data.default.m);
    } else if (attempt === 2) {
      PRIORITY_FEE = String(data.data.default.h);
    } else if (attempt === 3) {
      PRIORITY_FEE = String(data.data.default.vh);
    }

    await updateThePriorityFee(userId, tokenId, attempt, PRIORITY_FEE!);

    const { data: swapResponse } = await retryWithExponentialBackoff(
      () =>
        axios.get<SwapCompute>(
          `${API_URLS.SWAP_HOST}/compute/swap-base-in?inputMint=${inputMint}&outputMint=${outputMint}&amount=${amount}&slippageBps=${slippage}&txVersion=${txVersion}`
        ),
      3,
      logger
    );
    logger.info(JSON.stringify(swapResponse.data, null, 2));
    if (
      !swapResponse ||
      !swapResponse.data ||
      !swapResponse.data.outputAmount ||
      !swapResponse.success
    ) {
      logger.info(JSON.stringify(swapResponse, null, 2));
      throw new Error(`${JSON.stringify(swapResponse, null, 2)}`);
    }

    const tokenOutputAmount = swapResponse.data.outputAmount;
    const { data: swapTransactions } = await retryWithExponentialBackoff(
      () =>
        axios.post<{
          id: string;
          version: string;
          success: boolean;
          data: { transaction: string }[];
        }>(`${API_URLS.SWAP_HOST}/transaction/swap-base-in`, {
          computeUnitPriceMicroLamports: PRIORITY_FEE,
          swapResponse,
          txVersion,
          wallet: owner.publicKey.toBase58(),
          wrapSol: false,
          unwrapSol: false, // true means output mint receive sol, false means output mint received wsol
          inputAccount: inputTokenAcc?.toBase58(),
          outputAccount: outputTokenAcc?.toBase58(),
        }),
      3,
      logger
    );

    if (!swapTransactions) {
      throw new Error("Swap API failed.");
    }

    if (!swapTransactions.data) {
      throw new Error("Invalid response from Raydium swap API");
    }

    const allTxBuf = swapTransactions.data.map((tx: { transaction: string }) =>
      Buffer.from(tx.transaction, "base64")
    );
    const allTransactions = allTxBuf.map((txBuf: Buffer) =>
      isV0Tx ? VersionedTransaction.deserialize(txBuf) : Transaction.from(txBuf)
    );

    let idx = 0;
    if (!isV0Tx) {
      for (const tx of allTransactions) {
        logger.info(`${++idx} transaction sending...`);
        const transaction = tx as Transaction;
        transaction.sign(owner);
        const txId = await sendAndConfirmTransaction(
          connection,
          transaction,
          [owner],
          { skipPreflight: true }
        );
        logger.info(`${++idx} transaction confirmed, txId: ${txId}`);
        const transactionHashStatus = await checkTransactionStatusWithRetries(
          txId,
          logger
        );
        logger.info(
          `Transaction Hash Status for txId ${txId} is ${JSON.stringify(
            transactionHashStatus,
            null,
            2
          )}`
        );
        return {
          transactionhash: txId,
          status: transactionHashStatus.status,
          tokenOutputAmount,
          reason: transactionHashStatus.reason,
        };
      }
    } else {
      for (const tx of allTransactions) {
        idx++;
        let { blockhash, lastValidBlockHeight } =
          await connection.getLatestBlockhash({ commitment: "confirmed" });
        const transaction = tx as VersionedTransaction;
        transaction.message.recentBlockhash = blockhash;
        transaction.sign([owner]);

        let retryCount = 0;
        while (retryCount < 3) {
          try {
            const txId = await connection.sendTransaction(
              tx as VersionedTransaction,
              { skipPreflight: true }
            );
            logger.info(`Raw Transaction Sent Successfully. ${txId}`);

            try {
              const transactionResponse = await connection.confirmTransaction(
                {
                  blockhash,
                  lastValidBlockHeight,
                  signature: txId,
                },
                "confirmed"
              );
              logger.info(
                `${idx} Transaction confirmed : ${JSON.stringify(
                  transactionResponse,
                  null,
                  2
                )}`
              );
            } catch (error) {
              const typedError = error as Error;

              logger.error(
                `Error when confirming transaction : ${JSON.stringify(error)}`
              );
              if (
                typedError.message.includes("signature Unsubscribe error") ||
                typedError.message.includes("socket was not")
              ) {
                logger.warn(
                  "Detected signature unsubscribe error. Attempting to reconnect..."
                );

                try {
                  connection = await createConnection();
                  logger.info("Successfully reconnected");
                  await new Promise((resolve) => setTimeout(resolve, 2000));
                  try {
                    await connection.getEpochInfo();
                    logger.info("Connection is stable.");
                  } catch (healthError) {
                    logger.error(
                      `Health check failed: ${JSON.stringify(healthError)}`
                    );
                    throw new Error(
                      "Connection is not stable, please try again later."
                    );
                  }
                  const status = await checkTransactionStatusWithRetries(
                    txId,
                    logger
                  );
                  if (status.status === "success") {
                    return {
                      transactionhash: txId,
                      status: status.status,
                      tokenOutputAmount,
                      reason: status.reason,
                    };
                  }

                  retryCount++;
                  if (retryCount < 3) {
                    await new Promise((resolve) =>
                      setTimeout(resolve, Math.pow(2, retryCount) * 1000)
                    );
                    continue;
                  }
                } catch (reconnectError) {
                  logger.error(
                    `Error during reconnection: ${JSON.stringify(
                      reconnectError
                    )}`
                  );
                }
              }

              return { status: "failed" };
            }

            logger.info(
              `${idx} Transaction successful: https://explorer.solana.com/tx/${txId}`
            );
            const transactionHashStatus =
              await checkTransactionStatusWithRetries(txId, logger);
            logger.info(
              `Transaction Hash Status for txId ${txId} is ${JSON.stringify(
                transactionHashStatus,
                null,
                2
              )}`
            );
            return {
              transactionhash: txId,
              status: transactionHashStatus.status,
              tokenOutputAmount,
              reason: transactionHashStatus.reason,
            };
          } catch (error) {
            if (error instanceof TransactionExpiredBlockheightExceededError) {
              retryCount++;
              logger.warn(
                `Transaction expired. Retry attempt ${retryCount} of 3...`
              );
              logger.error(
                `Error Occur In TransactionExpired Block : ${JSON.stringify(
                  error
                )}`
              );
              ({ blockhash, lastValidBlockHeight } =
                await connection.getLatestBlockhash({
                  commitment: "confirmed",
                }));
              transaction.message.recentBlockhash = blockhash;
              transaction.sign([owner]);
            } else {
              const typedError = error as Error;

              if (typedError.message && typedError.message.includes("503")) {
                retryCount++;
                logger.warn(
                  `503 Service Unavailable error encountered. Retry attempt ${retryCount} of 3...`
                );
                if (retryCount < 3) {
                  await new Promise((resolve) =>
                    setTimeout(resolve, Math.pow(2, retryCount) * 1000)
                  );
                  ({ blockhash, lastValidBlockHeight } =
                    await connection.getLatestBlockhash({
                      commitment: "confirmed",
                    }));
                  transaction.message.recentBlockhash = blockhash;
                  transaction.sign([owner]);
                } else {
                  logger.error("503 error persists after 3 retry attempts.");
                  return {
                    status: "failed",
                    reason: "RPC unavailable after multiple attempts",
                  };
                }
              } else {
                logger.error(`Transaction failed: ${JSON.stringify(error)}`);
                return {
                  status: "failed",
                  reason: `Unexpected error: ${JSON.stringify(typedError)}`,
                };
              }
            }
          }
        }
        if (retryCount === 3) {
          logger.error("Transaction failed after 3 retry attempts.");
          return { status: "failed" };
        }
      }
    }
  } catch (error) {
    logger.error(
      `Attempt ${attempt} failed inside attemptSwap function ${JSON.stringify(
        error
      )}`
    );
    return { status: "failed" };
  }
};

export async function swapTokensOnRaydium(
  userId: mongoose.Schema.Types.ObjectId,
  tokenId: mongoose.Schema.Types.ObjectId,
  tokenIn: string,
  tokenOut: string,
  amount: number,
  inputTokenDecimals: number,
  privateKey: string,
  logger: Logger,
  slippageBps?: number
) {
  const maxRetries: number = 3;
  const retryDelay = 2000;
  let swapResponse;

  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      swapResponse = await attemptSwap(
        userId,
        tokenId,
        tokenIn,
        tokenOut,
        amount,
        inputTokenDecimals,
        privateKey,
        attempt,
        logger,
        slippageBps
      );
      if (swapResponse && swapResponse.status === "success") {
        return swapResponse;
      }
      if (attempt < maxRetries) {
        logger.info(
          `Attempt ${attempt + 1} Retrying in ${retryDelay / 1000} seconds...`
        );
        await new Promise((resolve) => setTimeout(resolve, retryDelay));
      }
    } catch (error: any) {
      logger.error(`Inside Catch block of swapTokens Function.`);
      logger.error(`Error while executing Swap: ${JSON.stringify(error)}`);
      return error;
    }
  }

  logger.error("All retry attempts failed.");
  return swapResponse;
}

const checkTransactionStatusWithRetries = async (
  txId: string,
  logger: Logger,
  maxTries = 20,
  delay = 2000
) => {
  let attempt = 0;

  const sleep = (ms: any) => new Promise((resolve) => setTimeout(resolve, ms));

  while (attempt < maxTries) {
    try {
      const transactionHashStatus = await connection.getSignatureStatus(txId, {
        searchTransactionHistory: true,
      });

      if (
        transactionHashStatus?.value &&
        transactionHashStatus.value.confirmationStatus === "finalized" &&
        transactionHashStatus?.value?.err === null
      ) {
        logger.info(
          `Successful transaction hash status from Raydium: ${JSON.stringify(
            transactionHashStatus,
            null,
            2
          )}`
        );
        return {
          transactionhash: txId,
          status: "success",
          reason: "Transaction Successful",
        };
      }

      if (transactionHashStatus?.value?.err) {
        const errorReason = JSON.stringify(
          transactionHashStatus.value.err,
          null,
          2
        );
        logger.info(
          `Failed transaction hash status from Raydium: ${JSON.stringify(
            transactionHashStatus,
            null,
            2
          )}`
        );
        return {
          transactionhash: txId,
          status: "failed",
          reason: errorReason,
        };
      }

      attempt += 1;
      logger.info(
        `Attempt ${attempt} of ${maxTries}: Transaction not finalized yet.`
      );
      await sleep(delay);
    } catch (error) {
      logger.error(
        `Error checking transaction status: ${JSON.stringify(error)}`
      );
      return {
        transactionhash: txId,
        status: "failed",
        reason: `Error checking transaction status: ${error}`,
      };
    }
  }

  logger.info(
    `Transaction not finalized after ${maxTries} attempts. Returning failed status.`
  );
  return {
    transactionhash: txId,
    status: "failed",
    reason: "Transaction not finalized within the allowed time.",
  };
};

const updateThePriorityFee = async (
  userId: mongoose.Schema.Types.ObjectId,
  tokenId: mongoose.Schema.Types.ObjectId,
  attempt: number,
  PRIORITY_FEE: string
) => {
  await usersModel.findOneAndUpdate(
    {
      _id: userId,
      monitorList: { $elemMatch: { tokenId: tokenId } },
    },
    {
      $set: {
        "monitorList.$[elem].priorityFee.currentPriorityFee": PRIORITY_FEE,
        "monitorList.$[elem].priorityFee.currentStatus":
          (attempt === 1 && "medium") ||
          (attempt === 2 && "high") ||
          (attempt === 3 && "veryHigh"),
      },
    },
    {
      arrayFilters: [{ "elem.tokenId": tokenId }],
    }
  );
};

// async function main() {
//   // @audit temp
//   const USDC_MINT = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
//   const TOKEN_MINT = "DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263";
//   const inputAmount = 0.0001;
//   const inputDecimals = 9;
//   const privateKey =
//     "";
//   const transactionHash = await swapTokensOnRaydium(
//     new mongoose.Types.ObjectId(
//       ""
//     ) as unknown as mongoose.Schema.Types.ObjectId,
//     new mongoose.Types.ObjectId(
//       ""
//     ) as unknown as mongoose.Schema.Types.ObjectId,
//     TOKEN_MINT,
//     USDC_MINT,
//     500,
//     6,
//     privateKey,
//     loggerBuy as any,
//     50
//   );
//   console.log("transactionHash :", transactionHash);
// }

// main();

@raydium-io raydium-io deleted a comment Jan 20, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant