diff --git a/bonfida/pom.xml b/bonfida/pom.xml index 733b8c9..ad234f0 100644 --- a/bonfida/pom.xml +++ b/bonfida/pom.xml @@ -5,7 +5,7 @@ solanaj-programs com.mmorrell - 1.31.0 + 1.32.0-SNAPSHOT 4.0.0 diff --git a/jupiter/pom.xml b/jupiter/pom.xml new file mode 100644 index 0000000..8c7abeb --- /dev/null +++ b/jupiter/pom.xml @@ -0,0 +1,28 @@ + + + 4.0.0 + + com.mmorrell + solanaj-programs + 1.32.0-SNAPSHOT + + + jupiter + + + 17 + 17 + UTF-8 + + + + com.mmorrell + openbook + 1.32.0-SNAPSHOT + compile + + + + \ No newline at end of file diff --git a/jupiter/src/main/java/com/mmorrell/jupiter/manager/JupiterManager.java b/jupiter/src/main/java/com/mmorrell/jupiter/manager/JupiterManager.java new file mode 100644 index 0000000..b38256a --- /dev/null +++ b/jupiter/src/main/java/com/mmorrell/jupiter/manager/JupiterManager.java @@ -0,0 +1,97 @@ +package com.mmorrell.jupiter.manager; + +import com.mmorrell.jupiter.model.*; +import lombok.extern.slf4j.Slf4j; +import org.p2p.solanaj.core.PublicKey; +import org.p2p.solanaj.rpc.Cluster; +import org.p2p.solanaj.rpc.RpcClient; +import org.p2p.solanaj.rpc.RpcException; +import org.p2p.solanaj.rpc.types.AccountInfo; + +import java.util.Base64; +import java.util.Optional; + +@Slf4j +public class JupiterManager { + + private final RpcClient client; + private static final PublicKey JUPITER_PROGRAM_ID = new PublicKey("PERPHjGBqRHArX4DySjwM6UJHiR3sWAatqfdBS2qQJu"); + + public JupiterManager() { + this.client = new RpcClient(Cluster.MAINNET); + } + + public JupiterManager(RpcClient client) { + this.client = client; + } + + public Optional getPosition(PublicKey positionPublicKey) { + try { + AccountInfo accountInfo = client.getApi().getAccountInfo(positionPublicKey); + if (accountInfo == null || accountInfo.getValue() == null) { + return Optional.empty(); + } + byte[] data = Base64.getDecoder().decode(accountInfo.getValue().getData().get(0)); + return Optional.of(JupiterPerpPosition.fromByteArray(data)); + } catch (RpcException e) { + log.warn("Error fetching position: {}", e.getMessage()); + return Optional.empty(); + } + } + + public Optional getPool(PublicKey poolPublicKey) { + try { + AccountInfo accountInfo = client.getApi().getAccountInfo(poolPublicKey); + if (accountInfo == null || accountInfo.getValue() == null) { + return Optional.empty(); + } + byte[] data = Base64.getDecoder().decode(accountInfo.getValue().getData().get(0)); + return Optional.of(JupiterPool.fromByteArray(data)); + } catch (RpcException e) { + log.warn("Error fetching pool: {}", e.getMessage()); + return Optional.empty(); + } + } + + public Optional getCustody(PublicKey custodyPublicKey) { + try { + AccountInfo accountInfo = client.getApi().getAccountInfo(custodyPublicKey); + if (accountInfo == null || accountInfo.getValue() == null) { + return Optional.empty(); + } + byte[] data = Base64.getDecoder().decode(accountInfo.getValue().getData().get(0)); + return Optional.of(JupiterCustody.fromByteArray(data)); + } catch (RpcException e) { + log.warn("Error fetching custody: {}", e.getMessage()); + return Optional.empty(); + } + } + + public Optional getPositionRequest(PublicKey positionRequestPublicKey) { + try { + AccountInfo accountInfo = client.getApi().getAccountInfo(positionRequestPublicKey); + if (accountInfo == null || accountInfo.getValue() == null) { + return Optional.empty(); + } + byte[] data = Base64.getDecoder().decode(accountInfo.getValue().getData().get(0)); + return Optional.of(JupiterPositionRequest.fromByteArray(data)); + } catch (RpcException e) { + log.warn("Error fetching position request: {}", e.getMessage()); + return Optional.empty(); + } + } + + public Optional getPerpetuals(PublicKey perpetualsPublicKey) { + try { + AccountInfo accountInfo = client.getApi().getAccountInfo(perpetualsPublicKey); + if (accountInfo == null || accountInfo.getValue() == null) { + return Optional.empty(); + } + byte[] data = Base64.getDecoder().decode(accountInfo.getValue().getData().get(0)); + return Optional.of(JupiterPerpetuals.fromByteArray(data)); + } catch (RpcException e) { + log.warn("Error fetching perpetuals: {}", e.getMessage()); + return Optional.empty(); + } + } +} diff --git a/jupiter/src/main/java/com/mmorrell/jupiter/model/JupiterCustody.java b/jupiter/src/main/java/com/mmorrell/jupiter/model/JupiterCustody.java new file mode 100644 index 0000000..f3798d9 --- /dev/null +++ b/jupiter/src/main/java/com/mmorrell/jupiter/model/JupiterCustody.java @@ -0,0 +1,227 @@ +package com.mmorrell.jupiter.model; + +import com.mmorrell.jupiter.util.JupiterUtil; +import com.mmorrell.openbook.OpenBookUtil; + +import lombok.Builder; +import lombok.Data; +import org.p2p.solanaj.core.PublicKey; + +/** + * Represents a Jupiter Custody account in Jupiter Perpetuals. + */ +@Data +@Builder +public class JupiterCustody { + private PublicKey pool; + private PublicKey mint; + private PublicKey tokenAccount; + private byte decimals; + private boolean isStable; + private OracleParams oracle; + private PricingParams pricing; + private Permissions permissions; + private long targetRatioBps; + private Assets assets; + private FundingRateState fundingRateState; + private byte bump; + private byte tokenAccountBump; + + @Data + @Builder + public static class OracleParams { + private PublicKey oracleAccount; + private byte oracleType; + private long maxPriceError; + private int maxPriceAgeSec; + } + + @Data + @Builder + public static class PricingParams { + private long tradeImpactFeeScalar; + private long buffer; + private long swapSpread; + private long maxLeverage; + private long maxGlobalLongSizes; + private long maxGlobalShortSizes; + } + + @Data + @Builder + public static class Permissions { + private boolean allowDeposit; + private boolean allowWithdraw; + private boolean allowTrade; + private boolean allowSwap; + private boolean allowAddLiquidity; + private boolean allowRemoveLiquidity; + private boolean allowUseAsCollateral; + } + + @Data + @Builder + public static class Assets { + private long feesReserves; + private long owned; + private long locked; + private long guaranteedUsd; + private long globalShortSizes; + private long globalShortAveragePrices; + } + + @Data + @Builder + public static class FundingRateState { + private long cumulativeInterestRate; + private long lastUpdate; + private long hourlyFundingDbps; + } + + /** + * Deserializes a byte array into a JupiterCustody object. + * + * @param data the byte array representing the account data. + * @return a JupiterCustody object. + */ + public static JupiterCustody fromByteArray(byte[] data) { + int offset = 8; // Skip discriminator + + PublicKey pool = PublicKey.readPubkey(data, offset); + offset += 32; + + PublicKey mint = PublicKey.readPubkey(data, offset); + offset += 32; + + PublicKey tokenAccount = PublicKey.readPubkey(data, offset); + offset += 32; + + byte decimals = data[offset++]; + boolean isStable = data[offset++] != 0; + + OracleParams oracle = readOracleParams(data, offset); + offset += 45; // 32 (publicKey) + 1 (oracleType) + 8 (maxPriceError) + 4 (maxPriceAgeSec) + + PricingParams pricing = readPricingParams(data, offset); + offset += 48; // Adjust based on actual size + + Permissions permissions = readPermissions(data, offset); + offset += 7; // Adjust based on actual size + + long targetRatioBps = JupiterUtil.readUint64(data, offset); + offset += 8; + + Assets assets = readAssets(data, offset); + offset += 48; // 6 fields * 8 bytes each + + FundingRateState fundingRateState = readFundingRateState(data, offset); + offset += 32; // 16 (cumulativeInterestRate) + 8 (lastUpdate) + 8 (hourlyFundingDbps) + + byte bump = data[offset++]; + byte tokenAccountBump = data[offset]; + + return JupiterCustody.builder() + .pool(pool) + .mint(mint) + .tokenAccount(tokenAccount) + .decimals(decimals) + .isStable(isStable) + .oracle(oracle) + .pricing(pricing) + .permissions(permissions) + .targetRatioBps(targetRatioBps) + .assets(assets) + .fundingRateState(fundingRateState) + .bump(bump) + .tokenAccountBump(tokenAccountBump) + .build(); + } + + private static OracleParams readOracleParams(byte[] data, int offset) { + PublicKey oracleAccount = PublicKey.readPubkey(data, offset); + offset += 32; + byte oracleType = data[offset++]; + long maxPriceError = JupiterUtil.readUint64(data, offset); + offset += 8; + int maxPriceAgeSec = OpenBookUtil.readInt32(data, offset); + + return OracleParams.builder() + .oracleAccount(oracleAccount) + .oracleType(oracleType) + .maxPriceError(maxPriceError) + .maxPriceAgeSec(maxPriceAgeSec) + .build(); + } + + private static PricingParams readPricingParams(byte[] data, int offset) { + long tradeImpactFeeScalar = JupiterUtil.readUint64(data, offset); + offset += 8; + long buffer = JupiterUtil.readUint64(data, offset); + offset += 8; + long swapSpread = JupiterUtil.readUint64(data, offset); + offset += 8; + long maxLeverage = JupiterUtil.readUint64(data, offset); + offset += 8; + long maxGlobalLongSizes = JupiterUtil.readUint64(data, offset); + offset += 8; + long maxGlobalShortSizes = JupiterUtil.readUint64(data, offset); + + return PricingParams.builder() + .tradeImpactFeeScalar(tradeImpactFeeScalar) + .buffer(buffer) + .swapSpread(swapSpread) + .maxLeverage(maxLeverage) + .maxGlobalLongSizes(maxGlobalLongSizes) + .maxGlobalShortSizes(maxGlobalShortSizes) + .build(); + } + + private static Permissions readPermissions(byte[] data, int offset) { + return Permissions.builder() + .allowDeposit(data[offset++] != 0) + .allowWithdraw(data[offset++] != 0) + .allowTrade(data[offset++] != 0) + .allowSwap(data[offset++] != 0) + .allowAddLiquidity(data[offset++] != 0) + .allowRemoveLiquidity(data[offset++] != 0) + .allowUseAsCollateral(data[offset] != 0) + .build(); + } + + private static Assets readAssets(byte[] data, int offset) { + long feesReserves = JupiterUtil.readUint64(data, offset); + offset += 8; + long owned = JupiterUtil.readUint64(data, offset); + offset += 8; + long locked = JupiterUtil.readUint64(data, offset); + offset += 8; + long guaranteedUsd = JupiterUtil.readUint64(data, offset); + offset += 8; + long globalShortSizes = JupiterUtil.readUint64(data, offset); + offset += 8; + long globalShortAveragePrices = JupiterUtil.readUint64(data, offset); + + return Assets.builder() + .feesReserves(feesReserves) + .owned(owned) + .locked(locked) + .guaranteedUsd(guaranteedUsd) + .globalShortSizes(globalShortSizes) + .globalShortAveragePrices(globalShortAveragePrices) + .build(); + } + + private static FundingRateState readFundingRateState(byte[] data, int offset) { + long cumulativeInterestRate = OpenBookUtil.readUint128(data, offset).longValue(); + offset += 16; + long lastUpdate = JupiterUtil.readInt64(data, offset); + offset += 8; + long hourlyFundingDbps = JupiterUtil.readUint64(data, offset); + + return FundingRateState.builder() + .cumulativeInterestRate(cumulativeInterestRate) + .lastUpdate(lastUpdate) + .hourlyFundingDbps(hourlyFundingDbps) + .build(); + } +} \ No newline at end of file diff --git a/jupiter/src/main/java/com/mmorrell/jupiter/model/JupiterPerpPosition.java b/jupiter/src/main/java/com/mmorrell/jupiter/model/JupiterPerpPosition.java new file mode 100644 index 0000000..d15ad51 --- /dev/null +++ b/jupiter/src/main/java/com/mmorrell/jupiter/model/JupiterPerpPosition.java @@ -0,0 +1,78 @@ +package com.mmorrell.jupiter.model; + +import lombok.Builder; +import lombok.Data; +import org.p2p.solanaj.core.PublicKey; + +/** + * 8 (padding) + + * 32 (owner) + + * 32 (pool) + + * 32 (custody) + + * 32 (collateralCustody) + + * 8 (openTime) + + * 8 (updateTime) + + * 4 (side) + + * 8 (price) + + * 8 (sizeUsd) + + * 8 (collateralUsd) + + * 8 (realisedPnlUsd) + + * 8 (cumulativeInterestSnapshot) + + * 8 (lockedAmount) + + * 4 (bump) = 8 + 128 + 64 + 4 + 4 = 216 bytes + */ +@Data +@Builder +public class JupiterPerpPosition { + private PublicKey owner; + private PublicKey pool; + private PublicKey custody; + private PublicKey collateralCustody; + private long openTime; + private long updateTime; + private Side side; + private long price; + private long sizeUsd; + private long collateralUsd; + private long realisedPnlUsd; + private long cumulativeInterestSnapshot; + private long lockedAmount; + private byte bump; + + public enum Side { + LONG, + SHORT + } + + public static JupiterPerpPosition fromByteArray(byte[] data) { + int offset = 8; // Start at offset 8 to skip the padding + return JupiterPerpPosition.builder() + .owner(PublicKey.readPubkey(data, offset)) + .pool(PublicKey.readPubkey(data, offset += 32)) + .custody(PublicKey.readPubkey(data, offset += 32)) + .collateralCustody(PublicKey.readPubkey(data, offset += 32)) + .openTime(readInt64(data, offset += 32)) + .updateTime(readInt64(data, offset += 8)) + .side(data[offset += 8] == 1 ? Side.LONG : Side.SHORT) + .price(readUint64(data, offset += 1)) + .sizeUsd(readUint64(data, offset += 8)) + .collateralUsd(readUint64(data, offset += 8)) + .realisedPnlUsd(readInt64(data, offset += 8)) + .cumulativeInterestSnapshot(readUint128(data, offset += 8)) + .lockedAmount(readUint64(data, offset += 16)) + .bump(data[offset += 8]) + .build(); + } + + private static long readInt64(byte[] data, int offset) { + return org.bitcoinj.core.Utils.readInt64(data, offset); + } + + private static long readUint64(byte[] data, int offset) { + return org.bitcoinj.core.Utils.readInt64(data, offset); + } + + private static long readUint128(byte[] data, int offset) { + return org.bitcoinj.core.Utils.readInt64(data, offset); + } +} \ No newline at end of file diff --git a/jupiter/src/main/java/com/mmorrell/jupiter/model/JupiterPerpetuals.java b/jupiter/src/main/java/com/mmorrell/jupiter/model/JupiterPerpetuals.java new file mode 100644 index 0000000..6c7a732 --- /dev/null +++ b/jupiter/src/main/java/com/mmorrell/jupiter/model/JupiterPerpetuals.java @@ -0,0 +1,81 @@ +package com.mmorrell.jupiter.model; + +import com.mmorrell.jupiter.util.JupiterUtil; +import lombok.Builder; +import lombok.Data; +import org.p2p.solanaj.core.PublicKey; + +/** + * Represents a Jupiter Perpetuals account in Jupiter Perpetuals. + */ +@Data +@Builder +public class JupiterPerpetuals { + private Permissions permissions; + private PublicKey pool; // Changed from List to PublicKey + private PublicKey admin; + private byte transferAuthorityBump; + private byte perpetualsBump; + private long inceptionTime; + + @Data + @Builder + public static class Permissions { + private boolean allowSwap; + private boolean allowAddLiquidity; + private boolean allowRemoveLiquidity; + private boolean allowIncreasePosition; // New field + private boolean allowDecreasePosition; // New field + private boolean allowCollateralWithdrawal; + private boolean allowLiquidatePosition; // New field + } + + /** + * Deserializes a byte array into a JupiterPerpetuals object. + * + * @param data the byte array representing the account data. + * @return a JupiterPerpetuals object. + */ + public static JupiterPerpetuals fromByteArray(byte[] data) { + int offset = 8; // Skip discriminator + + Permissions permissions = readPermissions(data, offset); + offset += 8; // Adjust based on actual size + + // Hardcoded offsets based on research + offset = 19; // Set offset to the found pool offset + PublicKey pool = PublicKey.readPubkey(data, offset); + offset += 32; // Move to the next field + + offset = 51; // Set offset to the found admin offset + PublicKey admin = PublicKey.readPubkey(data, offset); + offset += 32; + + byte transferAuthorityBump = data[offset++]; + byte perpetualsBump = data[offset++]; + + long inceptionTime = JupiterUtil.readInt64(data, offset); + + return JupiterPerpetuals.builder() + .permissions(permissions) + .pool(pool) // Set the single pool PublicKey + .admin(admin) + .transferAuthorityBump(transferAuthorityBump) + .perpetualsBump(perpetualsBump) + .inceptionTime(inceptionTime) + .build(); + } + + // Add private static methods to read Permissions and PublicKey + private static Permissions readPermissions(byte[] data, int offset) { + return Permissions.builder() + .allowSwap(data[offset++] != 0) + .allowAddLiquidity(data[offset++] != 0) + .allowRemoveLiquidity(data[offset++] != 0) + .allowIncreasePosition(data[offset++] != 0) // New field + .allowDecreasePosition(data[offset++] != 0) // New field + .allowCollateralWithdrawal(data[offset++] != 0) + .allowLiquidatePosition(data[offset] != 0) // New field + .build(); + } +} \ No newline at end of file diff --git a/jupiter/src/main/java/com/mmorrell/jupiter/model/JupiterPool.java b/jupiter/src/main/java/com/mmorrell/jupiter/model/JupiterPool.java new file mode 100644 index 0000000..c72cfb7 --- /dev/null +++ b/jupiter/src/main/java/com/mmorrell/jupiter/model/JupiterPool.java @@ -0,0 +1,172 @@ +package com.mmorrell.jupiter.model; + +import com.mmorrell.openbook.OpenBookUtil; +import lombok.Builder; +import lombok.Data; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.p2p.solanaj.core.PublicKey; + +import static com.mmorrell.openbook.OpenBookUtil.readInt32; + +/** + * Represents a Jupiter Perpetuals Pool account. + */ +@Data +@Builder +public class JupiterPool { + private String name; + private List custodies; + private long aumUsd; + private Limit limit; + private Fees fees; + private PoolApr poolApr; + private long maxRequestExecutionSec; + private byte bump; + private byte lpTokenBump; + private long inceptionTime; + + @Data + @Builder + public static class Limit { + private long maxAumUsd; + private long tokenWeightageBufferBps; + private long maxPositionUsd; + } + + @Data + @Builder + public static class Fees { + private long increasePositionBps; + private long decreasePositionBps; + private long addRemoveLiquidityBps; + private long swapBps; + private long taxBps; + private long stableSwapBps; + private long stableSwapTaxBps; + private long liquidationRewardBps; + private long protocolShareBps; + } + + @Data + @Builder + public static class PoolApr { + private long lastUpdated; + private long feeAprBps; + private long realizedFeeUsd; + } + + /** + * Deserializes a byte array into a JupiterPool object. + * + * @param data the byte array representing the account data. + * @return a JupiterPool object. + */ + public static JupiterPool fromByteArray(byte[] data) { + int offset = 8; // Skip discriminator + + String name = readString(data, offset); + offset += 4 + name.length(); // 4 bytes for length + actual string length + + List custodies = readPublicKeyList(data, offset); + offset += 4 + (custodies.size() * 32); // 4 bytes for vector length + 32 bytes per PublicKey + + long aumUsd = readUint128(data, offset); + offset += 16; + + Limit limit = readLimit(data, offset); + offset += 40; // 16 + 16 + 8 bytes each for maxAumUsd, tokenWeightageBufferBps, maxPositionUsd + + Fees fees = readFees(data, offset); + offset += 72; // 8 bytes each for 9 fee fields + + PoolApr poolApr = readPoolApr(data, offset); + offset += 24; + + long maxRequestExecutionSec = readInt64(data, offset); + offset += 8; + + byte bump = data[offset++]; + byte lpTokenBump = data[offset++]; + + long inceptionTime = readInt64(data, offset); + + return JupiterPool.builder() + .name(name) + .custodies(custodies) + .aumUsd(aumUsd) + .limit(limit) + .fees(fees) + .poolApr(poolApr) + .maxRequestExecutionSec(maxRequestExecutionSec) + .bump(bump) + .lpTokenBump(lpTokenBump) + .inceptionTime(inceptionTime) + .build(); + } + + private static String readString(byte[] data, int offset) { + int length = readInt32(data, offset); + offset += 4; + byte[] stringBytes = Arrays.copyOfRange(data, offset, offset + length); + return new String(stringBytes, StandardCharsets.UTF_8); + } + + private static List readPublicKeyList(byte[] data, int offset) { + List custodies = new ArrayList<>(); + int vectorLength = readInt32(data, offset); + offset += 4; // Move past the vector length + for (int i = 0; i < vectorLength; i++) { + PublicKey custody = PublicKey.readPubkey(data, offset); + custodies.add(custody); + offset += 32; // Move to the next PublicKey + } + return custodies; + } + + private static long readUint128(byte[] data, int offset) { + return OpenBookUtil.readUint128(data, offset).longValue(); + } + + private static Limit readLimit(byte[] data, int offset) { + return Limit.builder() + .maxAumUsd(readUint128(data, offset)) + .tokenWeightageBufferBps(readUint128(data, offset + 16)) + .maxPositionUsd(readUint64(data, offset + 32)) + .build(); + } + + private static Fees readFees(byte[] data, int offset) { + return Fees.builder() + .increasePositionBps(readUint64(data, offset)) + .decreasePositionBps(readUint64(data, offset + 8)) + .addRemoveLiquidityBps(readUint64(data, offset + 16)) + .swapBps(readUint64(data, offset + 24)) + .taxBps(readUint64(data, offset + 32)) + .stableSwapBps(readUint64(data, offset + 40)) + .stableSwapTaxBps(readUint64(data, offset + 48)) + .liquidationRewardBps(readUint64(data, offset + 56)) + .protocolShareBps(readUint64(data, offset + 64)) + .build(); + } + + private static PoolApr readPoolApr(byte[] data, int offset) { + return PoolApr.builder() + .lastUpdated(readInt64(data, offset)) + .feeAprBps(readInt64(data, offset + 8)) + .realizedFeeUsd(readInt64(data, offset + 16)) + .build(); + } + + private static long readInt64(byte[] data, int offset) { + return org.bitcoinj.core.Utils.readInt64(data, offset); + } + + private static long readUint64(byte[] data, int offset) { + return Long.parseUnsignedLong(Long.toUnsignedString(org.bitcoinj.core.Utils.readInt64(data, offset))); + } +} \ No newline at end of file diff --git a/jupiter/src/main/java/com/mmorrell/jupiter/model/JupiterPositionRequest.java b/jupiter/src/main/java/com/mmorrell/jupiter/model/JupiterPositionRequest.java new file mode 100644 index 0000000..5fafee3 --- /dev/null +++ b/jupiter/src/main/java/com/mmorrell/jupiter/model/JupiterPositionRequest.java @@ -0,0 +1,209 @@ +package com.mmorrell.jupiter.model; + +import com.mmorrell.jupiter.util.JupiterUtil; +import lombok.Builder; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.p2p.solanaj.core.PublicKey; + +import java.util.Arrays; + +/** + * Represents a Jupiter PositionRequest account in Jupiter Perpetuals. + */ +@Data +@Builder +@Slf4j +public class JupiterPositionRequest { + private PublicKey owner; + private PublicKey pool; + private PublicKey custody; + private PublicKey position; + private PublicKey mint; + private long openTime; + private long updateTime; + private long sizeUsdDelta; + private long collateralDelta; + private RequestChange requestChange; + private RequestType requestType; + private Side side; + private Long priceSlippage; + private Long jupiterMinimumOut; + private Long preSwapAmount; + private Long triggerPrice; + private Boolean triggerAboveThreshold; + private Boolean entirePosition; + private boolean executed; + private long counter; + private byte bump; + private PublicKey referral; + + /** + * Enum representing the type of request change. + */ + public enum RequestChange { + None, + Increase, + Decrease + } + + /** + * Enum representing the type of request. + */ + public enum RequestType { + Market, + Trigger + } + + /** + * Enum representing the side of the request. + */ + public enum Side { + None, + Long, + Short + } + + /** + * Deserializes a byte array into a JupiterPositionRequest object. + * + * @param data the byte array representing the account data. + * @return a JupiterPositionRequest object. + */ + public static JupiterPositionRequest fromByteArray(byte[] data) { + int offset = 8; // Skip discriminator + + PublicKey owner = PublicKey.readPubkey(data, offset); + offset += 32; + + PublicKey pool = PublicKey.readPubkey(data, offset); + offset += 32; + + PublicKey custody = PublicKey.readPubkey(data, offset); + offset += 32; + + PublicKey position = PublicKey.readPubkey(data, offset); + offset += 32; + + PublicKey mint = PublicKey.readPubkey(data, offset); + offset += 32; + + long openTime = JupiterUtil.readInt64(data, offset); + offset += 8; + + long updateTime = JupiterUtil.readInt64(data, offset); + offset += 8; + + long sizeUsdDelta = JupiterUtil.readUint64(data, offset); + offset += 8; + + long collateralDelta = JupiterUtil.readUint64(data, offset); + offset += 8; + + RequestChange requestChange = RequestChange.values()[data[offset++]]; + RequestType requestType = RequestType.values()[data[offset++]]; + Side side = Side.values()[data[offset++]]; + + // Read priceSlippage (Optional u64) + boolean hasPriceSlippage = data[offset] != 0; + Long priceSlippage = null; + if (hasPriceSlippage) { + priceSlippage = JupiterUtil.readUint64(data, offset + 1); + offset += 9; // 1 byte for option + 8 bytes for uint64 + } else { + offset += 1; // Only 1 byte for option + } + + // Read jupiterMinimumOut (Optional u64) + boolean hasJupiterMinimumOut = data[offset] != 0; + Long jupiterMinimumOut = null; + if (hasJupiterMinimumOut) { + jupiterMinimumOut = JupiterUtil.readUint64(data, offset + 1); + offset += 9; + } else { + offset += 1; + } + + // Read preSwapAmount (Optional u64) + boolean hasPreSwapAmount = data[offset] != 0; + Long preSwapAmount = null; + if (hasPreSwapAmount) { + preSwapAmount = JupiterUtil.readUint64(data, offset + 1); + offset += 9; + } else { + offset += 1; + } + + // Read triggerPrice (Optional u64) + boolean hasTriggerPrice = data[offset] != 0; + Long triggerPrice = null; + if (hasTriggerPrice) { + triggerPrice = JupiterUtil.readUint64(data, offset + 1); + offset += 9; + } else { + offset += 1; + } + + // Read triggerAboveThreshold (Optional boolean) + boolean hasTriggerAboveThreshold = data[offset] != 0; + Boolean triggerAboveThreshold = null; + if (hasTriggerAboveThreshold) { + triggerAboveThreshold = data[offset + 1] != 0; + offset += 2; // 1 byte for option + 1 byte for boolean + } else { + offset += 1; + } + + // Read entirePosition (Optional boolean) + boolean hasEntirePosition = data[offset] != 0; + Boolean entirePosition = null; + if (hasEntirePosition) { + entirePosition = data[offset + 1] != 0; + offset += 2; + } else { + offset += 1; + } + + boolean executed = data[offset++] != 0; + + long counter = JupiterUtil.readUint64(data, offset); + offset += 8; + + byte bump = data[offset++]; + + // Read referral (Optional PublicKey) + boolean hasReferral = data[offset] != 0; + PublicKey referral = null; + if (hasReferral) { + referral = PublicKey.readPubkey(data, offset + 1); + offset += 33; // 1 byte for option + 32 bytes for PublicKey + } else { + offset += 1; + } + + return JupiterPositionRequest.builder() + .owner(owner) + .pool(pool) + .custody(custody) + .position(position) + .mint(mint) + .openTime(openTime) + .updateTime(updateTime) + .sizeUsdDelta(sizeUsdDelta) + .collateralDelta(collateralDelta) + .requestChange(requestChange) + .requestType(requestType) + .side(side) + .priceSlippage(priceSlippage) + .jupiterMinimumOut(jupiterMinimumOut) + .preSwapAmount(preSwapAmount) + .triggerPrice(triggerPrice) + .triggerAboveThreshold(triggerAboveThreshold) + .entirePosition(entirePosition) + .executed(executed) + .counter(counter) + .bump(bump) + .referral(referral) + .build(); + } +} \ No newline at end of file diff --git a/jupiter/src/main/java/com/mmorrell/jupiter/model/JupiterTestOracle.java b/jupiter/src/main/java/com/mmorrell/jupiter/model/JupiterTestOracle.java new file mode 100644 index 0000000..434ca9a --- /dev/null +++ b/jupiter/src/main/java/com/mmorrell/jupiter/model/JupiterTestOracle.java @@ -0,0 +1,46 @@ +package com.mmorrell.jupiter.model; + +import com.mmorrell.jupiter.util.JupiterUtil; +import com.mmorrell.openbook.OpenBookUtil; +import lombok.Builder; +import lombok.Data; + +/** + * Represents a Jupiter TestOracle account in Jupiter Perpetuals. + */ +@Data +@Builder +public class JupiterTestOracle { + private long price; + private int expo; + private long conf; + private long publishTime; + + /** + * Deserializes a byte array into a JupiterTestOracle object. + * + * @param data the byte array representing the account data. + * @return a JupiterTestOracle object. + */ + public static JupiterTestOracle fromByteArray(byte[] data) { + int offset = 8; // Skip discriminator + + long price = JupiterUtil.readInt64(data, offset); + offset += 8; + + int expo = OpenBookUtil.readInt32(data, offset); + offset += 4; + + long conf = JupiterUtil.readUint64(data, offset); + offset += 8; + + long publishTime = JupiterUtil.readInt64(data, offset); + + return JupiterTestOracle.builder() + .price(price) + .expo(expo) + .conf(conf) + .publishTime(publishTime) + .build(); + } +} \ No newline at end of file diff --git a/jupiter/src/main/java/com/mmorrell/jupiter/util/JupiterUtil.java b/jupiter/src/main/java/com/mmorrell/jupiter/util/JupiterUtil.java new file mode 100644 index 0000000..4417ba3 --- /dev/null +++ b/jupiter/src/main/java/com/mmorrell/jupiter/util/JupiterUtil.java @@ -0,0 +1,35 @@ +package com.mmorrell.jupiter.util; + +import org.p2p.solanaj.core.PublicKey; + +public class JupiterUtil { + public static int readUint32(byte[] data, int offset) { + return (data[offset] & 0xFF) | + ((data[offset + 1] & 0xFF) << 8) | + ((data[offset + 2] & 0xFF) << 16) | + ((data[offset + 3] & 0xFF) << 24); + } + + public static long readUint64(byte[] data, int offset) { + return Long.parseUnsignedLong(Long.toUnsignedString(org.bitcoinj.core.Utils.readInt64(data, offset))); + } + + public static long readInt64(byte[] data, int offset) { + return org.bitcoinj.core.Utils.readInt64(data, offset); + } + + public static Long readOptionalUint64(byte[] data, int offset) { + boolean hasValue = data[offset] != 0; + return hasValue ? readUint64(data, offset + 1) : null; + } + + public static Boolean readOptionalBoolean(byte[] data, int offset) { + boolean hasValue = data[offset] != 0; + return hasValue ? data[offset + 1] != 0 : null; + } + + public static PublicKey readOptionalPublicKey(byte[] data, int offset) { + boolean hasValue = data[offset] != 0; + return hasValue ? PublicKey.readPubkey(data, offset + 1) : null; + } +} \ No newline at end of file diff --git a/jupiter/src/test/java/JupiterTest.java b/jupiter/src/test/java/JupiterTest.java new file mode 100644 index 0000000..55cdc8a --- /dev/null +++ b/jupiter/src/test/java/JupiterTest.java @@ -0,0 +1,324 @@ +import com.google.common.io.Files; +import com.mmorrell.jupiter.manager.JupiterManager; +import com.mmorrell.jupiter.model.*; +import lombok.extern.slf4j.Slf4j; +import org.bitcoinj.core.Base58; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.p2p.solanaj.core.PublicKey; +import org.p2p.solanaj.rpc.RpcClient; +import org.p2p.solanaj.rpc.RpcException; +import org.p2p.solanaj.rpc.types.AccountInfo; +import org.p2p.solanaj.rpc.types.Memcmp; +import org.p2p.solanaj.rpc.types.ProgramAccount; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.*; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Test class for Jupiter Perpetuals positions. + */ +@Slf4j +public class JupiterTest { + + private final RpcClient client = new RpcClient("https://mainnet.helius-rpc.com/?api-key=a778b653-bdd6-41bc-8cda-0c7377faf1dd"); + + @BeforeEach + public void setup() { + try { + Thread.sleep(1001L); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + @Test + public void testJupiterPerpPositionDeserialization() throws RpcException { + PublicKey positionPublicKey = new PublicKey("FdqbJAvADUJzZsBFK1ArhV79vXLmpKUMB4oXSrW8rSE"); + PublicKey positionPublicKeyOwner = new PublicKey("skynetDj29GH6o6bAqoixCpDuYtWqi1rm8ZNx1hB3vq"); + + // Fetch the account data + AccountInfo accountInfo = client.getApi().getAccountInfo(positionPublicKey); + + assertNotNull(accountInfo, "Account info should not be null"); + + byte[] data = Base64.getDecoder().decode(accountInfo.getValue().getData().get(0)); + + // Deserialize the data into JupiterPerpPosition + JupiterPerpPosition position = JupiterPerpPosition.fromByteArray(data); + + // Log the deserialized position + log.info("Deserialized JupiterPerpPosition: {}", position); + + // Assertions + assertNotNull(position); + assertEquals(positionPublicKeyOwner, position.getOwner()); + + // Add more specific assertions based on expected values + assertNotNull(position.getPool()); + assertNotNull(position.getCustody()); + assertNotNull(position.getCollateralCustody()); + assertTrue(position.getOpenTime() > 0); + assertTrue(position.getUpdateTime() > 0); + assertNotNull(position.getSide()); + assertTrue(position.getPrice() > 0); + assertTrue(position.getSizeUsd() > 0); + assertTrue(position.getCollateralUsd() > 0); + // Add more assertions as needed + } + + @Test + @Disabled + public void testGetAllJupiterPerpPositions() throws RpcException { + PublicKey programId = new PublicKey("PERPHjGBqRHArX4DySjwM6UJHiR3sWAatqfdBS2qQJu"); + + // Get the discriminator for the Position account + byte[] positionDiscriminator = getAccountDiscriminator("Position"); + + // Create a memcmp filter for the discriminator at offset 0 + Memcmp memcmpFilter = new Memcmp(0, Base58.encode(positionDiscriminator)); + + // Get all program accounts matching the filters + List positionAccounts = client.getApi().getProgramAccounts( + programId, + Collections.singletonList(memcmpFilter), + 216 + ); + + List positions = new ArrayList<>(); + for (ProgramAccount account : positionAccounts) { + // Decode the account data + byte[] data = account.getAccount().getDecodedData(); + + // Deserialize the data into JupiterPerpPosition + JupiterPerpPosition position = JupiterPerpPosition.fromByteArray(data); + + if (position.getSizeUsd() > 0) { + // Add to the list + positions.add(position); + } + } + + positions.sort(Comparator.comparingLong(JupiterPerpPosition::getSizeUsd)); + + // Log the positions + for (JupiterPerpPosition position : positions) { + double leverage = (double) position.getSizeUsd() / position.getCollateralUsd(); + log.info("Owner: {}, Size USD: {}, Leverage: {}", position.getOwner().toBase58(), position.getSizeUsd(), leverage); + } + } + + /** + * Calculates the account discriminator for a given account name. + * + * @param accountName the name of the account. + * @return the first 8 bytes of the SHA-256 hash of "account:". + */ + private byte[] getAccountDiscriminator(String accountName) { + String preimage = "account:" + accountName; + try { + MessageDigest hasher = MessageDigest.getInstance("SHA-256"); + hasher.update(preimage.getBytes(StandardCharsets.UTF_8)); + return Arrays.copyOfRange(hasher.digest(), 0, 8); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("SHA-256 algorithm not found for discriminator calculation."); + } + } + + @Test + public void testJupiterPoolDeserialization() throws RpcException { + PublicKey poolPublicKey = new PublicKey("5BUwFW4nRbftYTDMbgxykoFWqWHPzahFSNAaaaJtVKsq"); + + // Fetch the account data + AccountInfo accountInfo = client.getApi().getAccountInfo(poolPublicKey); + + assertNotNull(accountInfo, "Account info should not be null"); + + byte[] data = Base64.getDecoder().decode(accountInfo.getValue().getData().get(0)); + + // Deserialize the data into JupiterPool + JupiterPool pool = JupiterPool.fromByteArray(data); + + log.info("Deserialized JupiterPool: {}", pool); + + // Assertions + assertNotNull(pool); + assertEquals("Pool", pool.getName()); + assertEquals(5, pool.getCustodies().size()); + assertTrue(pool.getAumUsd() > 0); + assertNotNull(pool.getLimit()); + assertNotNull(pool.getFees()); + assertNotNull(pool.getPoolApr()); + assertEquals(45, pool.getMaxRequestExecutionSec()); + assertEquals((byte) 252, pool.getBump()); + assertEquals((byte) 254, pool.getLpTokenBump()); + assertEquals(1689677832, pool.getInceptionTime()); + } + + @Test + public void testJupiterCustodyDeserialization() throws RpcException { + PublicKey custodyPublicKey = new PublicKey("7xS2gz2bTp3fwCC7knJvUWTEU9Tycczu6VhJYKgi1wdz"); + + // Fetch the account data + AccountInfo accountInfo = client.getApi().getAccountInfo(custodyPublicKey); + + assertNotNull(accountInfo, "Account info should not be null"); + + byte[] data = Base64.getDecoder().decode(accountInfo.getValue().getData().get(0)); + + // Deserialize the data into JupiterCustody + JupiterCustody custody = JupiterCustody.fromByteArray(data); + + // Assertions + assertNotNull(custody); + assertNotNull(custody.getPool()); + assertNotNull(custody.getMint()); + assertNotNull(custody.getTokenAccount()); + + log.info("Deserialized JupiterCustody: {}", custody); + } + + @Test + public void testJupiterPerpetualsDeserialization() throws RpcException, IOException { + PublicKey perpetualsPublicKey = new PublicKey("H4ND9aYttUVLFmNypZqLjZ52FYiGvdEB45GmwNoKEjTj"); + + // Fetch the account data + AccountInfo accountInfo = client.getApi().getAccountInfo(perpetualsPublicKey); + + assertNotNull(accountInfo, "Account info should not be null"); + + byte[] data = Base64.getDecoder().decode(accountInfo.getValue().getData().get(0)); + + // Deserialize the data into JupiterPerpetuals + JupiterPerpetuals perpetuals = JupiterPerpetuals.fromByteArray(data); + + // Assertions + assertNotNull(perpetuals); + assertNotNull(perpetuals.getPermissions()); + assertNotNull(perpetuals.getPool()); + assertNotNull(perpetuals.getAdmin()); + + for (int i = 0; i < data.length; i++) { + try { + PublicKey pk = new PublicKey(Arrays.copyOfRange(data, i, i + 32)); + if (pk.toBase58().equalsIgnoreCase("5BUwFW4nRbftYTDMbgxykoFWqWHPzahFSNAaaaJtVKsq")) { + log.info("FOUND OFFSET 1 (POOL): {}", i); + } + if (pk.toBase58().equalsIgnoreCase("9hdBK7FUzv4NjZbtYfm39F5utJyFsmCwbF9Mow5Pr1sN")) { + log.info("FOUND OFFSET 2 (ADMIN): {}", i); + } + } catch (Exception ex) { + log.error(ex.getMessage()); + } + } + + log.info("Deserialized JupiterPerpetuals: {}", perpetuals); + + // Assuming one pool for now. The vector deserialization seemed off. + assertEquals("5BUwFW4nRbftYTDMbgxykoFWqWHPzahFSNAaaaJtVKsq", perpetuals.getPool().toBase58()); + + } + + @Test + public void testJupiterPositionRequestDeserialization() throws RpcException, IOException { + PublicKey positionRequestPublicKey = new PublicKey("APYrGNtsTTMpNNBQBpALxYnwYfKDQyFocf3d1j6jkuzf"); + + // Fetch the account data + AccountInfo accountInfo = client.getApi().getAccountInfo(positionRequestPublicKey); + + assertNotNull(accountInfo, "Account info should not be null"); + + byte[] data = Base64.getDecoder().decode(accountInfo.getValue().getData().get(0)); + Files.write(data, new File("jupiterPositionRequest.bin")); + + // Deserialize the data into JupiterPositionRequest + JupiterPositionRequest positionRequest = JupiterPositionRequest.fromByteArray(data); + + // Assertions + assertNotNull(positionRequest); + assertNotNull(positionRequest.getOwner()); + assertNotNull(positionRequest.getPool()); + assertNotNull(positionRequest.getCustody()); + assertTrue(positionRequest.getOpenTime() > 0); + assertTrue(positionRequest.getUpdateTime() > 0); + + log.info("Deserialized JupiterPositionRequest: {}", positionRequest); + } + + @Test + @Disabled + public void testJupiterTestOracleDeserialization() throws RpcException { + PublicKey testOraclePublicKey = new PublicKey("YourTestOraclePublicKeyHere"); + + // Fetch the account data + AccountInfo accountInfo = client.getApi().getAccountInfo(testOraclePublicKey); + + assertNotNull(accountInfo, "Account info should not be null"); + + byte[] data = Base64.getDecoder().decode(accountInfo.getValue().getData().get(0)); + + // Deserialize the data into JupiterTestOracle + JupiterTestOracle testOracle = JupiterTestOracle.fromByteArray(data); + + // Assertions + assertNotNull(testOracle); + assertTrue(testOracle.getPrice() != 0); + assertTrue(testOracle.getPublishTime() > 0); + // Add more assertions as needed + } + + @Test + public void testJupiterManager() { + JupiterManager manager = new JupiterManager(client); + + // Test getPosition + PublicKey positionPublicKey = new PublicKey("FdqbJAvADUJzZsBFK1ArhV79vXLmpKUMB4oXSrW8rSE"); + Optional position = manager.getPosition(positionPublicKey); + assertTrue(position.isPresent()); + assertEquals(new PublicKey("skynetDj29GH6o6bAqoixCpDuYtWqi1rm8ZNx1hB3vq"), position.get().getOwner()); + + // Test getPool + PublicKey poolPublicKey = new PublicKey("5BUwFW4nRbftYTDMbgxykoFWqWHPzahFSNAaaaJtVKsq"); + Optional pool = manager.getPool(poolPublicKey); + assertTrue(pool.isPresent()); + assertEquals("Pool", pool.get().getName()); + + // Test getCustody + PublicKey custodyPublicKey = new PublicKey("7xS2gz2bTp3fwCC7knJvUWTEU9Tycczu6VhJYKgi1wdz"); + Optional custody = manager.getCustody(custodyPublicKey); + assertTrue(custody.isPresent()); + assertNotNull(custody.get().getPool()); + + // Test getPositionRequest + PublicKey positionRequestPublicKey = new PublicKey("APYrGNtsTTMpNNBQBpALxYnwYfKDQyFocf3d1j6jkuzf"); + Optional positionRequest = manager.getPositionRequest(positionRequestPublicKey); + assertTrue(positionRequest.isPresent()); + assertNotNull(positionRequest.get().getOwner()); + + // Test getPerpetuals + PublicKey perpetualsPublicKey = new PublicKey("H4ND9aYttUVLFmNypZqLjZ52FYiGvdEB45GmwNoKEjTj"); + Optional perpetuals = manager.getPerpetuals(perpetualsPublicKey); + assertTrue(perpetuals.isPresent()); + assertNotNull(perpetuals.get().getPool()); + } + + @Test + public void testJupiterManagerWithInvalidPublicKeys() { + JupiterManager manager = new JupiterManager(client); + PublicKey invalidPublicKey = new PublicKey("1111111111111111111111111111111111111111111"); + + // Test all methods with invalid public key + assertFalse(manager.getPosition(invalidPublicKey).isPresent()); + assertFalse(manager.getPool(invalidPublicKey).isPresent()); + assertFalse(manager.getCustody(invalidPublicKey).isPresent()); + assertFalse(manager.getPositionRequest(invalidPublicKey).isPresent()); + assertFalse(manager.getPerpetuals(invalidPublicKey).isPresent()); + } +} diff --git a/magiceden/pom.xml b/magiceden/pom.xml index 29cc2c9..05a60b1 100644 --- a/magiceden/pom.xml +++ b/magiceden/pom.xml @@ -5,7 +5,7 @@ solanaj-programs com.mmorrell - 1.31.0 + 1.32.0-SNAPSHOT 4.0.0 diff --git a/mango/pom.xml b/mango/pom.xml index 43615fc..7ceff70 100644 --- a/mango/pom.xml +++ b/mango/pom.xml @@ -5,7 +5,7 @@ solanaj-programs com.mmorrell - 1.31.0 + 1.32.0-SNAPSHOT 4.0.0 diff --git a/metaplex/pom.xml b/metaplex/pom.xml index eb03e8f..d7cf57f 100644 --- a/metaplex/pom.xml +++ b/metaplex/pom.xml @@ -5,7 +5,7 @@ solanaj-programs com.mmorrell - 1.31.0 + 1.32.0-SNAPSHOT 4.0.0 diff --git a/openbook/pom.xml b/openbook/pom.xml index 91d5574..8aae976 100644 --- a/openbook/pom.xml +++ b/openbook/pom.xml @@ -5,7 +5,7 @@ solanaj-programs com.mmorrell - 1.31.0 + 1.32.0-SNAPSHOT 4.0.0 diff --git a/phoenix/pom.xml b/phoenix/pom.xml index 074d3b6..192ac16 100644 --- a/phoenix/pom.xml +++ b/phoenix/pom.xml @@ -6,7 +6,7 @@ com.mmorrell solanaj-programs - 1.31.0 + 1.32.0-SNAPSHOT phoenix @@ -20,13 +20,13 @@ com.mmorrell serum - 1.31.0 + 1.32.0-SNAPSHOT compile com.mmorrell metaplex - 1.31.0 + 1.32.0-SNAPSHOT test diff --git a/pom.xml b/pom.xml index 2a9ae6a..9febe09 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.mmorrell solanaj-programs pom - 1.31.0 + 1.32.0-SNAPSHOT ${project.groupId}:${project.artifactId} Program libraries for SolanaJ, a library for Solana RPC https://github.com/skynetcap/solanaj-programs @@ -37,6 +37,7 @@ openbook phoenix metaplex + jupiter diff --git a/pyth/pom.xml b/pyth/pom.xml index 4c1fe29..1c7cc3c 100644 --- a/pyth/pom.xml +++ b/pyth/pom.xml @@ -5,7 +5,7 @@ solanaj-programs com.mmorrell - 1.31.0 + 1.32.0-SNAPSHOT 4.0.0 diff --git a/serum/pom.xml b/serum/pom.xml index 01ee8be..4b23d8b 100644 --- a/serum/pom.xml +++ b/serum/pom.xml @@ -5,7 +5,7 @@ solanaj-programs com.mmorrell - 1.31.0 + 1.32.0-SNAPSHOT 4.0.0 diff --git a/zeta/pom.xml b/zeta/pom.xml index 121772a..cb5964a 100644 --- a/zeta/pom.xml +++ b/zeta/pom.xml @@ -5,7 +5,7 @@ solanaj-programs com.mmorrell - 1.31.0 + 1.32.0-SNAPSHOT 4.0.0