diff --git a/pom.xml b/pom.xml index d6e20d2..20525d0 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 + + @@ -145,7 +163,7 @@ /Users/chintan_mbp/.gnupg/ - 0x27FAE7D2 + 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; + } +}