68: Await CONFIRMED on-chain outcomes for Jupiter Swap and Perps operations

This commit is contained in:
2026-08-11 12:31:22 +02:00
parent cb2d3e2a16
commit b86ec37bb1
15 changed files with 765 additions and 34 deletions
@@ -1,6 +1,7 @@
package com.r35157.jupiterperpsalarm.impl.ref;
import com.r35157.cryptowallet.solana.SolanaWallet;
import com.r35157.libs.jupiter.JupiterTransactionOutcomeException;
import com.r35157.libs.jupiter.perps.JupiterPerpsPosition;
import com.r35157.libs.jupiter.perps.JupiterPerpsService;
@@ -66,11 +67,44 @@ public final class JupiterPerpsPositionDecreaseAlarmAction
System.out.println(
"Position Decrease Signature: " + signature
);
} catch (JupiterTransactionOutcomeException e) {
reportTransactionOutcome(e);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.err.println(
"Jupiter Perps position decrease was interrupted"
);
} catch(Exception e) {
System.err.println("EXCEPTION: " + e.getMessage());
}
}
private static void reportTransactionOutcome(
JupiterTransactionOutcomeException exception
) {
switch (exception.outcome().status()) {
case FAILED -> System.err.println(
"Jupiter Perps position decrease failed on-chain: "
+ "transaction "
+ exception.transactionSignature()
+ ", confirmation slot "
+ exception.outcome().slot()
+ ", Solana error "
+ exception.outcome().failureDetails()
);
case TIMED_OUT -> System.err.println(
"Jupiter Perps position decrease transaction "
+ exception.transactionSignature()
+ " has an unknown on-chain outcome after timeout; "
+ "no equivalent transaction was automatically "
+ "resubmitted"
);
case SUCCEEDED -> throw new IllegalStateException(
"A successful transaction outcome is not exceptional"
);
}
}
private JupiterPerpsPosition findOpenPosition(
JupiterPerpsPositionDecreaseAlarmActionConfiguration
.ResolvedPositionDecrease positionDecrease
@@ -1,6 +1,7 @@
package com.r35157.jupiterperpsalarm.impl.ref;
import com.r35157.cryptowallet.solana.SolanaWallet;
import com.r35157.libs.jupiter.JupiterTransactionOutcomeException;
import com.r35157.libs.jupiter.perps.JupiterPerpsService;
import com.r35157.libs.valuetypes.basic.MoneyAmount;
@@ -57,11 +58,44 @@ public final class JupiterPerpsPositionIncreaseAlarmAction implements Configured
);
System.out.println("Position Increase Signature: " + signature);
} catch (JupiterTransactionOutcomeException e) {
reportTransactionOutcome(e);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.err.println(
"Jupiter Perps position increase was interrupted"
);
} catch(Exception e) {
System.err.println("EXCEPTION: " + e.getMessage());
}
}
private static void reportTransactionOutcome(
JupiterTransactionOutcomeException exception
) {
switch (exception.outcome().status()) {
case FAILED -> System.err.println(
"Jupiter Perps position increase failed on-chain: "
+ "transaction "
+ exception.transactionSignature()
+ ", confirmation slot "
+ exception.outcome().slot()
+ ", Solana error "
+ exception.outcome().failureDetails()
);
case TIMED_OUT -> System.err.println(
"Jupiter Perps position increase transaction "
+ exception.transactionSignature()
+ " has an unknown on-chain outcome after timeout; "
+ "no equivalent transaction was automatically "
+ "resubmitted"
);
case SUCCEEDED -> throw new IllegalStateException(
"A successful transaction outcome is not exceptional"
);
}
}
private boolean isUSDCBalanceOk(JupiterPerpsPositionIncreaseAlarmActionConfiguration.ResolvedPositionIncrease positionIncrease)
throws IOException, InterruptedException {
ΩAmountΩ usdcBalance = wallet.getSPLTokenBalance(
@@ -0,0 +1,130 @@
package com.r35157.libs.jupiter;
import com.r35157.libs.solana.SolanaCommitment;
import com.r35157.libs.solana.SolanaTransactionOutcome;
import org.jetbrains.annotations.NotNull;
import java.util.Objects;
/**
* Reports a definitive on-chain failure or an unknown timeout for a Jupiter
* transaction whose signature is already known.
*
* <p>This checked exception never represents a successful transaction. A
* timed-out outcome is unknown and must not cause an equivalent transaction
* to be automatically resubmitted.</p>
*/
public final class JupiterTransactionOutcomeException extends Exception {
/**
* Creates an exception for a failed or timed-out Jupiter transaction.
*
* @param transactionSignature known submitted transaction signature
* @param requestedCommitment commitment requested from Solana
* @param outcome complete failed or timed-out transaction outcome
* @throws IllegalArgumentException if the signature is blank or the
* outcome is successful
* @throws NullPointerException if any argument is {@code null}
*/
public JupiterTransactionOutcomeException(
@NotNull ΩSolanaTransactionSignatureΩ transactionSignature,
@NotNull SolanaCommitment requestedCommitment,
@NotNull SolanaTransactionOutcome outcome
) {
super(buildMessage(
requireSignature(transactionSignature),
Objects.requireNonNull(
requestedCommitment,
"requestedCommitment"
),
requireExceptionalOutcome(outcome)
));
this.transactionSignature = transactionSignature;
this.requestedCommitment = requestedCommitment;
this.outcome = outcome;
}
/**
* Returns the known submitted transaction signature.
*
* @return non-blank Solana transaction signature
*/
public @NotNull ΩSolanaTransactionSignatureΩ transactionSignature() {
return transactionSignature;
}
/**
* Returns the commitment that the Jupiter operation required.
*
* @return requested Solana commitment
*/
public @NotNull SolanaCommitment requestedCommitment() {
return requestedCommitment;
}
/**
* Returns the complete failed or timed-out outcome.
*
* @return complete exceptional Solana transaction outcome
*/
public @NotNull SolanaTransactionOutcome outcome() {
return outcome;
}
private static ΩSolanaTransactionSignatureΩ requireSignature(
ΩSolanaTransactionSignatureΩ transactionSignature
) {
Objects.requireNonNull(
transactionSignature,
"transactionSignature"
);
if (transactionSignature.isBlank()) {
throw new IllegalArgumentException(
"Transaction signature must not be blank"
);
}
return transactionSignature;
}
private static SolanaTransactionOutcome requireExceptionalOutcome(
SolanaTransactionOutcome outcome
) {
Objects.requireNonNull(outcome, "outcome");
if (outcome.status() == SolanaTransactionOutcome.Status.SUCCEEDED) {
throw new IllegalArgumentException(
"A successful transaction outcome is not exceptional"
);
}
return outcome;
}
private static String buildMessage(
ΩSolanaTransactionSignatureΩ transactionSignature,
SolanaCommitment requestedCommitment,
SolanaTransactionOutcome outcome
) {
return switch (outcome.status()) {
case FAILED -> "Jupiter transaction "
+ transactionSignature
+ " definitively failed on-chain at "
+ requestedCommitment
+ " in slot "
+ outcome.slot()
+ ": "
+ outcome.failureDetails();
case TIMED_OUT -> "Jupiter transaction "
+ transactionSignature
+ " did not reach a definitive "
+ requestedCommitment
+ " outcome before timeout; the on-chain outcome is "
+ "unknown and an equivalent transaction must not be "
+ "automatically resubmitted";
case SUCCEEDED -> throw new IllegalArgumentException(
"A successful transaction outcome is not exceptional"
);
};
}
private final ΩSolanaTransactionSignatureΩ transactionSignature;
private final SolanaCommitment requestedCommitment;
private final SolanaTransactionOutcome outcome;
}
@@ -1,5 +1,6 @@
package com.r35157.libs.jupiter.perps;
import com.r35157.libs.jupiter.JupiterTransactionOutcomeException;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -12,7 +13,8 @@ import java.util.Set;
* operations for its configured wallet.
*
* <p>The service constructs and submits Jupiter transactions, while its
* configured wallet is responsible for signing them.</p>
* configured wallet is responsible for signing them. Position operations
* return normally only after independent Solana {@code CONFIRMED} success.</p>
*/
public interface JupiterPerpsService {
/**
@@ -49,17 +51,26 @@ public interface JupiterPerpsService {
* Opens or increases a Jupiter Perps position.
*
* <p>The service constructs the transaction, asks its configured wallet
* to sign it, and submits it to Jupiter.</p>
* to sign it, submits it to Jupiter, and independently awaits Solana
* {@code CONFIRMED} success. A timeout or communication error after
* submission leaves the on-chain outcome unknown and must not cause an
* automatic resubmission.</p>
*
* @param tradedTokenMint the mint address of the asset being traded
* @param direction whether the position is long or short
* @param inputTokenAmount the amount of USDC to supply as collateral
* @param sizeUsdDelta the requested increase in position size, denominated in USD
* @param maxSlippageBps the maximum accepted slippage, in basis points
* @return the Solana transaction signature
* @return the independently confirmed Solana transaction signature
* @throws IllegalArgumentException if an amount, mint, or slippage value is invalid
* @throws IOException if the transaction cannot be constructed, signed, or submitted
* @throws InterruptedException if the calling thread is interrupted
* @throws IOException if the transaction cannot be constructed, signed,
* submitted, or confirmed; after submission the
* on-chain outcome can be unknown
* @throws JupiterTransactionOutcomeException if the submitted transaction
* definitively fails on-chain or confirmation times out with an
* unknown outcome
* @throws InterruptedException if the calling thread is interrupted;
* interruption propagates directly
*/
@NotNull
ΩSolanaTransactionSignatureΩ executePositionIncrease(
@@ -68,22 +79,32 @@ public interface JupiterPerpsService {
@NotNull ΩUSDCAmountΩ inputTokenAmount,
@NotNull ΩUSDCAmountΩ sizeUsdDelta,
int maxSlippageBps
) throws IOException, InterruptedException;
) throws IOException, JupiterTransactionOutcomeException,
InterruptedException;
/**
* Decreases an existing Jupiter Perps position.
*
* <p>The service constructs the transaction, asks its configured wallet
* to sign it, and submits it to Jupiter.</p>
* to sign it, submits it to Jupiter, and independently awaits Solana
* {@code CONFIRMED} success. A timeout or communication error after
* submission leaves the on-chain outcome unknown and must not cause an
* automatic resubmission.</p>
*
* @param positionAccount the Jupiter Perps position to decrease
* @param receiveTokenMint the mint address of the token to receive
* @param sizeUsdDelta the requested decrease in position size, denominated in USD
* @param maxSlippageBps the maximum accepted slippage, in basis points
* @return the Solana transaction signature
* @return the independently confirmed Solana transaction signature
* @throws IllegalArgumentException if an amount, mint, or slippage value is invalid
* @throws IOException if the transaction cannot be constructed, signed, or submitted
* @throws InterruptedException if the calling thread is interrupted
* @throws IOException if the transaction cannot be constructed, signed,
* submitted, or confirmed; after submission the
* on-chain outcome can be unknown
* @throws JupiterTransactionOutcomeException if the submitted transaction
* definitively fails on-chain or confirmation times out with an
* unknown outcome
* @throws InterruptedException if the calling thread is interrupted;
* interruption propagates directly
*/
@NotNull
ΩSolanaTransactionSignatureΩ executePositionDecrease(
@@ -91,5 +112,6 @@ public interface JupiterPerpsService {
@NotNull ΩSPLMintAddressΩ receiveTokenMint,
@NotNull ΩUSDCAmountΩ sizeUsdDelta,
int maxSlippageBps
) throws IOException, InterruptedException;
) throws IOException, JupiterTransactionOutcomeException,
InterruptedException;
}
@@ -3,6 +3,7 @@ package com.r35157.libs.jupiter.perps.impl.anchoridl;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.r35157.assetaz.services.cis.CurrencyIdentityService;
import com.r35157.cryptowallet.solana.SolanaWallet;
import com.r35157.libs.jupiter.JupiterTransactionOutcomeException;
import com.r35157.libs.jupiter.perps.JupiterPerpsPosition;
import com.r35157.libs.jupiter.perps.JupiterPerpsPositionDirection;
import com.r35157.libs.jupiter.perps.JupiterPerpsService;
@@ -15,8 +16,10 @@ import com.r35157.libs.jupiter.perps.protocol.IncreasePositionResponse;
import com.r35157.libs.jupiter.perps.protocol.TransactionMetadata;
import com.r35157.libs.solana.SolanaAccountInfo;
import com.r35157.libs.solana.SolanaBlockChain;
import com.r35157.libs.solana.SolanaCommitment;
import com.r35157.libs.solana.SolanaProgramAccountMemcmpFilter;
import com.r35157.libs.solana.SolanaSignedTransaction;
import com.r35157.libs.solana.SolanaTransactionOutcome;
import com.r35157.libs.solana.SolanaUnsignedTransaction;
import com.r35157.libs.valuetypes.basic.MoneyAmount;
import org.jetbrains.annotations.NotNull;
@@ -30,6 +33,7 @@ import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
@@ -39,10 +43,45 @@ import static java.math.BigDecimal.ZERO;
public class AnchorIdlJupiterPerpsServiceImpl implements JupiterPerpsService {
/**
* Creates a Jupiter Perps service with a two-minute transaction
* confirmation timeout.
*
* @param solanaBlockChain blockchain used for account access and
* transaction confirmation
* @param wallet wallet used to sign Perps transactions
* @param currencyIdentityService canonical currency identity service
*/
public AnchorIdlJupiterPerpsServiceImpl(
SolanaBlockChain solanaBlockChain,
SolanaWallet wallet,
CurrencyIdentityService currencyIdentityService
@NotNull SolanaBlockChain solanaBlockChain,
@NotNull SolanaWallet wallet,
@NotNull CurrencyIdentityService currencyIdentityService
) {
this(
solanaBlockChain,
wallet,
currencyIdentityService,
DEFAULT_TRANSACTION_CONFIRMATION_TIMEOUT
);
}
/**
* Creates a Jupiter Perps service with an explicit confirmation timeout.
*
* @param solanaBlockChain blockchain used for account access and
* transaction confirmation
* @param wallet wallet used to sign Perps transactions
* @param currencyIdentityService canonical currency identity service
* @param transactionConfirmationTimeout positive timeout applied when
* independently awaiting submitted transaction signatures
* @throws NullPointerException if an argument is {@code null}
* @throws IllegalArgumentException if the timeout is zero or negative
*/
public AnchorIdlJupiterPerpsServiceImpl(
@NotNull SolanaBlockChain solanaBlockChain,
@NotNull SolanaWallet wallet,
@NotNull CurrencyIdentityService currencyIdentityService,
@NotNull Duration transactionConfirmationTimeout
) {
this.solanaBlockChain = Objects.requireNonNull(
solanaBlockChain,
@@ -53,6 +92,9 @@ public class AnchorIdlJupiterPerpsServiceImpl implements JupiterPerpsService {
currencyIdentityService,
"currencyIdentityService"
);
this.transactionConfirmationTimeout = requirePositiveTimeout(
transactionConfirmationTimeout
);
this.positionDecoder = new AnchorIdlJupiterPerpsPositionDecoder();
this.custodyDecoder = new AnchorIdlJupiterPerpsCustodyDecoder();
}
@@ -133,7 +175,8 @@ public class AnchorIdlJupiterPerpsServiceImpl implements JupiterPerpsService {
@NotNull ΩUSDCAmountΩ inputTokenAmount,
@NotNull ΩUSDCAmountΩ sizeUsdDelta,
int maxSlippageBps
) throws IOException, InterruptedException {
) throws IOException, JupiterTransactionOutcomeException,
InterruptedException {
SolanaUnsignedTransaction unsignedTransaction =
buildPositionIncreaseTransaction(
wallet.getAddress(),
@@ -147,7 +190,10 @@ public class AnchorIdlJupiterPerpsServiceImpl implements JupiterPerpsService {
SolanaSignedTransaction signedTransaction =
wallet.signTransaction(unsignedTransaction);
return executePositionIncreaseTransaction(signedTransaction);
ΩSolanaTransactionSignatureΩ transactionSignature =
executePositionIncreaseTransaction(signedTransaction);
awaitConfirmed("Jupiter Perps position increase", transactionSignature);
return transactionSignature;
}
private @NotNull SolanaUnsignedTransaction buildPositionIncreaseTransaction(
@@ -236,7 +282,8 @@ public class AnchorIdlJupiterPerpsServiceImpl implements JupiterPerpsService {
@NotNull ΩSPLMintAddressΩ receiveTokenMint,
@NotNull ΩUSDCAmountΩ sizeUsdDelta,
int maxSlippageBps
) throws IOException, InterruptedException {
) throws IOException, JupiterTransactionOutcomeException,
InterruptedException {
SolanaUnsignedTransaction unsignedTransaction =
buildPositionDecreaseTransaction(
positionAccount,
@@ -248,7 +295,55 @@ public class AnchorIdlJupiterPerpsServiceImpl implements JupiterPerpsService {
SolanaSignedTransaction signedTransaction =
wallet.signTransaction(unsignedTransaction);
return executePositionDecreaseTransaction(signedTransaction);
ΩSolanaTransactionSignatureΩ transactionSignature =
executePositionDecreaseTransaction(signedTransaction);
awaitConfirmed("Jupiter Perps position decrease", transactionSignature);
return transactionSignature;
}
private void awaitConfirmed(
String operation,
ΩSolanaTransactionSignatureΩ transactionSignature
) throws IOException, JupiterTransactionOutcomeException,
InterruptedException {
SolanaTransactionOutcome outcome;
try {
outcome = solanaBlockChain.awaitTransaction(
transactionSignature,
SolanaCommitment.CONFIRMED,
transactionConfirmationTimeout
);
} catch (IOException e) {
throw new IOException(
operation
+ " confirmation failed for transaction "
+ transactionSignature
+ "; the on-chain outcome is unknown",
e
);
}
switch (outcome.status()) {
case SUCCEEDED -> {
return;
}
case FAILED, TIMED_OUT -> throw
new JupiterTransactionOutcomeException(
transactionSignature,
SolanaCommitment.CONFIRMED,
outcome
);
}
}
private static Duration requirePositiveTimeout(Duration timeout) {
Objects.requireNonNull(timeout, "transactionConfirmationTimeout");
if (timeout.isZero() || timeout.isNegative()) {
throw new IllegalArgumentException(
"Transaction confirmation timeout must be greater than zero"
);
}
return timeout;
}
private @NotNull SolanaUnsignedTransaction buildPositionDecreaseTransaction(
@@ -806,6 +901,8 @@ public class AnchorIdlJupiterPerpsServiceImpl implements JupiterPerpsService {
private static final ΩJupiterPerpsProgramIdΩ JUPITER_PERPS_PROGRAM_ID = "PERPHjGBqRHArX4DySjwM6UJHiR3sWAatqfdBS2qQJu";
private static final URI INCREASE_POSITION_ENDPOINT = URI.create("https://perps-api.jup.ag/v2/positions/increase");
private static final Duration DEFAULT_TRANSACTION_CONFIRMATION_TIMEOUT =
Duration.ofMinutes(2);
private static final HttpClient HTTP_CLIENT = HttpClient.newHttpClient();
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private static final long RATE_POWER = 1_000_000_000L;
@@ -817,6 +914,7 @@ public class AnchorIdlJupiterPerpsServiceImpl implements JupiterPerpsService {
private final SolanaBlockChain solanaBlockChain;
private final SolanaWallet wallet;
private final CurrencyIdentityService currencyIdentityService;
private final Duration transactionConfirmationTimeout;
private final AnchorIdlJupiterPerpsPositionDecoder positionDecoder;
private final AnchorIdlJupiterPerpsCustodyDecoder custodyDecoder;
}
@@ -1,5 +1,6 @@
package com.r35157.libs.jupiter.swap;
import com.r35157.libs.jupiter.JupiterTransactionOutcomeException;
import com.r35157.libs.valuetypes.basic.MoneyAmount;
import org.jetbrains.annotations.NotNull;
@@ -11,18 +12,19 @@ import java.math.BigDecimal;
*
* <p>The service resolves token precision from Solana, obtains and validates a
* Jupiter Swap V2 order, asks its configured wallet to sign the transaction,
* and submits the signed transaction through Jupiter's managed execution
* endpoint.</p>
* submits the signed transaction through Jupiter's managed execution
* endpoint, and independently awaits Solana confirmation.</p>
*/
public interface JupiterSwapService {
/**
* Swaps an exact amount of one SPL token for another SPL token.
*
* <p>The returned amounts are the actual wallet-level amounts reported by
* Jupiter after successful execution, not the quoted amounts. If an error
* or interruption occurs after execution submission, the transaction's
* outcome can be unknown; callers must reload wallet balances before
* deciding whether to initiate another swap.</p>
* Jupiter after successful provider execution, not the quoted amounts.
* Normal return additionally means the reported signature independently
* reached Solana {@code CONFIRMED} success. A timed-out outcome or an I/O
* error after submission can leave the transaction outcome unknown and
* must never cause automatic resubmission.</p>
*
* @param inputTokenMint input SPL-token mint address
* @param inputTokenAmount exact input amount in human-readable token units
@@ -37,9 +39,14 @@ public interface JupiterSwapService {
* unsupported token program, or has invalid
* metadata
* @throws IOException if Solana or Jupiter communication, response
* decoding, signing, or execution fails
* decoding, signing, execution, or confirmation fails;
* after submission the on-chain outcome can be unknown
* @throws JupiterTransactionOutcomeException if the submitted transaction
* definitively fails on-chain or confirmation times out with an
* unknown outcome
* @throws InterruptedException if order pacing, Solana access, signing, or
* Jupiter communication is interrupted
* Jupiter communication or confirmation is
* interrupted; interruption propagates directly
*/
@NotNull
JupiterSwapResult swap(
@@ -47,5 +54,6 @@ public interface JupiterSwapService {
@NotNull ΩAmountΩ inputTokenAmount,
@NotNull ΩSPLMintAddressΩ outputTokenMint,
int maxSlippageBps
) throws IOException, InterruptedException;
) throws IOException, JupiterTransactionOutcomeException,
InterruptedException;
}
@@ -4,12 +4,15 @@ import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.r35157.cryptowallet.solana.SolanaWallet;
import com.r35157.libs.jupiter.JupiterTransactionOutcomeException;
import com.r35157.libs.jupiter.swap.JupiterSwapResult;
import com.r35157.libs.jupiter.swap.JupiterSwapService;
import com.r35157.libs.solana.SPLTokenSupply;
import com.r35157.libs.solana.SolanaAccountInfo;
import com.r35157.libs.solana.SolanaBlockChain;
import com.r35157.libs.solana.SolanaCommitment;
import com.r35157.libs.solana.SolanaSignedTransaction;
import com.r35157.libs.solana.SolanaTransactionOutcome;
import com.r35157.libs.solana.SolanaUnsignedTransaction;
import com.r35157.libs.solana.valuetypes.economic.SolanaSPLTokenProgram;
import com.r35157.libs.valuetypes.basic.MoneyAmount;
@@ -43,6 +46,29 @@ public final class JupiterSwapServiceImpl implements JupiterSwapService {
public JupiterSwapServiceImpl(
@NotNull SolanaBlockChain solanaBlockChain,
@NotNull SolanaWallet solanaWallet
) {
this(
solanaBlockChain,
solanaWallet,
DEFAULT_TRANSACTION_CONFIRMATION_TIMEOUT
);
}
/**
* Creates a Jupiter swap service with an explicit confirmation timeout.
*
* @param solanaBlockChain blockchain access used to resolve mint metadata
* and confirm submitted transactions
* @param solanaWallet wallet used as taker and transaction signer
* @param transactionConfirmationTimeout positive timeout applied when
* independently awaiting a submitted transaction signature
* @throws NullPointerException if an argument is {@code null}
* @throws IllegalArgumentException if the timeout is zero or negative
*/
public JupiterSwapServiceImpl(
@NotNull SolanaBlockChain solanaBlockChain,
@NotNull SolanaWallet solanaWallet,
@NotNull Duration transactionConfirmationTimeout
) {
this.solanaBlockChain = Objects.requireNonNull(
solanaBlockChain,
@@ -52,6 +78,9 @@ public final class JupiterSwapServiceImpl implements JupiterSwapService {
solanaWallet,
"solanaWallet"
);
this.transactionConfirmationTimeout = requirePositiveTimeout(
transactionConfirmationTimeout
);
}
@Override
@@ -60,7 +89,8 @@ public final class JupiterSwapServiceImpl implements JupiterSwapService {
@NotNull ΩAmountΩ inputTokenAmount,
@NotNull ΩSPLMintAddressΩ outputTokenMint,
int maxSlippageBps
) throws IOException, InterruptedException {
) throws IOException, JupiterTransactionOutcomeException,
InterruptedException {
validateArguments(
inputTokenMint,
inputTokenAmount,
@@ -116,11 +146,56 @@ public final class JupiterSwapServiceImpl implements JupiterSwapService {
validatedOrder.lastValidBlockHeight()
);
return validateExecution(
JupiterSwapResult result = validateExecution(
execution,
inputMetadata.decimals(),
outputMetadata.decimals()
);
awaitConfirmed(result.transactionSignature());
return result;
}
private void awaitConfirmed(
ΩSolanaTransactionSignatureΩ transactionSignature
) throws IOException, JupiterTransactionOutcomeException,
InterruptedException {
SolanaTransactionOutcome outcome;
try {
outcome = solanaBlockChain.awaitTransaction(
transactionSignature,
SolanaCommitment.CONFIRMED,
transactionConfirmationTimeout
);
} catch (IOException e) {
throw new IOException(
"Jupiter Swap confirmation failed for transaction "
+ transactionSignature
+ "; the on-chain outcome is unknown",
e
);
}
switch (outcome.status()) {
case SUCCEEDED -> {
return;
}
case FAILED, TIMED_OUT -> throw
new JupiterTransactionOutcomeException(
transactionSignature,
SolanaCommitment.CONFIRMED,
outcome
);
}
}
private static Duration requirePositiveTimeout(Duration timeout) {
Objects.requireNonNull(timeout, "transactionConfirmationTimeout");
if (timeout.isZero() || timeout.isNegative()) {
throw new IllegalArgumentException(
"Transaction confirmation timeout must be greater than zero"
);
}
return timeout;
}
private static void validateArguments(
@@ -691,6 +766,8 @@ public final class JupiterSwapServiceImpl implements JupiterSwapService {
"https://api.jup.ag/swap/v2/execute"
);
private static final Duration HTTP_TIMEOUT = Duration.ofSeconds(30);
private static final Duration DEFAULT_TRANSACTION_CONFIRMATION_TIMEOUT =
Duration.ofMinutes(2);
private static final HttpClient HTTP_CLIENT = HttpClient.newBuilder()
.connectTimeout(HTTP_TIMEOUT)
.build();
@@ -709,4 +786,5 @@ public final class JupiterSwapServiceImpl implements JupiterSwapService {
private final SolanaBlockChain solanaBlockChain;
private final SolanaWallet solanaWallet;
private final Duration transactionConfirmationTimeout;
}