Add Raydium implementation
This commit is contained in:
@@ -43,6 +43,7 @@ dependencies {
|
||||
runtimeOnly("org.apache.logging.log4j:log4j-core:2.26.0")
|
||||
runtimeOnly("org.apache.logging.log4j:log4j-slf4j2-impl:2.26.0")
|
||||
|
||||
implementation("com.fasterxml.jackson.core:jackson-databind:2.17.2")
|
||||
implementation("org.slf4j:slf4j-api:2.0.18")
|
||||
implementation("com.r35157.nenjim:hubd-api:0.1-dev")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,960 @@
|
||||
package com.r35157.libs.raydium.impl.ref;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.r35157.libs.raydium.Raydium;
|
||||
import com.r35157.libs.raydium.RaydiumConcentratedPositionState;
|
||||
import com.r35157.libs.raydium.RaydiumConcentratedPoolInfo;
|
||||
import com.r35157.libs.raydium.RaydiumConcentratedPoolState;
|
||||
import com.r35157.libs.raydium.RaydiumLiquidityPoolTokenAmounts;
|
||||
import com.r35157.libs.solana.SPLTokenHolding;
|
||||
import com.r35157.libs.solana.SPLTokenSupply;
|
||||
import com.r35157.libs.solana.SolanaAccountInfo;
|
||||
import com.r35157.libs.solana.SolanaBlockChain;
|
||||
import com.r35157.libs.solana.SolanaProgramAddressSeed;
|
||||
import com.r35157.libs.solana.valuetypes.SolanaProgramDerivedAddress;
|
||||
import com.r35157.libs.solana.valuetypes.WellKnownCurrencyTypes;
|
||||
import com.r35157.libs.solana.valuetypes.economic.SolanaSPLTokenProgram;
|
||||
import com.r35157.libs.valuetypes.basic.CurrencyType;
|
||||
import com.r35157.libs.valuetypes.basic.MoneyAmount;
|
||||
import com.r35157.libs.valuetypes.basic.Range;
|
||||
import java.io.IOException;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.BigInteger;
|
||||
import java.math.MathContext;
|
||||
import java.math.RoundingMode;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static com.r35157.libs.solana.valuetypes.economic.SolanaSPLTokenProgram.SPL_TOKEN_PROGRAM;
|
||||
import static com.r35157.libs.solana.valuetypes.economic.SolanaSPLTokenProgram.TOKEN_2022_PROGRAM;
|
||||
|
||||
public class RaydiumImpl implements Raydium {
|
||||
|
||||
public RaydiumImpl(SolanaBlockChain solanaBlockChain) {
|
||||
this.solanaBlockChain = solanaBlockChain;
|
||||
this.httpClient = HttpClient.newHttpClient();
|
||||
this.objectMapper = new ObjectMapper();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ΩSolanaAmountΩ fetchPoolPrice(ΩRaydiumLiquidityPoolIdΩ raydiumLiquidityPoolId) throws IOException, InterruptedException {
|
||||
ΩRestEndpointΩ endpoint = createPoolInfoByIdEndpoint(raydiumLiquidityPoolId);
|
||||
|
||||
JsonNode root = fetchJson(endpoint);
|
||||
|
||||
JsonNode firstPool = getFirstPoolNode(root);
|
||||
JsonNode priceNode = firstPool.path("price");
|
||||
|
||||
if (priceNode.isMissingNode() || !priceNode.isNumber()) {
|
||||
throw new IOException("Could NOT find field 'price' in JSON!");
|
||||
}
|
||||
|
||||
CurrencyType ct = WellKnownCurrencyTypes.SOLANA.getCurrencyType();
|
||||
ΩAmountΩ a = new ΩAmountΩ(priceNode.toString());
|
||||
ΩSolanaAmountΩ sa = new ΩSolanaAmountΩ(a, ct);
|
||||
|
||||
return sa;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<ΩRaydiumLiquidityPoolIdΩ> fetchLiquidityPoolIds(ΩSolanaAddressΩ ownerAddress) throws IOException, InterruptedException {
|
||||
Set<ΩRaydiumLiquidityPoolIdΩ> liquidityPoolIds = new HashSet<>();
|
||||
|
||||
liquidityPoolIds.addAll(fetchStandardLiquidityPoolIds(ownerAddress));
|
||||
liquidityPoolIds.addAll(fetchConcentratedLiquidityPoolIds(ownerAddress));
|
||||
|
||||
return Set.copyOf(liquidityPoolIds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<ΩRaydiumLiquidityPoolPositionNftIdΩ> fetchConcentratedPositionNftIds(
|
||||
ΩSolanaAddressΩ ownerAddress
|
||||
) throws IOException, InterruptedException {
|
||||
Set<ΩSolanaNFTAddressΩ> candidates;
|
||||
Set<ΩSolanaNFTAddressΩ> allCandidates = new HashSet<>();
|
||||
|
||||
System.out.println("Searching for legacy NFTs...");
|
||||
candidates = solanaBlockChain.getSolanaNFTCandidateAddresses(ownerAddress, SPL_TOKEN_PROGRAM);
|
||||
System.out.println("Found " + candidates.size() + " legacy NFTs");
|
||||
allCandidates.addAll(candidates);
|
||||
|
||||
System.out.println("Searching for token_2022 NFTs...");
|
||||
candidates = solanaBlockChain.getSolanaNFTCandidateAddresses(ownerAddress, TOKEN_2022_PROGRAM);
|
||||
System.out.println("Found " + candidates.size() + " token_2022 NFTs");
|
||||
allCandidates.addAll(candidates);
|
||||
|
||||
Set<ΩRaydiumLiquidityPoolPositionNftIdΩ> ids = filterCandidatePositionNftIds(allCandidates);
|
||||
return ids;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RaydiumLiquidityPoolTokenAmounts fetchStandardLiquidityPoolTokenAmounts(
|
||||
ΩRaydiumLiquidityPoolStandardIdΩ poolId
|
||||
) throws IOException, InterruptedException {
|
||||
RaydiumStandardPoolInfo poolInfo = fetchStandardPoolInfo(poolId);
|
||||
|
||||
RaydiumLiquidityPoolTokenAmounts amounts = new RaydiumLiquidityPoolTokenAmounts(
|
||||
poolInfo.poolId(),
|
||||
poolInfo.mintA(),
|
||||
poolInfo.amountA(),
|
||||
poolInfo.mintB(),
|
||||
poolInfo.amountB()
|
||||
);
|
||||
return amounts;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RaydiumLiquidityPoolTokenAmounts fetchStandardLiquidityPoolPositionTokenAmounts(
|
||||
ΩSolanaAddressΩ ownerAddress,
|
||||
ΩRaydiumLiquidityPoolStandardIdΩ poolId
|
||||
) throws IOException, InterruptedException {
|
||||
RaydiumStandardPoolInfo poolInfo = fetchStandardPoolInfo(poolId);
|
||||
|
||||
SPLTokenHolding ownerLpTokenHolding = fetchOwnerLpTokenHolding(
|
||||
ownerAddress,
|
||||
poolInfo.lpMint()
|
||||
);
|
||||
|
||||
SPLTokenSupply lpTokenSupply = solanaBlockChain.getSPLTokenSupply(
|
||||
poolInfo.lpMint(),
|
||||
SPL_TOKEN_PROGRAM
|
||||
);
|
||||
|
||||
ΩAmountΩ ownerLpAmount = ownerLpTokenHolding.uiAmount();
|
||||
ΩAmountΩ ownerShare = ownerLpAmount.divide(
|
||||
lpTokenSupply.uiAmount(),
|
||||
MathContext.DECIMAL128
|
||||
);
|
||||
|
||||
// TODO: This is a pool-share estimate, not an exact Raydium withdraw preview.
|
||||
// The calculation uses Raydium REST pool amounts and Solana LP mint supply:
|
||||
//
|
||||
// ownerShare = ownerLpAmount / lpMintSupply
|
||||
// ownerAmount = poolAmount * ownerShare
|
||||
//
|
||||
// This matches the simple LP-share model, but it may not match the Raydium UI's
|
||||
// remove-liquidity preview exactly.
|
||||
//
|
||||
// Observed example:
|
||||
// poolId: 8os8bnXoy5voKv3uBPPuVGyqWZGJaa2RRri5RbLUwPCY
|
||||
// LP amount owned: 0.077459898
|
||||
// LP mint supply: 0.077459898
|
||||
// calculated: 2.1594651 EVE / 31.867153 USDT
|
||||
// Raydium UI 100% withdraw preview: 2.1062559 EVE / 31.55046 USDT
|
||||
//
|
||||
// Likely reason:
|
||||
// The REST mintAmountA/mintAmountB values do not necessarily represent the
|
||||
// exact withdrawable vault amounts. Raydium's on-chain CPMM pool state has
|
||||
// fee buckets such as protocol fees, fund fees and creator fees. A precise
|
||||
// withdraw-preview calculation probably needs to decode the on-chain pool
|
||||
// state, fetch vault balances and subtract non-withdrawable fee amounts.
|
||||
//
|
||||
// Future fix:
|
||||
// Decode the on-chain CPMM pool state, fetch the token vault balances,
|
||||
// subtract protocol/fund/creator fee buckets, and calculate the owner's
|
||||
// share from those withdrawable balances instead of from REST mintAmountA
|
||||
// and mintAmountB.
|
||||
ΩAmountΩ ownerAmountA = poolInfo.amountA().multiply(ownerShare);
|
||||
ΩAmountΩ ownerAmountB = poolInfo.amountB().multiply(ownerShare);
|
||||
|
||||
RaydiumLiquidityPoolTokenAmounts amounts = new RaydiumLiquidityPoolTokenAmounts(
|
||||
poolInfo.poolId(),
|
||||
poolInfo.mintA(),
|
||||
ownerAmountA,
|
||||
poolInfo.mintB(),
|
||||
ownerAmountB
|
||||
);
|
||||
return amounts;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RaydiumConcentratedPositionState fetchConcentratedPositionState(
|
||||
ΩRaydiumLiquidityPoolPositionNftIdΩ positionNftId
|
||||
) throws IOException, InterruptedException {
|
||||
SolanaAccountInfo positionAccountInfo = fetchRaydiumPositionAccountInfo(positionNftId);
|
||||
byte[] accountData = decodeBase64AccountData(positionAccountInfo);
|
||||
|
||||
byte[] poolIdBytes = extractConcentratedLiquidityPoolIdBytes(accountData);
|
||||
ΩRaydiumLiquidityPoolConcentratedIdΩ poolId = solanaBlockChain.encodeSolanaAddress(poolIdBytes);
|
||||
|
||||
ΩraydiumLiquidityTickIndexΩ tickLowerIndex = extractConcentratedLiquidityTickIndex(
|
||||
accountData,
|
||||
RAYDIUM_POSITION_ACCOUNT_TICK_LOWER_INDEX_OFFSET,
|
||||
"tickLowerIndex"
|
||||
);
|
||||
|
||||
ΩraydiumLiquidityTickIndexΩ tickUpperIndex = extractConcentratedLiquidityTickIndex(
|
||||
accountData,
|
||||
RAYDIUM_POSITION_ACCOUNT_TICK_UPPER_INDEX_OFFSET,
|
||||
"tickUpperIndex"
|
||||
);
|
||||
|
||||
ΩRaydiumLiquidityΩ liquidity = extractConcentratedLiquidity(accountData);
|
||||
|
||||
RaydiumConcentratedPositionState state = new RaydiumConcentratedPositionState(
|
||||
positionNftId,
|
||||
poolId,
|
||||
tickLowerIndex,
|
||||
tickUpperIndex,
|
||||
liquidity
|
||||
);
|
||||
return state;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RaydiumConcentratedPoolInfo fetchConcentratedPoolInfo(
|
||||
ΩRaydiumLiquidityPoolConcentratedIdΩ poolId
|
||||
) throws IOException, InterruptedException {
|
||||
ΩRestEndpointΩ endpoint = createPoolInfoByIdEndpoint(poolId);
|
||||
|
||||
JsonNode root = fetchJson(endpoint);
|
||||
JsonNode firstPool = getFirstPoolNode(root);
|
||||
|
||||
ΩSPLMintAddressΩ mintA = extractMintAddress(firstPool, "mintA");
|
||||
ΩamountDecimalsΩ mintADecimals = extractMintDecimals(firstPool, "mintA");
|
||||
ΩSPLMintAddressΩ mintB = extractMintAddress(firstPool, "mintB");
|
||||
ΩamountDecimalsΩ mintBDecimals = extractMintDecimals(firstPool, "mintB");
|
||||
ΩPriceΩ priceEstimate = extractPriceEstimate(firstPool);
|
||||
|
||||
RaydiumConcentratedPoolInfo poolInfo = new RaydiumConcentratedPoolInfo(
|
||||
poolId,
|
||||
mintA,
|
||||
mintADecimals,
|
||||
mintB,
|
||||
mintBDecimals,
|
||||
priceEstimate
|
||||
);
|
||||
return poolInfo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RaydiumConcentratedPoolState fetchConcentratedPoolState(
|
||||
ΩRaydiumLiquidityPoolConcentratedIdΩ poolId
|
||||
) throws IOException, InterruptedException {
|
||||
SolanaAccountInfo poolAccountInfo = fetchRaydiumPoolAccountInfo(poolId);
|
||||
byte[] accountData = decodeBase64AccountData(poolAccountInfo);
|
||||
|
||||
ΩRaydiumLiquidityΩ liquidity = extractConcentratedPoolLiquidity(accountData);
|
||||
ΩRaydiumSqrtPriceX64Ω sqrtPriceX64 = extractConcentratedPoolSqrtPriceX64(accountData);
|
||||
ΩraydiumLiquidityTickIndexΩ tickCurrent = extractConcentratedLiquidityTickIndex(
|
||||
accountData,
|
||||
RAYDIUM_POOL_STATE_TICK_CURRENT_OFFSET,
|
||||
"tickCurrent"
|
||||
);
|
||||
|
||||
RaydiumConcentratedPoolState state = new RaydiumConcentratedPoolState(
|
||||
poolId,
|
||||
liquidity,
|
||||
sqrtPriceX64,
|
||||
tickCurrent
|
||||
);
|
||||
return state;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Range<ΩPriceΩ> calculateConcentratedPositionPriceRange(
|
||||
RaydiumConcentratedPositionState positionState,
|
||||
RaydiumConcentratedPoolInfo poolInfo,
|
||||
int decimalPlaces
|
||||
) {
|
||||
ΩPriceΩ priceFrom = calculatePriceFromTick(
|
||||
positionState.tickLowerIndex(),
|
||||
poolInfo,
|
||||
decimalPlaces
|
||||
);
|
||||
|
||||
ΩPriceΩ priceTo = calculatePriceFromTick(
|
||||
positionState.tickUpperIndex(),
|
||||
poolInfo,
|
||||
decimalPlaces
|
||||
);
|
||||
|
||||
Range<ΩPriceΩ> range = new Range<>(priceFrom, true, priceTo, false);
|
||||
return range;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RaydiumLiquidityPoolTokenAmounts calculateConcentratedPositionTokenAmounts(
|
||||
RaydiumConcentratedPositionState positionState,
|
||||
RaydiumConcentratedPoolInfo poolInfo,
|
||||
RaydiumConcentratedPoolState poolState
|
||||
) {
|
||||
RaydiumLiquidityPoolTokenAmounts amounts = calculateConcentratedPositionTokenAmounts(
|
||||
positionState,
|
||||
poolInfo,
|
||||
calculateCurrentSqrtRawPrice(poolState)
|
||||
);
|
||||
return amounts;
|
||||
}
|
||||
|
||||
private RaydiumLiquidityPoolTokenAmounts calculateConcentratedPositionTokenAmounts(
|
||||
RaydiumConcentratedPositionState positionState,
|
||||
RaydiumConcentratedPoolInfo poolInfo,
|
||||
ΩRaydiumSqrtPriceΩ sqrtPriceCurrent
|
||||
) {
|
||||
ΩRaydiumSqrtPriceΩ sqrtPriceLower = calculateSqrtRawPriceFromTick(positionState.tickLowerIndex());
|
||||
ΩRaydiumSqrtPriceΩ sqrtPriceUpper = calculateSqrtRawPriceFromTick(positionState.tickUpperIndex());
|
||||
BigDecimal liquidity = new BigDecimal(positionState.liquidity());
|
||||
|
||||
BigDecimal rawAmountA;
|
||||
BigDecimal rawAmountB;
|
||||
|
||||
if (sqrtPriceCurrent.compareTo(sqrtPriceLower) <= 0) {
|
||||
rawAmountA = calculateAmountAForLiquidity(liquidity, sqrtPriceLower, sqrtPriceUpper);
|
||||
rawAmountB = BigDecimal.ZERO;
|
||||
} else if (sqrtPriceCurrent.compareTo(sqrtPriceUpper) >= 0) {
|
||||
rawAmountA = BigDecimal.ZERO;
|
||||
rawAmountB = calculateAmountBForLiquidity(liquidity, sqrtPriceLower, sqrtPriceUpper);
|
||||
} else {
|
||||
rawAmountA = calculateAmountAForLiquidity(liquidity, sqrtPriceCurrent, sqrtPriceUpper);
|
||||
rawAmountB = calculateAmountBForLiquidity(liquidity, sqrtPriceLower, sqrtPriceCurrent);
|
||||
}
|
||||
|
||||
ΩAmountΩ amountA = convertRawTokenAmountToUiAmount(
|
||||
rawAmountA,
|
||||
poolInfo.mintADecimals()
|
||||
);
|
||||
|
||||
ΩAmountΩ amountB = convertRawTokenAmountToUiAmount(
|
||||
rawAmountB,
|
||||
poolInfo.mintBDecimals()
|
||||
);
|
||||
|
||||
RaydiumLiquidityPoolTokenAmounts amounts = new RaydiumLiquidityPoolTokenAmounts(
|
||||
poolInfo.poolId(),
|
||||
poolInfo.mintA(),
|
||||
amountA,
|
||||
poolInfo.mintB(),
|
||||
amountB
|
||||
);
|
||||
return amounts;
|
||||
}
|
||||
|
||||
private ΩRaydiumSqrtPriceΩ calculateCurrentSqrtRawPrice(RaydiumConcentratedPoolState poolState) {
|
||||
ΩRaydiumSqrtPriceΩ p = new BigDecimal(poolState.sqrtPriceX64()).divide(
|
||||
RAYDIUM_Q64_FACTOR,
|
||||
RAYDIUM_PRICE_MATH_CONTEXT
|
||||
);
|
||||
return p;
|
||||
}
|
||||
|
||||
private ΩRaydiumSqrtPriceΩ calculateSqrtRawPriceFromTick(ΩraydiumLiquidityTickIndexΩ tickIndex) {
|
||||
BigDecimal rawPrice = pow(RAYDIUM_TICK_BASE, tickIndex);
|
||||
BigDecimal result = rawPrice.sqrt(RAYDIUM_PRICE_MATH_CONTEXT);
|
||||
return result;
|
||||
}
|
||||
|
||||
private BigDecimal calculateAmountAForLiquidity(
|
||||
BigDecimal liquidity,
|
||||
ΩRaydiumSqrtPriceΩ sqrtPriceLower,
|
||||
ΩRaydiumSqrtPriceΩ sqrtPriceUpper
|
||||
) {
|
||||
BigDecimal numerator = liquidity.multiply(
|
||||
sqrtPriceUpper.subtract(sqrtPriceLower, RAYDIUM_PRICE_MATH_CONTEXT),
|
||||
RAYDIUM_PRICE_MATH_CONTEXT
|
||||
);
|
||||
|
||||
BigDecimal denominator = sqrtPriceUpper.multiply(
|
||||
sqrtPriceLower,
|
||||
RAYDIUM_PRICE_MATH_CONTEXT
|
||||
);
|
||||
|
||||
BigDecimal result = numerator.divide(
|
||||
denominator,
|
||||
RAYDIUM_PRICE_MATH_CONTEXT
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
private BigDecimal calculateAmountBForLiquidity(
|
||||
BigDecimal liquidity,
|
||||
ΩRaydiumSqrtPriceΩ sqrtPriceLower,
|
||||
ΩRaydiumSqrtPriceΩ sqrtPriceUpper
|
||||
) {
|
||||
BigDecimal result = liquidity.multiply(
|
||||
sqrtPriceUpper.subtract(sqrtPriceLower, RAYDIUM_PRICE_MATH_CONTEXT),
|
||||
RAYDIUM_PRICE_MATH_CONTEXT
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
private ΩAmountΩ convertRawTokenAmountToUiAmount(
|
||||
BigDecimal rawAmount,
|
||||
ΩamountDecimalsΩ decimals
|
||||
) {
|
||||
BigDecimal decimalFactor = pow(BigDecimal.TEN, decimals);
|
||||
|
||||
ΩAmountΩ a = rawAmount.divide(
|
||||
decimalFactor,
|
||||
RAYDIUM_PRICE_MATH_CONTEXT
|
||||
);
|
||||
|
||||
a = a.setScale(decimals, RoundingMode.HALF_UP);
|
||||
return a;
|
||||
}
|
||||
|
||||
private ΩPriceΩ calculatePriceFromTick(
|
||||
ΩraydiumLiquidityTickIndexΩ tickIndex,
|
||||
RaydiumConcentratedPoolInfo poolInfo,
|
||||
int decimalPlaces
|
||||
) {
|
||||
if (decimalPlaces < 0) {
|
||||
throw new IllegalArgumentException("decimalPlaces must not be negative!");
|
||||
}
|
||||
|
||||
BigDecimal rawPrice = pow(RAYDIUM_TICK_BASE, tickIndex);
|
||||
BigDecimal decimalFactor = calculateDecimalFactor(poolInfo);
|
||||
|
||||
ΩPriceΩ p = rawPrice.multiply(
|
||||
decimalFactor,
|
||||
RAYDIUM_PRICE_MATH_CONTEXT
|
||||
);
|
||||
|
||||
p = p.setScale(
|
||||
decimalPlaces,
|
||||
RoundingMode.HALF_UP
|
||||
);
|
||||
|
||||
return p;
|
||||
}
|
||||
|
||||
private BigDecimal calculateDecimalFactor(RaydiumConcentratedPoolInfo poolInfo) {
|
||||
BigDecimal result = pow(
|
||||
BigDecimal.TEN,
|
||||
poolInfo.mintADecimals() - poolInfo.mintBDecimals()
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private BigDecimal pow(BigDecimal base, int exponent) {
|
||||
BigDecimal power = base.pow(
|
||||
Math.abs(exponent),
|
||||
RAYDIUM_PRICE_MATH_CONTEXT
|
||||
);
|
||||
|
||||
if (exponent >= 0) {
|
||||
return power;
|
||||
}
|
||||
|
||||
BigDecimal result = BigDecimal.ONE.divide(
|
||||
power,
|
||||
RAYDIUM_PRICE_MATH_CONTEXT
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private SPLTokenHolding fetchOwnerLpTokenHolding(
|
||||
ΩSolanaAddressΩ ownerAddress,
|
||||
ΩSPLMintAddressΩ lpMint
|
||||
) throws IOException, InterruptedException {
|
||||
var tokenHoldings = solanaBlockChain.getSPLTokenHoldings(
|
||||
ownerAddress,
|
||||
SPL_TOKEN_PROGRAM
|
||||
);
|
||||
|
||||
SPLTokenHolding tokenHolding = tokenHoldings.get(lpMint);
|
||||
|
||||
if (tokenHolding == null) {
|
||||
throw new IOException("Owner does not hold LP token mint: " + lpMint);
|
||||
}
|
||||
|
||||
return tokenHolding;
|
||||
}
|
||||
|
||||
private RaydiumStandardPoolInfo fetchStandardPoolInfo(
|
||||
ΩRaydiumLiquidityPoolStandardIdΩ poolId
|
||||
) throws IOException, InterruptedException {
|
||||
ΩRestEndpointΩ endpoint = createPoolInfoByIdEndpoint(poolId);
|
||||
|
||||
JsonNode root = fetchJson(endpoint);
|
||||
JsonNode firstPool = getFirstPoolNode(root);
|
||||
|
||||
ΩSPLMintAddressΩ mintA = extractMintAddress(firstPool, "mintA");
|
||||
ΩAmountΩ amountA = extractAmount(firstPool, "mintAmountA");
|
||||
ΩSPLMintAddressΩ mintB = extractMintAddress(firstPool, "mintB");
|
||||
ΩAmountΩ amountB = extractAmount(firstPool, "mintAmountB");
|
||||
ΩSPLMintAddressΩ lpMint = extractMintAddress(firstPool, "lpMint");
|
||||
ΩAmountΩ totalLpAmount = extractAmount(firstPool, "lpAmount");
|
||||
|
||||
return new RaydiumStandardPoolInfo(
|
||||
poolId,
|
||||
mintA,
|
||||
amountA,
|
||||
mintB,
|
||||
amountB,
|
||||
lpMint,
|
||||
totalLpAmount
|
||||
);
|
||||
}
|
||||
|
||||
private ΩSPLMintAddressΩ extractMintAddress(JsonNode poolNode, String mintFieldName) throws IOException {
|
||||
JsonNode addressNode = poolNode.path(mintFieldName).path("address");
|
||||
|
||||
if (addressNode.isMissingNode() || !addressNode.isTextual()) {
|
||||
throw new IOException("Could NOT find textual field '" + mintFieldName + ".address' in pool JSON!");
|
||||
}
|
||||
|
||||
return addressNode.asText();
|
||||
}
|
||||
|
||||
private ΩAmountΩ extractAmount(JsonNode poolNode, String amountFieldName) throws IOException {
|
||||
JsonNode amountNode = poolNode.path(amountFieldName);
|
||||
|
||||
if (amountNode.isMissingNode() || !amountNode.isNumber()) {
|
||||
throw new IOException("Could NOT find numeric field '" + amountFieldName + "' in pool JSON!");
|
||||
}
|
||||
|
||||
return new BigDecimal(amountNode.toString());
|
||||
}
|
||||
|
||||
private ΩamountDecimalsΩ extractMintDecimals(JsonNode poolNode, String mintFieldName) throws IOException {
|
||||
JsonNode decimalsNode = poolNode.path(mintFieldName).path("decimals");
|
||||
|
||||
if (decimalsNode.isMissingNode() || !decimalsNode.isNumber()) {
|
||||
throw new IOException("Could NOT find numeric field '" + mintFieldName + ".decimals' in pool JSON!");
|
||||
}
|
||||
|
||||
return decimalsNode.asInt();
|
||||
}
|
||||
|
||||
private ΩPriceΩ extractPriceEstimate(JsonNode poolNode) throws IOException {
|
||||
JsonNode priceNode = poolNode.path("price");
|
||||
|
||||
if (priceNode.isMissingNode() || !priceNode.isNumber()) {
|
||||
throw new IOException("Could NOT find numeric field 'price' in pool JSON!");
|
||||
}
|
||||
|
||||
return new BigDecimal(priceNode.toString());
|
||||
}
|
||||
|
||||
private Set<ΩRaydiumLiquidityPoolStandardIdΩ> fetchStandardLiquidityPoolIds(ΩSolanaAddressΩ ownerAddress) throws IOException, InterruptedException {
|
||||
var tokenHoldings = solanaBlockChain.getSPLTokenHoldings(
|
||||
ownerAddress,
|
||||
SPL_TOKEN_PROGRAM
|
||||
);
|
||||
|
||||
Set<ΩSPLMintAddressΩ> mintAddresses = tokenHoldings.keySet();
|
||||
|
||||
if (mintAddresses.isEmpty()) {
|
||||
return Set.of();
|
||||
}
|
||||
|
||||
ΩRestEndpointΩ endpoint = createPoolInfoByCandidateLpMintsEndpoint(mintAddresses);
|
||||
JsonNode root = fetchJson(endpoint);
|
||||
|
||||
return extractStandardLiquidityPoolIds(root);
|
||||
}
|
||||
|
||||
private Set<ΩRaydiumLiquidityPoolConcentratedIdΩ> fetchConcentratedLiquidityPoolIds(
|
||||
ΩSolanaAddressΩ ownerAddress
|
||||
) throws IOException, InterruptedException {
|
||||
Set<ΩRaydiumLiquidityPoolPositionNftIdΩ> positionNftIds = fetchConcentratedPositionNftIds(ownerAddress);
|
||||
Set<SolanaAccountInfo> positionAccountInfos = fetchRaydiumPositionAccountInfos(positionNftIds);
|
||||
|
||||
return extractConcentratedLiquidityPoolIds(positionAccountInfos);
|
||||
}
|
||||
|
||||
private Set<ΩRaydiumLiquidityPoolPositionNftIdΩ> filterCandidatePositionNftIds(
|
||||
Set<ΩSolanaNFTAddressΩ> candidatePositionNftIds
|
||||
) throws IOException, InterruptedException {
|
||||
Set<ΩRaydiumLiquidityPoolPositionNftIdΩ> positionNftIds = new HashSet<>();
|
||||
|
||||
System.out.println("Filtering " + candidatePositionNftIds.size() + " NFTs for Raydium Concentrated pools...");
|
||||
for (ΩSolanaNFTAddressΩ candidatePositionNftId : candidatePositionNftIds) {
|
||||
System.out.print(" " + candidatePositionNftId + " ");
|
||||
SolanaProgramDerivedAddress positionPda = findRaydiumPositionPda(candidatePositionNftId);
|
||||
SolanaAccountInfo accountInfo = solanaBlockChain.getAccountInfo(positionPda.address());
|
||||
|
||||
if (accountInfo == null) {
|
||||
System.out.println("NO");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (RAYDIUM_CLMM_PROGRAM_ID.equals(accountInfo.owner())) {
|
||||
System.out.println("YES");
|
||||
positionNftIds.add(candidatePositionNftId);
|
||||
}
|
||||
}
|
||||
|
||||
return Set.copyOf(positionNftIds);
|
||||
}
|
||||
|
||||
private SolanaProgramDerivedAddress findRaydiumPositionPda(ΩSolanaNFTAddressΩ candidatePositionNftId) {
|
||||
return solanaBlockChain.findProgramAddress(
|
||||
RAYDIUM_CLMM_PROGRAM_ID,
|
||||
List.of(
|
||||
SolanaProgramAddressSeed.utf8("position"),
|
||||
SolanaProgramAddressSeed.solanaAddress(candidatePositionNftId)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private SolanaAccountInfo fetchRaydiumPositionAccountInfo(
|
||||
ΩRaydiumLiquidityPoolPositionNftIdΩ positionNftId
|
||||
) throws IOException, InterruptedException {
|
||||
SolanaProgramDerivedAddress positionPda = findRaydiumPositionPda(positionNftId);
|
||||
|
||||
SolanaAccountInfo accountInfo = solanaBlockChain.getAccountInfo(positionPda.address());
|
||||
|
||||
if (accountInfo == null) {
|
||||
throw new IOException("Raydium position account was not found for position NFT id: " + positionNftId);
|
||||
}
|
||||
|
||||
if (!RAYDIUM_CLMM_PROGRAM_ID.equals(accountInfo.owner())) {
|
||||
throw new IOException("Raydium position account is not owned by the Raydium CLMM program: " + positionPda.address());
|
||||
}
|
||||
|
||||
return accountInfo;
|
||||
}
|
||||
|
||||
private SolanaAccountInfo fetchRaydiumPoolAccountInfo(
|
||||
ΩRaydiumLiquidityPoolConcentratedIdΩ poolId
|
||||
) throws IOException, InterruptedException {
|
||||
SolanaAccountInfo accountInfo = solanaBlockChain.getAccountInfo(poolId);
|
||||
|
||||
if (accountInfo == null) {
|
||||
throw new IOException("Raydium concentrated pool account was not found for pool id: " + poolId);
|
||||
}
|
||||
|
||||
if (!RAYDIUM_CLMM_PROGRAM_ID.equals(accountInfo.owner())) {
|
||||
throw new IOException("Raydium concentrated pool account is not owned by the Raydium CLMM program: " + poolId);
|
||||
}
|
||||
|
||||
return accountInfo;
|
||||
}
|
||||
|
||||
private Set<SolanaAccountInfo> fetchRaydiumPositionAccountInfos(
|
||||
Set<ΩRaydiumLiquidityPoolPositionNftIdΩ> positionNftIds
|
||||
) throws IOException, InterruptedException {
|
||||
Set<SolanaAccountInfo> accountInfos = new HashSet<>();
|
||||
|
||||
for (ΩRaydiumLiquidityPoolPositionNftIdΩ positionNftId : positionNftIds) {
|
||||
accountInfos.add(fetchRaydiumPositionAccountInfo(positionNftId));
|
||||
}
|
||||
|
||||
return Set.copyOf(accountInfos);
|
||||
}
|
||||
|
||||
private Set<ΩRaydiumLiquidityPoolConcentratedIdΩ> extractConcentratedLiquidityPoolIds(
|
||||
Set<SolanaAccountInfo> positionAccountInfos
|
||||
) throws IOException {
|
||||
Set<ΩRaydiumLiquidityPoolConcentratedIdΩ> liquidityPoolIds = new HashSet<>();
|
||||
|
||||
for (SolanaAccountInfo positionAccountInfo : positionAccountInfos) {
|
||||
byte[] accountData = decodeBase64AccountData(positionAccountInfo);
|
||||
|
||||
byte[] poolIdBytes = extractConcentratedLiquidityPoolIdBytes(accountData);
|
||||
ΩRaydiumLiquidityPoolConcentratedIdΩ liquidityPoolId = solanaBlockChain.encodeSolanaAddress(poolIdBytes);
|
||||
|
||||
liquidityPoolIds.add(liquidityPoolId);
|
||||
}
|
||||
|
||||
return Set.copyOf(liquidityPoolIds);
|
||||
}
|
||||
|
||||
private Set<ΩRaydiumLiquidityPoolStandardIdΩ> extractStandardLiquidityPoolIds(JsonNode root) throws IOException {
|
||||
JsonNode data = root.path("data");
|
||||
if (!data.isArray()) {
|
||||
throw new IOException("Returned JSON do NOT contain a valid data-array!");
|
||||
}
|
||||
|
||||
Set<ΩRaydiumLiquidityPoolStandardIdΩ> liquidityPoolIds = new HashSet<>();
|
||||
|
||||
for (JsonNode poolNode : data) {
|
||||
if (poolNode == null || poolNode.isNull()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
JsonNode idNode = poolNode.path("id");
|
||||
|
||||
if (idNode.isMissingNode() || !idNode.isTextual()) {
|
||||
throw new IOException("Could NOT find textual field 'id' in pool JSON!");
|
||||
}
|
||||
|
||||
liquidityPoolIds.add(idNode.asText());
|
||||
}
|
||||
|
||||
return Set.copyOf(liquidityPoolIds);
|
||||
}
|
||||
|
||||
private ΩRestEndpointΩ createPoolInfoByIdEndpoint(ΩRaydiumLiquidityPoolIdΩ raydiumLiquidityPoolId) {
|
||||
return ΩRestEndpointΩ.create(RAYDIUM_API_V3_BASE_URI + "/pools/info/ids?ids=" + raydiumLiquidityPoolId);
|
||||
}
|
||||
|
||||
private JsonNode fetchJson(ΩRestEndpointΩ restEndpoint) throws IOException, InterruptedException {
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(restEndpoint)
|
||||
.GET()
|
||||
.header("Accept", "application/json")
|
||||
.build();
|
||||
|
||||
HttpResponse<String> response = sendThrottled(request);
|
||||
|
||||
if (response.statusCode() != 200) {
|
||||
throw new IOException("HTTP error: " + response.statusCode() + ", body: " + response.body());
|
||||
}
|
||||
|
||||
return objectMapper.readTree(response.body());
|
||||
}
|
||||
|
||||
private synchronized HttpResponse<String> sendThrottled(HttpRequest request) throws IOException, InterruptedException {
|
||||
waitBeforeRemoteCall();
|
||||
|
||||
try {
|
||||
return httpClient.send(
|
||||
request,
|
||||
HttpResponse.BodyHandlers.ofString()
|
||||
);
|
||||
} finally {
|
||||
previousRemoteCallTime = System.currentTimeMillis();
|
||||
}
|
||||
}
|
||||
|
||||
private void waitBeforeRemoteCall() throws InterruptedException {
|
||||
long now = System.currentTimeMillis();
|
||||
long elapsed = now - previousRemoteCallTime;
|
||||
|
||||
if (elapsed < MINIMUM_REMOTE_CALL_INTERVAL) {
|
||||
ΩmilliSecondsΩ sleepTime = MINIMUM_REMOTE_CALL_INTERVAL - elapsed;
|
||||
//System.out.println("Throttling Raydium request for " + sleepTime + "ms...");
|
||||
Thread.sleep(sleepTime);
|
||||
//System.out.println("Ready");
|
||||
}
|
||||
}
|
||||
|
||||
private ΩRestEndpointΩ createPoolInfoByCandidateLpMintsEndpoint(Set<ΩSPLMintAddressΩ> candidateLpMintAddresses) {
|
||||
String joinedLpMintAddresses = String.join(
|
||||
",",
|
||||
candidateLpMintAddresses.stream()
|
||||
.map(Object::toString)
|
||||
.toList()
|
||||
);
|
||||
|
||||
return ΩRestEndpointΩ.create(RAYDIUM_API_V3_BASE_URI + "/pools/info/lps?lps=" + joinedLpMintAddresses);
|
||||
}
|
||||
|
||||
private byte[] decodeBase64AccountData(SolanaAccountInfo accountInfo) throws IOException {
|
||||
try {
|
||||
return Base64.getDecoder().decode(accountInfo.dataBase64());
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new IOException("Could not decode Solana account data as Base64 for account: " + accountInfo.address(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] extractConcentratedLiquidityPoolIdBytes(byte[] accountData) throws IOException {
|
||||
ensureAccountDataContains(
|
||||
accountData,
|
||||
RAYDIUM_POSITION_ACCOUNT_POOL_ID_OFFSET,
|
||||
SOLANA_ADDRESS_LENGTH,
|
||||
"pool id"
|
||||
);
|
||||
|
||||
byte[] poolIdBytes = new byte[SOLANA_ADDRESS_LENGTH];
|
||||
|
||||
System.arraycopy(
|
||||
accountData,
|
||||
RAYDIUM_POSITION_ACCOUNT_POOL_ID_OFFSET,
|
||||
poolIdBytes,
|
||||
0,
|
||||
SOLANA_ADDRESS_LENGTH
|
||||
);
|
||||
|
||||
return poolIdBytes;
|
||||
}
|
||||
|
||||
private ΩraydiumLiquidityTickIndexΩ extractConcentratedLiquidityTickIndex(
|
||||
byte[] accountData,
|
||||
int offset,
|
||||
String fieldName
|
||||
) throws IOException {
|
||||
ensureAccountDataContains(
|
||||
accountData,
|
||||
offset,
|
||||
RAYDIUM_POSITION_ACCOUNT_TICK_INDEX_LENGTH,
|
||||
fieldName
|
||||
);
|
||||
|
||||
return (accountData[offset] & 0xFF)
|
||||
| ((accountData[offset + 1] & 0xFF) << 8)
|
||||
| ((accountData[offset + 2] & 0xFF) << 16)
|
||||
| ((accountData[offset + 3] & 0xFF) << 24);
|
||||
}
|
||||
|
||||
private ΩRaydiumLiquidityΩ extractConcentratedLiquidity(byte[] accountData) throws IOException {
|
||||
return extractUnsigned128LittleEndian(
|
||||
accountData,
|
||||
RAYDIUM_POSITION_ACCOUNT_LIQUIDITY_OFFSET,
|
||||
"liquidity"
|
||||
);
|
||||
}
|
||||
|
||||
private ΩRaydiumLiquidityΩ extractConcentratedPoolLiquidity(byte[] accountData) throws IOException {
|
||||
return extractUnsigned128LittleEndian(
|
||||
accountData,
|
||||
RAYDIUM_POOL_STATE_LIQUIDITY_OFFSET,
|
||||
"liquidity"
|
||||
);
|
||||
}
|
||||
|
||||
private ΩRaydiumSqrtPriceX64Ω extractConcentratedPoolSqrtPriceX64(byte[] accountData) throws IOException {
|
||||
return extractUnsigned128LittleEndian(
|
||||
accountData,
|
||||
RAYDIUM_POOL_STATE_SQRT_PRICE_X64_OFFSET,
|
||||
"sqrtPriceX64"
|
||||
);
|
||||
}
|
||||
|
||||
private BigInteger extractUnsigned128LittleEndian(
|
||||
byte[] accountData,
|
||||
int offset,
|
||||
String fieldName
|
||||
) throws IOException {
|
||||
ensureAccountDataContains(
|
||||
accountData,
|
||||
offset,
|
||||
RAYDIUM_U128_LENGTH,
|
||||
fieldName
|
||||
);
|
||||
|
||||
byte[] bigEndianBytes = new byte[RAYDIUM_U128_LENGTH + 1];
|
||||
|
||||
for (int i = 0; i < RAYDIUM_U128_LENGTH; i++) {
|
||||
bigEndianBytes[bigEndianBytes.length - 1 - i] = accountData[offset + i];
|
||||
}
|
||||
|
||||
return new BigInteger(bigEndianBytes);
|
||||
}
|
||||
|
||||
private void ensureAccountDataContains(
|
||||
byte[] accountData,
|
||||
int offset,
|
||||
int length,
|
||||
String fieldName
|
||||
) throws IOException {
|
||||
if (accountData.length < offset + length) {
|
||||
throw new IOException("Raydium position account data is too short to contain " + fieldName + ".");
|
||||
}
|
||||
}
|
||||
|
||||
private JsonNode getFirstPoolNode(JsonNode root) throws IOException {
|
||||
JsonNode data = root.path("data");
|
||||
|
||||
if (!data.isArray() || data.isEmpty()) {
|
||||
throw new IOException("Returned JSON do NOT contain a valid data-array!");
|
||||
}
|
||||
|
||||
JsonNode firstPool = data.get(0);
|
||||
|
||||
if (firstPool == null || firstPool.isNull()) {
|
||||
throw new IOException("Returned JSON data-array did NOT contain a pool!");
|
||||
}
|
||||
|
||||
return firstPool;
|
||||
}
|
||||
|
||||
private record RaydiumStandardPoolInfo(
|
||||
ΩRaydiumLiquidityPoolStandardIdΩ poolId,
|
||||
ΩSPLMintAddressΩ mintA,
|
||||
ΩAmountΩ amountA,
|
||||
ΩSPLMintAddressΩ mintB,
|
||||
ΩAmountΩ amountB,
|
||||
ΩSPLMintAddressΩ lpMint,
|
||||
ΩAmountΩ totalLpAmount
|
||||
) {
|
||||
}
|
||||
|
||||
private static final ΩRestEndpointBaseΩ RAYDIUM_API_V3_BASE_URI = ΩRestEndpointBaseΩ.create("https://api-v3.raydium.io");
|
||||
private static final ΩRaydiumProgramIdΩ RAYDIUM_CLMM_PROGRAM_ID = "CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK";
|
||||
private static final ΩmilliSecondsΩ MINIMUM_REMOTE_CALL_INTERVAL = 5000L;
|
||||
private static final BigDecimal RAYDIUM_TICK_BASE = new BigDecimal("1.0001");
|
||||
private static final BigDecimal RAYDIUM_Q64_FACTOR = new BigDecimal(BigInteger.ONE.shiftLeft(64));
|
||||
private static final MathContext RAYDIUM_PRICE_MATH_CONTEXT = MathContext.DECIMAL128;
|
||||
|
||||
/**
|
||||
* Byte layout used when decoding Raydium CLMM PersonalPositionState account data.
|
||||
*
|
||||
* <p>This layout is based on Raydium's on-chain {@code PersonalPositionState}
|
||||
* account. The account starts with the 8-byte Anchor discriminator, followed by
|
||||
* the packed position fields. The fields decoded here are the NFT mint, pool id,
|
||||
* lower/upper tick indexes and position liquidity.</p>
|
||||
*
|
||||
* <pre>
|
||||
* 0 - 8 Anchor discriminator
|
||||
* 8 - 9 bump
|
||||
* 9 - 41 nft_mint
|
||||
* 41 - 73 pool_id
|
||||
* 73 - 77 tick_lower_index i32 little-endian
|
||||
* 77 - 81 tick_upper_index i32 little-endian
|
||||
* 81 - 97 liquidity u128 little-endian
|
||||
* </pre>
|
||||
*
|
||||
* <p>If Raydium changes the on-chain {@code PersonalPositionState} layout, these
|
||||
* offsets must be checked and updated before decoding position state.</p>
|
||||
*/
|
||||
private static final int SOLANA_ADDRESS_LENGTH = 32;
|
||||
private static final int RAYDIUM_POSITION_ACCOUNT_DISCRIMINATOR_LENGTH = 8;
|
||||
private static final int RAYDIUM_POSITION_ACCOUNT_BUMP_LENGTH = 1;
|
||||
private static final int RAYDIUM_POSITION_ACCOUNT_NFT_MINT_OFFSET =
|
||||
RAYDIUM_POSITION_ACCOUNT_DISCRIMINATOR_LENGTH + RAYDIUM_POSITION_ACCOUNT_BUMP_LENGTH;
|
||||
private static final int RAYDIUM_POSITION_ACCOUNT_POOL_ID_OFFSET =
|
||||
RAYDIUM_POSITION_ACCOUNT_NFT_MINT_OFFSET + SOLANA_ADDRESS_LENGTH;
|
||||
private static final int RAYDIUM_POSITION_ACCOUNT_TICK_INDEX_LENGTH = 4;
|
||||
private static final int RAYDIUM_POSITION_ACCOUNT_TICK_LOWER_INDEX_OFFSET =
|
||||
RAYDIUM_POSITION_ACCOUNT_POOL_ID_OFFSET + SOLANA_ADDRESS_LENGTH;
|
||||
private static final int RAYDIUM_POSITION_ACCOUNT_TICK_UPPER_INDEX_OFFSET =
|
||||
RAYDIUM_POSITION_ACCOUNT_TICK_LOWER_INDEX_OFFSET + RAYDIUM_POSITION_ACCOUNT_TICK_INDEX_LENGTH;
|
||||
private static final int RAYDIUM_U128_LENGTH = 16;
|
||||
private static final int RAYDIUM_POSITION_ACCOUNT_LIQUIDITY_OFFSET =
|
||||
RAYDIUM_POSITION_ACCOUNT_TICK_UPPER_INDEX_OFFSET + RAYDIUM_POSITION_ACCOUNT_TICK_INDEX_LENGTH;
|
||||
|
||||
/**
|
||||
* Byte layout used when decoding Raydium CLMM PoolState account data.
|
||||
*
|
||||
* <p>This layout is based on Raydium's on-chain {@code PoolState} account.
|
||||
* The account starts with the 8-byte Anchor discriminator, followed by the packed
|
||||
* {@code PoolState} fields. The fields decoded here are pool liquidity,
|
||||
* current square-root price in Q64.64 format, and current tick index.</p>
|
||||
*
|
||||
* <pre>
|
||||
* 0 - 8 Anchor discriminator
|
||||
* 8 - 9 bump
|
||||
* 9 - 41 amm_config
|
||||
* 41 - 73 owner
|
||||
* 73 - 105 token_mint_0
|
||||
* 105 - 137 token_mint_1
|
||||
* 137 - 169 token_vault_0
|
||||
* 169 - 201 token_vault_1
|
||||
* 201 - 233 observation_key
|
||||
* 233 - 234 mint_decimals_0
|
||||
* 234 - 235 mint_decimals_1
|
||||
* 235 - 237 tick_spacing
|
||||
* 237 - 253 liquidity u128 little-endian
|
||||
* 253 - 269 sqrt_price_x64 u128 little-endian
|
||||
* 269 - 273 tick_current i32 little-endian
|
||||
* </pre>
|
||||
*
|
||||
* <p>If Raydium changes the on-chain {@code PoolState} layout, these offsets must
|
||||
* be checked and updated before decoding pool state.</p>
|
||||
*/
|
||||
private static final int RAYDIUM_POOL_STATE_LIQUIDITY_OFFSET = 237;
|
||||
private static final int RAYDIUM_POOL_STATE_SQRT_PRICE_X64_OFFSET =
|
||||
RAYDIUM_POOL_STATE_LIQUIDITY_OFFSET + RAYDIUM_U128_LENGTH;
|
||||
private static final int RAYDIUM_POOL_STATE_TICK_CURRENT_OFFSET =
|
||||
RAYDIUM_POOL_STATE_SQRT_PRICE_X64_OFFSET + RAYDIUM_U128_LENGTH;
|
||||
|
||||
private final SolanaBlockChain solanaBlockChain;
|
||||
private final HttpClient httpClient;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
private ΩmilliSecondsΩ previousRemoteCallTime = 0L;
|
||||
}
|
||||
Reference in New Issue
Block a user