From 2d4f179f3cb7ed102a953605d789420370d762c0 Mon Sep 17 00:00:00 2001 From: Chintan Patel Date: Tue, 13 Aug 2024 18:39:54 -0500 Subject: [PATCH 01/18] JUP-setup jupiter program to swap solana tokens and added functionality to deserialize transactions --- docs/README.md | 14 +- pom.xml | 14 +- .../java/org/p2p/solanaj/core/Account.java | 2 +- .../org/p2p/solanaj/core/AccountKeysList.java | 36 +-- .../org/p2p/solanaj/core/AccountMeta.java | 9 + .../org/p2p/solanaj/core/LegacyMessage.java | 170 ++++++++++++ .../p2p/solanaj/core/LegacyTransaction.java | 76 ++++++ .../core/LegacyTransactionBuilder.java | 35 +++ .../java/org/p2p/solanaj/core/Message.java | 251 ++++++++++++++---- .../java/org/p2p/solanaj/core/PublicKey.java | 19 +- .../org/p2p/solanaj/core/Transaction.java | 58 +++- .../p2p/solanaj/core/TransactionBuilder.java | 35 --- .../solanaj/programs/JupiterSwapProgram.java | 110 ++++++++ .../p2p/solanaj/programs/SystemProgram.java | 6 +- .../anchor/AnchorBasicTutorialProgram.java | 2 +- .../java/org/p2p/solanaj/rpc/Cluster.java | 8 +- .../p2p/solanaj/rpc/LoggingInterceptor.java | 2 + src/main/java/org/p2p/solanaj/rpc/RpcApi.java | 108 ++++++-- .../java/org/p2p/solanaj/rpc/RpcClient.java | 1 - .../org/p2p/solanaj/rpc/RpcException.java | 3 + .../solanaj/rpc/types/BlockProduction.java | 5 +- .../p2p/solanaj/rpc/types/ConfirmedBlock.java | 1 - .../rpc/types/ConfirmedSignFAddr2.java | 4 +- .../p2p/solanaj/rpc/types/ProgramAccount.java | 14 +- .../org/p2p/solanaj/rpc/types/RpcRequest.java | 8 +- .../org/p2p/solanaj/rpc/types/Supply.java | 1 - .../solanaj/rpc/types/config/Commitment.java | 6 +- .../types/config/ProgramAccountConfig.java | 12 +- .../config/RpcSendTransactionConfig.java | 4 +- .../org/p2p/solanaj/token/TokenManager.java | 30 +-- .../java/org/p2p/solanaj/utils/ByteUtils.java | 18 ++ .../p2p/solanaj/utils/GuardedArrayUtils.java | 42 +++ .../{ShortvecEncoding.java => Shortvec.java} | 18 +- .../org/p2p/solanaj/utils/TweetNaclFast.java | 56 ++-- .../solanaj/utils/bip32/wallet/HdAddress.java | 15 +- .../utils/bip32/wallet/SolanaBip44.java | 13 +- .../utils/bip32/wallet/SolanaCoin.java | 40 ++- .../solanaj/utils/bip32/wallet/key/HdKey.java | 46 +--- .../utils/bip32/wallet/key/HdPrivateKey.java | 12 +- .../utils/bip32/wallet/key/HdPublicKey.java | 12 +- .../utils/bip32/wallet/key/SolanaCurve.java | 6 +- .../p2p/solanaj/ws/SignatureNotification.java | 9 +- .../ws/SubscriptionWebSocketClient.java | 16 +- .../AccountNotificationEventListener.java | 1 - .../LogNotificationEventListener.java | 3 +- .../java/org/p2p/solanaj/core/AnchorTest.java | 8 +- ...essageTest.java => LegacyMessageTest.java} | 12 +- ...onTest.java => LegacyTransactionTest.java} | 16 +- .../org/p2p/solanaj/core/MainnetTest.java | 12 +- .../p2p/solanaj/programs/BPFLoaderTest.java | 12 +- .../solanaj/utils/ShortvecEncodingTest.java | 21 -- .../org/p2p/solanaj/utils/ShortvecTest.java | 21 ++ 52 files changed, 1033 insertions(+), 420 deletions(-) create mode 100644 src/main/java/org/p2p/solanaj/core/LegacyMessage.java create mode 100644 src/main/java/org/p2p/solanaj/core/LegacyTransaction.java create mode 100644 src/main/java/org/p2p/solanaj/core/LegacyTransactionBuilder.java delete mode 100644 src/main/java/org/p2p/solanaj/core/TransactionBuilder.java create mode 100644 src/main/java/org/p2p/solanaj/programs/JupiterSwapProgram.java create mode 100644 src/main/java/org/p2p/solanaj/utils/GuardedArrayUtils.java rename src/main/java/org/p2p/solanaj/utils/{ShortvecEncoding.java => Shortvec.java} (62%) rename src/test/java/org/p2p/solanaj/core/{MessageTest.java => LegacyMessageTest.java} (79%) rename src/test/java/org/p2p/solanaj/core/{TransactionTest.java => LegacyTransactionTest.java} (76%) delete mode 100644 src/test/java/org/p2p/solanaj/utils/ShortvecEncodingTest.java create mode 100644 src/test/java/org/p2p/solanaj/utils/ShortvecTest.java diff --git a/docs/README.md b/docs/README.md index 67d288be..bc166229 100644 --- a/docs/README.md +++ b/docs/README.md @@ -51,10 +51,10 @@ int lamports = 3000; Account signer = new Account(secret_key); -Transaction transaction = new Transaction(); -transaction.addInstruction(SystemProgram.transfer(fromPublicKey, toPublickKey, lamports)); +Transaction legacyTransaction = new Transaction(); +legacyTransaction.addInstruction(SystemProgram.transfer(fromPublicKey, toPublickKey, lamports)); -String signature = client.getApi().sendTransaction(transaction, signer); +String signature = client.getApi().sendTransaction(legacyTransaction, signer); ``` ##### Get balance @@ -77,18 +77,18 @@ final Market solUsdcMarket = new MarketBuilder() final OrderBook bids = solUsdcMarket.getBidOrderBook(); ``` -##### Send a transaction with call to the "Memo" program +##### Send a legacyTransaction with call to the "Memo" program ```java // Create account from private key final Account feePayer = new Account(Base58.decode(new String(data))); -final Transaction transaction = new Transaction(); +final Transaction legacyTransaction = new Transaction(); // Add instruction to write memo -transaction.addInstruction( +legacyTransaction.addInstruction( MemoProgram.writeUtf8(feePayer.getPublicKey(),"Hello from SolanaJ :)") ); -String response = result = client.getApi().sendTransaction(transaction, feePayer); +String response = result = client.getApi().sendTransaction(legacyTransaction, feePayer); ``` ## License diff --git a/pom.xml b/pom.xml index 66d2d5d2..ce09ae83 100644 --- a/pom.xml +++ b/pom.xml @@ -119,6 +119,18 @@ jackson-databind 2.17.2 + + + org.apache.httpcomponents.client5 + httpclient5 + 5.2.1 + + + + org.apache.commons + commons-lang3 + 3.15.0 + @@ -186,7 +198,7 @@ sign - c:/Users/Michael/.gnupg/ + /Users/chintan_mbp/.gnupg/ 0x27FAE7D2 diff --git a/src/main/java/org/p2p/solanaj/core/Account.java b/src/main/java/org/p2p/solanaj/core/Account.java index cc41eb67..54095897 100644 --- a/src/main/java/org/p2p/solanaj/core/Account.java +++ b/src/main/java/org/p2p/solanaj/core/Account.java @@ -10,7 +10,7 @@ import org.p2p.solanaj.utils.bip32.wallet.DerivableType; public class Account { - private TweetNaclFast.Signature.KeyPair keyPair; + private final TweetNaclFast.Signature.KeyPair keyPair; public Account() { this.keyPair = TweetNaclFast.Signature.keyPair(); diff --git a/src/main/java/org/p2p/solanaj/core/AccountKeysList.java b/src/main/java/org/p2p/solanaj/core/AccountKeysList.java index d4608a36..8dc448f1 100644 --- a/src/main/java/org/p2p/solanaj/core/AccountKeysList.java +++ b/src/main/java/org/p2p/solanaj/core/AccountKeysList.java @@ -6,10 +6,10 @@ import java.util.HashMap; public class AccountKeysList { - private HashMap accounts; + private final HashMap accounts; public AccountKeysList() { - accounts = new HashMap(); + accounts = new HashMap<>(); } public void add(AccountMeta accountMeta) { @@ -31,29 +31,31 @@ public void addAll(Collection metas) { } public ArrayList getList() { - ArrayList accountKeysList = new ArrayList(accounts.values()); + ArrayList accountKeysList = new ArrayList<>(accounts.values()); accountKeysList.sort(metaComparator); return accountKeysList; } - private static final Comparator metaComparator = new Comparator() { + private static final Comparator metaComparator = (am1, am2) -> { - @Override - public int compare(AccountMeta am1, AccountMeta am2) { - - int cmpSigner = am1.isSigner() == am2.isSigner() ? 0 : am1.isSigner() ? -1 : 1; - if (cmpSigner != 0) { - return cmpSigner; - } - - int cmpkWritable = am1.isWritable() == am2.isWritable() ? 0 : am1.isWritable() ? -1 : 1; - if (cmpkWritable != 0) { - return cmpkWritable; - } + int cmpSigner = am1.isSigner() == am2.isSigner() ? 0 : am1.isSigner() ? -1 : 1; + if (cmpSigner != 0) { + return cmpSigner; + } - return Integer.compare(cmpSigner, cmpkWritable); + int cmpkWritable = am1.isWritable() == am2.isWritable() ? 0 : am1.isWritable() ? -1 : 1; + if (cmpkWritable != 0) { + return cmpkWritable; } + + return Integer.compare(cmpSigner, cmpkWritable); }; + @Override + public String toString() { + return "AccountKeysList{" + + "accounts=" + accounts + + '}'; + } } diff --git a/src/main/java/org/p2p/solanaj/core/AccountMeta.java b/src/main/java/org/p2p/solanaj/core/AccountMeta.java index 58d47146..18cd0720 100644 --- a/src/main/java/org/p2p/solanaj/core/AccountMeta.java +++ b/src/main/java/org/p2p/solanaj/core/AccountMeta.java @@ -12,4 +12,13 @@ public class AccountMeta { private boolean isSigner; private boolean isWritable; + + @Override + public String toString() { + return "AccountMeta{" + + "publicKey=" + publicKey + + ", isSigner=" + isSigner + + ", isWritable=" + isWritable + + '}'; + } } \ No newline at end of file diff --git a/src/main/java/org/p2p/solanaj/core/LegacyMessage.java b/src/main/java/org/p2p/solanaj/core/LegacyMessage.java new file mode 100644 index 00000000..43ccacee --- /dev/null +++ b/src/main/java/org/p2p/solanaj/core/LegacyMessage.java @@ -0,0 +1,170 @@ +package org.p2p.solanaj.core; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; + +import org.bitcoinj.core.Base58; + +import org.p2p.solanaj.utils.Shortvec; + +public class LegacyMessage { + private static class MessageHeader { + static final int HEADER_LENGTH = 3; + + byte numRequiredSignatures = 0; + byte numReadonlySignedAccounts = 0; + byte numReadonlyUnsignedAccounts = 0; + + byte[] toByteArray() { + return new byte[] { numRequiredSignatures, numReadonlySignedAccounts, numReadonlyUnsignedAccounts }; + } + } + + private static class CompiledInstruction { + byte programIdIndex; + byte[] keyIndicesCount; + byte[] keyIndices; + byte[] dataLength; + byte[] data; + + int getLength() { + // 1 = programIdIndex length + return 1 + keyIndicesCount.length + keyIndices.length + dataLength.length + data.length; + } + } + + private static final int RECENT_BLOCK_HASH_LENGTH = 32; + + private String recentBlockhash; + private final AccountKeysList accountKeys; + private final List instructions; + private Account feePayer; + + public LegacyMessage() { + this.accountKeys = new AccountKeysList(); + this.instructions = new ArrayList<>(); + } + + public LegacyMessage addInstruction(TransactionInstruction instruction) { + accountKeys.addAll(instruction.getKeys()); + accountKeys.add(new AccountMeta(instruction.getProgramId(), false, false)); + instructions.add(instruction); + + return this; + } + + public void setRecentBlockHash(String recentBlockhash) { + this.recentBlockhash = recentBlockhash; + } + + public byte[] serialize() { + + if (recentBlockhash == null) { + throw new IllegalArgumentException("recentBlockhash required"); + } + + if (instructions.isEmpty()) { + throw new IllegalArgumentException("No instructions provided"); + } + + MessageHeader messageHeader = new MessageHeader(); + + List keysList = getAccountKeys(); + int accountKeysSize = keysList.size(); + + byte[] accountAddressesLength = Shortvec.encodeLength(accountKeysSize); + + int compiledInstructionsLength = 0; + List compiledInstructions = new ArrayList<>(); + + for (TransactionInstruction instruction : instructions) { + int keysSize = instruction.getKeys().size(); + + byte[] keyIndices = new byte[keysSize]; + for (int i = 0; i < keysSize; i++) { + keyIndices[i] = (byte) findAccountIndex(keysList, instruction.getKeys().get(i).getPublicKey()); + } + + CompiledInstruction compiledInstruction = new CompiledInstruction(); + compiledInstruction.programIdIndex = (byte) findAccountIndex(keysList, instruction.getProgramId()); + compiledInstruction.keyIndicesCount = Shortvec.encodeLength(keysSize); + compiledInstruction.keyIndices = keyIndices; + compiledInstruction.dataLength = Shortvec.encodeLength(instruction.getData().length); + compiledInstruction.data = instruction.getData(); + + compiledInstructions.add(compiledInstruction); + + compiledInstructionsLength += compiledInstruction.getLength(); + } + + byte[] instructionsLength = Shortvec.encodeLength(compiledInstructions.size()); + + int bufferSize = MessageHeader.HEADER_LENGTH + RECENT_BLOCK_HASH_LENGTH + accountAddressesLength.length + + (accountKeysSize * PublicKey.PUBLIC_KEY_LENGTH) + instructionsLength.length + + compiledInstructionsLength; + + ByteBuffer out = ByteBuffer.allocate(bufferSize); + + ByteBuffer accountKeysBuff = ByteBuffer.allocate(accountKeysSize * PublicKey.PUBLIC_KEY_LENGTH); + for (AccountMeta accountMeta : keysList) { + accountKeysBuff.put(accountMeta.getPublicKey().toByteArray()); + + if (accountMeta.isSigner()) { + messageHeader.numRequiredSignatures += 1; + if (!accountMeta.isWritable()) { + messageHeader.numReadonlySignedAccounts += 1; + } + } else { + if (!accountMeta.isWritable()) { + messageHeader.numReadonlyUnsignedAccounts += 1; + } + } + } + + out.put(messageHeader.toByteArray()); + + out.put(accountAddressesLength); + out.put(accountKeysBuff.array()); + + out.put(Base58.decode(recentBlockhash)); + + out.put(instructionsLength); + for (CompiledInstruction compiledInstruction : compiledInstructions) { + out.put(compiledInstruction.programIdIndex); + out.put(compiledInstruction.keyIndicesCount); + out.put(compiledInstruction.keyIndices); + out.put(compiledInstruction.dataLength); + out.put(compiledInstruction.data); + } + + return out.array(); + } + + protected void setFeePayer(Account feePayer) { + this.feePayer = feePayer; + } + + private List getAccountKeys() { + List keysList = accountKeys.getList(); + int feePayerIndex = findAccountIndex(keysList, feePayer.getPublicKey()); + + List newList = new ArrayList<>(); + AccountMeta feePayerMeta = keysList.get(feePayerIndex); + newList.add(new AccountMeta(feePayerMeta.getPublicKey(), true, true)); + keysList.remove(feePayerIndex); + newList.addAll(keysList); + + return newList; + } + + private int findAccountIndex(List accountMetaList, PublicKey key) { + for (int i = 0; i < accountMetaList.size(); i++) { + if (accountMetaList.get(i).getPublicKey().equals(key)) { + return i; + } + } + + throw new RuntimeException("unable to find account index"); + } +} diff --git a/src/main/java/org/p2p/solanaj/core/LegacyTransaction.java b/src/main/java/org/p2p/solanaj/core/LegacyTransaction.java new file mode 100644 index 00000000..01e50a27 --- /dev/null +++ b/src/main/java/org/p2p/solanaj/core/LegacyTransaction.java @@ -0,0 +1,76 @@ +package org.p2p.solanaj.core; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.bitcoinj.core.Base58; +import org.p2p.solanaj.utils.Shortvec; +import org.p2p.solanaj.utils.TweetNaclFast; + +public class LegacyTransaction { + + public static final int SIGNATURE_LENGTH = 64; + + private final LegacyMessage legacyMessage; + private final List signatures; + private byte[] serializedLegacyMessage; + + public LegacyTransaction() { + this.legacyMessage = new LegacyMessage(); + this.signatures = new ArrayList<>(); + } + + public LegacyTransaction addInstruction(TransactionInstruction instruction) { + legacyMessage.addInstruction(instruction); + + return this; + } + + public void setRecentBlockHash(String recentBlockhash) { + legacyMessage.setRecentBlockHash(recentBlockhash); + } + + public void sign(Account signer) { + sign(Collections.singletonList(signer)); + } + + public void sign(List signers) { + + if (signers.isEmpty()) { + throw new IllegalArgumentException("No signers"); + } + + Account feePayer = signers.get(0); + legacyMessage.setFeePayer(feePayer); + + serializedLegacyMessage = legacyMessage.serialize(); + + for (Account signer : signers) { + TweetNaclFast.Signature signatureProvider = new TweetNaclFast.Signature(new byte[0], signer.getSecretKey()); + byte[] signature = signatureProvider.detached(serializedLegacyMessage); + + signatures.add(Base58.encode(signature)); + } + } + + public byte[] serialize() { + int signaturesSize = signatures.size(); + byte[] signaturesLength = Shortvec.encodeLength(signaturesSize); + + ByteBuffer out = ByteBuffer + .allocate(signaturesLength.length + signaturesSize * SIGNATURE_LENGTH + serializedLegacyMessage.length); + + out.put(signaturesLength); + + for (String signature : signatures) { + byte[] rawSignature = Base58.decode(signature); + out.put(rawSignature); + } + + out.put(serializedLegacyMessage); + + return out.array(); + } +} \ No newline at end of file diff --git a/src/main/java/org/p2p/solanaj/core/LegacyTransactionBuilder.java b/src/main/java/org/p2p/solanaj/core/LegacyTransactionBuilder.java new file mode 100644 index 00000000..1412f286 --- /dev/null +++ b/src/main/java/org/p2p/solanaj/core/LegacyTransactionBuilder.java @@ -0,0 +1,35 @@ +package org.p2p.solanaj.core; + +import java.util.List; + +/** + * Builder for constructing {@link LegacyTransaction} objects to be used in sendLegacyTransaction. + */ +public class LegacyTransactionBuilder { + + private final LegacyTransaction legacyTransaction; + + public LegacyTransactionBuilder() { + legacyTransaction = new LegacyTransaction(); + } + + public LegacyTransactionBuilder addInstruction(TransactionInstruction transactionInstruction) { + legacyTransaction.addInstruction(transactionInstruction); + return this; + } + + public LegacyTransactionBuilder setRecentBlockHash(String recentBlockHash) { + legacyTransaction.setRecentBlockHash(recentBlockHash); + return this; + } + + public LegacyTransactionBuilder setSigners(List signers) { + legacyTransaction.sign(signers); + return this; + } + + public LegacyTransaction build() { + return legacyTransaction; + } + +} diff --git a/src/main/java/org/p2p/solanaj/core/Message.java b/src/main/java/org/p2p/solanaj/core/Message.java index 3badab14..bb7420e1 100644 --- a/src/main/java/org/p2p/solanaj/core/Message.java +++ b/src/main/java/org/p2p/solanaj/core/Message.java @@ -2,56 +2,137 @@ import java.nio.ByteBuffer; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import lombok.Getter; import org.bitcoinj.core.Base58; -import org.p2p.solanaj.utils.ShortvecEncoding; +import org.p2p.solanaj.utils.GuardedArrayUtils; +import org.p2p.solanaj.utils.Shortvec; public class Message { - private class MessageHeader { + @Getter + public static class MessageHeader { static final int HEADER_LENGTH = 3; - byte numRequiredSignatures = 0; - byte numReadonlySignedAccounts = 0; - byte numReadonlyUnsignedAccounts = 0; + private final byte numRequiredSignatures; + private final byte numReadonlySignedAccounts; + private final byte numReadonlyUnsignedAccounts; byte[] toByteArray() { return new byte[] { numRequiredSignatures, numReadonlySignedAccounts, numReadonlyUnsignedAccounts }; } + + MessageHeader(byte[] byteArray) { + numRequiredSignatures = byteArray[0]; + numReadonlySignedAccounts = byteArray[1]; + numReadonlyUnsignedAccounts = byteArray[2]; + } + + @Override + public String toString() { + return "MessageHeader{" + + "numRequiredSignatures=" + numRequiredSignatures + + ", numReadonlySignedAccounts=" + numReadonlySignedAccounts + + ", numReadonlyUnsignedAccounts=" + numReadonlyUnsignedAccounts + + '}'; + } } - private class CompiledInstruction { - byte programIdIndex; - byte[] keyIndicesCount; - byte[] keyIndices; - byte[] dataLength; - byte[] data; + @Getter + public static class CompiledInstruction { + private byte programIdIndex; + private byte[] keyIndicesCount; + private byte[] keyIndices; + private byte[] dataLength; + private byte[] data; int getLength() { // 1 = programIdIndex length return 1 + keyIndicesCount.length + keyIndices.length + dataLength.length + data.length; } + + @Override + public String toString() { + return "CompiledInstruction{" + + "programIdIndex=" + programIdIndex + + ", keyIndicesCount=" + Arrays.toString(keyIndicesCount) + + ", keyIndices=" + Arrays.toString(keyIndices) + + ", dataLength=" + Arrays.toString(dataLength) + + ", data=" + Arrays.toString(data) + + '}'; + } + } + + @Getter + public static class MessageAddressTableLookup { + private PublicKey accountKey; + private byte[] writableIndexesCountLength; + private byte[] writableIndexes; + private byte[] readonlyIndexesCountLength; + private byte[] readonlyIndexes; + + int getLength() { + // 1 = programIdIndex length + return PublicKey.PUBLIC_KEY_LENGTH + writableIndexesCountLength.length + writableIndexes.length + + readonlyIndexesCountLength.length + readonlyIndexes.length; + } + + @Override + public String toString() { + return "MessageAddressTableLookup{" + + "accountKey=" + accountKey + + ", writableIndexesCountLength=" + Arrays.toString(writableIndexesCountLength) + + ", writableIndexes=" + Arrays.toString(writableIndexes) + + ", readonlyIndexesCountLength=" + Arrays.toString(readonlyIndexesCountLength) + + ", readonlyIndexes=" + Arrays.toString(readonlyIndexes) + + '}'; + } } private static final int RECENT_BLOCK_HASH_LENGTH = 32; private MessageHeader messageHeader; private String recentBlockhash; - private AccountKeysList accountKeys; - private List instructions; + private final AccountKeysList accountKeys; + private final List compiledInstructions; + private final List addressTableLookups; private Account feePayer; public Message() { this.accountKeys = new AccountKeysList(); - this.instructions = new ArrayList(); + this.compiledInstructions = new ArrayList<>(); + this.addressTableLookups = new ArrayList<>(); + } + + public Message(MessageHeader messageHeader, String recentBlockhash, AccountKeysList accountKeys, + List compiledInstructions, List addressTableLookups) { + this.messageHeader = messageHeader; + this.recentBlockhash = recentBlockhash; + this.accountKeys = accountKeys; + this.compiledInstructions = compiledInstructions; + this.addressTableLookups = addressTableLookups; } public Message addInstruction(TransactionInstruction instruction) { accountKeys.addAll(instruction.getKeys()); accountKeys.add(new AccountMeta(instruction.getProgramId(), false, false)); - instructions.add(instruction); + List keysList = getAccountKeys(); + int keysSize = instruction.getKeys().size(); + + CompiledInstruction compiledInstruction = new CompiledInstruction(); + compiledInstruction.programIdIndex = (byte) findAccountIndex(keysList, instruction.getProgramId()); + compiledInstruction.keyIndicesCount = Shortvec.encodeLength(keysSize); + byte[] keyIndices = new byte[keysSize]; + for (int i = 0; i < instruction.getKeys().size(); i++) { + keyIndices[i] = (byte) findAccountIndex(keysList, instruction.getKeys().get(i).getPublicKey()); + } + compiledInstruction.keyIndices = keyIndices; + compiledInstruction.dataLength = Shortvec.encodeLength(instruction.getData().length); + compiledInstruction.data = instruction.getData(); + compiledInstructions.add(compiledInstruction); return this; } @@ -65,62 +146,35 @@ public byte[] serialize() { throw new IllegalArgumentException("recentBlockhash required"); } - if (instructions.size() == 0) { + if (compiledInstructions.isEmpty()) { throw new IllegalArgumentException("No instructions provided"); } - messageHeader = new MessageHeader(); - List keysList = getAccountKeys(); int accountKeysSize = keysList.size(); - byte[] accountAddressesLength = ShortvecEncoding.encodeLength(accountKeysSize); + byte[] accountAddressesLength = Shortvec.encodeLength(accountKeysSize); + byte[] instructionsCountLength = Shortvec.encodeLength(compiledInstructions.size()); int compiledInstructionsLength = 0; - List compiledInstructions = new ArrayList(); - - for (TransactionInstruction instruction : instructions) { - int keysSize = instruction.getKeys().size(); - - byte[] keyIndices = new byte[keysSize]; - for (int i = 0; i < keysSize; i++) { - keyIndices[i] = (byte) findAccountIndex(keysList, instruction.getKeys().get(i).getPublicKey()); - } - - CompiledInstruction compiledInstruction = new CompiledInstruction(); - compiledInstruction.programIdIndex = (byte) findAccountIndex(keysList, instruction.getProgramId()); - compiledInstruction.keyIndicesCount = ShortvecEncoding.encodeLength(keysSize); - compiledInstruction.keyIndices = keyIndices; - compiledInstruction.dataLength = ShortvecEncoding.encodeLength(instruction.getData().length); - compiledInstruction.data = instruction.getData(); - - compiledInstructions.add(compiledInstruction); - + for (CompiledInstruction compiledInstruction : this.compiledInstructions) { compiledInstructionsLength += compiledInstruction.getLength(); } - byte[] instructionsLength = ShortvecEncoding.encodeLength(compiledInstructions.size()); - + byte[] addressTableLookupsCountLength = Shortvec.encodeLength(addressTableLookups.size()); + int addressTableLookupsLength = 0; + for (MessageAddressTableLookup addressTableLookup : this.addressTableLookups) { + addressTableLookupsLength += addressTableLookup.getLength(); + } int bufferSize = MessageHeader.HEADER_LENGTH + RECENT_BLOCK_HASH_LENGTH + accountAddressesLength.length - + (accountKeysSize * PublicKey.PUBLIC_KEY_LENGTH) + instructionsLength.length - + compiledInstructionsLength; + + (accountKeysSize * PublicKey.PUBLIC_KEY_LENGTH) + instructionsCountLength.length + + compiledInstructionsLength + addressTableLookupsCountLength.length + addressTableLookupsLength; ByteBuffer out = ByteBuffer.allocate(bufferSize); ByteBuffer accountKeysBuff = ByteBuffer.allocate(accountKeysSize * PublicKey.PUBLIC_KEY_LENGTH); for (AccountMeta accountMeta : keysList) { accountKeysBuff.put(accountMeta.getPublicKey().toByteArray()); - - if (accountMeta.isSigner()) { - messageHeader.numRequiredSignatures += 1; - if (!accountMeta.isWritable()) { - messageHeader.numReadonlySignedAccounts += 1; - } - } else { - if (!accountMeta.isWritable()) { - messageHeader.numReadonlyUnsignedAccounts += 1; - } - } } out.put(messageHeader.toByteArray()); @@ -129,8 +183,7 @@ public byte[] serialize() { out.put(accountKeysBuff.array()); out.put(Base58.decode(recentBlockhash)); - - out.put(instructionsLength); + out.put(instructionsCountLength); for (CompiledInstruction compiledInstruction : compiledInstructions) { out.put(compiledInstruction.programIdIndex); out.put(compiledInstruction.keyIndicesCount); @@ -139,18 +192,88 @@ public byte[] serialize() { out.put(compiledInstruction.data); } + out.put(addressTableLookupsCountLength); + for (MessageAddressTableLookup addressTableLookup : addressTableLookups) { + out.put(addressTableLookup.accountKey.toByteArray()); + out.put(addressTableLookup.writableIndexesCountLength); + out.put(addressTableLookup.writableIndexes); + out.put(addressTableLookup.readonlyIndexesCountLength); + out.put(addressTableLookup.readonlyIndexes); + } + return out.array(); } + public static Message deserialize(List serializedMessageList) { + // Remove the byte as it is used to indicate legacy Transaction. + GuardedArrayUtils.guardedShift(serializedMessageList); + + // Remove three bytes for header + byte[] messageHeaderBytes = GuardedArrayUtils.guardedSplice(serializedMessageList, 0, MessageHeader.HEADER_LENGTH); + MessageHeader messageHeader = new MessageHeader(messageHeaderBytes); + + // Total static account keys + int accountKeysSize = Shortvec.decodeLength(serializedMessageList); + List accountKeys = new ArrayList<>(accountKeysSize); + for (int i = 0; i < accountKeysSize; i++) { + byte[] accountMetaPublicKeyByteArray = GuardedArrayUtils.guardedSplice(serializedMessageList, 0, + PublicKey.PUBLIC_KEY_LENGTH); + PublicKey publicKey = new PublicKey(accountMetaPublicKeyByteArray); + accountKeys.add(new AccountMeta(publicKey, false, false)); + } + AccountKeysList accountKeysList = new AccountKeysList(); + accountKeysList.addAll(accountKeys); + + // recent_blockhash + String recentBlockHash = Base58.encode(GuardedArrayUtils.guardedSplice(serializedMessageList, 0, + PublicKey.PUBLIC_KEY_LENGTH)); + + // Deserialize instructions + int instructionsLength = Shortvec.decodeLength(serializedMessageList); + List compiledInstructions = new ArrayList<>(instructionsLength); + for (int i = 0; i < instructionsLength; i++) { + CompiledInstruction compiledInstruction = new CompiledInstruction(); + compiledInstruction.programIdIndex = GuardedArrayUtils.guardedShift(serializedMessageList); + int keysSize = Shortvec.decodeLength(serializedMessageList); // keysSize + compiledInstruction.keyIndicesCount = Shortvec.encodeLength(keysSize); + compiledInstruction.keyIndices = GuardedArrayUtils.guardedSplice(serializedMessageList, 0, keysSize); + var dataLength = Shortvec.decodeLength(serializedMessageList); + compiledInstruction.dataLength = Shortvec.encodeLength(dataLength); + compiledInstruction.data = GuardedArrayUtils.guardedSplice(serializedMessageList, 0, dataLength); + + compiledInstructions.add(compiledInstruction); + + } + + // Deserialize addressTableLookups + int addressTableLookupsLength = Shortvec.decodeLength(serializedMessageList); + List addressTableLookups = new ArrayList<>(addressTableLookupsLength); + for (int i = 0; i < addressTableLookupsLength; i++) { + MessageAddressTableLookup addressTableLookup = new MessageAddressTableLookup(); + byte[] accountKeyByteArray = GuardedArrayUtils.guardedSplice(serializedMessageList, 0, PublicKey.PUBLIC_KEY_LENGTH); + addressTableLookup.accountKey = new PublicKey(accountKeyByteArray); + int writableIndexesLength = Shortvec.decodeLength(serializedMessageList); // keysSize + addressTableLookup.writableIndexesCountLength = Shortvec.encodeLength(writableIndexesLength); + addressTableLookup.writableIndexes = GuardedArrayUtils.guardedSplice(serializedMessageList, 0, writableIndexesLength); + int readonlyIndexesLength = Shortvec.decodeLength(serializedMessageList); + addressTableLookup.readonlyIndexesCountLength = Shortvec.encodeLength(readonlyIndexesLength); + addressTableLookup.readonlyIndexes = GuardedArrayUtils.guardedSplice(serializedMessageList, 0, readonlyIndexesLength); + + addressTableLookups.add(addressTableLookup); + } + + return new Message(messageHeader, recentBlockHash, accountKeysList, compiledInstructions, addressTableLookups); + } + protected void setFeePayer(Account feePayer) { this.feePayer = feePayer; } - private List getAccountKeys() { + public List getAccountKeys() { List keysList = accountKeys.getList(); int feePayerIndex = findAccountIndex(keysList, feePayer.getPublicKey()); - List newList = new ArrayList(); + List newList = new ArrayList<>(); AccountMeta feePayerMeta = keysList.get(feePayerIndex); newList.add(new AccountMeta(feePayerMeta.getPublicKey(), true, true)); keysList.remove(feePayerIndex); @@ -159,7 +282,7 @@ private List getAccountKeys() { return newList; } - private int findAccountIndex(List accountMetaList, PublicKey key) { + public int findAccountIndex(List accountMetaList, PublicKey key) { for (int i = 0; i < accountMetaList.size(); i++) { if (accountMetaList.get(i).getPublicKey().equals(key)) { return i; @@ -168,4 +291,16 @@ private int findAccountIndex(List accountMetaList, PublicKey key) { throw new RuntimeException("unable to find account index"); } + + @Override + public String toString() { + return "Message{" + + "messageHeader=" + messageHeader + + ", recentBlockhash='" + recentBlockhash + '\'' + + ", accountKeys=" + accountKeys + + ", compiledInstructions=" + compiledInstructions + + ", addressTableLookups=" + addressTableLookups + + ", feePayer=" + feePayer + + '}'; + } } diff --git a/src/main/java/org/p2p/solanaj/core/PublicKey.java b/src/main/java/org/p2p/solanaj/core/PublicKey.java index 64af1818..cf8deda5 100644 --- a/src/main/java/org/p2p/solanaj/core/PublicKey.java +++ b/src/main/java/org/p2p/solanaj/core/PublicKey.java @@ -6,6 +6,7 @@ import java.util.List; import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import lombok.Getter; import org.bitcoinj.core.Base58; import org.bitcoinj.core.Sha256Hash; import org.p2p.solanaj.utils.ByteUtils; @@ -17,7 +18,7 @@ public class PublicKey { public static final int PUBLIC_KEY_LENGTH = 32; - private byte[] pubkey; + private final byte[] pubkey; public PublicKey(String pubkey) { if (pubkey.length() < PUBLIC_KEY_LENGTH) { @@ -97,31 +98,23 @@ public static PublicKey createProgramAddress(List seeds, PublicKey progr return new PublicKey(hash); } + @Getter public static class ProgramDerivedAddress { - private PublicKey address; - private int nonce; + private final PublicKey address; + private final int nonce; public ProgramDerivedAddress(PublicKey address, int nonce) { this.address = address; this.nonce = nonce; } - public PublicKey getAddress() { - return address; - } - - public int getNonce() { - return nonce; - } - } public static ProgramDerivedAddress findProgramAddress(List seeds, PublicKey programId) throws Exception { int nonce = 255; PublicKey address; - List seedsWithNonce = new ArrayList(); - seedsWithNonce.addAll(seeds); + List seedsWithNonce = new ArrayList<>(seeds); while (nonce != 0) { try { diff --git a/src/main/java/org/p2p/solanaj/core/Transaction.java b/src/main/java/org/p2p/solanaj/core/Transaction.java index 397c7ba5..8b9590f7 100644 --- a/src/main/java/org/p2p/solanaj/core/Transaction.java +++ b/src/main/java/org/p2p/solanaj/core/Transaction.java @@ -3,23 +3,31 @@ import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; import org.bitcoinj.core.Base58; -import org.p2p.solanaj.utils.ShortvecEncoding; +import org.p2p.solanaj.utils.ByteUtils; +import org.p2p.solanaj.utils.GuardedArrayUtils; +import org.p2p.solanaj.utils.Shortvec; import org.p2p.solanaj.utils.TweetNaclFast; public class Transaction { public static final int SIGNATURE_LENGTH = 64; - private Message message; - private List signatures; + private final Message message; + private final List signatures; private byte[] serializedMessage; public Transaction() { this.message = new Message(); - this.signatures = new ArrayList(); + this.signatures = new ArrayList<>(); + } + + public Transaction(Message message, List signatures) { + this.message = message; + this.signatures = signatures; } public Transaction addInstruction(TransactionInstruction instruction) { @@ -33,31 +41,38 @@ public void setRecentBlockHash(String recentBlockhash) { } public void sign(Account signer) { - sign(Arrays.asList(signer)); + sign(Collections.singletonList(signer)); } public void sign(List signers) { - - if (signers.size() == 0) { + if (signers.isEmpty()) { throw new IllegalArgumentException("No signers"); } Account feePayer = signers.get(0); message.setFeePayer(feePayer); + List signerPubKeys = List.copyOf(message.getAccountKeys()); + signerPubKeys = signerPubKeys.subList(0, signers.size()); + serializedMessage = message.serialize(); for (Account signer : signers) { + int signerIndex = message.findAccountIndex(signerPubKeys, signer.getPublicKey()); + if (signerIndex < 0) { + throw new IllegalArgumentException("Cannot sign with non signer key: " + + signer.getPublicKey().toBase58()); + } TweetNaclFast.Signature signatureProvider = new TweetNaclFast.Signature(new byte[0], signer.getSecretKey()); byte[] signature = signatureProvider.detached(serializedMessage); - signatures.add(Base58.encode(signature)); + this.signatures.set(signerIndex, Base58.encode(signature)); } } public byte[] serialize() { int signaturesSize = signatures.size(); - byte[] signaturesLength = ShortvecEncoding.encodeLength(signaturesSize); + byte[] signaturesLength = Shortvec.encodeLength(signaturesSize); ByteBuffer out = ByteBuffer .allocate(signaturesLength.length + signaturesSize * SIGNATURE_LENGTH + serializedMessage.length); @@ -73,4 +88,29 @@ public byte[] serialize() { return out.array(); } + + public static Transaction deserialize(byte[] serializedTransaction) { + List serializedTransactionList = ByteUtils.toByteList(serializedTransaction); + + int signaturesSize = Shortvec.decodeLength(serializedTransactionList); + List signatures = new ArrayList<>(signaturesSize); + + for (int i = 0; i < signaturesSize; i++) { + + byte[] signatureBytes = GuardedArrayUtils.guardedSplice(serializedTransactionList, 0, SIGNATURE_LENGTH); + signatures.add(Base58.encode(signatureBytes)); + } + + Message message = Message.deserialize(serializedTransactionList); + return new Transaction(message, signatures); + } + + @Override + public String toString() { + return "Transaction{" + + "message=" + message + + ", signatures=" + signatures + + ", serializedMessage=" + Arrays.toString(serializedMessage) + + '}'; + } } diff --git a/src/main/java/org/p2p/solanaj/core/TransactionBuilder.java b/src/main/java/org/p2p/solanaj/core/TransactionBuilder.java deleted file mode 100644 index bc8b15fc..00000000 --- a/src/main/java/org/p2p/solanaj/core/TransactionBuilder.java +++ /dev/null @@ -1,35 +0,0 @@ -package org.p2p.solanaj.core; - -import java.util.List; - -/** - * Builder for constructing {@link Transaction} objects to be used in sendTransaction. - */ -public class TransactionBuilder { - - private final Transaction transaction; - - public TransactionBuilder() { - transaction = new Transaction(); - } - - public TransactionBuilder addInstruction(TransactionInstruction transactionInstruction) { - transaction.addInstruction(transactionInstruction); - return this; - } - - public TransactionBuilder setRecentBlockHash(String recentBlockHash) { - transaction.setRecentBlockHash(recentBlockHash); - return this; - } - - public TransactionBuilder setSigners(List signers) { - transaction.sign(signers); - return this; - } - - public Transaction build() { - return transaction; - } - -} diff --git a/src/main/java/org/p2p/solanaj/programs/JupiterSwapProgram.java b/src/main/java/org/p2p/solanaj/programs/JupiterSwapProgram.java new file mode 100644 index 00000000..1a631203 --- /dev/null +++ b/src/main/java/org/p2p/solanaj/programs/JupiterSwapProgram.java @@ -0,0 +1,110 @@ +package org.p2p.solanaj.programs; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.hc.client5.http.classic.methods.HttpGet; +import org.apache.hc.client5.http.classic.methods.HttpPost; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.client5.http.impl.classic.HttpClients; +import org.apache.hc.core5.http.HttpEntity; +import org.apache.hc.core5.http.io.HttpClientResponseHandler; +import org.apache.hc.core5.http.io.entity.EntityUtils; +import org.apache.hc.core5.http.io.entity.StringEntity; +import org.apache.hc.core5.net.URIBuilder; +import org.p2p.solanaj.core.Account; +import org.p2p.solanaj.core.Transaction; +import org.p2p.solanaj.rpc.RpcClient; +import org.p2p.solanaj.rpc.RpcException; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Base64; + +public final class JupiterSwapProgram { + + public static final String SOL_QUOTE_TOKEN = "So11111111111111111111111111111111111111112"; + + private static final CloseableHttpClient httpClient = HttpClients.createDefault(); + private static final ObjectMapper objectMapper = new ObjectMapper(); + + private static final HttpClientResponseHandler handler = response -> { + int status = response.getCode(); + if (status >= 200 && status < 300) { + HttpEntity entity = response.getEntity(); + return entity != null ? objectMapper.readTree(EntityUtils.toString(entity)) : null; + } else { + throw new IOException("Unexpected response status: " + status); + } + }; + + public static URI createQuoteUri(String inputToken, String outputToken, String amount, String slippage) { + try { + return new URIBuilder("https://quote-api.jup.ag/v6/quote") + .addParameter("inputMint", inputToken) + .addParameter("outputMint", outputToken) + .addParameter("amount", amount) + .addParameter("slippageBps", slippage) + .build(); + } catch (URISyntaxException e) { + throw new RuntimeException("Failed to create quote URI: ", e); + } + } + + public static JsonNode getJupiterQuote(URI quoteUri) { + HttpGet quoteRequest = new HttpGet(quoteUri); + + JsonNode quote; + try { + quote = httpClient.execute(quoteRequest, handler); + } catch (IOException e) { + throw new RuntimeException("Failed to retrieve quote from jupiter for " + quoteUri, e); + } + return quote; + } + + public static String swapToken(RpcClient rpcClient, Account account, JsonNode quote) { + SwapRequest swapRequestBody = new SwapRequest(quote, account.getPublicKey().toString()); + String jsonSwapRequestBody; + try { + jsonSwapRequestBody = objectMapper.writeValueAsString(swapRequestBody); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + + HttpPost swapRequest = new HttpPost("https://quote-api.jup.ag/v6/swap"); + swapRequest.setHeader("Content-Type", "application/json"); + swapRequest.setEntity(new StringEntity(jsonSwapRequestBody)); + + JsonNode swapRes; + try { + swapRes = httpClient.execute(swapRequest, handler); + } catch (IOException e) { + throw new RuntimeException(e); + } + + String swapTransaction = swapRes.get("swapTransaction").asText(); + + byte[] base64Decoded = Base64.getDecoder().decode(swapTransaction); + Transaction transaction = Transaction.deserialize(base64Decoded); + + try { + return rpcClient.getApi().sendTransaction(transaction, account); + } catch (RpcException e) { + throw new RuntimeException("Failed to send swap transaction", e); + } + } + + public static String swapToken(RpcClient rpcClient, Account account, String inputToken, String outputToken, String amount, String slippage) { + URI quoteUri = createQuoteUri(inputToken, outputToken, amount, slippage); + return swapToken(rpcClient, account, quoteUri); + } + + public static String swapToken(RpcClient rpcClient, Account account, URI quoteUri) { + JsonNode quote = getJupiterQuote(quoteUri); + return swapToken(rpcClient, account, quote); + } + + public record SwapRequest(Object quoteResponse, String userPublicKey) {} +} diff --git a/src/main/java/org/p2p/solanaj/programs/SystemProgram.java b/src/main/java/org/p2p/solanaj/programs/SystemProgram.java index 29bf8846..a128c606 100644 --- a/src/main/java/org/p2p/solanaj/programs/SystemProgram.java +++ b/src/main/java/org/p2p/solanaj/programs/SystemProgram.java @@ -15,7 +15,7 @@ public class SystemProgram extends Program { public static final int PROGRAM_INDEX_TRANSFER = 2; public static TransactionInstruction transfer(PublicKey fromPublicKey, PublicKey toPublickKey, long lamports) { - ArrayList keys = new ArrayList(); + ArrayList keys = new ArrayList<>(); keys.add(new AccountMeta(fromPublicKey, true, true)); keys.add(new AccountMeta(toPublickKey, false, true)); @@ -29,7 +29,7 @@ public static TransactionInstruction transfer(PublicKey fromPublicKey, PublicKey public static TransactionInstruction createAccount(PublicKey fromPublicKey, PublicKey newAccountPublikkey, long lamports, long space, PublicKey programId) { - ArrayList keys = new ArrayList(); + ArrayList keys = new ArrayList<>(); keys.add(new AccountMeta(fromPublicKey, true, true)); keys.add(new AccountMeta(newAccountPublikkey, true, true)); @@ -43,7 +43,7 @@ public static TransactionInstruction createAccount(PublicKey fromPublicKey, Publ } public static TransactionInstruction assign(PublicKey owner, PublicKey newOwner) { - ArrayList keys = new ArrayList(); + ArrayList keys = new ArrayList<>(); keys.add(new AccountMeta(owner, true, true)); byte[] data = new byte[4 + 32]; diff --git a/src/main/java/org/p2p/solanaj/programs/anchor/AnchorBasicTutorialProgram.java b/src/main/java/org/p2p/solanaj/programs/anchor/AnchorBasicTutorialProgram.java index d24cd4fd..2620929c 100644 --- a/src/main/java/org/p2p/solanaj/programs/anchor/AnchorBasicTutorialProgram.java +++ b/src/main/java/org/p2p/solanaj/programs/anchor/AnchorBasicTutorialProgram.java @@ -46,7 +46,7 @@ public static TransactionInstruction initialize(Account caller) { * @return byte array containing sighash for "global::initialize" */ private static byte[] encodeInitializeData() { - MessageDigest digest = null; + MessageDigest digest; byte[] encodedHash = null; int sigHashStart = 0; int sigHashEnd = 8; diff --git a/src/main/java/org/p2p/solanaj/rpc/Cluster.java b/src/main/java/org/p2p/solanaj/rpc/Cluster.java index 41452359..0657f194 100644 --- a/src/main/java/org/p2p/solanaj/rpc/Cluster.java +++ b/src/main/java/org/p2p/solanaj/rpc/Cluster.java @@ -1,18 +1,18 @@ package org.p2p.solanaj.rpc; +import lombok.Getter; + +@Getter public enum Cluster { DEVNET("https://api.devnet.solana.com"), TESTNET("https://api.testnet.solana.com"), MAINNET("https://api.mainnet-beta.solana.com"), ANKR("https://rpc.ankr.com/solana"); - private String endpoint; + private final String endpoint; Cluster(String endpoint) { this.endpoint = endpoint; } - public String getEndpoint() { - return endpoint; - } } diff --git a/src/main/java/org/p2p/solanaj/rpc/LoggingInterceptor.java b/src/main/java/org/p2p/solanaj/rpc/LoggingInterceptor.java index 0167f129..8960c9d6 100644 --- a/src/main/java/org/p2p/solanaj/rpc/LoggingInterceptor.java +++ b/src/main/java/org/p2p/solanaj/rpc/LoggingInterceptor.java @@ -4,6 +4,7 @@ import okhttp3.Request; import okhttp3.Response; import okio.Buffer; +import org.jetbrains.annotations.NotNull; import java.io.IOException; import java.util.logging.Logger; @@ -12,6 +13,7 @@ public class LoggingInterceptor implements Interceptor { private static final Logger LOGGER = Logger.getLogger(LoggingInterceptor.class.getName()); + @NotNull @Override public Response intercept(Chain chain) throws IOException { Request request = chain.request(); diff --git a/src/main/java/org/p2p/solanaj/rpc/RpcApi.java b/src/main/java/org/p2p/solanaj/rpc/RpcApi.java index 37f0b3d2..0a5006ae 100644 --- a/src/main/java/org/p2p/solanaj/rpc/RpcApi.java +++ b/src/main/java/org/p2p/solanaj/rpc/RpcApi.java @@ -3,6 +3,7 @@ import java.util.*; import java.util.stream.Collectors; import org.p2p.solanaj.core.Account; +import org.p2p.solanaj.core.LegacyTransaction; import org.p2p.solanaj.core.PublicKey; import org.p2p.solanaj.core.Transaction; import org.p2p.solanaj.rpc.types.*; @@ -27,7 +28,7 @@ import org.p2p.solanaj.ws.listeners.NotificationEventListener; public class RpcApi { - private RpcClient client; + private final RpcClient client; public RpcApi(RpcClient client) { this.client = client; @@ -61,6 +62,69 @@ public String getRecentBlockhash(Commitment commitment) throws RpcException { return client.call("getRecentBlockhash", params, RecentBlockhash.class).getValue().getBlockhash(); } + + public String sendLegacyTransaction(LegacyTransaction legacyTransaction, Account signer, String recentBlockHash) throws + RpcException { + return sendLegacyTransaction(legacyTransaction, Collections.singletonList(signer), recentBlockHash); + } + + public String sendLegacyTransaction(LegacyTransaction legacyTransaction, Account signer) throws RpcException { + return sendLegacyTransaction(legacyTransaction, Collections.singletonList(signer), null); + } + + /** + * Sends a transaction to the RPC server. + * + * @param legacyTransaction The transaction to send. + * @param signers The list of accounts signing the transaction. + * @param recentBlockHash The recent block hash. If null, it will be obtained from the RPC server. + * @param rpcSendTransactionConfig The configuration object for sending transactions via RPC. + * @return The transaction ID as a string. + * @throws RpcException If an error occurs during the RPC call. + */ + public String sendLegacyTransaction(LegacyTransaction legacyTransaction, List signers, String recentBlockHash, + RpcSendTransactionConfig rpcSendTransactionConfig) + throws RpcException { + if (recentBlockHash == null) { + recentBlockHash = getRecentBlockhash(); + } + legacyTransaction.setRecentBlockHash(recentBlockHash); + legacyTransaction.sign(signers); + byte[] serializedTransaction = legacyTransaction.serialize(); + + String base64Trx = Base64.getEncoder().encodeToString(serializedTransaction); + + List params = new ArrayList<>(); + + params.add(base64Trx); + params.add(rpcSendTransactionConfig); + + return client.call("sendLegacyTransaction", params, String.class); + } + + /** + * Sends a transaction to the network for processing. + * A default RpcSendTransactionConfig is used. + * + * @param legacyTransaction the transaction to send + * @param signers the list of accounts that will sign the transaction + * @param recentBlockHash the recent block hash to include in the transaction + * @return the result of the transaction + * @throws RpcException if an error occurs during the RPC call + */ + public String sendLegacyTransaction(LegacyTransaction legacyTransaction, List signers, String recentBlockHash) + throws RpcException { + return sendLegacyTransaction(legacyTransaction, signers, recentBlockHash, new RpcSendTransactionConfig()); + } + + public void sendAndConfirmLegacyTransaction(LegacyTransaction legacyTransaction, List signers, + NotificationEventListener listener) throws RpcException { + String signature = sendLegacyTransaction(legacyTransaction, signers, null); + + SubscriptionWebSocketClient subClient = SubscriptionWebSocketClient.getInstance(client.getEndpoint()); + subClient.signatureSubscribe(signature, listener); + } + public String sendTransaction(Transaction transaction, Account signer, String recentBlockHash) throws RpcException { return sendTransaction(transaction, Collections.singletonList(signer), recentBlockHash); @@ -81,7 +145,7 @@ public String sendTransaction(Transaction transaction, Account signer) throws Rp * @throws RpcException If an error occurs during the RPC call. */ public String sendTransaction(Transaction transaction, List signers, String recentBlockHash, - RpcSendTransactionConfig rpcSendTransactionConfig) + RpcSendTransactionConfig rpcSendTransactionConfig) throws RpcException { if (recentBlockHash == null) { recentBlockHash = getRecentBlockhash(); @@ -92,12 +156,12 @@ public String sendTransaction(Transaction transaction, List signers, St String base64Trx = Base64.getEncoder().encodeToString(serializedTransaction); - List params = new ArrayList(); + List params = new ArrayList<>(); params.add(base64Trx); params.add(rpcSendTransactionConfig); - return client.call("sendTransaction", params, String.class); + return client.call("sendLegacyTransaction", params, String.class); } /** @@ -116,7 +180,7 @@ public String sendTransaction(Transaction transaction, List signers, St } public void sendAndConfirmTransaction(Transaction transaction, List signers, - NotificationEventListener listener) throws RpcException { + NotificationEventListener listener) throws RpcException { String signature = sendTransaction(transaction, signers, null); SubscriptionWebSocketClient subClient = SubscriptionWebSocketClient.getInstance(client.getEndpoint()); @@ -159,14 +223,14 @@ public ConfirmedTransaction getTransaction(String signature, Commitment commitme @SuppressWarnings({ "unchecked", "rawtypes" }) public List getConfirmedSignaturesForAddress2(PublicKey account, int limit) throws RpcException { - List params = new ArrayList(); + List params = new ArrayList<>(); params.add(account.toString()); params.add(new ConfirmedSignFAddr2(limit, Commitment.CONFIRMED)); List rawResult = client.call("getConfirmedSignaturesForAddress2", params, List.class); - List result = new ArrayList(); + List result = new ArrayList<>(); for (AbstractMap item : rawResult) { result.add(new SignatureInformation(item)); } @@ -176,14 +240,14 @@ public List getConfirmedSignaturesForAddress2(PublicKey ac public List getSignaturesForAddress(PublicKey account, int limit, Commitment commitment) throws RpcException { - List params = new ArrayList(); + List params = new ArrayList<>(); params.add(account.toString()); params.add(new ConfirmedSignFAddr2(limit, commitment)); List rawResult = client.call("getSignaturesForAddress", params, List.class); - List result = new ArrayList(); + List result = new ArrayList<>(); for (AbstractMap item : rawResult) { result.add(new SignatureInformation(item)); } @@ -192,7 +256,7 @@ public List getSignaturesForAddress(PublicKey account, int } public List getProgramAccounts(PublicKey account, long offset, String bytes) throws RpcException { - List filters = new ArrayList(); + List filters = new ArrayList<>(); filters.add(new Filter(new Memcmp(offset, bytes))); ProgramAccountConfig programAccountConfig = new ProgramAccountConfig(filters); @@ -200,7 +264,7 @@ public List getProgramAccounts(PublicKey account, long offset, S } public List getProgramAccountsBase64(PublicKey account, long offset, String bytes) throws RpcException { - List filters = new ArrayList(); + List filters = new ArrayList<>(); Memcmp memcmp = new Memcmp(offset, bytes); filters.add(new Filter(memcmp)); @@ -217,7 +281,7 @@ public List getProgramAccounts(PublicKey account) throws RpcExce @SuppressWarnings({ "unchecked", "rawtypes" }) public List getProgramAccounts(PublicKey account, ProgramAccountConfig programAccountConfig) throws RpcException { - List params = new ArrayList(); + List params = new ArrayList<>(); params.add(account.toString()); @@ -227,7 +291,7 @@ public List getProgramAccounts(PublicKey account, ProgramAccount List rawResult = client.call("getProgramAccounts", params, List.class); - List result = new ArrayList(); + List result = new ArrayList<>(); for (AbstractMap item : rawResult) { result.add(new ProgramAccount(item)); } @@ -243,9 +307,7 @@ public List getProgramAccounts(PublicKey account, List m params.add(account.toString()); List filters = new ArrayList<>(); - memcmpList.forEach(memcmp -> { - filters.add(new Filter(memcmp)); - }); + memcmpList.forEach(memcmp -> filters.add(new Filter(memcmp))); filters.add(new DataSize(dataSize)); @@ -270,9 +332,7 @@ public List getProgramAccounts(PublicKey account, List m params.add(account.toString()); List filters = new ArrayList<>(); - memcmpList.forEach(memcmp -> { - filters.add(new Filter(memcmp)); - }); + memcmpList.forEach(memcmp -> filters.add(new Filter(memcmp))); ProgramAccountConfig programAccountConfig = new ProgramAccountConfig(filters); params.add(programAccountConfig); @@ -400,7 +460,7 @@ public String requestAirdrop(PublicKey address, long lamports) throws RpcExcepti } public String requestAirdrop(PublicKey address, long lamports, Commitment commitment) throws RpcException { - List params = new ArrayList(); + List params = new ArrayList<>(); params.add(address.toString()); params.add(lamports); @@ -412,7 +472,7 @@ public String requestAirdrop(PublicKey address, long lamports, Commitment commit } public BlockCommitment getBlockCommitment(long block) throws RpcException { - List params = new ArrayList(); + List params = new ArrayList<>(); params.add(block); @@ -511,7 +571,7 @@ public SimulatedTransaction simulateTransaction(String transaction, List getClusterNodes() throws RpcException { - List params = new ArrayList(); + List params = new ArrayList<>(); // TODO - fix uncasted type stuff List rawResult = client.call("getClusterNodes", params, List.class); @@ -573,7 +633,7 @@ public Block getBlock(int slot, Map optionalParams) throws RpcEx * @throws RpcException */ public SnapshotSlot getHighestSnapshotSlot() throws RpcException { - List params = new ArrayList(); + List params = new ArrayList<>(); return client.call("getHighestSnapshotSlot", params, SnapshotSlot.class); } @@ -597,7 +657,7 @@ public EpochInfo getEpochInfo(Commitment commitment) throws RpcException { } public EpochSchedule getEpochSchedule() throws RpcException { - List params = new ArrayList(); + List params = new ArrayList<>(); return client.call("getEpochSchedule", params, EpochSchedule.class); } diff --git a/src/main/java/org/p2p/solanaj/rpc/RpcClient.java b/src/main/java/org/p2p/solanaj/rpc/RpcClient.java index 83445da1..58ca62b0 100644 --- a/src/main/java/org/p2p/solanaj/rpc/RpcClient.java +++ b/src/main/java/org/p2p/solanaj/rpc/RpcClient.java @@ -91,7 +91,6 @@ public T call(String method, List params, Class clazz) throws Rpc try { Response response = httpClient.newCall(request).execute(); final String result = response.body().string(); - // System.out.println("Response = " + result); RpcResponse rpcResult = resultAdapter.fromJson(result); if (rpcResult.getError() != null) { diff --git a/src/main/java/org/p2p/solanaj/rpc/RpcException.java b/src/main/java/org/p2p/solanaj/rpc/RpcException.java index ae5ce2b4..b3b602e5 100644 --- a/src/main/java/org/p2p/solanaj/rpc/RpcException.java +++ b/src/main/java/org/p2p/solanaj/rpc/RpcException.java @@ -1,6 +1,9 @@ package org.p2p.solanaj.rpc; +import java.io.Serial; + public class RpcException extends Exception { + @Serial private final static long serialVersionUID = 8315999767009642193L; public RpcException(String message) { diff --git a/src/main/java/org/p2p/solanaj/rpc/types/BlockProduction.java b/src/main/java/org/p2p/solanaj/rpc/types/BlockProduction.java index 063651c5..dea23389 100644 --- a/src/main/java/org/p2p/solanaj/rpc/types/BlockProduction.java +++ b/src/main/java/org/p2p/solanaj/rpc/types/BlockProduction.java @@ -25,13 +25,10 @@ public static class BlockProductionRange { @Getter @ToString public static class BlockProductionValue { + @Getter @Json(name = "byIdentity") private Map> byIdentity; - public Map> getByIdentity() { - return byIdentity; - } - @Json(name = "range") private BlockProductionRange blockProductionRange; diff --git a/src/main/java/org/p2p/solanaj/rpc/types/ConfirmedBlock.java b/src/main/java/org/p2p/solanaj/rpc/types/ConfirmedBlock.java index f578a91a..36215430 100644 --- a/src/main/java/org/p2p/solanaj/rpc/types/ConfirmedBlock.java +++ b/src/main/java/org/p2p/solanaj/rpc/types/ConfirmedBlock.java @@ -1,7 +1,6 @@ package org.p2p.solanaj.rpc.types; import com.squareup.moshi.Json; -import lombok.Data; import lombok.Getter; import lombok.ToString; diff --git a/src/main/java/org/p2p/solanaj/rpc/types/ConfirmedSignFAddr2.java b/src/main/java/org/p2p/solanaj/rpc/types/ConfirmedSignFAddr2.java index 7d191986..c823abbe 100644 --- a/src/main/java/org/p2p/solanaj/rpc/types/ConfirmedSignFAddr2.java +++ b/src/main/java/org/p2p/solanaj/rpc/types/ConfirmedSignFAddr2.java @@ -6,7 +6,7 @@ public class ConfirmedSignFAddr2 { @Json(name = "limit") - private long limit; + private final long limit; @Json(name = "before") private String before; @@ -15,7 +15,7 @@ public class ConfirmedSignFAddr2 { private String until; @Json(name = "commitment") - private String commitment; + private final String commitment; public ConfirmedSignFAddr2(int limit, Commitment commitment) { this.limit = limit; diff --git a/src/main/java/org/p2p/solanaj/rpc/types/ProgramAccount.java b/src/main/java/org/p2p/solanaj/rpc/types/ProgramAccount.java index 1e8a33c3..447aaea8 100644 --- a/src/main/java/org/p2p/solanaj/rpc/types/ProgramAccount.java +++ b/src/main/java/org/p2p/solanaj/rpc/types/ProgramAccount.java @@ -24,16 +24,16 @@ public static class Account { private String data; @Json(name = "executable") - private boolean executable; + private final boolean executable; @Json(name = "lamports") - private double lamports; + private final double lamports; @Json(name = "owner") - private String owner; + private final String owner; @Json(name = "rentEpoch") - private double rentEpoch; + private final double rentEpoch; private String encoding; @@ -46,7 +46,7 @@ public Account(Object acc) { List dataList = ((List) rawData); this.data = dataList.get(0); - this.encoding = (String) dataList.get(1); + this.encoding = dataList.get(1); } else if (rawData instanceof String) { this.data = (String) rawData; } @@ -67,10 +67,10 @@ public byte[] getDecodedData() { } @Json(name = "account") - private Account account; + private final Account account; @Json(name = "pubkey") - private String pubkey; + private final String pubkey; public PublicKey getPublicKey() { return new PublicKey(pubkey); diff --git a/src/main/java/org/p2p/solanaj/rpc/types/RpcRequest.java b/src/main/java/org/p2p/solanaj/rpc/types/RpcRequest.java index 708fc096..7199bfb2 100644 --- a/src/main/java/org/p2p/solanaj/rpc/types/RpcRequest.java +++ b/src/main/java/org/p2p/solanaj/rpc/types/RpcRequest.java @@ -12,16 +12,16 @@ public class RpcRequest { @Json(name = "jsonrpc") - private String jsonrpc = "2.0"; + private final String jsonrpc = "2.0"; @Json(name = "method") - private String method; + private final String method; @Json(name = "params") - private List params; + private final List params; @Json(name = "id") - private String id = UUID.randomUUID().toString(); + private final String id = UUID.randomUUID().toString(); public RpcRequest(String method) { this(method, null); diff --git a/src/main/java/org/p2p/solanaj/rpc/types/Supply.java b/src/main/java/org/p2p/solanaj/rpc/types/Supply.java index 79ae2c4b..239d9a51 100644 --- a/src/main/java/org/p2p/solanaj/rpc/types/Supply.java +++ b/src/main/java/org/p2p/solanaj/rpc/types/Supply.java @@ -3,7 +3,6 @@ import com.squareup.moshi.Json; import lombok.Getter; import lombok.ToString; -import org.p2p.solanaj.core.PublicKey; import java.util.List; diff --git a/src/main/java/org/p2p/solanaj/rpc/types/config/Commitment.java b/src/main/java/org/p2p/solanaj/rpc/types/config/Commitment.java index cc6199f0..39ad04ef 100644 --- a/src/main/java/org/p2p/solanaj/rpc/types/config/Commitment.java +++ b/src/main/java/org/p2p/solanaj/rpc/types/config/Commitment.java @@ -1,5 +1,8 @@ package org.p2p.solanaj.rpc.types.config; +import lombok.Getter; + +@Getter public enum Commitment { FINALIZED("finalized"), @@ -17,7 +20,4 @@ public enum Commitment { this.value = value; } - public String getValue() { - return value; - } } diff --git a/src/main/java/org/p2p/solanaj/rpc/types/config/ProgramAccountConfig.java b/src/main/java/org/p2p/solanaj/rpc/types/config/ProgramAccountConfig.java index 5c7ffa6e..4a40e095 100644 --- a/src/main/java/org/p2p/solanaj/rpc/types/config/ProgramAccountConfig.java +++ b/src/main/java/org/p2p/solanaj/rpc/types/config/ProgramAccountConfig.java @@ -2,15 +2,18 @@ import java.util.List; +import lombok.Setter; import org.p2p.solanaj.rpc.types.config.RpcSendTransactionConfig.Encoding; public class ProgramAccountConfig { + @Setter private Encoding encoding = null; + @Setter private List filters = null; - private String commitment = "processed"; + private final String commitment = "processed"; public ProgramAccountConfig(List filters) { this.filters = filters; @@ -20,11 +23,4 @@ public ProgramAccountConfig(Encoding encoding) { this.encoding = encoding; } - public void setEncoding(Encoding encoding) { - this.encoding = encoding; - } - - public void setFilters(List filters) { - this.filters = filters; - } } \ No newline at end of file diff --git a/src/main/java/org/p2p/solanaj/rpc/types/config/RpcSendTransactionConfig.java b/src/main/java/org/p2p/solanaj/rpc/types/config/RpcSendTransactionConfig.java index 41960f7a..67446f7e 100644 --- a/src/main/java/org/p2p/solanaj/rpc/types/config/RpcSendTransactionConfig.java +++ b/src/main/java/org/p2p/solanaj/rpc/types/config/RpcSendTransactionConfig.java @@ -10,11 +10,11 @@ @Builder public class RpcSendTransactionConfig { - public static enum Encoding { + public enum Encoding { base64("base64"), base58("base58"); - private String enc; + private final String enc; Encoding(String enc) { this.enc = enc; diff --git a/src/main/java/org/p2p/solanaj/token/TokenManager.java b/src/main/java/org/p2p/solanaj/token/TokenManager.java index 9bf7757c..eff2ad90 100644 --- a/src/main/java/org/p2p/solanaj/token/TokenManager.java +++ b/src/main/java/org/p2p/solanaj/token/TokenManager.java @@ -1,8 +1,8 @@ package org.p2p.solanaj.token; import org.p2p.solanaj.core.Account; +import org.p2p.solanaj.core.LegacyTransaction; import org.p2p.solanaj.core.PublicKey; -import org.p2p.solanaj.core.Transaction; import org.p2p.solanaj.programs.MemoProgram; import org.p2p.solanaj.programs.TokenProgram; import org.p2p.solanaj.rpc.RpcClient; @@ -20,10 +20,10 @@ public TokenManager(final RpcClient client) { } public String transfer(final Account owner, final PublicKey source, final PublicKey destination, final PublicKey tokenMint, long amount) { - final Transaction transaction = new Transaction(); + final LegacyTransaction legacyTransaction = new LegacyTransaction(); // SPL token instruction - transaction.addInstruction( + legacyTransaction.addInstruction( TokenProgram.transfer( source, destination, @@ -33,17 +33,17 @@ public String transfer(final Account owner, final PublicKey source, final Public ); // Memo - transaction.addInstruction( + legacyTransaction.addInstruction( MemoProgram.writeUtf8( owner.getPublicKey(), "Hello from SolanaJ" ) ); - // Call sendTransaction + // Call sendLegacyTransaction String result = null; try { - result = client.getApi().sendTransaction(transaction, owner); + result = client.getApi().sendLegacyTransaction(legacyTransaction, owner); } catch (RpcException e) { e.printStackTrace(); } @@ -61,9 +61,9 @@ public String transferCheckedToSolAddress(final Account owner, final PublicKey s e.printStackTrace(); } - final Transaction transaction = new Transaction(); + final LegacyTransaction legacyTransaction = new LegacyTransaction(); // SPL token instruction - transaction.addInstruction( + legacyTransaction.addInstruction( TokenProgram.transferChecked( source, tokenAccount, @@ -75,17 +75,17 @@ public String transferCheckedToSolAddress(final Account owner, final PublicKey s ); // Memo - transaction.addInstruction( + legacyTransaction.addInstruction( MemoProgram.writeUtf8( owner.getPublicKey(), "Hello from SolanaJ" ) ); - // Call sendTransaction + // Call sendLegacyTransaction String result = null; try { - result = client.getApi().sendTransaction(transaction, owner); + result = client.getApi().sendLegacyTransaction(legacyTransaction, owner); } catch (RpcException e) { e.printStackTrace(); } @@ -94,10 +94,10 @@ public String transferCheckedToSolAddress(final Account owner, final PublicKey s } public String initializeAccount(Account newAccount, PublicKey usdcTokenMint, Account owner) { - final Transaction transaction = new Transaction(); + final LegacyTransaction legacyTransaction = new LegacyTransaction(); // SPL token instruction - transaction.addInstruction( + legacyTransaction.addInstruction( TokenProgram.initializeAccount( newAccount.getPublicKey(), usdcTokenMint, @@ -105,10 +105,10 @@ public String initializeAccount(Account newAccount, PublicKey usdcTokenMint, Acc ) ); - // Call sendTransaction + // Call sendLegacyTransaction String result = null; try { - result = client.getApi().sendTransaction(transaction, owner); + result = client.getApi().sendLegacyTransaction(legacyTransaction, owner); } catch (RpcException e) { e.printStackTrace(); } diff --git a/src/main/java/org/p2p/solanaj/utils/ByteUtils.java b/src/main/java/org/p2p/solanaj/utils/ByteUtils.java index 60ff25d6..b89204fe 100644 --- a/src/main/java/org/p2p/solanaj/utils/ByteUtils.java +++ b/src/main/java/org/p2p/solanaj/utils/ByteUtils.java @@ -1,11 +1,16 @@ package org.p2p.solanaj.utils; +import com.google.common.primitives.Bytes; + import static org.bitcoinj.core.Utils.*; import java.io.IOException; import java.io.OutputStream; import java.math.BigInteger; import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; public class ByteUtils { public static final int UINT_32_LENGTH = 4; @@ -72,4 +77,17 @@ public static int getBit(byte[] data, int pos) { return valInt; } + public static byte[] toByteArray(List byteList) { + return Bytes.toArray(byteList); + } + + public static List toByteList(byte[] bytes) { + return IntStream.range(0, bytes.length) + .mapToObj(i -> bytes[i]) + .collect(Collectors.toList()); + } + + public static byte[] emptyByteArray() { + return new byte[]{}; + } } diff --git a/src/main/java/org/p2p/solanaj/utils/GuardedArrayUtils.java b/src/main/java/org/p2p/solanaj/utils/GuardedArrayUtils.java new file mode 100644 index 00000000..c2fe99b7 --- /dev/null +++ b/src/main/java/org/p2p/solanaj/utils/GuardedArrayUtils.java @@ -0,0 +1,42 @@ +package org.p2p.solanaj.utils; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class GuardedArrayUtils { + + public static Byte guardedShift(List byteArray) { + if (byteArray.isEmpty()) { + throw new IllegalArgumentException("Byte array length is 0"); + } + return byteArray.remove(0); + } + + public static byte[] guardedSplice( + List byteList, + Integer start, + Integer deleteCount, + byte... items) { + List removedItems; + if (deleteCount != null) { + if (start + deleteCount > byteList.size()) { + throw new Error("Reached end of bytes"); + } + removedItems = new ArrayList<>(byteList.subList(start, start + deleteCount)); + byteList.subList(start, start + deleteCount).clear(); + } else { + if (start > byteList.size()) { + throw new Error("Reached end of bytes"); + } + removedItems = Collections.emptyList(); + } + List itemsToAdd = ByteUtils.toByteList(items); + byteList.addAll(start, itemsToAdd); + + if (!removedItems.isEmpty()) { + return ByteUtils.toByteArray(removedItems); + } + return ByteUtils.emptyByteArray(); + } +} diff --git a/src/main/java/org/p2p/solanaj/utils/ShortvecEncoding.java b/src/main/java/org/p2p/solanaj/utils/Shortvec.java similarity index 62% rename from src/main/java/org/p2p/solanaj/utils/ShortvecEncoding.java rename to src/main/java/org/p2p/solanaj/utils/Shortvec.java index 4a1d9080..478daf7c 100644 --- a/src/main/java/org/p2p/solanaj/utils/ShortvecEncoding.java +++ b/src/main/java/org/p2p/solanaj/utils/Shortvec.java @@ -1,8 +1,10 @@ package org.p2p.solanaj.utils; +import java.util.List; + import static org.bitcoinj.core.Utils.*; -public class ShortvecEncoding { +public class Shortvec { public static byte[] encodeLength(int len) { byte[] out = new byte[10]; @@ -27,4 +29,18 @@ public static byte[] encodeLength(int len) { return bytes; } + + public static int decodeLength(List dataBytesList) { + int len = 0; + int size = 0; + for (;;) { + int elem = (int) dataBytesList.remove(0); + len |= (elem & 0x7f) << (size * 7); + size += 1; + if ((elem & 0x80) == 0) { + break; + } + } + return len; + } } diff --git a/src/main/java/org/p2p/solanaj/utils/TweetNaclFast.java b/src/main/java/org/p2p/solanaj/utils/TweetNaclFast.java index 8e6b22a0..87bd6cea 100644 --- a/src/main/java/org/p2p/solanaj/utils/TweetNaclFast.java +++ b/src/main/java/org/p2p/solanaj/utils/TweetNaclFast.java @@ -4,6 +4,8 @@ // Copyright (c) 2014 Tom Zhou +import lombok.Getter; + import java.io.UnsupportedEncodingException; import java.security.SecureRandom; import java.lang.System; @@ -25,10 +27,10 @@ public static final class Box { private final static String TAG = "Box"; - private AtomicLong nonce; + private final AtomicLong nonce; - private byte [] theirPublicKey; - private byte [] mySecretKey; + private final byte [] theirPublicKey; + private final byte [] mySecretKey; private byte [] sharedKey; public Box(byte [] theirPublicKey, byte [] mySecretKey) { @@ -337,23 +339,17 @@ private byte[] generateNonce() { * */ public static final int overheadLength = 16; - public static class KeyPair { - private byte [] publicKey; - private byte [] secretKey; + @Getter + public static class KeyPair { + private final byte [] publicKey; + private final byte [] secretKey; public KeyPair() { publicKey = new byte[publicKeyLength]; secretKey = new byte[secretKeyLength]; } - public byte [] getPublicKey() { - return publicKey; - } - - public byte [] getSecretKey() { - return secretKey; - } - } + } /* * @description @@ -390,9 +386,9 @@ public static final class SecretBox { private final static String TAG = "SecretBox"; - private AtomicLong nonce; + private final AtomicLong nonce; - private byte [] key; + private final byte [] key; public SecretBox(byte [] key) { this(key, 68); @@ -687,8 +683,8 @@ public static final class Signature { private final static String TAG = "Signature"; - private byte [] theirPublicKey; - private byte [] mySecretKey; + private final byte [] theirPublicKey; + private final byte [] mySecretKey; public Signature(byte [] theirPublicKey, byte [] mySecretKey) { this.theirPublicKey = theirPublicKey; @@ -792,23 +788,17 @@ public boolean detached_verify(byte [] message, byte [] signature) { * Generates new random key pair for signing and * returns it as an object with publicKey and secretKey members * */ - public static class KeyPair { - private byte [] publicKey; - private byte [] secretKey; + @Getter + public static class KeyPair { + private final byte [] publicKey; + private final byte [] secretKey; public KeyPair() { publicKey = new byte[publicKeyLength]; secretKey = new byte[secretKeyLength]; } - public byte [] getPublicKey() { - return publicKey; - } - - public byte [] getSecretKey() { - return secretKey; - } - } + } /* * @description @@ -1451,10 +1441,10 @@ public static int crypto_stream_xor(byte [] c,int cpos, byte [] m,int mpos, long */ public static final class poly1305 { - private byte[] buffer; - private int[] r; - private int[] h; - private int[] pad; + private final byte[] buffer; + private final int[] r; + private final int[] h; + private final int[] pad; private int leftover; private int fin; diff --git a/src/main/java/org/p2p/solanaj/utils/bip32/wallet/HdAddress.java b/src/main/java/org/p2p/solanaj/utils/bip32/wallet/HdAddress.java index 782d1939..6dcc0e44 100644 --- a/src/main/java/org/p2p/solanaj/utils/bip32/wallet/HdAddress.java +++ b/src/main/java/org/p2p/solanaj/utils/bip32/wallet/HdAddress.java @@ -1,6 +1,7 @@ package org.p2p.solanaj.utils.bip32.wallet; +import lombok.Getter; import org.p2p.solanaj.utils.bip32.wallet.key.HdPrivateKey; import org.p2p.solanaj.utils.bip32.wallet.key.HdPublicKey; @@ -9,9 +10,12 @@ */ public class HdAddress { + @Getter private final HdPrivateKey privateKey; + @Getter private final HdPublicKey publicKey; private final SolanaCoin solanaCoin; + @Getter private final String path; public HdAddress(HdPrivateKey privateKey, HdPublicKey publicKey, SolanaCoin solanaCoin, String path) { @@ -21,19 +25,8 @@ public HdAddress(HdPrivateKey privateKey, HdPublicKey publicKey, SolanaCoin sola this.path = path; } - public HdPrivateKey getPrivateKey() { - return privateKey; - } - - public HdPublicKey getPublicKey() { - return publicKey; - } - public SolanaCoin getCoinType() { return solanaCoin; } - public String getPath() { - return path; - } } diff --git a/src/main/java/org/p2p/solanaj/utils/bip32/wallet/SolanaBip44.java b/src/main/java/org/p2p/solanaj/utils/bip32/wallet/SolanaBip44.java index 50b9fd9c..f0c1fa3b 100644 --- a/src/main/java/org/p2p/solanaj/utils/bip32/wallet/SolanaBip44.java +++ b/src/main/java/org/p2p/solanaj/utils/bip32/wallet/SolanaBip44.java @@ -29,14 +29,11 @@ public SolanaBip44(){ * @return PrivateKey */ public byte[] getPrivateKeyFromSeed(byte[] seed, DerivableType derivableType) { - switch (derivableType){ - case BIP44: - return getPrivateKeyFromBip44Seed(seed); - case BIP44CHANGE: - return getPrivateKeyFromBip44SeedWithChange(seed); - default: - throw new RuntimeException("DerivableType not supported"); - } + return switch (derivableType) { + case BIP44 -> getPrivateKeyFromBip44Seed(seed); + case BIP44CHANGE -> getPrivateKeyFromBip44SeedWithChange(seed); + default -> throw new RuntimeException("DerivableType not supported"); + }; } private byte[] getPrivateKeyFromBip44SeedWithChange(byte[] seed) { diff --git a/src/main/java/org/p2p/solanaj/utils/bip32/wallet/SolanaCoin.java b/src/main/java/org/p2p/solanaj/utils/bip32/wallet/SolanaCoin.java index 997f985f..99c1b321 100644 --- a/src/main/java/org/p2p/solanaj/utils/bip32/wallet/SolanaCoin.java +++ b/src/main/java/org/p2p/solanaj/utils/bip32/wallet/SolanaCoin.java @@ -1,30 +1,34 @@ package org.p2p.solanaj.utils.bip32.wallet; +import lombok.Getter; import org.p2p.solanaj.utils.bip32.wallet.key.SolanaCurve; public class SolanaCoin { - private final SolanaCurve curve = new SolanaCurve(); - private final long coinType = 501; - private final long purpose = 44; - private final boolean alwaysHardened = true; - /** - * Get the curve + * -- GETTER -- + * Get the curve * * @return curve */ - public SolanaCurve getCurve() { - return curve; - } - + @Getter + private final SolanaCurve curve = new SolanaCurve(); /** - * get the coin type + * -- GETTER -- + * get the coin type * * @return coin type */ - public long getCoinType() { - return coinType; - } + @Getter + private final long coinType = 501; + /** + * -- GETTER -- + * get the coin purpose + * + * @return purpose + */ + @Getter + private final long purpose = 44; + private final boolean alwaysHardened = true; /** * get whether the addresses must always be hardened @@ -35,12 +39,4 @@ public boolean getAlwaysHardened() { return alwaysHardened; } - /** - * get the coin purpose - * - * @return purpose - */ - public long getPurpose() { - return purpose; - } } diff --git a/src/main/java/org/p2p/solanaj/utils/bip32/wallet/key/HdKey.java b/src/main/java/org/p2p/solanaj/utils/bip32/wallet/key/HdKey.java index 2d23d340..9cbd5c39 100644 --- a/src/main/java/org/p2p/solanaj/utils/bip32/wallet/key/HdKey.java +++ b/src/main/java/org/p2p/solanaj/utils/bip32/wallet/key/HdKey.java @@ -1,5 +1,7 @@ package org.p2p.solanaj.utils.bip32.wallet.key; +import lombok.Getter; +import lombok.Setter; import org.p2p.solanaj.utils.bip32.crypto.Hash; import java.io.ByteArrayOutputStream; @@ -11,12 +13,17 @@ *

* Will probably be migrated to builder pattern. */ +@Setter public class HdKey { + @Getter private byte[] version; + @Getter private int depth; private byte[] fingerprint; private byte[] childNumber; + @Getter private byte[] chainCode; + @Getter private byte[] keyData; HdKey(byte[] version, int depth, byte[] fingerprint, byte[] childNumber, byte[] chainCode, byte[] keyData) { @@ -31,34 +38,6 @@ public class HdKey { HdKey() { } - public void setVersion(byte[] version) { - this.version = version; - } - - public void setDepth(int depth) { - this.depth = depth; - } - - public void setFingerprint(byte[] fingerprint) { - this.fingerprint = fingerprint; - } - - public void setChildNumber(byte[] childNumber) { - this.childNumber = childNumber; - } - - public void setChainCode(byte[] chainCode) { - this.chainCode = chainCode; - } - - public void setKeyData(byte[] keyData) { - this.keyData = keyData; - } - - public byte[] getChainCode() { - return chainCode; - } - /** * Get the full chain key. This is not the public/private key for the address. * @return full HD Key @@ -83,15 +62,4 @@ public byte[] getKey() { return key.toByteArray(); } - public int getDepth() { - return depth; - } - - public byte[] getKeyData() { - return keyData; - } - - public byte[] getVersion() { - return version; - } } diff --git a/src/main/java/org/p2p/solanaj/utils/bip32/wallet/key/HdPrivateKey.java b/src/main/java/org/p2p/solanaj/utils/bip32/wallet/key/HdPrivateKey.java index 3f972c50..848df106 100644 --- a/src/main/java/org/p2p/solanaj/utils/bip32/wallet/key/HdPrivateKey.java +++ b/src/main/java/org/p2p/solanaj/utils/bip32/wallet/key/HdPrivateKey.java @@ -1,16 +1,14 @@ package org.p2p.solanaj.utils.bip32.wallet.key; +import lombok.Getter; +import lombok.Setter; + /** * Defines a key with a given private key */ +@Setter +@Getter public class HdPrivateKey extends HdKey { private byte[] privateKey; - public byte[] getPrivateKey() { - return privateKey; - } - - public void setPrivateKey(byte[] privateKey) { - this.privateKey = privateKey; - } } diff --git a/src/main/java/org/p2p/solanaj/utils/bip32/wallet/key/HdPublicKey.java b/src/main/java/org/p2p/solanaj/utils/bip32/wallet/key/HdPublicKey.java index a7305f5e..0dfadccb 100644 --- a/src/main/java/org/p2p/solanaj/utils/bip32/wallet/key/HdPublicKey.java +++ b/src/main/java/org/p2p/solanaj/utils/bip32/wallet/key/HdPublicKey.java @@ -1,16 +1,14 @@ package org.p2p.solanaj.utils.bip32.wallet.key; +import lombok.Getter; +import lombok.Setter; + /** * Defines a key with a given public key */ +@Setter +@Getter public class HdPublicKey extends HdKey { private byte[] publicKey; - public byte[] getPublicKey() { - return publicKey; - } - - public void setPublicKey(byte[] publicKey) { - this.publicKey = publicKey; - } } diff --git a/src/main/java/org/p2p/solanaj/utils/bip32/wallet/key/SolanaCurve.java b/src/main/java/org/p2p/solanaj/utils/bip32/wallet/key/SolanaCurve.java index 18f891b6..82e57fc0 100644 --- a/src/main/java/org/p2p/solanaj/utils/bip32/wallet/key/SolanaCurve.java +++ b/src/main/java/org/p2p/solanaj/utils/bip32/wallet/key/SolanaCurve.java @@ -1,11 +1,11 @@ package org.p2p.solanaj.utils.bip32.wallet.key; +import lombok.Getter; + +@Getter public class SolanaCurve { private static final String ed25519Curve = "ed25519 seed"; private final String seed = SolanaCurve.ed25519Curve; - public String getSeed() { - return seed; - } } diff --git a/src/main/java/org/p2p/solanaj/ws/SignatureNotification.java b/src/main/java/org/p2p/solanaj/ws/SignatureNotification.java index 59af8557..eecc6254 100644 --- a/src/main/java/org/p2p/solanaj/ws/SignatureNotification.java +++ b/src/main/java/org/p2p/solanaj/ws/SignatureNotification.java @@ -1,16 +1,15 @@ package org.p2p.solanaj.ws; +import lombok.Getter; + +@Getter public class SignatureNotification { - private Object error; + private final Object error; public SignatureNotification(Object error) { this.error = error; } - public Object getError() { - return error; - } - public boolean hasError() { return error != null; } diff --git a/src/main/java/org/p2p/solanaj/ws/SubscriptionWebSocketClient.java b/src/main/java/org/p2p/solanaj/ws/SubscriptionWebSocketClient.java index bb833a7f..89345e86 100644 --- a/src/main/java/org/p2p/solanaj/ws/SubscriptionWebSocketClient.java +++ b/src/main/java/org/p2p/solanaj/ws/SubscriptionWebSocketClient.java @@ -22,7 +22,7 @@ public class SubscriptionWebSocketClient extends WebSocketClient { - private class SubscriptionParams { + private static class SubscriptionParams { RpcRequest request; NotificationEventListener listener; @@ -32,9 +32,9 @@ private class SubscriptionParams { } } - private Map subscriptions = new ConcurrentHashMap<>(); - private Map subscriptionIds = new ConcurrentHashMap<>(); - private Map subscriptionListeners = new ConcurrentHashMap<>(); + private final Map subscriptions = new ConcurrentHashMap<>(); + private final Map subscriptionIds = new ConcurrentHashMap<>(); + private final Map subscriptionListeners = new ConcurrentHashMap<>(); private static final Logger LOGGER = Logger.getLogger(SubscriptionWebSocketClient.class.getName()); public static SubscriptionWebSocketClient getExactPathInstance(String endpoint) { @@ -103,7 +103,7 @@ public void accountSubscribe(String key, NotificationEventListener listener) { } public void signatureSubscribe(String signature, NotificationEventListener listener) { - List params = new ArrayList(); + List params = new ArrayList<>(); params.add(signature); RpcRequest rpcRequest = new RpcRequest("signatureSubscribe", params); @@ -115,7 +115,7 @@ public void signatureSubscribe(String signature, NotificationEventListener liste } public void logsSubscribe(String mention, NotificationEventListener listener) { - List params = new ArrayList(); + List params = new ArrayList<>(); params.add(Map.of("mentions", List.of(mention))); params.add(Map.of("commitment", "finalized")); @@ -128,7 +128,7 @@ public void logsSubscribe(String mention, NotificationEventListener listener) { } public void logsSubscribe(List mentions, NotificationEventListener listener) { - List params = new ArrayList(); + List params = new ArrayList<>(); params.add(Map.of("mentions", mentions)); params.add(Map.of("commitment", "finalized")); @@ -203,7 +203,7 @@ public void onError(Exception ex) { } private void updateSubscriptions() { - if (isOpen() && subscriptions.size() > 0) { + if (isOpen() && !subscriptions.isEmpty()) { JsonAdapter rpcRequestJsonAdapter = new Moshi.Builder().build().adapter(RpcRequest.class); for (SubscriptionParams sub : subscriptions.values()) { diff --git a/src/main/java/org/p2p/solanaj/ws/listeners/AccountNotificationEventListener.java b/src/main/java/org/p2p/solanaj/ws/listeners/AccountNotificationEventListener.java index e3a5e7e8..9989bdbe 100644 --- a/src/main/java/org/p2p/solanaj/ws/listeners/AccountNotificationEventListener.java +++ b/src/main/java/org/p2p/solanaj/ws/listeners/AccountNotificationEventListener.java @@ -10,7 +10,6 @@ public class AccountNotificationEventListener implements NotificationEventListen * Handle Account notification event (change in data or change in lamports). Type of "data" is a Map. * @param data Map */ - @SuppressWarnings("rawtypes") @Override public void onNotificationEvent(Object data) { LOGGER.info("Raw = " + data); diff --git a/src/main/java/org/p2p/solanaj/ws/listeners/LogNotificationEventListener.java b/src/main/java/org/p2p/solanaj/ws/listeners/LogNotificationEventListener.java index 76bd3aca..73c02bd1 100644 --- a/src/main/java/org/p2p/solanaj/ws/listeners/LogNotificationEventListener.java +++ b/src/main/java/org/p2p/solanaj/ws/listeners/LogNotificationEventListener.java @@ -10,7 +10,7 @@ public class LogNotificationEventListener implements NotificationEventListener { private static final Logger LOGGER = Logger.getLogger(LogNotificationEventListener.class.getName()); private final RpcClient client; - private PublicKey listeningPubkey; + private final PublicKey listeningPubkey; public LogNotificationEventListener(RpcClient client, PublicKey listeningPubkey) { this.client = client; @@ -21,7 +21,6 @@ public LogNotificationEventListener(RpcClient client, PublicKey listeningPubkey) * Handle Account notification event (change in data or change in lamports). Type of "data" is a Map. * @param data Map */ - @SuppressWarnings("rawtypes") @Override public void onNotificationEvent(Object data) { if (data != null) { diff --git a/src/test/java/org/p2p/solanaj/core/AnchorTest.java b/src/test/java/org/p2p/solanaj/core/AnchorTest.java index 28a98f98..040fd088 100644 --- a/src/test/java/org/p2p/solanaj/core/AnchorTest.java +++ b/src/test/java/org/p2p/solanaj/core/AnchorTest.java @@ -25,19 +25,19 @@ public class AnchorTest extends AccountBasedTest { public void basicInitializeTest() { final Account feePayer = testAccount; - final Transaction transaction = new Transaction(); - transaction.addInstruction( + final LegacyTransaction legacyTransaction = new LegacyTransaction(); + legacyTransaction.addInstruction( AnchorBasicTutorialProgram.initialize(feePayer) ); - transaction.addInstruction( + legacyTransaction.addInstruction( MemoProgram.writeUtf8(feePayer.getPublicKey(), "I just called an Anchor program from SolanaJ.") ); final List signers = List.of(feePayer); String result = null; try { - result = client.getApi().sendTransaction(transaction, signers, null); + result = client.getApi().sendLegacyTransaction(legacyTransaction, signers, null); } catch (RpcException e) { e.printStackTrace(); } diff --git a/src/test/java/org/p2p/solanaj/core/MessageTest.java b/src/test/java/org/p2p/solanaj/core/LegacyMessageTest.java similarity index 79% rename from src/test/java/org/p2p/solanaj/core/MessageTest.java rename to src/test/java/org/p2p/solanaj/core/LegacyMessageTest.java index 8716223c..b5a47ced 100644 --- a/src/test/java/org/p2p/solanaj/core/MessageTest.java +++ b/src/test/java/org/p2p/solanaj/core/LegacyMessageTest.java @@ -6,7 +6,7 @@ import static org.junit.Assert.assertArrayEquals; -public class MessageTest { +public class LegacyMessageTest { @Test public void serializeMessage() { @@ -17,10 +17,10 @@ public void serializeMessage() { Account signer = new Account(Base58 .decode("4Z7cXSyeFR8wNGMVXUE1TwtKn5D5Vu7FzEv69dokLv7KrQk7h6pu4LF8ZRR9yQBhc7uSM6RTTZtU1fmaxiNrxXrs")); - Message message = new Message(); - message.addInstruction(SystemProgram.transfer(fromPublicKey, toPublickKey, lamports)); - message.setRecentBlockHash("Eit7RCyhUixAe2hGBS8oqnw59QK3kgMMjfLME5bm9wRn"); - message.setFeePayer(signer); + LegacyMessage legacyMessage = new LegacyMessage(); + legacyMessage.addInstruction(SystemProgram.transfer(fromPublicKey, toPublickKey, lamports)); + legacyMessage.setRecentBlockHash("Eit7RCyhUixAe2hGBS8oqnw59QK3kgMMjfLME5bm9wRn"); + legacyMessage.setFeePayer(signer); assertArrayEquals(new int[] { 1, 0, 1, 3, 6, 26, 217, 208, 83, 135, 21, 72, 83, 126, 222, 62, 38, 24, 73, 163, 223, 183, 253, 2, 250, 188, 117, 178, 35, 200, 228, 106, 219, 133, 61, 12, 235, 122, 188, 208, 216, 117, @@ -28,7 +28,7 @@ public void serializeMessage() { 188, 173, 205, 229, 170, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 203, 226, 136, 193, 153, 148, 240, 50, 230, 98, 9, 79, 221, 179, 243, 174, 90, 67, 104, 169, 6, 187, 165, 72, 36, 156, 19, 57, 132, 38, 69, 245, 1, 2, 2, 0, 1, 12, 2, 0, 0, 0, 184, 11, 0, - 0, 0, 0, 0, 0 }, toUnsignedByteArray(message.serialize())); + 0, 0, 0, 0, 0 }, toUnsignedByteArray(legacyMessage.serialize())); } int[] toUnsignedByteArray(byte[] in) { diff --git a/src/test/java/org/p2p/solanaj/core/TransactionTest.java b/src/test/java/org/p2p/solanaj/core/LegacyTransactionTest.java similarity index 76% rename from src/test/java/org/p2p/solanaj/core/TransactionTest.java rename to src/test/java/org/p2p/solanaj/core/LegacyTransactionTest.java index 7e8fa2bb..625605a5 100644 --- a/src/test/java/org/p2p/solanaj/core/TransactionTest.java +++ b/src/test/java/org/p2p/solanaj/core/LegacyTransactionTest.java @@ -11,7 +11,7 @@ import org.bitcoinj.core.Base58; -public class TransactionTest { +public class LegacyTransactionTest { private final static Account signer = new Account(Base58 .decode("4Z7cXSyeFR8wNGMVXUE1TwtKn5D5Vu7FzEv69dokLv7KrQk7h6pu4LF8ZRR9yQBhc7uSM6RTTZtU1fmaxiNrxXrs")); @@ -22,11 +22,11 @@ public void signAndSerialize() { PublicKey toPublickKey = new PublicKey("GrDMoeqMLFjeXQ24H56S1RLgT4R76jsuWCd6SvXyGPQ5"); int lamports = 3000; - Transaction transaction = new Transaction(); - transaction.addInstruction(SystemProgram.transfer(fromPublicKey, toPublickKey, lamports)); - transaction.setRecentBlockHash("Eit7RCyhUixAe2hGBS8oqnw59QK3kgMMjfLME5bm9wRn"); - transaction.sign(signer); - byte[] serializedTransaction = transaction.serialize(); + LegacyTransaction legacyTransaction = new LegacyTransaction(); + legacyTransaction.addInstruction(SystemProgram.transfer(fromPublicKey, toPublickKey, lamports)); + legacyTransaction.setRecentBlockHash("Eit7RCyhUixAe2hGBS8oqnw59QK3kgMMjfLME5bm9wRn"); + legacyTransaction.sign(signer); + byte[] serializedTransaction = legacyTransaction.serialize(); assertEquals( "ASdDdWBaKXVRA+6flVFiZokic9gK0+r1JWgwGg/GJAkLSreYrGF4rbTCXNJvyut6K6hupJtm72GztLbWNmRF1Q4BAAEDBhrZ0FOHFUhTft4+JhhJo9+3/QL6vHWyI8jkatuFPQzrerzQ2HXrwm2hsYGjM5s+8qMWlbt6vbxngnO8rc3lqgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAy+KIwZmU8DLmYglP3bPzrlpDaKkGu6VIJJwTOYQmRfUBAgIAAQwCAAAAuAsAAAAAAAA=", @@ -36,7 +36,7 @@ public void signAndSerialize() { @Test public void transactionBuilderTest() { final String memo = "Test memo"; - final Transaction transaction = new TransactionBuilder() + final LegacyTransaction legacyTransaction = new LegacyTransactionBuilder() .addInstruction( MemoProgram.writeUtf8( signer.getPublicKey(), @@ -49,7 +49,7 @@ public void transactionBuilderTest() { assertEquals( "AV6w4Af9PSHhNsTSal4vlPF7Su9QXgCVyfDChHImJITLcS5BlNotKFeMoGw87VwjS3eNA2JCL+MEoReynCNbWAoBAAECBhrZ0FOHFUhTft4+JhhJo9+3/QL6vHWyI8jkatuFPQwFSlNQ+F3IgtYUpVZyeIopbd8eq6vQpgZ4iEky9O72oMviiMGZlPAy5mIJT92z865aQ2ipBrulSCScEzmEJkX1AQEBAAlUZXN0IG1lbW8=", - Base64.getEncoder().encodeToString(transaction.serialize()) + Base64.getEncoder().encodeToString(legacyTransaction.serialize()) ); } diff --git a/src/test/java/org/p2p/solanaj/core/MainnetTest.java b/src/test/java/org/p2p/solanaj/core/MainnetTest.java index f8bdc97f..3a6828a1 100644 --- a/src/test/java/org/p2p/solanaj/core/MainnetTest.java +++ b/src/test/java/org/p2p/solanaj/core/MainnetTest.java @@ -4,7 +4,6 @@ import org.junit.Ignore; import org.junit.Test; import org.p2p.solanaj.programs.MemoProgram; -import org.p2p.solanaj.programs.SystemProgram; import org.p2p.solanaj.rpc.Cluster; import org.p2p.solanaj.rpc.RpcClient; import org.p2p.solanaj.rpc.RpcException; @@ -14,7 +13,6 @@ import org.p2p.solanaj.token.TokenManager; import java.util.*; -import java.util.stream.Collectors; import static org.junit.Assert.*; @@ -93,7 +91,7 @@ public void getAccountInfoJsonParsed() { } /** - * Calls sendTransaction with a call to the Memo program included. + * Calls sendLegacyTransaction with a call to the Memo program included. */ @Test @Ignore @@ -104,7 +102,7 @@ public void transactionMemoTest() { // Create account from private key final Account feePayer = testAccount; - final Transaction transaction = new Transaction(); + final LegacyTransaction legacyTransaction = new LegacyTransaction(); // First intruction it adds here is a small amount of SOL (like 0.000001) just to have some content in the tx // Probably not really needed @@ -117,14 +115,14 @@ public void transactionMemoTest() { // ); // Add instruction to write memo - transaction.addInstruction( + legacyTransaction.addInstruction( MemoProgram.writeUtf8(feePayer.getPublicKey(), "Twitter: skynetcap") ); - // Call sendTransaction + // Call sendLegacyTransaction String result = null; try { - result = client.getApi().sendTransaction(transaction, feePayer); + result = client.getApi().sendLegacyTransaction(legacyTransaction, feePayer); LOGGER.info("Result = " + result); } catch (RpcException e) { e.printStackTrace(); diff --git a/src/test/java/org/p2p/solanaj/programs/BPFLoaderTest.java b/src/test/java/org/p2p/solanaj/programs/BPFLoaderTest.java index 15856aba..ab68a18a 100644 --- a/src/test/java/org/p2p/solanaj/programs/BPFLoaderTest.java +++ b/src/test/java/org/p2p/solanaj/programs/BPFLoaderTest.java @@ -3,8 +3,8 @@ import org.junit.Ignore; import org.junit.Test; import org.p2p.solanaj.core.Account; +import org.p2p.solanaj.core.LegacyTransaction; import org.p2p.solanaj.core.PublicKey; -import org.p2p.solanaj.core.Transaction; import org.p2p.solanaj.rpc.Cluster; import org.p2p.solanaj.rpc.RpcClient; import org.p2p.solanaj.rpc.RpcException; @@ -30,12 +30,12 @@ public void initializeBufferTest() throws RpcException { System.out.println(account.getPublicKey().toBase58()); - Transaction transaction = new Transaction(); + LegacyTransaction legacyTransaction = new LegacyTransaction(); // initialize buffer Account bufferAccount = new Account(); - transaction.addInstruction( + legacyTransaction.addInstruction( SystemProgram.createAccount( account.getPublicKey(), bufferAccount.getPublicKey(), @@ -45,7 +45,7 @@ public void initializeBufferTest() throws RpcException { ) ); - transaction.addInstruction( + legacyTransaction.addInstruction( BPFLoader.initializeBuffer( bufferAccount.getPublicKey(), account.getPublicKey() @@ -53,9 +53,9 @@ public void initializeBufferTest() throws RpcException { ); String hash = client.getApi().getRecentBlockhash(); - transaction.setRecentBlockHash(hash); + legacyTransaction.setRecentBlockHash(hash); - System.out.println("TX: " + client.getApi().sendTransaction(transaction, List.of(account, bufferAccount), hash)); + System.out.println("TX: " + client.getApi().sendLegacyTransaction(legacyTransaction, List.of(account, bufferAccount), hash)); } } diff --git a/src/test/java/org/p2p/solanaj/utils/ShortvecEncodingTest.java b/src/test/java/org/p2p/solanaj/utils/ShortvecEncodingTest.java deleted file mode 100644 index 12f8686e..00000000 --- a/src/test/java/org/p2p/solanaj/utils/ShortvecEncodingTest.java +++ /dev/null @@ -1,21 +0,0 @@ -package org.p2p.solanaj.utils; - -import org.junit.Test; -import static org.junit.Assert.*; - -public class ShortvecEncodingTest { - - @Test - public void encodeLength() { - assertArrayEquals(new byte[] { 0 } /* [0] */, ShortvecEncoding.encodeLength(0)); - assertArrayEquals(new byte[] { 1 } /* [1] */, ShortvecEncoding.encodeLength(1)); - assertArrayEquals(new byte[] { 5 } /* [5] */, ShortvecEncoding.encodeLength(5)); - assertArrayEquals(new byte[] { 127 } /* [0x7f] */, ShortvecEncoding.encodeLength(127)); // 0x7f - assertArrayEquals(new byte[] { -128, 1 }/* [0x80, 0x01] */, ShortvecEncoding.encodeLength(128)); // 0x80 - assertArrayEquals(new byte[] { -1, 1 } /* [0xff, 0x01] */, ShortvecEncoding.encodeLength(255)); // 0xff - assertArrayEquals(new byte[] { -128, 2 } /* [0x80, 0x02] */, ShortvecEncoding.encodeLength(256)); // 0x100 - assertArrayEquals(new byte[] { -1, -1, 1 } /* [0xff, 0xff, 0x01] */, ShortvecEncoding.encodeLength(32767)); // 0x7fff - assertArrayEquals(new byte[] { -128, -128, -128, 1 } /* [0x80, 0x80, 0x80, 0x01] */, - ShortvecEncoding.encodeLength(2097152)); // 0x200000 - } -} diff --git a/src/test/java/org/p2p/solanaj/utils/ShortvecTest.java b/src/test/java/org/p2p/solanaj/utils/ShortvecTest.java new file mode 100644 index 00000000..4b0a7175 --- /dev/null +++ b/src/test/java/org/p2p/solanaj/utils/ShortvecTest.java @@ -0,0 +1,21 @@ +package org.p2p.solanaj.utils; + +import org.junit.Test; +import static org.junit.Assert.*; + +public class ShortvecTest { + + @Test + public void encodeLength() { + assertArrayEquals(new byte[] { 0 } /* [0] */, Shortvec.encodeLength(0)); + assertArrayEquals(new byte[] { 1 } /* [1] */, Shortvec.encodeLength(1)); + assertArrayEquals(new byte[] { 5 } /* [5] */, Shortvec.encodeLength(5)); + assertArrayEquals(new byte[] { 127 } /* [0x7f] */, Shortvec.encodeLength(127)); // 0x7f + assertArrayEquals(new byte[] { -128, 1 }/* [0x80, 0x01] */, Shortvec.encodeLength(128)); // 0x80 + assertArrayEquals(new byte[] { -1, 1 } /* [0xff, 0x01] */, Shortvec.encodeLength(255)); // 0xff + assertArrayEquals(new byte[] { -128, 2 } /* [0x80, 0x02] */, Shortvec.encodeLength(256)); // 0x100 + assertArrayEquals(new byte[] { -1, -1, 1 } /* [0xff, 0xff, 0x01] */, Shortvec.encodeLength(32767)); // 0x7fff + assertArrayEquals(new byte[] { -128, -128, -128, 1 } /* [0x80, 0x80, 0x80, 0x01] */, + Shortvec.encodeLength(2097152)); // 0x200000 + } +} From fbf186b325ebacfe1d283713d64e207aa3e974c6 Mon Sep 17 00:00:00 2001 From: Chintan Patel Date: Fri, 16 Aug 2024 18:19:17 -0500 Subject: [PATCH 02/18] Adding a capability to call getSignaturesForAddress rpc call with before as the parameter --- src/main/java/org/p2p/solanaj/rpc/RpcApi.java | 22 +++++++++++++++++++ .../rpc/types/ConfirmedSignFAddr2.java | 6 +++++ 2 files changed, 28 insertions(+) diff --git a/src/main/java/org/p2p/solanaj/rpc/RpcApi.java b/src/main/java/org/p2p/solanaj/rpc/RpcApi.java index 0a5006ae..1d7c13fe 100644 --- a/src/main/java/org/p2p/solanaj/rpc/RpcApi.java +++ b/src/main/java/org/p2p/solanaj/rpc/RpcApi.java @@ -255,6 +255,28 @@ public List getSignaturesForAddress(PublicKey account, int return result; } + public List getSignaturesForAddress(PublicKey account, String beforeSignature, int limit, + Commitment commitment) + throws RpcException { + if (beforeSignature == null) { + return getSignaturesForAddress(account, limit, commitment); + } + + List params = new ArrayList<>(); + + params.add(account.toString()); + params.add(new ConfirmedSignFAddr2(beforeSignature, limit, commitment)); + + List rawResult = client.call("getSignaturesForAddress", params, List.class); + + List result = new ArrayList<>(); + for (AbstractMap item : rawResult) { + result.add(new SignatureInformation(item)); + } + + return result; + } + public List getProgramAccounts(PublicKey account, long offset, String bytes) throws RpcException { List filters = new ArrayList<>(); filters.add(new Filter(new Memcmp(offset, bytes))); diff --git a/src/main/java/org/p2p/solanaj/rpc/types/ConfirmedSignFAddr2.java b/src/main/java/org/p2p/solanaj/rpc/types/ConfirmedSignFAddr2.java index c823abbe..5766de1e 100644 --- a/src/main/java/org/p2p/solanaj/rpc/types/ConfirmedSignFAddr2.java +++ b/src/main/java/org/p2p/solanaj/rpc/types/ConfirmedSignFAddr2.java @@ -21,4 +21,10 @@ public ConfirmedSignFAddr2(int limit, Commitment commitment) { this.limit = limit; this.commitment = commitment.getValue(); } + + public ConfirmedSignFAddr2(String before, int limit, Commitment commitment) { + this.before = before; + this.limit = limit; + this.commitment = commitment.getValue(); + } } \ No newline at end of file From dfaa37ec9ee593495d88e0d3bc3d34facd687d7e Mon Sep 17 00:00:00 2001 From: Chintan Patel Date: Sat, 17 Aug 2024 16:09:51 -0500 Subject: [PATCH 03/18] Added an ability to read unsigned 128 bits value --- src/main/java/org/p2p/solanaj/utils/ByteUtils.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/org/p2p/solanaj/utils/ByteUtils.java b/src/main/java/org/p2p/solanaj/utils/ByteUtils.java index b89204fe..3b3bcadf 100644 --- a/src/main/java/org/p2p/solanaj/utils/ByteUtils.java +++ b/src/main/java/org/p2p/solanaj/utils/ByteUtils.java @@ -15,6 +15,7 @@ public class ByteUtils { public static final int UINT_32_LENGTH = 4; public static final int UINT_64_LENGTH = 8; + public static final int UINT_128_LENGTH = 16; public static final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray(); public static byte[] readBytes(byte[] buf, int offset, int length) { @@ -48,6 +49,10 @@ public static void uint64ToByteStreamLE(BigInteger val, OutputStream stream) thr } } + public static BigInteger readUint128(byte[] buf, int offset) { + return new BigInteger(reverseBytes(readBytes(buf, offset, UINT_128_LENGTH))); + } + public static String bytesToHex(byte[] bytes) { char[] hexChars = new char[bytes.length * 2]; for (int j = 0; j < bytes.length; j++) { From 94b9f3f54df863da8c158a3eb8c226d9273703d5 Mon Sep 17 00:00:00 2001 From: Chintan Patel Date: Sun, 18 Aug 2024 00:49:21 -0500 Subject: [PATCH 04/18] Added some properties to parse the data received from raydium lp market --- .../p2p/solanaj/rpc/types/TokenResultObjects.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/main/java/org/p2p/solanaj/rpc/types/TokenResultObjects.java b/src/main/java/org/p2p/solanaj/rpc/types/TokenResultObjects.java index 4d1ff2c6..9c6ba69e 100644 --- a/src/main/java/org/p2p/solanaj/rpc/types/TokenResultObjects.java +++ b/src/main/java/org/p2p/solanaj/rpc/types/TokenResultObjects.java @@ -68,6 +68,21 @@ public static class TokenInfo { @Json(name = "tokenAmount") private TokenAmountInfo tokenAmount; + + @Json(name = "decimals") + private Integer decimals; + + @Json(name = "freezeAuthority") + private String freezeAuthority; + + @Json(name = "isInitialized") + private Boolean isInitialized; + + @Json(name = "mintAuthority") + private String mintAuthority; + + @Json(name = "supply") + private String supply; } @Getter From 28c592277b75ffa46dcb3ffd872033540c68a3b7 Mon Sep 17 00:00:00 2001 From: Chintan Patel Date: Tue, 10 Sep 2024 19:30:56 -0500 Subject: [PATCH 05/18] Added logsSubscribeWithId and logsUnSubscribe rpc method calls --- .../ws/SubscriptionWebSocketClient.java | 59 ++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/p2p/solanaj/ws/SubscriptionWebSocketClient.java b/src/main/java/org/p2p/solanaj/ws/SubscriptionWebSocketClient.java index 89345e86..875b9088 100644 --- a/src/main/java/org/p2p/solanaj/ws/SubscriptionWebSocketClient.java +++ b/src/main/java/org/p2p/solanaj/ws/SubscriptionWebSocketClient.java @@ -30,6 +30,10 @@ private static class SubscriptionParams { this.request = request; this.listener = listener; } + + SubscriptionParams(RpcRequest request) { + this.request = request; + } } private final Map subscriptions = new ConcurrentHashMap<>(); @@ -114,10 +118,40 @@ public void signatureSubscribe(String signature, NotificationEventListener liste updateSubscriptions(); } + public void transactionSubscribe(String key, NotificationEventListener listener) { + List params = new ArrayList<>(); + params.add(Map.of("vote", false, + "failed", false, + "accountRequired", key)); + params.add(Map.of("encoding", "jsonParsed", + "commitment", Commitment.PROCESSED.getValue(), + "transaction_details", "full")); + + RpcRequest rpcRequest = new RpcRequest("transactionSubscribe", params); + + subscriptions.put(rpcRequest.getId(), new SubscriptionParams(rpcRequest, listener)); + subscriptionIds.put(rpcRequest.getId(), 0L); + + updateSubscriptions(); + } + public void logsSubscribe(String mention, NotificationEventListener listener) { List params = new ArrayList<>(); params.add(Map.of("mentions", List.of(mention))); - params.add(Map.of("commitment", "finalized")); + params.add(Map.of("commitment", Commitment.PROCESSED.getValue())); + + RpcRequest rpcRequest = new RpcRequest("logsSubscribe", params); + + subscriptions.put(rpcRequest.getId(), new SubscriptionParams(rpcRequest, listener)); + subscriptionIds.put(rpcRequest.getId(), 0L); + + updateSubscriptions(); + } + + public String logsSubscribeWithId(String mention, NotificationEventListener listener) { + List params = new ArrayList<>(); + params.add(Map.of("mentions", List.of(mention))); + params.add(Map.of("commitment", Commitment.PROCESSED.getValue())); RpcRequest rpcRequest = new RpcRequest("logsSubscribe", params); @@ -125,6 +159,7 @@ public void logsSubscribe(String mention, NotificationEventListener listener) { subscriptionIds.put(rpcRequest.getId(), 0L); updateSubscriptions(); + return rpcRequest.getId(); } public void logsSubscribe(List mentions, NotificationEventListener listener) { @@ -140,6 +175,26 @@ public void logsSubscribe(List mentions, NotificationEventListener liste updateSubscriptions(); } + public void logsUnSubscribe(String subscriptionIdKey) { + List params = new ArrayList<>(); + + RpcRequest rpcRequest = new RpcRequest("logsUnSubscribe", params); + Long subscriptionId = subscriptionIds.get(subscriptionIdKey); + params.add(subscriptionId); + + subscriptions.remove(subscriptionIdKey); + subscriptionIds.remove(subscriptionIdKey); + subscriptionListeners.remove(subscriptionId); + + subscriptions.put(rpcRequest.getId(), new SubscriptionParams(rpcRequest)); + subscriptionIds.put(rpcRequest.getId(), 0L); + + updateSubscriptions(); + + subscriptions.remove(rpcRequest.getId()); + subscriptionIds.remove(rpcRequest.getId()); + } + @Override public void onOpen(ServerHandshake handshakedata) { LOGGER.info("Websocket connection opened"); @@ -154,6 +209,7 @@ public void onMessage(String message) { try { RpcResponse rpcResult = resultAdapter.fromJson(message); + assert rpcResult != null; String rpcResultId = rpcResult.getId(); if (rpcResultId != null) { if (subscriptionIds.containsKey(rpcResultId)) { @@ -169,6 +225,7 @@ public void onMessage(String message) { JsonAdapter notificationResultAdapter = new Moshi.Builder().build() .adapter(RpcNotificationResult.class); RpcNotificationResult result = notificationResultAdapter.fromJson(message); + assert result != null; NotificationEventListener listener = subscriptionListeners.get(result.getParams().getSubscription()); Map value = (Map) result.getParams().getResult().getValue(); From df7f9b77f45cf3c2099dac44e6be8c116d8c55f1 Mon Sep 17 00:00:00 2001 From: Chintan Patel Date: Sat, 14 Sep 2024 03:16:46 -0500 Subject: [PATCH 06/18] Updated onClose Subscription websocket so it can reconnect when connection is closed --- .../p2p/solanaj/ws/SubscriptionWebSocketClient.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/p2p/solanaj/ws/SubscriptionWebSocketClient.java b/src/main/java/org/p2p/solanaj/ws/SubscriptionWebSocketClient.java index 875b9088..608c8cc4 100644 --- a/src/main/java/org/p2p/solanaj/ws/SubscriptionWebSocketClient.java +++ b/src/main/java/org/p2p/solanaj/ws/SubscriptionWebSocketClient.java @@ -251,7 +251,15 @@ public void onMessage(String message) { public void onClose(int code, String reason, boolean remote) { System.out.println( "Connection closed by " + (remote ? "remote peer" : "us") + " Code: " + code + " Reason: " + reason); - + try { + boolean connected = reconnectBlocking(); + while (!connected) { + Thread.sleep(1000); + connected = reconnectBlocking(); + } + } catch (InterruptedException e) { + throw new RuntimeException(e); + } } @Override From 39e6d2ed49cd3650d52e5f38cd66cf9c843792bf Mon Sep 17 00:00:00 2001 From: Chintan Patel Date: Mon, 11 Nov 2024 19:18:12 -0600 Subject: [PATCH 07/18] Refactored Jupiter program swap token local field name to avoid confusion --- pom.xml | 2 +- .../org/p2p/solanaj/programs/JupiterSwapProgram.java | 4 ++-- .../p2p/solanaj/ws/SubscriptionWebSocketClient.java | 11 ++--------- 3 files changed, 5 insertions(+), 12 deletions(-) diff --git a/pom.xml b/pom.xml index ce09ae83..76cc8a28 100644 --- a/pom.xml +++ b/pom.xml @@ -199,7 +199,7 @@ /Users/chintan_mbp/.gnupg/ - 0x27FAE7D2 + 4B9021119F83557FF09AEBAAA4751749EC8210A3 diff --git a/src/main/java/org/p2p/solanaj/programs/JupiterSwapProgram.java b/src/main/java/org/p2p/solanaj/programs/JupiterSwapProgram.java index 1a631203..e0c595c1 100644 --- a/src/main/java/org/p2p/solanaj/programs/JupiterSwapProgram.java +++ b/src/main/java/org/p2p/solanaj/programs/JupiterSwapProgram.java @@ -102,8 +102,8 @@ public static String swapToken(RpcClient rpcClient, Account account, String inpu } public static String swapToken(RpcClient rpcClient, Account account, URI quoteUri) { - JsonNode quote = getJupiterQuote(quoteUri); - return swapToken(rpcClient, account, quote); + JsonNode quoteJsonNode = getJupiterQuote(quoteUri); + return swapToken(rpcClient, account, quoteJsonNode); } public record SwapRequest(Object quoteResponse, String userPublicKey) {} diff --git a/src/main/java/org/p2p/solanaj/ws/SubscriptionWebSocketClient.java b/src/main/java/org/p2p/solanaj/ws/SubscriptionWebSocketClient.java index 608c8cc4..b423c203 100644 --- a/src/main/java/org/p2p/solanaj/ws/SubscriptionWebSocketClient.java +++ b/src/main/java/org/p2p/solanaj/ws/SubscriptionWebSocketClient.java @@ -199,6 +199,7 @@ public void logsUnSubscribe(String subscriptionIdKey) { public void onOpen(ServerHandshake handshakedata) { LOGGER.info("Websocket connection opened"); updateSubscriptions(); + LOGGER.info("Size of subscriptions after onOpen: " + subscriptions.size()); } @SuppressWarnings({ "rawtypes" }) @@ -251,15 +252,7 @@ public void onMessage(String message) { public void onClose(int code, String reason, boolean remote) { System.out.println( "Connection closed by " + (remote ? "remote peer" : "us") + " Code: " + code + " Reason: " + reason); - try { - boolean connected = reconnectBlocking(); - while (!connected) { - Thread.sleep(1000); - connected = reconnectBlocking(); - } - } catch (InterruptedException e) { - throw new RuntimeException(e); - } + LOGGER.info("Size of subscriptions after closing: " + subscriptions.size()); } @Override From 3f79b4d8d1d5938fdc2e3e64b489d53c6a98c31b Mon Sep 17 00:00:00 2001 From: Chintan Patel Date: Mon, 11 Nov 2024 21:24:59 -0600 Subject: [PATCH 08/18] Fixed merged conflict and changed naming for legacy transaction and legacy message --- docs/README.md | 112 ++- pom.xml | 63 +- .../java/org/p2p/solanaj/core/Account.java | 27 + .../org/p2p/solanaj/core/AccountKeysList.java | 41 +- .../org/p2p/solanaj/core/AccountMeta.java | 13 +- .../org/p2p/solanaj/core/LegacyMessage.java | 33 +- .../p2p/solanaj/core/LegacyTransaction.java | 70 +- .../java/org/p2p/solanaj/core/Message.java | 53 +- .../java/org/p2p/solanaj/core/PublicKey.java | 78 +- .../org/p2p/solanaj/core/Transaction.java | 69 +- .../p2p/solanaj/core/TransactionBuilder.java | 85 +++ .../solanaj/core/TransactionInstruction.java | 122 +++- .../programs/AddressLookupTableProgram.java | 136 ++++ .../programs/AssociatedTokenProgram.java | 108 ++- .../org/p2p/solanaj/programs/BPFLoader.java | 241 +++++- .../programs/ComputeBudgetProgram.java | 84 ++- .../org/p2p/solanaj/programs/MemoProgram.java | 27 +- .../p2p/solanaj/programs/SystemProgram.java | 104 ++- .../p2p/solanaj/programs/TokenProgram.java | 316 +++++++- src/main/java/org/p2p/solanaj/rpc/RpcApi.java | 260 +++++-- .../java/org/p2p/solanaj/rpc/RpcClient.java | 169 +++-- .../rpc/types/ConfirmedTransaction.java | 6 + .../solanaj/rpc/types/LatestBlockhash.java | 23 + .../rpc/types/RecentPrioritizationFees.java | 21 + .../{Shortvec.java => ShortvecEncoding.java} | 2 +- .../ws/SubscriptionWebSocketClient.java | 686 +++++++++++++++--- .../AccountNotificationEventListener.java | 32 +- .../LogNotificationEventListener.java | 88 ++- .../p2p/solanaj/core/AccountBasedTest.java | 4 +- .../org/p2p/solanaj/core/AccountTest.java | 34 +- .../org/p2p/solanaj/core/AirdropTest.java | 48 -- .../java/org/p2p/solanaj/core/AnchorTest.java | 10 +- .../org/p2p/solanaj/core/BlockhashTest.java | 99 +++ .../p2p/solanaj/core/LegacyMessageTest.java | 5 +- .../solanaj/core/LegacyTransactionTest.java | 4 +- .../org/p2p/solanaj/core/MainnetTest.java | 130 +++- .../org/p2p/solanaj/core/PublicKeyTest.java | 50 +- .../org/p2p/solanaj/core/RpcClientTest.java | 12 +- .../org/p2p/solanaj/core/WebsocketTest.java | 284 +++++++- .../p2p/solanaj/manager/TokenManagerTest.java | 193 +++++ .../AddressLookupTableProgramTest.java | 74 ++ .../programs/AssociatedTokenProgramTest.java | 93 +++ .../p2p/solanaj/programs/BPFLoaderTest.java | 132 +++- .../programs/ComputeBudgetProgramTest.java | 76 ++ .../p2p/solanaj/programs/MemoProgramTest.java | 58 ++ .../solanaj/programs/SystemProgramTest.java | 90 ++- .../solanaj/programs/TokenProgramTest.java | 230 ++++++ .../org/p2p/solanaj/utils/ByteUtilsTest.java | 19 +- .../solanaj/utils/ShortvecEncodingTest.java | 21 + .../org/p2p/solanaj/utils/ShortvecTest.java | 21 - .../ws/LogNotificationEventListenerTest.java | 134 ++++ .../ws/SubscriptionWebSocketClientTest.java | 114 +++ 52 files changed, 4258 insertions(+), 746 deletions(-) create mode 100644 src/main/java/org/p2p/solanaj/core/TransactionBuilder.java create mode 100644 src/main/java/org/p2p/solanaj/programs/AddressLookupTableProgram.java create mode 100644 src/main/java/org/p2p/solanaj/rpc/types/LatestBlockhash.java create mode 100644 src/main/java/org/p2p/solanaj/rpc/types/RecentPrioritizationFees.java rename src/main/java/org/p2p/solanaj/utils/{Shortvec.java => ShortvecEncoding.java} (97%) delete mode 100644 src/test/java/org/p2p/solanaj/core/AirdropTest.java create mode 100644 src/test/java/org/p2p/solanaj/core/BlockhashTest.java create mode 100644 src/test/java/org/p2p/solanaj/manager/TokenManagerTest.java create mode 100644 src/test/java/org/p2p/solanaj/programs/AddressLookupTableProgramTest.java create mode 100644 src/test/java/org/p2p/solanaj/programs/AssociatedTokenProgramTest.java create mode 100644 src/test/java/org/p2p/solanaj/programs/ComputeBudgetProgramTest.java create mode 100644 src/test/java/org/p2p/solanaj/programs/MemoProgramTest.java create mode 100644 src/test/java/org/p2p/solanaj/programs/TokenProgramTest.java create mode 100644 src/test/java/org/p2p/solanaj/utils/ShortvecEncodingTest.java delete mode 100644 src/test/java/org/p2p/solanaj/utils/ShortvecTest.java create mode 100644 src/test/java/org/p2p/solanaj/ws/LogNotificationEventListenerTest.java create mode 100644 src/test/java/org/p2p/solanaj/ws/SubscriptionWebSocketClientTest.java diff --git a/docs/README.md b/docs/README.md index bc166229..13f70b84 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,46 +1,93 @@ # SolanaJ -Solana blockchain client, written in pure Java. -Solanaj is an API for integrating with Solana blockchain using the [Solana RPC API](https://docs.solana.com/apps/jsonrpc-api) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +[![Java Version](https://img.shields.io/badge/Java-17%2B-blue)](https://www.oracle.com/java/technologies/javase/jdk17-archive-downloads.html) +[![Maven Central](https://img.shields.io/maven-central/v/com.mmorrell/solanaj.svg)](https://search.maven.org/artifact/com.mmorrell/solanaj) +[![Solana](https://img.shields.io/badge/Solana-Compatible-blueviolet)](https://solana.com/) +[![Java](https://img.shields.io/badge/Pure-Java-orange)](https://www.java.com/) +[![Documentation](https://img.shields.io/badge/API-Documentation-lightgrey)](https://docs.solana.com/apps/jsonrpc-api) +[![Discord](https://img.shields.io/discord/889577356681945098?color=blueviolet)](https://discord.gg/solana) +[![GitHub Stars](https://img.shields.io/github/stars/skynetcap/solanaj?style=social)](https://github.com/skynetcap/solanaj) + +Solana blockchain client, written in pure Java. SolanaJ is an API for integrating with Solana blockchain using the [Solana RPC API](https://docs.solana.com/apps/jsonrpc-api). This fork includes functionality for multiple Solana programs, including the Serum DEX. -# SolanaJ-Programs -For SolanaJ implementations of popular Solana programs such as Serum, please visit: https://github.com/skynetcap/solanaj-programs +## Table of Contents + +- [SolanaJ-Programs](#solanaj-programs) +- [Requirements](#%EF%B8%8F-requirements) +- [Dependencies](#-dependencies) +- [Installation](#-installation) +- [Build](#%EF%B8%8F-build) +- [Examples](#-examples) + - [Transfer Lamports](#transfer-lamports) + - [Get Balance](#get-balance) + - [Get Serum Market + Orderbooks](#get-serum-market--orderbooks) + - [Send a Transaction with Memo Program](#send-a-transaction-with-memo-program) +- [Contributing](#-contributing) +- [License](#-license) + +## SolanaJ-Programs + +For SolanaJ implementations of popular Solana programs such as Serum, please visit: [https://github.com/skynetcap/solanaj-programs](https://github.com/skynetcap/solanaj-programs) + +## 🛠️ Requirements -## Requirements - Java 17+ -## Dependencies +## 📚 Dependencies + - bitcoinj - OkHttp - Moshi -## Installation -1. Add Maven dependency: +## 📦 Installation + +Add the following Maven dependency to your project's `pom.xml`: ```xml - com.mmorrell - solanaj - 1.17.6 + com.mmorrell + solanaj + 1.19.2 ``` -## Build -In pom.xml update the plugin maven-gpg-plugin configuration with your homedir and keyname. -To see if you have a gpg key run `gpg --list-secret-keys` -If nothing is returned create one with `gpg --full-generate-key` -Then run `mvn install` and the build should complete successfully. +## 🏗️ Build + +1. In `pom.xml`, update the `maven-gpg-plugin` configuration with your homedir and keyname: + ```xml - /home/phil/.gnupg/ - AE2D00367F40E980F7C62FF792C4533F3EE03477 + /home/your_username/.gnupg/ + YOUR_GPG_KEY_ID ``` -## Example -##### Transfer lamports +2. Check if you have a GPG key: + +```sh +gpg --list-secret-keys +``` + +3. If no key is returned, create one: + +```sh +gpg --full-generate-key +``` + +4. Run the Maven install command: + +```sh +mvn install +``` + +The build should complete successfully. + +## 🚀 Examples + +### Transfer Lamports ```java RpcClient client = new RpcClient(Cluster.TESTNET); @@ -57,7 +104,7 @@ legacyTransaction.addInstruction(SystemProgram.transfer(fromPublicKey, toPublick String signature = client.getApi().sendTransaction(legacyTransaction, signer); ``` -##### Get balance +### Get Balance ```java RpcClient client = new RpcClient(Cluster.TESTNET); @@ -65,7 +112,8 @@ RpcClient client = new RpcClient(Cluster.TESTNET); long balance = client.getApi().getBalance(new PublicKey("QqCCvshxtqMAL2CVALqiJB7uEeE5mjSPsseQdDzsRUo")); ``` -##### Get Serum market + orderbooks +### Get Serum Market + Orderbooks + ```java final PublicKey solUsdcPublicKey = new PublicKey("7xMDbYTCqQEcK2aM9LbetGtNFJpzKdfXzLL5juaLh4GJ"); final Market solUsdcMarket = new MarketBuilder() @@ -78,6 +126,7 @@ final OrderBook bids = solUsdcMarket.getBidOrderBook(); ``` ##### Send a legacyTransaction with call to the "Memo" program + ```java // Create account from private key final Account feePayer = new Account(Base58.decode(new String(data))); @@ -88,9 +137,22 @@ legacyTransaction.addInstruction( MemoProgram.writeUtf8(feePayer.getPublicKey(),"Hello from SolanaJ :)") ); -String response = result = client.getApi().sendTransaction(legacyTransaction, feePayer); +String response = client.getApi().sendTransaction(legacyTransaction, feePayer); ``` -## License +## 🤝 Contributing + +We welcome contributions to SolanaJ! Here's how you can help: + +1. Fork the repository +2. Create a new branch (`git checkout -b feature/your-feature-name`) +3. Make your changes +4. Commit your changes (`git commit -am 'Add some feature'`) +5. Push to the branch (`git push origin feature/your-feature-name`) +6. Create a new Pull Request + +Please make sure to update tests as appropriate and adhere to the existing coding style. + +## 📄 License -MIT License \ No newline at end of file +SolanaJ is open-source software licensed under the [MIT License](LICENSE). See the LICENSE file for more details. \ No newline at end of file diff --git a/pom.xml b/pom.xml index 76cc8a28..34f06c02 100644 --- a/pom.xml +++ b/pom.xml @@ -34,9 +34,9 @@ - junit - junit - 4.13.2 + org.junit.jupiter + junit-jupiter-api + 5.11.0 test @@ -120,6 +120,13 @@ 2.17.2 + + org.mockito + mockito-core + 3.12.4 + test + + org.apache.httpcomponents.client5 httpclient5 @@ -184,31 +191,45 @@ 17 + none + false - - org.apache.maven.plugins - maven-gpg-plugin - 3.2.4 - - - sign-artifacts - verify - - sign - - - /Users/chintan_mbp/.gnupg/ - 4B9021119F83557FF09AEBAAA4751749EC8210A3 - - - - org.apache.maven.plugins maven-compiler-plugin 3.13.0 + + maven-surefire-plugin + 3.2.5 + + + + deploy + + + + org.apache.maven.plugins + maven-gpg-plugin + + + sign-artifacts + verify + + sign + + + /Users/chintan_mbp/.gnupg/ + 4B9021119F83557FF09AEBAAA4751749EC8210A3 + + + + + + + + diff --git a/src/main/java/org/p2p/solanaj/core/Account.java b/src/main/java/org/p2p/solanaj/core/Account.java index 54095897..7be0df15 100644 --- a/src/main/java/org/p2p/solanaj/core/Account.java +++ b/src/main/java/org/p2p/solanaj/core/Account.java @@ -5,6 +5,7 @@ import java.util.List; import org.bitcoinj.crypto.*; +import org.bitcoinj.core.Base58; import org.p2p.solanaj.utils.TweetNaclFast; import org.p2p.solanaj.utils.bip32.wallet.SolanaBip44; import org.p2p.solanaj.utils.bip32.wallet.DerivableType; @@ -116,4 +117,30 @@ private static byte[] convertJsonStringToByteArray(String characters) { return buffer.array(); } + + /** + * Creates an Account from a base58-encoded private key string. + * @param base58PrivateKey The base58-encoded private key + * @return A new Account instance + */ + public static Account fromBase58PrivateKey(String base58PrivateKey) { + byte[] privateKey = Base58.decode(base58PrivateKey); + return new Account(privateKey); + } + + /** + * Returns the account's public key as a base58-encoded string. + * @return The base58-encoded public key + */ + public String getPublicKeyBase58() { + return this.getPublicKey().toBase58(); + } + + /** + * Returns the account's private key as a base58-encoded string. + * @return The base58-encoded private key + */ + public String getPrivateKeyBase58() { + return Base58.encode(this.getSecretKey()); + } } \ No newline at end of file diff --git a/src/main/java/org/p2p/solanaj/core/AccountKeysList.java b/src/main/java/org/p2p/solanaj/core/AccountKeysList.java index 8dc448f1..1a85ed72 100644 --- a/src/main/java/org/p2p/solanaj/core/AccountKeysList.java +++ b/src/main/java/org/p2p/solanaj/core/AccountKeysList.java @@ -1,12 +1,9 @@ package org.p2p.solanaj.core; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Comparator; -import java.util.HashMap; +import java.util.*; public class AccountKeysList { - private final HashMap accounts; + private final Map accounts; public AccountKeysList() { accounts = new HashMap<>(); @@ -14,48 +11,28 @@ public AccountKeysList() { public void add(AccountMeta accountMeta) { String key = accountMeta.getPublicKey().toString(); - - if (accounts.containsKey(key)) { - if (!accounts.get(key).isWritable() && accountMeta.isWritable()) { - accounts.put(key, accountMeta); - } - } else { - accounts.put(key, accountMeta); - } + accounts.merge(key, accountMeta, (existing, newMeta) -> + !existing.isWritable() && newMeta.isWritable() ? newMeta : existing); } public void addAll(Collection metas) { - for (AccountMeta meta : metas) { - add(meta); - } + metas.forEach(this::add); } public ArrayList getList() { ArrayList accountKeysList = new ArrayList<>(accounts.values()); accountKeysList.sort(metaComparator); - return accountKeysList; } - private static final Comparator metaComparator = (am1, am2) -> { - - int cmpSigner = am1.isSigner() == am2.isSigner() ? 0 : am1.isSigner() ? -1 : 1; - if (cmpSigner != 0) { - return cmpSigner; - } - - int cmpkWritable = am1.isWritable() == am2.isWritable() ? 0 : am1.isWritable() ? -1 : 1; - if (cmpkWritable != 0) { - return cmpkWritable; - } - - return Integer.compare(cmpSigner, cmpkWritable); - }; - @Override public String toString() { return "AccountKeysList{" + "accounts=" + accounts + '}'; } + + private static final Comparator metaComparator = Comparator + .comparing(AccountMeta::isSigner).reversed() + .thenComparing(AccountMeta::isWritable).reversed(); } diff --git a/src/main/java/org/p2p/solanaj/core/AccountMeta.java b/src/main/java/org/p2p/solanaj/core/AccountMeta.java index 18cd0720..b765f702 100644 --- a/src/main/java/org/p2p/solanaj/core/AccountMeta.java +++ b/src/main/java/org/p2p/solanaj/core/AccountMeta.java @@ -13,6 +13,17 @@ public class AccountMeta { private boolean isWritable; + /** + * Sorting based on isSigner and isWritable cannot fully meet the requirements. This value can be used for custom sorting, because if the order is incorrect during serialization, it may lead to failed method calls. + */ + private int sort = Integer.MAX_VALUE; + + public AccountMeta(PublicKey publicKey, boolean isSigner, boolean isWritable) { + this.publicKey = publicKey; + this.isSigner = isSigner; + this.isWritable = isWritable; + } + @Override public String toString() { return "AccountMeta{" + @@ -21,4 +32,4 @@ public String toString() { ", isWritable=" + isWritable + '}'; } -} \ No newline at end of file +} diff --git a/src/main/java/org/p2p/solanaj/core/LegacyMessage.java b/src/main/java/org/p2p/solanaj/core/LegacyMessage.java index 43ccacee..9b519452 100644 --- a/src/main/java/org/p2p/solanaj/core/LegacyMessage.java +++ b/src/main/java/org/p2p/solanaj/core/LegacyMessage.java @@ -1,12 +1,13 @@ package org.p2p.solanaj.core; +import org.bitcoinj.core.Base58; +import org.p2p.solanaj.utils.ShortvecEncoding; + import java.nio.ByteBuffer; import java.util.ArrayList; +import java.util.Comparator; import java.util.List; - -import org.bitcoinj.core.Base58; - -import org.p2p.solanaj.utils.Shortvec; +import java.util.stream.Collectors; public class LegacyMessage { private static class MessageHeader { @@ -36,6 +37,7 @@ int getLength() { private static final int RECENT_BLOCK_HASH_LENGTH = 32; + private MessageHeader messageHeader; private String recentBlockhash; private final AccountKeysList accountKeys; private final List instructions; @@ -68,12 +70,12 @@ public byte[] serialize() { throw new IllegalArgumentException("No instructions provided"); } - MessageHeader messageHeader = new MessageHeader(); + messageHeader = new MessageHeader(); List keysList = getAccountKeys(); int accountKeysSize = keysList.size(); - byte[] accountAddressesLength = Shortvec.encodeLength(accountKeysSize); + byte[] accountAddressesLength = ShortvecEncoding.encodeLength(accountKeysSize); int compiledInstructionsLength = 0; List compiledInstructions = new ArrayList<>(); @@ -88,9 +90,9 @@ public byte[] serialize() { CompiledInstruction compiledInstruction = new CompiledInstruction(); compiledInstruction.programIdIndex = (byte) findAccountIndex(keysList, instruction.getProgramId()); - compiledInstruction.keyIndicesCount = Shortvec.encodeLength(keysSize); + compiledInstruction.keyIndicesCount = ShortvecEncoding.encodeLength(keysSize); compiledInstruction.keyIndices = keyIndices; - compiledInstruction.dataLength = Shortvec.encodeLength(instruction.getData().length); + compiledInstruction.dataLength = ShortvecEncoding.encodeLength(instruction.getData().length); compiledInstruction.data = instruction.getData(); compiledInstructions.add(compiledInstruction); @@ -98,7 +100,7 @@ public byte[] serialize() { compiledInstructionsLength += compiledInstruction.getLength(); } - byte[] instructionsLength = Shortvec.encodeLength(compiledInstructions.size()); + byte[] instructionsLength = ShortvecEncoding.encodeLength(compiledInstructions.size()); int bufferSize = MessageHeader.HEADER_LENGTH + RECENT_BLOCK_HASH_LENGTH + accountAddressesLength.length + (accountKeysSize * PublicKey.PUBLIC_KEY_LENGTH) + instructionsLength.length @@ -147,9 +149,18 @@ protected void setFeePayer(Account feePayer) { private List getAccountKeys() { List keysList = accountKeys.getList(); - int feePayerIndex = findAccountIndex(keysList, feePayer.getPublicKey()); - List newList = new ArrayList<>(); + // Check whether custom sorting is needed. The `getAccountKeys()` method returns a reversed list of accounts, with signable and mutable accounts at the end, but the fee is placed first. When a transaction involves multiple accounts that need signing, an incorrect order can cause bugs. Change to custom sorting based on the contract order. + boolean needSort = keysList.stream().anyMatch(accountMeta -> accountMeta.getSort() < Integer.MAX_VALUE); + if (needSort) { + // Sort in ascending order based on the `sort` field. + return keysList.stream() + .sorted(Comparator.comparingInt(AccountMeta::getSort)) + .collect(Collectors.toList()); + } + + int feePayerIndex = findAccountIndex(keysList, feePayer.getPublicKey()); + List newList = new ArrayList(); AccountMeta feePayerMeta = keysList.get(feePayerIndex); newList.add(new AccountMeta(feePayerMeta.getPublicKey(), true, true)); keysList.remove(feePayerIndex); diff --git a/src/main/java/org/p2p/solanaj/core/LegacyTransaction.java b/src/main/java/org/p2p/solanaj/core/LegacyTransaction.java index 01e50a27..bc5bff99 100644 --- a/src/main/java/org/p2p/solanaj/core/LegacyTransaction.java +++ b/src/main/java/org/p2p/solanaj/core/LegacyTransaction.java @@ -2,11 +2,12 @@ import java.nio.ByteBuffer; import java.util.ArrayList; -import java.util.Collections; +import java.util.Arrays; import java.util.List; +import java.util.Objects; import org.bitcoinj.core.Base58; -import org.p2p.solanaj.utils.Shortvec; +import org.p2p.solanaj.utils.ShortvecEncoding; import org.p2p.solanaj.utils.TweetNaclFast; public class LegacyTransaction { @@ -17,29 +18,57 @@ public class LegacyTransaction { private final List signatures; private byte[] serializedLegacyMessage; + /** + * Constructs a new Legacy Transaction instance. + */ public LegacyTransaction() { this.legacyMessage = new LegacyMessage(); - this.signatures = new ArrayList<>(); + this.signatures = new ArrayList<>(); // Use diamond operator } + /** + * Adds an instruction to the legacy transaction. + * + * @param instruction The instruction to add + * @return This Transaction instance for method chaining + * @throws NullPointerException if the instruction is null + */ public LegacyTransaction addInstruction(TransactionInstruction instruction) { + Objects.requireNonNull(instruction, "Instruction cannot be null"); // Add input validation legacyMessage.addInstruction(instruction); - return this; } + /** + * Sets the recent blockhash for the legacy transaction. + * + * @param recentBlockhash The recent blockhash to set + * @throws NullPointerException if the recentBlockhash is null + */ public void setRecentBlockHash(String recentBlockhash) { + Objects.requireNonNull(recentBlockhash, "Recent blockhash cannot be null"); // Add input validation legacyMessage.setRecentBlockHash(recentBlockhash); } + /** + * Signs the legacy transaction with a single signer. + * + * @param signer The account to sign the transaction + * @throws NullPointerException if the signer is null + */ public void sign(Account signer) { - sign(Collections.singletonList(signer)); + sign(List.of(Objects.requireNonNull(signer, "Signer cannot be null"))); // Add input validation } + /** + * Signs the legacy transaction with multiple signers. + * + * @param signers The list of accounts to sign the transaction + * @throws IllegalArgumentException if no signers are provided + */ public void sign(List signers) { - - if (signers.isEmpty()) { - throw new IllegalArgumentException("No signers"); + if (signers == null || signers.isEmpty()) { + throw new IllegalArgumentException("No signers provided"); } Account feePayer = signers.get(0); @@ -48,19 +77,28 @@ public void sign(List signers) { serializedLegacyMessage = legacyMessage.serialize(); for (Account signer : signers) { - TweetNaclFast.Signature signatureProvider = new TweetNaclFast.Signature(new byte[0], signer.getSecretKey()); - byte[] signature = signatureProvider.detached(serializedLegacyMessage); - - signatures.add(Base58.encode(signature)); + try { + TweetNaclFast.Signature signatureProvider = new TweetNaclFast.Signature(new byte[0], signer.getSecretKey()); + byte[] signature = signatureProvider.detached(serializedLegacyMessage); + signatures.add(Base58.encode(signature)); + } catch (Exception e) { + throw new RuntimeException("Error signing transaction", e); // Improve exception handling + } } } + /** + * Serializes the legacy transaction into a byte array. + * + * @return The serialized transaction as a byte array + */ public byte[] serialize() { int signaturesSize = signatures.size(); - byte[] signaturesLength = Shortvec.encodeLength(signaturesSize); + byte[] signaturesLength = ShortvecEncoding.encodeLength(signaturesSize); - ByteBuffer out = ByteBuffer - .allocate(signaturesLength.length + signaturesSize * SIGNATURE_LENGTH + serializedLegacyMessage.length); + // Calculate total size before allocating ByteBuffer + int totalSize = signaturesLength.length + signaturesSize * SIGNATURE_LENGTH + serializedLegacyMessage.length; + ByteBuffer out = ByteBuffer.allocate(totalSize); out.put(signaturesLength); @@ -73,4 +111,4 @@ public byte[] serialize() { return out.array(); } -} \ No newline at end of file +} diff --git a/src/main/java/org/p2p/solanaj/core/Message.java b/src/main/java/org/p2p/solanaj/core/Message.java index bb7420e1..54dbf064 100644 --- a/src/main/java/org/p2p/solanaj/core/Message.java +++ b/src/main/java/org/p2p/solanaj/core/Message.java @@ -1,15 +1,19 @@ package org.p2p.solanaj.core; +import org.bitcoinj.core.Base58; +import org.p2p.solanaj.utils.ShortvecEncoding; + import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Comparator; +import java.util.stream.Collectors; import lombok.Getter; -import org.bitcoinj.core.Base58; import org.p2p.solanaj.utils.GuardedArrayUtils; -import org.p2p.solanaj.utils.Shortvec; + public class Message { @Getter @@ -124,13 +128,13 @@ public Message addInstruction(TransactionInstruction instruction) { CompiledInstruction compiledInstruction = new CompiledInstruction(); compiledInstruction.programIdIndex = (byte) findAccountIndex(keysList, instruction.getProgramId()); - compiledInstruction.keyIndicesCount = Shortvec.encodeLength(keysSize); + compiledInstruction.keyIndicesCount = ShortvecEncoding.encodeLength(keysSize); byte[] keyIndices = new byte[keysSize]; for (int i = 0; i < instruction.getKeys().size(); i++) { keyIndices[i] = (byte) findAccountIndex(keysList, instruction.getKeys().get(i).getPublicKey()); } compiledInstruction.keyIndices = keyIndices; - compiledInstruction.dataLength = Shortvec.encodeLength(instruction.getData().length); + compiledInstruction.dataLength = ShortvecEncoding.encodeLength(instruction.getData().length); compiledInstruction.data = instruction.getData(); compiledInstructions.add(compiledInstruction); return this; @@ -153,15 +157,15 @@ public byte[] serialize() { List keysList = getAccountKeys(); int accountKeysSize = keysList.size(); - byte[] accountAddressesLength = Shortvec.encodeLength(accountKeysSize); + byte[] accountAddressesLength = ShortvecEncoding.encodeLength(accountKeysSize); - byte[] instructionsCountLength = Shortvec.encodeLength(compiledInstructions.size()); + byte[] instructionsCountLength = ShortvecEncoding.encodeLength(compiledInstructions.size()); int compiledInstructionsLength = 0; for (CompiledInstruction compiledInstruction : this.compiledInstructions) { compiledInstructionsLength += compiledInstruction.getLength(); } - byte[] addressTableLookupsCountLength = Shortvec.encodeLength(addressTableLookups.size()); + byte[] addressTableLookupsCountLength = ShortvecEncoding.encodeLength(addressTableLookups.size()); int addressTableLookupsLength = 0; for (MessageAddressTableLookup addressTableLookup : this.addressTableLookups) { addressTableLookupsLength += addressTableLookup.getLength(); @@ -213,7 +217,7 @@ public static Message deserialize(List serializedMessageList) { MessageHeader messageHeader = new MessageHeader(messageHeaderBytes); // Total static account keys - int accountKeysSize = Shortvec.decodeLength(serializedMessageList); + int accountKeysSize = ShortvecEncoding.decodeLength(serializedMessageList); List accountKeys = new ArrayList<>(accountKeysSize); for (int i = 0; i < accountKeysSize; i++) { byte[] accountMetaPublicKeyByteArray = GuardedArrayUtils.guardedSplice(serializedMessageList, 0, @@ -229,16 +233,16 @@ public static Message deserialize(List serializedMessageList) { PublicKey.PUBLIC_KEY_LENGTH)); // Deserialize instructions - int instructionsLength = Shortvec.decodeLength(serializedMessageList); + int instructionsLength = ShortvecEncoding.decodeLength(serializedMessageList); List compiledInstructions = new ArrayList<>(instructionsLength); for (int i = 0; i < instructionsLength; i++) { CompiledInstruction compiledInstruction = new CompiledInstruction(); compiledInstruction.programIdIndex = GuardedArrayUtils.guardedShift(serializedMessageList); - int keysSize = Shortvec.decodeLength(serializedMessageList); // keysSize - compiledInstruction.keyIndicesCount = Shortvec.encodeLength(keysSize); + int keysSize = ShortvecEncoding.decodeLength(serializedMessageList); // keysSize + compiledInstruction.keyIndicesCount = ShortvecEncoding.encodeLength(keysSize); compiledInstruction.keyIndices = GuardedArrayUtils.guardedSplice(serializedMessageList, 0, keysSize); - var dataLength = Shortvec.decodeLength(serializedMessageList); - compiledInstruction.dataLength = Shortvec.encodeLength(dataLength); + var dataLength = ShortvecEncoding.decodeLength(serializedMessageList); + compiledInstruction.dataLength = ShortvecEncoding.encodeLength(dataLength); compiledInstruction.data = GuardedArrayUtils.guardedSplice(serializedMessageList, 0, dataLength); compiledInstructions.add(compiledInstruction); @@ -246,17 +250,17 @@ public static Message deserialize(List serializedMessageList) { } // Deserialize addressTableLookups - int addressTableLookupsLength = Shortvec.decodeLength(serializedMessageList); + int addressTableLookupsLength = ShortvecEncoding.decodeLength(serializedMessageList); List addressTableLookups = new ArrayList<>(addressTableLookupsLength); for (int i = 0; i < addressTableLookupsLength; i++) { MessageAddressTableLookup addressTableLookup = new MessageAddressTableLookup(); byte[] accountKeyByteArray = GuardedArrayUtils.guardedSplice(serializedMessageList, 0, PublicKey.PUBLIC_KEY_LENGTH); addressTableLookup.accountKey = new PublicKey(accountKeyByteArray); - int writableIndexesLength = Shortvec.decodeLength(serializedMessageList); // keysSize - addressTableLookup.writableIndexesCountLength = Shortvec.encodeLength(writableIndexesLength); + int writableIndexesLength = ShortvecEncoding.decodeLength(serializedMessageList); // keysSize + addressTableLookup.writableIndexesCountLength = ShortvecEncoding.encodeLength(writableIndexesLength); addressTableLookup.writableIndexes = GuardedArrayUtils.guardedSplice(serializedMessageList, 0, writableIndexesLength); - int readonlyIndexesLength = Shortvec.decodeLength(serializedMessageList); - addressTableLookup.readonlyIndexesCountLength = Shortvec.encodeLength(readonlyIndexesLength); + int readonlyIndexesLength = ShortvecEncoding.decodeLength(serializedMessageList); + addressTableLookup.readonlyIndexesCountLength = ShortvecEncoding.encodeLength(readonlyIndexesLength); addressTableLookup.readonlyIndexes = GuardedArrayUtils.guardedSplice(serializedMessageList, 0, readonlyIndexesLength); addressTableLookups.add(addressTableLookup); @@ -271,9 +275,18 @@ protected void setFeePayer(Account feePayer) { public List getAccountKeys() { List keysList = accountKeys.getList(); - int feePayerIndex = findAccountIndex(keysList, feePayer.getPublicKey()); - List newList = new ArrayList<>(); + // Check whether custom sorting is needed. The `getAccountKeys()` method returns a reversed list of accounts, with signable and mutable accounts at the end, but the fee is placed first. When a transaction involves multiple accounts that need signing, an incorrect order can cause bugs. Change to custom sorting based on the contract order. + boolean needSort = keysList.stream().anyMatch(accountMeta -> accountMeta.getSort() < Integer.MAX_VALUE); + if (needSort) { + // Sort in ascending order based on the `sort` field. + return keysList.stream() + .sorted(Comparator.comparingInt(AccountMeta::getSort)) + .collect(Collectors.toList()); + } + + int feePayerIndex = findAccountIndex(keysList, feePayer.getPublicKey()); + List newList = new ArrayList(); AccountMeta feePayerMeta = keysList.get(feePayerIndex); newList.add(new AccountMeta(feePayerMeta.getPublicKey(), true, true)); keysList.remove(feePayerIndex); diff --git a/src/main/java/org/p2p/solanaj/core/PublicKey.java b/src/main/java/org/p2p/solanaj/core/PublicKey.java index cf8deda5..9bfe8b96 100644 --- a/src/main/java/org/p2p/solanaj/core/PublicKey.java +++ b/src/main/java/org/p2p/solanaj/core/PublicKey.java @@ -1,6 +1,7 @@ package org.p2p.solanaj.core; import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -22,19 +23,16 @@ public class PublicKey { public PublicKey(String pubkey) { if (pubkey.length() < PUBLIC_KEY_LENGTH) { - throw new IllegalArgumentException("Invalid public key input"); + throw new IllegalArgumentException("Invalid public key input: length must be at least " + PUBLIC_KEY_LENGTH); } - this.pubkey = Base58.decode(pubkey); } public PublicKey(byte[] pubkey) { - - if (pubkey.length > PUBLIC_KEY_LENGTH) { - throw new IllegalArgumentException("Invalid public key input"); + if (pubkey.length != PUBLIC_KEY_LENGTH) { + throw new IllegalArgumentException("Invalid public key input: length must be exactly " + PUBLIC_KEY_LENGTH); } - - this.pubkey = pubkey; + this.pubkey = Arrays.copyOf(pubkey, PUBLIC_KEY_LENGTH); } public static PublicKey readPubkey(byte[] bytes, int offset) { @@ -61,14 +59,10 @@ public int hashCode() { @Override public boolean equals(Object o) { - if (this == o) - return true; - if (o == null) - return false; - if (getClass() != o.getClass()) - return false; - PublicKey person = (PublicKey) o; - return equals(person); + if (this == o) return true; + if (!(o instanceof PublicKey)) return false; + PublicKey other = (PublicKey) o; + return Arrays.equals(this.pubkey, other.pubkey); } public String toString() { @@ -76,26 +70,26 @@ public String toString() { } public static PublicKey createProgramAddress(List seeds, PublicKey programId) { - ByteArrayOutputStream buffer = new ByteArrayOutputStream(); - - for (byte[] seed : seeds) { - if (seed.length > 32) { - throw new IllegalArgumentException("Max seed length exceeded"); + try (ByteArrayOutputStream buffer = new ByteArrayOutputStream()) { + for (byte[] seed : seeds) { + if (seed.length > 32) { + throw new IllegalArgumentException("Max seed length exceeded: " + seed.length); + } + buffer.write(seed); } + buffer.write(programId.toByteArray()); + buffer.write("ProgramDerivedAddress".getBytes()); - buffer.writeBytes(seed); - } - - buffer.writeBytes(programId.toByteArray()); - buffer.writeBytes("ProgramDerivedAddress".getBytes()); + byte[] hash = Sha256Hash.hash(buffer.toByteArray()); - byte[] hash = Sha256Hash.hash(buffer.toByteArray()); + if (TweetNaclFast.is_on_curve(hash) != 0) { + throw new IllegalStateException("Invalid seeds, address must fall off the curve"); + } - if (TweetNaclFast.is_on_curve(hash) != 0) { - throw new RuntimeException("Invalid seeds, address must fall off the curve"); + return new PublicKey(hash); + } catch (IOException e) { + throw new RuntimeException("Error creating program address", e); } - - return new PublicKey(hash); } @Getter @@ -110,26 +104,18 @@ public ProgramDerivedAddress(PublicKey address, int nonce) { } - public static ProgramDerivedAddress findProgramAddress(List seeds, PublicKey programId) throws Exception { - int nonce = 255; - PublicKey address; - - List seedsWithNonce = new ArrayList<>(seeds); - - while (nonce != 0) { + public static ProgramDerivedAddress findProgramAddress(List seeds, PublicKey programId) { + for (int nonce = 255; nonce >= 0; nonce--) { try { + List seedsWithNonce = new ArrayList<>(seeds); seedsWithNonce.add(new byte[] { (byte) nonce }); - address = createProgramAddress(seedsWithNonce, programId); - } catch (Exception e) { - seedsWithNonce.remove(seedsWithNonce.size() - 1); - nonce--; - continue; + PublicKey address = createProgramAddress(seedsWithNonce, programId); + return new ProgramDerivedAddress(address, nonce); + } catch (IllegalStateException e) { + // Address was on the curve, try next nonce } - - return new ProgramDerivedAddress(address, nonce); } - - throw new Exception("Unable to find a viable program address nonce"); + throw new IllegalStateException("Unable to find a viable program address nonce"); } public static PublicKey valueOf(String publicKey) { diff --git a/src/main/java/org/p2p/solanaj/core/Transaction.java b/src/main/java/org/p2p/solanaj/core/Transaction.java index 8b9590f7..943fd8a6 100644 --- a/src/main/java/org/p2p/solanaj/core/Transaction.java +++ b/src/main/java/org/p2p/solanaj/core/Transaction.java @@ -5,13 +5,18 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Objects; import org.bitcoinj.core.Base58; import org.p2p.solanaj.utils.ByteUtils; import org.p2p.solanaj.utils.GuardedArrayUtils; -import org.p2p.solanaj.utils.Shortvec; +import org.p2p.solanaj.utils.ShortvecEncoding; import org.p2p.solanaj.utils.TweetNaclFast; +/** + * Represents a Solana transaction. + * This class allows for building, signing, and serializing transactions. + */ public class Transaction { public static final int SIGNATURE_LENGTH = 64; @@ -20,6 +25,9 @@ public class Transaction { private final List signatures; private byte[] serializedMessage; + /** + * Constructs a new Transaction instance. + */ public Transaction() { this.message = new Message(); this.signatures = new ArrayList<>(); @@ -30,23 +38,49 @@ public Transaction(Message message, List signatures) { this.signatures = signatures; } + /** + * Adds an instruction to the transaction. + * + * @param instruction The instruction to add + * @return This Transaction instance for method chaining + * @throws NullPointerException if the instruction is null + */ public Transaction addInstruction(TransactionInstruction instruction) { + Objects.requireNonNull(instruction, "Instruction cannot be null"); // Add input validation message.addInstruction(instruction); - return this; } + /** + * Sets the recent blockhash for the transaction. + * + * @param recentBlockhash The recent blockhash to set + * @throws NullPointerException if the recentBlockhash is null + */ public void setRecentBlockHash(String recentBlockhash) { + Objects.requireNonNull(recentBlockhash, "Recent blockhash cannot be null"); // Add input validation message.setRecentBlockHash(recentBlockhash); } + /** + * Signs the transaction with a single signer. + * + * @param signer The account to sign the transaction + * @throws NullPointerException if the signer is null + */ public void sign(Account signer) { - sign(Collections.singletonList(signer)); + sign(List.of(Objects.requireNonNull(signer, "Signer cannot be null"))); // Add input validation } + /** + * Signs the transaction with multiple signers. + * + * @param signers The list of accounts to sign the transaction + * @throws IllegalArgumentException if no signers are provided + */ public void sign(List signers) { - if (signers.isEmpty()) { - throw new IllegalArgumentException("No signers"); + if (signers == null || signers.isEmpty()) { + throw new IllegalArgumentException("No signers provided"); } Account feePayer = signers.get(0); @@ -63,19 +97,28 @@ public void sign(List signers) { throw new IllegalArgumentException("Cannot sign with non signer key: " + signer.getPublicKey().toBase58()); } - TweetNaclFast.Signature signatureProvider = new TweetNaclFast.Signature(new byte[0], signer.getSecretKey()); - byte[] signature = signatureProvider.detached(serializedMessage); - - this.signatures.set(signerIndex, Base58.encode(signature)); + try { + TweetNaclFast.Signature signatureProvider = new TweetNaclFast.Signature(new byte[0], signer.getSecretKey()); + byte[] signature = signatureProvider.detached(serializedMessage); + this.signatures.set(signerIndex, Base58.encode(signature)); + } catch (Exception e) { + throw new RuntimeException("Error signing transaction", e); // Improve exception handling + } } } + /** + * Serializes the transaction into a byte array. + * + * @return The serialized transaction as a byte array + */ public byte[] serialize() { int signaturesSize = signatures.size(); - byte[] signaturesLength = Shortvec.encodeLength(signaturesSize); + byte[] signaturesLength = ShortvecEncoding.encodeLength(signaturesSize); - ByteBuffer out = ByteBuffer - .allocate(signaturesLength.length + signaturesSize * SIGNATURE_LENGTH + serializedMessage.length); + // Calculate total size before allocating ByteBuffer + int totalSize = signaturesLength.length + signaturesSize * SIGNATURE_LENGTH + serializedMessage.length; + ByteBuffer out = ByteBuffer.allocate(totalSize); out.put(signaturesLength); @@ -92,7 +135,7 @@ public byte[] serialize() { public static Transaction deserialize(byte[] serializedTransaction) { List serializedTransactionList = ByteUtils.toByteList(serializedTransaction); - int signaturesSize = Shortvec.decodeLength(serializedTransactionList); + int signaturesSize = ShortvecEncoding.decodeLength(serializedTransactionList); List signatures = new ArrayList<>(signaturesSize); for (int i = 0; i < signaturesSize; i++) { diff --git a/src/main/java/org/p2p/solanaj/core/TransactionBuilder.java b/src/main/java/org/p2p/solanaj/core/TransactionBuilder.java new file mode 100644 index 00000000..2841ba52 --- /dev/null +++ b/src/main/java/org/p2p/solanaj/core/TransactionBuilder.java @@ -0,0 +1,85 @@ +package org.p2p.solanaj.core; + +import java.util.List; +import java.util.Objects; + +/** + * Builder for constructing {@link Transaction} objects to be used in sendTransaction. + */ +public class TransactionBuilder { + + private final Transaction transaction; + + /** + * Constructs a new TransactionBuilder. + */ + public TransactionBuilder() { + this.transaction = new Transaction(); + } + + /** + * Adds a single instruction to the transaction. + * + * @param transactionInstruction the instruction to add + * @return this builder for method chaining + * @throws NullPointerException if transactionInstruction is null + */ + public TransactionBuilder addInstruction(TransactionInstruction transactionInstruction) { + Objects.requireNonNull(transactionInstruction, "Transaction instruction cannot be null"); + transaction.addInstruction(transactionInstruction); + return this; + } + + /** + * Adds multiple instructions to the transaction. + * + * @param instructions the list of instructions to add + * @return this builder for method chaining + * @throws NullPointerException if instructions is null + */ + public TransactionBuilder addInstructions(List instructions) { + Objects.requireNonNull(instructions, "Instructions list cannot be null"); + instructions.forEach(this::addInstruction); + return this; + } + + /** + * Sets the recent block hash for the transaction. + * + * @param recentBlockHash the recent block hash to set + * @return this builder for method chaining + * @throws NullPointerException if recentBlockHash is null + */ + public TransactionBuilder setRecentBlockHash(String recentBlockHash) { + Objects.requireNonNull(recentBlockHash, "Recent block hash cannot be null"); + transaction.setRecentBlockHash(recentBlockHash); + return this; + } + + /** + * Sets the signers for the transaction and signs it. + * + * @param signers the list of signers + * @return this builder for method chaining + * @throws NullPointerException if signers is null + * @throws IllegalArgumentException if signers is empty + */ + public TransactionBuilder setSigners(List signers) { + Objects.requireNonNull(signers, "Signers list cannot be null"); + if (signers.isEmpty()) { + throw new IllegalArgumentException("Signers list cannot be empty"); + } + transaction.sign(signers); + return this; + } + + /** + * Builds and returns the constructed Transaction object. + * + * @return the built Transaction + */ + public Transaction build() { + return transaction; + } + +} diff --git a/src/main/java/org/p2p/solanaj/core/TransactionInstruction.java b/src/main/java/org/p2p/solanaj/core/TransactionInstruction.java index 06cf9343..578c599d 100644 --- a/src/main/java/org/p2p/solanaj/core/TransactionInstruction.java +++ b/src/main/java/org/p2p/solanaj/core/TransactionInstruction.java @@ -1,17 +1,127 @@ package org.p2p.solanaj.core; -import lombok.AllArgsConstructor; import lombok.Getter; - +import java.util.Collections; import java.util.List; +import java.util.Objects; +import java.util.Arrays; +/** + * Represents an instruction to be executed by a Solana program. + */ @Getter -@AllArgsConstructor public class TransactionInstruction { - private PublicKey programId; + private final PublicKey programId; + private final List keys; + private final byte[] data; + + public TransactionInstruction(PublicKey programId, List keys, byte[] data) { + this.programId = Objects.requireNonNull(programId, "Program ID cannot be null"); + this.keys = Collections.unmodifiableList(Objects.requireNonNull(keys, "Keys cannot be null")); + this.data = Arrays.copyOf(Objects.requireNonNull(data, "Data cannot be null"), data.length); + } + + /** + * Creates a new builder for TransactionInstruction. + * + * @return a new Builder instance + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Compares this TransactionInstruction with another object for equality. + * + * @param o the object to compare with + * @return true if the objects are equal, false otherwise + */ + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + TransactionInstruction that = (TransactionInstruction) o; + return Objects.equals(programId, that.programId) && + Objects.equals(keys, that.keys) && + Arrays.equals(data, that.data); + } + + /** + * Generates a hash code for this TransactionInstruction. + * + * @return the hash code + */ + @Override + public int hashCode() { + int result = Objects.hash(programId, keys); + result = 31 * result + Arrays.hashCode(data); + return result; + } + + /** + * Returns a string representation of this TransactionInstruction. + * + * @return a string representation of the object + */ + @Override + public String toString() { + return "TransactionInstruction{" + + "programId=" + programId + + ", keys=" + keys + + ", data=" + Arrays.toString(data) + + '}'; + } + + /** + * Builder class for creating TransactionInstruction instances. + */ + public static class Builder { + private PublicKey programId; + private List keys; + private byte[] data; + + /** + * Sets the program ID for this instruction. + * + * @param programId the PublicKey of the program to execute this instruction + * @return this Builder instance + */ + public Builder programId(PublicKey programId) { + this.programId = programId; + return this; + } + + /** + * Sets the list of account keys for this instruction. + * + * @param keys the list of AccountMeta objects representing the accounts + * @return this Builder instance + */ + public Builder keys(List keys) { + this.keys = keys; + return this; + } - private List keys; + /** + * Sets the instruction data. + * + * @param data the byte array containing the instruction data + * @return this Builder instance + */ + public Builder data(byte[] data) { + this.data = data; + return this; + } - private byte[] data; + /** + * Builds and returns a new TransactionInstruction instance. + * + * @return a new TransactionInstruction instance + * @throws NullPointerException if programId, keys, or data is null + */ + public TransactionInstruction build() { + return new TransactionInstruction(programId, keys, data); + } + } } diff --git a/src/main/java/org/p2p/solanaj/programs/AddressLookupTableProgram.java b/src/main/java/org/p2p/solanaj/programs/AddressLookupTableProgram.java new file mode 100644 index 00000000..d38ffc1d --- /dev/null +++ b/src/main/java/org/p2p/solanaj/programs/AddressLookupTableProgram.java @@ -0,0 +1,136 @@ +package org.p2p.solanaj.programs; + +import org.p2p.solanaj.core.AccountMeta; +import org.p2p.solanaj.core.PublicKey; +import org.p2p.solanaj.core.TransactionInstruction; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.List; + +/** + * Factory class for creating Address Lookup Table program instructions. + */ +public class AddressLookupTableProgram extends Program { + + /** The program ID for the Address Lookup Table program */ + public static final PublicKey PROGRAM_ID = new PublicKey("AddressLookupTab1e1111111111111111111111111"); + + private static final byte CREATE_LOOKUP_TABLE = 0; + private static final byte FREEZE_LOOKUP_TABLE = 1; + private static final byte EXTEND_LOOKUP_TABLE = 2; + private static final byte DEACTIVATE_LOOKUP_TABLE = 3; + private static final byte CLOSE_LOOKUP_TABLE = 4; + + /** + * Creates an instruction to create a new address lookup table. + * + * @param authority The authority (signer) that can modify the table + * @param payer The account paying for the table creation + * @param recentSlot A recent slot to derive the table's address + * @return A TransactionInstruction to create a new address lookup table + */ + public static TransactionInstruction createLookupTable(PublicKey authority, PublicKey payer, long recentSlot) { + PublicKey derivedAddress = PublicKey.findProgramAddress( + List.of(authority.toByteArray(), ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN).putLong(recentSlot).array()), + PROGRAM_ID + ).getAddress(); + + List keys = new ArrayList<>(); + keys.add(new AccountMeta(derivedAddress, false, true)); + keys.add(new AccountMeta(authority, true, false)); + keys.add(new AccountMeta(payer, true, true)); + keys.add(new AccountMeta(SystemProgram.PROGRAM_ID, false, false)); + + ByteBuffer data = ByteBuffer.allocate(9); + data.order(ByteOrder.LITTLE_ENDIAN); + data.put(CREATE_LOOKUP_TABLE); + data.putLong(recentSlot); + + return createTransactionInstruction(PROGRAM_ID, keys, data.array()); + } + + /** + * Creates an instruction to freeze an address lookup table. + * + * @param lookupTable The address of the lookup table to freeze + * @param authority The authority (signer) of the lookup table + * @return A TransactionInstruction to freeze an address lookup table + */ + public static TransactionInstruction freezeLookupTable(PublicKey lookupTable, PublicKey authority) { + List keys = new ArrayList<>(); + keys.add(new AccountMeta(lookupTable, false, true)); + keys.add(new AccountMeta(authority, true, false)); + + ByteBuffer data = ByteBuffer.allocate(1); + data.put(FREEZE_LOOKUP_TABLE); + + return createTransactionInstruction(PROGRAM_ID, keys, data.array()); + } + + /** + * Creates an instruction to extend an address lookup table. + * + * @param lookupTable The address of the lookup table to extend + * @param authority The authority (signer) of the lookup table + * @param payer The account paying for the table extension + * @param addresses The list of addresses to add to the table + * @return A TransactionInstruction to extend an address lookup table + */ + public static TransactionInstruction extendLookupTable(PublicKey lookupTable, PublicKey authority, PublicKey payer, List addresses) { + List keys = new ArrayList<>(); + keys.add(new AccountMeta(lookupTable, false, true)); + keys.add(new AccountMeta(authority, true, false)); + keys.add(new AccountMeta(payer, true, true)); + keys.add(new AccountMeta(SystemProgram.PROGRAM_ID, false, false)); + + ByteBuffer data = ByteBuffer.allocate(1 + 4 + addresses.size() * 32); + data.order(ByteOrder.LITTLE_ENDIAN); + data.put(EXTEND_LOOKUP_TABLE); + data.putInt(addresses.size()); + for (PublicKey address : addresses) { + data.put(address.toByteArray()); + } + + return createTransactionInstruction(PROGRAM_ID, keys, data.array()); + } + + /** + * Creates an instruction to deactivate an address lookup table. + * + * @param lookupTable The address of the lookup table to deactivate + * @param authority The authority (signer) of the lookup table + * @return A TransactionInstruction to deactivate an address lookup table + */ + public static TransactionInstruction deactivateLookupTable(PublicKey lookupTable, PublicKey authority) { + List keys = new ArrayList<>(); + keys.add(new AccountMeta(lookupTable, false, true)); + keys.add(new AccountMeta(authority, true, false)); + + ByteBuffer data = ByteBuffer.allocate(1); + data.put(DEACTIVATE_LOOKUP_TABLE); + + return createTransactionInstruction(PROGRAM_ID, keys, data.array()); + } + + /** + * Creates an instruction to close an address lookup table. + * + * @param lookupTable The address of the lookup table to close + * @param authority The authority (signer) of the lookup table + * @param recipient The account to receive the closed table's lamports + * @return A TransactionInstruction to close an address lookup table + */ + public static TransactionInstruction closeLookupTable(PublicKey lookupTable, PublicKey authority, PublicKey recipient) { + List keys = new ArrayList<>(); + keys.add(new AccountMeta(lookupTable, false, true)); + keys.add(new AccountMeta(authority, true, false)); + keys.add(new AccountMeta(recipient, false, true)); + + ByteBuffer data = ByteBuffer.allocate(1); + data.put(CLOSE_LOOKUP_TABLE); + + return createTransactionInstruction(PROGRAM_ID, keys, data.array()); + } +} \ No newline at end of file diff --git a/src/main/java/org/p2p/solanaj/programs/AssociatedTokenProgram.java b/src/main/java/org/p2p/solanaj/programs/AssociatedTokenProgram.java index 78bc844a..1a26e1e7 100644 --- a/src/main/java/org/p2p/solanaj/programs/AssociatedTokenProgram.java +++ b/src/main/java/org/p2p/solanaj/programs/AssociatedTokenProgram.java @@ -15,17 +15,96 @@ public class AssociatedTokenProgram extends Program { public static final PublicKey PROGRAM_ID = new PublicKey("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"); + private static final int CREATE_METHOD_ID = 0; private static final int CREATE_IDEMPOTENT_METHOD_ID = 1; + private static final int RECOVER_NESTED_METHOD_ID = 2; + /** + * Creates an associated token account for the given wallet address and token mint. + * Returns an error if the account already exists. + * + * @param fundingAccount The account funding the creation of the associated token account + * @param walletAddress The wallet address for the new associated token account + * @param mint The token mint for the new associated token account + * @return TransactionInstruction for creating the associated token account + */ + public static TransactionInstruction create(PublicKey fundingAccount, + PublicKey walletAddress, + PublicKey mint) { + return createInstruction(CREATE_METHOD_ID, fundingAccount, walletAddress, mint); + } + + /** + * Creates an associated token account for the given wallet address and token mint, + * if it doesn't already exist. Returns an error if the account exists but with a different owner. + * + * @param fundingAccount The account funding the creation of the associated token account + * @param walletAddress The wallet address for the new associated token account + * @param mint The token mint for the new associated token account + * @return TransactionInstruction for creating the associated token account idempotently + */ public static TransactionInstruction createIdempotent(PublicKey fundingAccount, PublicKey walletAddress, PublicKey mint) { + return createInstruction(CREATE_IDEMPOTENT_METHOD_ID, fundingAccount, walletAddress, mint); + } + + /** + * Transfers from and closes a nested associated token account: an associated token account + * owned by an associated token account. + * + * @param nestedAccount The nested associated token account to be closed + * @param nestedMint The token mint for the nested associated token account + * @param destinationAccount The wallet's associated token account to receive the tokens + * @param ownerAccount The owner associated token account address + * @param ownerMint The token mint for the owner associated token account + * @param wallet The wallet address for the owner associated token account + * @return TransactionInstruction for recovering a nested associated token account + */ + public static TransactionInstruction recoverNested(PublicKey nestedAccount, + PublicKey nestedMint, + PublicKey destinationAccount, + PublicKey ownerAccount, + PublicKey ownerMint, + PublicKey wallet) { final List keys = new ArrayList<>(); - // ATA pda - PublicKey pda = null; + keys.add(new AccountMeta(nestedAccount, false, true)); + keys.add(new AccountMeta(nestedMint, false, false)); + keys.add(new AccountMeta(destinationAccount, false, true)); + keys.add(new AccountMeta(ownerAccount, false, false)); + keys.add(new AccountMeta(ownerMint, false, false)); + keys.add(new AccountMeta(wallet, true, true)); + keys.add(new AccountMeta(TokenProgram.PROGRAM_ID, false, false)); + + byte[] transactionData = encodeInstructionData(RECOVER_NESTED_METHOD_ID); + + return createTransactionInstruction(PROGRAM_ID, keys, transactionData); + } + + private static TransactionInstruction createInstruction(int methodId, + PublicKey fundingAccount, + PublicKey walletAddress, + PublicKey mint) { + final List keys = new ArrayList<>(); + + PublicKey pda = findAssociatedTokenAddress(walletAddress, mint); + + keys.add(new AccountMeta(fundingAccount, true, true)); + keys.add(new AccountMeta(pda, false, true)); + keys.add(new AccountMeta(walletAddress, false, false)); + keys.add(new AccountMeta(mint, false, false)); + keys.add(new AccountMeta(SystemProgram.PROGRAM_ID, false, false)); + keys.add(new AccountMeta(TokenProgram.PROGRAM_ID, false, false)); + + byte[] transactionData = encodeInstructionData(methodId); + + return createTransactionInstruction(PROGRAM_ID, keys, transactionData); + } + + private static PublicKey findAssociatedTokenAddress(PublicKey walletAddress, PublicKey mint) { try { - pda = PublicKey.findProgramAddress( + PublicKey pda = PublicKey.findProgramAddress( List.of( walletAddress.toByteArray(), TokenProgram.PROGRAM_ID.toByteArray(), @@ -34,32 +113,17 @@ public static TransactionInstruction createIdempotent(PublicKey fundingAccount, PROGRAM_ID ).getAddress(); log.info("ATA: {}", pda.toBase58()); + return pda; } catch (Exception e) { log.error("Error finding ATA: {}", e.getMessage()); + throw new RuntimeException("Failed to find associated token address", e); } - - keys.add(new AccountMeta(fundingAccount, true, true)); - keys.add(new AccountMeta(pda, false, true)); - keys.add(new AccountMeta(walletAddress, false, false)); - keys.add(new AccountMeta(mint, false, false)); - keys.add(new AccountMeta(SystemProgram.PROGRAM_ID, false, false)); - keys.add(new AccountMeta(TokenProgram.PROGRAM_ID, false, false)); - - byte[] transactionData = encodeTransferTokenInstructionData(); - - return createTransactionInstruction( - PROGRAM_ID, - keys, - transactionData - ); } - private static byte[] encodeTransferTokenInstructionData() { + private static byte[] encodeInstructionData(int methodId) { ByteBuffer result = ByteBuffer.allocate(1); result.order(ByteOrder.LITTLE_ENDIAN); - result.put((byte) CREATE_IDEMPOTENT_METHOD_ID); - + result.put((byte) methodId); return result.array(); } - } diff --git a/src/main/java/org/p2p/solanaj/programs/BPFLoader.java b/src/main/java/org/p2p/solanaj/programs/BPFLoader.java index 15e9af8c..e8984878 100644 --- a/src/main/java/org/p2p/solanaj/programs/BPFLoader.java +++ b/src/main/java/org/p2p/solanaj/programs/BPFLoader.java @@ -7,56 +7,247 @@ import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; +/** + * BPFLoader program instructions. + * + * This class implements the instructions for the BPF Loader program as specified in: + * https://github.com/solana-labs/solana/blob/master/sdk/program/src/loader_upgradeable_instruction.rs + */ public class BPFLoader extends Program { public static final PublicKey PROGRAM_ID = new PublicKey("BPFLoaderUpgradeab1e11111111111111111111111"); + // Hardcoded public keys for system accounts + public static final PublicKey SYSVAR_RENT_PUBKEY = new PublicKey("SysvarRent111111111111111111111111111111111"); + public static final PublicKey SYSVAR_CLOCK_PUBKEY = new PublicKey("SysvarC1ock11111111111111111111111111111111"); + public static final PublicKey SYSTEM_PROGRAM_ID = new PublicKey("11111111111111111111111111111111"); + + /** + * Initialize a Buffer account. + * + * @param newAccount The account to initialize as a buffer. + * @param bufferAuthority The authority of the buffer (optional). + * @return TransactionInstruction for initializing the buffer. + */ public static TransactionInstruction initializeBuffer(final PublicKey newAccount, final PublicKey bufferAuthority) { final List keys = new ArrayList<>(); - keys.add(new AccountMeta(newAccount, false, true)); if (bufferAuthority != null) { keys.add(new AccountMeta(bufferAuthority, false, false)); } - ByteBuffer result = ByteBuffer.allocate(4); - result.order(ByteOrder.LITTLE_ENDIAN); - result.put((byte) 0); + ByteBuffer data = ByteBuffer.allocate(1).order(ByteOrder.LITTLE_ENDIAN); + data.put((byte) 0); // Instruction index for InitializeBuffer - return createTransactionInstruction( - PROGRAM_ID, - keys, - result.array() - ); + return createTransactionInstruction(PROGRAM_ID, keys, data.array()); } + /** + * Write program data into a Buffer account (previous version without offset). + * + * @param writeableBuffer The buffer account to write program data to. + * @param bufferAuthority The authority of the buffer. + * @param data The program data to write. + * @return TransactionInstruction for writing to the buffer. + */ public static TransactionInstruction write(final PublicKey writeableBuffer, final PublicKey bufferAuthority, - final byte[] payload) { - final List keys = new ArrayList<>(); + final byte[] data) { + return write(writeableBuffer, bufferAuthority, 0, data); + } + /** + * Write program data into a Buffer account. + * + * @param writeableBuffer The buffer account to write program data to. + * @param bufferAuthority The authority of the buffer. + * @param offset The offset at which to write the data. + * @param data The program data to write. + * @return TransactionInstruction for writing to the buffer. + */ + public static TransactionInstruction write(final PublicKey writeableBuffer, + final PublicKey bufferAuthority, + final long offset, + final byte[] data) { + final List keys = new ArrayList<>(); keys.add(new AccountMeta(writeableBuffer, false, true)); - if (bufferAuthority != null) { - keys.add(new AccountMeta(bufferAuthority, true, false)); + keys.add(new AccountMeta(bufferAuthority, true, false)); + + ByteBuffer instructionData = ByteBuffer.allocate(5 + data.length).order(ByteOrder.LITTLE_ENDIAN); + instructionData.put((byte) 1); // Instruction index for Write + instructionData.putInt((int) offset); + instructionData.put(data); + + return createTransactionInstruction(PROGRAM_ID, keys, instructionData.array()); + } + + /** + * Deploy an executable program. + * + * @param payer The account paying for the deployment. + * @param programData The program data account. + * @param program The program account. + * @param buffer The buffer containing the program data. + * @param programAuthority The program's authority. + * @param maxDataLen The maximum length of the program data. + * @return TransactionInstruction for deploying the program. + */ + public static TransactionInstruction deployWithMaxDataLen(final PublicKey payer, + final PublicKey programData, + final PublicKey program, + final PublicKey buffer, + final PublicKey programAuthority, + final long maxDataLen) { + final List keys = new ArrayList<>(); + keys.add(new AccountMeta(payer, true, true)); + keys.add(new AccountMeta(programData, false, true)); + keys.add(new AccountMeta(program, false, true)); + keys.add(new AccountMeta(buffer, false, true)); + keys.add(new AccountMeta(SYSVAR_RENT_PUBKEY, false, false)); + keys.add(new AccountMeta(SYSVAR_CLOCK_PUBKEY, false, false)); + keys.add(new AccountMeta(SYSTEM_PROGRAM_ID, false, false)); + keys.add(new AccountMeta(programAuthority, true, false)); + + ByteBuffer data = ByteBuffer.allocate(9).order(ByteOrder.LITTLE_ENDIAN); + data.put((byte) 2); // Instruction index for DeployWithMaxDataLen + data.putLong(maxDataLen); + + return createTransactionInstruction(PROGRAM_ID, keys, data.array()); + } + + /** + * Upgrade a program. + * + * @param programData The program data account. + * @param program The program account. + * @param buffer The buffer containing the updated program data. + * @param spillAccount The account to receive leftover lamports. + * @param programAuthority The program's authority. + * @return TransactionInstruction for upgrading the program. + */ + public static TransactionInstruction upgrade(final PublicKey programData, + final PublicKey program, + final PublicKey buffer, + final PublicKey spillAccount, + final PublicKey programAuthority) { + final List keys = new ArrayList<>(); + keys.add(new AccountMeta(programData, false, true)); + keys.add(new AccountMeta(program, false, true)); + keys.add(new AccountMeta(buffer, false, true)); + keys.add(new AccountMeta(spillAccount, false, true)); + keys.add(new AccountMeta(SYSVAR_RENT_PUBKEY, false, false)); + keys.add(new AccountMeta(SYSVAR_CLOCK_PUBKEY, false, false)); + keys.add(new AccountMeta(programAuthority, true, false)); + + ByteBuffer data = ByteBuffer.allocate(1).order(ByteOrder.LITTLE_ENDIAN); + data.put((byte) 3); // Instruction index for Upgrade + + return createTransactionInstruction(PROGRAM_ID, keys, data.array()); + } + + /** + * Set a new authority for a buffer or program. + * + * @param account The buffer or program data account. + * @param currentAuthority The current authority. + * @param newAuthority The new authority (optional). + * @return TransactionInstruction for setting the authority. + */ + public static TransactionInstruction setAuthority(final PublicKey account, + final PublicKey currentAuthority, + final PublicKey newAuthority) { + final List keys = new ArrayList<>(); + keys.add(new AccountMeta(account, false, true)); + keys.add(new AccountMeta(currentAuthority, true, false)); + if (newAuthority != null) { + keys.add(new AccountMeta(newAuthority, false, false)); + } + + ByteBuffer data = ByteBuffer.allocate(1).order(ByteOrder.LITTLE_ENDIAN); + data.put((byte) 4); // Instruction index for SetAuthority + + return createTransactionInstruction(PROGRAM_ID, keys, data.array()); + } + + /** + * Close a buffer or program account. + * + * @param account The account to close. + * @param recipient The account to receive the lamports. + * @param authority The account's authority (optional). + * @param programAccount The associated program account (optional). + * @return TransactionInstruction for closing the account. + */ + public static TransactionInstruction close(final PublicKey account, + final PublicKey recipient, + final PublicKey authority, + final PublicKey programAccount) { + final List keys = new ArrayList<>(); + keys.add(new AccountMeta(account, false, true)); + keys.add(new AccountMeta(recipient, false, true)); + if (authority != null) { + keys.add(new AccountMeta(authority, true, false)); + } + if (programAccount != null) { + keys.add(new AccountMeta(programAccount, false, true)); } - ByteBuffer result = ByteBuffer.allocate(8 + payload.length); - result.order(ByteOrder.LITTLE_ENDIAN); - result.put((byte) 1); - result.put(4, (byte) 0); // offset 0 always for now - result.put(8, payload); + ByteBuffer data = ByteBuffer.allocate(1).order(ByteOrder.LITTLE_ENDIAN); + data.put((byte) 5); // Instruction index for Close - System.out.println(Arrays.toString(result.array())); + return createTransactionInstruction(PROGRAM_ID, keys, data.array()); + } + + /** + * Extend a program's data length. + * + * @param programData The program data account. + * @param program The program account. + * @param payer The account paying for the extension (optional). + * @param additionalBytes The number of bytes to extend. + * @return TransactionInstruction for extending the program. + */ + public static TransactionInstruction extendProgram(final PublicKey programData, + final PublicKey program, + final PublicKey payer, + final int additionalBytes) { + final List keys = new ArrayList<>(); + keys.add(new AccountMeta(programData, false, true)); + keys.add(new AccountMeta(program, false, true)); + keys.add(new AccountMeta(SYSTEM_PROGRAM_ID, false, false)); + if (payer != null) { + keys.add(new AccountMeta(payer, true, true)); + } + + ByteBuffer data = ByteBuffer.allocate(5).order(ByteOrder.LITTLE_ENDIAN); + data.put((byte) 6); // Instruction index for ExtendProgram + data.putInt(additionalBytes); + + return createTransactionInstruction(PROGRAM_ID, keys, data.array()); + } + + /** + * Set a new authority for a buffer or program with authority check. + * + * @param account The buffer or program data account. + * @param currentAuthority The current authority. + * @param newAuthority The new authority. + * @return TransactionInstruction for setting the authority with a check. + */ + public static TransactionInstruction setAuthorityChecked(final PublicKey account, + final PublicKey currentAuthority, + final PublicKey newAuthority) { + final List keys = new ArrayList<>(); + keys.add(new AccountMeta(account, false, true)); + keys.add(new AccountMeta(currentAuthority, true, false)); + keys.add(new AccountMeta(newAuthority, true, false)); + ByteBuffer data = ByteBuffer.allocate(1).order(ByteOrder.LITTLE_ENDIAN); + data.put((byte) 7); // Instruction index for SetAuthorityChecked - return createTransactionInstruction( - PROGRAM_ID, - keys, - result.array() - ); + return createTransactionInstruction(PROGRAM_ID, keys, data.array()); } } diff --git a/src/main/java/org/p2p/solanaj/programs/ComputeBudgetProgram.java b/src/main/java/org/p2p/solanaj/programs/ComputeBudgetProgram.java index 90727b0b..2758ca7c 100644 --- a/src/main/java/org/p2p/solanaj/programs/ComputeBudgetProgram.java +++ b/src/main/java/org/p2p/solanaj/programs/ComputeBudgetProgram.java @@ -6,42 +6,72 @@ import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.Collections; +import java.util.ArrayList; +/** + * Factory class for creating ComputeBudget program instructions. + */ public class ComputeBudgetProgram extends Program { - private static final PublicKey PROGRAM_ID = + /** The program ID for the ComputeBudget program */ + public static final PublicKey PROGRAM_ID = PublicKey.valueOf("ComputeBudget111111111111111111111111111111"); + private static final byte REQUEST_HEAP_FRAME = 0x01; + private static final byte SET_COMPUTE_UNIT_LIMIT = 0x02; + private static final byte SET_COMPUTE_UNIT_PRICE = 0x03; + private static final byte SET_LOADED_ACCOUNTS_DATA_SIZE_LIMIT = 0x04; + + /** + * Creates an instruction to set the compute unit price. + * + * @param microLamports The desired price of a compute unit in micro-lamports. + * @return A TransactionInstruction to set the compute unit price. + */ public static TransactionInstruction setComputeUnitPrice(int microLamports) { - byte[] transactionData = encodeSetComputeUnitPriceTransaction( - microLamports - ); - - return createTransactionInstruction( - PROGRAM_ID, - Collections.emptyList(), - transactionData - ); + byte[] transactionData = encodeSetComputeUnitPriceTransaction(microLamports); + return createTransactionInstruction(PROGRAM_ID, Collections.emptyList(), transactionData); } + /** + * Creates an instruction to set the compute unit limit. + * + * @param units The desired maximum number of compute units. + * @return A TransactionInstruction to set the compute unit limit. + */ public static TransactionInstruction setComputeUnitLimit(int units) { - byte[] transactionData = encodeSetComputeUnitLimitTransaction( - units - ); - - return createTransactionInstruction( - PROGRAM_ID, - Collections.emptyList(), - transactionData - ); + byte[] transactionData = encodeSetComputeUnitLimitTransaction(units); + return createTransactionInstruction(PROGRAM_ID, Collections.emptyList(), transactionData); + } + + /** + * Creates an instruction to set the loaded accounts data size limit. + * + * @param bytes The desired maximum loaded accounts data size in bytes. + * @return A TransactionInstruction to set the loaded accounts data size limit. + */ + public static TransactionInstruction setLoadedAccountsDataSizeLimit(int bytes) { + byte[] transactionData = encodeSetLoadedAccountsDataSizeLimitTransaction(bytes); + return createTransactionInstruction(PROGRAM_ID, Collections.emptyList(), transactionData); + } + + /** + * Creates an instruction to request a heap frame. + * + * @param bytes The desired heap frame size in bytes. + * @return A TransactionInstruction to request a heap frame. + */ + public static TransactionInstruction requestHeapFrame(int bytes) { + byte[] data = new byte[]{REQUEST_HEAP_FRAME, (byte) (bytes & 0xFF), (byte) ((bytes >> 8) & 0xFF), (byte) ((bytes >> 16) & 0xFF), (byte) ((bytes >> 24) & 0xFF)}; + return new TransactionInstruction(PROGRAM_ID, new ArrayList<>(), data); } private static byte[] encodeSetComputeUnitPriceTransaction(int microLamports) { ByteBuffer result = ByteBuffer.allocate(9); result.order(ByteOrder.LITTLE_ENDIAN); - result.put(0, (byte) 0x03); - result.putLong(1, microLamports); + result.put(SET_COMPUTE_UNIT_PRICE); + result.putLong(microLamports); return result.array(); } @@ -50,10 +80,18 @@ private static byte[] encodeSetComputeUnitLimitTransaction(int units) { ByteBuffer result = ByteBuffer.allocate(5); result.order(ByteOrder.LITTLE_ENDIAN); - result.put(0, (byte) 0x02); - result.putInt(1, units); + result.put(SET_COMPUTE_UNIT_LIMIT); + result.putInt(units); return result.array(); } + private static byte[] encodeSetLoadedAccountsDataSizeLimitTransaction(int bytes) { + ByteBuffer result = ByteBuffer.allocate(5); + result.order(ByteOrder.LITTLE_ENDIAN); + result.put(SET_LOADED_ACCOUNTS_DATA_SIZE_LIMIT); + result.putInt(bytes); + return result.array(); + } + } diff --git a/src/main/java/org/p2p/solanaj/programs/MemoProgram.java b/src/main/java/org/p2p/solanaj/programs/MemoProgram.java index be3559fc..db72958a 100644 --- a/src/main/java/org/p2p/solanaj/programs/MemoProgram.java +++ b/src/main/java/org/p2p/solanaj/programs/MemoProgram.java @@ -21,25 +21,24 @@ public class MemoProgram extends Program { * @param account signer pubkey * @param memo utf-8 string to be written into Solana transaction * @return {@link TransactionInstruction} object with memo instruction + * @throws IllegalArgumentException if the account is null or the memo is null or empty */ public static TransactionInstruction writeUtf8(PublicKey account, String memo) { - // Add signer to AccountMeta keys - final List keys = Collections.singletonList( - new AccountMeta( - account, - true, - false - ) - - ); - + if (account == null) { + throw new IllegalArgumentException("Account cannot be null"); + } + if (memo == null || memo.isEmpty()) { + throw new IllegalArgumentException("Memo cannot be null or empty"); + } + // Convert memo string to UTF-8 byte array final byte[] memoBytes = memo.getBytes(StandardCharsets.UTF_8); - return createTransactionInstruction( - PROGRAM_ID, - keys, - memoBytes + // Add signer to AccountMeta keys + final List keys = Collections.singletonList( + new AccountMeta(account, true, false) ); + + return createTransactionInstruction(PROGRAM_ID, keys, memoBytes); } } diff --git a/src/main/java/org/p2p/solanaj/programs/SystemProgram.java b/src/main/java/org/p2p/solanaj/programs/SystemProgram.java index a128c606..177321a1 100644 --- a/src/main/java/org/p2p/solanaj/programs/SystemProgram.java +++ b/src/main/java/org/p2p/solanaj/programs/SystemProgram.java @@ -1,6 +1,7 @@ package org.p2p.solanaj.programs; -import java.util.ArrayList; +import java.util.List; +import java.util.Arrays; import org.p2p.solanaj.core.PublicKey; import org.p2p.solanaj.core.TransactionInstruction; @@ -8,48 +9,97 @@ import static org.bitcoinj.core.Utils.*; -public class SystemProgram extends Program { +/** + * Represents the System Program on the Solana blockchain. + * This class provides methods to create various system instructions. + */ +public class SystemProgram { + /** The program ID for the System Program */ public static final PublicKey PROGRAM_ID = new PublicKey("11111111111111111111111111111111"); - public static final int PROGRAM_INDEX_CREATE_ACCOUNT = 0; - public static final int PROGRAM_INDEX_TRANSFER = 2; + private static final int PROGRAM_INDEX_CREATE_ACCOUNT = 0; + private static final int PROGRAM_INDEX_ASSIGN = 1; + private static final int PROGRAM_INDEX_TRANSFER = 2; - public static TransactionInstruction transfer(PublicKey fromPublicKey, PublicKey toPublickKey, long lamports) { - ArrayList keys = new ArrayList<>(); - keys.add(new AccountMeta(fromPublicKey, true, true)); - keys.add(new AccountMeta(toPublickKey, false, true)); + private static final int UINT32_SIZE = 4; + private static final int INT64_SIZE = 8; + private static final int PUBKEY_SIZE = 32; - // 4 byte instruction index + 8 bytes lamports - byte[] data = new byte[4 + 8]; + private SystemProgram() { + // Private constructor to prevent instantiation + } + + /** + * Creates a transfer instruction. + * + * @param fromPublicKey The public key of the account to transfer from + * @param toPublicKey The public key of the account to transfer to + * @param lamports The number of lamports to transfer + * @return A TransactionInstruction object representing the transfer instruction + * @throws IllegalArgumentException if lamports is negative + */ + public static TransactionInstruction transfer(PublicKey fromPublicKey, PublicKey toPublicKey, long lamports) { + if (lamports < 0) { + throw new IllegalArgumentException("Lamports must be non-negative"); + } + + List keys = Arrays.asList( + new AccountMeta(fromPublicKey, true, true), + new AccountMeta(toPublicKey, false, true) + ); + + byte[] data = new byte[UINT32_SIZE + INT64_SIZE]; // 4 bytes for program index, 8 bytes for lamports uint32ToByteArrayLE(PROGRAM_INDEX_TRANSFER, data, 0); - int64ToByteArrayLE(lamports, data, 4); + int64ToByteArrayLE(lamports, data, UINT32_SIZE); - return createTransactionInstruction(PROGRAM_ID, keys, data); + return new TransactionInstruction(PROGRAM_ID, keys, data); } - public static TransactionInstruction createAccount(PublicKey fromPublicKey, PublicKey newAccountPublikkey, + /** + * Creates an instruction to create a new account. + * + * @param fromPublicKey The public key of the account funding the new account + * @param newAccountPublicKey The public key of the new account to be created + * @param lamports The number of lamports to transfer to the new account + * @param space The amount of space in bytes to allocate to the new account + * @param programId The program id to assign as the owner of the new account + * @return A TransactionInstruction object representing the create account instruction + * @throws IllegalArgumentException if lamports or space is negative + */ + public static TransactionInstruction createAccount(PublicKey fromPublicKey, PublicKey newAccountPublicKey, long lamports, long space, PublicKey programId) { - ArrayList keys = new ArrayList<>(); - keys.add(new AccountMeta(fromPublicKey, true, true)); - keys.add(new AccountMeta(newAccountPublikkey, true, true)); + if (lamports < 0 || space < 0) { + throw new IllegalArgumentException("Lamports and space must be non-negative"); + } + + List keys = Arrays.asList( + new AccountMeta(fromPublicKey, true, true), + new AccountMeta(newAccountPublicKey, true, true) + ); - byte[] data = new byte[4 + 8 + 8 + 32]; + byte[] data = new byte[UINT32_SIZE + INT64_SIZE + INT64_SIZE + PUBKEY_SIZE]; // 4 + 8 + 8 + 32 = 52 bytes uint32ToByteArrayLE(PROGRAM_INDEX_CREATE_ACCOUNT, data, 0); - int64ToByteArrayLE(lamports, data, 4); - int64ToByteArrayLE(space, data, 12); - System.arraycopy(programId.toByteArray(), 0, data, 20, 32); + int64ToByteArrayLE(lamports, data, UINT32_SIZE); + int64ToByteArrayLE(space, data, UINT32_SIZE + INT64_SIZE); + System.arraycopy(programId.toByteArray(), 0, data, UINT32_SIZE + INT64_SIZE + INT64_SIZE, PUBKEY_SIZE); - return createTransactionInstruction(PROGRAM_ID, keys, data); + return new TransactionInstruction(PROGRAM_ID, keys, data); } + /** + * Creates an instruction to assign a new owner to an account. + * + * @param owner The current owner of the account + * @param newOwner The new owner to assign to the account + * @return A TransactionInstruction object representing the assign instruction + */ public static TransactionInstruction assign(PublicKey owner, PublicKey newOwner) { - ArrayList keys = new ArrayList<>(); - keys.add(new AccountMeta(owner, true, true)); + List keys = List.of(new AccountMeta(owner, true, true)); - byte[] data = new byte[4 + 32]; - uint32ToByteArrayLE(1, data, 0); - System.arraycopy(newOwner.toByteArray(), 0, data, 4, 32); + byte[] data = new byte[UINT32_SIZE + PUBKEY_SIZE]; // 4 + 32 = 36 bytes + uint32ToByteArrayLE(PROGRAM_INDEX_ASSIGN, data, 0); + System.arraycopy(newOwner.toByteArray(), 0, data, UINT32_SIZE, PUBKEY_SIZE); - return createTransactionInstruction(PROGRAM_ID, keys, data); + return new TransactionInstruction(PROGRAM_ID, keys, data); } } diff --git a/src/main/java/org/p2p/solanaj/programs/TokenProgram.java b/src/main/java/org/p2p/solanaj/programs/TokenProgram.java index 8e66a25f..a80fca32 100644 --- a/src/main/java/org/p2p/solanaj/programs/TokenProgram.java +++ b/src/main/java/org/p2p/solanaj/programs/TokenProgram.java @@ -15,11 +15,25 @@ public class TokenProgram extends Program { public static final PublicKey PROGRAM_ID = new PublicKey("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"); - private static final PublicKey SYSVAR_RENT_PUBKEY = new PublicKey("SysvarRent111111111111111111111111111111111"); - private static final int INITIALIZE_METHOD_ID = 1; + /** + * The public key of the Solana rent sysvar. + * This sysvar provides information about the rent exempt minimum balance. + */ + public static final PublicKey SYSVAR_RENT_PUBKEY = new PublicKey("SysvarRent111111111111111111111111111111111"); + + private static final int INITIALIZE_MINT_METHOD_ID = 0; + private static final int INITIALIZE_ACCOUNT_METHOD_ID = 1; + private static final int INITIALIZE_MULTISIG_METHOD_ID = 2; private static final int TRANSFER_METHOD_ID = 3; + private static final int APPROVE_METHOD_ID = 4; + private static final int REVOKE_METHOD_ID = 5; + private static final int SET_AUTHORITY_METHOD_ID = 6; + private static final int MINT_TO_METHOD_ID = 7; + private static final int BURN_METHOD_ID = 8; private static final int CLOSE_ACCOUNT_METHOD_ID = 9; + private static final int FREEZE_ACCOUNT_METHOD_ID = 10; + private static final int THAW_ACCOUNT_METHOD_ID = 11; private static final int TRANSFER_CHECKED_METHOD_ID = 12; /** @@ -49,6 +63,17 @@ public static TransactionInstruction transfer(PublicKey source, PublicKey destin ); } + /** + * Creates a transaction instruction for a checked token transfer. + * + * @param source The public key of the source account + * @param destination The public key of the destination account + * @param amount The amount of tokens to transfer + * @param decimals The number of decimals in the token + * @param owner The public key of the source account owner + * @param tokenMint The public key of the token's mint + * @return A TransactionInstruction for the transfer + */ public static TransactionInstruction transferChecked(PublicKey source, PublicKey destination, long amount, byte decimals, PublicKey owner, PublicKey tokenMint) { final List keys = new ArrayList<>(); @@ -80,7 +105,7 @@ public static TransactionInstruction initializeAccount(final PublicKey account, ByteBuffer buffer = ByteBuffer.allocate(1); buffer.order(ByteOrder.LITTLE_ENDIAN); - buffer.put((byte) INITIALIZE_METHOD_ID); + buffer.put((byte) INITIALIZE_ACCOUNT_METHOD_ID); return createTransactionInstruction( PROGRAM_ID, @@ -89,12 +114,12 @@ public static TransactionInstruction initializeAccount(final PublicKey account, ); } - public static TransactionInstruction closeAccount(final PublicKey source, final PublicKey destination, final PublicKey owner) { + public static TransactionInstruction closeAccount(final PublicKey accountPubkey, final PublicKey destinationPubkey, final PublicKey ownerPubkey) { final List keys = new ArrayList<>(); - keys.add(new AccountMeta(source,false, true)); - keys.add(new AccountMeta(destination, false, true)); - keys.add(new AccountMeta(owner,true, false)); + keys.add(new AccountMeta(accountPubkey, false, true)); + keys.add(new AccountMeta(destinationPubkey, false, true)); + keys.add(new AccountMeta(ownerPubkey, true, false)); ByteBuffer buffer = ByteBuffer.allocate(1); buffer.order(ByteOrder.LITTLE_ENDIAN); @@ -107,6 +132,12 @@ public static TransactionInstruction closeAccount(final PublicKey source, final ); } + /** + * Encodes the transfer token instruction data. + * + * @param amount The amount of tokens to transfer + * @return A byte array containing the encoded instruction data + */ private static byte[] encodeTransferTokenInstructionData(long amount) { ByteBuffer result = ByteBuffer.allocate(9); result.order(ByteOrder.LITTLE_ENDIAN); @@ -117,6 +148,13 @@ private static byte[] encodeTransferTokenInstructionData(long amount) { return result.array(); } + /** + * Encodes the transfer checked token instruction data. + * + * @param amount The amount of tokens to transfer + * @param decimals The number of decimals in the token + * @return A byte array containing the encoded instruction data + */ private static byte[] encodeTransferCheckedTokenInstructionData(long amount, byte decimals) { ByteBuffer result = ByteBuffer.allocate(10); result.order(ByteOrder.LITTLE_ENDIAN); @@ -127,4 +165,268 @@ private static byte[] encodeTransferCheckedTokenInstructionData(long amount, byt return result.array(); } + + /** + * Creates an instruction to initialize a new mint. + * + * @param mintPubkey The public key of the mint to initialize + * @param decimals Number of base 10 digits to the right of the decimal place + * @param mintAuthority The authority/multisignature to mint tokens + * @param freezeAuthority The freeze authority/multisignature of the mint (optional) + * @return TransactionInstruction to initialize the mint + */ + public static TransactionInstruction initializeMint( + PublicKey mintPubkey, + int decimals, + PublicKey mintAuthority, + PublicKey freezeAuthority + ) { + List keys = new ArrayList<>(); + keys.add(new AccountMeta(mintPubkey, false, true)); + keys.add(new AccountMeta(SYSVAR_RENT_PUBKEY, false, false)); + + ByteBuffer buffer = ByteBuffer.allocate(1 + 1 + 32 + 1 + (freezeAuthority != null ? 32 : 0)); + buffer.order(ByteOrder.LITTLE_ENDIAN); + buffer.put((byte) INITIALIZE_MINT_METHOD_ID); + buffer.put((byte) decimals); + buffer.put(mintAuthority.toByteArray()); + buffer.put((byte) (freezeAuthority != null ? 1 : 0)); + if (freezeAuthority != null) { + buffer.put(freezeAuthority.toByteArray()); + } + + return createTransactionInstruction(PROGRAM_ID, keys, buffer.array()); + } + + /** + * Creates an instruction to initialize a multisig account. + * + * @param multisigPubkey The public key of the multisig account to initialize + * @param signerPubkeys The public keys of the signers + * @param m The number of required signatures + * @return TransactionInstruction to initialize the multisig account + */ + public static TransactionInstruction initializeMultisig( + PublicKey multisigPubkey, + List signerPubkeys, + int m + ) { + List keys = new ArrayList<>(); + keys.add(new AccountMeta(multisigPubkey, false, true)); + keys.add(new AccountMeta(SYSVAR_RENT_PUBKEY, false, false)); + for (PublicKey signerPubkey : signerPubkeys) { + keys.add(new AccountMeta(signerPubkey, false, false)); + } + + ByteBuffer buffer = ByteBuffer.allocate(2); + buffer.order(ByteOrder.LITTLE_ENDIAN); + buffer.put((byte) INITIALIZE_MULTISIG_METHOD_ID); + buffer.put((byte) m); + + return createTransactionInstruction(PROGRAM_ID, keys, buffer.array()); + } + + /** + * Creates an instruction to approve a delegate. + * + * @param sourcePubkey The public key of the source account + * @param delegatePubkey The public key of the delegate + * @param ownerPubkey The public key of the source account owner + * @param amount The amount of tokens to approve + * @return TransactionInstruction to approve the delegate + */ + public static TransactionInstruction approve( + PublicKey sourcePubkey, + PublicKey delegatePubkey, + PublicKey ownerPubkey, + long amount + ) { + List keys = new ArrayList<>(); + keys.add(new AccountMeta(sourcePubkey, false, true)); + keys.add(new AccountMeta(delegatePubkey, false, false)); + keys.add(new AccountMeta(ownerPubkey, true, false)); + + ByteBuffer buffer = ByteBuffer.allocate(9); + buffer.order(ByteOrder.LITTLE_ENDIAN); + buffer.put((byte) APPROVE_METHOD_ID); + buffer.putLong(amount); + + return createTransactionInstruction(PROGRAM_ID, keys, buffer.array()); + } + + /** + * Creates an instruction to revoke a delegate's authority. + * + * @param accountPubkey The public key of the token account + * @param ownerPubkey The public key of the token account owner + * @return TransactionInstruction to revoke the delegate's authority + */ + public static TransactionInstruction revoke( + PublicKey accountPubkey, + PublicKey ownerPubkey + ) { + List keys = new ArrayList<>(); + keys.add(new AccountMeta(accountPubkey, false, true)); + keys.add(new AccountMeta(ownerPubkey, true, false)); + + ByteBuffer buffer = ByteBuffer.allocate(1); + buffer.put((byte) REVOKE_METHOD_ID); + + return createTransactionInstruction(PROGRAM_ID, keys, buffer.array()); + } + + /** + * Creates an instruction to set a new authority of a mint or account. + * + * @param accountPubkey The public key of the mint or account + * @param currentAuthorityPubkey The current authority of the mint or account + * @param newAuthorityPubkey The new authority to set (optional) + * @param authorityType The type of authority to set + * @return TransactionInstruction to set the authority + */ + public static TransactionInstruction setAuthority( + PublicKey accountPubkey, + PublicKey currentAuthorityPubkey, + PublicKey newAuthorityPubkey, + AuthorityType authorityType + ) { + List keys = new ArrayList<>(); + keys.add(new AccountMeta(accountPubkey, false, true)); + keys.add(new AccountMeta(currentAuthorityPubkey, true, false)); + + ByteBuffer buffer = ByteBuffer.allocate(1 + 1 + 1 + (newAuthorityPubkey != null ? 32 : 0)); + buffer.order(ByteOrder.LITTLE_ENDIAN); + buffer.put((byte) SET_AUTHORITY_METHOD_ID); + buffer.put((byte) authorityType.getValue()); + buffer.put((byte) (newAuthorityPubkey != null ? 1 : 0)); + if (newAuthorityPubkey != null) { + buffer.put(newAuthorityPubkey.toByteArray()); + } + + return createTransactionInstruction(PROGRAM_ID, keys, buffer.array()); + } + + /** + * Creates an instruction to mint new tokens. + * + * @param mintPubkey The public key of the mint + * @param destinationPubkey The public key of the account to mint tokens to + * @param authorityPubkey The public key of the minting authority + * @param amount The amount of new tokens to mint + * @return TransactionInstruction to mint tokens + */ + public static TransactionInstruction mintTo( + PublicKey mintPubkey, + PublicKey destinationPubkey, + PublicKey authorityPubkey, + long amount + ) { + List keys = new ArrayList<>(); + keys.add(new AccountMeta(mintPubkey, false, true)); + keys.add(new AccountMeta(destinationPubkey, false, true)); + keys.add(new AccountMeta(authorityPubkey, true, false)); + + ByteBuffer buffer = ByteBuffer.allocate(9); + buffer.order(ByteOrder.LITTLE_ENDIAN); + buffer.put((byte) MINT_TO_METHOD_ID); + buffer.putLong(amount); + + return createTransactionInstruction(PROGRAM_ID, keys, buffer.array()); + } + + /** + * Creates an instruction to burn tokens. + * + * @param accountPubkey The public key of the account to burn tokens from + * @param mintPubkey The public key of the mint + * @param ownerPubkey The public key of the token account owner + * @param amount The amount of tokens to burn + * @return TransactionInstruction to burn tokens + */ + public static TransactionInstruction burn( + PublicKey accountPubkey, + PublicKey mintPubkey, + PublicKey ownerPubkey, + long amount + ) { + List keys = new ArrayList<>(); + keys.add(new AccountMeta(accountPubkey, false, true)); + keys.add(new AccountMeta(mintPubkey, false, true)); + keys.add(new AccountMeta(ownerPubkey, true, false)); + + ByteBuffer buffer = ByteBuffer.allocate(9); + buffer.order(ByteOrder.LITTLE_ENDIAN); + buffer.put((byte) BURN_METHOD_ID); + buffer.putLong(amount); + + return createTransactionInstruction(PROGRAM_ID, keys, buffer.array()); + } + + /** + * Creates an instruction to freeze an account. + * + * @param accountPubkey The public key of the account to freeze + * @param mintPubkey The public key of the mint + * @param authorityPubkey The public key of the freeze authority + * @return TransactionInstruction to freeze the account + */ + public static TransactionInstruction freezeAccount( + PublicKey accountPubkey, + PublicKey mintPubkey, + PublicKey authorityPubkey + ) { + List keys = new ArrayList<>(); + keys.add(new AccountMeta(accountPubkey, false, true)); + keys.add(new AccountMeta(mintPubkey, false, false)); + keys.add(new AccountMeta(authorityPubkey, true, false)); + + ByteBuffer buffer = ByteBuffer.allocate(1); + buffer.put((byte) FREEZE_ACCOUNT_METHOD_ID); + + return createTransactionInstruction(PROGRAM_ID, keys, buffer.array()); + } + + /** + * Creates an instruction to thaw a frozen account. + * + * @param accountPubkey The public key of the account to thaw + * @param mintPubkey The public key of the mint + * @param authorityPubkey The public key of the freeze authority + * @return TransactionInstruction to thaw the account + */ + public static TransactionInstruction thawAccount( + PublicKey accountPubkey, + PublicKey mintPubkey, + PublicKey authorityPubkey + ) { + List keys = new ArrayList<>(); + keys.add(new AccountMeta(accountPubkey, false, true)); + keys.add(new AccountMeta(mintPubkey, false, false)); + keys.add(new AccountMeta(authorityPubkey, true, false)); + + ByteBuffer buffer = ByteBuffer.allocate(1); + buffer.put((byte) THAW_ACCOUNT_METHOD_ID); + + return createTransactionInstruction(PROGRAM_ID, keys, buffer.array()); + } + + /** + * Enum representing the types of authorities that can be set for a mint or account. + */ + public enum AuthorityType { + MINT_TOKENS(0), + FREEZE_ACCOUNT(1), + ACCOUNT_OWNER(2), + CLOSE_ACCOUNT(3); + + private final int value; + + AuthorityType(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + } } diff --git a/src/main/java/org/p2p/solanaj/rpc/RpcApi.java b/src/main/java/org/p2p/solanaj/rpc/RpcApi.java index 1d7c13fe..b410dda6 100644 --- a/src/main/java/org/p2p/solanaj/rpc/RpcApi.java +++ b/src/main/java/org/p2p/solanaj/rpc/RpcApi.java @@ -1,32 +1,21 @@ package org.p2p.solanaj.rpc; -import java.util.*; -import java.util.stream.Collectors; import org.p2p.solanaj.core.Account; import org.p2p.solanaj.core.LegacyTransaction; import org.p2p.solanaj.core.PublicKey; import org.p2p.solanaj.core.Transaction; import org.p2p.solanaj.rpc.types.*; -import org.p2p.solanaj.rpc.types.config.BlockConfig; -import org.p2p.solanaj.rpc.types.ConfirmedSignFAddr2; -import org.p2p.solanaj.rpc.types.DataSize; -import org.p2p.solanaj.rpc.types.Filter; -import org.p2p.solanaj.rpc.types.Memcmp; -import org.p2p.solanaj.rpc.types.config.LargestAccountConfig; -import org.p2p.solanaj.rpc.types.config.LeaderScheduleConfig; -import org.p2p.solanaj.rpc.types.config.ProgramAccountConfig; -import org.p2p.solanaj.rpc.types.config.RpcEpochConfig; import org.p2p.solanaj.rpc.types.RpcResultTypes.ValueLong; -import org.p2p.solanaj.rpc.types.config.RpcSendTransactionConfig; +import org.p2p.solanaj.rpc.types.TokenResultObjects.TokenAccount; +import org.p2p.solanaj.rpc.types.TokenResultObjects.TokenAmountInfo; +import org.p2p.solanaj.rpc.types.config.*; import org.p2p.solanaj.rpc.types.config.RpcSendTransactionConfig.Encoding; -import org.p2p.solanaj.rpc.types.config.SignatureStatusConfig; -import org.p2p.solanaj.rpc.types.config.SimulateTransactionConfig; -import org.p2p.solanaj.rpc.types.TokenResultObjects.*; -import org.p2p.solanaj.rpc.types.config.Commitment; -import org.p2p.solanaj.rpc.types.config.VoteAccountConfig; import org.p2p.solanaj.ws.SubscriptionWebSocketClient; import org.p2p.solanaj.ws.listeners.NotificationEventListener; +import java.util.*; +import java.util.stream.Collectors; + public class RpcApi { private final RpcClient client; @@ -34,28 +23,30 @@ public RpcApi(RpcClient client) { this.client = client; } - public String getLatestBlockhash() throws RpcException { + public LatestBlockhash getLatestBlockhash() throws RpcException { return getLatestBlockhash(null); } - public String getLatestBlockhash(Commitment commitment) throws RpcException { + public LatestBlockhash getLatestBlockhash(Commitment commitment) throws RpcException { List params = new ArrayList<>(); - if (null != commitment) { + if (commitment != null) { params.add(Map.of("commitment", commitment.getValue())); } - return client.call("getLatestBlockhash", params, RecentBlockhash.class).getValue().getBlockhash(); + return client.call("getLatestBlockhash", params, LatestBlockhash.class); } + @Deprecated public String getRecentBlockhash() throws RpcException { return getRecentBlockhash(null); } + @Deprecated public String getRecentBlockhash(Commitment commitment) throws RpcException { List params = new ArrayList<>(); - if (null != commitment) { + if (commitment != null) { params.add(Map.of("commitment", commitment.getValue())); } @@ -148,7 +139,7 @@ public String sendTransaction(Transaction transaction, List signers, St RpcSendTransactionConfig rpcSendTransactionConfig) throws RpcException { if (recentBlockHash == null) { - recentBlockHash = getRecentBlockhash(); + recentBlockHash = getLatestBlockhash().getValue().getBlockhash(); } transaction.setRecentBlockHash(recentBlockHash); transaction.sign(signers); @@ -195,7 +186,7 @@ public long getBalance(PublicKey account, Commitment commitment) throws RpcExcep List params = new ArrayList<>(); params.add(account.toString()); - if (null != commitment) { + if (commitment != null) { params.add(Map.of("commitment", commitment.getValue())); } @@ -255,28 +246,6 @@ public List getSignaturesForAddress(PublicKey account, int return result; } - public List getSignaturesForAddress(PublicKey account, String beforeSignature, int limit, - Commitment commitment) - throws RpcException { - if (beforeSignature == null) { - return getSignaturesForAddress(account, limit, commitment); - } - - List params = new ArrayList<>(); - - params.add(account.toString()); - params.add(new ConfirmedSignFAddr2(beforeSignature, limit, commitment)); - - List rawResult = client.call("getSignaturesForAddress", params, List.class); - - List result = new ArrayList<>(); - for (AbstractMap item : rawResult) { - result.add(new SignatureInformation(item)); - } - - return result; - } - public List getProgramAccounts(PublicKey account, long offset, String bytes) throws RpcException { List filters = new ArrayList<>(); filters.add(new Filter(new Memcmp(offset, bytes))); @@ -357,6 +326,7 @@ public List getProgramAccounts(PublicKey account, List m memcmpList.forEach(memcmp -> filters.add(new Filter(memcmp))); ProgramAccountConfig programAccountConfig = new ProgramAccountConfig(filters); + programAccountConfig.setEncoding(Encoding.base64); params.add(programAccountConfig); List rawResult = client.call("getProgramAccounts", params, List.class); @@ -417,7 +387,7 @@ public long getMinimumBalanceForRentExemption(long dataLength, Commitment commit List params = new ArrayList<>(); params.add(dataLength); - if (null != commitment) { + if (commitment != null) { params.add(Map.of("commitment", commitment.getValue())); } @@ -445,7 +415,7 @@ public long getBlockHeight() throws RpcException { public long getBlockHeight(Commitment commitment) throws RpcException { List params = new ArrayList<>(); - if (null != commitment) { + if (commitment != null) { params.add(Map.of("commitment", commitment.getValue())); } return client.call("getBlockHeight", params, Long.class); @@ -486,7 +456,7 @@ public String requestAirdrop(PublicKey address, long lamports, Commitment commit params.add(address.toString()); params.add(lamports); - if (null != commitment) { + if (commitment != null) { params.add(Map.of("commitment", commitment.getValue())); } @@ -501,48 +471,139 @@ public BlockCommitment getBlockCommitment(long block) throws RpcException { return client.call("getBlockCommitment", params, BlockCommitment.class); } + @Deprecated public FeeCalculatorInfo getFeeCalculatorForBlockhash(String blockhash) throws RpcException { return getFeeCalculatorForBlockhash(blockhash, null); } + @Deprecated public FeeCalculatorInfo getFeeCalculatorForBlockhash(String blockhash, Commitment commitment) throws RpcException { List params = new ArrayList<>(); params.add(blockhash); - if (null != commitment) { + if (commitment != null) { params.add(Map.of("commitment", commitment.getValue())); } return client.call("getFeeCalculatorForBlockhash", params, FeeCalculatorInfo.class); } + @Deprecated public FeeRateGovernorInfo getFeeRateGovernor() throws RpcException { return client.call("getFeeRateGovernor", new ArrayList<>(), FeeRateGovernorInfo.class); } + /** + * Gets the fee the network will charge for a particular message + * + * @param message Base-64 encoded Message + * @return Fee for the message + * @throws RpcException if the RPC call fails + */ public Long getFeeForMessage(String message) throws RpcException { return getFeeForMessage(message, null); } + /** + * Gets the fee the network will charge for a particular message + * + * @param message Base-64 encoded Message + * @param commitment Optional commitment level + * @return Fee for the message + * @throws RpcException if the RPC call fails + */ public Long getFeeForMessage(String message, Commitment commitment) throws RpcException { List params = new ArrayList<>(); params.add(message); - if (null != commitment) { - params.add(Map.of("commitment", commitment.getValue())); + Map configMap = new HashMap<>(); + if (commitment != null) { + configMap.put("commitment", commitment.getValue()); } + params.add(configMap); - return client.call("getFeeForMessage", params, ValueLong.class).getValue(); + Long feeValue = client.call("getFeeForMessage", params, ValueLong.class).getValue(); + + if (feeValue == null) { + return 0L; + } else { + return feeValue; + } } + /** + * Gets a list of prioritization fees from recent blocks + * + * @return List of RecentPrioritizationFees + * @throws RpcException if the RPC call fails + */ + public List getRecentPrioritizationFees() throws RpcException { + return getRecentPrioritizationFees(null); + } + + /** + * Gets a list of prioritization fees from recent blocks + * + * @param addresses Optional list of PublicKey addresses to filter by + * @return List of RecentPrioritizationFees + * @throws RpcException if the RPC call fails + */ + public List getRecentPrioritizationFees(List addresses) throws RpcException { + List params = new ArrayList<>(); + + if (addresses != null) { + params.add(addresses.stream().map(PublicKey::toBase58).toList()); + } + + List> rawResult = client.call("getRecentPrioritizationFees", params, List.class); + + List result = new ArrayList<>(); + for (Map item : rawResult) { + result.add(new RecentPrioritizationFees(item)); + } + + return result; + } + + /** + * Gets the current stake minimum delegation + * + * @return Stake minimum delegation in lamports + * @throws RpcException if the RPC call fails + */ + public Long getStakeMinimumDelegation() throws RpcException { + return getStakeMinimumDelegation(null); + } + + /** + * Gets the current stake minimum delegation + * + * @param commitment Optional commitment level + * @return Stake minimum delegation in lamports + * @throws RpcException if the RPC call fails + */ + public Long getStakeMinimumDelegation(Commitment commitment) throws RpcException { + List params = new ArrayList<>(); + + if (commitment != null) { + Map configMap = new HashMap<>(); + configMap.put("commitment", commitment.getValue()); + params.add(configMap); + } + + return client.call("getStakeMinimumDelegation", params, ValueLong.class).getValue(); + } + + @Deprecated public FeesInfo getFees() throws RpcException { return getFees(null); } + @Deprecated public FeesInfo getFees(Commitment commitment) throws RpcException { List params = new ArrayList<>(); - if (null != commitment) { + if (commitment != null) { params.add(Map.of("commitment", commitment.getValue())); } @@ -556,7 +617,7 @@ public long getTransactionCount() throws RpcException { public long getTransactionCount(Commitment commitment) throws RpcException { List params = new ArrayList<>(); - if (null != commitment) { + if (commitment != null) { params.add(Map.of("commitment", commitment.getValue())); } @@ -632,7 +693,7 @@ public Block getBlock(int slot, Map optionalParams) throws RpcEx params.add(slot); - if (null != optionalParams) { + if (optionalParams != null) { BlockConfig blockConfig = new BlockConfig(); if (optionalParams.containsKey("commitment")) { Commitment commitment = (Commitment) optionalParams.get("commitment"); @@ -671,7 +732,7 @@ public EpochInfo getEpochInfo() throws RpcException { public EpochInfo getEpochInfo(Commitment commitment) throws RpcException { List params = new ArrayList<>(); - if (null != commitment) { + if (commitment != null) { params.add(Map.of("commitment", commitment.getValue())); } @@ -719,7 +780,7 @@ public InflationGovernor getInflationGovernor() throws RpcException { public InflationGovernor getInflationGovernor(Commitment commitment) throws RpcException { List params = new ArrayList<>(); - if (null != commitment) { + if (commitment != null) { params.add(Map.of("commitment", commitment.getValue())); } @@ -737,10 +798,10 @@ public List getInflationReward(List addresses, Long params.add(addresses.stream().map(PublicKey::toString).collect(Collectors.toList())); RpcEpochConfig rpcEpochConfig = new RpcEpochConfig(); - if (null != epoch) { + if (epoch != null) { rpcEpochConfig.setEpoch(epoch); } - if (null != commitment) { + if (commitment != null) { rpcEpochConfig.setCommitment(commitment.getValue()); } params.add(rpcEpochConfig); @@ -764,7 +825,7 @@ public long getSlot() throws RpcException { public long getSlot(Commitment commitment) throws RpcException { List params = new ArrayList<>(); - if (null != commitment) { + if (commitment != null) { params.add(Map.of("commitment", commitment.getValue())); } @@ -778,7 +839,7 @@ public PublicKey getSlotLeader() throws RpcException { public PublicKey getSlotLeader(Commitment commitment) throws RpcException { List params = new ArrayList<>(); - if (null != commitment) { + if (commitment != null) { params.add(Map.of("commitment", commitment.getValue())); } @@ -801,6 +862,7 @@ public List getSlotLeaders(long startSlot, long limit) throws RpcExce return result; } + @Deprecated public long getSnapshotSlot() throws RpcException { return client.call("getSnapshotSlot", new ArrayList<>(), Long.class); } @@ -831,7 +893,7 @@ public Supply getSupply() throws RpcException { public Supply getSupply(Commitment commitment) throws RpcException { List params = new ArrayList<>(); - if (null != commitment) { + if (commitment != null) { params.add(Map.of("commitment", commitment.getValue())); } @@ -873,7 +935,7 @@ public TokenResultObjects.TokenAmountInfo getTokenAccountBalance(PublicKey token List params = new ArrayList<>(); params.add(tokenAccount.toString()); - if (null != commitment) { + if (commitment != null) { params.add(Map.of("commitment", commitment.getValue())); } @@ -982,10 +1044,12 @@ public VoteAccounts getVoteAccounts(PublicKey votePubkey, Commitment commitment) return client.call("getVoteAccounts", params, VoteAccounts.class); } + @Deprecated public StakeActivation getStakeActivation(PublicKey publicKey) throws RpcException { return getStakeActivation(publicKey, null, null); } + @Deprecated public StakeActivation getStakeActivation(PublicKey publicKey, Long epoch, Commitment commitment) throws RpcException { List params = new ArrayList<>(); params.add(publicKey.toBase58()); @@ -1182,4 +1246,72 @@ public boolean isBlockhashValid(String blockHash, Commitment commitment, Long mi return result; } + /** + * Returns a list of confirmed blocks between two slots + * + * @param startSlot Start slot (inclusive) + * @param endSlot End slot (inclusive) + * @return List of block numbers between start_slot and end_slot + * @throws RpcException if the RPC call fails + */ + public List getBlocks(long startSlot, long endSlot) throws RpcException { + return getBlocks(startSlot, endSlot, null); + } + + /** + * Returns a list of confirmed blocks between two slots + * + * @param startSlot Start slot (inclusive) + * @param endSlot End slot (inclusive) + * @param commitment Bank state to query + * @return List of block numbers between start_slot and end_slot + * @throws RpcException if the RPC call fails + */ + public List getBlocks(long startSlot, long endSlot, Commitment commitment) throws RpcException { + List params = new ArrayList<>(); + params.add(startSlot); + params.add(endSlot); + + if (commitment != null) { + params.add(Map.of("commitment", commitment.getValue())); + } + + List result = client.call("getBlocks", params, List.class); + return result.stream().map(Double::longValue).collect(Collectors.toList()); + } + + /** + * Returns a list of confirmed blocks starting at the given slot + * + * @param startSlot Start slot + * @param limit Maximum number of blocks to return + * @return List of block numbers from start_slot to limit + * @throws RpcException if the RPC call fails + */ + public List getBlocksWithLimit(long startSlot, long limit) throws RpcException { + return getBlocksWithLimit(startSlot, limit, null); + } + + /** + * Returns a list of confirmed blocks starting at the given slot + * + * @param startSlot Start slot + * @param limit Maximum number of blocks to return + * @param commitment Bank state to query + * @return List of block numbers from start_slot to limit + * @throws RpcException if the RPC call fails + */ + public List getBlocksWithLimit(long startSlot, long limit, Commitment commitment) throws RpcException { + List params = new ArrayList<>(); + params.add(startSlot); + params.add(limit); + + if (commitment != null) { + params.add(Map.of("commitment", commitment.getValue())); + } + + List result = client.call("getBlocksWithLimit", params, List.class); + return result.stream().map(Double::longValue).collect(Collectors.toList()); + } + } diff --git a/src/main/java/org/p2p/solanaj/rpc/RpcClient.java b/src/main/java/org/p2p/solanaj/rpc/RpcClient.java index 58ca62b0..c01e6bed 100644 --- a/src/main/java/org/p2p/solanaj/rpc/RpcClient.java +++ b/src/main/java/org/p2p/solanaj/rpc/RpcClient.java @@ -7,7 +7,6 @@ import okhttp3.Response; import java.io.IOException; -import java.lang.reflect.Type; import java.util.List; import java.util.concurrent.TimeUnit; @@ -21,53 +20,83 @@ import javax.net.ssl.*; +import java.net.InetSocketAddress; +import java.net.Proxy; + +/** + * RpcClient is responsible for making RPC calls to a Solana cluster. + */ public class RpcClient { private static final MediaType JSON = MediaType.parse("application/json; charset=utf-8"); private String endpoint; private OkHttpClient httpClient; private RpcApi rpcApi; private WeightedCluster cluster; + private final Moshi moshi; // Reuse Moshi instance + /** + * Constructs an RpcClient with a specified weighted cluster. + * + * @param cluster the weighted cluster to use for RPC calls + */ public RpcClient(WeightedCluster cluster) { this.cluster = cluster; + this.endpoint = cluster.getEndpoints().get(0).getUrl(); // Initialize endpoint from the cluster + this.httpClient = new OkHttpClient.Builder().readTimeout(20, TimeUnit.SECONDS).build(); + this.rpcApi = new RpcApi(this); + this.moshi = new Moshi.Builder().build(); // Initialize Moshi } + /** + * Constructs an RpcClient with a specified cluster. + * + * @param endpoint the cluster endpoint + */ public RpcClient(Cluster endpoint) { this(endpoint.getEndpoint()); } + /** + * Constructs an RpcClient with a specified endpoint. + * + * @param endpoint the RPC endpoint + */ public RpcClient(String endpoint) { - this.endpoint = endpoint; - this.httpClient = new OkHttpClient.Builder() - .readTimeout(20, TimeUnit.SECONDS) - //.addInterceptor(new LoggingInterceptor()) - .build(); - rpcApi = new RpcApi(this); + this(endpoint, new OkHttpClient.Builder().readTimeout(20, TimeUnit.SECONDS).build()); } + /** + * Constructs an RpcClient with a specified endpoint and user agent. + * + * @param endpoint the RPC endpoint + * @param userAgent the user agent to set in the request header + */ public RpcClient(String endpoint, String userAgent) { - this.endpoint = endpoint; - this.httpClient = new OkHttpClient.Builder() - .addNetworkInterceptor( - chain -> chain.proceed( - chain.request() - .newBuilder() - .header("User-Agent", userAgent) - .build() - )) + this(endpoint, new OkHttpClient.Builder() + .addNetworkInterceptor(chain -> chain.proceed( + chain.request().newBuilder().header("User-Agent", userAgent).build())) .readTimeout(20, TimeUnit.SECONDS) - .build(); - rpcApi = new RpcApi(this); + .build()); } + /** + * Constructs an RpcClient with a specified endpoint and timeout. + * + * @param endpoint the RPC endpoint + * @param timeout the read timeout in seconds + */ public RpcClient(String endpoint, int timeout) { - this.endpoint = endpoint; - this.httpClient = new OkHttpClient.Builder() - .readTimeout(timeout, TimeUnit.SECONDS) - .build(); - rpcApi = new RpcApi(this); + this(endpoint, new OkHttpClient.Builder().readTimeout(timeout, TimeUnit.SECONDS).build()); } + /** + * Constructs an RpcClient with specified timeouts for read, connect, and write. + * + * @param endpoint the RPC endpoint + * @param readTimeoutMs the read timeout in milliseconds + * @param connectTimeoutMs the connect timeout in milliseconds + * @param writeTimeoutMs the write timeout in milliseconds + */ public RpcClient(String endpoint, int readTimeoutMs, int connectTimeoutMs, int writeTimeoutMs) { this.endpoint = endpoint; this.httpClient = new OkHttpClient.Builder() @@ -75,15 +104,54 @@ public RpcClient(String endpoint, int readTimeoutMs, int connectTimeoutMs, int w .connectTimeout(connectTimeoutMs, TimeUnit.MILLISECONDS) .writeTimeout(writeTimeoutMs, TimeUnit.MILLISECONDS) .build(); - rpcApi = new RpcApi(this); + this.rpcApi = new RpcApi(this); + this.moshi = new Moshi.Builder().build(); // Initialize Moshi + } + + /** + * Constructs an RpcClient with a specified endpoint and OkHttpClient. + * + * @param endpoint the RPC endpoint + * @param httpClient the OkHttpClient to use for requests + */ + public RpcClient(String endpoint, OkHttpClient httpClient) { + this.endpoint = endpoint; + this.httpClient = httpClient; + this.rpcApi = new RpcApi(this); + this.moshi = new Moshi.Builder().build(); // Initialize Moshi + } + + /** + * Constructs an RpcClient with a specified endpoint and SOCKS proxy. + * + * @param endpoint the RPC endpoint + * @param proxyHost the SOCKS proxy host + * @param proxyPort the SOCKS proxy port + */ + public RpcClient(String endpoint, String proxyHost, int proxyPort) { + this.endpoint = endpoint; + this.httpClient = new OkHttpClient.Builder() + .proxy(new Proxy(Proxy.Type.SOCKS, new InetSocketAddress(proxyHost, proxyPort))) // Set SOCKS proxy + .readTimeout(20, TimeUnit.SECONDS) + .build(); + this.rpcApi = new RpcApi(this); + this.moshi = new Moshi.Builder().build(); // Initialize Moshi } + /** + * Calls the specified RPC method with the given parameters. + * + * @param method the RPC method to call + * @param params the parameters for the RPC method + * @param clazz the class type of the expected result + * @return the result of the RPC call + * @throws RpcException if an error occurs during the RPC call + */ public T call(String method, List params, Class clazz) throws RpcException { RpcRequest rpcRequest = new RpcRequest(method, params); - JsonAdapter rpcRequestJsonAdapter = new Moshi.Builder().build().adapter(RpcRequest.class); - JsonAdapter> resultAdapter = new Moshi.Builder().build() - .adapter(Types.newParameterizedType(RpcResponse.class, Type.class.cast(clazz))); + JsonAdapter rpcRequestJsonAdapter = moshi.adapter(RpcRequest.class); + JsonAdapter> resultAdapter = moshi.adapter(Types.newParameterizedType(RpcResponse.class, clazz)); Request request = new Request.Builder().url(getEndpoint()) .post(RequestBody.create(rpcRequestJsonAdapter.toJson(rpcRequest), JSON)).build(); @@ -93,50 +161,55 @@ public T call(String method, List params, Class clazz) throws Rpc final String result = response.body().string(); RpcResponse rpcResult = resultAdapter.fromJson(result); - if (rpcResult.getError() != null) { - throw new RpcException(rpcResult.getError().getMessage()); + if (rpcResult == null || rpcResult.getError() != null) { + throw new RpcException(rpcResult != null ? rpcResult.getError().getMessage() : "RPC response is null"); } - return (T) rpcResult.getResult(); + return rpcResult.getResult(); } catch (SSLHandshakeException e) { this.httpClient = new OkHttpClient.Builder().build(); - throw new RpcException(e.getMessage()); + throw new RpcException("SSL Handshake failed: " + e.getMessage()); } catch (IOException e) { - throw new RpcException(e.getMessage()); + throw new RpcException("IO error during RPC call: " + e.getMessage()); } } + /** + * Returns the RpcApi instance associated with this client. + * + * @return the RpcApi instance + */ public RpcApi getApi() { return rpcApi; } + /** + * Returns the current RPC endpoint. + * + * @return the RPC endpoint + */ public String getEndpoint() { - if (cluster != null) { - return getWeightedEndpoint(); - } - return endpoint; + return (cluster != null) ? getWeightedEndpoint() : endpoint; } /** - * Returns RPC Endpoint based on a list of weighted endpoints - * Weighted endpoints can be given a integer weight, with higher weights used more than lower weights - * Total weights across all endpoints do not need to sum up to any specific number + * Returns RPC Endpoint based on a list of weighted endpoints. + * Weighted endpoints can be given an integer weight, with higher weights used more than lower weights. + * Total weights across all endpoints do not need to sum up to any specific number. * * @return String RPCEndpoint */ private String getWeightedEndpoint() { - int currentNumber = 0; - int randomMultiplier = cluster.endpoints.stream().mapToInt(WeightedEndpoint::getWeight).sum(); - double randomNumber = Math.random() * randomMultiplier; - String currentEndpoint = ""; + int totalWeight = cluster.endpoints.stream().mapToInt(WeightedEndpoint::getWeight).sum(); + double randomNumber = Math.random() * totalWeight; + int currentWeight = 0; + for (WeightedEndpoint endpoint : cluster.endpoints) { - if (randomNumber > currentNumber + endpoint.getWeight()) { - currentNumber += endpoint.getWeight(); - } else if (randomNumber >= currentNumber && randomNumber <= currentNumber + endpoint.getWeight()) { + currentWeight += endpoint.getWeight(); + if (randomNumber < currentWeight) { return endpoint.getUrl(); } } - return currentEndpoint; + return ""; // Return empty string if no endpoint is found } - } diff --git a/src/main/java/org/p2p/solanaj/rpc/types/ConfirmedTransaction.java b/src/main/java/org/p2p/solanaj/rpc/types/ConfirmedTransaction.java index 5b668a18..60c1e765 100644 --- a/src/main/java/org/p2p/solanaj/rpc/types/ConfirmedTransaction.java +++ b/src/main/java/org/p2p/solanaj/rpc/types/ConfirmedTransaction.java @@ -115,6 +115,12 @@ public static class Transaction { @Json(name = "signatures") private List signatures; + + + @Json(name = "blockTime") + private String blocktime; + + } @Json(name = "meta") diff --git a/src/main/java/org/p2p/solanaj/rpc/types/LatestBlockhash.java b/src/main/java/org/p2p/solanaj/rpc/types/LatestBlockhash.java new file mode 100644 index 00000000..482f639e --- /dev/null +++ b/src/main/java/org/p2p/solanaj/rpc/types/LatestBlockhash.java @@ -0,0 +1,23 @@ +package org.p2p.solanaj.rpc.types; + +import com.squareup.moshi.Json; +import lombok.Getter; +import lombok.ToString; + +@Getter +@ToString +public class LatestBlockhash extends RpcResultObject { + + @Getter + @ToString + public static class Value { + @Json(name = "blockhash") + private String blockhash; + + @Json(name = "lastValidBlockHeight") + private long lastValidBlockHeight; + } + + @Json(name = "value") + private Value value; +} diff --git a/src/main/java/org/p2p/solanaj/rpc/types/RecentPrioritizationFees.java b/src/main/java/org/p2p/solanaj/rpc/types/RecentPrioritizationFees.java new file mode 100644 index 00000000..a63de145 --- /dev/null +++ b/src/main/java/org/p2p/solanaj/rpc/types/RecentPrioritizationFees.java @@ -0,0 +1,21 @@ +package org.p2p.solanaj.rpc.types; + +import java.util.Map; + +public class RecentPrioritizationFees { + private long slot; + private long prioritizationFee; + + public RecentPrioritizationFees(Map jsonMap) { + this.slot = ((Number) jsonMap.get("slot")).longValue(); + this.prioritizationFee = ((Number) jsonMap.get("prioritizationFee")).longValue(); + } + + public long getSlot() { + return slot; + } + + public long getPrioritizationFee() { + return prioritizationFee; + } +} \ No newline at end of file diff --git a/src/main/java/org/p2p/solanaj/utils/Shortvec.java b/src/main/java/org/p2p/solanaj/utils/ShortvecEncoding.java similarity index 97% rename from src/main/java/org/p2p/solanaj/utils/Shortvec.java rename to src/main/java/org/p2p/solanaj/utils/ShortvecEncoding.java index 478daf7c..ab64b4a6 100644 --- a/src/main/java/org/p2p/solanaj/utils/Shortvec.java +++ b/src/main/java/org/p2p/solanaj/utils/ShortvecEncoding.java @@ -4,7 +4,7 @@ import static org.bitcoinj.core.Utils.*; -public class Shortvec { +public class ShortvecEncoding { public static byte[] encodeLength(int len) { byte[] out = new byte[10]; diff --git a/src/main/java/org/p2p/solanaj/ws/SubscriptionWebSocketClient.java b/src/main/java/org/p2p/solanaj/ws/SubscriptionWebSocketClient.java index b423c203..d5bc49b0 100644 --- a/src/main/java/org/p2p/solanaj/ws/SubscriptionWebSocketClient.java +++ b/src/main/java/org/p2p/solanaj/ws/SubscriptionWebSocketClient.java @@ -5,27 +5,77 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; +import java.util.HashMap; +import java.util.concurrent.*; +import java.util.logging.Level; import java.util.logging.Logger; +import java.util.concurrent.locks.ReentrantLock; +import java.util.concurrent.locks.Lock; +import org.p2p.solanaj.rpc.types.config.Commitment; import com.squareup.moshi.JsonAdapter; import com.squareup.moshi.Moshi; import com.squareup.moshi.Types; import org.java_websocket.client.WebSocketClient; +import org.java_websocket.framing.CloseFrame; import org.java_websocket.handshake.ServerHandshake; import org.p2p.solanaj.rpc.types.RpcNotificationResult; import org.p2p.solanaj.rpc.types.RpcRequest; import org.p2p.solanaj.rpc.types.RpcResponse; -import org.p2p.solanaj.rpc.types.config.Commitment; import org.p2p.solanaj.ws.listeners.NotificationEventListener; +/** + * SubscriptionWebSocketClient is a WebSocket client for managing subscriptions to various Solana events. + * + * This class allows users to subscribe to different types of notifications from the Solana blockchain, + * such as account updates, block updates, program updates, and vote updates. Each subscription is + * identified by a unique subscription ID, which is generated when a subscription request is made. + * The client maintains a mapping of these subscription IDs to their corresponding parameters and + * notification listeners, enabling efficient management of active subscriptions. + * + * Users can specify various parameters for their subscriptions, including the commitment level + * (e.g., FINALIZED, CONFIRMED) and the encoding format (e.g., jsonParsed, base64) for the data + * received in notifications. The client handles incoming WebSocket messages, processes notifications, + * and invokes the appropriate listener callbacks with the received data. + * + * The class also provides methods for unsubscribing from notifications, ensuring that resources + * are properly released when subscriptions are no longer needed. Thread safety is maintained + * through the use of locks to protect shared data structures during subscription management. + */ public class SubscriptionWebSocketClient extends WebSocketClient { + private static final Logger LOGGER = Logger.getLogger(SubscriptionWebSocketClient.class.getName()); + private static final int MAX_RECONNECT_DELAY = 30000; + private static final int INITIAL_RECONNECT_DELAY = 1000; + private static final int HEARTBEAT_INTERVAL = 30; + + private final Map subscriptions = new ConcurrentHashMap<>(); + private final Map subscriptionIds = new ConcurrentHashMap<>(); + private final Map subscriptionListeners = new ConcurrentHashMap<>(); + private final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); + private final CountDownLatch connectLatch = new CountDownLatch(1); + + private int reconnectDelay = INITIAL_RECONNECT_DELAY; + private final Moshi moshi = new Moshi.Builder().build(); + + private final Map activeSubscriptions = new ConcurrentHashMap<>(); + private final Lock subscriptionLock = new ReentrantLock(); + private final Lock listenerLock = new ReentrantLock(); + + /** + * Inner class to hold subscription parameters. + */ private static class SubscriptionParams { - RpcRequest request; + final RpcRequest request; NotificationEventListener listener; + /** + * Constructs a SubscriptionParams object. + * + * @param request The RPC request for the subscription + * @param listener The listener for notification events + */ SubscriptionParams(RpcRequest request, NotificationEventListener listener) { this.request = request; this.listener = listener; @@ -36,86 +86,91 @@ private static class SubscriptionParams { } } - private final Map subscriptions = new ConcurrentHashMap<>(); - private final Map subscriptionIds = new ConcurrentHashMap<>(); - private final Map subscriptionListeners = new ConcurrentHashMap<>(); - private static final Logger LOGGER = Logger.getLogger(SubscriptionWebSocketClient.class.getName()); - + /** + * Creates a SubscriptionWebSocketClient instance with the exact path provided. + * + * @param endpoint The WebSocket endpoint URL + * @return A new SubscriptionWebSocketClient instance + */ public static SubscriptionWebSocketClient getExactPathInstance(String endpoint) { - URI serverURI; - SubscriptionWebSocketClient instance; - try { - serverURI = new URI(endpoint); + URI serverURI = new URI(endpoint); + SubscriptionWebSocketClient instance = new SubscriptionWebSocketClient(serverURI); + if (!instance.isOpen()) { + instance.connect(); + } + return instance; } catch (URISyntaxException e) { - throw new IllegalArgumentException(e); - } - - instance = new SubscriptionWebSocketClient(serverURI); - - if (!instance.isOpen()) { - instance.connect(); + throw new IllegalArgumentException("Invalid endpoint URI", e); } - - return instance; } + /** + * Creates a SubscriptionWebSocketClient instance with a modified URI based on the provided endpoint. + * + * @param endpoint The endpoint URL + * @return A new SubscriptionWebSocketClient instance + */ public static SubscriptionWebSocketClient getInstance(String endpoint) { - URI serverURI; - URI endpointURI; - SubscriptionWebSocketClient instance; - try { - endpointURI = new URI(endpoint); - serverURI = new URI(endpointURI.getScheme() == "https" ? "wss" : "ws" + "://" + endpointURI.getHost()); - } catch (URISyntaxException e) { - throw new IllegalArgumentException(e); - } - - instance = new SubscriptionWebSocketClient(serverURI); - - if (!instance.isOpen()) { + URI endpointURI = new URI(endpoint); + String scheme = "https".equals(endpointURI.getScheme()) ? "wss" : "ws"; + URI serverURI = new URI(scheme + "://" + endpointURI.getHost()); + SubscriptionWebSocketClient instance = new SubscriptionWebSocketClient(serverURI); instance.connect(); + return instance; + } catch (URISyntaxException e) { + throw new IllegalArgumentException("Invalid endpoint URI", e); } - - return instance; } + /** + * Constructs a SubscriptionWebSocketClient with the given server URI. + * + * @param serverURI The URI of the WebSocket server + */ public SubscriptionWebSocketClient(URI serverURI) { super(serverURI); - } /** - * For example, used to "listen" to an private key's "tweets" - * By accountSubscribing to their private key(s) + * Subscribes to account updates for the given key with specified commitment level and encoding. * - * @param key - * @param listener + * @param key The account key to subscribe to + * @param listener The listener to handle notifications + * @param commitment The commitment level for the subscription + * @param encoding The encoding format for Account data */ - public void accountSubscribe(String key, NotificationEventListener listener) { + public void accountSubscribe(String key, NotificationEventListener listener, Commitment commitment, String encoding) { List params = new ArrayList<>(); params.add(key); - params.add(Map.of("encoding", "jsonParsed", "commitment", Commitment.PROCESSED.getValue())); + params.add(Map.of("encoding", encoding, "commitment", commitment.getValue())); RpcRequest rpcRequest = new RpcRequest("accountSubscribe", params); + addSubscription(rpcRequest, listener); + } - subscriptions.put(rpcRequest.getId(), new SubscriptionParams(rpcRequest, listener)); - subscriptionIds.put(rpcRequest.getId(), 0L); + // Overload methods to maintain backwards compatibility + public void accountSubscribe(String key, NotificationEventListener listener, Commitment commitment) { + accountSubscribe(key, listener, commitment, "jsonParsed"); + } - updateSubscriptions(); + public void accountSubscribe(String key, NotificationEventListener listener) { + accountSubscribe(key, listener, Commitment.FINALIZED, "jsonParsed"); } + /** + * Subscribes to signature updates for the given signature. + * + * @param signature The signature to subscribe to + * @param listener The listener to handle notifications + */ public void signatureSubscribe(String signature, NotificationEventListener listener) { List params = new ArrayList<>(); params.add(signature); RpcRequest rpcRequest = new RpcRequest("signatureSubscribe", params); - - subscriptions.put(rpcRequest.getId(), new SubscriptionParams(rpcRequest, listener)); - subscriptionIds.put(rpcRequest.getId(), 0L); - - updateSubscriptions(); + addSubscription(rpcRequest, listener); } public void transactionSubscribe(String key, NotificationEventListener listener) { @@ -135,7 +190,7 @@ public void transactionSubscribe(String key, NotificationEventListener listener) updateSubscriptions(); } - public void logsSubscribe(String mention, NotificationEventListener listener) { + public String logsSubscribeWithId(String mention, NotificationEventListener listener) { List params = new ArrayList<>(); params.add(Map.of("mentions", List.of(mention))); params.add(Map.of("commitment", Commitment.PROCESSED.getValue())); @@ -146,32 +201,215 @@ public void logsSubscribe(String mention, NotificationEventListener listener) { subscriptionIds.put(rpcRequest.getId(), 0L); updateSubscriptions(); + return rpcRequest.getId(); } - public String logsSubscribeWithId(String mention, NotificationEventListener listener) { + /** + * Subscribes to log updates for the given mention. + * + * @param mention The mention to subscribe to + * @param listener The listener to handle notifications + */ + public void logsSubscribe(String mention, NotificationEventListener listener) { + logsSubscribe(List.of(mention), listener); + } + + /** + * Subscribes to log updates for the given mentions. + * + * @param mentions The mentions to subscribe to + * @param listener The listener to handle notifications + */ + public void logsSubscribe(List mentions, NotificationEventListener listener) { List params = new ArrayList<>(); - params.add(Map.of("mentions", List.of(mention))); - params.add(Map.of("commitment", Commitment.PROCESSED.getValue())); + params.add(Map.of("mentions", mentions)); + params.add(Map.of("commitment", "finalized")); RpcRequest rpcRequest = new RpcRequest("logsSubscribe", params); + addSubscription(rpcRequest, listener); + } - subscriptions.put(rpcRequest.getId(), new SubscriptionParams(rpcRequest, listener)); - subscriptionIds.put(rpcRequest.getId(), 0L); + /** + * Subscribes to block updates. + * + * @param listener The listener to handle notifications + * @param commitment The commitment level for the subscription + * @param encoding The encoding format for block data + */ + public void blockSubscribe(NotificationEventListener listener, Commitment commitment, String encoding) { + List params = new ArrayList<>(); + params.add(Map.of("encoding", encoding, "commitment", commitment.getValue())); - updateSubscriptions(); - return rpcRequest.getId(); + RpcRequest rpcRequest = new RpcRequest("blockSubscribe", params); + addSubscription(rpcRequest, listener); } - public void logsSubscribe(List mentions, NotificationEventListener listener) { + public void blockSubscribe(NotificationEventListener listener, Commitment commitment) { + blockSubscribe(listener, commitment, "json"); + } + + public void blockSubscribe(NotificationEventListener listener) { + blockSubscribe(listener, Commitment.FINALIZED, "json"); + } + + /** + * Unsubscribes from block updates. + * + * @param subscriptionId The ID of the subscription to cancel + */ + public void blockUnsubscribe(String subscriptionId) { + unsubscribe("blockUnsubscribe", subscriptionId); + } + + /** + * Subscribes to program updates. + * + * @param programId The program ID to subscribe to + * @param listener The listener to handle notifications + * @param commitment The commitment level for the subscription + * @param encoding The encoding format for program data + */ + public void programSubscribe(String programId, NotificationEventListener listener, Commitment commitment, String encoding) { List params = new ArrayList<>(); - params.add(Map.of("mentions", mentions)); - params.add(Map.of("commitment", "finalized")); + params.add(programId); + params.add(Map.of("encoding", encoding, "commitment", commitment.getValue())); - RpcRequest rpcRequest = new RpcRequest("logsSubscribe", params); + RpcRequest rpcRequest = new RpcRequest("programSubscribe", params); + addSubscription(rpcRequest, listener); + } - subscriptions.put(rpcRequest.getId(), new SubscriptionParams(rpcRequest, listener)); - subscriptionIds.put(rpcRequest.getId(), null); + public void programSubscribe(String programId, NotificationEventListener listener, Commitment commitment) { + programSubscribe(programId, listener, commitment, "base64"); + } + + public void programSubscribe(String programId, NotificationEventListener listener) { + programSubscribe(programId, listener, Commitment.FINALIZED, "base64"); + } + + /** + * Unsubscribes from program updates. + * + * @param subscriptionId The ID of the subscription to cancel + */ + public void programUnsubscribe(String subscriptionId) { + unsubscribe("programUnsubscribe", subscriptionId); + } + /** + * Subscribes to root updates. + * + * @param listener The listener to handle notifications + */ + public void rootSubscribe(NotificationEventListener listener) { + RpcRequest rpcRequest = new RpcRequest("rootSubscribe", new ArrayList<>()); + addSubscription(rpcRequest, listener); + } + + /** + * Unsubscribes from root updates. + * + * @param subscriptionId The ID of the subscription to cancel + */ + public void rootUnsubscribe(String subscriptionId) { + unsubscribe("rootUnsubscribe", subscriptionId); + } + + /** + * Subscribes to slot updates. + * + * @param listener The listener to handle notifications + */ + public void slotSubscribe(NotificationEventListener listener) { + RpcRequest rpcRequest = new RpcRequest("slotSubscribe", new ArrayList<>()); + addSubscription(rpcRequest, listener); + } + + /** + * Unsubscribes from slot updates. + * + * @param subscriptionId The ID of the subscription to cancel + */ + public void slotUnsubscribe(String subscriptionId) { + unsubscribe("slotUnsubscribe", subscriptionId); + } + + /** + * Subscribes to slots updates. + * + * @param listener The listener to handle notifications + */ + public void slotsUpdatesSubscribe(NotificationEventListener listener) { + RpcRequest rpcRequest = new RpcRequest("slotsUpdatesSubscribe", new ArrayList<>()); + addSubscription(rpcRequest, listener); + } + + /** + * Unsubscribes from slots updates. + * + * @param subscriptionId The ID of the subscription to cancel + */ + public void slotsUpdatesUnsubscribe(String subscriptionId) { + unsubscribe("slotsUpdatesUnsubscribe", subscriptionId); + } + + /** + * Subscribes to vote updates. + * + * @param listener The listener to handle notifications + */ + public void voteSubscribe(NotificationEventListener listener) { + RpcRequest rpcRequest = new RpcRequest("voteSubscribe", new ArrayList<>()); + addSubscription(rpcRequest, listener); + } + + /** + * Unsubscribes from vote updates. + * + * @param subscriptionId The ID of the subscription to cancel + */ + public void voteUnsubscribe(String subscriptionId) { + unsubscribe("voteUnsubscribe", subscriptionId); + } + + /** + * Generic method to handle unsubscribe requests. + * + * @param method The unsubscribe method name + * @param subscriptionId The ID of the subscription to cancel + */ + private void unsubscribe(String method, String subscriptionId) { + List params = new ArrayList<>(); + params.add(Long.parseLong(subscriptionId)); + RpcRequest unsubRequest = new RpcRequest(method, params); + JsonAdapter rpcRequestJsonAdapter = moshi.adapter(RpcRequest.class); + send(rpcRequestJsonAdapter.toJson(unsubRequest)); + + subscriptionLock.lock(); + try { + activeSubscriptions.remove(subscriptionId); + subscriptionListeners.remove(Long.parseLong(subscriptionId)); + } finally { + subscriptionLock.unlock(); + } + LOGGER.info("Unsubscribed from " + method + " with ID: " + subscriptionId); + } + + /** + * Adds a subscription to the client. + * + * @param rpcRequest The RPC request for the subscription + * @param listener The listener for notification events + */ + public void addSubscription(RpcRequest rpcRequest, NotificationEventListener listener) { + String subscriptionId = rpcRequest.getId(); + subscriptionLock.lock(); + try { + activeSubscriptions.put(subscriptionId, new SubscriptionParams(rpcRequest, listener)); + subscriptions.put(subscriptionId, new SubscriptionParams(rpcRequest, listener)); + subscriptionIds.put(subscriptionId, 0L); + } finally { + subscriptionLock.unlock(); + } updateSubscriptions(); } @@ -195,79 +433,307 @@ public void logsUnSubscribe(String subscriptionIdKey) { subscriptionIds.remove(rpcRequest.getId()); } + /** + * Handles the WebSocket connection opening. + * + * @param handshakedata The server handshake data + */ @Override public void onOpen(ServerHandshake handshakedata) { - LOGGER.info("Websocket connection opened"); - updateSubscriptions(); - LOGGER.info("Size of subscriptions after onOpen: " + subscriptions.size()); + LOGGER.info("WebSocket connection opened"); + reconnectDelay = INITIAL_RECONNECT_DELAY; + startHeartbeat(); + connectLatch.countDown(); } - @SuppressWarnings({ "rawtypes" }) + /** + * Handles incoming WebSocket messages. + * + * @param message The received message + */ @Override public void onMessage(String message) { - JsonAdapter> resultAdapter = new Moshi.Builder().build() - .adapter(Types.newParameterizedType(RpcResponse.class, Long.class)); - try { + JsonAdapter> resultAdapter = moshi.adapter( + Types.newParameterizedType(RpcResponse.class, Long.class)); RpcResponse rpcResult = resultAdapter.fromJson(message); - assert rpcResult != null; - String rpcResultId = rpcResult.getId(); - if (rpcResultId != null) { - if (subscriptionIds.containsKey(rpcResultId)) { - try { - subscriptionIds.put(rpcResultId, rpcResult.getResult()); - subscriptionListeners.put(rpcResult.getResult(), subscriptions.get(rpcResultId).listener); - subscriptions.remove(rpcResultId); - } catch (NullPointerException ignored) { - - } - } + if(rpcResult!=null && rpcResult.getError()!=null){ + throw new IllegalStateException(rpcResult.getError().toString()); + } + if (rpcResult != null && rpcResult.getId() != null) { + handleSubscriptionResponse(rpcResult); } else { - JsonAdapter notificationResultAdapter = new Moshi.Builder().build() - .adapter(RpcNotificationResult.class); - RpcNotificationResult result = notificationResultAdapter.fromJson(message); - assert result != null; - NotificationEventListener listener = subscriptionListeners.get(result.getParams().getSubscription()); - - Map value = (Map) result.getParams().getResult().getValue(); - - switch (result.getMethod()) { - case "signatureNotification": - listener.onNotificationEvent(new SignatureNotification(value.get("err"))); - break; - case "accountNotification": - case "logsNotification": - if (listener != null) { + handleNotification(message); + } + } catch (Exception ex) { + LOGGER.log(Level.SEVERE, "Error processing message", ex); + } + } + + /** + * Handles subscription responses. + * + * @param rpcResult The RPC response + */ + private void handleSubscriptionResponse(RpcResponse rpcResult) { + String rpcResultId = rpcResult.getId(); + if (subscriptionIds.containsKey(rpcResultId)) { + subscriptionIds.put(rpcResultId, rpcResult.getResult()); + SubscriptionParams params = subscriptions.get(rpcResultId); + if (params != null) { + subscriptionListeners.put(rpcResult.getResult(), params.listener); + subscriptions.remove(rpcResultId); + // Update the activeSubscriptions map with the new subscription ID + activeSubscriptions.put(String.valueOf(rpcResult.getResult()), params); + activeSubscriptions.remove(rpcResultId); + } + } + } + + /** + * Handles notification messages. + * + * @param message The notification message + * @throws Exception If an error occurs while processing the notification + */ + private void handleNotification(String message) throws Exception { + JsonAdapter notificationResultAdapter = moshi.adapter(RpcNotificationResult.class); + RpcNotificationResult result = notificationResultAdapter.fromJson(message); + if (result != null) { + Long subscriptionId = result.getParams().getSubscription(); + listenerLock.lock(); + try { + NotificationEventListener listener = subscriptionListeners.get(subscriptionId); + if (listener != null) { + Map value = (Map) result.getParams().getResult().getValue(); + switch (result.getMethod()) { + case "signatureNotification": + listener.onNotificationEvent(new SignatureNotification(value.get("err"))); + break; + case "accountNotification": + case "logsNotification": + case "blockNotification": + case "programNotification": + case "rootNotification": + case "slotNotification": + case "slotsUpdatesNotification": + case "voteNotification": listener.onNotificationEvent(value); - } - break; + break; + default: + LOGGER.warning("Unknown notification method: " + result.getMethod()); + } + } else { + LOGGER.warning("No listener found for subscription ID: " + subscriptionId); } + } finally { + listenerLock.unlock(); } - } catch (Exception ex) { - ex.printStackTrace(); + } else { + LOGGER.warning("Received null notification result"); } } + /** + * Handles WebSocket connection closure. + * + * @param code The status code indicating why the connection was closed + * @param reason A human-readable explanation for the closure + * @param remote Whether the closure was initiated by the remote endpoint + */ @Override public void onClose(int code, String reason, boolean remote) { - System.out.println( - "Connection closed by " + (remote ? "remote peer" : "us") + " Code: " + code + " Reason: " + reason); - LOGGER.info("Size of subscriptions after closing: " + subscriptions.size()); + LOGGER.info("Connection closed by " + (remote ? "remote peer" : "us") + " Code: " + code + " Reason: " + reason); + stopHeartbeat(); + if (remote || code != CloseFrame.NORMAL) { + scheduleReconnect(); + } } + /** + * Handles WebSocket errors. + * + * @param ex The exception that describes the error + */ @Override public void onError(Exception ex) { - ex.printStackTrace(); + LOGGER.log(Level.SEVERE, "WebSocket error occurred", ex); + if (ex instanceof org.java_websocket.exceptions.WebsocketNotConnectedException) { + LOGGER.severe("WebSocket is not connected. Attempting to reconnect..."); + reconnect(); + } else if (ex instanceof org.java_websocket.exceptions.IncompleteHandshakeException) { + LOGGER.severe("Incomplete handshake. Check your connection parameters."); + } else if (ex instanceof java.net.SocketTimeoutException) { + LOGGER.severe("Connection timed out. Check network stability and server responsiveness."); + } else { + LOGGER.severe("Unexpected error: " + ex.getMessage()); + } } - private void updateSubscriptions() { - if (isOpen() && !subscriptions.isEmpty()) { - JsonAdapter rpcRequestJsonAdapter = new Moshi.Builder().build().adapter(RpcRequest.class); + /** + * Attempts to reconnect to the WebSocket server. + */ + public void reconnect() { + LOGGER.info("Attempting to reconnect..."); + try { + final boolean reconnectBlocking = reconnectBlocking(); + if(reconnectBlocking){ + resubscribeAll(); + } + } catch (InterruptedException e) { + LOGGER.warning("Reconnection interrupted: " + e.getMessage()); + Thread.currentThread().interrupt(); + } + } + + /** + * Starts the heartbeat mechanism to keep the connection alive. + */ + private void startHeartbeat() { + executor.scheduleAtFixedRate(this::sendPing, HEARTBEAT_INTERVAL, HEARTBEAT_INTERVAL, TimeUnit.SECONDS); + } + /** + * Stops the heartbeat mechanism. + */ + private void stopHeartbeat() { + executor.shutdown(); + try { + if (!executor.awaitTermination(800, TimeUnit.MILLISECONDS)) { + executor.shutdownNow(); + } + } catch (InterruptedException e) { + executor.shutdownNow(); + Thread.currentThread().interrupt(); + } + } + + /** + * Updates all active subscriptions. + */ + private void updateSubscriptions() { + if (isOpen()) { + JsonAdapter rpcRequestJsonAdapter = moshi.adapter(RpcRequest.class); for (SubscriptionParams sub : subscriptions.values()) { send(rpcRequestJsonAdapter.toJson(sub.request)); } + for (Map.Entry entry : subscriptionIds.entrySet()) { + if (entry.getValue() != 0L) { + SubscriptionParams params = subscriptions.get(entry.getKey()); + if (params != null) { + send(rpcRequestJsonAdapter.toJson(params.request)); + } + } + } } } + /** + * Schedules a reconnection attempt with exponential backoff. + */ + private void scheduleReconnect() { + executor.schedule(() -> { + reconnect(); + reconnectDelay = Math.min(reconnectDelay * 2, MAX_RECONNECT_DELAY); + }, reconnectDelay, TimeUnit.MILLISECONDS); + } + + /** + * Waits for the WebSocket connection to be established. + * + * @param timeout the maximum time to wait + * @param unit the time unit of the timeout argument + * @return true if the connection was successfully established, false if the timeout was reached + * @throws InterruptedException if the current thread is interrupted while waiting + */ + public boolean waitForConnection(long timeout, TimeUnit unit) throws InterruptedException { + return connectLatch.await(timeout, unit); + } + + private void resubscribeAll() { + LOGGER.info("Resubscribing to all active subscriptions"); + cleanSubscriptions(); + final Map activeSubscriptionsResubscribe = new HashMap<>(); + for (Map.Entry entry : activeSubscriptions.entrySet()) { + SubscriptionParams paramsOld = entry.getValue(); + final RpcRequest rpcRequest = paramsOld.request; + final NotificationEventListener notificationEventListener = paramsOld.listener; + final RpcRequest request = new RpcRequest(rpcRequest.getMethod(), rpcRequest.getParams()); + final String subscriptionId = request.getId(); + final SubscriptionParams params = new SubscriptionParams( + request, + notificationEventListener); + subscriptions.put(subscriptionId, params); + subscriptionIds.put(subscriptionId, 0L); + activeSubscriptionsResubscribe.put(subscriptionId, params); + } + activeSubscriptions.clear(); + activeSubscriptions.putAll(activeSubscriptionsResubscribe); + updateSubscriptions(); + } + + private void cleanSubscriptions(){ + subscriptions.clear(); + subscriptionIds.clear(); + subscriptionListeners.clear(); + } + + public void unsubscribe(String subscriptionId) { + SubscriptionParams params = activeSubscriptions.remove(subscriptionId); + if (params != null) { + // Send an unsubscribe request to the server + List unsubParams = new ArrayList<>(); + unsubParams.add(Long.parseLong(subscriptionId)); + RpcRequest unsubRequest = new RpcRequest(getUnsubscribeMethod(params.request.getMethod()), unsubParams); + JsonAdapter rpcRequestJsonAdapter = moshi.adapter(RpcRequest.class); + send(rpcRequestJsonAdapter.toJson(unsubRequest)); + + // Remove the subscription from subscriptionListeners + subscriptionListeners.remove(Long.parseLong(subscriptionId)); + LOGGER.info("Unsubscribed from subscription: " + subscriptionId); + } else { + LOGGER.warning("Attempted to unsubscribe from non-existent subscription: " + subscriptionId); + } + } + + private String getUnsubscribeMethod(String subscribeMethod) { + switch (subscribeMethod) { + case "accountSubscribe": + return "accountUnsubscribe"; + case "logsSubscribe": + return "logsUnsubscribe"; + case "signatureSubscribe": + return "signatureUnsubscribe"; + case "blockSubscribe": + return "blockUnsubscribe"; + case "programSubscribe": + return "programUnsubscribe"; + case "rootSubscribe": + return "rootUnsubscribe"; + case "slotSubscribe": + return "slotUnsubscribe"; + case "slotsUpdatesSubscribe": + return "slotsUpdatesUnsubscribe"; + case "voteSubscribe": + return "voteUnsubscribe"; + // Add more cases for other subscription types as needed + default: + throw new IllegalArgumentException("Unknown subscribe method: " + subscribeMethod); + } + } + + /** + * Gets the subscription ID for a given account. + * + * @param account The account to get the subscription ID for + * @return The subscription ID, or null if not found + */ + public String getSubscriptionId(String account) { + for (Map.Entry entry : activeSubscriptions.entrySet()) { + if (entry.getValue().request.getParams().get(0).equals(account)) { + return entry.getKey(); + } + } + return null; + } } diff --git a/src/main/java/org/p2p/solanaj/ws/listeners/AccountNotificationEventListener.java b/src/main/java/org/p2p/solanaj/ws/listeners/AccountNotificationEventListener.java index 9989bdbe..61fb0685 100644 --- a/src/main/java/org/p2p/solanaj/ws/listeners/AccountNotificationEventListener.java +++ b/src/main/java/org/p2p/solanaj/ws/listeners/AccountNotificationEventListener.java @@ -1,17 +1,43 @@ package org.p2p.solanaj.ws.listeners; +import java.util.Map; +import java.util.HashMap; import java.util.logging.Logger; +import java.util.logging.Level; +/** + * Listener for account notification events in Solana. + * Handles changes in account data or lamports. + */ public class AccountNotificationEventListener implements NotificationEventListener { private static final Logger LOGGER = Logger.getLogger(AccountNotificationEventListener.class.getName()); /** - * Handle Account notification event (change in data or change in lamports). Type of "data" is a Map. - * @param data Map + * Handles account notification events. + * @param data The notification data, expected to be a Map. */ @Override public void onNotificationEvent(Object data) { - LOGGER.info("Raw = " + data); + if (!(data instanceof Map)) { + LOGGER.log(Level.WARNING, "Invalid data type received: {0}", data.getClass().getName()); + return; + } + + Map rawMap = (Map) data; + Map accountData = new HashMap<>(); + + for (Map.Entry entry : rawMap.entrySet()) { + if (entry.getKey() instanceof String) { + accountData.put((String) entry.getKey(), entry.getValue()); + } + } + + String accountKey = (String) accountData.get("accountKey"); + Long lamports = (Long) accountData.get("lamports"); + String owner = (String) accountData.get("owner"); + + LOGGER.log(Level.INFO, "Account notification received for account: {0}", accountKey); + LOGGER.log(Level.INFO, "Lamports: {0}, Owner: {1}", new Object[]{lamports, owner}); } } diff --git a/src/main/java/org/p2p/solanaj/ws/listeners/LogNotificationEventListener.java b/src/main/java/org/p2p/solanaj/ws/listeners/LogNotificationEventListener.java index 73c02bd1..b99f7a0d 100644 --- a/src/main/java/org/p2p/solanaj/ws/listeners/LogNotificationEventListener.java +++ b/src/main/java/org/p2p/solanaj/ws/listeners/LogNotificationEventListener.java @@ -2,32 +2,100 @@ import org.p2p.solanaj.core.PublicKey; import org.p2p.solanaj.rpc.RpcClient; - -import java.util.AbstractMap; +import java.util.Map; +import java.util.List; import java.util.logging.Logger; +import java.util.HashMap; +/** + * A listener for Solana log notifications. + * This class implements the NotificationEventListener interface and provides + * functionality to process and log notification events from the Solana blockchain. + */ public class LogNotificationEventListener implements NotificationEventListener { - private static final Logger LOGGER = Logger.getLogger(LogNotificationEventListener.class.getName()); private final RpcClient client; private final PublicKey listeningPubkey; + /** + * Constructs a new LogNotificationEventListener. + * + * @param client The RpcClient used for communication with the Solana network. + * @param listeningPubkey The PublicKey this listener is associated with. + */ public LogNotificationEventListener(RpcClient client, PublicKey listeningPubkey) { this.client = client; this.listeningPubkey = listeningPubkey; } /** - * Handle Account notification event (change in data or change in lamports). Type of "data" is a Map. - * @param data Map + * Processes a notification event. + * This method logs the received notification data, including the transaction + * signature and associated logs. + * + * @param data The notification data object. */ @Override public void onNotificationEvent(Object data) { - if (data != null) { - AbstractMap map = (AbstractMap) data; - LOGGER.info(String.format("Data = %s", map)); - String signature = map.get("signature"); - LOGGER.info("Signature = " + signature); + if (data == null) { + LOGGER.warning("Received null data in onNotificationEvent"); + return; + } + + if (!(data instanceof Map)) { + LOGGER.warning("Received invalid data type in onNotificationEvent: " + data.getClass().getName()); + return; + } + + Map rawMap = (Map) data; + Map notificationData = new HashMap<>(); + + for (Map.Entry entry : rawMap.entrySet()) { + if (entry.getKey() instanceof String) { + notificationData.put((String) entry.getKey(), entry.getValue()); + } + } + + String signature = (String) notificationData.get("signature"); + List logs = null; + Object logsObj = notificationData.get("logs"); + if (logsObj instanceof List) { + logs = ((List) logsObj).stream() + .filter(String.class::isInstance) + .map(String.class::cast) + .toList(); + } + + if (signature == null || logs == null || logs.isEmpty()) { + LOGGER.warning("Missing required fields in notification data"); + return; } + + LOGGER.info("Received notification for transaction: " + signature); + for (String log : logs) { + LOGGER.info("Log: " + log); + } + + // Here you could add more specific processing based on the log contents + // For example, checking for specific program invocations or state changes + + } + + /** + * Gets the RpcClient associated with this listener. + * + * @return The RpcClient instance. + */ + public RpcClient getClient() { + return client; + } + + /** + * Gets the PublicKey this listener is associated with. + * + * @return The PublicKey instance. + */ + public PublicKey getListeningPubkey() { + return listeningPubkey; } } diff --git a/src/test/java/org/p2p/solanaj/core/AccountBasedTest.java b/src/test/java/org/p2p/solanaj/core/AccountBasedTest.java index 123a67ce..cb168c7e 100644 --- a/src/test/java/org/p2p/solanaj/core/AccountBasedTest.java +++ b/src/test/java/org/p2p/solanaj/core/AccountBasedTest.java @@ -1,7 +1,7 @@ package org.p2p.solanaj.core; import org.bitcoinj.core.Base58; -import org.junit.BeforeClass; +import org.junit.jupiter.api.BeforeAll; import java.io.FileInputStream; import java.io.IOException; @@ -19,7 +19,7 @@ public class AccountBasedTest { public static PublicKey usdcDestination; public static final Logger LOGGER = Logger.getLogger(AccountBasedTest.class.getName()); - @BeforeClass + @BeforeAll public static void setup() { // Build account from secretkey.dat byte[] data = new byte[0]; diff --git a/src/test/java/org/p2p/solanaj/core/AccountTest.java b/src/test/java/org/p2p/solanaj/core/AccountTest.java index 2c7192d9..7a9c0c8f 100644 --- a/src/test/java/org/p2p/solanaj/core/AccountTest.java +++ b/src/test/java/org/p2p/solanaj/core/AccountTest.java @@ -1,7 +1,7 @@ package org.p2p.solanaj.core; -import org.junit.Test; -import static org.junit.Assert.*; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; import java.util.Arrays; import java.util.logging.Logger; @@ -71,4 +71,34 @@ public void fromBip39MnemonicTest() { assertEquals("BeepMww3KwiDeEhEeZmqk4TegvJYNuDERPWm142X6Mx3", account.getPublicKey().toBase58()); } + @Test + public void testAccountEquality() { + byte[] secretKey = Base58.decode("4Z7cXSyeFR8wNGMVXUE1TwtKn5D5Vu7FzEv69dokLv7KrQk7h6pu4LF8ZRR9yQBhc7uSM6RTTZtU1fmaxiNrxXrs"); + Account account1 = new Account(secretKey); + Account account2 = new Account(secretKey); + Account account3 = new Account(); + + assertEquals(account1.getPublicKey(), account2.getPublicKey()); + assertNotEquals(account1.getPublicKey(), account3.getPublicKey()); + } + + @Test + public void testInvalidSecretKeyLength() { + byte[] invalidSecretKey = new byte[63]; // Invalid length + assertThrows(ArrayIndexOutOfBoundsException.class, () -> new Account(invalidSecretKey)); + } + + @Test + public void testFromBip44MnemonicWithPassphrase() { + Account acc = Account.fromBip44Mnemonic( + Arrays.asList("hint", "begin", "crowd", "dolphin", "drive", "render", + "finger", "above", "sponsor", "prize", "runway", "invest", "dizzy", "pony", "bitter", "trial", "ignore", + "crop", "please", "industry", "hockey", "wire", "use", "side"), + "passphrase123" + ); + + assertNotNull(acc); + assertNotEquals("G75kGJiizyFNdnvvHxkrBrcwLomGJT2CigdXnsYzrFHv", acc.getPublicKey().toString()); + } + } diff --git a/src/test/java/org/p2p/solanaj/core/AirdropTest.java b/src/test/java/org/p2p/solanaj/core/AirdropTest.java deleted file mode 100644 index 6658e9d8..00000000 --- a/src/test/java/org/p2p/solanaj/core/AirdropTest.java +++ /dev/null @@ -1,48 +0,0 @@ -package org.p2p.solanaj.core; - -import org.junit.Ignore; -import org.junit.Test; -import org.p2p.solanaj.rpc.Cluster; -import org.p2p.solanaj.rpc.RpcClient; -import org.p2p.solanaj.token.TokenManager; - -import java.util.List; - -import static org.junit.Assert.assertTrue; - -/** - * Test to iterate a list of SOL addresses and sent a fraction of a penny to. - * This illustrates the ability to Airdrop a given token directly to SOl addresses. - */ -public class AirdropTest extends AccountBasedTest { - private static final PublicKey USDC_TOKEN_MINT = new PublicKey("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"); - - private final PublicKey publicKey = solDestination; - private final RpcClient client = new RpcClient(Cluster.TESTNET); - public final TokenManager tokenManager = new TokenManager(client); - - // List of recipients - ETL a file into this - private final List recipients = List.of(publicKey); - - private static final long AIRDROP_AMOUNT = 100; - private static final byte AIRDROP_DECIMALS = 6; - - @Test - @Ignore - public void airdropTest() { - // Send airdrop - recipients.forEach(recipient -> { - tokenManager.transferCheckedToSolAddress( - testAccount, - usdcSource, - publicKey, - USDC_TOKEN_MINT, - AIRDROP_AMOUNT, - AIRDROP_DECIMALS - ); - LOGGER.info("Airdropped tokens to " + recipient.toBase58()); - }); - - assertTrue(true); - } -} diff --git a/src/test/java/org/p2p/solanaj/core/AnchorTest.java b/src/test/java/org/p2p/solanaj/core/AnchorTest.java index 040fd088..8fbb66a5 100644 --- a/src/test/java/org/p2p/solanaj/core/AnchorTest.java +++ b/src/test/java/org/p2p/solanaj/core/AnchorTest.java @@ -1,7 +1,9 @@ package org.p2p.solanaj.core; -import org.junit.Ignore; -import org.junit.Test; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Disabled; +import static org.junit.jupiter.api.Assertions.*; + import org.p2p.solanaj.programs.MemoProgram; import org.p2p.solanaj.programs.anchor.AnchorBasicTutorialProgram; import org.p2p.solanaj.rpc.Cluster; @@ -10,8 +12,6 @@ import java.util.List; -import static org.junit.Assert.assertNotNull; - public class AnchorTest extends AccountBasedTest { private final RpcClient client = new RpcClient(Cluster.TESTNET); @@ -21,7 +21,7 @@ public class AnchorTest extends AccountBasedTest { * Also attaches a memo. */ @Test - @Ignore + @Disabled public void basicInitializeTest() { final Account feePayer = testAccount; diff --git a/src/test/java/org/p2p/solanaj/core/BlockhashTest.java b/src/test/java/org/p2p/solanaj/core/BlockhashTest.java new file mode 100644 index 00000000..2b7a229a --- /dev/null +++ b/src/test/java/org/p2p/solanaj/core/BlockhashTest.java @@ -0,0 +1,99 @@ +package org.p2p.solanaj.core; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.p2p.solanaj.rpc.RpcApi; +import org.p2p.solanaj.rpc.RpcClient; +import org.p2p.solanaj.rpc.RpcException; +import org.p2p.solanaj.rpc.types.LatestBlockhash; +import org.p2p.solanaj.rpc.types.config.Commitment; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Test class for the getLatestBlockhash method in RpcApi using real Solana network data + */ +public class BlockhashTest { + + private RpcApi rpcApi; + + @BeforeEach + public void setup() { + // Use a public Solana testnet or devnet endpoint + String endpoint = "https://api.devnet.solana.com"; + RpcClient rpcClient = new RpcClient(endpoint); + rpcApi = new RpcApi(rpcClient); + } + + /** + * Test getLatestBlockhash without commitment + */ + @Test + public void testGetLatestBlockhashWithoutCommitment() throws RpcException { + // Act + LatestBlockhash latestBlockhash = rpcApi.getLatestBlockhash(); + + // Assert + assertNotNull(latestBlockhash); + assertNotNull(latestBlockhash.getValue()); + assertNotNull(latestBlockhash.getValue().getBlockhash()); + assertTrue(latestBlockhash.getValue().getLastValidBlockHeight() > 0); + } + + /** + * Test getLatestBlockhash with commitment + */ + @Test + public void testGetLatestBlockhashWithCommitment() throws RpcException { + // Arrange + Commitment commitment = Commitment.FINALIZED; + + // Act + LatestBlockhash latestBlockhash = rpcApi.getLatestBlockhash(commitment); + + // Assert + assertNotNull(latestBlockhash); + assertNotNull(latestBlockhash.getValue()); + assertNotNull(latestBlockhash.getValue().getBlockhash()); + assertTrue(latestBlockhash.getValue().getBlockhash().matches("^[1-9A-HJ-NP-Za-km-z]{44}$")); + assertTrue(latestBlockhash.getValue().getLastValidBlockHeight() > 0); + } + + /** + * Test that getLatestBlockhash returns different values over time + */ + @Test + public void testGetLatestBlockhashChangesOverTime() throws RpcException, InterruptedException { + // Act + LatestBlockhash firstBlockhash = rpcApi.getLatestBlockhash(); + Thread.sleep(5000); // Wait for 5 seconds + LatestBlockhash secondBlockhash = rpcApi.getLatestBlockhash(); + + // Assert + assertNotEquals(firstBlockhash.getValue().getBlockhash(), secondBlockhash.getValue().getBlockhash(), "Blockhashes should be different after waiting"); + assertTrue(secondBlockhash.getValue().getLastValidBlockHeight() >= firstBlockhash.getValue().getLastValidBlockHeight(), "Last valid block height should not decrease"); + } + + /** + * Test getLatestBlockhash with different commitment levels + */ + @Test + public void testGetLatestBlockhashWithDifferentCommitments() throws RpcException { + // Act + LatestBlockhash processedBlockhash = rpcApi.getLatestBlockhash(Commitment.PROCESSED); + LatestBlockhash confirmedBlockhash = rpcApi.getLatestBlockhash(Commitment.CONFIRMED); + LatestBlockhash finalizedBlockhash = rpcApi.getLatestBlockhash(Commitment.FINALIZED); + + // Assert + assertNotNull(processedBlockhash); + assertNotNull(confirmedBlockhash); + assertNotNull(finalizedBlockhash); + + // Note: These might be the same in some cases, but generally should be different + System.out.println("Processed blockhash: " + processedBlockhash.getValue().getBlockhash() + ", Last valid block height: " + processedBlockhash.getValue().getLastValidBlockHeight()); + System.out.println("Confirmed blockhash: " + confirmedBlockhash.getValue().getBlockhash() + ", Last valid block height: " + confirmedBlockhash.getValue().getLastValidBlockHeight()); + System.out.println("Finalized blockhash: " + finalizedBlockhash.getValue().getBlockhash() + ", Last valid block height: " + finalizedBlockhash.getValue().getLastValidBlockHeight()); + + assertTrue(confirmedBlockhash.getValue().getLastValidBlockHeight() >= finalizedBlockhash.getValue().getLastValidBlockHeight()); + } +} diff --git a/src/test/java/org/p2p/solanaj/core/LegacyMessageTest.java b/src/test/java/org/p2p/solanaj/core/LegacyMessageTest.java index b5a47ced..e6e43c07 100644 --- a/src/test/java/org/p2p/solanaj/core/LegacyMessageTest.java +++ b/src/test/java/org/p2p/solanaj/core/LegacyMessageTest.java @@ -1,11 +1,10 @@ package org.p2p.solanaj.core; import org.bitcoinj.core.Base58; -import org.junit.Test; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; import org.p2p.solanaj.programs.SystemProgram; -import static org.junit.Assert.assertArrayEquals; - public class LegacyMessageTest { @Test diff --git a/src/test/java/org/p2p/solanaj/core/LegacyTransactionTest.java b/src/test/java/org/p2p/solanaj/core/LegacyTransactionTest.java index 625605a5..4cf3f941 100644 --- a/src/test/java/org/p2p/solanaj/core/LegacyTransactionTest.java +++ b/src/test/java/org/p2p/solanaj/core/LegacyTransactionTest.java @@ -3,8 +3,8 @@ import org.p2p.solanaj.programs.MemoProgram; import org.p2p.solanaj.programs.SystemProgram; -import org.junit.Test; -import static org.junit.Assert.*; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; import java.util.Base64; import java.util.List; diff --git a/src/test/java/org/p2p/solanaj/core/MainnetTest.java b/src/test/java/org/p2p/solanaj/core/MainnetTest.java index 3a6828a1..0aa2cf6d 100644 --- a/src/test/java/org/p2p/solanaj/core/MainnetTest.java +++ b/src/test/java/org/p2p/solanaj/core/MainnetTest.java @@ -1,8 +1,10 @@ package org.p2p.solanaj.core; -import org.junit.Before; -import org.junit.Ignore; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Disabled; +import static org.junit.jupiter.api.Assertions.*; + import org.p2p.solanaj.programs.MemoProgram; import org.p2p.solanaj.rpc.Cluster; import org.p2p.solanaj.rpc.RpcClient; @@ -14,8 +16,6 @@ import java.util.*; -import static org.junit.Assert.*; - public class MainnetTest extends AccountBasedTest { private final RpcClient client = new RpcClient(Cluster.MAINNET); @@ -25,7 +25,7 @@ public class MainnetTest extends AccountBasedTest { private static final long LAMPORTS_PER_SOL = 1000000000L; - @Before + @BeforeEach public void beforeMethod() throws InterruptedException { // Prevent RPCPool rate limit Thread.sleep(200L); @@ -66,7 +66,7 @@ public void getAccountInfoRootCommitment() { final AccountInfo accountInfo = client.getApi().getAccountInfo(PublicKey.valueOf( "So11111111111111111111111111111111111111112"), Map.of("commitment", Commitment.ROOT)); final double balance = (double) accountInfo.getValue().getLamports() / LAMPORTS_PER_SOL; - + LOGGER.info("balance = " + balance); // Verify any balance assertTrue(balance > 0); } catch (RpcException e) { @@ -94,7 +94,7 @@ public void getAccountInfoJsonParsed() { * Calls sendLegacyTransaction with a call to the Memo program included. */ @Test - @Ignore + @Disabled public void transactionMemoTest() { final int lamports = 1111; final PublicKey destination = solDestination; @@ -272,7 +272,7 @@ public void getInflationGovernorTest() throws RpcException { } @Test - @Ignore + @Disabled public void getInflationRewardTest() throws RpcException { List inflationRewards = client.getApi().getInflationReward( Arrays.asList( @@ -323,6 +323,7 @@ public void getSlotLeadersTest() throws RpcException { } @Test + @Disabled public void getSnapshotSlotTest() throws RpcException { long snapshotSlot = client.getApi().getSnapshotSlot(); LOGGER.info(String.format("Snapshot slot = %d", snapshotSlot)); @@ -344,7 +345,7 @@ public void getIdentityTest() throws RpcException { } @Test - @Ignore + @Disabled public void getSupplyTest() throws RpcException { Supply supply = client.getApi().getSupply(); LOGGER.info(supply.toString()); @@ -397,7 +398,7 @@ public void getTokenSupplyTest() throws RpcException { } @Test - @Ignore + @Disabled public void getTokenLargestAccountsTest() throws RpcException { List tokenAccounts = client.getApi().getTokenLargestAccounts(PublicKey.valueOf( "4k3Dyjzvzp8eMZWUXbBCjEvwSkkk59S5iCNLY3QrkX6R")); @@ -424,7 +425,7 @@ public void getTokenAccountsByOwnerTest() throws RpcException { } @Test - @Ignore + @Disabled public void getTokenAccountsByDelegateTest() throws RpcException { Map requiredParams = Map.of("mint", USDC_TOKEN_MINT); TokenAccountInfo tokenAccount = client.getApi().getTokenAccountsByDelegate(PublicKey.valueOf( @@ -443,7 +444,7 @@ public void getTransactionCountTest() throws RpcException { } @Test - @Ignore + @Disabled public void getFeeCalculatorForBlockhashTest() throws RpcException, InterruptedException { String recentBlockHash = client.getApi().getRecentBlockhash(); Thread.sleep(20000L); @@ -455,6 +456,7 @@ public void getFeeCalculatorForBlockhashTest() throws RpcException, InterruptedE } @Test + @Disabled public void getFeesRateGovernorTest() throws RpcException { FeeRateGovernorInfo feeRateGovernorInfo = client.getApi().getFeeRateGovernor(); LOGGER.info(feeRateGovernorInfo.getValue().getFeeRateGovernor().toString()); @@ -468,6 +470,7 @@ public void getFeesRateGovernorTest() throws RpcException { } @Test + @Disabled public void getFeesInfoTest() throws RpcException { FeesInfo feesInfo = client.getApi().getFees(); LOGGER.info(feesInfo.toString()); @@ -485,7 +488,7 @@ public void getFeeForMessageTest() throws RpcException { } @Test - @Ignore + @Disabled public void getMaxRetransmitSlotTest() throws RpcException { long maxRetransmitSlot = client.getApi().getMaxRetransmitSlot(); LOGGER.info("maxRetransmitSlot = " + maxRetransmitSlot); @@ -507,13 +510,14 @@ public void getMinimumBalanceForRentExemptionTest() throws RpcException { } @Test + @Disabled public void getRecentBlockhashTest() throws RpcException { String recentBlockhash = client.getApi().getRecentBlockhash(); LOGGER.info(String.format("Recent blockhash = %s", recentBlockhash)); assertNotNull(recentBlockhash); } - @Ignore + @Disabled @Test public void getStakeActivationTest() throws RpcException { StakeActivation stakeActivation = client.getApi().getStakeActivation( @@ -529,7 +533,7 @@ public void getStakeActivationTest() throws RpcException { assertEquals(0, stakeActivation.getInactive()); } - @Ignore + @Disabled @Test public void simulateTransactionTest() throws RpcException { String transaction = "ASdDdWBaKXVRA+6flVFiZokic9gK0+r1JWgwGg/GJAkLSreYrGF4rbTCXNJvyut6K6hupJtm72GztLbWNmRF1Q4BAAEDBhrZ0FOHFUhTft4+JhhJo9+3/QL6vHWyI8jkatuFPQzrerzQ2HXrwm2hsYGjM5s+8qMWlbt6vbxngnO8rc3lqgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAy+KIwZmU8DLmYglP3bPzrlpDaKkGu6VIJJwTOYQmRfUBAgIAAQwCAAAAuAsAAAAAAAA="; @@ -539,7 +543,7 @@ public void simulateTransactionTest() throws RpcException { } @Test - @Ignore + @Disabled public void sendTokenTest() { final PublicKey source = usdcSource; // Private key's USDC token account final PublicKey destination = usdcDestination; // Destination's USDC account @@ -561,7 +565,7 @@ public void sendTokenTest() { } @Test - @Ignore + @Disabled public void transferCheckedTest() { final PublicKey source = usdcSource; // Private key's USDC token account final PublicKey destination = solDestination; @@ -592,7 +596,7 @@ public void transferCheckedTest() { } @Test - @Ignore + @Disabled public void initializeAccountTest() { final Account owner = testAccount; final Account newAccount = new Account(); @@ -608,14 +612,14 @@ public void initializeAccountTest() { } @Test - @Ignore + @Disabled public void getConfirmedBlockTest() throws RpcException { ConfirmedBlock block = this.client.getApi().getConfirmedBlock(124398367); assertEquals(124398367, block.getParentSlot()); } @Test - @Ignore + @Disabled public void getSignaturesForAddressTest() throws RpcException { List confirmedSignatures = client.getApi().getSignaturesForAddress( solDestination, @@ -628,7 +632,7 @@ public void getSignaturesForAddressTest() throws RpcException { } @Test - @Ignore + @Disabled public void getBlockTest() throws RpcException { Block block = this.client.getApi().getBlock(124398367); assertEquals(112516757, block.getBlockHeight()); @@ -641,7 +645,7 @@ public void getBlockTest() throws RpcException { // Ignored since some validators can only get recent blocks @Test - @Ignore + @Disabled public void getConfirmedBlocksTest() throws RpcException { List blocks = this.client.getApi().getConfirmedBlocks(5); List singleBlock = this.client.getApi().getConfirmedBlocks(5, 5); @@ -656,7 +660,7 @@ public void getVoteAccountsTest() throws RpcException { } @Test - @Ignore + @Disabled public void getSignatureStatusesTest() throws RpcException { SignatureStatuses signatureStatuses = client.getApi().getSignatureStatuses( List.of( @@ -678,7 +682,7 @@ public void getRecentPerformanceSamplesLimitTest() throws RpcException { } @Test - @Ignore + @Disabled public void getHealthTest() throws RpcException { boolean isHealthy = client.getApi().getHealth(); @@ -686,7 +690,7 @@ public void getHealthTest() throws RpcException { } @Test - @Ignore + @Disabled public void getLargestAccountsTest() throws RpcException { List largeAccounts = client.getApi().getLargestAccounts(); @@ -702,7 +706,7 @@ public void getLeaderScheduleTest() throws RpcException { } @Test - @Ignore + @Disabled public void getLeaderScheduleTest_identity() throws RpcException { List leaderSchedules = client.getApi().getLeaderSchedule(null, "12oRmi8YDbqpkn326MdjwFeZ1bh3t7zVw8Nra2QK2SnR", null); @@ -778,6 +782,7 @@ public void getTransactionTest() throws RpcException { } @Test + @Disabled public void isBlockhashValidTest() throws RpcException, InterruptedException { String recentBlockHash = client.getApi().getRecentBlockhash(); Thread.sleep(500L); @@ -795,4 +800,75 @@ public void rentEpochTest() { throw new RuntimeException(e); } } + + @Test + public void testGetStakeMinimumDelegationWithoutCommitment() throws RpcException { + Long minDelegation = client.getApi().getStakeMinimumDelegation(); + assertNotNull(minDelegation); + assertTrue(minDelegation > 0); + LOGGER.info("Minimum stake delegation (without commitment): " + minDelegation); + } + + /** + * Test getStakeMinimumDelegation with commitment + */ + @Test + public void testGetStakeMinimumDelegationWithCommitment() throws RpcException { + Long minDelegation = client.getApi().getStakeMinimumDelegation(Commitment.FINALIZED); + assertNotNull(minDelegation); + assertTrue(minDelegation > 0); + LOGGER.info("Minimum stake delegation (with FINALIZED commitment): " + minDelegation); + } + + @Test + public void testGetBlocks() throws RpcException { + long startSlot = client.getApi().getSlot() - 10; // 10 slots before the current slot + long endSlot = client.getApi().getSlot(); + + List blocks = client.getApi().getBlocks(startSlot, endSlot); + + assertNotNull(blocks); + assertFalse(blocks.isEmpty()); + assertTrue(blocks.get(0) >= startSlot); + assertTrue(blocks.get(blocks.size() - 1) <= endSlot); + } + + @Test + public void testGetBlocksWithCommitment() throws RpcException { + long startSlot = client.getApi().getSlot() - 10; // 10 slots before the current slot + long endSlot = client.getApi().getSlot(); + + List blocks = client.getApi().getBlocks(startSlot, endSlot, Commitment.CONFIRMED); + + assertNotNull(blocks); + assertFalse(blocks.isEmpty()); + assertTrue(blocks.get(0) >= startSlot); + assertTrue(blocks.get(blocks.size() - 1) <= endSlot); + } + + @Test + public void testGetBlocksWithLimit() throws RpcException { + long startSlot = client.getApi().getSlot() - 10; // 10 slots before the current slot + long limit = 5; + + List blocks = client.getApi().getBlocksWithLimit(startSlot, limit); + + assertNotNull(blocks); + assertFalse(blocks.isEmpty()); + assertTrue(blocks.get(0) >= startSlot); + assertTrue(blocks.size() <= limit); + } + + @Test + public void testGetBlocksWithLimitAndCommitment() throws RpcException { + long startSlot = client.getApi().getSlot() - 10; // 10 slots before the current slot + long limit = 5; + + List blocks = client.getApi().getBlocksWithLimit(startSlot, limit, Commitment.CONFIRMED); + + assertNotNull(blocks); + assertFalse(blocks.isEmpty()); + assertTrue(blocks.get(0) >= startSlot); + assertTrue(blocks.size() <= limit); + } } \ No newline at end of file diff --git a/src/test/java/org/p2p/solanaj/core/PublicKeyTest.java b/src/test/java/org/p2p/solanaj/core/PublicKeyTest.java index c71f7622..2f66fbf3 100644 --- a/src/test/java/org/p2p/solanaj/core/PublicKeyTest.java +++ b/src/test/java/org/p2p/solanaj/core/PublicKeyTest.java @@ -1,21 +1,22 @@ package org.p2p.solanaj.core; -import org.junit.Test; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; import org.p2p.solanaj.core.PublicKey.ProgramDerivedAddress; -import static org.junit.Assert.*; - import java.io.ByteArrayOutputStream; import java.util.Arrays; public class PublicKeyTest { - @Test(expected = IllegalArgumentException.class) - public void ivalidKeys() { - new PublicKey(new byte[] { 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0 }); - new PublicKey("300000000000000000000000000000000000000000000000000000000000000000000"); - new PublicKey("300000000000000000000000000000000000000000000000000000000000000"); + @Test + public void testInvalidKeys() { + assertThrows(IllegalArgumentException.class, () -> { + new PublicKey(new byte[] { 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0 }); + new PublicKey("300000000000000000000000000000000000000000000000000000000000000000000"); + new PublicKey("300000000000000000000000000000000000000000000000000000000000000"); + }); } @Test @@ -107,4 +108,35 @@ public void findProgramAddress1() throws Exception { assertEquals(programAddress2.getNonce(), 254); } + @Test + public void testToString() { + PublicKey key = new PublicKey("CiDwVBFgWV9E5MvXWoLgnEgn2hK7rJikbvfWavzAQz3"); + assertEquals("CiDwVBFgWV9E5MvXWoLgnEgn2hK7rJikbvfWavzAQz3", key.toString()); + } + + @Test + public void testHashCode() { + PublicKey key1 = new PublicKey("11111111111111111111111111111111"); + PublicKey key2 = new PublicKey("11111111111111111111111111111111"); + PublicKey key3 = new PublicKey("22222222222222222222222222222222"); + + assertEquals(key1.hashCode(), key2.hashCode()); + assertNotEquals(key1.hashCode(), key3.hashCode()); + } + + @Test + public void testInvalidBase58Key() { + assertThrows(IllegalArgumentException.class, () -> new PublicKey("InvalidBase58Key")); + } + + @Test + public void testFindProgramAddressWithLargeNonce() throws Exception { + PublicKey programId = new PublicKey("BPFLoader1111111111111111111111111111111111"); + ProgramDerivedAddress pda = PublicKey.findProgramAddress( + Arrays.asList("LargeNonceTest".getBytes()), + programId + ); + assertTrue(pda.getNonce() >= 0 && pda.getNonce() <= 255); + } + } diff --git a/src/test/java/org/p2p/solanaj/core/RpcClientTest.java b/src/test/java/org/p2p/solanaj/core/RpcClientTest.java index b7efd0da..74e6a9b9 100644 --- a/src/test/java/org/p2p/solanaj/core/RpcClientTest.java +++ b/src/test/java/org/p2p/solanaj/core/RpcClientTest.java @@ -1,7 +1,7 @@ package org.p2p.solanaj.core; -import org.junit.Assert; -import org.junit.Test; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; import org.p2p.solanaj.rpc.RpcClient; import org.p2p.solanaj.rpc.WeightedCluster; import org.p2p.solanaj.rpc.types.WeightedEndpoint; @@ -44,10 +44,10 @@ public void WeightRpcClientTest() { float endpoint1Percentage = (float)endpoint1Occurence / (float)WEIGHTED_TEST_OCCURRENCE; float endpoint2Percentage = (float)endpoint2Occurence / (float)WEIGHTED_TEST_OCCURRENCE; float endpoint3Percentage = (float)endpoint3Occurence / (float)WEIGHTED_TEST_OCCURRENCE; - Assert.assertEquals(0.1f, endpoint0Percentage, 0.03); - Assert.assertEquals(0.2f, endpoint1Percentage, 0.03); - Assert.assertEquals(0.3f, endpoint2Percentage, 0.03); - Assert.assertEquals(0.4f, endpoint3Percentage, 0.03); + assertEquals(0.1f, endpoint0Percentage, 0.03); + assertEquals(0.2f, endpoint1Percentage, 0.03); + assertEquals(0.3f, endpoint2Percentage, 0.03); + assertEquals(0.4f, endpoint3Percentage, 0.03); } } diff --git a/src/test/java/org/p2p/solanaj/core/WebsocketTest.java b/src/test/java/org/p2p/solanaj/core/WebsocketTest.java index 301e9994..6a1a8253 100644 --- a/src/test/java/org/p2p/solanaj/core/WebsocketTest.java +++ b/src/test/java/org/p2p/solanaj/core/WebsocketTest.java @@ -1,51 +1,269 @@ package org.p2p.solanaj.core; -import org.junit.Ignore; -import org.junit.Test; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Disabled; +import static org.junit.jupiter.api.Assertions.*; import org.p2p.solanaj.rpc.Cluster; import org.p2p.solanaj.ws.SubscriptionWebSocketClient; +import org.p2p.solanaj.ws.listeners.NotificationEventListener; -import java.util.List; -import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; import java.util.logging.Logger; +import java.util.Map; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeoutException; -import static org.junit.Assert.assertTrue; - +/** + * Test class for WebSocket functionality in the Solana Java client. + */ +@Disabled public class WebsocketTest { - private final SubscriptionWebSocketClient devnetClient = SubscriptionWebSocketClient.getInstance( - Cluster.DEVNET.getEndpoint() - ); private static final Logger LOGGER = Logger.getLogger(WebsocketTest.class.getName()); + private static final long TEST_TIMEOUT_MS = 180000; // 3 minutes + + private static final String TEST_ACCOUNT = "SysvarRecentB1ockHashes11111111111111111111"; + private static final String SYSVAR_CLOCK = "SysvarC1ock11111111111111111111111111111111"; + private static final long CONNECTION_TIMEOUT = 10; + private static final long NOTIFICATION_TIMEOUT = 120; + + private SubscriptionWebSocketClient createClient() { + URI serverURI; + try { + serverURI = new URI(Cluster.MAINNET.getEndpoint().replace("https", "wss")); + } catch (URISyntaxException e) { + throw new RuntimeException("Invalid URI", e); + } + return SubscriptionWebSocketClient.getInstance(serverURI.toString()); + } + + /** + * Test account subscription functionality. + * This test subscribes to an account and waits for a notification. + * + * @throws Exception if any error occurs during the test + */ + @Test + public void testAccountSubscribe() throws Exception { + SubscriptionWebSocketClient client = null; + ExecutorService executor = Executors.newSingleThreadExecutor(); + Future future = null; + + try { + future = executor.submit(() -> { + try { + runAccountSubscribeTest(); + } catch (Exception e) { + LOGGER.severe("Error in test execution: " + e.getMessage()); + throw new RuntimeException(e); + } + }); + + future.get(TEST_TIMEOUT_MS, TimeUnit.MILLISECONDS); + } catch (TimeoutException e) { + LOGGER.severe("Test timed out after " + TEST_TIMEOUT_MS + " ms"); + if (future != null) { + future.cancel(true); + } + fail("Test timed out"); + } finally { + executor.shutdownNow(); + if (client != null) { + client.close(); + } + } + } + + private void runAccountSubscribeTest() throws Exception { + SubscriptionWebSocketClient client = null; + try { + LOGGER.info("Creating WebSocket client"); + client = createClient(); + + LOGGER.info("Waiting for WebSocket connection"); + if (!client.waitForConnection(CONNECTION_TIMEOUT, TimeUnit.SECONDS)) { + throw new RuntimeException("Failed to establish WebSocket connection"); + } + LOGGER.info("WebSocket connection established"); + + CountDownLatch latch = new CountDownLatch(1); + AtomicReference> resultRef = new AtomicReference<>(); + + LOGGER.info("Subscribing to account: " + TEST_ACCOUNT); + client.accountSubscribe(TEST_ACCOUNT, (NotificationEventListener) data -> { + LOGGER.info("Received notification: " + data); + resultRef.set((Map) data); + latch.countDown(); + }); + + LOGGER.info("Waiting for notification"); + if (!latch.await(NOTIFICATION_TIMEOUT, TimeUnit.SECONDS)) { + throw new RuntimeException("Test timed out waiting for notification from " + TEST_ACCOUNT); + } + + Map result = resultRef.get(); + if (result == null) { + throw new RuntimeException("Notification should not be null"); + } + + LOGGER.info("Received result structure: " + result); + + validateAccountData(result); + LOGGER.info("Test completed successfully"); + } finally { + if (client != null) { + LOGGER.info("Closing WebSocket client"); + client.close(); + } + } + } @Test - @Ignore - public void pythWebsocketTest() { - devnetClient.accountSubscribe( - PublicKey.valueOf("E36MyBbavhYKHVLWR79GiReNNnBDiHj6nWA7htbkNZbh").toBase58(), - data -> { - Map map = (Map) data; - String base64 = (String)((List) map.get("data")).get(0); - LOGGER.info( - String.format( - "Event = %s", - map - ) - ); - LOGGER.info( - String.format( - "Data = %s", - base64 - ) - ); + public void testMultipleSubscriptions() throws Exception { + SubscriptionWebSocketClient client = null; + CountDownLatch latch = new CountDownLatch(2); + try { + client = createClient(); + if (!client.waitForConnection(CONNECTION_TIMEOUT, TimeUnit.SECONDS)) { + fail("Failed to establish WebSocket connection"); + } + + CompletableFuture> future1 = new CompletableFuture<>(); + CompletableFuture> future2 = new CompletableFuture<>(); + + LOGGER.info("Starting multiple subscriptions test"); + + NotificationEventListener listener1 = data -> { + LOGGER.info("Received notification for subscription 1 (TEST_ACCOUNT): " + data); + if (!future1.isDone()) { + future1.complete((Map) data); + latch.countDown(); } - ); + }; + NotificationEventListener listener2 = data -> { + LOGGER.info("Received notification for subscription 2 (SYSVAR_CLOCK): " + data); + if (!future2.isDone()) { + future2.complete((Map) data); + latch.countDown(); + } + }; + + LOGGER.info("Subscribing to TEST_ACCOUNT (1st subscription)"); + client.accountSubscribe(TEST_ACCOUNT, listener1); + LOGGER.info("Subscribing to SYSVAR_CLOCK (2nd subscription)"); + client.accountSubscribe(SYSVAR_CLOCK, listener2); + + LOGGER.info("Waiting for notifications..."); + if (!latch.await(NOTIFICATION_TIMEOUT, TimeUnit.SECONDS)) { + LOGGER.warning("Test timed out waiting for notifications"); + if (!future1.isDone()) { + fail("Timed out waiting for notification from " + TEST_ACCOUNT); + } else { + fail("Timed out waiting for notification from " + SYSVAR_CLOCK); + } + } + + LOGGER.info("Latch count reached zero, proceeding with assertions"); + + Map result1 = future1.get(5, TimeUnit.SECONDS); + Map result2 = future2.get(5, TimeUnit.SECONDS); + + assertNotNull(result1, "Notification 1 should not be null"); + assertNotNull(result2, "Notification 2 should not be null"); + + LOGGER.info("Received data for subscription 1 (TEST_ACCOUNT): " + result1); + LOGGER.info("Received data for subscription 2 (SYSVAR_CLOCK): " + result2); + + validateAccountData(result1); + validateAccountData(result2); + } catch (Exception e) { + LOGGER.severe("Error occurred: " + e.getMessage()); + LOGGER.info("Latch count at error: " + latch.getCount()); + fail("Test failed: " + e.getMessage()); + } finally { + if (client != null) { + LOGGER.info("Closing WebSocket connection"); + client.close(); + } + } + } + + @Test + public void testAccountUnsubscribe() throws Exception { + SubscriptionWebSocketClient client = null; try { - Thread.sleep(120000L); - } catch (InterruptedException e) { - e.printStackTrace(); + client = createClient(); + if (!client.waitForConnection(CONNECTION_TIMEOUT, TimeUnit.SECONDS)) { + fail("Failed to establish WebSocket connection"); + } + + CountDownLatch subscribeLatch = new CountDownLatch(1); + CountDownLatch unsubscribeLatch = new CountDownLatch(1); + AtomicReference subscriptionId = new AtomicReference<>(); + AtomicInteger notificationCount = new AtomicInteger(0); + + NotificationEventListener listener = data -> { + LOGGER.info("Received notification: " + data); + notificationCount.incrementAndGet(); + if (subscribeLatch.getCount() > 0) { + subscribeLatch.countDown(); + } + }; + + LOGGER.info("Subscribing to TEST_ACCOUNT"); + client.accountSubscribe(TEST_ACCOUNT, listener); + + if (!subscribeLatch.await(NOTIFICATION_TIMEOUT, TimeUnit.SECONDS)) { + fail("Timed out waiting for initial notification"); + } + + // Wait for a short time to potentially receive more notifications + Thread.sleep(5000); + + int initialNotifications = notificationCount.get(); + LOGGER.info("Received " + initialNotifications + " notifications before unsubscribing"); + + // Unsubscribe + subscriptionId.set(client.getSubscriptionId(TEST_ACCOUNT)); + assertNotNull("Subscription ID should not be null", subscriptionId.get()); + LOGGER.info("Unsubscribing from subscription ID: " + subscriptionId.get()); + client.unsubscribe(subscriptionId.get()); + + // Wait for a short time after unsubscribing + Thread.sleep(5000); + + int finalNotifications = notificationCount.get(); + LOGGER.info("Received " + finalNotifications + " notifications after unsubscribing"); + + // Check that we didn't receive any new notifications after unsubscribing + assertEquals(initialNotifications, finalNotifications, + "Should not receive new notifications after unsubscribing"); + + // Try to unsubscribe again (should not throw an exception) + client.unsubscribe(subscriptionId.get()); + + LOGGER.info("Unsubscribe test completed successfully"); + } finally { + if (client != null) { + client.close(); + } } - assertTrue(true); + } + + private void validateAccountData(Map data) { + // Implement proper validation logic here + assertNotNull(data, "Account data should not be null"); + assertTrue(data.containsKey("lamports"), "Account data should contain 'lamports'"); + assertTrue(data.containsKey("data"), "Account data should contain 'data'"); + // Add more specific validations as needed } } diff --git a/src/test/java/org/p2p/solanaj/manager/TokenManagerTest.java b/src/test/java/org/p2p/solanaj/manager/TokenManagerTest.java new file mode 100644 index 00000000..894d9d80 --- /dev/null +++ b/src/test/java/org/p2p/solanaj/manager/TokenManagerTest.java @@ -0,0 +1,193 @@ +package org.p2p.solanaj.manager; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +import org.mockito.Mockito; +import org.p2p.solanaj.core.Account; +import org.p2p.solanaj.core.PublicKey; +import org.p2p.solanaj.core.LegacyTransaction; +import org.p2p.solanaj.programs.SystemProgram; +import org.p2p.solanaj.rpc.RpcClient; +import org.p2p.solanaj.rpc.RpcApi; +import org.p2p.solanaj.rpc.RpcException; +import org.p2p.solanaj.token.TokenManager; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +/** + * Unit tests for the TokenManager class, which handles token-related operations + * in the Solana blockchain. + */ +public class TokenManagerTest { + + private RpcClient mockRpcClient; + private RpcApi mockRpcApi; + private TokenManager tokenManager; + private Account owner; + private PublicKey source; + private PublicKey destination; + private PublicKey tokenMint; + + /** + * Sets up the test environment before each test case. + * Initializes mock objects and the TokenManager instance. + */ + @BeforeEach + public void setUp() { + mockRpcClient = Mockito.mock(RpcClient.class); + mockRpcApi = Mockito.mock(RpcApi.class); + when(mockRpcClient.getApi()).thenReturn(mockRpcApi); + tokenManager = new TokenManager(mockRpcClient); + + owner = new Account(); + source = new PublicKey("DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263"); + destination = new PublicKey("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"); + tokenMint = new PublicKey("So11111111111111111111111111111111111111112"); + } + + /** + * Tests the transfer method of the TokenManager. + * Verifies that the transaction ID returned matches the expected ID. + * + * @throws RpcException if there is an error during the RPC call. + */ + @Test + public void testTransfer() throws RpcException { + long amount = 1000L; + String expectedTxId = "MockTransactionId"; + + when(mockRpcApi.sendLegacyTransaction(any(LegacyTransaction.class), eq(owner))).thenReturn(expectedTxId); + + String result = tokenManager.transfer(owner, source, destination, tokenMint, amount); + + assertEquals(expectedTxId, result); + verify(mockRpcApi).sendLegacyTransaction(any(LegacyTransaction.class), eq(owner)); + } + + /** + * Tests the transferCheckedToSolAddress method of the TokenManager. + * Verifies that the transaction ID returned matches the expected ID. + * + * @throws RpcException if there is an error during the RPC call. + */ + @Test + public void testTransferCheckedToSolAddress() throws RpcException { + long amount = 1000L; + byte decimals = 9; + PublicKey destinationATA = new PublicKey("5h3Q4g1g1g1g1g1g1g1g1g1g1g1g1g1g1g1g1g1g1"); // Example destination ATA + String expectedTxId = "MockTransactionId"; + + when(mockRpcApi.getTokenAccountsByOwner(eq(destination), eq(tokenMint))).thenReturn(destinationATA); + when(mockRpcApi.sendLegacyTransaction(any(LegacyTransaction.class), eq(owner))).thenReturn(expectedTxId); + + String result = tokenManager.transferCheckedToSolAddress(owner, source, destination, tokenMint, amount, decimals); + + assertEquals(expectedTxId, result); + verify(mockRpcApi).getTokenAccountsByOwner(eq(destination), eq(tokenMint)); + verify(mockRpcApi).sendLegacyTransaction(any(LegacyTransaction.class), eq(owner)); + } + + /** + * Tests the initializeAccount method of the TokenManager. + * Verifies that the transaction ID returned matches the expected ID. + * + * @throws RpcException if there is an error during the RPC call. + */ + @Test + public void testInitializeAccount() throws RpcException { + Account newAccount = new Account(); + PublicKey usdcTokenMint = new PublicKey("A4k3Dyjzvzp8e1Z1g1g1g1g1g1g1g1g1g1g1g1g1g1"); // Example USDC mint + String expectedTxId = "MockTransactionId"; + + when(mockRpcApi.sendLegacyTransaction(any(LegacyTransaction.class), eq(owner))).thenReturn(expectedTxId); + + String result = tokenManager.initializeAccount(newAccount, usdcTokenMint, owner); + + assertEquals(expectedTxId, result); + verify(mockRpcApi).sendLegacyTransaction(any(LegacyTransaction.class), eq(owner)); + } + + /** + * Tests the transferCheckedToSolAddress method for transferring arbitrary tokens. + * Verifies that the transaction ID returned matches the expected ID. + * + * @throws RpcException if there is an error during the RPC call. + */ + @Test + public void testTransferArbitraryToken() throws RpcException { + // Example for transferring BONK tokens + PublicKey bonkMint = new PublicKey("DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263"); + long amount = 1_000_000L; // 1 BONK (assuming 5 decimals) + byte decimals = 5; + + PublicKey sourceATA = new PublicKey("J3dxNj7nDRRqRRXuEMynDG57DkZK4jYRuv3Garmb1i99"); + PublicKey destinationATA = new PublicKey("AyGCwnwxQMCqaU4ixReHt8h5W4dwmxU7eM3BEQBdWVca"); + String expectedTxId = "MockTransactionId"; + + when(mockRpcApi.getTokenAccountsByOwner(eq(destination), eq(bonkMint))).thenReturn(destinationATA); + when(mockRpcApi.sendLegacyTransaction(any(LegacyTransaction.class), eq(owner))).thenReturn(expectedTxId); + + String result = tokenManager.transferCheckedToSolAddress(owner, sourceATA, destination, bonkMint, amount, decimals); + + assertEquals(expectedTxId, result); + verify(mockRpcApi).getTokenAccountsByOwner(eq(destination), eq(bonkMint)); + verify(mockRpcApi).sendLegacyTransaction(any(LegacyTransaction.class), eq(owner)); + } + + /** + * Tests the transfer method for transferring native SOL. + * Verifies that the transaction ID returned matches the expected ID. + * + * @throws RpcException if there is an error during the RPC call. + */ + @Test + public void testTransferSOL() throws RpcException { + // For transferring native SOL, we use SystemProgram instead of TokenProgram + PublicKey recipient = new PublicKey("3n1k1g1g1g1g1g1g1g1g1g1g1g1g1g1g1g1g1g1g1"); // Example recipient address + long amount = 1_000_000_000L; // 1 SOL (lamports) + String expectedTxId = "MockTransactionId"; + + when(mockRpcApi.sendLegacyTransaction(any(LegacyTransaction.class), eq(owner))).thenReturn(expectedTxId); + + LegacyTransaction transaction = new LegacyTransaction(); + transaction.addInstruction( + SystemProgram.transfer(owner.getPublicKey(), recipient, amount) + ); + + String result = mockRpcApi.sendLegacyTransaction(transaction, owner); + + assertEquals(expectedTxId, result); + verify(mockRpcApi).sendLegacyTransaction(any(LegacyTransaction.class), eq(owner)); + } + + /** + * Tests the transferCheckedToSolAddress method for transferring Wrapped SOL (WSOL). + * Verifies that the transaction ID returned matches the expected ID. + * + * @throws RpcException if there is an error during the RPC call. + */ + @Test + public void testTransferWSOL() throws RpcException { + // For transferring Wrapped SOL (WSOL) + PublicKey wsolMint = new PublicKey("So11111111111111111111111111111111111111112"); + long amount = 1_000_000_000L; // 1 WSOL (9 decimals) + byte decimals = 9; + + PublicKey sourceWSOLATA = new PublicKey("J3dxNj7nDRRqRRXuEMynDG57DkZK4jYRuv3Garmb1i99"); + PublicKey destinationWSOLATA = new PublicKey("AyGCwnwxQMCqaU4ixReHt8h5W4dwmxU7eM3BEQBdWVca"); + String expectedTxId = "MockTransactionId"; + + when(mockRpcApi.getTokenAccountsByOwner(eq(destination), eq(wsolMint))).thenReturn(destinationWSOLATA); + when(mockRpcApi.sendLegacyTransaction(any(LegacyTransaction.class), eq(owner))).thenReturn(expectedTxId); + + String result = tokenManager.transferCheckedToSolAddress(owner, sourceWSOLATA, destination, wsolMint, amount, decimals); + + assertEquals(expectedTxId, result); + verify(mockRpcApi).getTokenAccountsByOwner(eq(destination), eq(wsolMint)); + verify(mockRpcApi).sendLegacyTransaction(any(LegacyTransaction.class), eq(owner)); + } +} \ No newline at end of file diff --git a/src/test/java/org/p2p/solanaj/programs/AddressLookupTableProgramTest.java b/src/test/java/org/p2p/solanaj/programs/AddressLookupTableProgramTest.java new file mode 100644 index 00000000..75871ffd --- /dev/null +++ b/src/test/java/org/p2p/solanaj/programs/AddressLookupTableProgramTest.java @@ -0,0 +1,74 @@ +package org.p2p.solanaj.programs; + +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +import org.p2p.solanaj.core.PublicKey; +import org.p2p.solanaj.core.TransactionInstruction; + +import java.util.Collections; + +public class AddressLookupTableProgramTest { + + private static final PublicKey AUTHORITY = new PublicKey("BPFLoaderUpgradeab1e11111111111111111111111"); + private static final PublicKey PAYER = new PublicKey("11111111111111111111111111111111"); + private static final PublicKey LOOKUP_TABLE = new PublicKey("AddressLookupTab1e1111111111111111111111111"); + private static final long RECENT_SLOT = 123456; + + /** + * Test for creating a lookup table. + */ + @Test + public void testCreateLookupTable() { + TransactionInstruction instruction = AddressLookupTableProgram.createLookupTable(AUTHORITY, PAYER, RECENT_SLOT); + assertNotNull(instruction); + assertEquals(AddressLookupTableProgram.PROGRAM_ID, instruction.getProgramId()); + assertEquals(4, instruction.getKeys().size()); // Check number of keys + } + + /** + * Test for freezing a lookup table. + */ + @Test + public void testFreezeLookupTable() { + TransactionInstruction instruction = AddressLookupTableProgram.freezeLookupTable(LOOKUP_TABLE, AUTHORITY); + assertNotNull(instruction); + assertEquals(AddressLookupTableProgram.PROGRAM_ID, instruction.getProgramId()); + assertEquals(2, instruction.getKeys().size()); // Check number of keys + } + + /** + * Test for extending a lookup table. + */ + @Test + public void testExtendLookupTable() { + PublicKey addressToAdd = new PublicKey("SysvarC1ock11111111111111111111111111111111"); + TransactionInstruction instruction = AddressLookupTableProgram.extendLookupTable(LOOKUP_TABLE, AUTHORITY, PAYER, Collections.singletonList(addressToAdd)); + assertNotNull(instruction); + assertEquals(AddressLookupTableProgram.PROGRAM_ID, instruction.getProgramId()); + assertEquals(4, instruction.getKeys().size()); // Check number of keys + } + + /** + * Test for deactivating a lookup table. + */ + @Test + public void testDeactivateLookupTable() { + TransactionInstruction instruction = AddressLookupTableProgram.deactivateLookupTable(LOOKUP_TABLE, AUTHORITY); + assertNotNull(instruction); + assertEquals(AddressLookupTableProgram.PROGRAM_ID, instruction.getProgramId()); + assertEquals(2, instruction.getKeys().size()); // Check number of keys + } + + /** + * Test for closing a lookup table. + */ + @Test + public void testCloseLookupTable() { + PublicKey recipient = new PublicKey("SysvarRent111111111111111111111111111111111"); + TransactionInstruction instruction = AddressLookupTableProgram.closeLookupTable(LOOKUP_TABLE, AUTHORITY, recipient); + assertNotNull(instruction); + assertEquals(AddressLookupTableProgram.PROGRAM_ID, instruction.getProgramId()); + assertEquals(3, instruction.getKeys().size()); // Check number of keys + } +} \ No newline at end of file diff --git a/src/test/java/org/p2p/solanaj/programs/AssociatedTokenProgramTest.java b/src/test/java/org/p2p/solanaj/programs/AssociatedTokenProgramTest.java new file mode 100644 index 00000000..1a88f8a9 --- /dev/null +++ b/src/test/java/org/p2p/solanaj/programs/AssociatedTokenProgramTest.java @@ -0,0 +1,93 @@ +package org.p2p.solanaj.programs; + +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +import org.p2p.solanaj.core.AccountMeta; +import org.p2p.solanaj.core.PublicKey; +import org.p2p.solanaj.core.TransactionInstruction; + +import java.util.List; + +public class AssociatedTokenProgramTest { + + // Using real Solana addresses + private static final PublicKey FUNDING_ACCOUNT = new PublicKey("Gh9ZwEmdLJ8DscKNTkTqPbNwLNNBjuSzaG9Vp2KGtKJr"); + private static final PublicKey WALLET_ADDRESS = new PublicKey("6sbzC1eH4FTujJXWj51eQe25cYvr4xfXbJ1vAj7j2k5J"); + private static final PublicKey MINT = new PublicKey("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"); // USDC mint + private static final PublicKey NESTED_ACCOUNT = new PublicKey("4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU"); + private static final PublicKey NESTED_MINT = new PublicKey("So11111111111111111111111111111111111111112"); // Wrapped SOL mint + private static final PublicKey DESTINATION_ACCOUNT = new PublicKey("5omQJtDUHA3gMFdHEQg1zZSvcBUVzey5WaKWYRmqF1Vj"); + private static final PublicKey OWNER_ACCOUNT = new PublicKey("7UX2i7SucgLMQcfZ75s3VXmZZY4YRUyJN9X1RgfMoDUi"); + private static final PublicKey OWNER_MINT = new PublicKey("mSoLzYCxHdYgdzU16g5QSh3i5K3z3KZK7ytfqcJm7So"); // Marinade staked SOL mint + + @Test + public void testCreate() { + TransactionInstruction instruction = AssociatedTokenProgram.create(FUNDING_ACCOUNT, WALLET_ADDRESS, MINT); + + assertEquals(AssociatedTokenProgram.PROGRAM_ID, instruction.getProgramId()); + assertEquals(6, instruction.getKeys().size()); + assertEquals(1, instruction.getData().length); + assertEquals(0, instruction.getData()[0]); + + verifyCommonAccountMetas(instruction.getKeys()); + } + + @Test + public void testCreateIdempotent() { + TransactionInstruction instruction = AssociatedTokenProgram.createIdempotent(FUNDING_ACCOUNT, WALLET_ADDRESS, MINT); + + assertEquals(AssociatedTokenProgram.PROGRAM_ID, instruction.getProgramId()); + assertEquals(6, instruction.getKeys().size()); + assertEquals(1, instruction.getData().length); + assertEquals(1, instruction.getData()[0]); + + verifyCommonAccountMetas(instruction.getKeys()); + } + + @Test + public void testRecoverNested() { + TransactionInstruction instruction = AssociatedTokenProgram.recoverNested( + NESTED_ACCOUNT, NESTED_MINT, DESTINATION_ACCOUNT, OWNER_ACCOUNT, OWNER_MINT, WALLET_ADDRESS); + + assertEquals(AssociatedTokenProgram.PROGRAM_ID, instruction.getProgramId()); + assertEquals(7, instruction.getKeys().size()); + assertEquals(1, instruction.getData().length); + assertEquals(2, instruction.getData()[0]); + + List keys = instruction.getKeys(); + assertEquals(NESTED_ACCOUNT, keys.get(0).getPublicKey()); + assertEquals(NESTED_MINT, keys.get(1).getPublicKey()); + assertEquals(DESTINATION_ACCOUNT, keys.get(2).getPublicKey()); + assertEquals(OWNER_ACCOUNT, keys.get(3).getPublicKey()); + assertEquals(OWNER_MINT, keys.get(4).getPublicKey()); + assertEquals(WALLET_ADDRESS, keys.get(5).getPublicKey()); + assertEquals(TokenProgram.PROGRAM_ID, keys.get(6).getPublicKey()); + } + + private void verifyCommonAccountMetas(List keys) { + assertEquals(FUNDING_ACCOUNT, keys.get(0).getPublicKey()); + assertTrue(keys.get(0).isSigner()); + assertTrue(keys.get(0).isWritable()); + + // PDA account + assertFalse(keys.get(1).isSigner()); + assertTrue(keys.get(1).isWritable()); + + assertEquals(WALLET_ADDRESS, keys.get(2).getPublicKey()); + assertFalse(keys.get(2).isSigner()); + assertFalse(keys.get(2).isWritable()); + + assertEquals(MINT, keys.get(3).getPublicKey()); + assertFalse(keys.get(3).isSigner()); + assertFalse(keys.get(3).isWritable()); + + assertEquals(SystemProgram.PROGRAM_ID, keys.get(4).getPublicKey()); + assertFalse(keys.get(4).isSigner()); + assertFalse(keys.get(4).isWritable()); + + assertEquals(TokenProgram.PROGRAM_ID, keys.get(5).getPublicKey()); + assertFalse(keys.get(5).isSigner()); + assertFalse(keys.get(5).isWritable()); + } +} \ No newline at end of file diff --git a/src/test/java/org/p2p/solanaj/programs/BPFLoaderTest.java b/src/test/java/org/p2p/solanaj/programs/BPFLoaderTest.java index ab68a18a..17465e82 100644 --- a/src/test/java/org/p2p/solanaj/programs/BPFLoaderTest.java +++ b/src/test/java/org/p2p/solanaj/programs/BPFLoaderTest.java @@ -1,47 +1,134 @@ package org.p2p.solanaj.programs; -import org.junit.Ignore; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Disabled; +import static org.junit.jupiter.api.Assertions.*; import org.p2p.solanaj.core.Account; import org.p2p.solanaj.core.LegacyTransaction; -import org.p2p.solanaj.core.PublicKey; +import org.p2p.solanaj.core.TransactionInstruction; import org.p2p.solanaj.rpc.Cluster; import org.p2p.solanaj.rpc.RpcClient; import org.p2p.solanaj.rpc.RpcException; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Paths; import java.util.List; +/** + * Test class for BPFLoader program instructions. + */ public class BPFLoaderTest { - private final RpcClient client = new RpcClient(Cluster.MAINNET); + private RpcClient client; + private Account payer; + private Account bufferAccount; + private Account programAccount; + private Account programDataAccount; + + @BeforeEach + public void setUp() { + client = new RpcClient(Cluster.DEVNET); + payer = new Account(); + bufferAccount = new Account(); + programAccount = new Account(); + programDataAccount = new Account(); + } @Test - @Ignore - public void initializeBufferTest() throws RpcException { - Account account = null; - try { - account = Account.fromJson(Files.readString(Paths.get("src/test/resources/mainnet.json"))); - } catch (IOException e) { - e.printStackTrace(); - } + public void testInitializeBuffer() { + TransactionInstruction instruction = BPFLoader.initializeBuffer( + bufferAccount.getPublicKey(), + payer.getPublicKey() + ); - System.out.println(account.getPublicKey().toBase58()); + assertEquals(BPFLoader.PROGRAM_ID, instruction.getProgramId()); + assertEquals(2, instruction.getKeys().size()); + assertEquals(1, instruction.getData().length); + assertEquals(0, instruction.getData()[0]); + } - LegacyTransaction legacyTransaction = new LegacyTransaction(); + @Test + public void testWrite() { + byte[] data = new byte[]{1, 2, 3, 4, 5}; + TransactionInstruction instruction = BPFLoader.write( + bufferAccount.getPublicKey(), + payer.getPublicKey(), + 10, + data + ); + + assertEquals(BPFLoader.PROGRAM_ID, instruction.getProgramId()); + assertEquals(2, instruction.getKeys().size()); + assertEquals(10, instruction.getData().length); + assertEquals(1, instruction.getData()[0]); + } + + @Test + public void testDeployWithMaxDataLen() { + TransactionInstruction instruction = BPFLoader.deployWithMaxDataLen( + payer.getPublicKey(), + programDataAccount.getPublicKey(), + programAccount.getPublicKey(), + bufferAccount.getPublicKey(), + payer.getPublicKey(), + 1000 + ); + + assertEquals(BPFLoader.PROGRAM_ID, instruction.getProgramId()); + assertEquals(8, instruction.getKeys().size()); + assertEquals(9, instruction.getData().length); + assertEquals(2, instruction.getData()[0]); + } - // initialize buffer - Account bufferAccount = new Account(); + @Test + public void testUpgrade() { + TransactionInstruction instruction = BPFLoader.upgrade( + programDataAccount.getPublicKey(), + programAccount.getPublicKey(), + bufferAccount.getPublicKey(), + payer.getPublicKey(), + payer.getPublicKey() + ); + + assertEquals(BPFLoader.PROGRAM_ID, instruction.getProgramId()); + assertEquals(7, instruction.getKeys().size()); + assertEquals(1, instruction.getData().length); + assertEquals(3, instruction.getData()[0]); + } + + @Test + public void testSetAuthority() { + TransactionInstruction instruction = BPFLoader.setAuthority( + programDataAccount.getPublicKey(), + payer.getPublicKey(), + new Account().getPublicKey() + ); + assertEquals(BPFLoader.PROGRAM_ID, instruction.getProgramId()); + assertEquals(3, instruction.getKeys().size()); + assertEquals(1, instruction.getData().length); + assertEquals(4, instruction.getData()[0]); + } + + // ... existing code ... + + /** + * Integration test for initializing a buffer. + * Note: This test is ignored by default as it requires a connection to the Solana network. + */ + @Test + @Disabled + public void initializeBufferIntegrationTest() throws RpcException { + Account account = new Account(); // Replace with your actual account setup + LegacyTransaction legacyTransaction = new LegacyTransaction(); + + // Initialize buffer legacyTransaction.addInstruction( SystemProgram.createAccount( account.getPublicKey(), bufferAccount.getPublicKey(), 3290880, 165L, - PublicKey.valueOf("BPFLoaderUpgradeab1e11111111111111111111111") + BPFLoader.PROGRAM_ID ) ); @@ -55,7 +142,8 @@ public void initializeBufferTest() throws RpcException { String hash = client.getApi().getRecentBlockhash(); legacyTransaction.setRecentBlockHash(hash); - System.out.println("TX: " + client.getApi().sendLegacyTransaction(legacyTransaction, List.of(account, bufferAccount), hash)); - + String txId = client.getApi().sendLegacyTransaction(legacyTransaction, List.of(account, bufferAccount), hash); + assertNotNull(txId); + System.out.println("Transaction ID: " + txId); } } diff --git a/src/test/java/org/p2p/solanaj/programs/ComputeBudgetProgramTest.java b/src/test/java/org/p2p/solanaj/programs/ComputeBudgetProgramTest.java new file mode 100644 index 00000000..5579b7c6 --- /dev/null +++ b/src/test/java/org/p2p/solanaj/programs/ComputeBudgetProgramTest.java @@ -0,0 +1,76 @@ +package org.p2p.solanaj.programs; + +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; +import org.p2p.solanaj.core.TransactionInstruction; + +/** + * Test class for ComputeBudgetProgram. + * This class contains unit tests for various methods in the ComputeBudgetProgram class. + */ +public class ComputeBudgetProgramTest { + + /** + * Test the setComputeUnitPrice method of ComputeBudgetProgram. + * Verifies that the instruction is created correctly with the right program ID, keys, and data. + */ + @Test + public void testSetComputeUnitPrice() { + int microLamports = 1000; + TransactionInstruction instruction = ComputeBudgetProgram.setComputeUnitPrice(microLamports); + + assertEquals(ComputeBudgetProgram.PROGRAM_ID, instruction.getProgramId()); + assertEquals(0, instruction.getKeys().size()); + + byte[] expectedData = new byte[]{0x03, (byte) 0xE8, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + assertArrayEquals(expectedData, instruction.getData()); + } + + /** + * Test the setComputeUnitLimit method of ComputeBudgetProgram. + * Verifies that the instruction is created correctly with the right program ID, keys, and data. + */ + @Test + public void testSetComputeUnitLimit() { + int units = 200000; + TransactionInstruction instruction = ComputeBudgetProgram.setComputeUnitLimit(units); + + assertEquals(ComputeBudgetProgram.PROGRAM_ID, instruction.getProgramId()); + assertEquals(0, instruction.getKeys().size()); + + byte[] expectedData = new byte[]{0x02, 0x40, 0x0D, 0x03, 0x00}; + assertArrayEquals(expectedData, instruction.getData()); + } + + /** + * Test the requestHeapFrame method of ComputeBudgetProgram. + * Verifies that the instruction is created correctly with the right program ID, keys, and data. + */ + @Test + public void testRequestHeapFrame() { + int bytes = 32768; + TransactionInstruction instruction = ComputeBudgetProgram.requestHeapFrame(bytes); + + assertEquals(ComputeBudgetProgram.PROGRAM_ID, instruction.getProgramId()); + assertEquals(0, instruction.getKeys().size()); + + byte[] expectedData = new byte[]{0x01, 0x00, (byte) 0x80, 0x00, 0x00}; + assertArrayEquals(expectedData, instruction.getData()); + } + + /** + * Test the setLoadedAccountsDataSizeLimit method of ComputeBudgetProgram. + * Verifies that the instruction is created correctly with the right program ID, keys, and data. + */ + @Test + public void testSetLoadedAccountsDataSizeLimit() { + int bytes = 65536; + TransactionInstruction instruction = ComputeBudgetProgram.setLoadedAccountsDataSizeLimit(bytes); + + assertEquals(ComputeBudgetProgram.PROGRAM_ID, instruction.getProgramId()); + assertEquals(0, instruction.getKeys().size()); + + byte[] expectedData = new byte[]{0x04, 0x00, 0x00, 0x01, 0x00}; + assertArrayEquals(expectedData, instruction.getData()); + } +} diff --git a/src/test/java/org/p2p/solanaj/programs/MemoProgramTest.java b/src/test/java/org/p2p/solanaj/programs/MemoProgramTest.java new file mode 100644 index 00000000..214cc96a --- /dev/null +++ b/src/test/java/org/p2p/solanaj/programs/MemoProgramTest.java @@ -0,0 +1,58 @@ +package org.p2p.solanaj.programs; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Disabled; +import static org.junit.jupiter.api.Assertions.*; +import org.p2p.solanaj.core.PublicKey; +import org.p2p.solanaj.core.TransactionInstruction; + +import java.nio.charset.StandardCharsets; + +public class MemoProgramTest { + + @Test + public void testWriteUtf8_ValidInput() { + PublicKey account = new PublicKey("11111111111111111111111111111111"); + String memo = "Test memo"; + + TransactionInstruction instruction = MemoProgram.writeUtf8(account, memo); + + assertNotNull(instruction); + assertEquals(MemoProgram.PROGRAM_ID, instruction.getProgramId()); + assertEquals(1, instruction.getKeys().size()); + assertEquals(account, instruction.getKeys().get(0).getPublicKey()); + assertTrue(instruction.getKeys().get(0).isSigner()); + assertFalse(instruction.getKeys().get(0).isWritable()); + assertArrayEquals(memo.getBytes(StandardCharsets.UTF_8), instruction.getData()); + } + + @Test + public void testWriteUtf8_NullAccount() { + assertThrows(IllegalArgumentException.class, () -> MemoProgram.writeUtf8(null, "Test memo")); + } + + @Test + public void testWriteUtf8_NullMemo() { + PublicKey account = new PublicKey("11111111111111111111111111111111"); + assertThrows(IllegalArgumentException.class, () -> MemoProgram.writeUtf8(account, null)); + } + + @Test + public void testWriteUtf8_EmptyMemo() { + PublicKey account = new PublicKey("11111111111111111111111111111111"); + assertThrows(IllegalArgumentException.class, () -> MemoProgram.writeUtf8(account, "")); + } + + @Test + public void testWriteUtf8_LongMemo() { + PublicKey account = new PublicKey("11111111111111111111111111111111"); + String longMemo = String.join("", java.util.Collections.nCopies(1000, "A")); + + TransactionInstruction instruction = MemoProgram.writeUtf8(account, longMemo); + + assertNotNull(instruction); + assertEquals(MemoProgram.PROGRAM_ID, instruction.getProgramId()); + assertArrayEquals(longMemo.getBytes(StandardCharsets.UTF_8), instruction.getData()); + } +} diff --git a/src/test/java/org/p2p/solanaj/programs/SystemProgramTest.java b/src/test/java/org/p2p/solanaj/programs/SystemProgramTest.java index 1d1d0187..2cdec47f 100644 --- a/src/test/java/org/p2p/solanaj/programs/SystemProgramTest.java +++ b/src/test/java/org/p2p/solanaj/programs/SystemProgramTest.java @@ -3,35 +3,99 @@ import org.p2p.solanaj.core.PublicKey; import org.p2p.solanaj.core.TransactionInstruction; -import org.junit.Test; -import static org.junit.Assert.*; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Disabled; +import static org.junit.jupiter.api.Assertions.*; import org.bitcoinj.core.Base58; public class SystemProgramTest { @Test - public void transferInstruction() { + public void testTransferInstruction() { PublicKey fromPublicKey = new PublicKey("QqCCvshxtqMAL2CVALqiJB7uEeE5mjSPsseQdDzsRUo"); - PublicKey toPublickKey = new PublicKey("GrDMoeqMLFjeXQ24H56S1RLgT4R76jsuWCd6SvXyGPQ5"); - int lamports = 3000; + PublicKey toPublicKey = new PublicKey("GrDMoeqMLFjeXQ24H56S1RLgT4R76jsuWCd6SvXyGPQ5"); + long lamports = 3000; - TransactionInstruction instruction = SystemProgram.transfer(fromPublicKey, toPublickKey, lamports); + TransactionInstruction instruction = SystemProgram.transfer(fromPublicKey, toPublicKey, lamports); assertEquals(SystemProgram.PROGRAM_ID, instruction.getProgramId()); assertEquals(2, instruction.getKeys().size()); - assertEquals(toPublickKey, instruction.getKeys().get(1).getPublicKey()); + assertEquals(fromPublicKey, instruction.getKeys().get(0).getPublicKey()); + assertTrue(instruction.getKeys().get(0).isSigner()); + assertTrue(instruction.getKeys().get(0).isWritable()); + assertEquals(toPublicKey, instruction.getKeys().get(1).getPublicKey()); + assertFalse(instruction.getKeys().get(1).isSigner()); + assertTrue(instruction.getKeys().get(1).isWritable()); - assertArrayEquals(new byte[] { 2, 0, 0, 0, -72, 11, 0, 0, 0, 0, 0, 0 }, instruction.getData()); + byte[] expectedData = new byte[]{2, 0, 0, 0, -72, 11, 0, 0, 0, 0, 0, 0}; + assertArrayEquals(expectedData, instruction.getData()); } @Test - public void createAccountInstruction() { - TransactionInstruction instruction = SystemProgram.createAccount(SystemProgram.PROGRAM_ID, - SystemProgram.PROGRAM_ID, 2039280, 165, SystemProgram.PROGRAM_ID); + public void testTransferInstructionWithNegativeLamports() { + assertThrows(IllegalArgumentException.class, () -> { + PublicKey fromPublicKey = new PublicKey("QqCCvshxtqMAL2CVALqiJB7uEeE5mjSPsseQdDzsRUo"); + PublicKey toPublicKey = new PublicKey("GrDMoeqMLFjeXQ24H56S1RLgT4R76jsuWCd6SvXyGPQ5"); + long negativeLamports = -1; - assertEquals("11119os1e9qSs2u7TsThXqkBSRUo9x7kpbdqtNNbTeaxHGPdWbvoHsks9hpp6mb2ed1NeB", - Base58.encode(instruction.getData())); + SystemProgram.transfer(fromPublicKey, toPublicKey, negativeLamports); + }); } + @Test + public void testCreateAccountInstruction() { + PublicKey fromPublicKey = new PublicKey("QqCCvshxtqMAL2CVALqiJB7uEeE5mjSPsseQdDzsRUo"); + PublicKey newAccountPublicKey = new PublicKey("GrDMoeqMLFjeXQ24H56S1RLgT4R76jsuWCd6SvXyGPQ5"); + long lamports = 2039280; + long space = 165; + PublicKey programId = SystemProgram.PROGRAM_ID; + + TransactionInstruction instruction = SystemProgram.createAccount(fromPublicKey, newAccountPublicKey, lamports, space, programId); + + assertEquals(SystemProgram.PROGRAM_ID, instruction.getProgramId()); + assertEquals(2, instruction.getKeys().size()); + assertEquals(fromPublicKey, instruction.getKeys().get(0).getPublicKey()); + assertTrue(instruction.getKeys().get(0).isSigner()); + assertTrue(instruction.getKeys().get(0).isWritable()); + assertEquals(newAccountPublicKey, instruction.getKeys().get(1).getPublicKey()); + assertTrue(instruction.getKeys().get(1).isSigner()); + assertTrue(instruction.getKeys().get(1).isWritable()); + + String expectedDataBase58 = "11119os1e9qSs2u7TsThXqkBSRUo9x7kpbdqtNNbTeaxHGPdWbvoHsks9hpp6mb2ed1NeB"; + assertEquals(expectedDataBase58, Base58.encode(instruction.getData())); + } + + @Test + public void testCreateAccountInstructionWithNegativeLamports() { + assertThrows(IllegalArgumentException.class, () -> { + PublicKey fromPublicKey = new PublicKey("QqCCvshxtqMAL2CVALqiJB7uEeE5mjSPsseQdDzsRUo"); + PublicKey newAccountPublicKey = new PublicKey("GrDMoeqMLFjeXQ24H56S1RLgT4R76jsuWCd6SvXyGPQ5"); + long negativeLamports = -1; + long space = 165; + PublicKey programId = SystemProgram.PROGRAM_ID; + + SystemProgram.createAccount(fromPublicKey, newAccountPublicKey, negativeLamports, space, programId); + }); + } + + @Test + public void testAssignInstruction() { + PublicKey owner = new PublicKey("QqCCvshxtqMAL2CVALqiJB7uEeE5mjSPsseQdDzsRUo"); + PublicKey newOwner = new PublicKey("GrDMoeqMLFjeXQ24H56S1RLgT4R76jsuWCd6SvXyGPQ5"); + + TransactionInstruction instruction = SystemProgram.assign(owner, newOwner); + + assertEquals(SystemProgram.PROGRAM_ID, instruction.getProgramId()); + assertEquals(1, instruction.getKeys().size()); + assertEquals(owner, instruction.getKeys().get(0).getPublicKey()); + assertTrue(instruction.getKeys().get(0).isSigner()); + assertTrue(instruction.getKeys().get(0).isWritable()); + + byte[] expectedData = new byte[36]; + expectedData[0] = 1; // PROGRAM_INDEX_ASSIGN + System.arraycopy(newOwner.toByteArray(), 0, expectedData, 4, 32); + assertArrayEquals(expectedData, instruction.getData()); + } } diff --git a/src/test/java/org/p2p/solanaj/programs/TokenProgramTest.java b/src/test/java/org/p2p/solanaj/programs/TokenProgramTest.java new file mode 100644 index 00000000..5d649db6 --- /dev/null +++ b/src/test/java/org/p2p/solanaj/programs/TokenProgramTest.java @@ -0,0 +1,230 @@ +package org.p2p.solanaj.programs; + +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; +import org.p2p.solanaj.core.PublicKey; +import org.p2p.solanaj.core.TransactionInstruction; + +import java.util.Arrays; +import java.util.List; + +/** + * Test class for TokenProgram + * + * These tests verify the correct creation of TransactionInstructions for various + * SPL Token operations, including initialization, transfers, and account management. + */ +public class TokenProgramTest { + + /** + * Tests the initializeMint instruction creation for TokenProgram. + * This instruction is used to create a new SPL Token mint. + */ + @Test + public void testInitializeMint() { + PublicKey mintPubkey = new PublicKey("Gh9ZwEmdLJ8DscKNTkTqPbNwLNNBjuSzaG9Vp2KGtKJr"); + int decimals = 9; + PublicKey mintAuthority = new PublicKey("FuLFkNQzNEAzZ2dEgXVUqVVLxJYLYhbSgpZf9RVVXZuT"); + PublicKey freezeAuthority = new PublicKey("HNGVuL5kqjDehw7KR63w9gxow32sX6xzRNgLb8GkbwCM"); + + TransactionInstruction instruction = TokenProgram.initializeMint(mintPubkey, decimals, mintAuthority, freezeAuthority); + + assertEquals(TokenProgram.PROGRAM_ID, instruction.getProgramId()); + assertEquals(2, instruction.getKeys().size()); + assertEquals(mintPubkey, instruction.getKeys().get(0).getPublicKey()); + assertFalse(instruction.getKeys().get(0).isSigner()); + assertTrue(instruction.getKeys().get(0).isWritable()); + assertEquals(TokenProgram.SYSVAR_RENT_PUBKEY, instruction.getKeys().get(1).getPublicKey()); + + byte[] actualData = instruction.getData(); + assertEquals(67, actualData.length); + assertEquals(0, actualData[0]); // Instruction type: InitializeMint + assertEquals(9, actualData[1]); // Decimals + // The next byte (actualData[2]) is part of the 64-byte authority data + assertEquals(-35, actualData[2]); // First byte of authority data (unsigned: 221) + } + + /** + * Tests the initializeMultisig instruction creation for TokenProgram. + * This instruction is used to create a new multisig account for SPL Tokens. + */ + @Test + public void testInitializeMultisig() { + PublicKey multisigPubkey = new PublicKey("Gh9ZwEmdLJ8DscKNTkTqPbNwLNNBjuSzaG9Vp2KGtKJr"); + List signerPubkeys = Arrays.asList( + new PublicKey("FuLFkNQzNEAzZ2dEgXVUqVVLxJYLYhbSgpZf9RVVXZuT"), + new PublicKey("HNGVuL5kqjDehw7KR63w9gxow32sX6xzRNgLb8GkbwCM") + ); + int m = 2; + + TransactionInstruction instruction = TokenProgram.initializeMultisig(multisigPubkey, signerPubkeys, m); + + assertEquals(TokenProgram.PROGRAM_ID, instruction.getProgramId()); + assertEquals(4, instruction.getKeys().size()); + assertEquals(multisigPubkey, instruction.getKeys().get(0).getPublicKey()); + assertFalse(instruction.getKeys().get(0).isSigner()); + assertTrue(instruction.getKeys().get(0).isWritable()); + assertEquals(TokenProgram.SYSVAR_RENT_PUBKEY, instruction.getKeys().get(1).getPublicKey()); + + byte[] expectedData = new byte[]{2, 2}; // [Instruction type: InitializeMultisig, Number of signers (m)] + assertArrayEquals(expectedData, instruction.getData()); + } + + /** + * Tests the approve instruction creation for TokenProgram. + * This instruction is used to approve a delegate to transfer tokens from an account. + */ + @Test + public void testApprove() { + PublicKey sourcePubkey = new PublicKey("Gh9ZwEmdLJ8DscKNTkTqPbNwLNNBjuSzaG9Vp2KGtKJr"); + PublicKey delegatePubkey = new PublicKey("FuLFkNQzNEAzZ2dEgXVUqVVLxJYLYhbSgpZf9RVVXZuT"); + PublicKey ownerPubkey = new PublicKey("HNGVuL5kqjDehw7KR63w9gxow32sX6xzRNgLb8GkbwCM"); + long amount = 1000000000; // 1 billion (assuming 9 decimals) + + TransactionInstruction instruction = TokenProgram.approve(sourcePubkey, delegatePubkey, ownerPubkey, amount); + + assertEquals(TokenProgram.PROGRAM_ID, instruction.getProgramId()); + assertEquals(3, instruction.getKeys().size()); + assertEquals(sourcePubkey, instruction.getKeys().get(0).getPublicKey()); + assertFalse(instruction.getKeys().get(0).isSigner()); + assertTrue(instruction.getKeys().get(0).isWritable()); + assertEquals(delegatePubkey, instruction.getKeys().get(1).getPublicKey()); + assertFalse(instruction.getKeys().get(1).isSigner()); + assertFalse(instruction.getKeys().get(1).isWritable()); + assertEquals(ownerPubkey, instruction.getKeys().get(2).getPublicKey()); + assertTrue(instruction.getKeys().get(2).isSigner()); + assertFalse(instruction.getKeys().get(2).isWritable()); + + byte[] actualData = instruction.getData(); + assertEquals(9, actualData.length); + assertEquals(4, actualData[0]); // Instruction type: Approve + assertEquals(0, actualData[1]); // First byte of amount (little-endian) + assertEquals(-54, actualData[2]); // Second byte of amount (unsigned: 202) + // Full 8-byte representation of 1000000000: [0, 202, 154, 59, 0, 0, 0, 0] + } + + /** + * Tests the transfer instruction creation for TokenProgram. + * This instruction is used to transfer tokens between accounts. + */ + @Test + public void testTransfer() { + PublicKey source = new PublicKey("Gh9ZwEmdLJ8DscKNTkTqPbNwLNNBjuSzaG9Vp2KGtKJr"); + PublicKey destination = new PublicKey("FuLFkNQzNEAzZ2dEgXVUqVVLxJYLYhbSgpZf9RVVXZuT"); + long amount = 1000000000; // 1 billion (assuming 9 decimals) + PublicKey owner = new PublicKey("HNGVuL5kqjDehw7KR63w9gxow32sX6xzRNgLb8GkbwCM"); + + TransactionInstruction instruction = TokenProgram.transfer(source, destination, amount, owner); + + assertEquals(TokenProgram.PROGRAM_ID, instruction.getProgramId()); + assertEquals(3, instruction.getKeys().size()); + assertEquals(source, instruction.getKeys().get(0).getPublicKey()); + assertEquals(destination, instruction.getKeys().get(1).getPublicKey()); + assertEquals(owner, instruction.getKeys().get(2).getPublicKey()); + + byte[] actualData = instruction.getData(); + assertEquals(9, actualData.length); + assertEquals(3, actualData[0]); // Instruction type: Transfer + assertEquals(0, actualData[1]); // First byte of amount (little-endian) + assertEquals(-54, actualData[2]); // Second byte of amount (unsigned: 202) + // Full 8-byte representation of 1000000000: [0, 202, 154, 59, 0, 0, 0, 0] + } + + /** + * Tests the burn instruction creation for TokenProgram. + * This instruction is used to burn (destroy) tokens. + */ + @Test + public void testBurn() { + PublicKey account = new PublicKey("Gh9ZwEmdLJ8DscKNTkTqPbNwLNNBjuSzaG9Vp2KGtKJr"); + PublicKey mint = new PublicKey("FuLFkNQzNEAzZ2dEgXVUqVVLxJYLYhbSgpZf9RVVXZuT"); + PublicKey owner = new PublicKey("HNGVuL5kqjDehw7KR63w9gxow32sX6xzRNgLb8GkbwCM"); + long amount = 500000000; // 500 million (assuming 9 decimals) + + TransactionInstruction instruction = TokenProgram.burn(account, mint, owner, amount); + + assertEquals(TokenProgram.PROGRAM_ID, instruction.getProgramId()); + assertEquals(3, instruction.getKeys().size()); + assertEquals(account, instruction.getKeys().get(0).getPublicKey()); + assertEquals(mint, instruction.getKeys().get(1).getPublicKey()); + assertEquals(owner, instruction.getKeys().get(2).getPublicKey()); + + byte[] actualData = instruction.getData(); + assertEquals(9, actualData.length); + assertEquals(8, actualData[0]); // Instruction type: Burn + assertEquals(0, actualData[1]); // First byte of amount (little-endian) + assertEquals(101, actualData[2]); // Second byte of amount + // Full 8-byte representation of 500000000: [0, 101, 205, 29, 0, 0, 0, 0] + } + + /** + * Tests the mintTo instruction creation for TokenProgram. + * This instruction is used to mint new tokens to an account. + */ + @Test + public void testMintTo() { + PublicKey mint = new PublicKey("Gh9ZwEmdLJ8DscKNTkTqPbNwLNNBjuSzaG9Vp2KGtKJr"); + PublicKey destination = new PublicKey("FuLFkNQzNEAzZ2dEgXVUqVVLxJYLYhbSgpZf9RVVXZuT"); + PublicKey authority = new PublicKey("HNGVuL5kqjDehw7KR63w9gxow32sX6xzRNgLb8GkbwCM"); + long amount = 750000000; // 750 million (assuming 9 decimals) + + TransactionInstruction instruction = TokenProgram.mintTo(mint, destination, authority, amount); + + assertEquals(TokenProgram.PROGRAM_ID, instruction.getProgramId()); + assertEquals(3, instruction.getKeys().size()); + assertEquals(mint, instruction.getKeys().get(0).getPublicKey()); + assertEquals(destination, instruction.getKeys().get(1).getPublicKey()); + assertEquals(authority, instruction.getKeys().get(2).getPublicKey()); + + byte[] actualData = instruction.getData(); + assertEquals(9, actualData.length); + assertEquals(7, actualData[0]); // Instruction type: MintTo + assertEquals(-128, actualData[1]); // First byte of amount (unsigned: 128) + assertEquals(23, actualData[2]); // Second byte of amount + // Full 8-byte representation of 750000000: [128, 23, 223, 44, 0, 0, 0, 0] + } + + /** + * Tests the freezeAccount instruction creation for TokenProgram. + * This instruction is used to freeze an account, preventing transfers. + */ + @Test + public void testFreezeAccount() { + PublicKey account = new PublicKey("Gh9ZwEmdLJ8DscKNTkTqPbNwLNNBjuSzaG9Vp2KGtKJr"); + PublicKey mint = new PublicKey("FuLFkNQzNEAzZ2dEgXVUqVVLxJYLYhbSgpZf9RVVXZuT"); + PublicKey authority = new PublicKey("HNGVuL5kqjDehw7KR63w9gxow32sX6xzRNgLb8GkbwCM"); + + TransactionInstruction instruction = TokenProgram.freezeAccount(account, mint, authority); + + assertEquals(TokenProgram.PROGRAM_ID, instruction.getProgramId()); + assertEquals(3, instruction.getKeys().size()); + assertEquals(account, instruction.getKeys().get(0).getPublicKey()); + assertEquals(mint, instruction.getKeys().get(1).getPublicKey()); + assertEquals(authority, instruction.getKeys().get(2).getPublicKey()); + + byte[] expectedData = new byte[]{0x0A}; // Instruction type: FreezeAccount + assertArrayEquals(expectedData, instruction.getData()); + } + + /** + * Tests the thawAccount instruction creation for TokenProgram. + * This instruction is used to thaw a frozen account, allowing transfers. + */ + @Test + public void testThawAccount() { + PublicKey account = new PublicKey("Gh9ZwEmdLJ8DscKNTkTqPbNwLNNBjuSzaG9Vp2KGtKJr"); + PublicKey mint = new PublicKey("FuLFkNQzNEAzZ2dEgXVUqVVLxJYLYhbSgpZf9RVVXZuT"); + PublicKey authority = new PublicKey("HNGVuL5kqjDehw7KR63w9gxow32sX6xzRNgLb8GkbwCM"); + + TransactionInstruction instruction = TokenProgram.thawAccount(account, mint, authority); + + assertEquals(TokenProgram.PROGRAM_ID, instruction.getProgramId()); + assertEquals(3, instruction.getKeys().size()); + assertEquals(account, instruction.getKeys().get(0).getPublicKey()); + assertEquals(mint, instruction.getKeys().get(1).getPublicKey()); + assertEquals(authority, instruction.getKeys().get(2).getPublicKey()); + + byte[] expectedData = new byte[]{0x0B}; // Instruction type: ThawAccount + assertArrayEquals(expectedData, instruction.getData()); + } +} \ No newline at end of file diff --git a/src/test/java/org/p2p/solanaj/utils/ByteUtilsTest.java b/src/test/java/org/p2p/solanaj/utils/ByteUtilsTest.java index a5d36055..6b81b3c8 100644 --- a/src/test/java/org/p2p/solanaj/utils/ByteUtilsTest.java +++ b/src/test/java/org/p2p/solanaj/utils/ByteUtilsTest.java @@ -1,7 +1,7 @@ package org.p2p.solanaj.utils; -import org.junit.Test; -import static org.junit.Assert.*; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -29,13 +29,16 @@ public void readUint64() throws IOException { assertEquals(bigIntValue, bn.toString()); } - @Test(expected = RuntimeException.class) + @Test public void uint64ToByteStreamLE() { - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - try { - ByteUtils.uint64ToByteStreamLE(new BigInteger("137001898677442802701"), bos); - } catch (IOException e) { - } + assertThrows(RuntimeException.class, () -> { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + try { + ByteUtils.uint64ToByteStreamLE(new BigInteger("137001898677442802701"), bos); + } catch (IOException e) { + + } + }); } } diff --git a/src/test/java/org/p2p/solanaj/utils/ShortvecEncodingTest.java b/src/test/java/org/p2p/solanaj/utils/ShortvecEncodingTest.java new file mode 100644 index 00000000..4d6ee62b --- /dev/null +++ b/src/test/java/org/p2p/solanaj/utils/ShortvecEncodingTest.java @@ -0,0 +1,21 @@ +package org.p2p.solanaj.utils; + +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +public class ShortvecEncodingTest { + + @Test + public void encodeLength() { + assertArrayEquals(new byte[] { 0 } /* [0] */, ShortvecEncoding.encodeLength(0)); + assertArrayEquals(new byte[] { 1 } /* [1] */, ShortvecEncoding.encodeLength(1)); + assertArrayEquals(new byte[] { 5 } /* [5] */, ShortvecEncoding.encodeLength(5)); + assertArrayEquals(new byte[] { 127 } /* [0x7f] */, ShortvecEncoding.encodeLength(127)); // 0x7f + assertArrayEquals(new byte[] { -128, 1 }/* [0x80, 0x01] */, ShortvecEncoding.encodeLength(128)); // 0x80 + assertArrayEquals(new byte[] { -1, 1 } /* [0xff, 0x01] */, ShortvecEncoding.encodeLength(255)); // 0xff + assertArrayEquals(new byte[] { -128, 2 } /* [0x80, 0x02] */, ShortvecEncoding.encodeLength(256)); // 0x100 + assertArrayEquals(new byte[] { -1, -1, 1 } /* [0xff, 0xff, 0x01] */, ShortvecEncoding.encodeLength(32767)); // 0x7fff + assertArrayEquals(new byte[] { -128, -128, -128, 1 } /* [0x80, 0x80, 0x80, 0x01] */, + ShortvecEncoding.encodeLength(2097152)); // 0x200000 + } +} diff --git a/src/test/java/org/p2p/solanaj/utils/ShortvecTest.java b/src/test/java/org/p2p/solanaj/utils/ShortvecTest.java deleted file mode 100644 index 4b0a7175..00000000 --- a/src/test/java/org/p2p/solanaj/utils/ShortvecTest.java +++ /dev/null @@ -1,21 +0,0 @@ -package org.p2p.solanaj.utils; - -import org.junit.Test; -import static org.junit.Assert.*; - -public class ShortvecTest { - - @Test - public void encodeLength() { - assertArrayEquals(new byte[] { 0 } /* [0] */, Shortvec.encodeLength(0)); - assertArrayEquals(new byte[] { 1 } /* [1] */, Shortvec.encodeLength(1)); - assertArrayEquals(new byte[] { 5 } /* [5] */, Shortvec.encodeLength(5)); - assertArrayEquals(new byte[] { 127 } /* [0x7f] */, Shortvec.encodeLength(127)); // 0x7f - assertArrayEquals(new byte[] { -128, 1 }/* [0x80, 0x01] */, Shortvec.encodeLength(128)); // 0x80 - assertArrayEquals(new byte[] { -1, 1 } /* [0xff, 0x01] */, Shortvec.encodeLength(255)); // 0xff - assertArrayEquals(new byte[] { -128, 2 } /* [0x80, 0x02] */, Shortvec.encodeLength(256)); // 0x100 - assertArrayEquals(new byte[] { -1, -1, 1 } /* [0xff, 0xff, 0x01] */, Shortvec.encodeLength(32767)); // 0x7fff - assertArrayEquals(new byte[] { -128, -128, -128, 1 } /* [0x80, 0x80, 0x80, 0x01] */, - Shortvec.encodeLength(2097152)); // 0x200000 - } -} diff --git a/src/test/java/org/p2p/solanaj/ws/LogNotificationEventListenerTest.java b/src/test/java/org/p2p/solanaj/ws/LogNotificationEventListenerTest.java new file mode 100644 index 00000000..b071ab65 --- /dev/null +++ b/src/test/java/org/p2p/solanaj/ws/LogNotificationEventListenerTest.java @@ -0,0 +1,134 @@ +package org.p2p.solanaj.ws; + +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.p2p.solanaj.core.PublicKey; +import org.p2p.solanaj.rpc.RpcClient; +import org.p2p.solanaj.ws.listeners.LogNotificationEventListener; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Disabled; +import static org.junit.jupiter.api.Assertions.*; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.LogRecord; +import java.util.logging.Logger; +import java.util.ArrayList; + +public class LogNotificationEventListenerTest { + + @Mock + private RpcClient mockRpcClient; + + private PublicKey testPublicKey; + private LogNotificationEventListener listener; + private TestLogHandler logHandler; + + @BeforeEach + public void setUp() { + MockitoAnnotations.openMocks(this); + testPublicKey = new PublicKey("PhoeNiXZ8ByJGLkxNfZRnkUfjvmuYqLR89jjFHGqdXY"); + listener = new LogNotificationEventListener(mockRpcClient, testPublicKey); + + logHandler = new TestLogHandler(); + Logger logger = Logger.getLogger(LogNotificationEventListener.class.getName()); + logger.addHandler(logHandler); + logger.setLevel(Level.ALL); + } + + @AfterEach + public void tearDown() { + Logger logger = Logger.getLogger(LogNotificationEventListener.class.getName()); + logger.removeHandler(logHandler); + } + + /** + * Tests the onNotificationEvent method with valid data. + * Verifies that the listener processes the data correctly without throwing exceptions. + */ + @Test + public void testOnNotificationEvent_ValidData() { + Map testData = new HashMap<>(); + // Using a realistic Solana transaction signature + String realSignature = "5wHu1qwD4kLwYvKNyZzjuoMYpGHSreYitBUJb7TQx3hngzs7jq6hBwZWwGcRQK3H9rw7Fxgb3zBYLXqjrDkDvnqf"; + testData.put("signature", realSignature); + List logs = Arrays.asList( + "Program 11111111111111111111111111111111 invoke [1]", + "Program 11111111111111111111111111111111 success" + ); + testData.put("logs", logs); + + listener.onNotificationEvent(testData); + + assertTrue(logHandler.hasMessage("Received notification for transaction: " + realSignature)); + assertTrue(logHandler.hasMessage("Log: Program 11111111111111111111111111111111 invoke [1]")); + assertTrue(logHandler.hasMessage("Log: Program 11111111111111111111111111111111 success")); + } + + /** + * Tests the onNotificationEvent method with null data. + * Verifies that the listener handles null input gracefully. + */ + @Test + public void testOnNotificationEvent_NullData() { + listener.onNotificationEvent(null); + assertTrue(logHandler.hasMessage("Received null data in onNotificationEvent")); + } + + /** + * Tests the onNotificationEvent method with invalid data type. + * Verifies that the listener handles unexpected input types correctly. + */ + @Test + public void testOnNotificationEvent_InvalidDataType() { + listener.onNotificationEvent("Invalid data type"); + assertTrue(logHandler.hasMessage("Received invalid data type in onNotificationEvent: java.lang.String")); + } + + /** + * Tests the onNotificationEvent method with missing required fields. + * Verifies that the listener handles incomplete data appropriately. + */ + @Test + public void testOnNotificationEvent_MissingFields() { + Map testData = new HashMap<>(); + testData.put("signature", "someSignature"); + // Missing 'logs' field + listener.onNotificationEvent(testData); + assertTrue(logHandler.hasMessage("Missing required fields in notification data")); + } + + /** + * Tests the getter methods of the listener. + * Verifies that the getClient and getListeningPubkey methods return the expected values. + */ + @Test + public void testGetters() { + assertEquals(mockRpcClient, listener.getClient(), "RpcClient should match"); + assertEquals(testPublicKey, listener.getListeningPubkey(), "PublicKey should match"); + } + + private static class TestLogHandler extends Handler { + private final List logs = new ArrayList<>(); + + @Override + public void publish(LogRecord record) { + logs.add(record); + } + + @Override + public void flush() {} + + @Override + public void close() throws SecurityException {} + + public boolean hasMessage(String message) { + return logs.stream().anyMatch(record -> record.getMessage().contains(message)); + } + } +} diff --git a/src/test/java/org/p2p/solanaj/ws/SubscriptionWebSocketClientTest.java b/src/test/java/org/p2p/solanaj/ws/SubscriptionWebSocketClientTest.java new file mode 100644 index 00000000..6984ee08 --- /dev/null +++ b/src/test/java/org/p2p/solanaj/ws/SubscriptionWebSocketClientTest.java @@ -0,0 +1,114 @@ +package org.p2p.solanaj.ws; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Disabled; +import static org.junit.jupiter.api.Assertions.*; +import org.java_websocket.handshake.ServerHandshake; + +import java.net.URI; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +/** + * Test class for SubscriptionWebSocketClient using a real devnet connection + */ +class SubscriptionWebSocketClientTest { + + private static final String DEVNET_WS_URL = "wss://api.devnet.solana.com"; + private SubscriptionWebSocketClient client; + private CountDownLatch connectionLatch; + + /** + * Set up the test environment + */ + @BeforeEach + void setUp() throws Exception { + connectionLatch = new CountDownLatch(1); + client = new SubscriptionWebSocketClient(new URI(DEVNET_WS_URL)) { + @Override + public void onOpen(ServerHandshake handshakedata) { + super.onOpen(handshakedata); + connectionLatch.countDown(); + } + }; + client.connect(); + assertTrue(connectionLatch.await(10, TimeUnit.SECONDS), "Connection timed out"); + } + + /** + * Clean up after each test + */ + @AfterEach + void tearDown() { + if (client != null && client.isOpen()) { + client.close(); + } + } + + /** + * Tests that the connection can be established successfully + */ + @Test + void testConnectionEstablished() { + assertTrue(client.isOpen(), "WebSocket should be open"); + } + + /** + * Tests that the client can send and receive messages + */ + @Test + void testSendAndReceiveMessage() throws Exception { + CountDownLatch messageLatch = new CountDownLatch(1); + final String[] receivedMessage = new String[1]; + + client = new SubscriptionWebSocketClient(new URI(DEVNET_WS_URL)) { + @Override + public void onMessage(String message) { + receivedMessage[0] = message; + messageLatch.countDown(); + } + }; + client.connect(); + assertTrue(connectionLatch.await(10, TimeUnit.SECONDS), "Connection timed out"); + + // Ensure client is connected before sending message + while (!client.isOpen()) { + Thread.sleep(100); + } + + String testMessage = "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"getHealth\"}"; + client.send(testMessage); + + assertTrue(messageLatch.await(10, TimeUnit.SECONDS), "Message response timed out"); + assertNotNull(receivedMessage[0], "Received message should not be null"); + + System.out.println("Received message: " + receivedMessage[0]); + + assertTrue(receivedMessage[0].contains("result") || receivedMessage[0].contains("error"), + "Received message should contain 'result' or 'error'"); + } + + /** + * Tests that the client can handle connection closure and reconnection + */ + @Test + void testConnectionCloseAndReconnect() throws Exception { + client.close(); + assertFalse(client.isOpen(), "WebSocket should be closed"); + + CountDownLatch reconnectLatch = new CountDownLatch(1); + client = new SubscriptionWebSocketClient(new URI(DEVNET_WS_URL)) { + @Override + public void onOpen(ServerHandshake handshakedata) { + super.onOpen(handshakedata); + reconnectLatch.countDown(); + } + }; + client.connect(); + + assertTrue(reconnectLatch.await(10, TimeUnit.SECONDS), "Reconnection timed out"); + assertTrue(client.isOpen(), "WebSocket should be open after reconnection"); + } +} From c388174613c8cf5e57b2f0b4bc98533b8b756ab6 Mon Sep 17 00:00:00 2001 From: Chintan Patel Date: Mon, 11 Nov 2024 21:49:42 -0600 Subject: [PATCH 09/18] Fixed conflict that was not removed from comments and the compiler didn't throw an error. --- docs/README.md | 8 -------- pom.xml | 7 ------- .../org/p2p/solanaj/ws/SubscriptionWebSocketClient.java | 3 --- 3 files changed, 18 deletions(-) diff --git a/docs/README.md b/docs/README.md index 5b11fb16..13f70b84 100644 --- a/docs/README.md +++ b/docs/README.md @@ -125,11 +125,7 @@ final Market solUsdcMarket = new MarketBuilder() final OrderBook bids = solUsdcMarket.getBidOrderBook(); ``` -<<<<<<< HEAD ##### Send a legacyTransaction with call to the "Memo" program -======= -### Send a Transaction with Memo Program ->>>>>>> main ```java // Create account from private key @@ -141,11 +137,7 @@ legacyTransaction.addInstruction( MemoProgram.writeUtf8(feePayer.getPublicKey(),"Hello from SolanaJ :)") ); -<<<<<<< HEAD String response = client.getApi().sendTransaction(legacyTransaction, feePayer); -======= -String response = client.getApi().sendTransaction(transaction, feePayer); ->>>>>>> main ``` ## 🤝 Contributing diff --git a/pom.xml b/pom.xml index dd28a9d6..6e0f855d 100644 --- a/pom.xml +++ b/pom.xml @@ -126,13 +126,6 @@ test - - org.mockito - mockito-core - 3.12.4 - test - - org.apache.httpcomponents.client5 httpclient5 diff --git a/src/main/java/org/p2p/solanaj/ws/SubscriptionWebSocketClient.java b/src/main/java/org/p2p/solanaj/ws/SubscriptionWebSocketClient.java index 0daf965a..d5bc49b0 100644 --- a/src/main/java/org/p2p/solanaj/ws/SubscriptionWebSocketClient.java +++ b/src/main/java/org/p2p/solanaj/ws/SubscriptionWebSocketClient.java @@ -106,7 +106,6 @@ public static SubscriptionWebSocketClient getExactPathInstance(String endpoint) } /** -<<<<<<< HEAD * Creates a SubscriptionWebSocketClient instance with a modified URI based on the provided endpoint. * * @param endpoint The endpoint URL @@ -126,8 +125,6 @@ public static SubscriptionWebSocketClient getInstance(String endpoint) { } /** -======= ->>>>>>> main * Constructs a SubscriptionWebSocketClient with the given server URI. * * @param serverURI The URI of the WebSocket server From 6f77a4c2d382802764a96401751c108c0e9d43b6 Mon Sep 17 00:00:00 2001 From: Chintan Patel Date: Tue, 12 Nov 2024 20:40:27 -0600 Subject: [PATCH 10/18] Updated getSignaturesForAddress Rpc call to include before parameter --- src/main/java/org/p2p/solanaj/rpc/RpcApi.java | 18 ++++++++++++++++++ .../solanaj/rpc/types/ConfirmedSignFAddr2.java | 10 ++++++++++ 2 files changed, 28 insertions(+) diff --git a/src/main/java/org/p2p/solanaj/rpc/RpcApi.java b/src/main/java/org/p2p/solanaj/rpc/RpcApi.java index b410dda6..7f9ac34c 100644 --- a/src/main/java/org/p2p/solanaj/rpc/RpcApi.java +++ b/src/main/java/org/p2p/solanaj/rpc/RpcApi.java @@ -246,6 +246,24 @@ public List getSignaturesForAddress(PublicKey account, int return result; } + public List getSignaturesForAddress(PublicKey account, int limit, Commitment commitment, + String before) + throws RpcException { + List params = new ArrayList<>(); + + params.add(account.toString()); + params.add(new ConfirmedSignFAddr2(before, limit, commitment)); + + List rawResult = client.call("getSignaturesForAddress", params, List.class); + + List result = new ArrayList<>(); + for (AbstractMap item : rawResult) { + result.add(new SignatureInformation(item)); + } + + return result; + } + public List getProgramAccounts(PublicKey account, long offset, String bytes) throws RpcException { List filters = new ArrayList<>(); filters.add(new Filter(new Memcmp(offset, bytes))); diff --git a/src/main/java/org/p2p/solanaj/rpc/types/ConfirmedSignFAddr2.java b/src/main/java/org/p2p/solanaj/rpc/types/ConfirmedSignFAddr2.java index 5766de1e..56f33aed 100644 --- a/src/main/java/org/p2p/solanaj/rpc/types/ConfirmedSignFAddr2.java +++ b/src/main/java/org/p2p/solanaj/rpc/types/ConfirmedSignFAddr2.java @@ -27,4 +27,14 @@ public ConfirmedSignFAddr2(String before, int limit, Commitment commitment) { this.limit = limit; this.commitment = commitment.getValue(); } + + @Override + public String toString() { + return "ConfirmedSignFAddr2{" + + "limit=" + limit + + ", before='" + before + '\'' + + ", until='" + until + '\'' + + ", commitment='" + commitment + '\'' + + '}'; + } } \ No newline at end of file From 646ec91a6348c1d06683ca9562fcd661b5f68f91 Mon Sep 17 00:00:00 2001 From: Chintan Patel Date: Sat, 30 Nov 2024 02:57:57 -0600 Subject: [PATCH 11/18] Added digital asset standard api getTokenAccounts to fetch all holders of a token --- src/main/java/org/p2p/solanaj/rpc/RpcApi.java | 5 ++ .../java/org/p2p/solanaj/rpc/RpcClient.java | 2 +- .../org/p2p/solanaj/rpc/types/RpcRequest.java | 4 +- .../rpc/types/TokenAccountResponse.java | 47 +++++++++++++++++++ .../rpc/types/config/TokenAccountConfig.java | 44 +++++++++++++++++ .../ws/SubscriptionWebSocketClient.java | 4 +- 6 files changed, 102 insertions(+), 4 deletions(-) create mode 100644 src/main/java/org/p2p/solanaj/rpc/types/TokenAccountResponse.java create mode 100644 src/main/java/org/p2p/solanaj/rpc/types/config/TokenAccountConfig.java diff --git a/src/main/java/org/p2p/solanaj/rpc/RpcApi.java b/src/main/java/org/p2p/solanaj/rpc/RpcApi.java index 7f9ac34c..c1cad51b 100644 --- a/src/main/java/org/p2p/solanaj/rpc/RpcApi.java +++ b/src/main/java/org/p2p/solanaj/rpc/RpcApi.java @@ -357,6 +357,11 @@ public List getProgramAccounts(PublicKey account, List m return result; } + public TokenAccountResponse getTokenAccounts(TokenAccountConfig tokenAccountConfig) + throws RpcException { + return client.call("getTokenAccounts", tokenAccountConfig, TokenAccountResponse.class); + } + public AccountInfo getAccountInfo(PublicKey account) throws RpcException { return getAccountInfo(account, new HashMap<>()); } diff --git a/src/main/java/org/p2p/solanaj/rpc/RpcClient.java b/src/main/java/org/p2p/solanaj/rpc/RpcClient.java index c01e6bed..42b8dd56 100644 --- a/src/main/java/org/p2p/solanaj/rpc/RpcClient.java +++ b/src/main/java/org/p2p/solanaj/rpc/RpcClient.java @@ -147,7 +147,7 @@ public RpcClient(String endpoint, String proxyHost, int proxyPort) { * @return the result of the RPC call * @throws RpcException if an error occurs during the RPC call */ - public T call(String method, List params, Class clazz) throws RpcException { + public T call(String method, Object params, Class clazz) throws RpcException { RpcRequest rpcRequest = new RpcRequest(method, params); JsonAdapter rpcRequestJsonAdapter = moshi.adapter(RpcRequest.class); diff --git a/src/main/java/org/p2p/solanaj/rpc/types/RpcRequest.java b/src/main/java/org/p2p/solanaj/rpc/types/RpcRequest.java index 7199bfb2..5ea10291 100644 --- a/src/main/java/org/p2p/solanaj/rpc/types/RpcRequest.java +++ b/src/main/java/org/p2p/solanaj/rpc/types/RpcRequest.java @@ -18,7 +18,7 @@ public class RpcRequest { private final String method; @Json(name = "params") - private final List params; + private final Object params; @Json(name = "id") private final String id = UUID.randomUUID().toString(); @@ -27,7 +27,7 @@ public RpcRequest(String method) { this(method, null); } - public RpcRequest(String method, List params) { + public RpcRequest(String method, Object params) { this.method = method; this.params = params; } diff --git a/src/main/java/org/p2p/solanaj/rpc/types/TokenAccountResponse.java b/src/main/java/org/p2p/solanaj/rpc/types/TokenAccountResponse.java new file mode 100644 index 00000000..9ab556fa --- /dev/null +++ b/src/main/java/org/p2p/solanaj/rpc/types/TokenAccountResponse.java @@ -0,0 +1,47 @@ +package org.p2p.solanaj.rpc.types; + +import java.util.List; + +import com.squareup.moshi.Json; + +import lombok.Getter; +import lombok.ToString; + +@Getter +@ToString +public class TokenAccountResponse { + + @Json(name = "total") + private int total; + + @Json(name = "limit") + private int limit; + + @Json(name = "cursor") + private String cursor; + + @Json(name = "token_accounts") + private List tokenAccounts; + + @Getter + @ToString + public static class TokenAccount { + @Json(name = "address") + private String address; + + @Json(name = "mint") + private String mint; + + @Json(name = "owner") + private String owner; + + @Json(name = "amount") + private Long amount; + + @Json(name = "delegated_amount") + private Long delegatedAmount; + + @Json(name = "frozen") + private Boolean frozen; + } +} diff --git a/src/main/java/org/p2p/solanaj/rpc/types/config/TokenAccountConfig.java b/src/main/java/org/p2p/solanaj/rpc/types/config/TokenAccountConfig.java new file mode 100644 index 00000000..f263b573 --- /dev/null +++ b/src/main/java/org/p2p/solanaj/rpc/types/config/TokenAccountConfig.java @@ -0,0 +1,44 @@ +package org.p2p.solanaj.rpc.types.config; + +import lombok.Setter; + +public class TokenAccountConfig { + + @Setter + private String mint; + + @Setter + private String owner = null; + + @Setter + private Integer page = null; + + @Setter + private Integer limit = null; + + @Setter + private String cursor = null; + + @Setter + private String before = null; + + @Setter + private String after = null; + + public TokenAccountConfig() {} + + public TokenAccountConfig(String mint, Integer limit, String cursor) { + this.mint = mint; + this.limit = limit; + this.cursor = cursor; + } + + public TokenAccountConfig(String mint, Integer limit) { + this.mint = mint; + this.cursor = cursor; + } + + public TokenAccountConfig(String mint) { + this.mint = mint; + } +} \ No newline at end of file diff --git a/src/main/java/org/p2p/solanaj/ws/SubscriptionWebSocketClient.java b/src/main/java/org/p2p/solanaj/ws/SubscriptionWebSocketClient.java index d5bc49b0..64d94cb5 100644 --- a/src/main/java/org/p2p/solanaj/ws/SubscriptionWebSocketClient.java +++ b/src/main/java/org/p2p/solanaj/ws/SubscriptionWebSocketClient.java @@ -730,7 +730,9 @@ private String getUnsubscribeMethod(String subscribeMethod) { */ public String getSubscriptionId(String account) { for (Map.Entry entry : activeSubscriptions.entrySet()) { - if (entry.getValue().request.getParams().get(0).equals(account)) { + Object params = entry.getValue().request.getParams(); + List paramsList = (List) params; + if (paramsList.get(0).equals(account)) { return entry.getKey(); } } From 969733492c09846a24aefe14a1efa80a63e691f9 Mon Sep 17 00:00:00 2001 From: Chintan Patel Date: Sat, 30 Nov 2024 19:59:18 -0600 Subject: [PATCH 12/18] Added digital asset standard api searchAssets method to search assets based on assets config --- src/main/java/org/p2p/solanaj/rpc/RpcApi.java | 8 +- .../org/p2p/solanaj/rpc/types/Assets.java | 282 ++++++++++++++++++ .../rpc/types/SearchAssetsResponse.java | 10 + ...sponse.java => TokenAccountsResponse.java} | 2 +- .../rpc/types/config/SearchAssetsConfig.java | 140 +++++++++ ...ntConfig.java => TokenAccountsConfig.java} | 22 +- 6 files changed, 447 insertions(+), 17 deletions(-) create mode 100644 src/main/java/org/p2p/solanaj/rpc/types/Assets.java create mode 100644 src/main/java/org/p2p/solanaj/rpc/types/SearchAssetsResponse.java rename src/main/java/org/p2p/solanaj/rpc/types/{TokenAccountResponse.java => TokenAccountsResponse.java} (95%) create mode 100644 src/main/java/org/p2p/solanaj/rpc/types/config/SearchAssetsConfig.java rename src/main/java/org/p2p/solanaj/rpc/types/config/{TokenAccountConfig.java => TokenAccountsConfig.java} (55%) diff --git a/src/main/java/org/p2p/solanaj/rpc/RpcApi.java b/src/main/java/org/p2p/solanaj/rpc/RpcApi.java index c1cad51b..e148d4d7 100644 --- a/src/main/java/org/p2p/solanaj/rpc/RpcApi.java +++ b/src/main/java/org/p2p/solanaj/rpc/RpcApi.java @@ -357,9 +357,13 @@ public List getProgramAccounts(PublicKey account, List m return result; } - public TokenAccountResponse getTokenAccounts(TokenAccountConfig tokenAccountConfig) + public TokenAccountsResponse getTokenAccounts(TokenAccountsConfig tokenAccountsConfig) throws RpcException { - return client.call("getTokenAccounts", tokenAccountConfig, TokenAccountResponse.class); + return client.call("getTokenAccounts", tokenAccountsConfig, TokenAccountsResponse.class); + } + + public SearchAssetsResponse searchAssets(SearchAssetsConfig searchAssetsConfig) throws RpcException { + return client.call("searchAssets", searchAssetsConfig, SearchAssetsResponse.class); } public AccountInfo getAccountInfo(PublicKey account) throws RpcException { diff --git a/src/main/java/org/p2p/solanaj/rpc/types/Assets.java b/src/main/java/org/p2p/solanaj/rpc/types/Assets.java new file mode 100644 index 00000000..99bfbc58 --- /dev/null +++ b/src/main/java/org/p2p/solanaj/rpc/types/Assets.java @@ -0,0 +1,282 @@ +package org.p2p.solanaj.rpc.types; + +import com.squareup.moshi.Json; +import lombok.Getter; +import lombok.ToString; +import org.p2p.solanaj.rpc.types.config.SearchAssetsConfig; + +import java.util.List; + +@Getter +@ToString +public class Assets { + private Integer total; + private Integer limit; + private Integer page; + @Json(name = "items") + private List items; + private NativeBalance nativeBalance; + + @Getter + @ToString + static class Asset { + private SearchAssetsConfig.Interface iterface; + private String id; + private Object content; + private List authorities; + private Object compression; + private Object grouping; + private Object royalty; + private List creators; + private Object ownership; + private Boolean mutable; + private Boolean burnt; + @Json(name = "mint_extensions") + private MintExtensions mintExtensions; + private Supply supply = null; + @Json(name = "token_info") + private TokenInfo tokenInfo; + private Inscription inscription; + private Object spl20; + } + + @Getter + @ToString + static class MintExtensions { + @Json(name = "confidential_transfer_mint") + private ConfidentialTransferMint confidentialTransferMint; + @Json(name = "confidential_transfer_fee_config") + private ConfidentialTransferFeeConfig confidentialTransferFeeConfig; + @Json(name = "transfer_fee_config") + private TransferFeeConfig transferFeeConfig; + @Json(name = "metadata_pointer") + private MetadataPointer metadataPointer; + @Json(name = "mint_close_authority") + private MintCloseAuthority mintCloseAuthority; + @Json(name = "permanent_delegate") + private PermanentDelegate permanentDelegate; + @Json(name = "transfer_hook") + private TransferHook transferHook; + @Json(name = "interest_bearing_config") + private InterestBearingConfig interestBearingConfig; + @Json(name = "default_account_state") + private DefaultAccountState defaultAccountState; + @Json(name = "confidential_transfer_account") + private ConfidentialTransferAccount confidentialTransferAccount; + @Json(name = "metadataobject") + private Metadata metadata; + } + + @Getter + @ToString + static class ConfidentialTransferMint { + @Json(name = "authority") + private String authority; + @Json(name = "auto_approve_new_accounts") + private Boolean autoApproveNewAccounts; + @Json(name = "auditor_elgamal_pubkey") + private String auditorElgamalPubkey; + } + + @Getter + @ToString + static class ConfidentialTransferFeeConfig { + @Json(name = "authority") + private String authority; + @Json(name = "withdraw_withheld_authority_elgamal_pubkey") + private String withdrawWithheldAuthorityElgamalPubkey; + @Json(name = "harvest_to_mint_enabled") + private Boolean harvestToMintEnabled; + @Json(name = "withheld_amount") + private String withheldAmount; + } + + @Getter + @ToString + static class TransferFeeConfig { + @Json(name = "transfer_fee_config_authority") + private String transferFeeConfigAuthority; + @Json(name = "withdraw_withheld_authority") + private String withdrawWithheldAuthority; + @Json(name = "withheld_amount") + private Double withheldAmount; + @Json(name = "older_transfer_fee") + private OlderTransferFee olderTransferFee; + @Json(name = "newer_transfer_fee") + private NewerTransferFee newerTransferFee; + } + + @Getter + @ToString + static class OlderTransferFee { + private String epoch; + @Json(name = "maximum_fee") + private String maximumFee; + @Json(name = "transfer_fee_basis_points") + private String transferFeeBasisPoints; + } + + @Getter + @ToString + static class NewerTransferFee { + private String epoch; + } + + @Getter + @ToString + static class MetadataPointer { + private String authority; + @Json(name = "metadata_address") + private String metadataAddress; + } + + @Getter + @ToString + static class MintCloseAuthority { + @Json(name = "close_authority") + private String closeAuthority; + } + + @Getter + @ToString + static class PermanentDelegate { + private String delegate; + } + + @Getter + @ToString + static class TransferHook { + private String authority; + @Json(name = "program_id") + private String programId; + } + + @Getter + @ToString + static class InterestBearingConfig { + @Json(name = "rate_authority") + private String rateAuthority; + @Json(name = "initialization_timestamp") + private Double initializationTimestamp; + @Json(name = "pre_update_average_rate") + private Double preUpdateAverageRate; + @Json(name = "last_update_timestamp") + private Double lastUpdateTimestamp; + @Json(name = "current_rate") + private Double currentRate; + } + + @Getter + @ToString + static class DefaultAccountState { + private String state; + } + + @Getter + @ToString + static class ConfidentialTransferAccount { + private Boolean approved; + @Json(name = "elgamal_pubkey") + private String elgamalPubkey; + @Json(name = "pending_balance_lo") + private String pendingBalanceLo; + @Json(name = "pending_balance_hi") + private String pendingBalanceHi; + @Json(name = "available_balance") + private String availableBalance; + @Json(name = "decryptable_available_balance") + private String decryptableAvailableBalance; + @Json(name = "allow_confidential_credits") + private Boolean allowConfidentialCredits; + @Json(name = "allow_non_confidential_credits") + private Boolean allowNonConfidentialCredits; + @Json(name = "pending_balance_credit_counter") + private Integer pendingBalanceCreditCounter; + @Json(name = "maximum_pending_balance_credit_counter") + private Integer maximumPendingBalanceCreditCounter; + @Json(name = "expected_pending_balance_credit_counter") + private Integer expectedPendingBalanceCreditCounter; + @Json(name = "actual_pending_balance_credit_counter") + private Integer actualPendingBalanceCreditCounter; + } + + @Getter + @ToString + static class Metadata { + @Json(name = "update_authority") + private String updateAuthority; + private String mint; + private String name; + private String symbol; + private String uri; + @Json(name = "additional_metadata") + private List additionalMetadata; + } + + @Getter + @ToString + static class KeyValue { + private String key; + private String value; + } + + @Getter + @ToString + static class Supply { + @Json(name = "print_max_supply") + private Long printMaxSupply; + @Json(name = "print_current_supply") + private Long printCurrentSupply; + @Json(name = "edition_nonce") + private Integer editionNonce; + @Json(name = "edition_number") + private Integer editionNumber = null; + } + + @Getter + @ToString + static class TokenInfo { + private String symbol; + private Long balance; + private Long supply; + private Integer decimals; + @Json(name = "token_program") + private String tokenProgram; + @Json(name = "associated_token_address") + private String associatedTokenAddress; + @Json(name = "price_info") + private PriceInfo priceInfo; + } + + @Getter + @ToString + static class PriceInfo { + @Json(name = "price_per_token") + private Double pricePerToken; + @Json(name = "total_price") + private Double totalPrice; + private String currency; + } + + @Getter + @ToString + static class Inscription { + private Integer order; + private Integer size; + private String contentType; + private String encoding; + private String validationHash; + private String inscriptionDataAccount; + private String authority; + } + + @Getter + @ToString + static class NativeBalance { + private Long lamports; + @Json(name = "price_per_sol") + private Double pricePerSol; + @Json(name = "total_price") + private Double totalPrice; + } +} diff --git a/src/main/java/org/p2p/solanaj/rpc/types/SearchAssetsResponse.java b/src/main/java/org/p2p/solanaj/rpc/types/SearchAssetsResponse.java new file mode 100644 index 00000000..02c21183 --- /dev/null +++ b/src/main/java/org/p2p/solanaj/rpc/types/SearchAssetsResponse.java @@ -0,0 +1,10 @@ +package org.p2p.solanaj.rpc.types; + +import lombok.Getter; +import lombok.ToString; + +@Getter +@ToString +public class SearchAssetsResponse { + private Assets assets; +} diff --git a/src/main/java/org/p2p/solanaj/rpc/types/TokenAccountResponse.java b/src/main/java/org/p2p/solanaj/rpc/types/TokenAccountsResponse.java similarity index 95% rename from src/main/java/org/p2p/solanaj/rpc/types/TokenAccountResponse.java rename to src/main/java/org/p2p/solanaj/rpc/types/TokenAccountsResponse.java index 9ab556fa..df51ef55 100644 --- a/src/main/java/org/p2p/solanaj/rpc/types/TokenAccountResponse.java +++ b/src/main/java/org/p2p/solanaj/rpc/types/TokenAccountsResponse.java @@ -9,7 +9,7 @@ @Getter @ToString -public class TokenAccountResponse { +public class TokenAccountsResponse { @Json(name = "total") private int total; diff --git a/src/main/java/org/p2p/solanaj/rpc/types/config/SearchAssetsConfig.java b/src/main/java/org/p2p/solanaj/rpc/types/config/SearchAssetsConfig.java new file mode 100644 index 00000000..346f4cb5 --- /dev/null +++ b/src/main/java/org/p2p/solanaj/rpc/types/config/SearchAssetsConfig.java @@ -0,0 +1,140 @@ +package org.p2p.solanaj.rpc.types.config; + +import com.squareup.moshi.Json; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +@Setter +public class SearchAssetsConfig { + + private Integer page = null; + + private String authorityAddress = null; + + private Integer limit = null; + + @Json(name = "sortBy") + private SortByObject sortBy = null; + + private Boolean compressed = null; + + private Boolean compressible = null; + + private Integer delegate = null; + + private String creatorAddress = null; + + private Boolean creatorVerified = null; + + private List grouping = null; + + private Long supply = null; + + private String supplyMint = null; + + private Boolean frozen = null; + + private Boolean burnt = null; + + @Json(name = "interface") + private Interface anInterface = null; + + private String ownerAddress = null; + + private String royaltyTargetType = null; + + private Integer royanltyTarget = null; + + private Integer royaltyAmount = null; + + private Integer ownerType = null; + + private String before = null; + + private String after = null; + + private Options options = null; + + private TokenType tokenType = TokenType.FUNGIBLE; + + public SearchAssetsConfig() {} + + public SearchAssetsConfig(String ownerAddress, int limit, int page) { + this.ownerAddress = ownerAddress; + this.limit = limit; + this.page = page; + } + + public SearchAssetsConfig(String ownerAddress, int limit) { + this.ownerAddress = ownerAddress; + this.limit = limit; + } + + public SearchAssetsConfig(String ownerAddress) { + this.ownerAddress = ownerAddress; + } + + @Getter + @Setter + static class SortByObject { + @Getter + @AllArgsConstructor + enum SortBy { + CREATED("created"), + RECENT_ACTION("recent_action"), + UPDATED("updated"), + NONE("none"); + private final String value; + } + @Getter + @AllArgsConstructor + enum SortDirection { + ASC("asc"), + DESC("desc"); + private final String value; + } + private SortBy sortBy = SortBy.NONE; + private SortDirection sortDirection = SortDirection.ASC; + } + + @Getter + @Setter + static class Options { + private Boolean showUnverifiedCollections = null; + private Boolean showCollectionMetadata = null; + private Boolean showGrandTotal = null; + private Boolean showNativeBalance = null; + private Boolean showInscription = null; + private Boolean showZeroBalance = null; + } + + @Getter + @AllArgsConstructor + public enum Interface { + V1_NFT("V1_NFT"), + V1_PRINT("V1_PRINT"), + LEGACY_NFT("LEGACY_NFT"), + V2_NFT("V2_NFT"), + FUNGIBLE_ASSET("FungibleAsset"), + FUNGIBLE_TOKEN("FungibleToken"), + CUSTOM("Custom"), + IDENTITY("Identity"), + EXECUTABLE("Executable"), + PROGRAMMABLE_NFT("ProgrammableNFT"); + private final String value; + } + + @Getter + @AllArgsConstructor + enum TokenType { + FUNGIBLE("fungible"), + NON_FUNGIBLE("nonFungible"), + REGULAR_NFT("regularNFT"), + COMPRESSED_NFT("compressedNFT"), + ALL("all"); + private final String value; + } +} diff --git a/src/main/java/org/p2p/solanaj/rpc/types/config/TokenAccountConfig.java b/src/main/java/org/p2p/solanaj/rpc/types/config/TokenAccountsConfig.java similarity index 55% rename from src/main/java/org/p2p/solanaj/rpc/types/config/TokenAccountConfig.java rename to src/main/java/org/p2p/solanaj/rpc/types/config/TokenAccountsConfig.java index f263b573..eb91ce4e 100644 --- a/src/main/java/org/p2p/solanaj/rpc/types/config/TokenAccountConfig.java +++ b/src/main/java/org/p2p/solanaj/rpc/types/config/TokenAccountsConfig.java @@ -2,43 +2,37 @@ import lombok.Setter; -public class TokenAccountConfig { +@Setter +public class TokenAccountsConfig { - @Setter private String mint; - @Setter private String owner = null; - @Setter private Integer page = null; - @Setter private Integer limit = null; - @Setter private String cursor = null; - @Setter private String before = null; - @Setter private String after = null; - public TokenAccountConfig() {} + public TokenAccountsConfig() {} - public TokenAccountConfig(String mint, Integer limit, String cursor) { + public TokenAccountsConfig(String mint, Integer limit, String cursor) { this.mint = mint; this.limit = limit; this.cursor = cursor; } - public TokenAccountConfig(String mint, Integer limit) { + public TokenAccountsConfig(String mint, Integer limit) { this.mint = mint; - this.cursor = cursor; + this.limit = limit; } - public TokenAccountConfig(String mint) { + public TokenAccountsConfig(String mint) { this.mint = mint; } -} \ No newline at end of file +} From 301a1500197a8837689626f986fc754dfb8e3f38 Mon Sep 17 00:00:00 2001 From: Chintan Patel Date: Mon, 2 Dec 2024 22:26:35 -0600 Subject: [PATCH 13/18] Added cursor for searchAssets rpc das call --- src/main/java/org/p2p/solanaj/rpc/types/Assets.java | 1 + .../p2p/solanaj/rpc/types/config/SearchAssetsConfig.java | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/src/main/java/org/p2p/solanaj/rpc/types/Assets.java b/src/main/java/org/p2p/solanaj/rpc/types/Assets.java index 99bfbc58..1d6d3cd9 100644 --- a/src/main/java/org/p2p/solanaj/rpc/types/Assets.java +++ b/src/main/java/org/p2p/solanaj/rpc/types/Assets.java @@ -15,6 +15,7 @@ public class Assets { private Integer page; @Json(name = "items") private List items; + private String cursor; private NativeBalance nativeBalance; @Getter diff --git a/src/main/java/org/p2p/solanaj/rpc/types/config/SearchAssetsConfig.java b/src/main/java/org/p2p/solanaj/rpc/types/config/SearchAssetsConfig.java index 346f4cb5..0dfb4934 100644 --- a/src/main/java/org/p2p/solanaj/rpc/types/config/SearchAssetsConfig.java +++ b/src/main/java/org/p2p/solanaj/rpc/types/config/SearchAssetsConfig.java @@ -12,6 +12,8 @@ public class SearchAssetsConfig { private Integer page = null; + private String cursor = null; + private String authorityAddress = null; private Integer limit = null; @@ -68,6 +70,12 @@ public SearchAssetsConfig(String ownerAddress, int limit, int page) { this.page = page; } + public SearchAssetsConfig(String ownerAddress, int limit, String cursor) { + this.ownerAddress = ownerAddress; + this.limit = limit; + this.cursor = cursor; + } + public SearchAssetsConfig(String ownerAddress, int limit) { this.ownerAddress = ownerAddress; this.limit = limit; From c377e2dbd286a3f8012c27607435d5d0266016be Mon Sep 17 00:00:00 2001 From: Chintan Patel Date: Mon, 2 Dec 2024 22:32:18 -0600 Subject: [PATCH 14/18] Made Holder Asset class public --- src/main/java/org/p2p/solanaj/rpc/types/Assets.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/p2p/solanaj/rpc/types/Assets.java b/src/main/java/org/p2p/solanaj/rpc/types/Assets.java index 1d6d3cd9..bb17b3ed 100644 --- a/src/main/java/org/p2p/solanaj/rpc/types/Assets.java +++ b/src/main/java/org/p2p/solanaj/rpc/types/Assets.java @@ -20,7 +20,7 @@ public class Assets { @Getter @ToString - static class Asset { + public static class Asset { private SearchAssetsConfig.Interface iterface; private String id; private Object content; From 0f4f828a2f68d1327936ac1968e5c84c83ff32cb Mon Sep 17 00:00:00 2001 From: Chintan Patel Date: Mon, 2 Dec 2024 23:27:08 -0600 Subject: [PATCH 15/18] Changed enum tokenType to string to be correctly parsed by rpc request before sending api call --- .../org/p2p/solanaj/rpc/types/config/SearchAssetsConfig.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/p2p/solanaj/rpc/types/config/SearchAssetsConfig.java b/src/main/java/org/p2p/solanaj/rpc/types/config/SearchAssetsConfig.java index 0dfb4934..a4b2a1e2 100644 --- a/src/main/java/org/p2p/solanaj/rpc/types/config/SearchAssetsConfig.java +++ b/src/main/java/org/p2p/solanaj/rpc/types/config/SearchAssetsConfig.java @@ -60,7 +60,7 @@ public class SearchAssetsConfig { private Options options = null; - private TokenType tokenType = TokenType.FUNGIBLE; + private String tokenType = TokenType.FUNGIBLE.getName(); public SearchAssetsConfig() {} @@ -143,6 +143,7 @@ enum TokenType { REGULAR_NFT("regularNFT"), COMPRESSED_NFT("compressedNFT"), ALL("all"); - private final String value; + @Json + private final String name; } } From a6531a95fbcc1daac023bf6a54d3db98f28be78e Mon Sep 17 00:00:00 2001 From: Chintan Patel Date: Mon, 2 Dec 2024 23:41:26 -0600 Subject: [PATCH 16/18] Modified the search asset response dto --- .../org/p2p/solanaj/rpc/types/Assets.java | 283 ------------------ .../rpc/types/SearchAssetsResponse.java | 275 ++++++++++++++++- 2 files changed, 274 insertions(+), 284 deletions(-) delete mode 100644 src/main/java/org/p2p/solanaj/rpc/types/Assets.java diff --git a/src/main/java/org/p2p/solanaj/rpc/types/Assets.java b/src/main/java/org/p2p/solanaj/rpc/types/Assets.java deleted file mode 100644 index bb17b3ed..00000000 --- a/src/main/java/org/p2p/solanaj/rpc/types/Assets.java +++ /dev/null @@ -1,283 +0,0 @@ -package org.p2p.solanaj.rpc.types; - -import com.squareup.moshi.Json; -import lombok.Getter; -import lombok.ToString; -import org.p2p.solanaj.rpc.types.config.SearchAssetsConfig; - -import java.util.List; - -@Getter -@ToString -public class Assets { - private Integer total; - private Integer limit; - private Integer page; - @Json(name = "items") - private List items; - private String cursor; - private NativeBalance nativeBalance; - - @Getter - @ToString - public static class Asset { - private SearchAssetsConfig.Interface iterface; - private String id; - private Object content; - private List authorities; - private Object compression; - private Object grouping; - private Object royalty; - private List creators; - private Object ownership; - private Boolean mutable; - private Boolean burnt; - @Json(name = "mint_extensions") - private MintExtensions mintExtensions; - private Supply supply = null; - @Json(name = "token_info") - private TokenInfo tokenInfo; - private Inscription inscription; - private Object spl20; - } - - @Getter - @ToString - static class MintExtensions { - @Json(name = "confidential_transfer_mint") - private ConfidentialTransferMint confidentialTransferMint; - @Json(name = "confidential_transfer_fee_config") - private ConfidentialTransferFeeConfig confidentialTransferFeeConfig; - @Json(name = "transfer_fee_config") - private TransferFeeConfig transferFeeConfig; - @Json(name = "metadata_pointer") - private MetadataPointer metadataPointer; - @Json(name = "mint_close_authority") - private MintCloseAuthority mintCloseAuthority; - @Json(name = "permanent_delegate") - private PermanentDelegate permanentDelegate; - @Json(name = "transfer_hook") - private TransferHook transferHook; - @Json(name = "interest_bearing_config") - private InterestBearingConfig interestBearingConfig; - @Json(name = "default_account_state") - private DefaultAccountState defaultAccountState; - @Json(name = "confidential_transfer_account") - private ConfidentialTransferAccount confidentialTransferAccount; - @Json(name = "metadataobject") - private Metadata metadata; - } - - @Getter - @ToString - static class ConfidentialTransferMint { - @Json(name = "authority") - private String authority; - @Json(name = "auto_approve_new_accounts") - private Boolean autoApproveNewAccounts; - @Json(name = "auditor_elgamal_pubkey") - private String auditorElgamalPubkey; - } - - @Getter - @ToString - static class ConfidentialTransferFeeConfig { - @Json(name = "authority") - private String authority; - @Json(name = "withdraw_withheld_authority_elgamal_pubkey") - private String withdrawWithheldAuthorityElgamalPubkey; - @Json(name = "harvest_to_mint_enabled") - private Boolean harvestToMintEnabled; - @Json(name = "withheld_amount") - private String withheldAmount; - } - - @Getter - @ToString - static class TransferFeeConfig { - @Json(name = "transfer_fee_config_authority") - private String transferFeeConfigAuthority; - @Json(name = "withdraw_withheld_authority") - private String withdrawWithheldAuthority; - @Json(name = "withheld_amount") - private Double withheldAmount; - @Json(name = "older_transfer_fee") - private OlderTransferFee olderTransferFee; - @Json(name = "newer_transfer_fee") - private NewerTransferFee newerTransferFee; - } - - @Getter - @ToString - static class OlderTransferFee { - private String epoch; - @Json(name = "maximum_fee") - private String maximumFee; - @Json(name = "transfer_fee_basis_points") - private String transferFeeBasisPoints; - } - - @Getter - @ToString - static class NewerTransferFee { - private String epoch; - } - - @Getter - @ToString - static class MetadataPointer { - private String authority; - @Json(name = "metadata_address") - private String metadataAddress; - } - - @Getter - @ToString - static class MintCloseAuthority { - @Json(name = "close_authority") - private String closeAuthority; - } - - @Getter - @ToString - static class PermanentDelegate { - private String delegate; - } - - @Getter - @ToString - static class TransferHook { - private String authority; - @Json(name = "program_id") - private String programId; - } - - @Getter - @ToString - static class InterestBearingConfig { - @Json(name = "rate_authority") - private String rateAuthority; - @Json(name = "initialization_timestamp") - private Double initializationTimestamp; - @Json(name = "pre_update_average_rate") - private Double preUpdateAverageRate; - @Json(name = "last_update_timestamp") - private Double lastUpdateTimestamp; - @Json(name = "current_rate") - private Double currentRate; - } - - @Getter - @ToString - static class DefaultAccountState { - private String state; - } - - @Getter - @ToString - static class ConfidentialTransferAccount { - private Boolean approved; - @Json(name = "elgamal_pubkey") - private String elgamalPubkey; - @Json(name = "pending_balance_lo") - private String pendingBalanceLo; - @Json(name = "pending_balance_hi") - private String pendingBalanceHi; - @Json(name = "available_balance") - private String availableBalance; - @Json(name = "decryptable_available_balance") - private String decryptableAvailableBalance; - @Json(name = "allow_confidential_credits") - private Boolean allowConfidentialCredits; - @Json(name = "allow_non_confidential_credits") - private Boolean allowNonConfidentialCredits; - @Json(name = "pending_balance_credit_counter") - private Integer pendingBalanceCreditCounter; - @Json(name = "maximum_pending_balance_credit_counter") - private Integer maximumPendingBalanceCreditCounter; - @Json(name = "expected_pending_balance_credit_counter") - private Integer expectedPendingBalanceCreditCounter; - @Json(name = "actual_pending_balance_credit_counter") - private Integer actualPendingBalanceCreditCounter; - } - - @Getter - @ToString - static class Metadata { - @Json(name = "update_authority") - private String updateAuthority; - private String mint; - private String name; - private String symbol; - private String uri; - @Json(name = "additional_metadata") - private List additionalMetadata; - } - - @Getter - @ToString - static class KeyValue { - private String key; - private String value; - } - - @Getter - @ToString - static class Supply { - @Json(name = "print_max_supply") - private Long printMaxSupply; - @Json(name = "print_current_supply") - private Long printCurrentSupply; - @Json(name = "edition_nonce") - private Integer editionNonce; - @Json(name = "edition_number") - private Integer editionNumber = null; - } - - @Getter - @ToString - static class TokenInfo { - private String symbol; - private Long balance; - private Long supply; - private Integer decimals; - @Json(name = "token_program") - private String tokenProgram; - @Json(name = "associated_token_address") - private String associatedTokenAddress; - @Json(name = "price_info") - private PriceInfo priceInfo; - } - - @Getter - @ToString - static class PriceInfo { - @Json(name = "price_per_token") - private Double pricePerToken; - @Json(name = "total_price") - private Double totalPrice; - private String currency; - } - - @Getter - @ToString - static class Inscription { - private Integer order; - private Integer size; - private String contentType; - private String encoding; - private String validationHash; - private String inscriptionDataAccount; - private String authority; - } - - @Getter - @ToString - static class NativeBalance { - private Long lamports; - @Json(name = "price_per_sol") - private Double pricePerSol; - @Json(name = "total_price") - private Double totalPrice; - } -} diff --git a/src/main/java/org/p2p/solanaj/rpc/types/SearchAssetsResponse.java b/src/main/java/org/p2p/solanaj/rpc/types/SearchAssetsResponse.java index 02c21183..ecf95aeb 100644 --- a/src/main/java/org/p2p/solanaj/rpc/types/SearchAssetsResponse.java +++ b/src/main/java/org/p2p/solanaj/rpc/types/SearchAssetsResponse.java @@ -1,10 +1,283 @@ package org.p2p.solanaj.rpc.types; +import com.squareup.moshi.Json; import lombok.Getter; import lombok.ToString; +import org.p2p.solanaj.rpc.types.config.SearchAssetsConfig; + +import java.util.List; @Getter @ToString public class SearchAssetsResponse { - private Assets assets; + private Integer total; + private Integer limit; + private Integer page; + @Json(name = "items") + private List items; + private String cursor; + private NativeBalance nativeBalance; + + @Getter + @ToString + public static class Asset { + private SearchAssetsConfig.Interface iterface; + private String id; + private Object content; + private List authorities; + private Object compression; + private Object grouping; + private Object royalty; + private List creators; + private Object ownership; + private Boolean mutable; + private Boolean burnt; + @Json(name = "mint_extensions") + private MintExtensions mintExtensions; + private Supply supply = null; + @Json(name = "token_info") + private TokenInfo tokenInfo; + private Inscription inscription; + private Object spl20; + } + + @Getter + @ToString + static class MintExtensions { + @Json(name = "confidential_transfer_mint") + private ConfidentialTransferMint confidentialTransferMint; + @Json(name = "confidential_transfer_fee_config") + private ConfidentialTransferFeeConfig confidentialTransferFeeConfig; + @Json(name = "transfer_fee_config") + private TransferFeeConfig transferFeeConfig; + @Json(name = "metadata_pointer") + private MetadataPointer metadataPointer; + @Json(name = "mint_close_authority") + private MintCloseAuthority mintCloseAuthority; + @Json(name = "permanent_delegate") + private PermanentDelegate permanentDelegate; + @Json(name = "transfer_hook") + private TransferHook transferHook; + @Json(name = "interest_bearing_config") + private InterestBearingConfig interestBearingConfig; + @Json(name = "default_account_state") + private DefaultAccountState defaultAccountState; + @Json(name = "confidential_transfer_account") + private ConfidentialTransferAccount confidentialTransferAccount; + @Json(name = "metadataobject") + private Metadata metadata; + } + + @Getter + @ToString + static class ConfidentialTransferMint { + @Json(name = "authority") + private String authority; + @Json(name = "auto_approve_new_accounts") + private Boolean autoApproveNewAccounts; + @Json(name = "auditor_elgamal_pubkey") + private String auditorElgamalPubkey; + } + + @Getter + @ToString + static class ConfidentialTransferFeeConfig { + @Json(name = "authority") + private String authority; + @Json(name = "withdraw_withheld_authority_elgamal_pubkey") + private String withdrawWithheldAuthorityElgamalPubkey; + @Json(name = "harvest_to_mint_enabled") + private Boolean harvestToMintEnabled; + @Json(name = "withheld_amount") + private String withheldAmount; + } + + @Getter + @ToString + static class TransferFeeConfig { + @Json(name = "transfer_fee_config_authority") + private String transferFeeConfigAuthority; + @Json(name = "withdraw_withheld_authority") + private String withdrawWithheldAuthority; + @Json(name = "withheld_amount") + private Double withheldAmount; + @Json(name = "older_transfer_fee") + private OlderTransferFee olderTransferFee; + @Json(name = "newer_transfer_fee") + private NewerTransferFee newerTransferFee; + } + + @Getter + @ToString + static class OlderTransferFee { + private String epoch; + @Json(name = "maximum_fee") + private String maximumFee; + @Json(name = "transfer_fee_basis_points") + private String transferFeeBasisPoints; + } + + @Getter + @ToString + static class NewerTransferFee { + private String epoch; + } + + @Getter + @ToString + static class MetadataPointer { + private String authority; + @Json(name = "metadata_address") + private String metadataAddress; + } + + @Getter + @ToString + static class MintCloseAuthority { + @Json(name = "close_authority") + private String closeAuthority; + } + + @Getter + @ToString + static class PermanentDelegate { + private String delegate; + } + + @Getter + @ToString + static class TransferHook { + private String authority; + @Json(name = "program_id") + private String programId; + } + + @Getter + @ToString + static class InterestBearingConfig { + @Json(name = "rate_authority") + private String rateAuthority; + @Json(name = "initialization_timestamp") + private Double initializationTimestamp; + @Json(name = "pre_update_average_rate") + private Double preUpdateAverageRate; + @Json(name = "last_update_timestamp") + private Double lastUpdateTimestamp; + @Json(name = "current_rate") + private Double currentRate; + } + + @Getter + @ToString + static class DefaultAccountState { + private String state; + } + + @Getter + @ToString + static class ConfidentialTransferAccount { + private Boolean approved; + @Json(name = "elgamal_pubkey") + private String elgamalPubkey; + @Json(name = "pending_balance_lo") + private String pendingBalanceLo; + @Json(name = "pending_balance_hi") + private String pendingBalanceHi; + @Json(name = "available_balance") + private String availableBalance; + @Json(name = "decryptable_available_balance") + private String decryptableAvailableBalance; + @Json(name = "allow_confidential_credits") + private Boolean allowConfidentialCredits; + @Json(name = "allow_non_confidential_credits") + private Boolean allowNonConfidentialCredits; + @Json(name = "pending_balance_credit_counter") + private Integer pendingBalanceCreditCounter; + @Json(name = "maximum_pending_balance_credit_counter") + private Integer maximumPendingBalanceCreditCounter; + @Json(name = "expected_pending_balance_credit_counter") + private Integer expectedPendingBalanceCreditCounter; + @Json(name = "actual_pending_balance_credit_counter") + private Integer actualPendingBalanceCreditCounter; + } + + @Getter + @ToString + static class Metadata { + @Json(name = "update_authority") + private String updateAuthority; + private String mint; + private String name; + private String symbol; + private String uri; + @Json(name = "additional_metadata") + private List additionalMetadata; + } + + @Getter + @ToString + static class KeyValue { + private String key; + private String value; + } + + @Getter + @ToString + static class Supply { + @Json(name = "print_max_supply") + private Long printMaxSupply; + @Json(name = "print_current_supply") + private Long printCurrentSupply; + @Json(name = "edition_nonce") + private Integer editionNonce; + @Json(name = "edition_number") + private Integer editionNumber = null; + } + + @Getter + @ToString + static class TokenInfo { + private String symbol; + private Long balance; + private Long supply; + private Integer decimals; + @Json(name = "token_program") + private String tokenProgram; + @Json(name = "associated_token_address") + private String associatedTokenAddress; + @Json(name = "price_info") + private PriceInfo priceInfo; + } + + @Getter + @ToString + static class PriceInfo { + @Json(name = "price_per_token") + private Double pricePerToken; + @Json(name = "total_price") + private Double totalPrice; + private String currency; + } + + @Getter + @ToString + static class Inscription { + private Integer order; + private Integer size; + private String contentType; + private String encoding; + private String validationHash; + private String inscriptionDataAccount; + private String authority; + } + + @Getter + @ToString + static class NativeBalance { + private Long lamports; + @Json(name = "price_per_sol") + private Double pricePerSol; + @Json(name = "total_price") + private Double totalPrice; + } } From 16681033d1238a909be959651e334d721beb9c4c Mon Sep 17 00:00:00 2001 From: Chintan Patel Date: Mon, 2 Dec 2024 23:50:52 -0600 Subject: [PATCH 17/18] Modified inner classes of search asset request and response to be accessible outside --- .../rpc/types/SearchAssetsResponse.java | 40 +++++++++---------- .../rpc/types/config/SearchAssetsConfig.java | 10 ++--- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/src/main/java/org/p2p/solanaj/rpc/types/SearchAssetsResponse.java b/src/main/java/org/p2p/solanaj/rpc/types/SearchAssetsResponse.java index ecf95aeb..536e7775 100644 --- a/src/main/java/org/p2p/solanaj/rpc/types/SearchAssetsResponse.java +++ b/src/main/java/org/p2p/solanaj/rpc/types/SearchAssetsResponse.java @@ -43,7 +43,7 @@ public static class Asset { @Getter @ToString - static class MintExtensions { + public static class MintExtensions { @Json(name = "confidential_transfer_mint") private ConfidentialTransferMint confidentialTransferMint; @Json(name = "confidential_transfer_fee_config") @@ -70,7 +70,7 @@ static class MintExtensions { @Getter @ToString - static class ConfidentialTransferMint { + public static class ConfidentialTransferMint { @Json(name = "authority") private String authority; @Json(name = "auto_approve_new_accounts") @@ -81,7 +81,7 @@ static class ConfidentialTransferMint { @Getter @ToString - static class ConfidentialTransferFeeConfig { + public static class ConfidentialTransferFeeConfig { @Json(name = "authority") private String authority; @Json(name = "withdraw_withheld_authority_elgamal_pubkey") @@ -94,7 +94,7 @@ static class ConfidentialTransferFeeConfig { @Getter @ToString - static class TransferFeeConfig { + public static class TransferFeeConfig { @Json(name = "transfer_fee_config_authority") private String transferFeeConfigAuthority; @Json(name = "withdraw_withheld_authority") @@ -109,7 +109,7 @@ static class TransferFeeConfig { @Getter @ToString - static class OlderTransferFee { + public static class OlderTransferFee { private String epoch; @Json(name = "maximum_fee") private String maximumFee; @@ -119,13 +119,13 @@ static class OlderTransferFee { @Getter @ToString - static class NewerTransferFee { + public static class NewerTransferFee { private String epoch; } @Getter @ToString - static class MetadataPointer { + public static class MetadataPointer { private String authority; @Json(name = "metadata_address") private String metadataAddress; @@ -133,20 +133,20 @@ static class MetadataPointer { @Getter @ToString - static class MintCloseAuthority { + public static class MintCloseAuthority { @Json(name = "close_authority") private String closeAuthority; } @Getter @ToString - static class PermanentDelegate { + public static class PermanentDelegate { private String delegate; } @Getter @ToString - static class TransferHook { + public static class TransferHook { private String authority; @Json(name = "program_id") private String programId; @@ -154,7 +154,7 @@ static class TransferHook { @Getter @ToString - static class InterestBearingConfig { + public static class InterestBearingConfig { @Json(name = "rate_authority") private String rateAuthority; @Json(name = "initialization_timestamp") @@ -169,13 +169,13 @@ static class InterestBearingConfig { @Getter @ToString - static class DefaultAccountState { + public static class DefaultAccountState { private String state; } @Getter @ToString - static class ConfidentialTransferAccount { + public static class ConfidentialTransferAccount { private Boolean approved; @Json(name = "elgamal_pubkey") private String elgamalPubkey; @@ -203,7 +203,7 @@ static class ConfidentialTransferAccount { @Getter @ToString - static class Metadata { + public static class Metadata { @Json(name = "update_authority") private String updateAuthority; private String mint; @@ -216,14 +216,14 @@ static class Metadata { @Getter @ToString - static class KeyValue { + public static class KeyValue { private String key; private String value; } @Getter @ToString - static class Supply { + public static class Supply { @Json(name = "print_max_supply") private Long printMaxSupply; @Json(name = "print_current_supply") @@ -236,7 +236,7 @@ static class Supply { @Getter @ToString - static class TokenInfo { + public static class TokenInfo { private String symbol; private Long balance; private Long supply; @@ -251,7 +251,7 @@ static class TokenInfo { @Getter @ToString - static class PriceInfo { + public static class PriceInfo { @Json(name = "price_per_token") private Double pricePerToken; @Json(name = "total_price") @@ -261,7 +261,7 @@ static class PriceInfo { @Getter @ToString - static class Inscription { + public static class Inscription { private Integer order; private Integer size; private String contentType; @@ -273,7 +273,7 @@ static class Inscription { @Getter @ToString - static class NativeBalance { + public static class NativeBalance { private Long lamports; @Json(name = "price_per_sol") private Double pricePerSol; diff --git a/src/main/java/org/p2p/solanaj/rpc/types/config/SearchAssetsConfig.java b/src/main/java/org/p2p/solanaj/rpc/types/config/SearchAssetsConfig.java index a4b2a1e2..e1bcfcd2 100644 --- a/src/main/java/org/p2p/solanaj/rpc/types/config/SearchAssetsConfig.java +++ b/src/main/java/org/p2p/solanaj/rpc/types/config/SearchAssetsConfig.java @@ -87,10 +87,10 @@ public SearchAssetsConfig(String ownerAddress) { @Getter @Setter - static class SortByObject { + public static class SortByObject { @Getter @AllArgsConstructor - enum SortBy { + public enum SortBy { CREATED("created"), RECENT_ACTION("recent_action"), UPDATED("updated"), @@ -99,7 +99,7 @@ enum SortBy { } @Getter @AllArgsConstructor - enum SortDirection { + public enum SortDirection { ASC("asc"), DESC("desc"); private final String value; @@ -110,7 +110,7 @@ enum SortDirection { @Getter @Setter - static class Options { + public static class Options { private Boolean showUnverifiedCollections = null; private Boolean showCollectionMetadata = null; private Boolean showGrandTotal = null; @@ -137,7 +137,7 @@ public enum Interface { @Getter @AllArgsConstructor - enum TokenType { + public enum TokenType { FUNGIBLE("fungible"), NON_FUNGIBLE("nonFungible"), REGULAR_NFT("regularNFT"), From bff7f384cc575d4aae1827618d7e3afc6e3917d4 Mon Sep 17 00:00:00 2001 From: Chintan Patel Date: Sun, 15 Dec 2024 19:11:36 -0600 Subject: [PATCH 18/18] Updated searchassetsresponse dto type --- .../java/org/p2p/solanaj/rpc/types/SearchAssetsResponse.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/p2p/solanaj/rpc/types/SearchAssetsResponse.java b/src/main/java/org/p2p/solanaj/rpc/types/SearchAssetsResponse.java index 536e7775..bafd5198 100644 --- a/src/main/java/org/p2p/solanaj/rpc/types/SearchAssetsResponse.java +++ b/src/main/java/org/p2p/solanaj/rpc/types/SearchAssetsResponse.java @@ -5,6 +5,7 @@ import lombok.ToString; import org.p2p.solanaj.rpc.types.config.SearchAssetsConfig; +import java.math.BigDecimal; import java.util.List; @Getter @@ -238,8 +239,8 @@ public static class Supply { @ToString public static class TokenInfo { private String symbol; - private Long balance; - private Long supply; + private String balance; + private String supply; private Integer decimals; @Json(name = "token_program") private String tokenProgram;