skynetcap/solanaj

Issue with Performing Token Swap via Serialized Transaction in Solana4J

Opened this issue · 0 comments

I am attempting to implement a token swap on Solana using the Jupiter API for quotes and serialized transactions. After receiving a base64-encoded transaction from Jupiter's API, I decode, sign, and try to send the transaction using Solana4J. However, the transaction fails, and I need assistance to ensure compatibility with Solana4J's transaction handling methods.

Steps Taken:

  1. Get Quote: Fetch a quote from Jupiter's /quote endpoint for swapping SOL to a specific SPL token.
  2. Serialized Transaction: Use the Jupiter /swap endpoint to obtain a base64-encoded transaction.
  3. Deserialize, Sign, and Send: Decode the base64 transaction.
  4. Create a new Transaction object in Solana4J.
  5. Sign the transaction using the user's private key.
  6. Attempt to send the transaction using sendTransaction.

Problem: The transaction consistently fails during deserialization and signing, with issues around setting the byte array and handling token mint authority.

Code Snippet: Below is the code segment used for deserializing, signing, and sending the transaction:


package com.tradingbot.service;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.p2p.solanaj.core.Account;
import org.p2p.solanaj.core.PublicKey;
import org.p2p.solanaj.core.Transaction;
import org.p2p.solanaj.programs.TokenProgram;
import org.p2p.solanaj.rpc.RpcClient;
import org.p2p.solanaj.rpc.types.config.Commitment;

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.Base64;

public class JupiterSwapService {

    private static final String JUPITER_QUOTE_URL = "https://quote-api.jup.ag/v6/quote";
    private static final String JUPITER_SWAP_URL = "https://quote-api.jup.ag/v6/swap";
    private static final String INPUT_SOL_MINT = "So11111111111111111111111111111111111111112"; // SOL mint address

    private final RpcClient solanaClient;
    private final ObjectMapper objectMapper = new ObjectMapper();
    private final HttpClient httpClient = HttpClient.newHttpClient();

    public JupiterSwapService(RpcClient solanaClient) {
        this.solanaClient = solanaClient;
    }

    // Step 1: Get a quote for the swap
    private String getQuote(final double amountInSol, final double slippageBps, final String outputTokenMint) throws Exception {
        long amountInLamports = (long) (amountInSol * 1e9);  // SOL to lamports
        String requestUrl = String.format(
                "%s?inputMint=%s&outputMint=%s&amount=%d&slippageBps=%d",
                JUPITER_QUOTE_URL, INPUT_SOL_MINT, outputTokenMint, amountInLamports, (int) (slippageBps * 100)
        );

        HttpRequest request = HttpRequest.newBuilder()
                .uri(new URI(requestUrl))
                .GET()
                .header("Accept", "application/json")
                .build();

        HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());

        if (response.statusCode() == 200) {
            JsonNode responseJson = objectMapper.readTree(response.body());
            return responseJson.toString(); // Return the full quote response as JSON string
        } else {
            throw new RuntimeException(STR."Failed to get quote: \{response.body()}");
        }
    }

    // Step 2: Get the serialized transaction from Jupiter using the quote
    private String getSerializedSwapTransaction(String quoteResponse, String userPublicKey) throws Exception {
        String jsonPayload = String.format(
                "{\"quoteResponse\": %s, \"userPublicKey\": \"%s\", \"wrapAndUnwrapSol\": true}",
                quoteResponse, userPublicKey
        );

        HttpRequest request = HttpRequest.newBuilder()
                .uri(new URI(JUPITER_SWAP_URL))
                .header("Content-Type", "application/json")
                .POST(HttpRequest.BodyPublishers.ofString(jsonPayload))
                .build();

        HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());

        if (response.statusCode() == 200) {
            JsonNode responseJson = objectMapper.readTree(response.body());
            return responseJson.path("swapTransaction").asText();  // Get the base64-encoded transaction
        } else {
            throw new RuntimeException(STR."Failed to fetch swap transaction: \{response.body()}");
        }
    }

    // Step 2: Deserialize, sign, and send the transaction
    private String signAndSendTransaction(String base64Transaction, Account userAccount, String outputTokenMint) throws Exception {
        // Step 2.2: Create a new Transaction object and set the bytes directly
        Transaction transaction = new Transaction();
        transaction.addInstruction(TokenProgram.transfer(PublicKey.valueOf(outputTokenMint), userAccount.getPublicKey(), 1000000, PublicKey.valueOf(userAccount.getPrivateKeyBase58())));

        // Step 2.3: Set the recent blockhash
        String latestBlockhash = solanaClient.getApi().getLatestBlockhash().getValue().getBlockhash();
        transaction.setRecentBlockHash(latestBlockhash);

        // Step 2.4: Sign the transaction with the user's account
        transaction.sign(userAccount);

        // Step 2.5: Serialize the signed transaction to base64 for sending
        String base64SignedTransaction = Base64.getEncoder().encodeToString(transaction.serialize());

        // Step 3: Send the signed transaction to Solana
        String transactionSignature = solanaClient.getApi().sendTransaction(transaction, userAccount);

        // Step 4: Confirm the transaction
        solanaClient.getApi().getTransaction(transactionSignature, Commitment.CONFIRMED);

        return transactionSignature;
    }

    // Main function to perform the swap
    public String performSwap(double amountInSol, Account userAccount, final String outputTokenMint) throws Exception {
        // Step 1: Get the quote
        String quoteResponse = getQuote(amountInSol, 0.5, outputTokenMint);  // 0.5% slippage
        if (quoteResponse == null) {
            throw new RuntimeException("Failed to get a valid quote from Jupiter.");
        }

        // Step 2: Get the serialized transaction using the quote
        String userPublicKey = userAccount.getPublicKey().toBase58();
        String serializedTransaction = getSerializedSwapTransaction(quoteResponse, userPublicKey);

        if (serializedTransaction == null) {
            throw new RuntimeException("Failed to get serialized swap transaction from Jupiter.");
        }

        // Step 3: Sign and send the transaction
        return signAndSendTransaction(serializedTransaction, userAccount, outputTokenMint);
    }
}

I am new in crypto so, please, don't judge me. The example provided by Jupiter which I follow:
https://station.jup.ag/docs/apis/swap-api