From cf73a03ff531d44a6aa52dea402a99332fbc4a22c2f58f359d19968d0596e128 Mon Sep 17 00:00:00 2001 From: Minimons Date: Thu, 2 Jul 2026 20:35:25 +0200 Subject: [PATCH] Mess --- build.gradle.kts | 1 - .../valuetypes/WellKnownCurrencyTypes.java | 52 ++++++ .../com/r35157/libs/codec/Base58Codec.tjava | 5 + .../jupiter/perps/JupiterPerpsPosition.tjava | 47 +++++ .../perps/JupiterPerpsPositionDirection.tjava | 9 + .../jupiter/perps/JupiterPerpsService.tjava | 51 ++++++ .../AnchorIdlJupiterPerpsCustodyDecoder.tjava | 82 +++++---- ...AnchorIdlJupiterPerpsPositionDecoder.tjava | 70 ++++++-- .../AnchorIdlJupiterPerpsServiceImpl.tjava | 68 +++++-- .../impl/anchoridl/CustodyAccountInfo.tjava | 9 + .../impl/anchoridl/DecodingPrimitives.tjava | 31 ++++ .../anchoridl/JupiterPerpsPositionInfo.tjava | 12 ++ .../com/r35157/libs/raydium/Raydium.tjava | 7 +- .../raydium/RaydiumConcentratedPoolInfo.tjava | 5 +- .../RaydiumConcentratedPoolState.tjava | 5 +- .../RaydiumConcentratedPositionState.tjava | 1 + ...RaydiumLiquidityPoolPositionStandard.tjava | 5 +- .../raydium/RaydiumLiquidityPoolPrice.tjava | 12 +- .../RaydiumLiquidityPoolTokenAmounts.tjava | 5 +- .../r35157/libs/solana/SPLTokenSupply.tjava | 5 +- .../r35157/libs/solana/SolanaBlockChain.tjava | 167 ++++++++++++++++++ .../SolanaProgramAccountMemcmpFilter.tjava | 17 ++ .../libs/valuetypes/basic/MoneyPrice.tjava | 8 + .../r35157/nenjim/hubd/impl/ref/Main.tjava | 36 ++-- 24 files changed, 618 insertions(+), 92 deletions(-) create mode 100644 src/main/java/com/r35157/libs/solana/valuetypes/WellKnownCurrencyTypes.java create mode 100644 src/main/tjava/com/r35157/libs/codec/Base58Codec.tjava create mode 100644 src/main/tjava/com/r35157/libs/jupiter/perps/JupiterPerpsPosition.tjava create mode 100644 src/main/tjava/com/r35157/libs/jupiter/perps/JupiterPerpsPositionDirection.tjava create mode 100644 src/main/tjava/com/r35157/libs/jupiter/perps/JupiterPerpsService.tjava create mode 100644 src/main/tjava/com/r35157/libs/jupiter/perps/impl/anchoridl/CustodyAccountInfo.tjava create mode 100644 src/main/tjava/com/r35157/libs/jupiter/perps/impl/anchoridl/DecodingPrimitives.tjava create mode 100644 src/main/tjava/com/r35157/libs/jupiter/perps/impl/anchoridl/JupiterPerpsPositionInfo.tjava create mode 100644 src/main/tjava/com/r35157/libs/solana/SolanaBlockChain.tjava create mode 100644 src/main/tjava/com/r35157/libs/solana/SolanaProgramAccountMemcmpFilter.tjava create mode 100644 src/main/tjava/com/r35157/libs/valuetypes/basic/MoneyPrice.tjava diff --git a/build.gradle.kts b/build.gradle.kts index c0b0f7c..ade0647 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -44,7 +44,6 @@ dependencies { runtimeOnly("org.apache.logging.log4j:log4j-core:2.26.0") runtimeOnly("org.apache.logging.log4j:log4j-slf4j2-impl:2.26.0") - implementation("com.r35157.nenjim:hubd-api:0.1-dev") implementation("com.fasterxml.jackson.core:jackson-databind:2.18.6") implementation("com.fazecast:jSerialComm:2.11.4") implementation("com.google.code.gson:gson:2.14.0") diff --git a/src/main/java/com/r35157/libs/solana/valuetypes/WellKnownCurrencyTypes.java b/src/main/java/com/r35157/libs/solana/valuetypes/WellKnownCurrencyTypes.java new file mode 100644 index 0000000..7c88278 --- /dev/null +++ b/src/main/java/com/r35157/libs/solana/valuetypes/WellKnownCurrencyTypes.java @@ -0,0 +1,52 @@ +package com.r35157.libs.solana.valuetypes; + +import com.r35157.libs.valuetypes.basic.CurrencyType; + +import java.util.UUID; + +/** + * Defines well-known currency types used by the Solana integration. + * + *

Each enum value wraps a {@link CurrencyType} with a stable identifier and a + * human-readable currency name. These predefined values are intended for common + * currencies that the Solana-related modules need to reference consistently.

+ */ +public enum WellKnownCurrencyTypes { + /** + * Native Solana currency. + */ + SOLANA(new CurrencyType( + UUID.fromString("019e0116-fce5-792f-a647-fa6da4dffec5"), + "Solana", + "SOL") + ), + + /** + * Syrup USDC token currency. + */ + SYRUPUSDC(new CurrencyType( + UUID.fromString("019e1d51-0600-7956-8231-f3b7058a91c2"), + "SyrupUSDC", + "SyrupUSDC") + ); + + /** + * Creates a well-known currency type entry. + * + * @param currencyType the currency type represented by this enum value + */ + WellKnownCurrencyTypes(CurrencyType currencyType) { + this.currencyType = currencyType; + } + + /** + * Returns the currency type represented by this enum value. + * + * @return the represented currency type + */ + public CurrencyType getCurrencyType() { + return currencyType; + } + + private final CurrencyType currencyType; +} \ No newline at end of file diff --git a/src/main/tjava/com/r35157/libs/codec/Base58Codec.tjava b/src/main/tjava/com/r35157/libs/codec/Base58Codec.tjava new file mode 100644 index 0000000..e3bc6ab --- /dev/null +++ b/src/main/tjava/com/r35157/libs/codec/Base58Codec.tjava @@ -0,0 +1,5 @@ +package com.r35157.libs.codec; + +public interface Base58Codec { + String encode(byte[] input); +} 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..548ab0c --- /dev/null +++ b/src/main/tjava/com/r35157/libs/jupiter/perps/JupiterPerpsPosition.tjava @@ -0,0 +1,47 @@ +package com.r35157.libs.jupiter.perps; + +import java.math.BigDecimal; + +import com.r35157.libs.valuetypes.basic.MoneyAmount; + +/** + * 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.

+ * + * @param positionAccount the Solana account address of the Jupiter Perps position + * @param tradedTokenMint the mint address of the token being traded + * @param direction whether the position is long or short + * @param value the amount the position is worth if closed now + * @param size the leveraged amount used to open the contracts + * @param pnl the amount in usd in profit or loss on this position + * @param pnlPercent the profit and loss represented as a percentage + * @param leverage + * @param entryPrice the entry price of the position, denominated in USDC + * @param marketPrice the current spot price of the token + * @param collateral the amount of USD representing the collateral for this position + * @param totalFees the total amount of fees (TODO: is that including pending/due fees) + * @param borrowFeesDue the amount of USD that is currently outstanding + * @param closeFeePending the fee in USD for closing the account (TODO: multiple accounts - when adding collateral?) + * @param accountRent refundable amount locked for Solana account renting + */ +public record JupiterPerpsPosition( + ΩJupiterPerpsPositionAccountΩ positionAccount, + ΩSPLMintAddressΩ tradedTokenMint, + JupiterPerpsPositionDirection direction, + ΩUSDCAmountΩ value, + ΩUSDCAmountΩ size, + ΩUSDCAmountΩ pnl, + BigDecimal pnlPercent, + BigDecimal leverage, + ΩUSDCPriceΩ entryPrice, + ΩUSDCPriceΩ marketPrice, + ΩUSDCAmountΩ collateral, + ΩUSDCAmountΩ totalFees, + ΩUSDCAmountΩ borrowFeesDue, + ΩUSDCAmountΩ closeFeePending, + ΩSolanaAmountΩ accountRent +) { +} \ No newline at end of file diff --git a/src/main/tjava/com/r35157/libs/jupiter/perps/JupiterPerpsPositionDirection.tjava b/src/main/tjava/com/r35157/libs/jupiter/perps/JupiterPerpsPositionDirection.tjava new file mode 100644 index 0000000..05c4066 --- /dev/null +++ b/src/main/tjava/com/r35157/libs/jupiter/perps/JupiterPerpsPositionDirection.tjava @@ -0,0 +1,9 @@ +package com.r35157.libs.jupiter.perps; + +/** + * Direction of a Jupiter Perps position. + */ +public enum JupiterPerpsPositionDirection { + LONG, + SHORT +} \ No newline at end of file diff --git a/src/main/tjava/com/r35157/libs/jupiter/perps/JupiterPerpsService.tjava b/src/main/tjava/com/r35157/libs/jupiter/perps/JupiterPerpsService.tjava new file mode 100644 index 0000000..45ac671 --- /dev/null +++ b/src/main/tjava/com/r35157/libs/jupiter/perps/JupiterPerpsService.tjava @@ -0,0 +1,51 @@ +package com.r35157.libs.jupiter.perps; + +import com.r35157.libs.solana.SolanaAccountInfo; +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; +import java.util.Set; + +/** + * Service for reading Jupiter Perps data. + * + *

This service is read-only. It does not open, close, modify, or sign transactions + * for Jupiter Perpetual Contracts.

+ * + *

NOTICE: The first supported operation is reading a known Jupiter Perps position account + * and returning its decoded position data.

+ */ +public interface JupiterPerpsService { + /** + * 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(@NotNull SolanaAccountInfo accountInfo) + throws IOException, InterruptedException + { + JupiterPerpsPosition getPosition(ΩJupiterPerpsPositionAccountΩ positionAccount) + throws IOException, InterruptedException; + */ + + /** + * Finds open Jupiter Perps positions owned by a wallet. + * + *

This method returns decoded Jupiter Perps position objects. It does not return + * raw Solana accounts or account ids.

+ * + * @param owner the wallet address that owns the Jupiter Perps positions + * @return the open Jupiter Perps positions owned by the wallet + * @throws IOException if the position accounts could not be fetched or decoded + * @throws InterruptedException if the calling thread is interrupted while fetching positions + */ + Set getOpenPositions(ΩSolanaWalletIdΩ owner) + throws IOException, InterruptedException; +} \ No newline at end of file diff --git a/src/main/tjava/com/r35157/libs/jupiter/perps/impl/anchoridl/AnchorIdlJupiterPerpsCustodyDecoder.tjava b/src/main/tjava/com/r35157/libs/jupiter/perps/impl/anchoridl/AnchorIdlJupiterPerpsCustodyDecoder.tjava index 04fcda1..facd83e 100644 --- a/src/main/tjava/com/r35157/libs/jupiter/perps/impl/anchoridl/AnchorIdlJupiterPerpsCustodyDecoder.tjava +++ b/src/main/tjava/com/r35157/libs/jupiter/perps/impl/anchoridl/AnchorIdlJupiterPerpsCustodyDecoder.tjava @@ -4,47 +4,67 @@ import com.r35157.libs.codec.Base58Codec; import com.r35157.libs.codec.impl.ref.Base58CodecImpl; import com.r35157.libs.solana.SolanaAccountInfo; +import java.math.BigInteger; import java.util.Base64; -class AnchorIdlJupiterPerpsCustodyDecoder { +import static com.r35157.libs.jupiter.perps.impl.anchoridl.DecodingPrimitives.*; - ΩSPLMintAddressΩ decodeMint( - SolanaAccountInfo custodyAccountInfo - ) { - byte[] data = Base64.getDecoder().decode(custodyAccountInfo.dataBase64()); +public class AnchorIdlJupiterPerpsCustodyDecoder { - if (data.length < MINT_OFFSET + PUBLIC_KEY_LENGTH) { - throw new IllegalArgumentException( - "Jupiter Perps custody account data is too short: " + data.length - ); - } + public CustodyAccountInfo decode(SolanaAccountInfo custodyAccountInfo) { + byte[] data = decodeIdl(custodyAccountInfo); - return readPublicKey(data, MINT_OFFSET); - } - - private ΩSPLMintAddressΩ readPublicKey( - byte[] data, - int offset - ) { - byte[] publicKeyBytes = new byte[PUBLIC_KEY_LENGTH]; - - System.arraycopy( - data, - offset, - publicKeyBytes, - 0, - PUBLIC_KEY_LENGTH + CustodyAccountInfo cai = new CustodyAccountInfo( + decodeMintAddress(data), + decodeCumulativeInterestRate(data) ); - return base58.encode(publicKeyBytes); + return cai; } - private static final int ANCHOR_DISCRIMINATOR_LENGTH = 8; - private static final int PUBLIC_KEY_LENGTH = 32; + private byte[] decodeIdl(SolanaAccountInfo custodyAccountInfo) { + byte[] data = base64.decode(custodyAccountInfo.dataBase64()); - private static final int MINT_OFFSET = - ANCHOR_DISCRIMINATOR_LENGTH - + PUBLIC_KEY_LENGTH; // pool + if (data.length < CUMULATIVE_INTEREST_RATE_OFFSET + U128_LENGTH) { + String errMsg = "Jupiter Perps custody account data is too short: " + data.length; + throw new IllegalArgumentException(errMsg); + } + + return data; + } + + private ΩSPLMintAddressΩ decodeMintAddress(byte[] data) { + byte[] mintAddressBytes = readPublicKey(data, MINT_OFFSET); + ΩSPLMintAddressΩ mintAddress = base58.encode(mintAddressBytes); + + return mintAddress; + } + + private BigInteger decodeCumulativeInterestRate(byte[] data) { + byte[] bytes = readU128(data, CUMULATIVE_INTEREST_RATE_OFFSET); + BigInteger cumulativeInterestRate = new BigInteger(1, bytes); + + return cumulativeInterestRate; + } + + // Offsets: + // 8 discriminator + // +32 pool + // +32 mint + // +32 token_account + // +1 decimals + // +1 is_stable + // +45 oracle + // +48 pricing + // +7 permissions + // +8 target_ratio_bps + // +48 assets + + private static final int ANCHOR_DISCRIMINATOR_LENGTH = 8; + private static final int MINT_OFFSET = ANCHOR_DISCRIMINATOR_LENGTH + PUBLIC_KEY_LENGTH; + private static final int FUNDING_RATE_STATE_OFFSET = 262; + private static final int CUMULATIVE_INTEREST_RATE_OFFSET = FUNDING_RATE_STATE_OFFSET; private static final Base58Codec base58 = new Base58CodecImpl(); + private static final Base64.Decoder base64 = Base64.getDecoder(); } \ 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 index b4a52b5..03ae283 100644 --- 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 @@ -7,17 +7,15 @@ import com.r35157.libs.jupiter.perps.JupiterPerpsPositionDirection; import com.r35157.libs.solana.SolanaAccountInfo; import java.math.BigDecimal; +import java.math.BigInteger; +import java.math.RoundingMode; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.Base64; -class AnchorIdlJupiterPerpsPositionDecoder { +public class AnchorIdlJupiterPerpsPositionDecoder { - JupiterPerpsPosition decode( - ΩJupiterPerpsPositionAccountΩ positionAccount, - SolanaAccountInfo accountInfo, - ΩSPLMintAddressΩ tradedTokenMint - ) { + public JupiterPerpsPositionInfo decode(SolanaAccountInfo accountInfo) { byte[] data = Base64.getDecoder().decode(accountInfo.dataBase64()); if (data.length < PRICE_OFFSET + U64_LENGTH) { @@ -56,19 +54,34 @@ class AnchorIdlJupiterPerpsPositionDecoder { .valueOf(rawSizeUsd) .movePointLeft(6); - JupiterPerpsPosition pos = new JupiterPerpsPosition( - positionAccount, + JupiterPerpsPositionInfo posInfo = new JupiterPerpsPositionInfo( entryPrice, direction, - tradedTokenMint, sizeUsd, collateralUsd ); - return pos; + return posInfo; } - ΩSolanaAddressΩ decodeCustodyAccount(SolanaAccountInfo accountInfo) { + + public BigInteger decodeCumulativeInterestSnapshot(SolanaAccountInfo accountInfo) { + byte[] data = Base64.getDecoder().decode(accountInfo.dataBase64()); + + if (data.length < CUMULATIVE_INTEREST_SNAPSHOT_OFFSET + U128_LENGTH) { + throw new IllegalArgumentException( + "Jupiter Perps position account data is too short: " + data.length + ); + } + + BigInteger value = readU128( + data, + CUMULATIVE_INTEREST_SNAPSHOT_OFFSET + ); + return value; + } + + public ΩSolanaAddressΩ decodeCustodyAccount(SolanaAccountInfo accountInfo) { byte[] data = Base64.getDecoder().decode(accountInfo.dataBase64()); if (data.length < CUSTODY_OFFSET + PUBLIC_KEY_LENGTH) { @@ -83,6 +96,23 @@ class AnchorIdlJupiterPerpsPositionDecoder { ); } + public ΩSolanaAddressΩ decodeCollateralCustodyAccount( + SolanaAccountInfo accountInfo + ) { + byte[] data = Base64.getDecoder().decode(accountInfo.dataBase64()); + + if (data.length < COLLATERAL_CUSTODY_OFFSET + PUBLIC_KEY_LENGTH) { + throw new IllegalArgumentException( + "Jupiter Perps position account data is too short: " + data.length + ); + } + + return readPublicKey( + data, + COLLATERAL_CUSTODY_OFFSET + ); + } + private JupiterPerpsPositionDirection decodeDirection( byte rawSide ) { @@ -115,11 +145,25 @@ class AnchorIdlJupiterPerpsPositionDecoder { return base58.encode(publicKeyBytes); } + private static BigInteger readU128( + byte[] data, + int offset + ) { + byte[] bytes = new byte[U128_LENGTH]; + + for (int i = 0; i < U128_LENGTH; i++) { + bytes[i] = data[offset + U128_LENGTH - 1 - i]; + } + + return new BigInteger(1, bytes); + } + 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 SIDE_ENUM_LENGTH = 1; + private static final int I64_LENGTH = 8; private static final int U64_LENGTH = 8; + private static final int U128_LENGTH = 16; private static final int OWNER_OFFSET = ANCHOR_DISCRIMINATOR_LENGTH; private static final int POOL_OFFSET = OWNER_OFFSET + PUBLIC_KEY_LENGTH; @@ -131,6 +175,8 @@ class AnchorIdlJupiterPerpsPositionDecoder { private static final int PRICE_OFFSET = SIDE_OFFSET + SIDE_ENUM_LENGTH; private static final int SIZE_USD_OFFSET = PRICE_OFFSET + U64_LENGTH; private static final int COLLATERAL_USD_OFFSET = SIZE_USD_OFFSET + U64_LENGTH; + private static final int REALISED_PNL_USD_OFFSET = COLLATERAL_USD_OFFSET + U64_LENGTH; + private static final int CUMULATIVE_INTEREST_SNAPSHOT_OFFSET = REALISED_PNL_USD_OFFSET + I64_LENGTH; private static final Base58Codec base58 = new Base58CodecImpl(); } \ No newline at end of file diff --git a/src/main/tjava/com/r35157/libs/jupiter/perps/impl/anchoridl/AnchorIdlJupiterPerpsServiceImpl.tjava b/src/main/tjava/com/r35157/libs/jupiter/perps/impl/anchoridl/AnchorIdlJupiterPerpsServiceImpl.tjava index f3349f3..a6eb56f 100644 --- a/src/main/tjava/com/r35157/libs/jupiter/perps/impl/anchoridl/AnchorIdlJupiterPerpsServiceImpl.tjava +++ b/src/main/tjava/com/r35157/libs/jupiter/perps/impl/anchoridl/AnchorIdlJupiterPerpsServiceImpl.tjava @@ -5,8 +5,12 @@ import com.r35157.libs.jupiter.perps.JupiterPerpsService; import com.r35157.libs.solana.SolanaAccountInfo; import com.r35157.libs.solana.SolanaBlockChain; import com.r35157.libs.solana.SolanaProgramAccountMemcmpFilter; +import org.jetbrains.annotations.NotNull; import java.io.IOException; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.math.RoundingMode; import java.util.HashSet; import java.util.Set; @@ -20,26 +24,47 @@ public class AnchorIdlJupiterPerpsServiceImpl implements JupiterPerpsService { this.custodyDecoder = new AnchorIdlJupiterPerpsCustodyDecoder(); } + /* @Override - public JupiterPerpsPosition getPosition(ΩJupiterPerpsPositionAccountΩ positionAccount) - throws IOException, InterruptedException { - SolanaAccountInfo accountInfo = solanaBlockChain.getAccountInfo(positionAccount); + public JupiterPerpsPosition getPosition(@NotNull SolanaAccountInfo accountInfo) + throws IOException, InterruptedException + { + guard(accountInfo); - if (accountInfo == null) { - throw new IllegalArgumentException("Jupiter Perps position account does not exist: " + positionAccount); - } + JupiterPerpsPositionInfo posInfo = positionDecoder.decode(accountInfo); + ΩUSDCAmountΩ sizeUsd = posInfo.sizeUsd(); - if (!JUPITER_PERPS_PROGRAM_ID.equals(accountInfo.owner())) { - throw new IllegalArgumentException( - "Account is not owned by Jupiter Perps program: " + positionAccount - ); - } + CustodyAccountInfo cai = custodyDecoder.decode(custodyAccountInfo); + BigInteger cumulativeInterestSnapshot = positionDecoder.decodeCumulativeInterestSnapshot(positionAccountInfo); + ΩUSDCAmountΩ borrowFeePendingUsd = calculateBorrowFeeUsd( + cumulativeInterestSnapshot, + sizeUsd + ); - ΩSPLMintAddressΩ tradedTokenMint = getTradedTokenMint(accountInfo); - JupiterPerpsPosition pos = positionDecoder.decode(positionAccount, accountInfo, tradedTokenMint); + JupiterPerpsPosition pos = new JupiterPerpsPosition( + accountInfo, + posInfo.entryPrice(), + posInfo.direction(), + sizeUsd, + posInfo.collateralUsd(), + cai.mintAddress(), + borrowFeePendingUsd + ); return pos; } + */ + + private void guard(SolanaAccountInfo accountInfo) { + ΩSolanaProgramIdΩ owner = accountInfo.owner(); + if (!JUPITER_PERPS_PROGRAM_ID.equals(owner)) { + String errorMsg = "Account '" + accountInfo.address() + "' is not owned by Jupiter Perps program (" + + JUPITER_PERPS_PROGRAM_ID + "), instead it is owned by '" + + owner + + "'"; + throw new IllegalArgumentException(errorMsg); + } + } @Override public Set getOpenPositions(ΩSolanaWalletIdΩ owner) @@ -73,7 +98,20 @@ public class AnchorIdlJupiterPerpsServiceImpl implements JupiterPerpsService { return Set.copyOf(positions); } - private ΩSPLMintAddressΩ getTradedTokenMint(SolanaAccountInfo positionAccountInfo) + private BigDecimal calculateBorrowFeeUsd( + BigInteger cumulativeInterestSnapshot, + ΩUSDCAmountΩ sizeUsd + ) { + BigInteger difference = currentCumulativeInterestRate.subtract(cumulativeInterestSnapshot); + BigDecimal borrowFeeUsd = + new BigDecimal(difference) + .multiply(sizeUsd) + .divide(BigDecimal.valueOf(1_000_000_000L), 6, RoundingMode.CEILING); + + return borrowFeeUsd; + } + + /*private ΩSPLMintAddressΩ getTradedTokenMint(SolanaAccountInfo positionAccountInfo) throws IOException, InterruptedException { ΩSolanaAddressΩ custodyAccount = positionDecoder.decodeCustodyAccount(positionAccountInfo); @@ -87,7 +125,7 @@ public class AnchorIdlJupiterPerpsServiceImpl implements JupiterPerpsService { ΩSPLMintAddressΩ mintAddress = custodyDecoder.decodeMint(custodyAccountInfo); return mintAddress; - } + }*/ private static final ΩJupiterPerpsProgramIdΩ JUPITER_PERPS_PROGRAM_ID = "PERPHjGBqRHArX4DySjwM6UJHiR3sWAatqfdBS2qQJu"; diff --git a/src/main/tjava/com/r35157/libs/jupiter/perps/impl/anchoridl/CustodyAccountInfo.tjava b/src/main/tjava/com/r35157/libs/jupiter/perps/impl/anchoridl/CustodyAccountInfo.tjava new file mode 100644 index 0000000..a7913f5 --- /dev/null +++ b/src/main/tjava/com/r35157/libs/jupiter/perps/impl/anchoridl/CustodyAccountInfo.tjava @@ -0,0 +1,9 @@ +package com.r35157.libs.jupiter.perps.impl.anchoridl; + +import java.math.BigInteger; + +public record CustodyAccountInfo( + ΩSPLMintAddressΩ mintAddress, + BigInteger currentCumulativeInterestRate +) { +} diff --git a/src/main/tjava/com/r35157/libs/jupiter/perps/impl/anchoridl/DecodingPrimitives.tjava b/src/main/tjava/com/r35157/libs/jupiter/perps/impl/anchoridl/DecodingPrimitives.tjava new file mode 100644 index 0000000..ee72dc7 --- /dev/null +++ b/src/main/tjava/com/r35157/libs/jupiter/perps/impl/anchoridl/DecodingPrimitives.tjava @@ -0,0 +1,31 @@ +package com.r35157.libs.jupiter.perps.impl.anchoridl; + +public class DecodingPrimitives { + + public static byte[] readU128(byte[] data, int offset) { + byte[] bytes = new byte[U128_LENGTH]; + + for (int i = 0; i < U128_LENGTH; i++) { + bytes[i] = data[offset + U128_LENGTH - 1 - i]; + } + + return bytes; + } + + public static byte[] readPublicKey(byte[] data, int offset) { + byte[] publicKeyBytes = new byte[PUBLIC_KEY_LENGTH]; + + System.arraycopy( + data, + offset, + publicKeyBytes, + 0, + PUBLIC_KEY_LENGTH + ); + + return publicKeyBytes; + } + + public static final int U128_LENGTH = 16; + public static final int PUBLIC_KEY_LENGTH = 32; +} diff --git a/src/main/tjava/com/r35157/libs/jupiter/perps/impl/anchoridl/JupiterPerpsPositionInfo.tjava b/src/main/tjava/com/r35157/libs/jupiter/perps/impl/anchoridl/JupiterPerpsPositionInfo.tjava new file mode 100644 index 0000000..07c05a7 --- /dev/null +++ b/src/main/tjava/com/r35157/libs/jupiter/perps/impl/anchoridl/JupiterPerpsPositionInfo.tjava @@ -0,0 +1,12 @@ +package com.r35157.libs.jupiter.perps.impl.anchoridl; + +import com.r35157.libs.jupiter.perps.JupiterPerpsPositionDirection; + +import java.math.BigDecimal; + +public record JupiterPerpsPositionInfo( + ΩUSDCPriceΩ entryPrice, + JupiterPerpsPositionDirection direction, + ΩUSDCAmountΩ sizeUsd, + ΩUSDCAmountΩ collateralUsd +) {} diff --git a/src/main/tjava/com/r35157/libs/raydium/Raydium.tjava b/src/main/tjava/com/r35157/libs/raydium/Raydium.tjava index 9e56d6d..11213da 100644 --- a/src/main/tjava/com/r35157/libs/raydium/Raydium.tjava +++ b/src/main/tjava/com/r35157/libs/raydium/Raydium.tjava @@ -1,6 +1,5 @@ package com.r35157.libs.raydium; -import com.r35157.libs.valuetypes.basic.AssetPrice; import com.r35157.libs.valuetypes.basic.MoneyAmount; import com.r35157.libs.valuetypes.basic.Range; @@ -16,11 +15,11 @@ public interface Raydium { * to any Raydium liquidity pool supported by the implementation.

* * @param poolId the Raydium liquidity pool id - * @return the current pool price + * @return the current pool price expressed as a Solana amount * @throws IOException if the price could not be fetched or the response could not be parsed * @throws InterruptedException if the calling thread is interrupted while fetching the price */ - AssetPrice fetchPoolPrice(ΩRaydiumLiquidityPoolIdΩ poolId) throws IOException, InterruptedException; + ΩSolanaAmountΩ fetchPoolPrice(ΩRaydiumLiquidityPoolIdΩ poolId) throws IOException, InterruptedException; /** * Fetches the Raydium liquidity pool ids where the supplied Solana owner address has positions. @@ -184,7 +183,7 @@ public interface Raydium { * @param decimalPlaces the number decimal places in the resulting price currency * @return the price range represented by the concentrated liquidity position */ - Range calculateConcentratedPositionPriceRange( + Range<ΩPriceΩ> calculateConcentratedPositionPriceRange( RaydiumConcentratedPositionState positionState, RaydiumConcentratedPoolInfo poolInfo, int decimalPlaces diff --git a/src/main/tjava/com/r35157/libs/raydium/RaydiumConcentratedPoolInfo.tjava b/src/main/tjava/com/r35157/libs/raydium/RaydiumConcentratedPoolInfo.tjava index 4f8683f..269b440 100644 --- a/src/main/tjava/com/r35157/libs/raydium/RaydiumConcentratedPoolInfo.tjava +++ b/src/main/tjava/com/r35157/libs/raydium/RaydiumConcentratedPoolInfo.tjava @@ -1,7 +1,5 @@ package com.r35157.libs.raydium; -import java.math.BigDecimal; - /** * Represents basic information about a Raydium concentrated liquidity pool. * @@ -19,6 +17,9 @@ import java.math.BigDecimal; * @param mintBDecimals the number of decimals used by token B * @param priceEstimate the pool price estimate as reported by Raydium */ + +import java.math.BigDecimal; + public record RaydiumConcentratedPoolInfo( ΩRaydiumLiquidityPoolConcentratedIdΩ poolId, ΩSPLMintAddressΩ mintA, diff --git a/src/main/tjava/com/r35157/libs/raydium/RaydiumConcentratedPoolState.tjava b/src/main/tjava/com/r35157/libs/raydium/RaydiumConcentratedPoolState.tjava index 97ebca3..e9de50f 100644 --- a/src/main/tjava/com/r35157/libs/raydium/RaydiumConcentratedPoolState.tjava +++ b/src/main/tjava/com/r35157/libs/raydium/RaydiumConcentratedPoolState.tjava @@ -1,7 +1,5 @@ package com.r35157.libs.raydium; -import java.math.BigInteger; - /** * Represents raw on-chain state for a Raydium concentrated liquidity pool. * @@ -23,6 +21,9 @@ import java.math.BigInteger; * @param sqrtPriceX64 the current square-root price in Q64.64 fixed-point format * @param tickCurrent the current Raydium liquidity tick index */ + +import java.math.BigInteger; + public record RaydiumConcentratedPoolState( ΩRaydiumLiquidityPoolConcentratedIdΩ poolId, ΩRaydiumLiquidityΩ liquidity, diff --git a/src/main/tjava/com/r35157/libs/raydium/RaydiumConcentratedPositionState.tjava b/src/main/tjava/com/r35157/libs/raydium/RaydiumConcentratedPositionState.tjava index ccc5496..b1f07aa 100644 --- a/src/main/tjava/com/r35157/libs/raydium/RaydiumConcentratedPositionState.tjava +++ b/src/main/tjava/com/r35157/libs/raydium/RaydiumConcentratedPositionState.tjava @@ -1,5 +1,6 @@ package com.r35157.libs.raydium; +import java.math.BigDecimal; import java.math.BigInteger; public record RaydiumConcentratedPositionState( diff --git a/src/main/tjava/com/r35157/libs/raydium/RaydiumLiquidityPoolPositionStandard.tjava b/src/main/tjava/com/r35157/libs/raydium/RaydiumLiquidityPoolPositionStandard.tjava index 2991e8f..6cfe215 100644 --- a/src/main/tjava/com/r35157/libs/raydium/RaydiumLiquidityPoolPositionStandard.tjava +++ b/src/main/tjava/com/r35157/libs/raydium/RaydiumLiquidityPoolPositionStandard.tjava @@ -1,7 +1,5 @@ package com.r35157.libs.raydium; -import java.math.BigDecimal; - /** * Represents a standard Raydium liquidity pool position. * @@ -19,6 +17,9 @@ import java.math.BigDecimal; * @param lpTokenAccount the SPL token account holding the liquidity pool tokens * @param lpTokenAmount the amount of liquidity pool tokens held in the SPL token account */ + +import java.math.BigDecimal; + public record RaydiumLiquidityPoolPositionStandard( ΩRaydiumLiquidityPoolPositionIdΩ positionId, ΩRaydiumLiquidityPoolPositionMintIdΩ lpMintId, diff --git a/src/main/tjava/com/r35157/libs/raydium/RaydiumLiquidityPoolPrice.tjava b/src/main/tjava/com/r35157/libs/raydium/RaydiumLiquidityPoolPrice.tjava index e44309e..24bd54d 100644 --- a/src/main/tjava/com/r35157/libs/raydium/RaydiumLiquidityPoolPrice.tjava +++ b/src/main/tjava/com/r35157/libs/raydium/RaydiumLiquidityPoolPrice.tjava @@ -1,18 +1,18 @@ package com.r35157.libs.raydium; -import com.r35157.libs.valuetypes.basic.AssetPrice; +import com.r35157.libs.valuetypes.basic.MoneyAmount; /** - * Represents the current price of the tokens in a Raydium liquidity pool. + * Represents the price of a Raydium liquidity pool. * - *

The 'poolId' identifies the Raydium liquidity pool for which the current token price applies. - * The 'price' represents the price value returned or calculated for that pool.

+ *

The pool id identifies the Raydium liquidity pool for which the price applies. + * The amount contains the price value returned or calculated for that pool.

* * @param poolId the Raydium liquidity pool id that the price belongs to - * @param price the price for a token in the liquidity pool + * @param amount the price amount for the liquidity pool */ public record RaydiumLiquidityPoolPrice( ΩRaydiumLiquidityPoolIdΩ poolId, - AssetPrice price + MoneyAmount amount ) { } \ No newline at end of file diff --git a/src/main/tjava/com/r35157/libs/raydium/RaydiumLiquidityPoolTokenAmounts.tjava b/src/main/tjava/com/r35157/libs/raydium/RaydiumLiquidityPoolTokenAmounts.tjava index c229b1e..b0241aa 100644 --- a/src/main/tjava/com/r35157/libs/raydium/RaydiumLiquidityPoolTokenAmounts.tjava +++ b/src/main/tjava/com/r35157/libs/raydium/RaydiumLiquidityPoolTokenAmounts.tjava @@ -1,7 +1,5 @@ package com.r35157.libs.raydium; -import java.math.BigDecimal; - /** * Represents token amounts for a Raydium liquidity pool or liquidity position. * @@ -18,6 +16,9 @@ import java.math.BigDecimal; * @param mintB the SPL mint address of token B * @param amountB the amount of token B */ + +import java.math.BigDecimal; + public record RaydiumLiquidityPoolTokenAmounts( ΩRaydiumLiquidityPoolIdΩ poolId, ΩSPLMintAddressΩ mintA, diff --git a/src/main/tjava/com/r35157/libs/solana/SPLTokenSupply.tjava b/src/main/tjava/com/r35157/libs/solana/SPLTokenSupply.tjava index f858e7d..5dcbd16 100644 --- a/src/main/tjava/com/r35157/libs/solana/SPLTokenSupply.tjava +++ b/src/main/tjava/com/r35157/libs/solana/SPLTokenSupply.tjava @@ -1,7 +1,5 @@ package com.r35157.libs.solana; -import java.math.BigDecimal; - /** * Represents the total supply of an SPL token mint. * @@ -14,6 +12,9 @@ import java.math.BigDecimal; * @param decimals the number of decimals used by the token mint * @param programId the SPL token program id for the mint */ + +import java.math.BigDecimal; + public record SPLTokenSupply( ΩSPLMintAddressΩ mintAddress, ΩAmountΩ uiAmount, diff --git a/src/main/tjava/com/r35157/libs/solana/SolanaBlockChain.tjava b/src/main/tjava/com/r35157/libs/solana/SolanaBlockChain.tjava new file mode 100644 index 0000000..982aea0 --- /dev/null +++ b/src/main/tjava/com/r35157/libs/solana/SolanaBlockChain.tjava @@ -0,0 +1,167 @@ +package com.r35157.libs.solana; + +import com.r35157.libs.solana.valuetypes.SolanaProgramDerivedAddress; +import com.r35157.libs.solana.valuetypes.economic.SolanaSPLTokenProgram; +import com.r35157.libs.valuetypes.basic.MoneyAmount; + +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Provides read-oriented access to the Solana blockchain. + * + *

This interface exposes the Solana operations needed by higher-level + * integrations. It can fetch native SOL balances, SPL token holdings, NFT-like + * token holding candidates, account information and program derived addresses.

+ * + *

The interface is intentionally generic and does not contain Raydium-specific + * logic. Higher-level integrations are expected to interpret Solana accounts, + * token holdings and derived addresses according to their own domain rules.

+ */ +public interface SolanaBlockChain { + /** + * Fetches the native SOL balance for a Solana address. + * + * @param address the Solana address to inspect + * @return the native SOL balance for the address + * @throws IOException if the balance could not be fetched or parsed + * @throws InterruptedException if the calling thread is interrupted while fetching the balance + */ + ΩSolanaAmountΩ getBalanceInSolana(ΩSolanaAddressΩ address) throws IOException, InterruptedException; + + /** + * Fetches the native SOL balance for a Solana address in lamports. + * + *

Lamports are the smallest unit of native SOL.

+ * + * @param address the Solana address to inspect + * @return the native SOL balance for the address in lamports + * @throws IOException if the balance could not be fetched or parsed + * @throws InterruptedException if the calling thread is interrupted while fetching the balance + */ + ΩlamportsΩ getBalanceInLamport(ΩSolanaAddressΩ address) throws IOException, InterruptedException; + + /** + * Fetches SPL token holdings owned by a Solana address for a specific token program. + * + *

The supplied token program decides which token accounts are inspected. For example, + * callers may query the original SPL Token Program or the Token-2022 Program depending + * on which token accounts they need to discover.

+ * + * @param ownerAddress the Solana owner address whose token holdings should be inspected + * @param splProgramId the SPL token program to query + * @return a map of SPL mint addresses to token holding information + * @throws IOException if the token holdings could not be fetched or parsed + * @throws InterruptedException if the calling thread is interrupted while fetching token holdings + */ + Map<ΩSPLMintAddressΩ, SPLTokenHolding> getSPLTokenHoldings( + ΩSolanaAddressΩ ownerAddress, + SolanaSPLTokenProgram splProgramId + ) throws IOException, InterruptedException; + + /** + * Fetches NFT-like token mint address candidates owned by a Solana address for a specific token program. + * + *

This method identifies token holdings that look like NFTs within the supplied token + * program. A returned address is only a candidate. Higher-level integrations are responsible + * for deciding whether the returned address has domain-specific meaning.

+ * + * // TODO This method currently identifies candidates from the owner's token holdings only. + * // A token with zero decimals and an owner balance of one is not guaranteed to be a real NFT, + * // because the mint's total supply may still be greater than one. A future implementation + * // should verify the mint supply, for example by using Solana getTokenSupply, before treating + * // the result as a confirmed NFT. + * + * @param ownerAddress the Solana owner address whose NFT-like holdings should be inspected + * @param splProgram the SPL token program to query + * @return the NFT-like Solana mint address candidates owned by the address + * @throws IOException if the NFT candidate addresses could not be fetched or parsed + * @throws InterruptedException if the calling thread is interrupted while fetching NFT candidate addresses + */ + Set<ΩSolanaNFTAddressΩ> getSolanaNFTCandidateAddresses( + ΩSolanaAddressΩ ownerAddress, + SolanaSPLTokenProgram splProgram + ) throws IOException, InterruptedException; + + /** + * Finds a Solana program derived address for a program id and a set of seeds. + * + *

The seeds describe the logical inputs used to derive the address. The implementation + * is responsible for converting each seed into the byte representation required by Solana.

+ * + * @param programId the Solana program id used to derive the address + * @param seeds the seeds used when deriving the program address + * @return the derived Solana address together with its bump value + */ + SolanaProgramDerivedAddress findProgramAddress( + ΩSolanaProgramIdΩ programId, + List seeds + ); + + /** + * Fetches account information for a Solana account address. + * + *

If the account does not exist, this method returns {@code null}. If the account exists, + * the returned value contains the account address, the owning Solana program id and the + * account data encoded as Base64.

+ * + * @param accountAddress the Solana account address to inspect + * @return account information, or {@code null} if the account does not exist + * @throws IOException if the account information could not be fetched or parsed + * @throws InterruptedException if the calling thread is interrupted while fetching account information + */ + SolanaAccountInfo getAccountInfo(ΩSolanaAddressΩ accountAddress) + throws IOException, InterruptedException; + + /** + * Encodes a raw 32-byte Solana address into its textual Solana address representation. + * + *

This method is intended for callers that have obtained raw Solana address bytes from + * account data and need the normal string representation used elsewhere in the API.

+ * + * @param addressBytes the raw 32-byte Solana address + * @return the encoded Solana address + * @throws IllegalArgumentException if the supplied byte array is not a valid Solana address length + */ + ΩSolanaAddressΩ encodeSolanaAddress(byte[] addressBytes); + + /** + * Fetches the total supply of an SPL token mint for a specific token program. + * + *

The supplied token program identifies which SPL token program owns the mint, + * for example the original SPL Token Program or the Token-2022 Program.

+ * + * @param mintAddress the SPL mint address whose supply should be fetched + * @param splProgram the SPL token program to query + * @return the SPL token supply for the mint + * @throws IOException if the token supply could not be fetched or parsed + * @throws InterruptedException if the calling thread is interrupted while fetching token supply + */ + SPLTokenSupply getSPLTokenSupply( + ΩSPLMintAddressΩ mintAddress, + SolanaSPLTokenProgram splProgram + ) throws IOException, InterruptedException; + + /** + * Fetches accounts owned by a Solana program using server-side account data filters. + * + *

This method uses Solana's {@code getProgramAccounts} RPC call. The supplied filters + * are sent to the RPC node, so matching is performed server-side instead of fetching all + * accounts owned by the program and filtering them locally.

+ * + *

The initial supported filter type is {@link SolanaProgramAccountMemcmpFilter}, which + * matches bytes at a specific offset in the account data.

+ * + * @param programId the Solana program id that owns the accounts to search + * @param filters the memcmp filters to apply when searching program accounts + * @return the matching program accounts + * @throws IOException if the program accounts could not be fetched or parsed + * @throws InterruptedException if the calling thread is interrupted while fetching program accounts + */ + Set getProgramAccounts( + ΩSolanaProgramIdΩ programId, + Set filters + ) throws IOException, InterruptedException; +} \ No newline at end of file diff --git a/src/main/tjava/com/r35157/libs/solana/SolanaProgramAccountMemcmpFilter.tjava b/src/main/tjava/com/r35157/libs/solana/SolanaProgramAccountMemcmpFilter.tjava new file mode 100644 index 0000000..eb523be --- /dev/null +++ b/src/main/tjava/com/r35157/libs/solana/SolanaProgramAccountMemcmpFilter.tjava @@ -0,0 +1,17 @@ +package com.r35157.libs.solana; + +/** + * Filter used when fetching accounts owned by a Solana program. + * + *

The initial supported filter type is {@code memcmp}, which asks the + * Solana RPC node to only return accounts where the account data at a specific + * byte offset matches a base58 encoded value.

+ * + * @param offset the byte offset in the account data where comparison starts + * @param bytes the base58 encoded bytes to match + */ +public record SolanaProgramAccountMemcmpFilter( + int offset, + String bytes +) { +} \ No newline at end of file diff --git a/src/main/tjava/com/r35157/libs/valuetypes/basic/MoneyPrice.tjava b/src/main/tjava/com/r35157/libs/valuetypes/basic/MoneyPrice.tjava new file mode 100644 index 0000000..4480d0d --- /dev/null +++ b/src/main/tjava/com/r35157/libs/valuetypes/basic/MoneyPrice.tjava @@ -0,0 +1,8 @@ +package com.r35157.libs.valuetypes.basic; + +import java.math.BigDecimal; + +public record MoneyPrice( + ΩPriceΩ price, + CurrencyType currencyType +) { } diff --git a/src/main/tjava/com/r35157/nenjim/hubd/impl/ref/Main.tjava b/src/main/tjava/com/r35157/nenjim/hubd/impl/ref/Main.tjava index ed7957e..57f803e 100644 --- a/src/main/tjava/com/r35157/nenjim/hubd/impl/ref/Main.tjava +++ b/src/main/tjava/com/r35157/nenjim/hubd/impl/ref/Main.tjava @@ -5,34 +5,44 @@ import com.r35157.libs.jupiter.perps.JupiterPerpsService; import com.r35157.libs.jupiter.perps.impl.anchoridl.AnchorIdlJupiterPerpsServiceImpl; import com.r35157.libs.solana.SolanaBlockChain; import com.r35157.libs.solana.impl.ref.SolanaBlockChainImpl; -import com.r35157.nenjim.hubd.ctx.Context; -import com.r35157.nenjim.hubd.NenjimHub; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.lang.management.ClassLoadingMXBean; -import java.lang.management.ManagementFactory; -import com.r35157.nenjim.hubd.ctx.ContextManager; -import com.r35157.nenjim.hubd.journal.JournalManager; -import com.r35157.nenjim.hubd.impl.ref.JournalManagerImpl; -import java.nio.file.Files; -import java.nio.file.Path; import java.util.Set; public class Main { // TODO: Consider if we really need a Main class or we just need to move the main method to NenjimHubImpl? static void main(String[] args) throws Exception { - NenjimHubImpl nenjimHub = new NenjimHubImpl(); + //NenjimHubImpl nenjimHub = new NenjimHubImpl(); - /* SolanaBlockChain sbc = new SolanaBlockChainImpl(); JupiterPerpsService jupiter = new AnchorIdlJupiterPerpsServiceImpl(sbc); ΩSolanaWalletIdΩ walletId = "vj98roDZ7744EBfxyuDFkKpEGCsKQLr7K8UFRumJNHf"; + Set positions = jupiter.getOpenPositions(walletId); + + for (JupiterPerpsPosition position : positions) { + System.out.println(" PositionAccount: " + position.positionAccount()); + System.out.println(" Token mint: " + position.tradedTokenMint()); + System.out.println(" Direction: " + position.direction()); + System.out.println(" Value: " + position.value()); + System.out.println(" Size $: " + position.size()); + System.out.println(" PnL: " + position.pnl() + " " + position.pnlPercent()); + System.out.println(" Leverage: " + position.leverage() + "x"); + System.out.println(" Entry price: " + position.entryPrice()); + System.out.println(" Market price: " + position.marketPrice()); + System.out.println(" Collateral: " + position.collateral()); + System.out.println(" Totals fees: " + position.totalFees()); + System.out.println(" Borrow Fees Due: " + position.borrowFeesDue()); + System.out.println(" Close Fees Pending: " + position.closeFeePending()); + System.out.println("Amount locked for account rent (SOL): " + position.accountRent()); + + System.out.println(); + } + int a=0; - */ - nenjimHub.awaitShutdown(); + //nenjimHub.awaitShutdown(); /* try { log.info("Auto-starting 2 Nenjim application(s)...");