This commit is contained in:
2026-07-02 20:35:25 +02:00
parent 932b2589e4
commit cf73a03ff5
24 changed files with 618 additions and 92 deletions
-1
View File
@@ -44,7 +44,6 @@ dependencies {
runtimeOnly("org.apache.logging.log4j:log4j-core:2.26.0") runtimeOnly("org.apache.logging.log4j:log4j-core:2.26.0")
runtimeOnly("org.apache.logging.log4j:log4j-slf4j2-impl: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.fasterxml.jackson.core:jackson-databind:2.18.6")
implementation("com.fazecast:jSerialComm:2.11.4") implementation("com.fazecast:jSerialComm:2.11.4")
implementation("com.google.code.gson:gson:2.14.0") implementation("com.google.code.gson:gson:2.14.0")
@@ -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.
*
* <p>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.</p>
*/
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;
}
@@ -0,0 +1,5 @@
package com.r35157.libs.codec;
public interface Base58Codec {
String encode(byte[] input);
}
@@ -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.
*
* <p>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.</p>
*
* @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
) {
}
@@ -0,0 +1,9 @@
package com.r35157.libs.jupiter.perps;
/**
* Direction of a Jupiter Perps position.
*/
public enum JupiterPerpsPositionDirection {
LONG,
SHORT
}
@@ -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.
*
* <p>This service is read-only. It does not open, close, modify, or sign transactions
* for Jupiter Perpetual Contracts.</p>
*
* <p>NOTICE: The first supported operation is reading a known Jupiter Perps position account
* and returning its decoded position data.</p>
*/
public interface JupiterPerpsService {
/**
* Reads a Jupiter Perps position from a known position account.
*
* <p>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.</p>
*
* @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.
*
* <p>This method returns decoded Jupiter Perps position objects. It does not return
* raw Solana accounts or account ids.</p>
*
* @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<JupiterPerpsPosition> getOpenPositions(ΩSolanaWalletIdΩ owner)
throws IOException, InterruptedException;
}
@@ -4,47 +4,67 @@ import com.r35157.libs.codec.Base58Codec;
import com.r35157.libs.codec.impl.ref.Base58CodecImpl; import com.r35157.libs.codec.impl.ref.Base58CodecImpl;
import com.r35157.libs.solana.SolanaAccountInfo; import com.r35157.libs.solana.SolanaAccountInfo;
import java.math.BigInteger;
import java.util.Base64; import java.util.Base64;
class AnchorIdlJupiterPerpsCustodyDecoder { import static com.r35157.libs.jupiter.perps.impl.anchoridl.DecodingPrimitives.*;
ΩSPLMintAddressΩ decodeMint( public class AnchorIdlJupiterPerpsCustodyDecoder {
SolanaAccountInfo custodyAccountInfo
) {
byte[] data = Base64.getDecoder().decode(custodyAccountInfo.dataBase64());
if (data.length < MINT_OFFSET + PUBLIC_KEY_LENGTH) { public CustodyAccountInfo decode(SolanaAccountInfo custodyAccountInfo) {
throw new IllegalArgumentException( byte[] data = decodeIdl(custodyAccountInfo);
"Jupiter Perps custody account data is too short: " + data.length
);
}
return readPublicKey(data, MINT_OFFSET); CustodyAccountInfo cai = new CustodyAccountInfo(
} decodeMintAddress(data),
decodeCumulativeInterestRate(data)
private ΩSPLMintAddressΩ readPublicKey(
byte[] data,
int offset
) {
byte[] publicKeyBytes = new byte[PUBLIC_KEY_LENGTH];
System.arraycopy(
data,
offset,
publicKeyBytes,
0,
PUBLIC_KEY_LENGTH
); );
return base58.encode(publicKeyBytes); return cai;
} }
private byte[] decodeIdl(SolanaAccountInfo custodyAccountInfo) {
byte[] data = base64.decode(custodyAccountInfo.dataBase64());
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 ANCHOR_DISCRIMINATOR_LENGTH = 8;
private static final int PUBLIC_KEY_LENGTH = 32; 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 MINT_OFFSET = private static final int CUMULATIVE_INTEREST_RATE_OFFSET = FUNDING_RATE_STATE_OFFSET;
ANCHOR_DISCRIMINATOR_LENGTH
+ PUBLIC_KEY_LENGTH; // pool
private static final Base58Codec base58 = new Base58CodecImpl(); private static final Base58Codec base58 = new Base58CodecImpl();
private static final Base64.Decoder base64 = Base64.getDecoder();
} }
@@ -7,17 +7,15 @@ import com.r35157.libs.jupiter.perps.JupiterPerpsPositionDirection;
import com.r35157.libs.solana.SolanaAccountInfo; import com.r35157.libs.solana.SolanaAccountInfo;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.RoundingMode;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.ByteOrder; import java.nio.ByteOrder;
import java.util.Base64; import java.util.Base64;
class AnchorIdlJupiterPerpsPositionDecoder { public class AnchorIdlJupiterPerpsPositionDecoder {
JupiterPerpsPosition decode( public JupiterPerpsPositionInfo decode(SolanaAccountInfo accountInfo) {
ΩJupiterPerpsPositionAccountΩ positionAccount,
SolanaAccountInfo accountInfo,
ΩSPLMintAddressΩ tradedTokenMint
) {
byte[] data = Base64.getDecoder().decode(accountInfo.dataBase64()); byte[] data = Base64.getDecoder().decode(accountInfo.dataBase64());
if (data.length < PRICE_OFFSET + U64_LENGTH) { if (data.length < PRICE_OFFSET + U64_LENGTH) {
@@ -56,19 +54,34 @@ class AnchorIdlJupiterPerpsPositionDecoder {
.valueOf(rawSizeUsd) .valueOf(rawSizeUsd)
.movePointLeft(6); .movePointLeft(6);
JupiterPerpsPosition pos = new JupiterPerpsPosition( JupiterPerpsPositionInfo posInfo = new JupiterPerpsPositionInfo(
positionAccount,
entryPrice, entryPrice,
direction, direction,
tradedTokenMint,
sizeUsd, sizeUsd,
collateralUsd 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()); byte[] data = Base64.getDecoder().decode(accountInfo.dataBase64());
if (data.length < CUSTODY_OFFSET + PUBLIC_KEY_LENGTH) { 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( private JupiterPerpsPositionDirection decodeDirection(
byte rawSide byte rawSide
) { ) {
@@ -115,11 +145,25 @@ class AnchorIdlJupiterPerpsPositionDecoder {
return base58.encode(publicKeyBytes); 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 ANCHOR_DISCRIMINATOR_LENGTH = 8;
private static final int PUBLIC_KEY_LENGTH = 32; 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 SIDE_ENUM_LENGTH = 1;
private static final int I64_LENGTH = 8;
private static final int U64_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 OWNER_OFFSET = ANCHOR_DISCRIMINATOR_LENGTH;
private static final int POOL_OFFSET = OWNER_OFFSET + PUBLIC_KEY_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 PRICE_OFFSET = SIDE_OFFSET + SIDE_ENUM_LENGTH;
private static final int SIZE_USD_OFFSET = PRICE_OFFSET + U64_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 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(); private static final Base58Codec base58 = new Base58CodecImpl();
} }
@@ -5,8 +5,12 @@ import com.r35157.libs.jupiter.perps.JupiterPerpsService;
import com.r35157.libs.solana.SolanaAccountInfo; import com.r35157.libs.solana.SolanaAccountInfo;
import com.r35157.libs.solana.SolanaBlockChain; import com.r35157.libs.solana.SolanaBlockChain;
import com.r35157.libs.solana.SolanaProgramAccountMemcmpFilter; import com.r35157.libs.solana.SolanaProgramAccountMemcmpFilter;
import org.jetbrains.annotations.NotNull;
import java.io.IOException; import java.io.IOException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.RoundingMode;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set; import java.util.Set;
@@ -20,26 +24,47 @@ public class AnchorIdlJupiterPerpsServiceImpl implements JupiterPerpsService {
this.custodyDecoder = new AnchorIdlJupiterPerpsCustodyDecoder(); this.custodyDecoder = new AnchorIdlJupiterPerpsCustodyDecoder();
} }
/*
@Override @Override
public JupiterPerpsPosition getPosition(ΩJupiterPerpsPositionAccountΩ positionAccount) public JupiterPerpsPosition getPosition(@NotNull SolanaAccountInfo accountInfo)
throws IOException, InterruptedException { throws IOException, InterruptedException
SolanaAccountInfo accountInfo = solanaBlockChain.getAccountInfo(positionAccount); {
guard(accountInfo);
if (accountInfo == null) { JupiterPerpsPositionInfo posInfo = positionDecoder.decode(accountInfo);
throw new IllegalArgumentException("Jupiter Perps position account does not exist: " + positionAccount); ΩUSDCAmountΩ sizeUsd = posInfo.sizeUsd();
}
if (!JUPITER_PERPS_PROGRAM_ID.equals(accountInfo.owner())) { CustodyAccountInfo cai = custodyDecoder.decode(custodyAccountInfo);
throw new IllegalArgumentException( BigInteger cumulativeInterestSnapshot = positionDecoder.decodeCumulativeInterestSnapshot(positionAccountInfo);
"Account is not owned by Jupiter Perps program: " + positionAccount ΩUSDCAmountΩ borrowFeePendingUsd = calculateBorrowFeeUsd(
cumulativeInterestSnapshot,
sizeUsd
); );
}
ΩSPLMintAddressΩ tradedTokenMint = getTradedTokenMint(accountInfo); JupiterPerpsPosition pos = new JupiterPerpsPosition(
JupiterPerpsPosition pos = positionDecoder.decode(positionAccount, accountInfo, tradedTokenMint); accountInfo,
posInfo.entryPrice(),
posInfo.direction(),
sizeUsd,
posInfo.collateralUsd(),
cai.mintAddress(),
borrowFeePendingUsd
);
return pos; 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 @Override
public Set<JupiterPerpsPosition> getOpenPositions(ΩSolanaWalletIdΩ owner) public Set<JupiterPerpsPosition> getOpenPositions(ΩSolanaWalletIdΩ owner)
@@ -73,7 +98,20 @@ public class AnchorIdlJupiterPerpsServiceImpl implements JupiterPerpsService {
return Set.copyOf(positions); 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 throws IOException, InterruptedException
{ {
ΩSolanaAddressΩ custodyAccount = positionDecoder.decodeCustodyAccount(positionAccountInfo); ΩSolanaAddressΩ custodyAccount = positionDecoder.decodeCustodyAccount(positionAccountInfo);
@@ -87,7 +125,7 @@ public class AnchorIdlJupiterPerpsServiceImpl implements JupiterPerpsService {
ΩSPLMintAddressΩ mintAddress = custodyDecoder.decodeMint(custodyAccountInfo); ΩSPLMintAddressΩ mintAddress = custodyDecoder.decodeMint(custodyAccountInfo);
return mintAddress; return mintAddress;
} }*/
private static final ΩJupiterPerpsProgramIdΩ JUPITER_PERPS_PROGRAM_ID = private static final ΩJupiterPerpsProgramIdΩ JUPITER_PERPS_PROGRAM_ID =
"PERPHjGBqRHArX4DySjwM6UJHiR3sWAatqfdBS2qQJu"; "PERPHjGBqRHArX4DySjwM6UJHiR3sWAatqfdBS2qQJu";
@@ -0,0 +1,9 @@
package com.r35157.libs.jupiter.perps.impl.anchoridl;
import java.math.BigInteger;
public record CustodyAccountInfo(
ΩSPLMintAddressΩ mintAddress,
BigInteger currentCumulativeInterestRate
) {
}
@@ -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;
}
@@ -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
) {}
@@ -1,6 +1,5 @@
package com.r35157.libs.raydium; 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.MoneyAmount;
import com.r35157.libs.valuetypes.basic.Range; import com.r35157.libs.valuetypes.basic.Range;
@@ -16,11 +15,11 @@ public interface Raydium {
* to any Raydium liquidity pool supported by the implementation.</p> * to any Raydium liquidity pool supported by the implementation.</p>
* *
* @param poolId the Raydium liquidity pool id * @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 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 * @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. * 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 * @param decimalPlaces the number decimal places in the resulting price currency
* @return the price range represented by the concentrated liquidity position * @return the price range represented by the concentrated liquidity position
*/ */
Range<AssetPrice> calculateConcentratedPositionPriceRange( Range<ΩPriceΩ> calculateConcentratedPositionPriceRange(
RaydiumConcentratedPositionState positionState, RaydiumConcentratedPositionState positionState,
RaydiumConcentratedPoolInfo poolInfo, RaydiumConcentratedPoolInfo poolInfo,
int decimalPlaces int decimalPlaces
@@ -1,7 +1,5 @@
package com.r35157.libs.raydium; package com.r35157.libs.raydium;
import java.math.BigDecimal;
/** /**
* Represents basic information about a Raydium concentrated liquidity pool. * 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 mintBDecimals the number of decimals used by token B
* @param priceEstimate the pool price estimate as reported by Raydium * @param priceEstimate the pool price estimate as reported by Raydium
*/ */
import java.math.BigDecimal;
public record RaydiumConcentratedPoolInfo( public record RaydiumConcentratedPoolInfo(
ΩRaydiumLiquidityPoolConcentratedIdΩ poolId, ΩRaydiumLiquidityPoolConcentratedIdΩ poolId,
ΩSPLMintAddressΩ mintA, ΩSPLMintAddressΩ mintA,
@@ -1,7 +1,5 @@
package com.r35157.libs.raydium; package com.r35157.libs.raydium;
import java.math.BigInteger;
/** /**
* Represents raw on-chain state for a Raydium concentrated liquidity pool. * 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 sqrtPriceX64 the current square-root price in Q64.64 fixed-point format
* @param tickCurrent the current Raydium liquidity tick index * @param tickCurrent the current Raydium liquidity tick index
*/ */
import java.math.BigInteger;
public record RaydiumConcentratedPoolState( public record RaydiumConcentratedPoolState(
ΩRaydiumLiquidityPoolConcentratedIdΩ poolId, ΩRaydiumLiquidityPoolConcentratedIdΩ poolId,
ΩRaydiumLiquidityΩ liquidity, ΩRaydiumLiquidityΩ liquidity,
@@ -1,5 +1,6 @@
package com.r35157.libs.raydium; package com.r35157.libs.raydium;
import java.math.BigDecimal;
import java.math.BigInteger; import java.math.BigInteger;
public record RaydiumConcentratedPositionState( public record RaydiumConcentratedPositionState(
@@ -1,7 +1,5 @@
package com.r35157.libs.raydium; package com.r35157.libs.raydium;
import java.math.BigDecimal;
/** /**
* Represents a standard Raydium liquidity pool position. * 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 lpTokenAccount the SPL token account holding the liquidity pool tokens
* @param lpTokenAmount the amount of liquidity pool tokens held in the SPL token account * @param lpTokenAmount the amount of liquidity pool tokens held in the SPL token account
*/ */
import java.math.BigDecimal;
public record RaydiumLiquidityPoolPositionStandard( public record RaydiumLiquidityPoolPositionStandard(
ΩRaydiumLiquidityPoolPositionIdΩ positionId, ΩRaydiumLiquidityPoolPositionIdΩ positionId,
ΩRaydiumLiquidityPoolPositionMintIdΩ lpMintId, ΩRaydiumLiquidityPoolPositionMintIdΩ lpMintId,
@@ -1,18 +1,18 @@
package com.r35157.libs.raydium; 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.
* *
* <p>The 'poolId' identifies the Raydium liquidity pool for which the current token price applies. * <p>The pool id identifies the Raydium liquidity pool for which the price applies.
* The 'price' represents the price value returned or calculated for that pool.</p> * The amount contains the price value returned or calculated for that pool.</p>
* *
* @param poolId the Raydium liquidity pool id that the price belongs to * @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( public record RaydiumLiquidityPoolPrice(
ΩRaydiumLiquidityPoolIdΩ poolId, ΩRaydiumLiquidityPoolIdΩ poolId,
AssetPrice price MoneyAmount amount
) { ) {
} }
@@ -1,7 +1,5 @@
package com.r35157.libs.raydium; package com.r35157.libs.raydium;
import java.math.BigDecimal;
/** /**
* Represents token amounts for a Raydium liquidity pool or liquidity position. * 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 mintB the SPL mint address of token B
* @param amountB the amount of token B * @param amountB the amount of token B
*/ */
import java.math.BigDecimal;
public record RaydiumLiquidityPoolTokenAmounts( public record RaydiumLiquidityPoolTokenAmounts(
ΩRaydiumLiquidityPoolIdΩ poolId, ΩRaydiumLiquidityPoolIdΩ poolId,
ΩSPLMintAddressΩ mintA, ΩSPLMintAddressΩ mintA,
@@ -1,7 +1,5 @@
package com.r35157.libs.solana; package com.r35157.libs.solana;
import java.math.BigDecimal;
/** /**
* Represents the total supply of an SPL token mint. * 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 decimals the number of decimals used by the token mint
* @param programId the SPL token program id for the mint * @param programId the SPL token program id for the mint
*/ */
import java.math.BigDecimal;
public record SPLTokenSupply( public record SPLTokenSupply(
ΩSPLMintAddressΩ mintAddress, ΩSPLMintAddressΩ mintAddress,
ΩAmountΩ uiAmount, ΩAmountΩ uiAmount,
@@ -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.
*
* <p>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.</p>
*
* <p>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.</p>
*/
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.
*
* <p>Lamports are the smallest unit of native SOL.</p>
*
* @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.
*
* <p>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.</p>
*
* @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.
*
* <p>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.</p>
*
* // 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.
*
* <p>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.</p>
*
* @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<SolanaProgramAddressSeed> seeds
);
/**
* Fetches account information for a Solana account address.
*
* <p>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.</p>
*
* @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.
*
* <p>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.</p>
*
* @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.
*
* <p>The supplied token program identifies which SPL token program owns the mint,
* for example the original SPL Token Program or the Token-2022 Program.</p>
*
* @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.
*
* <p>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.</p>
*
* <p>The initial supported filter type is {@link SolanaProgramAccountMemcmpFilter}, which
* matches bytes at a specific offset in the account data.</p>
*
* @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<SolanaAccountInfo> getProgramAccounts(
ΩSolanaProgramIdΩ programId,
Set<SolanaProgramAccountMemcmpFilter> filters
) throws IOException, InterruptedException;
}
@@ -0,0 +1,17 @@
package com.r35157.libs.solana;
/**
* Filter used when fetching accounts owned by a Solana program.
*
* <p>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.</p>
*
* @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
) {
}
@@ -0,0 +1,8 @@
package com.r35157.libs.valuetypes.basic;
import java.math.BigDecimal;
public record MoneyPrice(
ΩPriceΩ price,
CurrencyType currencyType
) { }
@@ -5,34 +5,44 @@ import com.r35157.libs.jupiter.perps.JupiterPerpsService;
import com.r35157.libs.jupiter.perps.impl.anchoridl.AnchorIdlJupiterPerpsServiceImpl; import com.r35157.libs.jupiter.perps.impl.anchoridl.AnchorIdlJupiterPerpsServiceImpl;
import com.r35157.libs.solana.SolanaBlockChain; import com.r35157.libs.solana.SolanaBlockChain;
import com.r35157.libs.solana.impl.ref.SolanaBlockChainImpl; 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.Logger;
import org.slf4j.LoggerFactory; 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; import java.util.Set;
public class Main { public class Main {
// TODO: Consider if we really need a Main class or we just need to move the main method to NenjimHubImpl? // 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 { static void main(String[] args) throws Exception {
NenjimHubImpl nenjimHub = new NenjimHubImpl(); //NenjimHubImpl nenjimHub = new NenjimHubImpl();
/*
SolanaBlockChain sbc = new SolanaBlockChainImpl(); SolanaBlockChain sbc = new SolanaBlockChainImpl();
JupiterPerpsService jupiter = new AnchorIdlJupiterPerpsServiceImpl(sbc); JupiterPerpsService jupiter = new AnchorIdlJupiterPerpsServiceImpl(sbc);
ΩSolanaWalletIdΩ walletId = "vj98roDZ7744EBfxyuDFkKpEGCsKQLr7K8UFRumJNHf"; ΩSolanaWalletIdΩ walletId = "vj98roDZ7744EBfxyuDFkKpEGCsKQLr7K8UFRumJNHf";
Set<JupiterPerpsPosition> positions = jupiter.getOpenPositions(walletId); Set<JupiterPerpsPosition> 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; int a=0;
*/ //nenjimHub.awaitShutdown();
nenjimHub.awaitShutdown();
/* try { /* try {
log.info("Auto-starting 2 Nenjim application(s)..."); log.info("Auto-starting 2 Nenjim application(s)...");