38: Add support for building and executing Jupiter Perps position decrease transactions
This commit is contained in:
@@ -103,4 +103,53 @@ public interface JupiterPerpsService {
|
||||
ΩSolanaTransactionSignatureΩ executePositionIncreaseTransaction(
|
||||
@NotNull SolanaSignedTransaction transaction
|
||||
) throws IOException, InterruptedException;
|
||||
|
||||
/**
|
||||
* Constructs an unsigned Solana transaction for decreasing
|
||||
* an existing Jupiter Perps position.
|
||||
*
|
||||
* <p>This method does not sign or submit the transaction and therefore
|
||||
* does not modify blockchain state. The returned transaction must be
|
||||
* signed by the position owner's wallet and submitted separately.</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 complete unsigned Solana transaction
|
||||
* @throws IllegalArgumentException if an amount, mint, or slippage value is invalid
|
||||
* @throws IOException if Jupiter cannot construct the transaction or its response
|
||||
* cannot be decoded
|
||||
* @throws InterruptedException if the calling thread is interrupted while
|
||||
* communicating with Jupiter
|
||||
*/
|
||||
@NotNull SolanaUnsignedTransaction buildPositionDecreaseTransaction(
|
||||
@NotNull ΩJupiterPerpsPositionAccountΩ positionAccount,
|
||||
@NotNull ΩSPLMintAddressΩ receiveTokenMint,
|
||||
@NotNull ΩUSDCAmountΩ sizeUsdDelta,
|
||||
int maxSlippageBps
|
||||
) throws IOException, InterruptedException;
|
||||
|
||||
/**
|
||||
* Executes a signed Jupiter Perps position-decrease transaction.
|
||||
*
|
||||
* <p>The supplied transaction must previously have been constructed by
|
||||
* {@link #buildPositionDecreaseTransaction} and signed by its required
|
||||
* Solana wallet.</p>
|
||||
*
|
||||
* <p>Unlike the build method, this operation submits the transaction and
|
||||
* may therefore modify blockchain state.</p>
|
||||
*
|
||||
* @param transaction the complete signed position-decrease transaction
|
||||
* @return the Solana transaction signature
|
||||
* @throws IllegalArgumentException if the transaction is missing or blank
|
||||
* @throws IOException if Jupiter rejects the transaction or its response
|
||||
* cannot be decoded
|
||||
* @throws InterruptedException if the calling thread is interrupted while
|
||||
* communicating with Jupiter
|
||||
*/
|
||||
@NotNull
|
||||
ΩSolanaTransactionSignatureΩ executePositionDecreaseTransaction(
|
||||
@NotNull SolanaSignedTransaction transaction
|
||||
) throws IOException, InterruptedException;
|
||||
}
|
||||
+220
-3
@@ -4,6 +4,8 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.r35157.libs.jupiter.perps.JupiterPerpsPosition;
|
||||
import com.r35157.libs.jupiter.perps.JupiterPerpsPositionDirection;
|
||||
import com.r35157.libs.jupiter.perps.JupiterPerpsService;
|
||||
import com.r35157.libs.jupiter.perps.protocol.DecreasePositionRequest;
|
||||
import com.r35157.libs.jupiter.perps.protocol.DecreasePositionResponse;
|
||||
import com.r35157.libs.jupiter.perps.protocol.ExecuteTransactionRequest;
|
||||
import com.r35157.libs.jupiter.perps.protocol.ExecuteTransactionResponse;
|
||||
import com.r35157.libs.jupiter.perps.protocol.IncreasePositionRequest;
|
||||
@@ -193,6 +195,120 @@ public class AnchorIdlJupiterPerpsServiceImpl implements JupiterPerpsService {
|
||||
return response.txid();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull SolanaUnsignedTransaction buildPositionDecreaseTransaction(
|
||||
@NotNull ΩJupiterPerpsPositionAccountΩ positionAccount,
|
||||
@NotNull ΩSPLMintAddressΩ receiveTokenMint,
|
||||
@NotNull ΩUSDCAmountΩ sizeUsdDelta,
|
||||
int maxSlippageBps
|
||||
) throws IOException, InterruptedException {
|
||||
DecreasePositionRequest request = buildDecreasePositionRequest(
|
||||
positionAccount,
|
||||
receiveTokenMint,
|
||||
sizeUsdDelta,
|
||||
maxSlippageBps
|
||||
);
|
||||
|
||||
DecreasePositionResponse response =
|
||||
requestPositionDecrease(request);
|
||||
|
||||
TransactionMetadata metadata = response.txMetadata();
|
||||
|
||||
if (response.serializedTxBase64() == null
|
||||
|| response.serializedTxBase64().isBlank()) {
|
||||
throw new IOException(
|
||||
"Jupiter Perps response did not contain a serialized transaction"
|
||||
);
|
||||
}
|
||||
|
||||
if (metadata == null) {
|
||||
throw new IOException(
|
||||
"Jupiter Perps response did not contain transaction metadata"
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
return new SolanaUnsignedTransaction(
|
||||
response.serializedTxBase64(),
|
||||
metadata.blockhash(),
|
||||
Long.parseLong(metadata.lastValidBlockHeight())
|
||||
);
|
||||
} catch (NumberFormatException e) {
|
||||
throw new IOException(
|
||||
"Invalid lastValidBlockHeight returned by Jupiter: "
|
||||
+ metadata.lastValidBlockHeight(),
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull ΩSolanaTransactionSignatureΩ
|
||||
executePositionDecreaseTransaction(
|
||||
@NotNull SolanaSignedTransaction transaction
|
||||
) throws IOException, InterruptedException {
|
||||
if (transaction.serializedTransaction() == null
|
||||
|| transaction.serializedTransaction().isBlank()) {
|
||||
throw new IllegalArgumentException(
|
||||
"Signed transaction must not be blank"
|
||||
);
|
||||
}
|
||||
|
||||
ExecuteTransactionRequest request =
|
||||
new ExecuteTransactionRequest(
|
||||
"decrease-position",
|
||||
transaction.serializedTransaction()
|
||||
);
|
||||
|
||||
ExecuteTransactionResponse response =
|
||||
requestTransactionExecution(request);
|
||||
|
||||
if (response.txid() == null || response.txid().isBlank()) {
|
||||
throw new IOException(
|
||||
"Jupiter transaction execution response did not "
|
||||
+ "contain a transaction signature"
|
||||
);
|
||||
}
|
||||
|
||||
return response.txid();
|
||||
}
|
||||
|
||||
private static DecreasePositionRequest buildDecreasePositionRequest(
|
||||
ΩJupiterPerpsPositionAccountΩ positionAccount,
|
||||
ΩSPLMintAddressΩ receiveTokenMint,
|
||||
ΩUSDCAmountΩ sizeUsdDelta,
|
||||
int maxSlippageBps
|
||||
) {
|
||||
if (positionAccount.isBlank()) {
|
||||
throw new IllegalArgumentException(
|
||||
"Position account must not be blank"
|
||||
);
|
||||
}
|
||||
|
||||
if (sizeUsdDelta.signum() <= 0) {
|
||||
throw new IllegalArgumentException(
|
||||
"Position size delta must be greater than zero: "
|
||||
+ sizeUsdDelta
|
||||
);
|
||||
}
|
||||
|
||||
if (maxSlippageBps < 0 || maxSlippageBps > BASIS_POINTS_DIVISOR) {
|
||||
throw new IllegalArgumentException(
|
||||
"Maximum slippage must be between 0 and "
|
||||
+ BASIS_POINTS_DIVISOR
|
||||
+ " basis points: "
|
||||
+ maxSlippageBps
|
||||
);
|
||||
}
|
||||
|
||||
return new DecreasePositionRequest(
|
||||
positionAccount,
|
||||
toProtocolReceiveToken(receiveTokenMint),
|
||||
toMicroAmount(sizeUsdDelta),
|
||||
Integer.toString(maxSlippageBps)
|
||||
);
|
||||
}
|
||||
|
||||
private JupiterPerpsPosition buildPosition(
|
||||
ΩJupiterPerpsPositionAccountΩ positionAccount,
|
||||
JupiterPerpsPositionInfo perpsPositionInfo
|
||||
@@ -415,6 +531,31 @@ public class AnchorIdlJupiterPerpsServiceImpl implements JupiterPerpsService {
|
||||
);
|
||||
}
|
||||
|
||||
private static String toProtocolReceiveToken(
|
||||
ΩSPLMintAddressΩ receiveTokenMint
|
||||
) {
|
||||
if (SOL_MINT.equals(receiveTokenMint)) {
|
||||
return "SOL";
|
||||
}
|
||||
|
||||
if (BTC_MINT.equals(receiveTokenMint)) {
|
||||
return "BTC";
|
||||
}
|
||||
|
||||
if (ETH_MINT.equals(receiveTokenMint)) {
|
||||
return "ETH";
|
||||
}
|
||||
|
||||
if (USDC_MINT.equals(receiveTokenMint)) {
|
||||
return "USDC";
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException(
|
||||
"Unsupported Jupiter Perps receive-token mint: "
|
||||
+ receiveTokenMint
|
||||
);
|
||||
}
|
||||
|
||||
private static String toProtocolSide(
|
||||
JupiterPerpsPositionDirection direction
|
||||
) {
|
||||
@@ -488,6 +629,76 @@ public class AnchorIdlJupiterPerpsServiceImpl implements JupiterPerpsService {
|
||||
return response;
|
||||
}
|
||||
|
||||
private static DecreasePositionResponse requestPositionDecrease(
|
||||
DecreasePositionRequest request
|
||||
) throws IOException, InterruptedException {
|
||||
String requestJson = OBJECT_MAPPER.writeValueAsString(request);
|
||||
|
||||
HttpRequest httpRequest = HttpRequest.newBuilder()
|
||||
.uri(DECREASE_POSITION_ENDPOINT)
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Accept", "application/json")
|
||||
.header("x-client-platform", "nenjim-hub")
|
||||
.POST(HttpRequest.BodyPublishers.ofString(requestJson))
|
||||
.build();
|
||||
|
||||
HttpResponse<String> httpResponse = HTTP_CLIENT.send(
|
||||
httpRequest,
|
||||
HttpResponse.BodyHandlers.ofString()
|
||||
);
|
||||
|
||||
if (httpResponse.statusCode() < 200
|
||||
|| httpResponse.statusCode() >= 300) {
|
||||
throw new IOException(
|
||||
"Jupiter Perps decrease-position request failed with HTTP "
|
||||
+ httpResponse.statusCode()
|
||||
+ ": "
|
||||
+ httpResponse.body()
|
||||
);
|
||||
}
|
||||
|
||||
DecreasePositionResponse response = OBJECT_MAPPER.readValue(
|
||||
httpResponse.body(),
|
||||
DecreasePositionResponse.class
|
||||
);
|
||||
|
||||
TransactionMetadata metadata = response.txMetadata();
|
||||
|
||||
if (metadata == null) {
|
||||
throw new IOException(
|
||||
"Jupiter Perps response did not contain transaction metadata"
|
||||
);
|
||||
}
|
||||
|
||||
if (metadata.blockhash() == null || metadata.blockhash().isBlank()) {
|
||||
throw new IOException(
|
||||
"Jupiter Perps response did not contain a blockhash"
|
||||
);
|
||||
}
|
||||
|
||||
long lastValidBlockHeight;
|
||||
|
||||
try {
|
||||
lastValidBlockHeight =
|
||||
Long.parseLong(metadata.lastValidBlockHeight());
|
||||
} catch (NumberFormatException e) {
|
||||
throw new IOException(
|
||||
"Invalid lastValidBlockHeight returned by Jupiter: "
|
||||
+ metadata.lastValidBlockHeight(),
|
||||
e
|
||||
);
|
||||
}
|
||||
|
||||
if (lastValidBlockHeight <= 0) {
|
||||
throw new IOException(
|
||||
"Invalid lastValidBlockHeight returned by Jupiter: "
|
||||
+ lastValidBlockHeight
|
||||
);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
private static ExecuteTransactionResponse requestTransactionExecution(
|
||||
ExecuteTransactionRequest request
|
||||
) throws IOException, InterruptedException {
|
||||
@@ -523,15 +734,21 @@ public class AnchorIdlJupiterPerpsServiceImpl implements JupiterPerpsService {
|
||||
);
|
||||
}
|
||||
|
||||
private static final ΩSPLMintAddressΩ SOL_MINT = "So11111111111111111111111111111111111111112";
|
||||
private static final ΩSPLMintAddressΩ BTC_MINT = "3NZ9JMVBmGAqocybic2c7LQCJScmgsAZ6vQqTDzcqmJh";
|
||||
private static final ΩSPLMintAddressΩ ETH_MINT = "7vfCXTUXx5WJV5JADk17DUJ4ksgau7utNKj4b963voxs";
|
||||
private static final ΩSPLMintAddressΩ BTC_MINT = "3NZ9JMVBmGAqocybic2c7LQCJScmgsAZ6vQqTDzcqmJh";
|
||||
private static final ΩSPLMintAddressΩ ETH_MINT = "7vfCXTUXx5WJV5JADk17DUJ4ksgau7utNKj4b963voxs";
|
||||
private static final ΩSPLMintAddressΩ SOL_MINT = "So11111111111111111111111111111111111111112";
|
||||
private static final ΩSPLMintAddressΩ USDC_MINT = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
|
||||
|
||||
private static final URI EXECUTE_TRANSACTION_ENDPOINT =
|
||||
URI.create(
|
||||
"https://perps-api.jup.ag/v2/transaction/execute"
|
||||
);
|
||||
|
||||
private static final URI DECREASE_POSITION_ENDPOINT =
|
||||
URI.create(
|
||||
"https://perps-api.jup.ag/v2/positions/decrease"
|
||||
);
|
||||
|
||||
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");
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.r35157.libs.jupiter.perps.protocol;
|
||||
|
||||
/**
|
||||
* Jupiter Perps protocol request for constructing an unsigned
|
||||
* position-decrease transaction.
|
||||
*
|
||||
* @param positionPubkey the Solana account address of the position to decrease
|
||||
* @param receiveToken the symbol of the token to receive
|
||||
* @param sizeUsdDelta the requested position-size decrease in micro-USD,
|
||||
* encoded as a decimal string
|
||||
* @param maxSlippageBps the maximum accepted slippage in basis points,
|
||||
* encoded as a decimal string
|
||||
*/
|
||||
public record DecreasePositionRequest(
|
||||
String positionPubkey,
|
||||
String receiveToken,
|
||||
String sizeUsdDelta,
|
||||
String maxSlippageBps
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.r35157.libs.jupiter.perps.protocol;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
|
||||
/**
|
||||
* Relevant fields returned by Jupiter after constructing an unsigned
|
||||
* position-decrease transaction.
|
||||
*
|
||||
* <p>Additional response fields that are not required by the current
|
||||
* implementation are ignored during decoding.</p>
|
||||
*
|
||||
* @param positionPubkey the Solana account address of the decreased position
|
||||
* @param serializedTxBase64 the complete unsigned Solana transaction,
|
||||
* encoded as Base64
|
||||
* @param txMetadata metadata describing the transaction's blockhash
|
||||
* and validity period
|
||||
*/
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public record DecreasePositionResponse(
|
||||
String positionPubkey,
|
||||
String serializedTxBase64,
|
||||
TransactionMetadata txMetadata
|
||||
) {
|
||||
}
|
||||
Reference in New Issue
Block a user