diff --git a/docs/README.md b/docs/README.md index 2c18d0b3..13f70b84 100644 --- a/docs/README.md +++ b/docs/README.md @@ -98,10 +98,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 @@ -125,19 +125,19 @@ final Market solUsdcMarket = new MarketBuilder() final OrderBook bids = solUsdcMarket.getBidOrderBook(); ``` -### Send a Transaction with 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 = client.getApi().sendTransaction(transaction, feePayer); +String response = client.getApi().sendTransaction(legacyTransaction, feePayer); ``` ## 🤝 Contributing diff --git a/pom.xml b/pom.xml index 65d8825f..3d701479 100644 --- a/pom.xml +++ b/pom.xml @@ -125,6 +125,19 @@ 5.14.2 test + + + org.apache.httpcomponents.client5 + httpclient5 + 5.2.1 + + + + org.apache.commons + commons-lang3 + 3.15.0 + + com.syntifi.near borshj diff --git a/src/main/java/org/p2p/solanaj/core/AccountKeysList.java b/src/main/java/org/p2p/solanaj/core/AccountKeysList.java index 4a03bf46..754273c5 100644 --- a/src/main/java/org/p2p/solanaj/core/AccountKeysList.java +++ b/src/main/java/org/p2p/solanaj/core/AccountKeysList.java @@ -32,6 +32,13 @@ public ArrayList getList() { return accountKeysList; } + @Override + public String toString() { + return "AccountKeysList{" + + "accounts=" + accounts + + '}'; + } + private static final Comparator metaComparator = Comparator .comparing(AccountMeta::isSigner) .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 bfd6e525..b765f702 100644 --- a/src/main/java/org/p2p/solanaj/core/AccountMeta.java +++ b/src/main/java/org/p2p/solanaj/core/AccountMeta.java @@ -13,4 +13,23 @@ public class AccountMeta { private boolean isWritable; -} \ No newline at end of file + /** + * 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{" + + "publicKey=" + publicKey + + ", isSigner=" + isSigner + + ", isWritable=" + isWritable + + '}'; + } +} 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..9b519452 --- /dev/null +++ b/src/main/java/org/p2p/solanaj/core/LegacyMessage.java @@ -0,0 +1,181 @@ +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 java.util.stream.Collectors; + +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 MessageHeader messageHeader; + 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 = new MessageHeader(); + + List keysList = getAccountKeys(); + int accountKeysSize = keysList.size(); + + byte[] accountAddressesLength = ShortvecEncoding.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 = ShortvecEncoding.encodeLength(keysSize); + compiledInstruction.keyIndices = keyIndices; + compiledInstruction.dataLength = ShortvecEncoding.encodeLength(instruction.getData().length); + compiledInstruction.data = instruction.getData(); + + compiledInstructions.add(compiledInstruction); + + compiledInstructionsLength += compiledInstruction.getLength(); + } + + byte[] instructionsLength = ShortvecEncoding.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(); + + // 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); + 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..bc5bff99 --- /dev/null +++ b/src/main/java/org/p2p/solanaj/core/LegacyTransaction.java @@ -0,0 +1,114 @@ +package org.p2p.solanaj.core; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +import org.bitcoinj.core.Base58; +import org.p2p.solanaj.utils.ShortvecEncoding; +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; + + /** + * Constructs a new Legacy Transaction instance. + */ + public LegacyTransaction() { + this.legacyMessage = new LegacyMessage(); + 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(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 == null || signers.isEmpty()) { + throw new IllegalArgumentException("No signers provided"); + } + + Account feePayer = signers.get(0); + legacyMessage.setFeePayer(feePayer); + + serializedLegacyMessage = legacyMessage.serialize(); + + for (Account signer : signers) { + 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 = ShortvecEncoding.encodeLength(signaturesSize); + + // Calculate total size before allocating ByteBuffer + int totalSize = signaturesLength.length + signaturesSize * SIGNATURE_LENGTH + serializedLegacyMessage.length; + ByteBuffer out = ByteBuffer.allocate(totalSize); + + out.put(signaturesLength); + + for (String signature : signatures) { + byte[] rawSignature = Base58.decode(signature); + out.put(rawSignature); + } + + out.put(serializedLegacyMessage); + + return out.array(); + } +} 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 b9ca7687..2616dd9d 100644 --- a/src/main/java/org/p2p/solanaj/core/Message.java +++ b/src/main/java/org/p2p/solanaj/core/Message.java @@ -5,54 +5,137 @@ import java.nio.ByteBuffer; import java.util.ArrayList; -import java.util.Comparator; +import java.util.Arrays; import java.util.List; +import java.util.Comparator; import java.util.stream.Collectors; +import lombok.Getter; + +import org.p2p.solanaj.utils.GuardedArrayUtils; + 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 = 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 = ShortvecEncoding.encodeLength(instruction.getData().length); + compiledInstruction.data = instruction.getData(); + compiledInstructions.add(compiledInstruction); return this; } @@ -66,62 +149,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[] instructionsCountLength = ShortvecEncoding.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 = ShortvecEncoding.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()); @@ -130,8 +186,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); @@ -140,21 +195,106 @@ 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 = ShortvecEncoding.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 = 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 = ShortvecEncoding.decodeLength(serializedMessageList); // keysSize + compiledInstruction.keyIndicesCount = ShortvecEncoding.encodeLength(keysSize); + compiledInstruction.keyIndices = GuardedArrayUtils.guardedSplice(serializedMessageList, 0, keysSize); + var dataLength = ShortvecEncoding.decodeLength(serializedMessageList); + compiledInstruction.dataLength = ShortvecEncoding.encodeLength(dataLength); + compiledInstruction.data = GuardedArrayUtils.guardedSplice(serializedMessageList, 0, dataLength); + + compiledInstructions.add(compiledInstruction); + + } + + // Deserialize addressTableLookups + 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 = ShortvecEncoding.decodeLength(serializedMessageList); // keysSize + addressTableLookup.writableIndexesCountLength = ShortvecEncoding.encodeLength(writableIndexesLength); + addressTableLookup.writableIndexes = GuardedArrayUtils.guardedSplice(serializedMessageList, 0, writableIndexesLength); + int readonlyIndexesLength = ShortvecEncoding.decodeLength(serializedMessageList); + addressTableLookup.readonlyIndexesCountLength = ShortvecEncoding.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; } public List getAccountKeys() { - AccountKeysList accounts = new AccountKeysList(); - accounts.add(new AccountMeta(feePayer.getPublicKey(), true, true)); - accounts.addAll(accountKeys); - return accounts.getList(); + List keysList = accountKeys.getList(); + + // 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); + newList.addAll(keysList); + + 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; @@ -163,4 +303,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 2f21e46b..ef27be58 100644 --- a/src/main/java/org/p2p/solanaj/core/PublicKey.java +++ b/src/main/java/org/p2p/solanaj/core/PublicKey.java @@ -7,6 +7,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; @@ -91,6 +92,7 @@ public static PublicKey createProgramAddress(List seeds, PublicKey progr } } + public static PublicKey createWithSeed(PublicKey fromPublicKey, String seed, PublicKey programId) { try (ByteArrayOutputStream buffer = new ByteArrayOutputStream()) { if (seed.length() > 32) { @@ -108,23 +110,16 @@ public static PublicKey createWithSeed(PublicKey fromPublicKey, String seed, Pub } } + @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) { diff --git a/src/main/java/org/p2p/solanaj/core/Transaction.java b/src/main/java/org/p2p/solanaj/core/Transaction.java index bab10710..bde1b3d5 100644 --- a/src/main/java/org/p2p/solanaj/core/Transaction.java +++ b/src/main/java/org/p2p/solanaj/core/Transaction.java @@ -7,6 +7,8 @@ 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.ShortvecEncoding; import org.p2p.solanaj.utils.TweetNaclFast; @@ -27,7 +29,12 @@ public class Transaction { */ public Transaction() { this.message = new Message(); - this.signatures = new ArrayList<>(); // Use diamond operator + this.signatures = new ArrayList<>(); + } + + public Transaction(Message message, List signatures) { + this.message = message; + this.signatures = signatures; } /** @@ -61,7 +68,7 @@ public void setRecentBlockHash(String recentBlockhash) { * @throws NullPointerException if the signer is null */ public void sign(Account signer) { - sign(Arrays.asList(Objects.requireNonNull(signer, "Signer cannot be null"))); // Add input validation + sign(List.of(Objects.requireNonNull(signer, "Signer cannot be null"))); // Add input validation } /** @@ -78,13 +85,21 @@ public void sign(List 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()); + } try { 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)); } catch (Exception e) { throw new RuntimeException("Error signing transaction", e); // Improve exception handling } @@ -115,4 +130,29 @@ public byte[] serialize() { return out.array(); } + + public static Transaction deserialize(byte[] serializedTransaction) { + List serializedTransactionList = ByteUtils.toByteList(serializedTransaction); + + int signaturesSize = ShortvecEncoding.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/programs/JupiterSwapProgram.java b/src/main/java/org/p2p/solanaj/programs/JupiterSwapProgram.java new file mode 100644 index 00000000..e0c595c1 --- /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 quoteJsonNode = getJupiterQuote(quoteUri); + return swapToken(rpcClient, account, quoteJsonNode); + } + + public record SwapRequest(Object quoteResponse, String userPublicKey) {} +} 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 0e0109a3..99a92a01 100644 --- a/src/main/java/org/p2p/solanaj/rpc/RpcApi.java +++ b/src/main/java/org/p2p/solanaj/rpc/RpcApi.java @@ -1,6 +1,7 @@ package org.p2p.solanaj.rpc; 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.*; @@ -16,7 +17,7 @@ import java.util.stream.Collectors; public class RpcApi { - private RpcClient client; + private final RpcClient client; public RpcApi(RpcClient client) { this.client = client; @@ -52,6 +53,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); @@ -72,7 +136,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 = getLatestBlockhash().getValue().getBlockhash(); @@ -88,7 +152,7 @@ public String sendTransaction(Transaction transaction, List signers, St params.add(base64Trx); params.add(rpcSendTransactionConfig); - return client.call("sendTransaction", params, String.class); + return client.call("sendLegacyTransaction", params, String.class); } /** @@ -209,6 +273,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))); @@ -261,9 +343,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)); @@ -288,9 +368,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); programAccountConfig.setEncoding(Encoding.base64); @@ -306,6 +384,15 @@ public List getProgramAccounts(PublicKey account, List m return result; } + public TokenAccountsResponse getTokenAccounts(TokenAccountsConfig tokenAccountsConfig) + throws RpcException { + 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 { 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/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..56f33aed 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,10 +15,26 @@ 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; this.commitment = commitment.getValue(); } + + public ConfirmedSignFAddr2(String before, int limit, Commitment commitment) { + this.before = before; + 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 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..5ea10291 100644 --- a/src/main/java/org/p2p/solanaj/rpc/types/RpcRequest.java +++ b/src/main/java/org/p2p/solanaj/rpc/types/RpcRequest.java @@ -12,22 +12,22 @@ 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 Object params; @Json(name = "id") - private String id = UUID.randomUUID().toString(); + private final String id = UUID.randomUUID().toString(); 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/SearchAssetsResponse.java b/src/main/java/org/p2p/solanaj/rpc/types/SearchAssetsResponse.java new file mode 100644 index 00000000..bafd5198 --- /dev/null +++ b/src/main/java/org/p2p/solanaj/rpc/types/SearchAssetsResponse.java @@ -0,0 +1,284 @@ +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.math.BigDecimal; +import java.util.List; + +@Getter +@ToString +public class SearchAssetsResponse { + 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 + public 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 + public 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 + public 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 + public 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 + public static class OlderTransferFee { + private String epoch; + @Json(name = "maximum_fee") + private String maximumFee; + @Json(name = "transfer_fee_basis_points") + private String transferFeeBasisPoints; + } + + @Getter + @ToString + public static class NewerTransferFee { + private String epoch; + } + + @Getter + @ToString + public static class MetadataPointer { + private String authority; + @Json(name = "metadata_address") + private String metadataAddress; + } + + @Getter + @ToString + public static class MintCloseAuthority { + @Json(name = "close_authority") + private String closeAuthority; + } + + @Getter + @ToString + public static class PermanentDelegate { + private String delegate; + } + + @Getter + @ToString + public static class TransferHook { + private String authority; + @Json(name = "program_id") + private String programId; + } + + @Getter + @ToString + public 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 + public static class DefaultAccountState { + private String state; + } + + @Getter + @ToString + public 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 + public 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 + public static class KeyValue { + private String key; + private String value; + } + + @Getter + @ToString + public 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 + public static class TokenInfo { + private String symbol; + private String balance; + private String 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 + public static class PriceInfo { + @Json(name = "price_per_token") + private Double pricePerToken; + @Json(name = "total_price") + private Double totalPrice; + private String currency; + } + + @Getter + @ToString + public 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 + public 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/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/TokenAccountsResponse.java b/src/main/java/org/p2p/solanaj/rpc/types/TokenAccountsResponse.java new file mode 100644 index 00000000..df51ef55 --- /dev/null +++ b/src/main/java/org/p2p/solanaj/rpc/types/TokenAccountsResponse.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 TokenAccountsResponse { + + @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/TokenResultObjects.java b/src/main/java/org/p2p/solanaj/rpc/types/TokenResultObjects.java index 46e28073..968a365e 100644 --- a/src/main/java/org/p2p/solanaj/rpc/types/TokenResultObjects.java +++ b/src/main/java/org/p2p/solanaj/rpc/types/TokenResultObjects.java @@ -71,19 +71,19 @@ public static class TokenInfo { private TokenAmountInfo tokenAmount; @Json(name = "decimals") - private int 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; - - @Json(name = "isInitialized") - private boolean isInitialized; + private String supply; // Optional extensions for token2022 @Json(name = "extensions") 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/rpc/types/config/SearchAssetsConfig.java b/src/main/java/org/p2p/solanaj/rpc/types/config/SearchAssetsConfig.java new file mode 100644 index 00000000..e1bcfcd2 --- /dev/null +++ b/src/main/java/org/p2p/solanaj/rpc/types/config/SearchAssetsConfig.java @@ -0,0 +1,149 @@ +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 cursor = 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 String tokenType = TokenType.FUNGIBLE.getName(); + + 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, String cursor) { + this.ownerAddress = ownerAddress; + this.limit = limit; + this.cursor = cursor; + } + + public SearchAssetsConfig(String ownerAddress, int limit) { + this.ownerAddress = ownerAddress; + this.limit = limit; + } + + public SearchAssetsConfig(String ownerAddress) { + this.ownerAddress = ownerAddress; + } + + @Getter + @Setter + public static class SortByObject { + @Getter + @AllArgsConstructor + public enum SortBy { + CREATED("created"), + RECENT_ACTION("recent_action"), + UPDATED("updated"), + NONE("none"); + private final String value; + } + @Getter + @AllArgsConstructor + public enum SortDirection { + ASC("asc"), + DESC("desc"); + private final String value; + } + private SortBy sortBy = SortBy.NONE; + private SortDirection sortDirection = SortDirection.ASC; + } + + @Getter + @Setter + public 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 + public enum TokenType { + FUNGIBLE("fungible"), + NON_FUNGIBLE("nonFungible"), + REGULAR_NFT("regularNFT"), + COMPRESSED_NFT("compressedNFT"), + ALL("all"); + @Json + private final String name; + } +} diff --git a/src/main/java/org/p2p/solanaj/rpc/types/config/TokenAccountsConfig.java b/src/main/java/org/p2p/solanaj/rpc/types/config/TokenAccountsConfig.java new file mode 100644 index 00000000..eb91ce4e --- /dev/null +++ b/src/main/java/org/p2p/solanaj/rpc/types/config/TokenAccountsConfig.java @@ -0,0 +1,38 @@ +package org.p2p.solanaj.rpc.types.config; + +import lombok.Setter; + +@Setter +public class TokenAccountsConfig { + + private String mint; + + private String owner = null; + + private Integer page = null; + + private Integer limit = null; + + private String cursor = null; + + private String before = null; + + private String after = null; + + public TokenAccountsConfig() {} + + public TokenAccountsConfig(String mint, Integer limit, String cursor) { + this.mint = mint; + this.limit = limit; + this.cursor = cursor; + } + + public TokenAccountsConfig(String mint, Integer limit) { + this.mint = mint; + this.limit = limit; + } + + public TokenAccountsConfig(String mint) { + this.mint = mint; + } +} 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..3b3bcadf 100644 --- a/src/main/java/org/p2p/solanaj/utils/ByteUtils.java +++ b/src/main/java/org/p2p/solanaj/utils/ByteUtils.java @@ -1,15 +1,21 @@ 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; 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) { @@ -43,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++) { @@ -72,4 +82,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/ShortvecEncoding.java index 4a1d9080..ab64b4a6 100644 --- a/src/main/java/org/p2p/solanaj/utils/ShortvecEncoding.java +++ b/src/main/java/org/p2p/solanaj/utils/ShortvecEncoding.java @@ -1,5 +1,7 @@ package org.p2p.solanaj.utils; +import java.util.List; + import static org.bitcoinj.core.Utils.*; public class ShortvecEncoding { @@ -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 1fe6f8cd..64d94cb5 100644 --- a/src/main/java/org/p2p/solanaj/ws/SubscriptionWebSocketClient.java +++ b/src/main/java/org/p2p/solanaj/ws/SubscriptionWebSocketClient.java @@ -68,7 +68,7 @@ public class SubscriptionWebSocketClient extends WebSocketClient { */ private static class SubscriptionParams { final RpcRequest request; - final NotificationEventListener listener; + NotificationEventListener listener; /** * Constructs a SubscriptionParams object. @@ -80,6 +80,10 @@ private static class SubscriptionParams { this.request = request; this.listener = listener; } + + SubscriptionParams(RpcRequest request) { + this.request = request; + } } /** @@ -169,6 +173,37 @@ public void signatureSubscribe(String signature, NotificationEventListener liste addSubscription(rpcRequest, listener); } + 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 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); + + subscriptions.put(rpcRequest.getId(), new SubscriptionParams(rpcRequest, listener)); + subscriptionIds.put(rpcRequest.getId(), 0L); + + updateSubscriptions(); + return rpcRequest.getId(); + } + /** * Subscribes to log updates for the given mention. * @@ -378,6 +413,26 @@ public void addSubscription(RpcRequest rpcRequest, NotificationEventListener lis 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()); + } + /** * Handles the WebSocket connection opening. * @@ -675,10 +730,12 @@ 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(); } } return null; } -} \ No newline at end of file +} diff --git a/src/test/java/org/p2p/solanaj/core/AnchorTest.java b/src/test/java/org/p2p/solanaj/core/AnchorTest.java index 7fd1c781..8fbb66a5 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 05db0fda..e6e43c07 100644 --- a/src/test/java/org/p2p/solanaj/core/MessageTest.java +++ b/src/test/java/org/p2p/solanaj/core/LegacyMessageTest.java @@ -5,7 +5,7 @@ import static org.junit.jupiter.api.Assertions.*; import org.p2p.solanaj.programs.SystemProgram; -public class MessageTest { +public class LegacyMessageTest { @Test public void serializeMessage() { @@ -16,10 +16,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, @@ -27,7 +27,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 eae14086..4cf3f941 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 23ace977..6e838f57 100644 --- a/src/test/java/org/p2p/solanaj/core/MainnetTest.java +++ b/src/test/java/org/p2p/solanaj/core/MainnetTest.java @@ -214,7 +214,7 @@ public void getToken2022Metadata() throws RpcException { } /** - * Calls sendTransaction with a call to the Memo program included. + * Calls sendLegacyTransaction with a call to the Memo program included. */ @Test @Disabled @@ -225,7 +225,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 @@ -238,14 +238,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/manager/TokenManagerTest.java b/src/test/java/org/p2p/solanaj/manager/TokenManagerTest.java index 9691158b..894d9d80 100644 --- a/src/test/java/org/p2p/solanaj/manager/TokenManagerTest.java +++ b/src/test/java/org/p2p/solanaj/manager/TokenManagerTest.java @@ -2,13 +2,12 @@ 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.mockito.Mockito; import org.p2p.solanaj.core.Account; import org.p2p.solanaj.core.PublicKey; -import org.p2p.solanaj.core.Transaction; +import org.p2p.solanaj.core.LegacyTransaction; import org.p2p.solanaj.programs.SystemProgram; import org.p2p.solanaj.rpc.RpcClient; import org.p2p.solanaj.rpc.RpcApi; @@ -61,12 +60,12 @@ public void testTransfer() throws RpcException { long amount = 1000L; String expectedTxId = "MockTransactionId"; - when(mockRpcApi.sendTransaction(any(Transaction.class), eq(owner))).thenReturn(expectedTxId); + when(mockRpcApi.sendLegacyTransaction(any(LegacyTransaction.class), eq(owner))).thenReturn(expectedTxId); String result = tokenManager.transfer(owner, source, destination, tokenMint, amount); assertEquals(expectedTxId, result); - verify(mockRpcApi).sendTransaction(any(Transaction.class), eq(owner)); + verify(mockRpcApi).sendLegacyTransaction(any(LegacyTransaction.class), eq(owner)); } /** @@ -83,13 +82,13 @@ public void testTransferCheckedToSolAddress() throws RpcException { String expectedTxId = "MockTransactionId"; when(mockRpcApi.getTokenAccountsByOwner(eq(destination), eq(tokenMint))).thenReturn(destinationATA); - when(mockRpcApi.sendTransaction(any(Transaction.class), eq(owner))).thenReturn(expectedTxId); + 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).sendTransaction(any(Transaction.class), eq(owner)); + verify(mockRpcApi).sendLegacyTransaction(any(LegacyTransaction.class), eq(owner)); } /** @@ -104,12 +103,12 @@ public void testInitializeAccount() throws RpcException { PublicKey usdcTokenMint = new PublicKey("A4k3Dyjzvzp8e1Z1g1g1g1g1g1g1g1g1g1g1g1g1g1"); // Example USDC mint String expectedTxId = "MockTransactionId"; - when(mockRpcApi.sendTransaction(any(Transaction.class), eq(owner))).thenReturn(expectedTxId); + when(mockRpcApi.sendLegacyTransaction(any(LegacyTransaction.class), eq(owner))).thenReturn(expectedTxId); String result = tokenManager.initializeAccount(newAccount, usdcTokenMint, owner); assertEquals(expectedTxId, result); - verify(mockRpcApi).sendTransaction(any(Transaction.class), eq(owner)); + verify(mockRpcApi).sendLegacyTransaction(any(LegacyTransaction.class), eq(owner)); } /** @@ -130,13 +129,13 @@ public void testTransferArbitraryToken() throws RpcException { String expectedTxId = "MockTransactionId"; when(mockRpcApi.getTokenAccountsByOwner(eq(destination), eq(bonkMint))).thenReturn(destinationATA); - when(mockRpcApi.sendTransaction(any(Transaction.class), eq(owner))).thenReturn(expectedTxId); + 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).sendTransaction(any(Transaction.class), eq(owner)); + verify(mockRpcApi).sendLegacyTransaction(any(LegacyTransaction.class), eq(owner)); } /** @@ -152,17 +151,17 @@ public void testTransferSOL() throws RpcException { long amount = 1_000_000_000L; // 1 SOL (lamports) String expectedTxId = "MockTransactionId"; - when(mockRpcApi.sendTransaction(any(Transaction.class), eq(owner))).thenReturn(expectedTxId); + when(mockRpcApi.sendLegacyTransaction(any(LegacyTransaction.class), eq(owner))).thenReturn(expectedTxId); - Transaction transaction = new Transaction(); + LegacyTransaction transaction = new LegacyTransaction(); transaction.addInstruction( SystemProgram.transfer(owner.getPublicKey(), recipient, amount) ); - String result = mockRpcApi.sendTransaction(transaction, owner); + String result = mockRpcApi.sendLegacyTransaction(transaction, owner); assertEquals(expectedTxId, result); - verify(mockRpcApi).sendTransaction(any(Transaction.class), eq(owner)); + verify(mockRpcApi).sendLegacyTransaction(any(LegacyTransaction.class), eq(owner)); } /** @@ -183,12 +182,12 @@ public void testTransferWSOL() throws RpcException { String expectedTxId = "MockTransactionId"; when(mockRpcApi.getTokenAccountsByOwner(eq(destination), eq(wsolMint))).thenReturn(destinationWSOLATA); - when(mockRpcApi.sendTransaction(any(Transaction.class), eq(owner))).thenReturn(expectedTxId); + 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).sendTransaction(any(Transaction.class), eq(owner)); + verify(mockRpcApi).sendLegacyTransaction(any(LegacyTransaction.class), eq(owner)); } } \ 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 633155b4..17465e82 100644 --- a/src/test/java/org/p2p/solanaj/programs/BPFLoaderTest.java +++ b/src/test/java/org/p2p/solanaj/programs/BPFLoaderTest.java @@ -5,7 +5,7 @@ import org.junit.jupiter.api.Disabled; import static org.junit.jupiter.api.Assertions.*; import org.p2p.solanaj.core.Account; -import org.p2p.solanaj.core.Transaction; +import org.p2p.solanaj.core.LegacyTransaction; import org.p2p.solanaj.core.TransactionInstruction; import org.p2p.solanaj.rpc.Cluster; import org.p2p.solanaj.rpc.RpcClient; @@ -119,10 +119,10 @@ public void testSetAuthority() { @Disabled public void initializeBufferIntegrationTest() throws RpcException { Account account = new Account(); // Replace with your actual account setup - Transaction transaction = new Transaction(); + LegacyTransaction legacyTransaction = new LegacyTransaction(); // Initialize buffer - transaction.addInstruction( + legacyTransaction.addInstruction( SystemProgram.createAccount( account.getPublicKey(), bufferAccount.getPublicKey(), @@ -132,7 +132,7 @@ public void initializeBufferIntegrationTest() throws RpcException { ) ); - transaction.addInstruction( + legacyTransaction.addInstruction( BPFLoader.initializeBuffer( bufferAccount.getPublicKey(), account.getPublicKey() @@ -140,9 +140,9 @@ public void initializeBufferIntegrationTest() throws RpcException { ); String hash = client.getApi().getRecentBlockhash(); - transaction.setRecentBlockHash(hash); + legacyTransaction.setRecentBlockHash(hash); - String txId = client.getApi().sendTransaction(transaction, List.of(account, bufferAccount), hash); + String txId = client.getApi().sendLegacyTransaction(legacyTransaction, List.of(account, bufferAccount), hash); assertNotNull(txId); System.out.println("Transaction ID: " + txId); }