-
Notifications
You must be signed in to change notification settings - Fork 11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Setup solana liquidity pool checker tool #16
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> | ||
<parent> | ||
<artifactId>solanaj-programs</artifactId> | ||
<groupId>com.mmorrell</groupId> | ||
<version>1.30.5</version> | ||
</parent> | ||
<modelVersion>4.0.0</modelVersion> | ||
|
||
<artifactId>raydium</artifactId> | ||
|
||
<properties> | ||
<maven.compiler.source>17</maven.compiler.source> | ||
<maven.compiler.target>17</maven.compiler.target> | ||
</properties> | ||
</project> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<JsonNode> getHttpRequestHandler() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As mentioned earlier, would prefer to use OkHttp as a single HTTP library. If you don't have bandwidth for those modifications, I can possibly just merge it and handle the tech debt later. Let me know your thoughts. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry late reply. If not yet done, I will work on this week. |
||
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); | ||
} | ||
}; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<BigInteger> 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<BigInteger> readU64PaddingList(byte[] data, MutableInt offset) { | ||
int u64PaddingStartingOffset = offset.getValue(); | ||
List<BigInteger> 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; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
SolanaJ already provides OkHttp which has proven to be a strong HTTP library. I'm slightly averse to bundling yet another HTTP library (Apache HTTPClient). I'd be interested in hearing reasoning for Apache.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I went with apache because I worked with it in the past. Not much reasoning. I have not worked with OkHttp, but I will update the code to use that instead of apache lib.