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-core:2.26.0")
|
||||||
runtimeOnly("org.apache.logging.log4j:log4j-slf4j2-impl: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("org.slf4j:slf4j-api:2.0.18")
|
||||||
implementation("com.r35157.nenjim:hubd-api:0.1-dev")
|
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