diff --git a/src/main/tjava/com/r35157/libs/jupiter/perps/JupiterPerpsPosition.tjava b/src/main/tjava/com/r35157/libs/jupiter/perps/JupiterPerpsPosition.tjava new file mode 100644 index 0000000..238643b --- /dev/null +++ b/src/main/tjava/com/r35157/libs/jupiter/perps/JupiterPerpsPosition.tjava @@ -0,0 +1,22 @@ +package com.r35157.libs.jupiter.perps; + +import java.math.BigDecimal; + +/** + * Represents a Jupiter Perps position. + * + *
A Jupiter Perps position is represented on-chain by a Solana account owned by + * the Jupiter Perps program. This record contains the public API view of such a + * position.
+ * + *The initial version of this API only exposes the position account and the entry + * price. More position fields may be added later as the Jupiter Perps API matures.
+ * + * @param positionAccount the Solana account address of the Jupiter Perps position + * @param entryPrice the entry price of the position, denominated in USDC + */ +public record JupiterPerpsPosition( + ΩJupiterPerpsPositionAccountΩ positionAccount, + ΩUSDCPriceΩ entryPrice +) { +} \ No newline at end of file diff --git a/src/main/tjava/com/r35157/libs/jupiter/perps/JupiterPerpsPositionService.tjava b/src/main/tjava/com/r35157/libs/jupiter/perps/JupiterPerpsPositionService.tjava new file mode 100644 index 0000000..2f47ad3 --- /dev/null +++ b/src/main/tjava/com/r35157/libs/jupiter/perps/JupiterPerpsPositionService.tjava @@ -0,0 +1,30 @@ +package com.r35157.libs.jupiter.perps; + +import java.io.IOException; + +/** + * Service for reading Jupiter Perps positions. + * + *This service is read-only. It does not open, close, modify, or sign transactions + * for Jupiter Perps positions.
+ * + *The first supported operation is reading a known Jupiter Perps position account + * and returning its decoded position data.
+ */ +public interface JupiterPerpsPositionService { + /** + * Reads a Jupiter Perps position from a known position account. + * + *The supplied account must be the Solana account that stores the Jupiter Perps + * position state. It is not the wallet address, token account, custody account, pool + * account, or position request account.
+ * + * @param positionAccount the Solana account address of the Jupiter Perps position + * @return the decoded Jupiter Perps position + * @throws IOException if the position account could not be fetched or decoded + * @throws InterruptedException if the calling thread is interrupted while fetching + * the position account + */ + JupiterPerpsPosition getPosition(ΩJupiterPerpsPositionAccountΩ positionAccount) + throws IOException, InterruptedException; +} \ No newline at end of file diff --git a/src/main/tjava/com/r35157/libs/jupiter/perps/impl/anchoridl/AnchorIdlJupiterPerpsPositionDecoder.tjava b/src/main/tjava/com/r35157/libs/jupiter/perps/impl/anchoridl/AnchorIdlJupiterPerpsPositionDecoder.tjava new file mode 100644 index 0000000..3748edb --- /dev/null +++ b/src/main/tjava/com/r35157/libs/jupiter/perps/impl/anchoridl/AnchorIdlJupiterPerpsPositionDecoder.tjava @@ -0,0 +1,57 @@ +package com.r35157.libs.jupiter.perps.impl.anchoridl; + +import com.r35157.libs.jupiter.perps.JupiterPerpsPosition; +import com.r35157.libs.solana.SolanaAccountInfo; + +import java.math.BigDecimal; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Base64; + +class AnchorIdlJupiterPerpsPositionDecoder { + + JupiterPerpsPosition decode( + ΩJupiterPerpsPositionAccountΩ positionAccount, + SolanaAccountInfo accountInfo + ) { + byte[] data = Base64.getDecoder().decode(accountInfo.dataBase64()); + + if (data.length < PRICE_OFFSET + U64_LENGTH) { + throw new IllegalArgumentException( + "Jupiter Perps position account data is too short: " + data.length + ); + } + + long rawEntryPrice = ByteBuffer + .wrap(data, PRICE_OFFSET, U64_LENGTH) + .order(ByteOrder.LITTLE_ENDIAN) + .getLong(); + + ΩUSDCPriceΩ entryPrice = BigDecimal + .valueOf(rawEntryPrice) + .movePointLeft(6); + + JupiterPerpsPosition pos = new JupiterPerpsPosition( + positionAccount, + entryPrice + ); + + return pos; + } + + private static final int ANCHOR_DISCRIMINATOR_LENGTH = 8; + private static final int PUBLIC_KEY_LENGTH = 32; + private static final int I64_LENGTH = 8; + private static final int U64_LENGTH = 8; + private static final int SIDE_ENUM_LENGTH = 1; + + private static final int PRICE_OFFSET = + ANCHOR_DISCRIMINATOR_LENGTH + + PUBLIC_KEY_LENGTH // owner + + PUBLIC_KEY_LENGTH // pool + + PUBLIC_KEY_LENGTH // custody + + PUBLIC_KEY_LENGTH // collateralCustody + + I64_LENGTH // openTime + + I64_LENGTH // updateTime + + SIDE_ENUM_LENGTH; // side +} \ No newline at end of file diff --git a/src/main/tjava/com/r35157/libs/jupiter/perps/impl/anchoridl/AnchorIdlJupiterPerpsPositionServiceImpl.tjava b/src/main/tjava/com/r35157/libs/jupiter/perps/impl/anchoridl/AnchorIdlJupiterPerpsPositionServiceImpl.tjava new file mode 100644 index 0000000..79d4cbf --- /dev/null +++ b/src/main/tjava/com/r35157/libs/jupiter/perps/impl/anchoridl/AnchorIdlJupiterPerpsPositionServiceImpl.tjava @@ -0,0 +1,43 @@ +package com.r35157.libs.jupiter.perps.impl.anchoridl; + +import com.r35157.libs.jupiter.perps.JupiterPerpsPosition; +import com.r35157.libs.jupiter.perps.JupiterPerpsPositionService; +import com.r35157.libs.solana.SolanaAccountInfo; +import com.r35157.libs.solana.SolanaBlockChain; + +import java.io.IOException; + +public class AnchorIdlJupiterPerpsPositionServiceImpl implements JupiterPerpsPositionService { + + public AnchorIdlJupiterPerpsPositionServiceImpl( + SolanaBlockChain solanaBlockChain + ) { + this.solanaBlockChain = solanaBlockChain; + this.positionDecoder = new AnchorIdlJupiterPerpsPositionDecoder(); + } + + @Override + public JupiterPerpsPosition getPosition(ΩJupiterPerpsPositionAccountΩ positionAccount) + throws IOException, InterruptedException { + SolanaAccountInfo accountInfo = solanaBlockChain.getAccountInfo(positionAccount); + + if (accountInfo == null) { + throw new IllegalArgumentException("Jupiter Perps position account does not exist: " + positionAccount); + } + + if (!JUPITER_PERPS_PROGRAM_ID.equals(accountInfo.owner())) { + throw new IllegalArgumentException( + "Account is not owned by Jupiter Perps program: " + positionAccount + ); + } + + JupiterPerpsPosition pos = positionDecoder.decode(positionAccount, accountInfo); + return pos; + } + + private static final ΩJupiterPerpsProgramIdΩ JUPITER_PERPS_PROGRAM_ID = + "PERPHjGBqRHArX4DySjwM6UJHiR3sWAatqfdBS2qQJu"; + + private final SolanaBlockChain solanaBlockChain; + private final AnchorIdlJupiterPerpsPositionDecoder positionDecoder; +} \ No newline at end of file