68: Await CONFIRMED on-chain outcomes for Jupiter Swap and Perps operations
This commit is contained in:
+34
@@ -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
|
||||
|
||||
+34
@@ -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;
|
||||
}
|
||||
|
||||
+105
-7
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user