diff --git a/pom.xml b/pom.xml
index 9cb6a1c..9dc4154 100644
--- a/pom.xml
+++ b/pom.xml
@@ -60,7 +60,7 @@
com.mmorrell
solanaj
- 1.17.2
+ 1.17.6
org.projectlombok
@@ -78,6 +78,24 @@
2.22.1
+
+ org.apache.httpcomponents.client5
+ httpclient5
+ 5.2.1
+
+
+
+ com.fasterxml.jackson.core
+ jackson-core
+ 2.17.0
+
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+ 2.17.0
+
+
@@ -177,4 +195,4 @@
https://oss.sonatype.org/content/repositories/releases/
-
\ No newline at end of file
+
diff --git a/raydium/pom.xml b/raydium/pom.xml
new file mode 100644
index 0000000..3a21b66
--- /dev/null
+++ b/raydium/pom.xml
@@ -0,0 +1,17 @@
+
+
+
+ solanaj-programs
+ com.mmorrell
+ 1.30.5
+
+ 4.0.0
+
+ raydium
+
+
+ 17
+ 17
+
+
diff --git a/raydium/src/main/java/com/mmorrell/raydium/manager/RaydiumLPManager.java b/raydium/src/main/java/com/mmorrell/raydium/manager/RaydiumLPManager.java
new file mode 100644
index 0000000..0a1f4ad
--- /dev/null
+++ b/raydium/src/main/java/com/mmorrell/raydium/manager/RaydiumLPManager.java
@@ -0,0 +1,148 @@
+package com.mmorrell.raydium.manager;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.mmorrell.raydium.model.LiquidityState;
+import org.apache.hc.client5.http.classic.methods.HttpGet;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
+import org.apache.hc.client5.http.impl.classic.HttpClients;
+import org.apache.hc.core5.http.HttpEntity;
+import org.apache.hc.core5.http.io.HttpClientResponseHandler;
+import org.apache.hc.core5.http.io.entity.EntityUtils;
+import org.apache.hc.core5.net.URIBuilder;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+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.TokenResultObjects;
+
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.math.RoundingMode;
+import java.net.URI;
+import java.net.URISyntaxException;
+
+public class RaydiumLPManager {
+ private static final Logger LOGGER = LogManager.getLogger(RaydiumLPManager.class);
+ private static final ObjectMapper objectMapper = new ObjectMapper();
+ public static final CloseableHttpClient httpClient = HttpClients.createDefault();
+
+ private final RpcClient client;
+
+ public RaydiumLPManager(final RpcClient client) {
+ this.client = client;
+ }
+
+ public boolean isLpLockedGivenTokenAddress(String tokenAddress) {
+ String lpMarketAddress = getRaydiumLpMarketPublicKey(tokenAddress);
+ return isLpLockedGivenLpMarketAddress(lpMarketAddress);
+ }
+
+ public boolean isLpLockedGivenLpMarketAddress(String lpMarketAddress) {
+ BigDecimal burnPercent = checkRaydiumLpBurnPercentageGivenLpMarketAddress(lpMarketAddress);
+ return burnPercent.compareTo(BigDecimal.valueOf(95)) > 0;
+ }
+
+ public BigDecimal checkRaydiumLpBurnPercentageGivenTokenAddress(String tokenAddress) {
+ String lpMarketAddress = getRaydiumLpMarketPublicKey(tokenAddress);
+ return checkRaydiumLpBurnPercentageGivenLpMarketAddress(lpMarketAddress);
+ }
+
+ public BigDecimal checkRaydiumLpBurnPercentageGivenLpMarketAddress(String lpMarketAddress) {
+ PublicKey lpMarketPublicKey = new PublicKey(lpMarketAddress);
+ AccountInfo lpMarketAddressInfo;
+ try {
+ lpMarketAddressInfo = client.getApi().getAccountInfo(lpMarketPublicKey);
+ } catch (RpcException e) {
+ LOGGER.error("RPC request issue getting lp market address account info: {}", lpMarketPublicKey);
+ throw new RuntimeException(e);
+ }
+
+ assert lpMarketAddressInfo != null;
+ LiquidityState liquidityState = LiquidityState.decode(lpMarketAddressInfo.getDecodedData());
+
+ PublicKey lpMint = liquidityState.lpMint();
+ BigInteger lpReserveBigInteger = liquidityState.u64LpReserve();
+ BigDecimal lpReserve = new BigDecimal(lpReserveBigInteger);
+
+ TokenResultObjects.TokenInfo info;
+ try {
+ info = client.getApi().getSplTokenAccountInfo(lpMint)
+ .getValue()
+ .getData()
+ .getParsed()
+ .getInfo();
+ } catch (RpcException e) {
+ LOGGER.error("Failed: Issue getting account info for lp mint: {}", lpMint);
+ throw new RuntimeException(e);
+ }
+
+ Integer decimals = info.getDecimals();
+ lpReserve = lpReserve.divide(BigDecimal.valueOf(10).pow(decimals), 2, RoundingMode.HALF_UP).subtract(BigDecimal.ONE);
+ BigDecimal actualSupply = new BigDecimal(info.getSupply()).divide(BigDecimal.valueOf(10).pow(decimals), 2, RoundingMode.HALF_UP);
+ BigDecimal maxLpSupply = actualSupply.max(lpReserve);
+
+ BigDecimal burnAmt = maxLpSupply.subtract(actualSupply);
+ try {
+ return burnAmt.divide(maxLpSupply, 2, RoundingMode.HALF_UP).multiply(BigDecimal.valueOf(100));
+ } catch (Exception e) {
+ if (e.getMessage().equals("/ by zero")) {
+ return BigDecimal.ZERO;
+ }
+ throw new RuntimeException(String.format("Failed to divide burnAmt %s by maxLpSupply %s: ", burnAmt, maxLpSupply), e);
+ }
+ }
+
+ public String getRaydiumLpMarketPublicKey(String tokenAddress) {
+ HttpGet tokenLpMarketGetHttpRequest = createGetHttpRequest(tokenAddress);
+ JsonNode tokenInfoJsonNode;
+ try {
+ tokenInfoJsonNode = httpClient.execute(tokenLpMarketGetHttpRequest, getHttpRequestHandler());
+ } catch (IOException e) {
+ LOGGER.error("Failed: Issue getting from geckoTerminal for token: {}", tokenAddress);
+ throw new RuntimeException(e);
+ }
+ return tokenInfoJsonNode.get("data")
+ .get("relationships")
+ .get("top_pools")
+ .withArrayProperty("data")
+ .get(0)
+ .get("id")
+ .asText()
+ .replace("solana_", "");
+ }
+
+ public HttpGet createGetHttpRequest(String tokenAddress) {
+ String endpoint = "https://api.geckoterminal.com/api/v2/networks/solana/tokens/" + tokenAddress;
+ URI tokenInfoURI;
+ try {
+ tokenInfoURI = new URIBuilder(endpoint)
+ .build();
+ LOGGER.info("Creating uri for GeckoTerminal endpoint: {}", endpoint);
+ } catch (URISyntaxException e) {
+ LOGGER.error("Failed: Issue creating URI from geckoTerminal for token: {}", tokenAddress);
+ throw new RuntimeException("Failed to send get request to geckoTerminal, ", e);
+ }
+
+ HttpGet tokenInfoGetRequest = new HttpGet(tokenInfoURI);
+ tokenInfoGetRequest.setHeader("accept", "application/json");
+
+ return tokenInfoGetRequest;
+ }
+
+ private static HttpClientResponseHandler getHttpRequestHandler() {
+ return response -> {
+ int status = response.getCode();
+ if (status >= 200 && status < 300) {
+ HttpEntity entity = response.getEntity();
+ return entity != null ? objectMapper.readTree(EntityUtils.toString(entity)) : null;
+ } else {
+ LOGGER.error("Failed: Issue getting http request handler");
+ throw new RuntimeException("Failed: Unexpected response status: " + status);
+ }
+ };
+ }
+}
diff --git a/raydium/src/main/java/com/mmorrell/raydium/model/LiquidityState.java b/raydium/src/main/java/com/mmorrell/raydium/model/LiquidityState.java
new file mode 100644
index 0000000..28cffc6
--- /dev/null
+++ b/raydium/src/main/java/com/mmorrell/raydium/model/LiquidityState.java
@@ -0,0 +1,126 @@
+package com.mmorrell.raydium.model;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.Setter;
+import org.p2p.solanaj.core.PublicKey;
+import org.p2p.solanaj.utils.ByteUtils;
+
+import java.math.BigInteger;
+import java.util.ArrayList;
+import java.util.List;
+
+public record LiquidityState(BigInteger u64Status, BigInteger u64Nonce, BigInteger u64MaxOrder, BigInteger u64Depth,
+ BigInteger u64BaseDecimal, BigInteger u64QuoteDecimal, BigInteger u64State,
+ BigInteger u64ResetFlag, BigInteger u64MinSize, BigInteger u64VolMaxCutRatio,
+ BigInteger u64AmountWaveRatio, BigInteger u64BaseLotSize, BigInteger u64QuoteLotSize,
+ BigInteger u64MinPriceMultiplier, BigInteger u64MaxPriceMultiplier,
+ BigInteger u64SystemDecimalValue, BigInteger u64MinSeparateNumerator,
+ BigInteger u64MinSeparateDenominator, BigInteger u64TradeFeeNumerator,
+ BigInteger u64TradeFeeDenominator, BigInteger u64PnlNumerator,
+ BigInteger u64PnlDenominator, BigInteger u64SwapFeeNumerator,
+ BigInteger u64SwapFeeDenominator, BigInteger u64BaseNeedTakePnl,
+ BigInteger u64QuoteNeedTakePnl, BigInteger u64QuoteTotalPnl, BigInteger u64BaseTotalPnl,
+ BigInteger u64PoolOpenTime, BigInteger u64PunishPcAmount, BigInteger u64PunishCoinAmount,
+ BigInteger u64OrderbookToInitTime, BigInteger u128SwapBaseInAmount,
+ BigInteger u128SwapQuoteOutAmount, BigInteger u64SwapBase2QuoteFee,
+ BigInteger u128SwapQuoteInAmount, BigInteger u128SwapBaseOutAmount,
+ BigInteger u64SwapQuote2BaseFee, PublicKey baseVault, PublicKey quoteVault,
+ PublicKey baseMint, PublicKey quoteMint, PublicKey lpMint, PublicKey openOrders,
+ PublicKey marketId, PublicKey marketProgramId, PublicKey targetOrders,
+ PublicKey withdrawQueue, PublicKey lpVault, PublicKey owner, BigInteger u64LpReserve,
+ List u64Padding) {
+ public static LiquidityState decode(byte[] data) {
+ MutableInt offset = new MutableInt(0);
+ return new LiquidityState(
+ readUint64(data, offset),
+ readUint64(data, offset),
+ readUint64(data, offset),
+ readUint64(data, offset),
+ readUint64(data, offset),
+ readUint64(data, offset),
+ readUint64(data, offset),
+ readUint64(data, offset),
+ readUint64(data, offset),
+ readUint64(data, offset),
+ readUint64(data, offset),
+ readUint64(data, offset),
+ readUint64(data, offset),
+ readUint64(data, offset),
+ readUint64(data, offset),
+ readUint64(data, offset),
+ readUint64(data, offset),
+ readUint64(data, offset),
+ readUint64(data, offset),
+ readUint64(data, offset),
+ readUint64(data, offset),
+ readUint64(data, offset),
+ readUint64(data, offset),
+ readUint64(data, offset),
+ readUint64(data, offset),
+ readUint64(data, offset),
+ readUint64(data, offset),
+ readUint64(data, offset),
+ readUint64(data, offset),
+ readUint64(data, offset),
+ readUint64(data, offset),
+ readUint64(data, offset),
+ readUint128(data, offset),
+ readUint128(data, offset),
+ readUint64(data, offset),
+ readUint128(data, offset),
+ readUint128(data, offset),
+ readUint64(data, offset),
+ readPubkey(data, offset),
+ readPubkey(data, offset),
+ readPubkey(data, offset),
+ readPubkey(data, offset),
+ readPubkey(data, offset),
+ readPubkey(data, offset),
+ readPubkey(data, offset),
+ readPubkey(data, offset),
+ readPubkey(data, offset),
+ readPubkey(data, offset),
+ readPubkey(data, offset),
+ readPubkey(data, offset),
+ readUint64(data, offset),
+ readU64PaddingList(data, offset)
+ );
+ }
+
+ private static BigInteger readUint64(byte[] data, MutableInt offset) {
+ BigInteger value = ByteUtils.readUint64(data, offset.getValue());
+ offset.setValue(offset.getValue() + ByteUtils.UINT_64_LENGTH);
+ return value;
+ }
+
+ private static BigInteger readUint128(byte[] data, MutableInt offset) {
+ BigInteger value = ByteUtils.readUint128(data, offset.getValue());
+ offset.setValue(offset.getValue() + ByteUtils.UINT_128_LENGTH);
+ return value;
+ }
+
+ private static PublicKey readPubkey(byte[] data, MutableInt offset) {
+ PublicKey publicKey = PublicKey.readPubkey(data, offset.getValue());
+ offset.setValue(offset.getValue() + PublicKey.PUBLIC_KEY_LENGTH);
+ return publicKey;
+ }
+
+ private static List readU64PaddingList(byte[] data, MutableInt offset) {
+ int u64PaddingStartingOffset = offset.getValue();
+ List u64PaddingList = new ArrayList<>();
+ while (u64PaddingStartingOffset < data.length) {
+ u64PaddingList.add(ByteUtils.readUint64(data, u64PaddingStartingOffset));
+ u64PaddingStartingOffset += 8;
+ }
+ return u64PaddingList;
+ }
+
+ // Wrapper class to hold an integer value
+ @Setter
+ @Getter
+ @AllArgsConstructor
+ private static class MutableInt {
+ private int value;
+ }
+}