89: Review
This commit is contained in:
@@ -57,13 +57,16 @@ identities come from CIS; token programs and precision come from chain metadata.
|
||||
A single worker runs immediately on startup and then with fixed delay after
|
||||
completion. Every iteration follows the same three independent steps:
|
||||
|
||||
1. Read current on-chain pool state in SyrupUSDC per EVE. For each owned,
|
||||
positive-liquidity position in the configured pool, withdraw complete
|
||||
1. Discover the wallet's position NFTs. For each owned, positive-liquidity
|
||||
position in the configured pool, read fresh on-chain pool state in
|
||||
SyrupUSDC per EVE after discovery and any earlier withdrawals. Withdraw complete
|
||||
liquidity when its normalized **upper** boundary satisfies
|
||||
`U <= P * (1 - h/100)`. Equality qualifies. Both token orientations use
|
||||
exact squared Q64.64 comparisons, without display-price rounding. At
|
||||
`P=15, h=10`, `13–13.5` qualifies and `13.5–14` does not. Withdrawals retain
|
||||
the position and NFT, and return pool-token fees to the same wallet.
|
||||
Each position uses its own fresh state for both eligibility and withdrawal
|
||||
minimums; the price is not reused across positions.
|
||||
2. Read current SyrupUSDC balance and swap the full balance above its reserve
|
||||
into USDC through the confirmed exact-input Jupiter operation.
|
||||
3. Read current USDC balance and send the full balance above its reserve to
|
||||
@@ -71,8 +74,8 @@ completion. Every iteration follows the same three independent steps:
|
||||
The transfer submits once and is awaited at `CONFIRMED`.
|
||||
|
||||
Missing/zero balances and balances equal to their reserves are skipped. Amounts
|
||||
are rounded down to mint precision. An unusable positive market price skips
|
||||
withdrawals while retaining steps 2 and 3. EVE returned as principal/fees and
|
||||
are rounded down to mint precision. An unavailable or unusable pool price stops
|
||||
the remaining withdrawals in that iteration while retaining steps 2 and 3. EVE returned as principal/fees and
|
||||
other reward tokens remain in the wallet and never enter the conversion/payout
|
||||
pipeline. All available SyrupUSDC/USDC is eligible, including manual deposits.
|
||||
|
||||
@@ -98,14 +101,21 @@ Withdrawal and closure are separate actions in
|
||||
|
||||
Raydium validates pool/personal-position discriminators, the owned NFT holding,
|
||||
current complete liquidity, mint precision, vaults and tick arrays before signing.
|
||||
Missing destination ATAs are created idempotently in the same transaction,
|
||||
which requests a 600,000 compute-unit limit.
|
||||
For every pool or reward mint, existing owner token accounts are reused after
|
||||
validating their mint, owner, token program and initialized state, including
|
||||
empty accounts and accounts outside the owner's ATA. Only when no owner holding
|
||||
exists does the withdrawal use or idempotently create its ATA. A failed holdings
|
||||
read aborts rather than creating an additional account. This preserves the
|
||||
existing wallet's single-account-per-mint balance/transfer contract. The
|
||||
transaction requests a 600,000 compute-unit limit.
|
||||
The bitmap extension and initialized reward triplets are included when required.
|
||||
The integer principal math matches Raydium's
|
||||
[tick algorithm](https://github.com/raydium-io/raydium-clmm/blob/master/programs/amm/src/libraries/tick_math.rs).
|
||||
Minimum principal amounts are floored after applying explicit withdrawal
|
||||
slippage. An amount too small to protect at least one raw unit is rejected.
|
||||
No withdrawal always substitutes zero for both minimums.
|
||||
Price movement after the per-position read remains possible before execution;
|
||||
principal minimums protect received amounts, not the harvest-distance threshold.
|
||||
|
||||
The current EVE mint uses Token-2022 metadata pointer/token metadata extensions;
|
||||
SyrupUSDC uses legacy SPL. Read-only mainnet inspection on 2026-10-04 found the
|
||||
@@ -122,7 +132,10 @@ The Gradle JUnit fixtures exercise exact threshold equality, inversion, raw
|
||||
principal amounts, owner/pool filtering, reserves, independent failures,
|
||||
restart recovery and shutdown. Captured serialized transactions verify account
|
||||
order, full liquidity, minimum output, NFT retention, bitmap/reward accounts and
|
||||
one submission/confirmation attempt. All financial dependencies are controlled;
|
||||
one submission/confirmation attempt. Review regression fixtures also cover price
|
||||
changes during discovery or earlier withdrawals in both token orientations,
|
||||
quote consistency, and reuse/validation of existing pool and reward token accounts
|
||||
under both token programs. All financial dependencies are controlled;
|
||||
no verification submits live withdrawals, swaps or transfers.
|
||||
|
||||
```sh
|
||||
|
||||
+11
-4
@@ -85,15 +85,22 @@ public final class EvelynLiquidityServiceImpl implements EvelynLiquidityService,
|
||||
}
|
||||
|
||||
private void withdrawEligiblePositions() throws Exception {
|
||||
for (ΩRaydiumLiquidityPoolPositionNftIdΩ nft : raydium.fetchConcentratedPositionNftIds(wallet.getAddress())) {
|
||||
checkInterrupted();
|
||||
RaydiumConcentratedPositionState position;
|
||||
try {
|
||||
position = raydium.fetchConcentratedPositionState(nft);
|
||||
if (!nft.equals(position.nftId()) || !configuration.poolId().equals(position.poolId()) || position.liquidity().signum() <= 0) continue;
|
||||
} catch (InterruptedException exception) { throw exception; }
|
||||
catch (Exception exception) { log.warn("withdraw position={} failed", nft, exception); continue; }
|
||||
|
||||
// Discovery and earlier withdrawals may take time; never reuse their price.
|
||||
// A failed pool read ends withdrawals for this iteration, leaving later steps independent.
|
||||
RaydiumConcentratedPoolState state = raydium.fetchConcentratedPoolState(configuration.poolId());
|
||||
if (!configuration.poolId().equals(state.poolId()) || state.sqrtPriceX64() == null || state.sqrtPriceX64().signum() <= 0) {
|
||||
throw new IOException("Unusable current CLMM pool price");
|
||||
}
|
||||
for (ΩRaydiumLiquidityPoolPositionNftIdΩ nft : raydium.fetchConcentratedPositionNftIds(wallet.getAddress())) {
|
||||
checkInterrupted();
|
||||
try {
|
||||
RaydiumConcentratedPositionState position = raydium.fetchConcentratedPositionState(nft);
|
||||
if (!nft.equals(position.nftId()) || !configuration.poolId().equals(position.poolId()) || position.liquidity().signum() <= 0) continue;
|
||||
if (!RaydiumClmmMath.isAtOrBelowHarvestThreshold(position, state, baseIsMintA, configuration.harvestDistancePercent())) continue;
|
||||
log.info("withdraw position={} liquidity={}", nft, position.liquidity());
|
||||
ΩSolanaTransactionSignatureΩ signature = raydium.withdrawConcentratedPositionLiquidity(wallet, position, state, configuration.maximumWithdrawalSlippageBps());
|
||||
|
||||
@@ -20,6 +20,8 @@ public interface Raydium extends NenjimComponent {
|
||||
* Withdraws the snapshot's full liquidity and fees to its NFT owner, preserving
|
||||
* the NFT, position and token accounts. Minimums use quoteState and explicit
|
||||
* slippage in [0,10000); the current position must still match the snapshot.
|
||||
* Reuses validated owner token holdings, including non-associated accounts;
|
||||
* creates destination ATAs only when no owner holding exists.
|
||||
* Submits once and awaits CONFIRMED. Failures after submission may be unknown.
|
||||
*/
|
||||
@org.jetbrains.annotations.NotNull
|
||||
|
||||
@@ -210,6 +210,13 @@ final class RaydiumClmmWithdrawal {
|
||||
|
||||
private ΩSPLTokenAccountΩ destination(ΩSolanaWalletIdΩ wallet, ΩSPLMintAddressΩ mint, SolanaSPLTokenProgram program, List<Instruction> instructions)
|
||||
throws IOException, InterruptedException {
|
||||
// Preserve the existing wallet's single-account-per-mint contract, even for non-ATAs.
|
||||
// Failed discovery must abort rather than risk creating a second owner account.
|
||||
SPLTokenHolding holding = chain.getSPLTokenHoldings(wallet, program).get(mint);
|
||||
if (holding != null) {
|
||||
validateTokenAccount(holding.tokenAccount(), mint, wallet, program);
|
||||
return holding.tokenAccount();
|
||||
}
|
||||
ΩSPLTokenAccountΩ ata = chain.findAssociatedTokenAccount(wallet, mint, program);
|
||||
if (chain.getAccountInfo(ata) == null) {
|
||||
instructions.add(new Instruction(ATA_PROGRAM, List.of(new Meta(wallet, true, true),
|
||||
|
||||
+69
-1
@@ -106,6 +106,68 @@ class EvelynLiquidityServiceImplTest {
|
||||
assertTrue(fixture.positions.get("owned").liquidity().signum() > 0);
|
||||
}
|
||||
}
|
||||
@Test void priceDeclineDuringDiscoveryPreservesLiquidityInBothOrientations() throws Exception {
|
||||
for (boolean reversed : List.of(false, true)) {
|
||||
Fixture fixture = new Fixture(); fixture.reversed = reversed;
|
||||
fixture.sqrt = RaydiumClmmMath.sqrtPriceAtTick(reversed ? -1200 : 1200);
|
||||
fixture.afterDiscovery = () -> fixture.sqrt = RaydiumClmmMath.sqrtPriceAtTick(reversed ? -600 : 600);
|
||||
fixture.balances.put(SYRUP, BigDecimal.ONE); fixture.balances.put(USDC, new BigDecimal("2"));
|
||||
try (var service = fixture.service("0", "0", "10")) {
|
||||
service.initialize(); service.runIteration();
|
||||
assertEquals(List.of("swap:1.000000", "send:2.000000"), fixture.operations);
|
||||
assertEquals(BigInteger.valueOf(1_000_000), fixture.positions.get("owned").liquidity());
|
||||
assertEquals(1, fixture.poolReads);
|
||||
}
|
||||
}
|
||||
}
|
||||
@Test void priceDeclineDuringWithdrawalPreservesLaterPositionInBothOrientations() throws Exception {
|
||||
for (boolean reversed : List.of(false, true)) {
|
||||
Fixture fixture = new Fixture(); fixture.reversed = reversed;
|
||||
fixture.positions.put("later", new RaydiumConcentratedPositionState("later", POOL, -100, 0, BigInteger.valueOf(1_000_000)));
|
||||
fixture.sqrt = RaydiumClmmMath.sqrtPriceAtTick(reversed ? -1200 : 1200);
|
||||
fixture.afterWithdrawal = () -> fixture.sqrt = RaydiumClmmMath.sqrtPriceAtTick(reversed ? -600 : 600);
|
||||
try (var service = fixture.service("0", "0", "10")) {
|
||||
service.initialize(); service.runIteration();
|
||||
assertEquals(List.of("withdraw:owned"), fixture.operations);
|
||||
assertEquals(BigInteger.ZERO, fixture.positions.get("owned").liquidity());
|
||||
assertEquals(BigInteger.valueOf(1_000_000), fixture.positions.get("later").liquidity());
|
||||
assertEquals(RaydiumClmmMath.sqrtPriceAtTick(reversed ? -1200 : 1200), fixture.withdrawQuote.sqrtPriceX64());
|
||||
assertEquals(2, fixture.poolReads);
|
||||
}
|
||||
}
|
||||
}
|
||||
@Test void priceRiseUsesTheNewStateForEligibilityAndWithdrawalQuote() throws Exception {
|
||||
for (boolean reversed : List.of(false, true)) {
|
||||
Fixture fixture = new Fixture(); fixture.reversed = reversed;
|
||||
fixture.positions.put("later", new RaydiumConcentratedPositionState("later", POOL,
|
||||
reversed ? -300 : 100, reversed ? -200 : 200, BigInteger.valueOf(1_000_000)));
|
||||
fixture.sqrt = RaydiumClmmMath.sqrtPriceAtTick(reversed ? -1200 : 1200);
|
||||
fixture.afterWithdrawal = () -> fixture.sqrt = RaydiumClmmMath.sqrtPriceAtTick(reversed ? -1800 : 1800);
|
||||
try (var service = fixture.service("0", "0", "10")) {
|
||||
service.initialize(); service.runIteration();
|
||||
assertEquals(List.of("withdraw:owned", "withdraw:later"), fixture.operations);
|
||||
assertEquals(RaydiumClmmMath.sqrtPriceAtTick(reversed ? -1800 : 1800), fixture.withdrawQuote.sqrtPriceX64());
|
||||
assertEquals(2, fixture.poolReads);
|
||||
}
|
||||
}
|
||||
}
|
||||
@Test void unusablePriceBetweenPositionsStopsWithdrawalsAndKeepsLaterStepsIndependent() throws Exception {
|
||||
for (boolean readFails : List.of(false, true)) {
|
||||
Fixture fixture = new Fixture(); fixture.sqrt = RaydiumClmmMath.sqrtPriceAtTick(1200);
|
||||
for (String nft : List.of("later", "last")) {
|
||||
fixture.positions.put(nft, new RaydiumConcentratedPositionState(nft, POOL, -100, 0, BigInteger.valueOf(1_000_000)));
|
||||
}
|
||||
fixture.afterWithdrawal = () -> { fixture.priceFails = readFails; fixture.sqrt = BigInteger.ZERO; };
|
||||
fixture.balances.put(SYRUP, BigDecimal.ONE); fixture.balances.put(USDC, new BigDecimal("2"));
|
||||
try (var service = fixture.service("0", "0", "10")) {
|
||||
service.initialize(); service.runIteration();
|
||||
assertEquals(List.of("withdraw:owned", "swap:1.000000", "send:2.000000"), fixture.operations);
|
||||
assertTrue(fixture.positions.get("later").liquidity().signum() > 0);
|
||||
assertTrue(fixture.positions.get("last").liquidity().signum() > 0);
|
||||
assertEquals(2, fixture.poolReads);
|
||||
}
|
||||
}
|
||||
}
|
||||
@Test void invalidIdentityPrecisionAndBindingFailBeforeWorker() throws Exception {
|
||||
Fixture fixture = new Fixture();
|
||||
try (var service = fixture.service("0.0000001", "0", "10")) {
|
||||
@@ -187,6 +249,9 @@ class EvelynLiquidityServiceImplTest {
|
||||
Object creationPolicy;
|
||||
SolanaWallet withdrawWallet;
|
||||
RaydiumConcentratedPositionState withdrawPosition;
|
||||
RaydiumConcentratedPoolState withdrawQuote;
|
||||
int poolReads;
|
||||
Runnable afterDiscovery = () -> { }, afterWithdrawal = () -> { };
|
||||
final SolanaWallet wallet = proxy(SolanaWallet.class, (method, args) -> switch (method) {
|
||||
case "getAddress" -> walletAddress;
|
||||
case "getSPLTokenBalance" -> { balanceReads.add((String) args[0]); yield balances.get(args[0]); }
|
||||
@@ -217,15 +282,17 @@ class EvelynLiquidityServiceImplTest {
|
||||
final Raydium raydium = proxy(Raydium.class, (method, args) -> switch (method) {
|
||||
case "fetchConcentratedPoolInfoOnChain" -> new RaydiumConcentratedPoolInfo(POOL, wrongPool ? USDC : reversed ? SYRUP : EVE, reversed ? 6 : 8, reversed ? EVE : SYRUP, reversed ? 8 : 6, BigDecimal.ONE);
|
||||
case "fetchConcentratedPoolState" -> {
|
||||
poolReads++;
|
||||
if (priceFails) throw new IOException("fixture pool price failure");
|
||||
yield new RaydiumConcentratedPoolState(POOL, BigInteger.ONE, sqrt, 0);
|
||||
}
|
||||
case "fetchConcentratedPositionNftIds" -> { assertEquals(OWNER, args[0]); yield new LinkedHashSet<>(positions.keySet()); }
|
||||
case "fetchConcentratedPositionNftIds" -> { assertEquals(OWNER, args[0]); afterDiscovery.run(); yield new LinkedHashSet<>(positions.keySet()); }
|
||||
case "fetchConcentratedPositionState" -> {
|
||||
if (args[0].equals(unreadable)) throw new IOException("fixture unreadable position"); yield positions.get(args[0]);
|
||||
}
|
||||
case "withdrawConcentratedPositionLiquidity" -> {
|
||||
withdrawWallet = (SolanaWallet) args[0]; withdrawPosition = (RaydiumConcentratedPositionState) args[1]; withdrawSlippage = (int) args[3];
|
||||
withdrawQuote = (RaydiumConcentratedPoolState) args[2];
|
||||
operations.add("withdraw:" + withdrawPosition.nftId());
|
||||
if (interruptWithdraw) throw new InterruptedException("fixture interrupted");
|
||||
if (blockWithdraw) {
|
||||
@@ -236,6 +303,7 @@ class EvelynLiquidityServiceImplTest {
|
||||
if (withdrawFails) throw new IOException("fixture withdrawal failure");
|
||||
if(creditProceeds) balances.merge(SYRUP,new BigDecimal("3.04"),BigDecimal::add);
|
||||
positions.put(withdrawPosition.nftId(), new RaydiumConcentratedPositionState(withdrawPosition.nftId(), POOL, -100, 0, BigInteger.ZERO));
|
||||
afterWithdrawal.run();
|
||||
yield "withdrawal-signature";
|
||||
}
|
||||
default -> throw new AssertionError("Unexpected Raydium operation: " + method);
|
||||
|
||||
@@ -118,6 +118,69 @@ class RaydiumClmmWithdrawalTest {
|
||||
assertEquals(NFT_ACCOUNT,transaction.instructions.getLast().accounts.get(1));
|
||||
assertFalse(transaction.writable.contains(NFT_ACCOUNT));
|
||||
}
|
||||
@Test void existingOwnerAccountsUnderBothProgramsAreReusedEvenWhenEmpty() throws Exception {
|
||||
for (boolean associated : List.of(false, true)) {
|
||||
Fixture fixture = new Fixture();
|
||||
String eveAccount = associated ? fixture.ataA : address(40);
|
||||
String syrupAccount = associated ? fixture.ataB : address(41);
|
||||
fixture.holding(eveAccount, EVE, SolanaSPLTokenProgram.TOKEN_2022_PROGRAM, 8, BigDecimal.ZERO);
|
||||
fixture.holding(syrupAccount, SYRUP, SolanaSPLTokenProgram.SPL_TOKEN_PROGRAM, 6, BigDecimal.ONE);
|
||||
fixture.operation().withdraw(fixture.wallet, fixture.position, fixture.quote, 100);
|
||||
Parsed transaction = parse(fixture.transaction);
|
||||
assertEquals(2, transaction.instructions.size()); // Compute budget and decrease, no new accounts.
|
||||
ParsedInstruction decrease = transaction.instructions.getLast();
|
||||
assertEquals(eveAccount, decrease.accounts.get(9));
|
||||
assertEquals(syrupAccount, decrease.accounts.get(10));
|
||||
assertEquals(syrupAccount, fixture.chain.getSPLTokenHoldings(OWNER, SolanaSPLTokenProgram.SPL_TOKEN_PROGRAM).get(SYRUP).tokenAccount());
|
||||
if (!associated) {
|
||||
assertFalse(transaction.keys.contains(fixture.ataA));
|
||||
assertFalse(transaction.keys.contains(fixture.ataB));
|
||||
}
|
||||
assertEquals(1, fixture.submissions);
|
||||
}
|
||||
}
|
||||
@Test void existingRewardAccountIsReusedUnderEitherTokenProgram() throws Exception {
|
||||
for (SolanaSPLTokenProgram program : SolanaSPLTokenProgram.values()) {
|
||||
Fixture fixture = new Fixture();
|
||||
String reward = address(42), vault = address(43), destination = address(44);
|
||||
byte[] pool = fixture.data(POOL);
|
||||
addressInto(pool, 397 + 57, reward); addressInto(pool, 397 + 89, vault);
|
||||
fixture.put(POOL, RaydiumClmmWithdrawal.PROGRAM, pool);
|
||||
fixture.put(reward, program.getAddress(), mint(6, program == SolanaSPLTokenProgram.TOKEN_2022_PROGRAM));
|
||||
fixture.put(vault, program.getAddress(), token(reward, POOL));
|
||||
fixture.holding(destination, reward, program, 6, BigDecimal.ZERO);
|
||||
fixture.operation().withdraw(fixture.wallet, fixture.position, fixture.quote, 100);
|
||||
Parsed transaction = parse(fixture.transaction);
|
||||
ParsedInstruction decrease = transaction.instructions.getLast();
|
||||
assertEquals(vault, decrease.accounts.get(16));
|
||||
assertEquals(destination, decrease.accounts.get(17));
|
||||
assertEquals(reward, decrease.accounts.get(18));
|
||||
assertFalse(transaction.keys.contains(fixture.math.findAssociatedTokenAccount(OWNER, reward, program)));
|
||||
assertEquals(4, transaction.instructions.size()); // Only the two absent pool-token ATAs.
|
||||
}
|
||||
}
|
||||
@Test void invalidExistingOwnerAccountFailsBeforeSigningWithoutReplacement() throws Exception {
|
||||
for (int scenario = 0; scenario < 5; scenario++) {
|
||||
Fixture fixture = new Fixture(); String existing = address(45);
|
||||
fixture.holding(existing, SYRUP, SolanaSPLTokenProgram.SPL_TOKEN_PROGRAM, 6, BigDecimal.ONE);
|
||||
byte[] data = fixture.data(existing); String program = LEGACY;
|
||||
switch (scenario) {
|
||||
case 0 -> addressInto(data, 32, address(46));
|
||||
case 1 -> addressInto(data, 0, EVE);
|
||||
case 2 -> program = TOKEN2022;
|
||||
case 3 -> data[108] = 0;
|
||||
case 4 -> data[108] = 2;
|
||||
}
|
||||
fixture.put(existing, program, data);
|
||||
assertThrows(IOException.class, () -> fixture.operation().withdraw(fixture.wallet, fixture.position, fixture.quote, 100));
|
||||
assertEquals(0, fixture.submissions); assertNull(fixture.transaction);
|
||||
}
|
||||
}
|
||||
@Test void failedDestinationHoldingsReadDoesNotFallBackToCreatingAnAta() throws Exception {
|
||||
Fixture fixture = new Fixture(); fixture.unreadableHoldingsProgram = TOKEN2022;
|
||||
assertThrows(IOException.class, () -> fixture.operation().withdraw(fixture.wallet, fixture.position, fixture.quote, 100));
|
||||
assertEquals(0, fixture.submissions); assertNull(fixture.transaction);
|
||||
}
|
||||
@Test void discoveryIsolatesCandidateAndTokenProgramReadFailures() throws Exception {
|
||||
Fixture fixture = new Fixture();
|
||||
String badNft=address(31);fixture.unreadablePosition=fixture.pda("position",SolanaProgramAddressSeed.solanaAddress(badNft));
|
||||
@@ -137,12 +200,13 @@ class RaydiumClmmWithdrawalTest {
|
||||
private static final class Fixture {
|
||||
final SolanaBlockChainImpl math = new SolanaBlockChainImpl(HttpClient.newHttpClient(), new ObjectMapper(), new HardcodedCurrencyIdentityService());
|
||||
final Map<String, SolanaAccountInfo> accounts = new HashMap<>();
|
||||
final Map<String, SPLTokenHolding> ownerHoldings = new HashMap<>();
|
||||
String personal, protocol, lowerArray, upperArray, ataA, ataB;
|
||||
RaydiumConcentratedPositionState position;
|
||||
RaydiumConcentratedPoolState quote;
|
||||
SolanaUnsignedTransaction transaction;
|
||||
boolean ownsNft = true, timeout, legacyDiscoveryFails;
|
||||
String nftProgram=LEGACY,unreadablePosition;
|
||||
String nftProgram=LEGACY,unreadablePosition,unreadableHoldingsProgram;
|
||||
Set<String> candidateNfts=Set.of(NFT);
|
||||
int submissions, confirmations, tickSpacing=60;
|
||||
final SolanaWallet wallet = proxy(SolanaWallet.class, (method, args) -> switch (method) {
|
||||
@@ -164,7 +228,16 @@ class RaydiumClmmWithdrawalTest {
|
||||
case "findAssociatedTokenAccount" -> math.findAssociatedTokenAccount((String) args[0], (String) args[1], (SolanaSPLTokenProgram) args[2]);
|
||||
case "encodeSolanaAddress" -> math.encodeSolanaAddress((byte[]) args[0]);
|
||||
case "getLatestBlockhash" -> new SolanaLatestBlockhash(address(30), 100);
|
||||
case "getSPLTokenHoldings" -> { assertEquals(OWNER, args[0]); yield ownsNft ? Map.of(NFT, new SPLTokenHolding(NFT_ACCOUNT, NFT, BigDecimal.ONE, "1", 0, nftProgram)) : Map.of(); }
|
||||
case "getSPLTokenHoldings" -> {
|
||||
assertEquals(OWNER, args[0]); String program = ((SolanaSPLTokenProgram) args[1]).getAddress();
|
||||
if (program.equals(unreadableHoldingsProgram)) throw new IOException("fixture unreadable owner holdings");
|
||||
Map<String, SPLTokenHolding> holdings = new HashMap<>();
|
||||
for (SPLTokenHolding holding : ownerHoldings.values()) {
|
||||
if (program.equals(holding.programId())) holdings.put(holding.mintAddress(), holding);
|
||||
}
|
||||
if (ownsNft && program.equals(nftProgram)) holdings.put(NFT, new SPLTokenHolding(NFT_ACCOUNT, NFT, BigDecimal.ONE, "1", 0, nftProgram));
|
||||
yield holdings;
|
||||
}
|
||||
case "awaitTransaction" -> {
|
||||
assertEquals("fixture-signature", args[0]); assertEquals(SolanaCommitment.CONFIRMED, args[1]); confirmations++;
|
||||
yield timeout ? new SolanaTransactionOutcome(SolanaTransactionOutcome.Status.TIMED_OUT, null, null)
|
||||
@@ -217,6 +290,13 @@ class RaydiumClmmWithdrawalTest {
|
||||
return math.findProgramAddress(RaydiumClmmWithdrawal.PROGRAM,seeds).address();
|
||||
}
|
||||
void put(String account,String owner,byte[] data) {accounts.put(account,new SolanaAccountInfo(account,owner,Base64.getEncoder().encodeToString(data)));}
|
||||
void holding(String account, String mint, SolanaSPLTokenProgram program, int decimals, BigDecimal amount) {
|
||||
BigInteger raw = amount.movePointRight(decimals).toBigIntegerExact();
|
||||
byte[] data = token(mint, OWNER);
|
||||
for (int i = 0; i < 8; i++) data[64 + i] = raw.shiftRight(8 * i).byteValue();
|
||||
put(account, program.getAddress(), data);
|
||||
ownerHoldings.put(mint, new SPLTokenHolding(account, mint, amount, raw.toString(), decimals, program.getAddress()));
|
||||
}
|
||||
byte[] data(String account) {return Base64.getDecoder().decode(accounts.get(account).data());}
|
||||
RaydiumClmmWithdrawal operation() {return new RaydiumClmmWithdrawal(chain);}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user