Skip to content
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

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 21 additions & 3 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
<dependency>
<groupId>com.mmorrell</groupId>
<artifactId>solanaj</artifactId>
<version>1.17.2</version>
<version>1.17.6</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
Expand All @@ -78,6 +78,24 @@
<version>2.22.1</version>
</dependency>

<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
Copy link
Owner

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.

Copy link
Author

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.

<version>5.2.1</version>
</dependency>

<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.17.0</version>
</dependency>

<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.17.0</version>
</dependency>

</dependencies>

<distributionManagement>
Expand Down Expand Up @@ -144,8 +162,8 @@
<goal>sign</goal>
</goals>
<configuration>
<homedir>c:/Users/Michael/.gnupg/</homedir>
<keyname>0x27FAE7D2</keyname>
<homedir>/Users/chintan_mbp/.gnupg/</homedir>
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can move the maven-gpg-plugin to only run during the deploy job, thus removing the GPG signing requirement for non-deploys. See https://github.com/skynetcap/solanaj/blob/main/pom.xml#L198 for reference. So far I will be the only one deploying, so can leave the paths as they were, once it's moved to the deploy profile.

i.e.

<profiles>
        <profile>
            <id>deploy</id>
            <build>
                <plugins>
                    <plugin>
                        <groupId>org.apache.maven.plugins</groupId>
                        <artifactId>maven-gpg-plugin</artifactId>
                        <executions>
                            <execution>
                                <id>sign-artifacts</id>
                                <phase>verify</phase>
                                <goals>
                                    <goal>sign</goal>
                                </goals>
                                <configuration>
                                    <homedir>c:/Users/Michael/.gnupg/</homedir>
                                    <keyname>0x27FAE7D2</keyname>
                                </configuration>
                            </execution>
                        </executions>
                    </plugin>
                </plugins>
            </build>
        </profile>
    </profiles>
    ```

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update: I went ahead and did this myself.

<keyname></keyname>
</configuration>
</execution>
</executions>
Expand Down
17 changes: 17 additions & 0 deletions raydium/pom.xml
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() {
Copy link
Owner

Choose a reason for hiding this comment

The 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.

Copy link
Author

Choose a reason for hiding this comment

The 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);
}
};
}
}
126 changes: 126 additions & 0 deletions raydium/src/main/java/com/mmorrell/raydium/model/LiquidityState.java
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;
}
}