From 3f492ff599b29d35762be8f42b228e304dc21ca3 Mon Sep 17 00:00:00 2001 From: skynetcap <100323448+skynetcap@users.noreply.github.com> Date: Sun, 22 Sep 2024 23:09:24 -0700 Subject: [PATCH 1/8] Convert financial fields to double and improve position logging Changed long to double for financial fields in JupiterPerpPosition and introduced POSITION_DIVISOR for conversion. Enhanced testGetAllJupiterPerpPositions to log formatted, comprehensive position details including entry price and leverage. (cherry picked from commit fe05a02400866690efe99df3bf250f81f119459a) --- .../jupiter/model/JupiterPerpPosition.java | 26 ++++++++++--------- jupiter/src/test/java/JupiterTest.java | 21 ++++++++++++--- 2 files changed, 32 insertions(+), 15 deletions(-) diff --git a/jupiter/src/main/java/com/mmorrell/jupiter/model/JupiterPerpPosition.java b/jupiter/src/main/java/com/mmorrell/jupiter/model/JupiterPerpPosition.java index d15ad51..206bfc8 100644 --- a/jupiter/src/main/java/com/mmorrell/jupiter/model/JupiterPerpPosition.java +++ b/jupiter/src/main/java/com/mmorrell/jupiter/model/JupiterPerpPosition.java @@ -31,12 +31,12 @@ public class JupiterPerpPosition { private long openTime; private long updateTime; private Side side; - private long price; - private long sizeUsd; - private long collateralUsd; - private long realisedPnlUsd; - private long cumulativeInterestSnapshot; - private long lockedAmount; + private double price; + private double sizeUsd; + private double collateralUsd; + private double realisedPnlUsd; + private double cumulativeInterestSnapshot; + private double lockedAmount; private byte bump; public enum Side { @@ -44,6 +44,8 @@ public enum Side { SHORT } + private static final double POSITION_DIVISOR = 1_000_000.00; + public static JupiterPerpPosition fromByteArray(byte[] data) { int offset = 8; // Start at offset 8 to skip the padding return JupiterPerpPosition.builder() @@ -54,12 +56,12 @@ public static JupiterPerpPosition fromByteArray(byte[] data) { .openTime(readInt64(data, offset += 32)) .updateTime(readInt64(data, offset += 8)) .side(data[offset += 8] == 1 ? Side.LONG : Side.SHORT) - .price(readUint64(data, offset += 1)) - .sizeUsd(readUint64(data, offset += 8)) - .collateralUsd(readUint64(data, offset += 8)) - .realisedPnlUsd(readInt64(data, offset += 8)) - .cumulativeInterestSnapshot(readUint128(data, offset += 8)) - .lockedAmount(readUint64(data, offset += 16)) + .price(readUint64(data, offset += 1) / POSITION_DIVISOR) + .sizeUsd(readUint64(data, offset += 8) / POSITION_DIVISOR) + .collateralUsd(readUint64(data, offset += 8) / POSITION_DIVISOR) + .realisedPnlUsd(readInt64(data, offset += 8) / POSITION_DIVISOR) + .cumulativeInterestSnapshot(readUint128(data, offset += 8) / POSITION_DIVISOR) + .lockedAmount(readUint64(data, offset += 16) / POSITION_DIVISOR) .bump(data[offset += 8]) .build(); } diff --git a/jupiter/src/test/java/JupiterTest.java b/jupiter/src/test/java/JupiterTest.java index 55cdc8a..358e033 100644 --- a/jupiter/src/test/java/JupiterTest.java +++ b/jupiter/src/test/java/JupiterTest.java @@ -75,7 +75,6 @@ public void testJupiterPerpPositionDeserialization() throws RpcException { } @Test - @Disabled public void testGetAllJupiterPerpPositions() throws RpcException { PublicKey programId = new PublicKey("PERPHjGBqRHArX4DySjwM6UJHiR3sWAatqfdBS2qQJu"); @@ -106,12 +105,28 @@ public void testGetAllJupiterPerpPositions() throws RpcException { } } - positions.sort(Comparator.comparingLong(JupiterPerpPosition::getSizeUsd)); + positions.sort(Comparator.comparingDouble(JupiterPerpPosition::getSizeUsd).reversed()); // Log the positions for (JupiterPerpPosition position : positions) { double leverage = (double) position.getSizeUsd() / position.getCollateralUsd(); - log.info("Owner: {}, Size USD: {}, Leverage: {}", position.getOwner().toBase58(), position.getSizeUsd(), leverage); + // log.info("Owner: {}, Size USD: {}, Leverage: {}", position.getOwner().toBase58(), position.getSizeUsd(), leverage); + } + + for (int i = 0; i < 4; i++) { + JupiterPerpPosition position = positions.get(i); + double leverage = position.getSizeUsd() / position.getCollateralUsd(); + + log.info( + String.format( + "Position #%d: Owner[%s], Size[$%.2f], Entry[$%.2f] Leverage: %.2fx", + i + 1, + position.getOwner().toBase58().substring(0, 5).concat("..."), + position.getSizeUsd(), + position.getPrice(), + leverage + ) + ); } } From 59d378d97e2d1e45aab453eaa47dc610070bba03 Mon Sep 17 00:00:00 2001 From: skynetcap <100323448+skynetcap@users.noreply.github.com> Date: Tue, 1 Oct 2024 22:37:23 -0700 Subject: [PATCH 2/8] Add Jupiter DCA model and enhance related functionalities Introduce the `JupiterDca` model to represent Dollar-Cost Averaging accounts. Added methods for deserialization and retrieving all DCA accounts. Updated Maven version across various modules to 1.33.0-SNAPSHOT. Enhanced `OpenBookManager` and `JupiterManager` with new methods and constants for improved functionality. --- bonfida/pom.xml | 2 +- jupiter/pom.xml | 4 +- .../jupiter/manager/JupiterManager.java | 41 ++++- .../mmorrell/jupiter/model/JupiterDca.java | 149 ++++++++++++++++++ .../mmorrell/jupiter/util/JupiterUtil.java | 22 +++ jupiter/src/test/java/JupiterTest.java | 58 ++++--- magiceden/pom.xml | 2 +- mango/pom.xml | 2 +- metaplex/pom.xml | 2 +- openbook/pom.xml | 2 +- .../openbook/manager/OpenBookManager.java | 12 +- phoenix/pom.xml | 6 +- pom.xml | 2 +- pyth/pom.xml | 2 +- serum/pom.xml | 2 +- zeta/pom.xml | 2 +- 16 files changed, 270 insertions(+), 40 deletions(-) create mode 100644 jupiter/src/main/java/com/mmorrell/jupiter/model/JupiterDca.java diff --git a/bonfida/pom.xml b/bonfida/pom.xml index ad234f0..147c402 100644 --- a/bonfida/pom.xml +++ b/bonfida/pom.xml @@ -5,7 +5,7 @@ solanaj-programs com.mmorrell - 1.32.0-SNAPSHOT + 1.33.0-SNAPSHOT 4.0.0 diff --git a/jupiter/pom.xml b/jupiter/pom.xml index 8c7abeb..3b7c7ae 100644 --- a/jupiter/pom.xml +++ b/jupiter/pom.xml @@ -6,7 +6,7 @@ com.mmorrell solanaj-programs - 1.32.0-SNAPSHOT + 1.33.0-SNAPSHOT jupiter @@ -20,7 +20,7 @@ com.mmorrell openbook - 1.32.0-SNAPSHOT + 1.33.0-SNAPSHOT compile diff --git a/jupiter/src/main/java/com/mmorrell/jupiter/manager/JupiterManager.java b/jupiter/src/main/java/com/mmorrell/jupiter/manager/JupiterManager.java index b38256a..084c117 100644 --- a/jupiter/src/main/java/com/mmorrell/jupiter/manager/JupiterManager.java +++ b/jupiter/src/main/java/com/mmorrell/jupiter/manager/JupiterManager.java @@ -1,21 +1,26 @@ package com.mmorrell.jupiter.manager; import com.mmorrell.jupiter.model.*; +import com.mmorrell.jupiter.util.JupiterUtil; import lombok.extern.slf4j.Slf4j; +import org.bitcoinj.core.Base58; import org.p2p.solanaj.core.PublicKey; import org.p2p.solanaj.rpc.Cluster; import org.p2p.solanaj.rpc.RpcClient; import org.p2p.solanaj.rpc.RpcException; import org.p2p.solanaj.rpc.types.AccountInfo; +import org.p2p.solanaj.rpc.types.Memcmp; +import org.p2p.solanaj.rpc.types.ProgramAccount; -import java.util.Base64; -import java.util.Optional; +import java.util.*; @Slf4j public class JupiterManager { private final RpcClient client; private static final PublicKey JUPITER_PROGRAM_ID = new PublicKey("PERPHjGBqRHArX4DySjwM6UJHiR3sWAatqfdBS2qQJu"); + private static final String DCA_PROGRAM_ID = "DCA265Vj8a9CEuX1eb1LWRnDT7uK6q1xMipnNyatn23M"; // Replace with actual DCA Program ID + private static final int DCA_ACCOUNT_SIZE = 289; // Updated based on JupiterDca structure public JupiterManager() { this.client = new RpcClient(Cluster.MAINNET); @@ -94,4 +99,34 @@ public Optional getPerpetuals(PublicKey perpetualsPublicKey) return Optional.empty(); } } -} + + /** + * Retrieves all Jupiter DCA accounts. + * + * @return a list of JupiterDca objects. + * @throws RpcException if the RPC call fails. + */ + public List getAllDcaAccounts() throws RpcException { + PublicKey programId = new PublicKey(DCA_PROGRAM_ID); + + byte[] dcaDiscriminator = JupiterUtil.getAccountDiscriminator("Dca"); + + // Create a memcmp filter for the discriminator at offset 0 + Memcmp memCmpFilter = new Memcmp(0, Base58.encode(dcaDiscriminator)); + + List accounts = client.getApi().getProgramAccounts( + programId, + List.of(memCmpFilter), + DCA_ACCOUNT_SIZE + ); + + List dcaAccounts = new ArrayList<>(); + for (ProgramAccount account : accounts) { + byte[] data = account.getAccount().getDecodedData(); + JupiterDca dca = JupiterDca.fromByteArray(data); + dcaAccounts.add(dca); + } + + return dcaAccounts; + } +} \ No newline at end of file diff --git a/jupiter/src/main/java/com/mmorrell/jupiter/model/JupiterDca.java b/jupiter/src/main/java/com/mmorrell/jupiter/model/JupiterDca.java new file mode 100644 index 0000000..cf70043 --- /dev/null +++ b/jupiter/src/main/java/com/mmorrell/jupiter/model/JupiterDca.java @@ -0,0 +1,149 @@ +package com.mmorrell.jupiter.model; + +import com.mmorrell.jupiter.util.JupiterUtil; +import lombok.Builder; +import lombok.Data; +import org.p2p.solanaj.core.PublicKey; + +/** + * Represents a Jupiter DCA (Dollar-Cost Averaging) account. + */ +@Data +@Builder +public class JupiterDca { + private PublicKey user; + private PublicKey inputMint; + private PublicKey outputMint; + private long idx; + private long nextCycleAt; + private long inDeposited; + private long inWithdrawn; + private long outWithdrawn; + private long inUsed; + private long outReceived; + private long inAmountPerCycle; + private long cycleFrequency; + private long nextCycleAmountLeft; + private PublicKey inAccount; + private PublicKey outAccount; + private long minOutAmount; + private long maxOutAmount; + private long keeperInBalanceBeforeBorrow; + private long dcaOutBalanceBeforeSwap; + private long createdAt; + private byte bump; + + /** + * Deserializes a byte array into a JupiterDca object. + * + * @param data the byte array to deserialize. + * @return the deserialized JupiterDca object. + */ + public static JupiterDca fromByteArray(byte[] data) { + int offset = 8; + + PublicKey user = PublicKey.readPubkey(data, offset); + offset += 32; + + PublicKey inputMint = PublicKey.readPubkey(data, offset); + offset += 32; + + PublicKey outputMint = PublicKey.readPubkey(data, offset); + offset += 32; + + long idx = readLong(data, offset); + offset += 8; + + long nextCycleAt = readLong(data, offset); + offset += 8; + + long inDeposited = readLong(data, offset); + offset += 8; + + long inWithdrawn = readLong(data, offset); + offset += 8; + + long outWithdrawn = readLong(data, offset); + offset += 8; + + long inUsed = readLong(data, offset); + offset += 8; + + long outReceived = readLong(data, offset); + offset += 8; + + long inAmountPerCycle = readLong(data, offset); + offset += 8; + + long cycleFrequency = readLong(data, offset); + offset += 8; + + long nextCycleAmountLeft = readLong(data, offset); + offset += 8; + + PublicKey inAccount = PublicKey.readPubkey(data, offset); + offset += 32; + + PublicKey outAccount = PublicKey.readPubkey(data, offset); + offset += 32; + + long minOutAmount = readLong(data, offset); + offset += 8; + + long maxOutAmount = readLong(data, offset); + offset += 8; + + long keeperInBalanceBeforeBorrow = readLong(data, offset); + offset += 8; + + long dcaOutBalanceBeforeSwap = readLong(data, offset); + offset += 8; + + long createdAt = readLong(data, offset); + offset += 8; + + byte bump = data[offset]; + + return JupiterDca.builder() + .user(user) + .inputMint(inputMint) + .outputMint(outputMint) + .idx(idx) + .nextCycleAt(nextCycleAt) + .inDeposited(inDeposited) + .inWithdrawn(inWithdrawn) + .outWithdrawn(outWithdrawn) + .inUsed(inUsed) + .outReceived(outReceived) + .inAmountPerCycle(inAmountPerCycle) + .cycleFrequency(cycleFrequency) + .nextCycleAmountLeft(nextCycleAmountLeft) + .inAccount(inAccount) + .outAccount(outAccount) + .minOutAmount(minOutAmount) + .maxOutAmount(maxOutAmount) + .keeperInBalanceBeforeBorrow(keeperInBalanceBeforeBorrow) + .dcaOutBalanceBeforeSwap(dcaOutBalanceBeforeSwap) + .createdAt(createdAt) + .bump(bump) + .build(); + } + + /** + * Reads a long value from the byte array at the specified offset. + * + * @param data the byte array. + * @param offset the offset to start reading from. + * @return the long value. + */ + private static long readLong(byte[] data, int offset) { + return ((long) (data[offset] & 0xFF)) | + (((long) (data[offset + 1] & 0xFF)) << 8) | + (((long) (data[offset + 2] & 0xFF)) << 16) | + (((long) (data[offset + 3] & 0xFF)) << 24) | + (((long) (data[offset + 4] & 0xFF)) << 32) | + (((long) (data[offset + 5] & 0xFF)) << 40) | + (((long) (data[offset + 6] & 0xFF)) << 48) | + (((long) (data[offset + 7] & 0xFF)) << 56); + } +} \ No newline at end of file diff --git a/jupiter/src/main/java/com/mmorrell/jupiter/util/JupiterUtil.java b/jupiter/src/main/java/com/mmorrell/jupiter/util/JupiterUtil.java index 4417ba3..de88b83 100644 --- a/jupiter/src/main/java/com/mmorrell/jupiter/util/JupiterUtil.java +++ b/jupiter/src/main/java/com/mmorrell/jupiter/util/JupiterUtil.java @@ -2,6 +2,11 @@ import org.p2p.solanaj.core.PublicKey; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; + public class JupiterUtil { public static int readUint32(byte[] data, int offset) { return (data[offset] & 0xFF) | @@ -32,4 +37,21 @@ public static PublicKey readOptionalPublicKey(byte[] data, int offset) { boolean hasValue = data[offset] != 0; return hasValue ? PublicKey.readPubkey(data, offset + 1) : null; } + + /** + * Calculates the account discriminator for a given account name. + * + * @param accountName the name of the account. + * @return the first 8 bytes of the SHA-256 hash of "account:". + */ + public static byte[] getAccountDiscriminator(String accountName) { + String preimage = "account:" + accountName; + try { + MessageDigest hasher = MessageDigest.getInstance("SHA-256"); + hasher.update(preimage.getBytes(StandardCharsets.UTF_8)); + return Arrays.copyOfRange(hasher.digest(), 0, 8); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("SHA-256 algorithm not found for discriminator calculation."); + } + } } \ No newline at end of file diff --git a/jupiter/src/test/java/JupiterTest.java b/jupiter/src/test/java/JupiterTest.java index 358e033..95477af 100644 --- a/jupiter/src/test/java/JupiterTest.java +++ b/jupiter/src/test/java/JupiterTest.java @@ -1,6 +1,7 @@ import com.google.common.io.Files; import com.mmorrell.jupiter.manager.JupiterManager; import com.mmorrell.jupiter.model.*; +import com.mmorrell.jupiter.util.JupiterUtil; import lombok.extern.slf4j.Slf4j; import org.bitcoinj.core.Base58; import org.junit.jupiter.api.BeforeEach; @@ -23,7 +24,7 @@ import static org.junit.jupiter.api.Assertions.*; /** - * Test class for Jupiter Perpetuals positions. + * Test class for Jupiter Perpetuals positions and DCA accounts. */ @Slf4j public class JupiterTest { @@ -79,7 +80,7 @@ public void testGetAllJupiterPerpPositions() throws RpcException { PublicKey programId = new PublicKey("PERPHjGBqRHArX4DySjwM6UJHiR3sWAatqfdBS2qQJu"); // Get the discriminator for the Position account - byte[] positionDiscriminator = getAccountDiscriminator("Position"); + byte[] positionDiscriminator = JupiterUtil.getAccountDiscriminator("Position"); // Create a memcmp filter for the discriminator at offset 0 Memcmp memcmpFilter = new Memcmp(0, Base58.encode(positionDiscriminator)); @@ -130,23 +131,6 @@ public void testGetAllJupiterPerpPositions() throws RpcException { } } - /** - * Calculates the account discriminator for a given account name. - * - * @param accountName the name of the account. - * @return the first 8 bytes of the SHA-256 hash of "account:". - */ - private byte[] getAccountDiscriminator(String accountName) { - String preimage = "account:" + accountName; - try { - MessageDigest hasher = MessageDigest.getInstance("SHA-256"); - hasher.update(preimage.getBytes(StandardCharsets.UTF_8)); - return Arrays.copyOfRange(hasher.digest(), 0, 8); - } catch (NoSuchAlgorithmException e) { - throw new RuntimeException("SHA-256 algorithm not found for discriminator calculation."); - } - } - @Test public void testJupiterPoolDeserialization() throws RpcException { PublicKey poolPublicKey = new PublicKey("5BUwFW4nRbftYTDMbgxykoFWqWHPzahFSNAaaaJtVKsq"); @@ -336,4 +320,38 @@ public void testJupiterManagerWithInvalidPublicKeys() { assertFalse(manager.getPositionRequest(invalidPublicKey).isPresent()); assertFalse(manager.getPerpetuals(invalidPublicKey).isPresent()); } -} + + @Test + public void testGetAllJupiterDcaAccounts() { + JupiterManager manager = new JupiterManager(client); + try { + List dcaAccounts = manager.getAllDcaAccounts(); + assertNotNull(dcaAccounts, "DCA accounts list should not be null"); + assertTrue(dcaAccounts.size() > 0, "DCA accounts list should contain at least one account"); + + for (JupiterDca dca : dcaAccounts) { + assertNotNull(dca.getUser(), "DCA user should not be null"); + assertNotNull(dca.getInputMint(), "DCA inputMint should not be null"); + assertNotNull(dca.getOutputMint(), "DCA outputMint should not be null"); + assertTrue(dca.getNextCycleAt() > 0, "DCA nextCycleAt should be greater than 0"); + assertTrue(dca.getOutWithdrawn() >= 0, "DCA outWithdrawn should be non-negative"); + assertTrue(dca.getInUsed() >= 0, "DCA inUsed should be non-negative"); + assertTrue(dca.getOutReceived() >= 0, "DCA outReceived should be non-negative"); + assertTrue(dca.getInAmountPerCycle() > 0, "DCA inAmountPerCycle should be greater than 0"); + assertTrue(dca.getCycleFrequency() > 0, "DCA cycleFrequency should be greater than 0"); + assertTrue(dca.getNextCycleAmountLeft() >= 0, "DCA nextCycleAmountLeft should be non-negative"); + assertNotNull(dca.getInAccount(), "DCA inAccount should not be null"); + assertNotNull(dca.getOutAccount(), "DCA outAccount should not be null"); + assertTrue(dca.getCreatedAt() > 0, "DCA createdAt should be greater than 0"); + } + + // Log the retrieved DCA accounts + for (JupiterDca dca : dcaAccounts) { + log.info("JupiterDca: {}", dca); + } + + } catch (RpcException e) { + fail("RPC Exception occurred: " + e.getMessage()); + } + } +} \ No newline at end of file diff --git a/magiceden/pom.xml b/magiceden/pom.xml index 05a60b1..98b561b 100644 --- a/magiceden/pom.xml +++ b/magiceden/pom.xml @@ -5,7 +5,7 @@ solanaj-programs com.mmorrell - 1.32.0-SNAPSHOT + 1.33.0-SNAPSHOT 4.0.0 diff --git a/mango/pom.xml b/mango/pom.xml index 7ceff70..5729f67 100644 --- a/mango/pom.xml +++ b/mango/pom.xml @@ -5,7 +5,7 @@ solanaj-programs com.mmorrell - 1.32.0-SNAPSHOT + 1.33.0-SNAPSHOT 4.0.0 diff --git a/metaplex/pom.xml b/metaplex/pom.xml index d7cf57f..83dbae4 100644 --- a/metaplex/pom.xml +++ b/metaplex/pom.xml @@ -5,7 +5,7 @@ solanaj-programs com.mmorrell - 1.32.0-SNAPSHOT + 1.33.0-SNAPSHOT 4.0.0 diff --git a/openbook/pom.xml b/openbook/pom.xml index 8aae976..b6f9439 100644 --- a/openbook/pom.xml +++ b/openbook/pom.xml @@ -5,7 +5,7 @@ solanaj-programs com.mmorrell - 1.32.0-SNAPSHOT + 1.33.0-SNAPSHOT 4.0.0 diff --git a/openbook/src/main/java/com/mmorrell/openbook/manager/OpenBookManager.java b/openbook/src/main/java/com/mmorrell/openbook/manager/OpenBookManager.java index 50b80b8..7a31ea2 100644 --- a/openbook/src/main/java/com/mmorrell/openbook/manager/OpenBookManager.java +++ b/openbook/src/main/java/com/mmorrell/openbook/manager/OpenBookManager.java @@ -41,6 +41,7 @@ public class OpenBookManager { private final Map marketCache = new HashMap<>(); private final static int CONSUME_EVENTS_DEFAULT_FEE = 11; + private final static int DEFAULT_PRIORITY_LIMIT = 50_000; public OpenBookManager(RpcClient client) { this.client = client; @@ -196,7 +197,7 @@ public Optional getOpenOrdersAccount(PublicKey ooa) { * otherwise an empty Optional. */ public Optional consumeEvents(Account caller, PublicKey marketId, long limit, @Nullable String memo, - int priorityFee) { + int priorityFee, int priorityLimit) { Optional marketOptional = getMarket(marketId, true, false); if (marketOptional.isEmpty()) { return Optional.empty(); @@ -218,7 +219,7 @@ public Optional consumeEvents(Account caller, PublicKey marketId, long l log.info("Cranking {}: {}", market.getName(), peopleToCrank); Transaction tx = new Transaction(); - tx.addInstruction(ComputeBudgetProgram.setComputeUnitLimit(50_000)); + tx.addInstruction(ComputeBudgetProgram.setComputeUnitLimit(priorityLimit)); tx.addInstruction(ComputeBudgetProgram.setComputeUnitPrice(priorityFee)); tx.addInstruction( OpenbookProgram.consumeEvents( @@ -262,6 +263,11 @@ public Optional consumeEvents(Account caller, PublicKey marketId, long l * @return An Optional String representing the consumed events */ public Optional consumeEvents(Account caller, PublicKey marketId, long limit, @Nullable String memo) { - return consumeEvents(caller, marketId, limit, memo, CONSUME_EVENTS_DEFAULT_FEE); + return consumeEvents(caller, marketId, limit, memo, CONSUME_EVENTS_DEFAULT_FEE, DEFAULT_PRIORITY_LIMIT); + } + + public Optional consumeEvents(Account caller, PublicKey marketId, long limit, @Nullable String memo, + int priorityFee) { + return consumeEvents(caller, marketId, limit, memo, CONSUME_EVENTS_DEFAULT_FEE, DEFAULT_PRIORITY_LIMIT); } } diff --git a/phoenix/pom.xml b/phoenix/pom.xml index 192ac16..52102c0 100644 --- a/phoenix/pom.xml +++ b/phoenix/pom.xml @@ -6,7 +6,7 @@ com.mmorrell solanaj-programs - 1.32.0-SNAPSHOT + 1.33.0-SNAPSHOT phoenix @@ -20,13 +20,13 @@ com.mmorrell serum - 1.32.0-SNAPSHOT + 1.33.0-SNAPSHOT compile com.mmorrell metaplex - 1.32.0-SNAPSHOT + 1.33.0-SNAPSHOT test diff --git a/pom.xml b/pom.xml index 9febe09..d5571a1 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.mmorrell solanaj-programs pom - 1.32.0-SNAPSHOT + 1.33.0-SNAPSHOT ${project.groupId}:${project.artifactId} Program libraries for SolanaJ, a library for Solana RPC https://github.com/skynetcap/solanaj-programs diff --git a/pyth/pom.xml b/pyth/pom.xml index 1c7cc3c..914d8c8 100644 --- a/pyth/pom.xml +++ b/pyth/pom.xml @@ -5,7 +5,7 @@ solanaj-programs com.mmorrell - 1.32.0-SNAPSHOT + 1.33.0-SNAPSHOT 4.0.0 diff --git a/serum/pom.xml b/serum/pom.xml index 4b23d8b..d708e87 100644 --- a/serum/pom.xml +++ b/serum/pom.xml @@ -5,7 +5,7 @@ solanaj-programs com.mmorrell - 1.32.0-SNAPSHOT + 1.33.0-SNAPSHOT 4.0.0 diff --git a/zeta/pom.xml b/zeta/pom.xml index cb5964a..dc04f43 100644 --- a/zeta/pom.xml +++ b/zeta/pom.xml @@ -5,7 +5,7 @@ solanaj-programs com.mmorrell - 1.32.0-SNAPSHOT + 1.33.0-SNAPSHOT 4.0.0 From 24abd1e7b99c476176fa53b6d1c0a1204206d84e Mon Sep 17 00:00:00 2001 From: skynetcap <100323448+skynetcap@users.noreply.github.com> Date: Tue, 1 Oct 2024 22:48:29 -0700 Subject: [PATCH 3/8] Handle RpcException and add getMostRecentJupiterDcaAccounts test. Refactored getAllDcaAccounts to handle RpcException, ensuring it returns an empty list and logs a warning in case of failure. Added a new test method getMostRecentJupiterDcaAccounts to fetch and log the most recent DCA accounts based on their index. --- .../jupiter/manager/JupiterManager.java | 33 +++++----- jupiter/src/test/java/JupiterTest.java | 61 +++++++++++-------- 2 files changed, 54 insertions(+), 40 deletions(-) diff --git a/jupiter/src/main/java/com/mmorrell/jupiter/manager/JupiterManager.java b/jupiter/src/main/java/com/mmorrell/jupiter/manager/JupiterManager.java index 084c117..faec699 100644 --- a/jupiter/src/main/java/com/mmorrell/jupiter/manager/JupiterManager.java +++ b/jupiter/src/main/java/com/mmorrell/jupiter/manager/JupiterManager.java @@ -106,7 +106,7 @@ public Optional getPerpetuals(PublicKey perpetualsPublicKey) * @return a list of JupiterDca objects. * @throws RpcException if the RPC call fails. */ - public List getAllDcaAccounts() throws RpcException { + public List getAllDcaAccounts() { PublicKey programId = new PublicKey(DCA_PROGRAM_ID); byte[] dcaDiscriminator = JupiterUtil.getAccountDiscriminator("Dca"); @@ -114,19 +114,24 @@ public List getAllDcaAccounts() throws RpcException { // Create a memcmp filter for the discriminator at offset 0 Memcmp memCmpFilter = new Memcmp(0, Base58.encode(dcaDiscriminator)); - List accounts = client.getApi().getProgramAccounts( - programId, - List.of(memCmpFilter), - DCA_ACCOUNT_SIZE - ); - - List dcaAccounts = new ArrayList<>(); - for (ProgramAccount account : accounts) { - byte[] data = account.getAccount().getDecodedData(); - JupiterDca dca = JupiterDca.fromByteArray(data); - dcaAccounts.add(dca); - } + try { + List accounts = client.getApi().getProgramAccounts( + programId, + List.of(memCmpFilter), + DCA_ACCOUNT_SIZE + ); + + List dcaAccounts = new ArrayList<>(); + for (ProgramAccount account : accounts) { + byte[] data = account.getAccount().getDecodedData(); + JupiterDca dca = JupiterDca.fromByteArray(data); + dcaAccounts.add(dca); + } - return dcaAccounts; + return dcaAccounts; + } catch (RpcException ex) { + log.warn("Error fetching DCA accounts: {}", ex.getMessage()); + return Collections.emptyList(); + } } } \ No newline at end of file diff --git a/jupiter/src/test/java/JupiterTest.java b/jupiter/src/test/java/JupiterTest.java index 95477af..022452f 100644 --- a/jupiter/src/test/java/JupiterTest.java +++ b/jupiter/src/test/java/JupiterTest.java @@ -19,6 +19,7 @@ import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import java.time.Instant; import java.util.*; import static org.junit.jupiter.api.Assertions.*; @@ -324,34 +325,42 @@ public void testJupiterManagerWithInvalidPublicKeys() { @Test public void testGetAllJupiterDcaAccounts() { JupiterManager manager = new JupiterManager(client); - try { - List dcaAccounts = manager.getAllDcaAccounts(); - assertNotNull(dcaAccounts, "DCA accounts list should not be null"); - assertTrue(dcaAccounts.size() > 0, "DCA accounts list should contain at least one account"); - - for (JupiterDca dca : dcaAccounts) { - assertNotNull(dca.getUser(), "DCA user should not be null"); - assertNotNull(dca.getInputMint(), "DCA inputMint should not be null"); - assertNotNull(dca.getOutputMint(), "DCA outputMint should not be null"); - assertTrue(dca.getNextCycleAt() > 0, "DCA nextCycleAt should be greater than 0"); - assertTrue(dca.getOutWithdrawn() >= 0, "DCA outWithdrawn should be non-negative"); - assertTrue(dca.getInUsed() >= 0, "DCA inUsed should be non-negative"); - assertTrue(dca.getOutReceived() >= 0, "DCA outReceived should be non-negative"); - assertTrue(dca.getInAmountPerCycle() > 0, "DCA inAmountPerCycle should be greater than 0"); - assertTrue(dca.getCycleFrequency() > 0, "DCA cycleFrequency should be greater than 0"); - assertTrue(dca.getNextCycleAmountLeft() >= 0, "DCA nextCycleAmountLeft should be non-negative"); - assertNotNull(dca.getInAccount(), "DCA inAccount should not be null"); - assertNotNull(dca.getOutAccount(), "DCA outAccount should not be null"); - assertTrue(dca.getCreatedAt() > 0, "DCA createdAt should be greater than 0"); - } - // Log the retrieved DCA accounts - for (JupiterDca dca : dcaAccounts) { - log.info("JupiterDca: {}", dca); - } + List dcaAccounts = manager.getAllDcaAccounts(); + assertNotNull(dcaAccounts, "DCA accounts list should not be null"); + assertTrue(dcaAccounts.size() > 0, "DCA accounts list should contain at least one account"); + + for (JupiterDca dca : dcaAccounts) { + assertNotNull(dca.getUser(), "DCA user should not be null"); + assertNotNull(dca.getInputMint(), "DCA inputMint should not be null"); + assertNotNull(dca.getOutputMint(), "DCA outputMint should not be null"); + assertTrue(dca.getNextCycleAt() > 0, "DCA nextCycleAt should be greater than 0"); + assertTrue(dca.getOutWithdrawn() >= 0, "DCA outWithdrawn should be non-negative"); + assertTrue(dca.getInUsed() >= 0, "DCA inUsed should be non-negative"); + assertTrue(dca.getOutReceived() >= 0, "DCA outReceived should be non-negative"); + assertTrue(dca.getInAmountPerCycle() > 0, "DCA inAmountPerCycle should be greater than 0"); + assertTrue(dca.getCycleFrequency() > 0, "DCA cycleFrequency should be greater than 0"); + assertTrue(dca.getNextCycleAmountLeft() >= 0, "DCA nextCycleAmountLeft should be non-negative"); + assertNotNull(dca.getInAccount(), "DCA inAccount should not be null"); + assertNotNull(dca.getOutAccount(), "DCA outAccount should not be null"); + assertTrue(dca.getCreatedAt() > 0, "DCA createdAt should be greater than 0"); + } + + // Log the retrieved DCA accounts + for (JupiterDca dca : dcaAccounts) { + log.info("JupiterDca: {}", dca); + } + + } - } catch (RpcException e) { - fail("RPC Exception occurred: " + e.getMessage()); + @Test + public void getMostRecentJupiterDcaAccounts() { + JupiterManager manager = new JupiterManager(client); + List dcaAccounts = manager.getAllDcaAccounts(); + dcaAccounts.sort(Comparator.comparingLong(JupiterDca::getIdx).reversed()); + for (int i = 0; i < 10; i++) { + JupiterDca dca = dcaAccounts.get(i); + log.info("DCA #{}: {}", i + 1, dca); } } } \ No newline at end of file From 5c3780e6910e60a2ff2b2f22a8aa4e10478f8bad Mon Sep 17 00:00:00 2001 From: skynetcap <100323448+skynetcap@users.noreply.github.com> Date: Tue, 1 Oct 2024 23:04:11 -0700 Subject: [PATCH 4/8] Refactor sorting and logging in JupiterDca tests Updated the sorting criterion in `getMostRecentJupiterDcaAccounts` to use creation timestamp and enhanced logging details. Added filtering to display only active JupiterDca accounts based on specific conditions and included the size of the filtered list in the logs. --- jupiter/src/test/java/JupiterTest.java | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/jupiter/src/test/java/JupiterTest.java b/jupiter/src/test/java/JupiterTest.java index 022452f..b8e6f11 100644 --- a/jupiter/src/test/java/JupiterTest.java +++ b/jupiter/src/test/java/JupiterTest.java @@ -273,7 +273,7 @@ public void testJupiterTestOracleDeserialization() throws RpcException { assertTrue(testOracle.getPublishTime() > 0); // Add more assertions as needed } - + @Test public void testJupiterManager() { JupiterManager manager = new JupiterManager(client); @@ -357,10 +357,20 @@ public void testGetAllJupiterDcaAccounts() { public void getMostRecentJupiterDcaAccounts() { JupiterManager manager = new JupiterManager(client); List dcaAccounts = manager.getAllDcaAccounts(); - dcaAccounts.sort(Comparator.comparingLong(JupiterDca::getIdx).reversed()); + dcaAccounts.sort(Comparator.comparingLong(JupiterDca::getCreatedAt).reversed()); + for (int i = 0; i < 10; i++) { JupiterDca dca = dcaAccounts.get(i); - log.info("DCA #{}: {}", i + 1, dca); + log.info("DCA {} #{}: {}", Instant.ofEpochSecond(dca.getCreatedAt()),i + 1, dca); } + + log.info("Only showing ones where nextCycleAt > createdAt && nextCycleAt > now && inUsed < inDeposited"); + List activeDcas = dcaAccounts.stream() + .filter(dca -> dca.getNextCycleAt() > dca.getCreatedAt() && dca.getNextCycleAt() > Instant.now().getEpochSecond()) + .filter(dca -> dca.getInUsed() < dca.getInDeposited()) + .toList(); + + activeDcas.forEach(dca -> log.info("DCA {} #{}: {}", Instant.ofEpochSecond(dca.getCreatedAt()), dcaAccounts.indexOf(dca) + 1, dca)); + log.info("Size: {}", activeDcas.size()); } } \ No newline at end of file From 1c3e118eb34b726ec69cc5aad7957b2a0c77d34c Mon Sep 17 00:00:00 2001 From: skynetcap <100323448+skynetcap@users.noreply.github.com> Date: Tue, 1 Oct 2024 23:22:34 -0700 Subject: [PATCH 5/8] Add test for filtering active DCA orders Introduced a new unit test `testGetOpenDcaOrders` to validate the filtering criteria for active DCA orders based on provided constraints such as next cycle, usage, and in-range amounts. This ensures proper functionality by using mock token prices and asserting the resultant order properties. --- jupiter/src/test/java/JupiterTest.java | 84 ++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/jupiter/src/test/java/JupiterTest.java b/jupiter/src/test/java/JupiterTest.java index b8e6f11..8c4531a 100644 --- a/jupiter/src/test/java/JupiterTest.java +++ b/jupiter/src/test/java/JupiterTest.java @@ -21,6 +21,7 @@ import java.security.NoSuchAlgorithmException; import java.time.Instant; import java.util.*; +import java.util.stream.Collectors; import static org.junit.jupiter.api.Assertions.*; @@ -373,4 +374,87 @@ public void getMostRecentJupiterDcaAccounts() { activeDcas.forEach(dca -> log.info("DCA {} #{}: {}", Instant.ofEpochSecond(dca.getCreatedAt()), dcaAccounts.indexOf(dca) + 1, dca)); log.info("Size: {}", activeDcas.size()); } + + @Test + public void testGetOpenDcaOrders() { + JupiterManager manager = new JupiterManager(client); + + // Mocking price data for tokens (Replace with actual price retrieval in production) + Map tokenPrices = new HashMap<>(); + tokenPrices.put("Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB", 1.0); // Example price for inputMint + tokenPrices.put("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", 1.0); // Example price for another inputMint + tokenPrices.put("So11111111111111111111111111111111111111112", 147.0); // Example price for inputMint + tokenPrices.put("DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263", 0.00002479); // Example price for outputMint + // Add more token prices as needed + + List dcaAccounts = manager.getAllDcaAccounts(); + assertNotNull(dcaAccounts, "DCA accounts list should not be null"); + assertTrue(dcaAccounts.size() > 0, "DCA accounts list should contain at least one account"); + + long now = Instant.now().getEpochSecond(); + + List openDcaOrders = dcaAccounts.stream() + // Filter where nextCycleAt > createdAt and nextCycleAt > now and inUsed < inDeposited + .filter(dca -> dca.getNextCycleAt() > dca.getCreatedAt() + && dca.getNextCycleAt() > now + && dca.getInUsed() < dca.getInDeposited()) + // Filter where percent_remaining > 0 + .filter(dca -> { + double remainingInputAmount = (double) (dca.getInDeposited() - dca.getInUsed()); + double inputAmount = (double) dca.getInDeposited() / Math.pow(10, 6); // Assuming 6 decimals + double percentRemaining = 100 * remainingInputAmount / inputAmount; + return percentRemaining > 0; + }) + // Filter based on is_in_range constraints + .filter(dca -> { + double inputOrderSize = (double) dca.getInAmountPerCycle() / Math.pow(10, 6); // Assuming 6 decimals + + // Retrieve prices; default to 1.0 if not found + double inputPriceUsd = tokenPrices.getOrDefault(dca.getInputMint().toBase58(), 1.0); + double outputPriceUsd = tokenPrices.getOrDefault(dca.getOutputMint().toBase58(), 1.0); + + double outputOrderSize = inputOrderSize * inputPriceUsd / outputPriceUsd; + + boolean minInRange = (dca.getMinOutAmount() == 0) + || (outputOrderSize >= ((double) dca.getMinOutAmount() / Math.pow(10, 6))); // Assuming 6 decimals + boolean maxInRange = (dca.getMaxOutAmount() == 0) + || (outputOrderSize <= ((double) dca.getMaxOutAmount() / Math.pow(10, 6))); // Assuming 6 decimals + + return minInRange && maxInRange; + }) + .collect(Collectors.toList()); + + assertNotNull(openDcaOrders, "Open DCA orders list should not be null"); + + // Assertions to ensure all open DCA orders meet the criteria + openDcaOrders.forEach(dca -> { + // Verify nextCycleAt constraints + assertTrue(dca.getNextCycleAt() > dca.getCreatedAt(), "nextCycleAt should be greater than createdAt"); + assertTrue(dca.getNextCycleAt() > now, "nextCycleAt should be in the future"); + assertTrue(dca.getInUsed() < dca.getInDeposited(), "inUsed should be less than inDeposited"); + + // Verify percent_remaining > 0 + double remainingInputAmount = (double) (dca.getInDeposited() - dca.getInUsed()); + double inputAmount = (double) dca.getInDeposited() / Math.pow(10, 6); // Assuming 6 decimals + double percentRemaining = 100 * remainingInputAmount / inputAmount; + assertTrue(percentRemaining > 0, "Percent remaining should be greater than 0"); + + // Verify is_in_range constraints + double inputOrderSize = (double) dca.getInAmountPerCycle() / Math.pow(10, 6); // Assuming 6 decimals + double inputPriceUsd = tokenPrices.getOrDefault(dca.getInputMint().toBase58(), 1.0); + double outputPriceUsd = tokenPrices.getOrDefault(dca.getOutputMint().toBase58(), 1.0); + double outputOrderSize = inputOrderSize * inputPriceUsd / outputPriceUsd; + + boolean minInRange = (dca.getMinOutAmount() == 0) + || (outputOrderSize >= ((double) dca.getMinOutAmount() / Math.pow(10, 6))); // Assuming 6 decimals + boolean maxInRange = (dca.getMaxOutAmount() == 0) + || (outputOrderSize <= ((double) dca.getMaxOutAmount() / Math.pow(10, 6))); // Assuming 6 decimals + + assertTrue(minInRange, "Output order size should be within the minimum range"); + assertTrue(maxInRange, "Output order size should be within the maximum range"); + }); + + log.info("Open DCA Orders [{}]: {}", openDcaOrders.size(), openDcaOrders); + log.info("Size: {}", openDcaOrders.size()); + } } \ No newline at end of file From 62299e22c046ce49ac36ba7c0753bbeb3ca9acfe Mon Sep 17 00:00:00 2001 From: skynetcap <100323448+skynetcap@users.noreply.github.com> Date: Tue, 1 Oct 2024 23:39:59 -0700 Subject: [PATCH 6/8] Refactor DCA program ID handling and add user-specific DCA retrieval Refactor the DCA program ID from string to PublicKey type for consistency. Added a new method to retrieve DCA accounts filtered by user and updated tests to validate new functionality. --- .../jupiter/manager/JupiterManager.java | 34 ++++++++++++++++--- jupiter/src/test/java/JupiterTest.java | 17 ++++++++++ 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/jupiter/src/main/java/com/mmorrell/jupiter/manager/JupiterManager.java b/jupiter/src/main/java/com/mmorrell/jupiter/manager/JupiterManager.java index faec699..883f9ae 100644 --- a/jupiter/src/main/java/com/mmorrell/jupiter/manager/JupiterManager.java +++ b/jupiter/src/main/java/com/mmorrell/jupiter/manager/JupiterManager.java @@ -19,7 +19,7 @@ public class JupiterManager { private final RpcClient client; private static final PublicKey JUPITER_PROGRAM_ID = new PublicKey("PERPHjGBqRHArX4DySjwM6UJHiR3sWAatqfdBS2qQJu"); - private static final String DCA_PROGRAM_ID = "DCA265Vj8a9CEuX1eb1LWRnDT7uK6q1xMipnNyatn23M"; // Replace with actual DCA Program ID + private static final PublicKey DCA_PROGRAM_ID = new PublicKey("DCA265Vj8a9CEuX1eb1LWRnDT7uK6q1xMipnNyatn23M"); private static final int DCA_ACCOUNT_SIZE = 289; // Updated based on JupiterDca structure public JupiterManager() { @@ -107,8 +107,6 @@ public Optional getPerpetuals(PublicKey perpetualsPublicKey) * @throws RpcException if the RPC call fails. */ public List getAllDcaAccounts() { - PublicKey programId = new PublicKey(DCA_PROGRAM_ID); - byte[] dcaDiscriminator = JupiterUtil.getAccountDiscriminator("Dca"); // Create a memcmp filter for the discriminator at offset 0 @@ -116,7 +114,7 @@ public List getAllDcaAccounts() { try { List accounts = client.getApi().getProgramAccounts( - programId, + DCA_PROGRAM_ID, List.of(memCmpFilter), DCA_ACCOUNT_SIZE ); @@ -134,4 +132,32 @@ public List getAllDcaAccounts() { return Collections.emptyList(); } } + + public List getAllDcaAccounts(PublicKey user) { + byte[] dcaDiscriminator = JupiterUtil.getAccountDiscriminator("Dca"); + + // Create a memcmp filter for the discriminator at offset 0 + Memcmp memCmpFilter = new Memcmp(0, Base58.encode(dcaDiscriminator)); + Memcmp memCmpFilterUser = new Memcmp(8, Base58.encode(user.toByteArray())); + + try { + List accounts = client.getApi().getProgramAccounts( + DCA_PROGRAM_ID, + List.of(memCmpFilter, memCmpFilterUser), + DCA_ACCOUNT_SIZE + ); + + List dcaAccounts = new ArrayList<>(); + for (ProgramAccount account : accounts) { + byte[] data = account.getAccount().getDecodedData(); + JupiterDca dca = JupiterDca.fromByteArray(data); + dcaAccounts.add(dca); + } + + return dcaAccounts; + } catch (RpcException ex) { + log.warn("Error fetching DCA accounts: {}", ex.getMessage()); + return Collections.emptyList(); + } + } } \ No newline at end of file diff --git a/jupiter/src/test/java/JupiterTest.java b/jupiter/src/test/java/JupiterTest.java index 8c4531a..50dd7b3 100644 --- a/jupiter/src/test/java/JupiterTest.java +++ b/jupiter/src/test/java/JupiterTest.java @@ -457,4 +457,21 @@ public void testGetOpenDcaOrders() { log.info("Open DCA Orders [{}]: {}", openDcaOrders.size(), openDcaOrders); log.info("Size: {}", openDcaOrders.size()); } + + @Test + public void testGetDcaAccountsByUserFiltering() { + JupiterManager manager = new JupiterManager(client); + List jupiterDcas = manager.getAllDcaAccounts() + .stream() + .filter(jupiterDca -> jupiterDca.getUser().equals(new PublicKey("ESmavfhN3JKy3q3iJfP2FJYWNDRWVEkcKmzzVfetU5eB"))) + .toList(); + log.info("DCAs for ESmavfhN3JKy3q3iJfP2FJYWNDRWVEkcKmzzVfetU5eB: {}", jupiterDcas); + } + + @Test + public void testGetDcaAccountsByUser() { + JupiterManager manager = new JupiterManager(client); + List jupiterDcas = manager.getAllDcaAccounts(new PublicKey("ESmavfhN3JKy3q3iJfP2FJYWNDRWVEkcKmzzVfetU5eB")); + log.info("DCAs for user: {}", jupiterDcas); + } } \ No newline at end of file From b691a355d730d7cadf6b49aafb7ded066a0c3f4d Mon Sep 17 00:00:00 2001 From: skynetcap <100323448+skynetcap@users.noreply.github.com> Date: Wed, 2 Oct 2024 13:37:39 -0700 Subject: [PATCH 7/8] Refactor method names and disable flaky tests Renamed `getAllDcaAccounts` to `getDcaAccounts` in `JupiterManager` for consistency. Disabled several tests due to reliance on hardcoded accounts and potential flakiness. --- .../jupiter/manager/JupiterManager.java | 4 +-- jupiter/src/test/java/JupiterTest.java | 26 ++++++++++--------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/jupiter/src/main/java/com/mmorrell/jupiter/manager/JupiterManager.java b/jupiter/src/main/java/com/mmorrell/jupiter/manager/JupiterManager.java index 883f9ae..1d5c81c 100644 --- a/jupiter/src/main/java/com/mmorrell/jupiter/manager/JupiterManager.java +++ b/jupiter/src/main/java/com/mmorrell/jupiter/manager/JupiterManager.java @@ -106,7 +106,7 @@ public Optional getPerpetuals(PublicKey perpetualsPublicKey) * @return a list of JupiterDca objects. * @throws RpcException if the RPC call fails. */ - public List getAllDcaAccounts() { + public List getDcaAccounts() { byte[] dcaDiscriminator = JupiterUtil.getAccountDiscriminator("Dca"); // Create a memcmp filter for the discriminator at offset 0 @@ -133,7 +133,7 @@ public List getAllDcaAccounts() { } } - public List getAllDcaAccounts(PublicKey user) { + public List getDcaAccounts(PublicKey user) { byte[] dcaDiscriminator = JupiterUtil.getAccountDiscriminator("Dca"); // Create a memcmp filter for the discriminator at offset 0 diff --git a/jupiter/src/test/java/JupiterTest.java b/jupiter/src/test/java/JupiterTest.java index 50dd7b3..be85080 100644 --- a/jupiter/src/test/java/JupiterTest.java +++ b/jupiter/src/test/java/JupiterTest.java @@ -16,9 +16,6 @@ import java.io.File; import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; import java.time.Instant; import java.util.*; import java.util.stream.Collectors; @@ -36,16 +33,17 @@ public class JupiterTest { @BeforeEach public void setup() { try { - Thread.sleep(1001L); + Thread.sleep(1500L); } catch (InterruptedException e) { throw new RuntimeException(e); } } @Test + @Disabled public void testJupiterPerpPositionDeserialization() throws RpcException { - PublicKey positionPublicKey = new PublicKey("FdqbJAvADUJzZsBFK1ArhV79vXLmpKUMB4oXSrW8rSE"); - PublicKey positionPublicKeyOwner = new PublicKey("skynetDj29GH6o6bAqoixCpDuYtWqi1rm8ZNx1hB3vq"); + PublicKey positionPublicKey = new PublicKey("63sifZpCp9peUq4sfQfxruvKFUCkwLcRfUVVC2mSGDug"); + PublicKey positionPublicKeyOwner = new PublicKey("CMo1gA6YQebnSxXNYK8KawpczFaYLuUgyAf5FRAoryRQ"); // Fetch the account data AccountInfo accountInfo = client.getApi().getAccountInfo(positionPublicKey); @@ -78,6 +76,7 @@ public void testJupiterPerpPositionDeserialization() throws RpcException { } @Test + @Disabled public void testGetAllJupiterPerpPositions() throws RpcException { PublicKey programId = new PublicKey("PERPHjGBqRHArX4DySjwM6UJHiR3sWAatqfdBS2qQJu"); @@ -228,6 +227,7 @@ public void testJupiterPerpetualsDeserialization() throws RpcException, IOExcept } @Test + @Disabled public void testJupiterPositionRequestDeserialization() throws RpcException, IOException { PublicKey positionRequestPublicKey = new PublicKey("APYrGNtsTTMpNNBQBpALxYnwYfKDQyFocf3d1j6jkuzf"); @@ -275,7 +275,9 @@ public void testJupiterTestOracleDeserialization() throws RpcException { // Add more assertions as needed } + // Disabled since it relies on hardcoded accounts (and positions always close) @Test + @Disabled public void testJupiterManager() { JupiterManager manager = new JupiterManager(client); @@ -327,7 +329,7 @@ public void testJupiterManagerWithInvalidPublicKeys() { public void testGetAllJupiterDcaAccounts() { JupiterManager manager = new JupiterManager(client); - List dcaAccounts = manager.getAllDcaAccounts(); + List dcaAccounts = manager.getDcaAccounts(); assertNotNull(dcaAccounts, "DCA accounts list should not be null"); assertTrue(dcaAccounts.size() > 0, "DCA accounts list should contain at least one account"); @@ -357,7 +359,7 @@ public void testGetAllJupiterDcaAccounts() { @Test public void getMostRecentJupiterDcaAccounts() { JupiterManager manager = new JupiterManager(client); - List dcaAccounts = manager.getAllDcaAccounts(); + List dcaAccounts = manager.getDcaAccounts(); dcaAccounts.sort(Comparator.comparingLong(JupiterDca::getCreatedAt).reversed()); for (int i = 0; i < 10; i++) { @@ -387,9 +389,9 @@ public void testGetOpenDcaOrders() { tokenPrices.put("DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263", 0.00002479); // Example price for outputMint // Add more token prices as needed - List dcaAccounts = manager.getAllDcaAccounts(); + List dcaAccounts = manager.getDcaAccounts(); assertNotNull(dcaAccounts, "DCA accounts list should not be null"); - assertTrue(dcaAccounts.size() > 0, "DCA accounts list should contain at least one account"); + assertFalse(dcaAccounts.isEmpty(), "DCA accounts list should contain at least one account"); long now = Instant.now().getEpochSecond(); @@ -461,7 +463,7 @@ public void testGetOpenDcaOrders() { @Test public void testGetDcaAccountsByUserFiltering() { JupiterManager manager = new JupiterManager(client); - List jupiterDcas = manager.getAllDcaAccounts() + List jupiterDcas = manager.getDcaAccounts() .stream() .filter(jupiterDca -> jupiterDca.getUser().equals(new PublicKey("ESmavfhN3JKy3q3iJfP2FJYWNDRWVEkcKmzzVfetU5eB"))) .toList(); @@ -471,7 +473,7 @@ public void testGetDcaAccountsByUserFiltering() { @Test public void testGetDcaAccountsByUser() { JupiterManager manager = new JupiterManager(client); - List jupiterDcas = manager.getAllDcaAccounts(new PublicKey("ESmavfhN3JKy3q3iJfP2FJYWNDRWVEkcKmzzVfetU5eB")); + List jupiterDcas = manager.getDcaAccounts(new PublicKey("ESmavfhN3JKy3q3iJfP2FJYWNDRWVEkcKmzzVfetU5eB")); log.info("DCAs for user: {}", jupiterDcas); } } \ No newline at end of file From b7bb8b1e0ccd8901c13378e9b0883f2de529bd69 Mon Sep 17 00:00:00 2001 From: skynetcap <100323448+skynetcap@users.noreply.github.com> Date: Wed, 2 Oct 2024 14:31:42 -0700 Subject: [PATCH 8/8] Add DCA statistics and aggregation methods Introduced JupiterUserDcaStats class for aggregating user's DCA statistics. Enhanced JupiterManager with methods for DCA order filtering, aggregation, and detailed statistics. Updated test cases to validate these new functionalities. --- .../jupiter/manager/JupiterManager.java | 117 +++++++++++ .../mmorrell/jupiter/model/JupiterDca.java | 128 ++++++++++++ .../jupiter/model/JupiterUserDcaStats.java | 19 ++ jupiter/src/test/java/JupiterTest.java | 185 +++++++++++++++++- 4 files changed, 444 insertions(+), 5 deletions(-) create mode 100644 jupiter/src/main/java/com/mmorrell/jupiter/model/JupiterUserDcaStats.java diff --git a/jupiter/src/main/java/com/mmorrell/jupiter/manager/JupiterManager.java b/jupiter/src/main/java/com/mmorrell/jupiter/manager/JupiterManager.java index 1d5c81c..2340e23 100644 --- a/jupiter/src/main/java/com/mmorrell/jupiter/manager/JupiterManager.java +++ b/jupiter/src/main/java/com/mmorrell/jupiter/manager/JupiterManager.java @@ -2,6 +2,8 @@ import com.mmorrell.jupiter.model.*; import com.mmorrell.jupiter.util.JupiterUtil; +import java.util.Collections; + import lombok.extern.slf4j.Slf4j; import org.bitcoinj.core.Base58; import org.p2p.solanaj.core.PublicKey; @@ -13,6 +15,8 @@ import org.p2p.solanaj.rpc.types.ProgramAccount; import java.util.*; +import java.time.Instant; +import java.util.stream.Collectors; @Slf4j public class JupiterManager { @@ -26,6 +30,7 @@ public JupiterManager() { this.client = new RpcClient(Cluster.MAINNET); } + public JupiterManager(RpcClient client) { this.client = client; } @@ -160,4 +165,116 @@ public List getDcaAccounts(PublicKey user) { return Collections.emptyList(); } } + + public List getActiveDcaOrders() { + long now = Instant.now().getEpochSecond(); + return getDcaAccounts().stream() + .filter(dca -> dca.getNextCycleAt() > now && dca.getInUsed() < dca.getInDeposited()) + .collect(Collectors.toList()); + } + + public List getDcaOrdersByTokenPair(PublicKey inputMint, PublicKey outputMint) { + return getDcaAccounts().stream() + .filter(dca -> dca.getInputMint().equals(inputMint) && dca.getOutputMint().equals(outputMint)) + .collect(Collectors.toList()); + } + + public double getAggregatedDcaVolume(PublicKey inputMint, PublicKey outputMint, long startTime, long endTime) { + return getDcaAccounts().stream() + .filter(dca -> dca.getInputMint().equals(inputMint) + && dca.getOutputMint().equals(outputMint) + && dca.getCreatedAt() >= startTime + && dca.getCreatedAt() <= endTime) + .mapToDouble(dca -> (double) dca.getInDeposited() / Math.pow(10, 6)) + .sum(); + } + + public List, Long>> getMostPopularDcaPairs(int limit) { + Map, Long> pairCounts = getDcaAccounts().stream() + .collect(Collectors.groupingBy( + dca -> new AbstractMap.SimpleEntry<>(dca.getInputMint(), dca.getOutputMint()), + Collectors.counting() + )); + + return pairCounts.entrySet().stream() + .sorted(Map.Entry., Long>comparingByValue().reversed()) + .limit(limit) + .collect(Collectors.toList()); + } + + /** + * Retrieves completed Jupiter DCA orders. + * A DCA order is considered completed if it has fully utilized its deposited amount or has expired. + * + * @return a list of completed JupiterDca objects. + */ + public List getCompletedDcaOrders() { + long now = Instant.now().getEpochSecond(); + return getDcaAccounts().stream() + .filter(dca -> dca.getInUsed() >= dca.getInDeposited() || dca.getNextCycleAt() <= now) + .collect(Collectors.toList()); + } + + /** + * Retrieves Jupiter DCA orders created within a specific time range. + * + * @param startTime the start epoch time. + * @param endTime the end epoch time. + * @return a list of JupiterDca objects within the specified time range. + */ + public List getDcaOrdersByTimeRange(long startTime, long endTime) { + return getDcaAccounts().stream() + .filter(dca -> dca.getCreatedAt() >= startTime && dca.getCreatedAt() <= endTime) + .collect(Collectors.toList()); + } + + /** + * Retrieves Jupiter DCA orders sorted by the total deposited amount in descending order. + * + * @return a list of JupiterDca objects sorted by volume. + */ + public List getDcaOrdersSortedByVolume() { + return getDcaAccounts().stream() + .sorted(Comparator.comparingLong(JupiterDca::getInDeposited).reversed()) + .collect(Collectors.toList()); + } + + /** + * Retrieves aggregated statistics for a specific user's DCA activities. + * + * @param user the PublicKey of the user. + * @return a UserDcaStats object containing aggregated statistics. + */ + public JupiterUserDcaStats getUserDcaStatistics(PublicKey user) { + List userDcas = getDcaAccounts(user); + + long totalOrders = userDcas.size(); + double totalVolume = userDcas.stream() + .mapToDouble(dca -> (double) dca.getInDeposited() / Math.pow(10, 6)) + .sum(); + Set uniqueInputTokens = userDcas.stream() + .map(JupiterDca::getInputMint) + .collect(Collectors.toSet()); + Set uniqueOutputTokens = userDcas.stream() + .map(JupiterDca::getOutputMint) + .collect(Collectors.toSet()); + + return new JupiterUserDcaStats(totalOrders, totalVolume, uniqueInputTokens, uniqueOutputTokens); + } + + + /** + * Retrieves the most recent Jupiter DCA orders up to the specified limit. + * + * @param limit the maximum number of recent orders to retrieve. + * @return a list of recent JupiterDca objects. + */ + public List getRecentDcaOrders(int limit) { + return getDcaAccounts().stream() + .sorted(Comparator.comparingLong(JupiterDca::getCreatedAt).reversed()) + .limit(limit) + .collect(Collectors.toList()); + } + + // Additional methods can be added here as needed for other use cases. } \ No newline at end of file diff --git a/jupiter/src/main/java/com/mmorrell/jupiter/model/JupiterDca.java b/jupiter/src/main/java/com/mmorrell/jupiter/model/JupiterDca.java index cf70043..4ff85d8 100644 --- a/jupiter/src/main/java/com/mmorrell/jupiter/model/JupiterDca.java +++ b/jupiter/src/main/java/com/mmorrell/jupiter/model/JupiterDca.java @@ -5,12 +5,19 @@ import lombok.Data; import org.p2p.solanaj.core.PublicKey; +import java.math.BigDecimal; +import java.math.RoundingMode; + /** * Represents a Jupiter DCA (Dollar-Cost Averaging) account. */ @Data @Builder public class JupiterDca { + private static final PublicKey USDC_MINT = new PublicKey("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"); + private static final PublicKey USDT_MINT = new PublicKey("Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB"); + private static final int DECIMALS = 6; + private PublicKey user; private PublicKey inputMint; private PublicKey outputMint; @@ -33,6 +40,127 @@ public class JupiterDca { private long createdAt; private byte bump; + /** + * Checks if the input mint is a stablecoin (USDC or USDT). + * + * @return true if the input mint is USDC or USDT, false otherwise. + */ + public boolean isInputStablecoin() { + return inputMint.equals(USDC_MINT) || inputMint.equals(USDT_MINT); + } + + /** + * Checks if the output mint is a stablecoin (USDC or USDT). + * + * @return true if the output mint is USDC or USDT, false otherwise. + */ + public boolean isOutputStablecoin() { + return outputMint.equals(USDC_MINT) || outputMint.equals(USDT_MINT); + } + + /** + * Calculates the USD value of the deposited amount. + * + * @return the USD value of the deposited amount, or null if neither input nor output is a stablecoin. + */ + public BigDecimal getInDepositedUsd() { + return isInputStablecoin() ? convertToUsd(inDeposited) : + (isOutputStablecoin() ? convertToUsd(outReceived) : null); + } + + /** + * Calculates the USD value of the withdrawn amount. + * + * @return the USD value of the withdrawn amount, or null if neither input nor output is a stablecoin. + */ + public BigDecimal getInWithdrawnUsd() { + return isInputStablecoin() ? convertToUsd(inWithdrawn) : + (isOutputStablecoin() ? convertToUsd(outWithdrawn) : null); + } + + /** + * Calculates the USD value of the used amount. + * + * @return the USD value of the used amount, or null if neither input nor output is a stablecoin. + */ + public BigDecimal getInUsedUsd() { + return isInputStablecoin() ? convertToUsd(inUsed) : + (isOutputStablecoin() ? convertToUsd(outReceived) : null); + } + + /** + * Calculates the USD value of the amount per cycle. + * + * @return the USD value of the amount per cycle, or null if neither input nor output is a stablecoin. + */ + public BigDecimal getInAmountPerCycleUsd() { + return isInputStablecoin() ? convertToUsd(inAmountPerCycle) : null; + } + + /** + * Calculates the total notional value in the original token. + * + * @return the total notional value in the original token. + */ + public BigDecimal getTotalNotional() { + return convertToDecimal(inDeposited); + } + + /** + * Calculates the total notional value in USD. + * + * @return the total notional value in USD, or null if neither input nor output is a stablecoin. + */ + public BigDecimal getTotalNotionalUsd() { + return isInputStablecoin() ? convertToUsd(inDeposited) : + (isOutputStablecoin() ? convertToUsd(outReceived) : null); + } + + /** + * Calculates the remaining notional value in the original token. + * + * @return the remaining notional value in the original token. + */ + public BigDecimal getRemainingNotional() { + return convertToDecimal(inDeposited - inUsed); + } + + /** + * Calculates the remaining notional value in USD. + * + * @return the remaining notional value in USD, or null if neither input nor output is a stablecoin. + */ + public BigDecimal getRemainingNotionalUsd() { + if (isInputStablecoin()) { + return convertToUsd(inDeposited - inUsed); + } else if (isOutputStablecoin()) { + return convertToUsd(outReceived - outWithdrawn); + } + return null; + } + + /** + * Converts a token amount to its USD value. + * + * @param amount the token amount to convert. + * @return the USD value of the token amount. + */ + private BigDecimal convertToUsd(long amount) { + return BigDecimal.valueOf(amount) + .divide(BigDecimal.valueOf(Math.pow(10, DECIMALS)), 2, RoundingMode.HALF_UP); + } + + /** + * Converts a token amount to its decimal representation. + * + * @param amount the token amount to convert. + * @return the decimal representation of the token amount. + */ + private BigDecimal convertToDecimal(long amount) { + return BigDecimal.valueOf(amount) + .divide(BigDecimal.valueOf(Math.pow(10, DECIMALS)), DECIMALS, RoundingMode.HALF_UP); + } + /** * Deserializes a byte array into a JupiterDca object. * diff --git a/jupiter/src/main/java/com/mmorrell/jupiter/model/JupiterUserDcaStats.java b/jupiter/src/main/java/com/mmorrell/jupiter/model/JupiterUserDcaStats.java new file mode 100644 index 0000000..c0998a4 --- /dev/null +++ b/jupiter/src/main/java/com/mmorrell/jupiter/model/JupiterUserDcaStats.java @@ -0,0 +1,19 @@ +package com.mmorrell.jupiter.model; + +import lombok.AllArgsConstructor; +import lombok.Data; + +import java.util.Set; +import org.p2p.solanaj.core.PublicKey; + +/** + * Data Transfer Object for aggregating a user's DCA statistics. + */ +@Data +@AllArgsConstructor +public class JupiterUserDcaStats { + private long totalOrders; + private double totalVolumeUsd; + private Set uniqueInputTokens; + private Set uniqueOutputTokens; +} \ No newline at end of file diff --git a/jupiter/src/test/java/JupiterTest.java b/jupiter/src/test/java/JupiterTest.java index be85080..c8ba2b3 100644 --- a/jupiter/src/test/java/JupiterTest.java +++ b/jupiter/src/test/java/JupiterTest.java @@ -342,7 +342,7 @@ public void testGetAllJupiterDcaAccounts() { assertTrue(dca.getInUsed() >= 0, "DCA inUsed should be non-negative"); assertTrue(dca.getOutReceived() >= 0, "DCA outReceived should be non-negative"); assertTrue(dca.getInAmountPerCycle() > 0, "DCA inAmountPerCycle should be greater than 0"); - assertTrue(dca.getCycleFrequency() > 0, "DCA cycleFrequency should be greater than 0"); + assertTrue(dca.getCycleFrequency() > 0, "DCA cykleFrequency should be greater than 0"); assertTrue(dca.getNextCycleAmountLeft() >= 0, "DCA nextCycleAmountLeft should be non-negative"); assertNotNull(dca.getInAccount(), "DCA inAccount should not be null"); assertNotNull(dca.getOutAccount(), "DCA outAccount should not be null"); @@ -418,9 +418,9 @@ public void testGetOpenDcaOrders() { double outputOrderSize = inputOrderSize * inputPriceUsd / outputPriceUsd; boolean minInRange = (dca.getMinOutAmount() == 0) - || (outputOrderSize >= ((double) dca.getMinOutAmount() / Math.pow(10, 6))); // Assuming 6 decimals + || (outputOrderSize >= ((double) dca.getMinOutAmount() / Math.pow(10, 6))); // Assuming 6 decimals boolean maxInRange = (dca.getMaxOutAmount() == 0) - || (outputOrderSize <= ((double) dca.getMaxOutAmount() / Math.pow(10, 6))); // Assuming 6 decimals + || (outputOrderSize <= ((double) dca.getMaxOutAmount() / Math.pow(10, 6))); // Assuming 6 decimals return minInRange && maxInRange; }) @@ -448,14 +448,15 @@ public void testGetOpenDcaOrders() { double outputOrderSize = inputOrderSize * inputPriceUsd / outputPriceUsd; boolean minInRange = (dca.getMinOutAmount() == 0) - || (outputOrderSize >= ((double) dca.getMinOutAmount() / Math.pow(10, 6))); // Assuming 6 decimals + || (outputOrderSize >= ((double) dca.getMinOutAmount() / Math.pow(10, 6))); // Assuming 6 decimals boolean maxInRange = (dca.getMaxOutAmount() == 0) - || (outputOrderSize <= ((double) dca.getMaxOutAmount() / Math.pow(10, 6))); // Assuming 6 decimals + || (outputOrderSize <= ((double) dca.getMaxOutAmount() / Math.pow(10, 6))); // Assuming 6 decimals assertTrue(minInRange, "Output order size should be within the minimum range"); assertTrue(maxInRange, "Output order size should be within the maximum range"); }); + log.info("Open DCA Orders [{}]: {}", openDcaOrders.size(), openDcaOrders); log.info("Size: {}", openDcaOrders.size()); } @@ -476,4 +477,178 @@ public void testGetDcaAccountsByUser() { List jupiterDcas = manager.getDcaAccounts(new PublicKey("ESmavfhN3JKy3q3iJfP2FJYWNDRWVEkcKmzzVfetU5eB")); log.info("DCAs for user: {}", jupiterDcas); } + + /** + * Tests retrieving completed DCA orders. + */ + @Test + public void testGetCompletedDcaOrders() { + JupiterManager manager = new JupiterManager(client); + List completedDcaOrders = manager.getCompletedDcaOrders(); + + assertNotNull(completedDcaOrders, "Completed DCA orders list should not be null"); + // This assertion depends on expected data; adjust as necessary + // assertFalse(completedDcaOrders.isEmpty(), "Completed DCA orders list should contain at least one account"); + + // Verify each completed DCA order meets the completion criteria + completedDcaOrders.forEach(dca -> { + boolean isUsedFully = dca.getInUsed() >= dca.getInDeposited(); + boolean isExpired = dca.getNextCycleAt() <= Instant.now().getEpochSecond(); + assertTrue(isUsedFully || isExpired, "DCA order should be either fully used or expired"); + }); + + log.info("Completed DCA Orders [{}]: {}", completedDcaOrders.size(), completedDcaOrders); + } + + /** + * Tests retrieving DCA orders within a specific time range. + */ + @Test + public void testGetDcaOrdersByTimeRange() { + JupiterManager manager = new JupiterManager(client); + long startTime = Instant.now().minusSeconds(86400).getEpochSecond(); // 24 hours ago + long endTime = Instant.now().getEpochSecond(); + + List dcaOrders = manager.getDcaOrdersByTimeRange(startTime, endTime); + + assertNotNull(dcaOrders, "DCA orders list should not be null"); + // Adjust the following assertion based on expected data + // assertFalse(dcaOrders.isEmpty(), "DCA orders list should contain at least one account within the time range"); + + // Verify each DCA order falls within the specified time range + dcaOrders.forEach(dca -> { + assertTrue(dca.getCreatedAt() >= startTime && dca.getCreatedAt() <= endTime, + "DCA order should be within the specified time range"); + }); + + log.info("DCA Orders within Time Range [{} - {}]: {}", startTime, endTime, dcaOrders.size()); + } + + /** + * Tests retrieving DCA orders sorted by volume. + */ + @Test + public void testGetDcaOrdersSortedByVolume() { + JupiterManager manager = new JupiterManager(client); + List sortedDcaOrders = manager.getDcaOrdersSortedByVolume(); + + assertNotNull(sortedDcaOrders, "Sorted DCA orders list should not be null"); + assertFalse(sortedDcaOrders.isEmpty(), "Sorted DCA orders list should contain at least one account"); + + // Verify the list is sorted in descending order of inDeposited + for (int i = 0; i < sortedDcaOrders.size() - 1; i++) { + assertTrue(sortedDcaOrders.get(i).getInDeposited() >= sortedDcaOrders.get(i + 1).getInDeposited(), + "DCA orders should be sorted by deposited amount in descending order"); + } + + log.info("Sorted DCA Orders by Volume [{}]: {}", sortedDcaOrders.size(), sortedDcaOrders); + } + + /** + * Tests retrieving user-specific DCA statistics. + */ + @Test + public void testGetUserDcaStatistics() { + JupiterManager manager = new JupiterManager(client); + PublicKey userPublicKey = new PublicKey("ESmavfhN3JKy3q3iJfP2FJYWNDRWVEkcKmzzVfetU5eB"); + JupiterUserDcaStats stats = manager.getUserDcaStatistics(userPublicKey); + + assertNotNull(stats, "User DCA statistics should not be null"); + assertTrue(stats.getTotalOrders() >= 0, "Total orders should be non-negative"); + assertTrue(stats.getTotalVolumeUsd() >= 0, "Total volume should be non-negative"); + assertNotNull(stats.getUniqueInputTokens(), "Unique input tokens should not be null"); + assertNotNull(stats.getUniqueOutputTokens(), "Unique output tokens should not be null"); + + log.info("User DCA Statistics: {}", stats); + } + + /** + * Tests retrieving recent DCA orders. + */ + @Test + public void testGetRecentDcaOrders() { + JupiterManager manager = new JupiterManager(client); + int limit = 10; + List recentDcaOrders = manager.getRecentDcaOrders(limit); + + assertNotNull(recentDcaOrders, "Recent DCA orders list should not be null"); + assertTrue(recentDcaOrders.size() <= limit, "Recent DCA orders list should not exceed the specified limit"); + + // Verify the list is sorted in descending order of createdAt + for (int i = 0; i < recentDcaOrders.size() - 1; i++) { + assertTrue(recentDcaOrders.get(i).getCreatedAt() >= recentDcaOrders.get(i + 1).getCreatedAt(), + "DCA orders should be sorted by createdAt in descending order"); + } + + log.info("Recent DCA Orders [{}]: {}", recentDcaOrders.size(), recentDcaOrders); + } + + @Test + public void testGetActiveDcaOrders() { + JupiterManager manager = new JupiterManager(client); + List activeDcaOrders = manager.getActiveDcaOrders(); + + assertNotNull(activeDcaOrders, "Active DCA orders list should not be null"); + + activeDcaOrders.forEach(dca -> { + assertTrue(dca.getInUsed() < dca.getInDeposited(), "Used amount should be less than deposited amount"); + assertTrue(dca.getInDeposited() > 0, "Deposited amount should be greater than 0"); + assertTrue(dca.getInAmountPerCycle() > 0, "Amount per cycle should be greater than 0"); + assertTrue(dca.getCycleFrequency() > 0, "Cycle frequency should be greater than 0"); + assertNotNull(dca.getUser(), "User should not be null"); + assertNotNull(dca.getInputMint(), "Input mint should not be null"); + assertNotNull(dca.getOutputMint(), "Output mint should not be null"); + }); + + log.info("Active DCA Orders [{}]: {}", activeDcaOrders.size(), activeDcaOrders); + } + + @Test + public void testGetDcaOrdersByTokenPair() { + JupiterManager manager = new JupiterManager(client); + PublicKey inputMint = new PublicKey("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"); // USDC + PublicKey outputMint = new PublicKey("So11111111111111111111111111111111111111112"); // SOL + + List dcaOrdersByPair = manager.getDcaOrdersByTokenPair(inputMint, outputMint); + + assertNotNull(dcaOrdersByPair, "DCA orders by token pair list should not be null"); + dcaOrdersByPair.forEach(dca -> { + assertEquals(inputMint, dca.getInputMint(), "Input mint should match"); + assertEquals(outputMint, dca.getOutputMint(), "Output mint should match"); + }); + + log.info("DCA Orders for USDC-SOL pair [{}]: {}", dcaOrdersByPair.size(), dcaOrdersByPair); + } + + @Test + public void testGetAggregatedDcaVolume() { + JupiterManager manager = new JupiterManager(client); + PublicKey inputMint = new PublicKey("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"); // USDC + PublicKey outputMint = new PublicKey("So11111111111111111111111111111111111111112"); // SOL + long startTime = Instant.now().minusSeconds(86400 * 7).getEpochSecond(); // 7 days ago + long endTime = Instant.now().getEpochSecond(); + + double aggregatedVolume = manager.getAggregatedDcaVolume(inputMint, outputMint, startTime, endTime); + + assertTrue(aggregatedVolume >= 0, "Aggregated volume should be non-negative"); + log.info("Aggregated DCA Volume for USDC-SOL pair in the last 7 days: {}", aggregatedVolume); + } + + @Test + public void testGetMostPopularDcaPairs() { + JupiterManager manager = new JupiterManager(client); + int limit = 5; + + var popularPairs = manager.getMostPopularDcaPairs(limit); + + assertNotNull(popularPairs, "Popular DCA pairs list should not be null"); + assertTrue(popularPairs.size() <= limit, "Number of popular pairs should not exceed the limit"); + + for (int i = 0; i < popularPairs.size() - 1; i++) { + assertTrue(popularPairs.get(i).getValue() >= popularPairs.get(i + 1).getValue(), + "Pairs should be sorted by count in descending order"); + } + + log.info("Most Popular DCA Pairs [{}]: {}", popularPairs.size(), popularPairs); + } } \ No newline at end of file