anza-xyz / wallet-adapter

Modular TypeScript wallet adapters and components for Solana applications.
https://anza-xyz.github.io/wallet-adapter/
Apache License 2.0
1.44k stars 906 forks source link

How to correctly implement a token minting flow with Solana in a React app? #940

Closed jaca420 closed 3 months ago

jaca420 commented 3 months ago

Not sure if this is a but, or just something that I'm doing wrong, but I'm working on a React app integrated with Solana blockchain, and I'm facing a challenge with the token minting flow. The goal is for users to request minting of new tokens through a backend service, which then creates and signs a transaction. The user's wallet should submit this transaction to the blockchain and pay the associated fees.

Here's the flow I need:

  1. User requests token minting via UI.
  2. React app sends a request to the backend.
  3. Backend creates a signed transaction with minting instructions.
  4. Backend sends the signed transaction back to the frontend.
  5. User's wallet submits the transaction, paying the fees.

Issue:

The backend seems to create the transaction successfully, but when the frontend tries to submit it, I encounter an unexpected error.

Backend Code (Node.js):

router.post("/sol", authenticationMiddleware, corsMiddleware, async (req, res) => {
    try {
        const { decodedToken } = res.locals;

        if (!decodedToken) {
            return res.status(500).send("Token error");
        }

        const getUser = await db.collection("users").doc(decodedToken.uid).get();
        const user = getUser.data();
        const userTokens = Math.floor(user?.tokens); // Use numeric value for Solana transactions
        const userWallet = user?.solWallet;

        // Assuming you have a function to create a transaction for minting tokens
        // This function should prepare the transaction with the mint instruction but not sign it
        // The transaction is then serialized to a base64 string to be sent to the client
        const transactionBase64 = await prepareMintTransaction(userWallet, userTokens);
        return res.status(200).send({ transaction: transactionBase64 });

    } catch (error) {
        console.log("Error:", error);
        return res.status(500).send({ "error": "An error occurred while processing your request. Please try again." });
    }
});

async function prepareMintTransaction(userWallet, userTokens) {

    // Convert the secret key and mint public key string to their respective types
    const mintAuthoritySecretKey = Uint8Array.from(MINT_AUTHORITY_SECRET_KEY);
    const mintPublicKeyStr = MINT_PUBLIC_KEY_STR;
    const userPublicKeyStr = userWallet; // Assuming userWallet is a string
    const amount = userTokens; // Assuming userTokens is the amount to mint

    const connection = new web3.Connection(web3.clusterApiUrl("devnet"), "confirmed");
    const mintPublicKey = new web3.PublicKey(mintPublicKeyStr);
    const userPublicKey = new web3.PublicKey(userPublicKeyStr);
    const mintAuthorityKeypair = web3.Keypair.fromSecretKey(mintAuthoritySecretKey);

    // Fetch or create the associated token account for the recipient user
    const toTokenAccount = await splToken.getOrCreateAssociatedTokenAccount(
        connection,
        mintAuthorityKeypair,
        mintPublicKey,
        userPublicKey
    );

    // Make sure the amount is specified correctly, accounting for decimals.
    // If your token has 9 decimals and you want to mint 1 token, you need to pass 1000000000 as the amount.
    const decimals = 9; // This should match the decimals of your token.
    const mintAmount = BigInt(amount) * BigInt(10 ** decimals);

    // Create the 'mint to' instruction
    const mintToInstruction = splToken.createMintToInstruction(
        mintPublicKey,                // The public key of the mint
        toTokenAccount.address,       // The associated token account of the recipient
        mintAuthorityKeypair.publicKey, // The mint authority public key
        mintAmount,                   // The amount to mint, as a BigInt or number
    );

    // Create a new transaction and add the mint instruction
    let theTransaction = new web3.Transaction().add(mintToInstruction);

    // Fetch the recent blockhash to include in the transaction
    // theTransaction.recentBlockhash = (await connection.getRecentBlockhash()).blockhash;
    const { blockhash } = (await connection.getLatestBlockhash());
    theTransaction.recentBlockhash = blockhash;
    // Temporarily set the fee payer for the sake of transaction structure
    theTransaction.feePayer = mintAuthorityKeypair.publicKey;

    // const serializedTransaction = transaction.serializeMessage();
    const serializedTransaction = theTransaction.serialize({
        requireAllSignatures: false, // Indicates not all signatures are present yet
        verifySignatures: false, // No need to verify signatures at this point
    });

    // Encode the serialized transaction to base64
    const base64Transaction = serializedTransaction.toString("base64");

    return base64Transaction;
}

I deployed a program here: https://solscan.io/token/2RbY2xRtSwgvKdyqvvFpZPrDwknnZY63HqfSQGxr5x6y?cluster=devnet

In the above code, the MINT_PUBLIC_KEY_STR is defined like this:

const MINT_PUBLIC_KEY_STR = "2RbY2xRtSwgvKdyqvvFpZPrDwknnZY63HqfSQGxr5x6y";

The MINT_AUTHORITY_SECRET_KEY is the value taken from mint-authority.json from the project that deployed the program.

Frontend Code (React):


const fetchTransactionData = async () => {
    try {
      const token = await auth?.currentUser?.getIdToken(true);
      const api = "http://localhost:5001/my-app/us-central1/api/claim/sol";
      const response = await axios.post(
        api,
        {},
        {
          headers: {
            Authorization: `Bearer ${token}`,
            "Content-Type": "application/json",
          },
        }
      );
      return response.data.transaction; // Return the base64 encoded transaction
    } catch (error) {
      console.error(
        "Error fetching transaction data:",
        error.response?.data || error.message
      );
      throw error; // Re-throw the error to handle it in the calling function
    }
  };

  const handleSignAndSendTransaction = async (base64Transaction) => {
    if (!publicKey || !signTransaction) {
      console.log("Wallet not connected");
      return;
    }

    try {
      // Convert the base64 encoded transaction to a Transaction object
      const transactionBuffer = Buffer.from(base64Transaction, "base64");
      const transaction = Transaction.from(transactionBuffer);

      let signedTransaction;
      try {
        signedTransaction = await signTransaction(transaction);
      } catch (signingError) {
        // Handle errors that occur during the signing process
        console.error("Error during transaction signing:", signingError);
        throw signingError; // Re-throw the error to prevent further execution
      }

      // Send the signed transaction to the blockchain
      const signature = await connection.sendRawTransaction(
        signedTransaction.serialize()
      );
      console.log("Transaction signature:", signature);

      // Confirm the transaction
      const confirmation = await connection.confirmTransaction(
        signature,
        "confirmed"
      );
      console.log("Transaction confirmation:", confirmation);
    } catch (error) {
      console.error("Error signing or sending the transaction:", error);
    }
  };

  const handleClaimTokens = async () => {
    try {
      const base64Transaction = await fetchTransactionData(); // Fetch the transaction data
      if (base64Transaction) {
        await handleSignAndSendTransaction(base64Transaction); // Sign and send the transaction
      }
    } catch (error) {
      console.error("Error in handleClaimTokens:", error);
    }
  };

Screenshot of the error below

enter image description here