diff --git a/src/main/java/org/p2p/solanaj/core/AddressTableLookup.java b/src/main/java/org/p2p/solanaj/core/AddressTableLookup.java new file mode 100644 index 00000000..8acad34b --- /dev/null +++ b/src/main/java/org/p2p/solanaj/core/AddressTableLookup.java @@ -0,0 +1,84 @@ +package org.p2p.solanaj.core; + +import com.squareup.moshi.Json; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.p2p.solanaj.utils.ShortvecEncoding; + +import java.nio.ByteBuffer; +import java.util.List; + +/** + * Represents an Address Lookup Table (ALT) in Solana. + *

+ * ALTs allow transactions to reference additional addresses required for execution, + * enabling transactions that exceed the maximum number of accounts. + *

+ */ +@AllArgsConstructor +public class AddressTableLookup { + @Json(name = "accountKey") + private String accountKey; + + @Getter + @Json(name = "writableIndexes") + private List writableIndexes; + + @Getter + @Json(name = "readonlyIndexes") + private List readonlyIndexes; + + /** + * Constructs an AddressTableLookup with the given parameters. + * + * @param accountKey The public key of the address table + * @param writableIndexes The list of writable indexes + * @param readonlyIndexes The list of readonly indexes + */ + public AddressTableLookup(PublicKey accountKey, List writableIndexes, List readonlyIndexes) { + this.accountKey = accountKey.toBase58(); + this.writableIndexes = writableIndexes; + this.readonlyIndexes = readonlyIndexes; + } + + /** + * Gets the account key as a PublicKey object. + * + * @return the account key + */ + public PublicKey getAccountKey() { + return new PublicKey(accountKey); + } + + /** + * Calculates the serialized size of the AddressTableLookup. + * + * @return the size in bytes + */ + public int getSerializedSize() { + return 32 + // PublicKey size + ShortvecEncoding.decodeLength(writableIndexes) + + writableIndexes.size() + + ShortvecEncoding.decodeLength(readonlyIndexes) + + readonlyIndexes.size(); + } + + /** + * Serializes the AddressTableLookup into a byte array. + * + * @return the serialized byte array + */ + public byte[] serialize() { + ByteBuffer buffer = ByteBuffer.allocate(getSerializedSize()); + buffer.put(getAccountKey().toByteArray()); + buffer.put(ShortvecEncoding.encodeLength(writableIndexes.size())); + for (Byte index : writableIndexes) { + buffer.put(index.byteValue()); + } + buffer.put(ShortvecEncoding.encodeLength(readonlyIndexes.size())); + for (Byte index : readonlyIndexes) { + buffer.put(index.byteValue()); + } + return buffer.array(); + } +} \ No newline at end of file diff --git a/src/main/java/org/p2p/solanaj/core/Message.java b/src/main/java/org/p2p/solanaj/core/Message.java index 1c5bbda0..d9932b35 100644 --- a/src/main/java/org/p2p/solanaj/core/Message.java +++ b/src/main/java/org/p2p/solanaj/core/Message.java @@ -42,10 +42,12 @@ int getLength() { private AccountKeysList accountKeys; private List instructions; private Account feePayer; + private List addressTableLookups; public Message() { this.accountKeys = new AccountKeysList(); this.instructions = new ArrayList(); + this.addressTableLookups = new ArrayList<>(); } public Message addInstruction(TransactionInstruction instruction) { @@ -140,6 +142,15 @@ public byte[] serialize() { out.put(compiledInstruction.data); } + // Serialize address table lookups if present + if (!addressTableLookups.isEmpty()) { + byte[] addressTableLookupsLength = ShortvecEncoding.encodeLength(addressTableLookups.size()); + out.put(addressTableLookupsLength); + for (AddressTableLookup lookup : addressTableLookups) { + out.put(lookup.serialize()); + } + } + return out.array(); } @@ -178,4 +189,12 @@ private int findAccountIndex(List accountMetaList, PublicKey key) { throw new RuntimeException("unable to find account index"); } + + public void addAddressTableLookup(AddressTableLookup lookup) { + addressTableLookups.add(lookup); + } + + public List getAddressTableLookups() { + return addressTableLookups; + } } diff --git a/src/main/java/org/p2p/solanaj/core/Transaction.java b/src/main/java/org/p2p/solanaj/core/Transaction.java index bab10710..aeeba1e8 100644 --- a/src/main/java/org/p2p/solanaj/core/Transaction.java +++ b/src/main/java/org/p2p/solanaj/core/Transaction.java @@ -2,17 +2,21 @@ import java.nio.ByteBuffer; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import java.util.Objects; +import lombok.Getter; +import lombok.Setter; import org.bitcoinj.core.Base58; import org.p2p.solanaj.utils.ShortvecEncoding; import org.p2p.solanaj.utils.TweetNaclFast; /** * Represents a Solana transaction. + *

* This class allows for building, signing, and serializing transactions. + * It supports both legacy and versioned transactions with Address Lookup Tables (ALTs). + *

*/ public class Transaction { @@ -22,12 +26,18 @@ public class Transaction { private final List signatures; private byte[] serializedMessage; + @Getter + @Setter + private byte version = (byte) 0xFF; // Default to legacy version + + private final List addressTableLookups = new ArrayList<>(); + /** * Constructs a new Transaction instance. */ public Transaction() { this.message = new Message(); - this.signatures = new ArrayList<>(); // Use diamond operator + this.signatures = new ArrayList<>(); } /** @@ -38,7 +48,7 @@ public Transaction() { * @throws NullPointerException if the instruction is null */ public Transaction addInstruction(TransactionInstruction instruction) { - Objects.requireNonNull(instruction, "Instruction cannot be null"); // Add input validation + Objects.requireNonNull(instruction, "Instruction cannot be null"); message.addInstruction(instruction); return this; } @@ -50,7 +60,7 @@ public Transaction addInstruction(TransactionInstruction instruction) { * @throws NullPointerException if the recentBlockhash is null */ public void setRecentBlockHash(String recentBlockhash) { - Objects.requireNonNull(recentBlockhash, "Recent blockhash cannot be null"); // Add input validation + Objects.requireNonNull(recentBlockhash, "Recent blockhash cannot be null"); message.setRecentBlockHash(recentBlockhash); } @@ -61,7 +71,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"))); } /** @@ -86,7 +96,7 @@ public void sign(List signers) { byte[] signature = signatureProvider.detached(serializedMessage); signatures.add(Base58.encode(signature)); } catch (Exception e) { - throw new RuntimeException("Error signing transaction", e); // Improve exception handling + throw new RuntimeException("Error signing transaction", e); } } } @@ -97,13 +107,26 @@ public void sign(List signers) { * @return The serialized transaction as a byte array */ public byte[] serialize() { + byte[] serializedMessage = message.serialize(); int signaturesSize = signatures.size(); byte[] signaturesLength = ShortvecEncoding.encodeLength(signaturesSize); - // Calculate total size before allocating ByteBuffer int totalSize = signaturesLength.length + signaturesSize * SIGNATURE_LENGTH + serializedMessage.length; + if (version != (byte) 0xFF) { + totalSize += 1; // Add 1 byte for version + if (version == 0) { + totalSize += ShortvecEncoding.encodeLength(addressTableLookups.size()).length; + for (AddressTableLookup lookup : addressTableLookups) { + totalSize += lookup.getSerializedSize(); + } + } + } + ByteBuffer out = ByteBuffer.allocate(totalSize); + if (version != (byte) 0xFF) { + out.put(version); + } out.put(signaturesLength); for (String signature : signatures) { @@ -113,6 +136,25 @@ public byte[] serialize() { out.put(serializedMessage); + if (version == 0) { + byte[] addressTableLookupsLength = ShortvecEncoding.encodeLength(addressTableLookups.size()); + out.put(addressTableLookupsLength); + for (AddressTableLookup lookup : addressTableLookups) { + out.put(lookup.serialize()); + } + } + return out.array(); } -} + + /** + * Adds an address table lookup to the transaction. + * + * @param tablePubkey The public key of the address table + * @param writableIndexes The list of writable indexes + * @param readonlyIndexes The list of readonly indexes + */ + public void addAddressTableLookup(PublicKey tablePubkey, List writableIndexes, List readonlyIndexes) { + addressTableLookups.add(new AddressTableLookup(tablePubkey, writableIndexes, readonlyIndexes)); + } +} \ No newline at end of file diff --git a/src/main/java/org/p2p/solanaj/core/TransactionBuilder.java b/src/main/java/org/p2p/solanaj/core/TransactionBuilder.java index 2841ba52..4c3a6870 100644 --- a/src/main/java/org/p2p/solanaj/core/TransactionBuilder.java +++ b/src/main/java/org/p2p/solanaj/core/TransactionBuilder.java @@ -1,17 +1,19 @@ package org.p2p.solanaj.core; +import org.p2p.solanaj.programs.AddressLookupTableProgram; + import java.util.List; import java.util.Objects; /** - * Builder for constructing {@link Transaction} objects to be used in sendTransaction. + * Builder class for constructing Transactions. */ public class TransactionBuilder { private final Transaction transaction; /** - * Constructs a new TransactionBuilder. + * Constructs a new TransactionBuilder instance. */ public TransactionBuilder() { this.transaction = new Transaction(); @@ -44,25 +46,24 @@ public TransactionBuilder addInstructions(List instructi } /** - * Sets the recent block hash for the transaction. + * Sets the recent blockhash for the transaction. * - * @param recentBlockHash the recent block hash to set - * @return this builder for method chaining - * @throws NullPointerException if recentBlockHash is null + * @param recentBlockhash The recent blockhash to set + * @return This builder for method chaining + * @throws NullPointerException if the recentBlockhash is null */ - public TransactionBuilder setRecentBlockHash(String recentBlockHash) { - Objects.requireNonNull(recentBlockHash, "Recent block hash cannot be null"); - transaction.setRecentBlockHash(recentBlockHash); + public TransactionBuilder setRecentBlockHash(String recentBlockhash) { + transaction.setRecentBlockHash(recentBlockhash); return this; } /** - * Sets the signers for the transaction and signs it. + * Sets the fee payer and signs the transaction with the provided signers. * - * @param signers the list of signers - * @return this builder for method chaining - * @throws NullPointerException if signers is null - * @throws IllegalArgumentException if signers is empty + * @param signers The list of signers; the first signer is the fee payer + * @return This builder for method chaining + * @throws NullPointerException if the signers list is null + * @throws IllegalArgumentException if the signers list is empty */ public TransactionBuilder setSigners(List signers) { Objects.requireNonNull(signers, "Signers list cannot be null"); @@ -73,6 +74,30 @@ public TransactionBuilder setSigners(List signers) { return this; } + /** + * Sets the version for the transaction. + * + * @param version the version to set + * @return this builder for method chaining + */ + public TransactionBuilder setVersion(byte version) { + transaction.setVersion(version); + return this; + } + + /** + * Adds an address table lookup to the transaction. + * + * @param tablePubkey the public key of the address table + * @param writableIndexes the list of writable indexes + * @param readonlyIndexes the list of readonly indexes + * @return this builder for method chaining + */ + public TransactionBuilder addAddressTableLookup(PublicKey tablePubkey, List writableIndexes, List readonlyIndexes) { + transaction.addAddressTableLookup(tablePubkey, writableIndexes, readonlyIndexes); + return this; + } + /** * Builds and returns the constructed Transaction object. * @@ -81,5 +106,4 @@ public TransactionBuilder setSigners(List signers) { public Transaction build() { return transaction; } - -} +} \ No newline at end of file diff --git a/src/main/java/org/p2p/solanaj/programs/AddressLookupTableProgram.java b/src/main/java/org/p2p/solanaj/programs/AddressLookupTableProgram.java index d38ffc1d..14e0ef52 100644 --- a/src/main/java/org/p2p/solanaj/programs/AddressLookupTableProgram.java +++ b/src/main/java/org/p2p/solanaj/programs/AddressLookupTableProgram.java @@ -26,14 +26,17 @@ public class AddressLookupTableProgram extends Program { /** * Creates an instruction to create a new address lookup table. * - * @param authority The authority (signer) that can modify the table - * @param payer The account paying for the table creation - * @param recentSlot A recent slot to derive the table's address + * @param authority The authority (signer) that can modify the table + * @param payer The account paying for the table creation + * @param recentSlot A recent slot to derive the table's address * @return A TransactionInstruction to create a new address lookup table */ public static TransactionInstruction createLookupTable(PublicKey authority, PublicKey payer, long recentSlot) { PublicKey derivedAddress = PublicKey.findProgramAddress( - List.of(authority.toByteArray(), ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN).putLong(recentSlot).array()), + List.of( + authority.toByteArray(), + ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN).putLong(recentSlot).array() + ), PROGRAM_ID ).getAddress(); @@ -55,7 +58,7 @@ public static TransactionInstruction createLookupTable(PublicKey authority, Publ * Creates an instruction to freeze an address lookup table. * * @param lookupTable The address of the lookup table to freeze - * @param authority The authority (signer) of the lookup table + * @param authority The authority (signer) of the lookup table * @return A TransactionInstruction to freeze an address lookup table */ public static TransactionInstruction freezeLookupTable(PublicKey lookupTable, PublicKey authority) { @@ -73,17 +76,16 @@ public static TransactionInstruction freezeLookupTable(PublicKey lookupTable, Pu * Creates an instruction to extend an address lookup table. * * @param lookupTable The address of the lookup table to extend - * @param authority The authority (signer) of the lookup table - * @param payer The account paying for the table extension - * @param addresses The list of addresses to add to the table + * @param payer The account paying for the table extension + * @param authority The authority (signer) of the lookup table + * @param addresses The list of addresses to add to the table * @return A TransactionInstruction to extend an address lookup table */ - public static TransactionInstruction extendLookupTable(PublicKey lookupTable, PublicKey authority, PublicKey payer, List addresses) { + public static TransactionInstruction extendLookupTable(PublicKey lookupTable, PublicKey payer, PublicKey authority, List addresses) { List keys = new ArrayList<>(); keys.add(new AccountMeta(lookupTable, false, true)); - keys.add(new AccountMeta(authority, true, false)); keys.add(new AccountMeta(payer, true, true)); - keys.add(new AccountMeta(SystemProgram.PROGRAM_ID, false, false)); + keys.add(new AccountMeta(authority, true, false)); ByteBuffer data = ByteBuffer.allocate(1 + 4 + addresses.size() * 32); data.order(ByteOrder.LITTLE_ENDIAN); @@ -100,7 +102,7 @@ public static TransactionInstruction extendLookupTable(PublicKey lookupTable, Pu * Creates an instruction to deactivate an address lookup table. * * @param lookupTable The address of the lookup table to deactivate - * @param authority The authority (signer) of the lookup table + * @param authority The authority (signer) of the lookup table * @return A TransactionInstruction to deactivate an address lookup table */ public static TransactionInstruction deactivateLookupTable(PublicKey lookupTable, PublicKey authority) { @@ -118,8 +120,8 @@ public static TransactionInstruction deactivateLookupTable(PublicKey lookupTable * Creates an instruction to close an address lookup table. * * @param lookupTable The address of the lookup table to close - * @param authority The authority (signer) of the lookup table - * @param recipient The account to receive the closed table's lamports + * @param authority The authority (signer) of the lookup table + * @param recipient The account to receive the closed table's lamports * @return A TransactionInstruction to close an address lookup table */ public static TransactionInstruction closeLookupTable(PublicKey lookupTable, PublicKey authority, PublicKey recipient) { diff --git a/src/main/java/org/p2p/solanaj/rpc/types/ConfirmedTransaction.java b/src/main/java/org/p2p/solanaj/rpc/types/ConfirmedTransaction.java index 60c1e765..00980cfc 100644 --- a/src/main/java/org/p2p/solanaj/rpc/types/ConfirmedTransaction.java +++ b/src/main/java/org/p2p/solanaj/rpc/types/ConfirmedTransaction.java @@ -1,6 +1,10 @@ package org.p2p.solanaj.rpc.types; +import org.p2p.solanaj.core.AddressTableLookup; +import org.p2p.solanaj.core.PublicKey; import java.util.List; +import java.util.stream.Collectors; +import java.util.Collections; import com.squareup.moshi.Json; import lombok.Getter; @@ -42,8 +46,14 @@ public static class Instruction { @ToString public static class Message { + @Json(name = "version") + private byte version; + + @Json(name = "addressTableLookups") + private List addressTableLookups; + @Json(name = "accountKeys") - private List accountKeys; + private List accountKeyStrings; @Json(name = "header") private Header header; @@ -53,6 +63,24 @@ public static class Message { @Json(name = "recentBlockhash") private String recentBlockhash; + + /** + * Gets the list of account keys involved in the transaction as PublicKey objects. + * @return The list of account keys as PublicKey objects. + */ + public List getAccountKeys() { + return accountKeyStrings.stream() + .map(PublicKey::new) + .collect(Collectors.toList()); + } + + /** + * Gets the list of address table lookups. + * @return The list of address table lookups. + */ + public List getAddressTableLookups() { + return addressTableLookups != null ? addressTableLookups : Collections.emptyList(); + } } @Getter @@ -131,4 +159,8 @@ public static class Transaction { @Json(name = "transaction") private Transaction transaction; + + public Transaction getTransaction() { + return transaction; + } } diff --git a/src/main/java/org/p2p/solanaj/utils/ShortvecEncoding.java b/src/main/java/org/p2p/solanaj/utils/ShortvecEncoding.java index 4a1d9080..dc74dec8 100644 --- a/src/main/java/org/p2p/solanaj/utils/ShortvecEncoding.java +++ b/src/main/java/org/p2p/solanaj/utils/ShortvecEncoding.java @@ -1,5 +1,8 @@ package org.p2p.solanaj.utils; +import java.util.ArrayList; +import java.util.List; + import static org.bitcoinj.core.Utils.*; public class ShortvecEncoding { @@ -27,4 +30,19 @@ public static byte[] encodeLength(int len) { return bytes; } + + public static int decodeLength(List dataBytesList) { + List dataBytes = new ArrayList<>(dataBytesList); + int len = 0; + int size = 0; + for (;;) { + int elem = (int) dataBytes.remove(0); + len |= (elem & 0x7f) << (size * 7); + size += 1; + if ((elem & 0x80) == 0) { + break; + } + } + return len; + } } diff --git a/src/test/java/org/p2p/solanaj/core/MainnetTest.java b/src/test/java/org/p2p/solanaj/core/MainnetTest.java index 98dcf242..fd13815d 100644 --- a/src/test/java/org/p2p/solanaj/core/MainnetTest.java +++ b/src/test/java/org/p2p/solanaj/core/MainnetTest.java @@ -323,6 +323,8 @@ public void getSlotLeadersTest() throws RpcException { } @Test + @Disabled + @Deprecated public void getSnapshotSlotTest() throws RpcException { long snapshotSlot = client.getApi().getSnapshotSlot(); LOGGER.info(String.format("Snapshot slot = %d", snapshotSlot)); @@ -455,6 +457,8 @@ public void getFeeCalculatorForBlockhashTest() throws RpcException, InterruptedE } @Test + @Disabled + @Deprecated public void getFeesRateGovernorTest() throws RpcException { FeeRateGovernorInfo feeRateGovernorInfo = client.getApi().getFeeRateGovernor(); LOGGER.info(feeRateGovernorInfo.getValue().getFeeRateGovernor().toString()); @@ -468,6 +472,8 @@ public void getFeesRateGovernorTest() throws RpcException { } @Test + @Disabled + @Deprecated public void getFeesInfoTest() throws RpcException { FeesInfo feesInfo = client.getApi().getFees(); LOGGER.info(feesInfo.toString()); @@ -507,6 +513,8 @@ public void getMinimumBalanceForRentExemptionTest() throws RpcException { } @Test + @Disabled + @Deprecated public void getRecentBlockhashTest() throws RpcException { String recentBlockhash = client.getApi().getRecentBlockhash(); LOGGER.info(String.format("Recent blockhash = %s", recentBlockhash)); @@ -762,24 +770,24 @@ public void getTransactionTest() throws RpcException { ConfirmedTransaction transactionInfo = client.getApi().getTransaction(transactionSignature); - String fromKey = transactionInfo.getTransaction().getMessage().getAccountKeys().get(0); - String toKey = transactionInfo.getTransaction().getMessage().getAccountKeys().get(1); + PublicKey fromKey = transactionInfo.getTransaction().getMessage().getAccountKeys().get(0); + PublicKey toKey = transactionInfo.getTransaction().getMessage().getAccountKeys().get(1); - assertEquals("HHntUXQbUBdx8HZQQaT7W1ZSgKRitMtForz4YJXc6qF6", fromKey); - assertEquals("6QcgNYEqHeUohoJWR5ppuRg9Ugh6scMzJY4j4tFnrZMu", toKey); + assertEquals("HHntUXQbUBdx8HZQQaT7W1ZSgKRitMtForz4YJXc6qF6", fromKey.toBase58()); + assertEquals("6QcgNYEqHeUohoJWR5ppuRg9Ugh6scMzJY4j4tFnrZMu", toKey.toBase58()); ConfirmedTransaction transactionInfoCommitted = client.getApi().getTransaction(transactionSignature, Commitment.CONFIRMED); - String fromKeyCommitted = transactionInfoCommitted.getTransaction().getMessage().getAccountKeys().get(0); - String toKeyCommitted = transactionInfoCommitted.getTransaction().getMessage().getAccountKeys().get(1); + PublicKey fromKeyCommitted = transactionInfoCommitted.getTransaction().getMessage().getAccountKeys().get(0); + PublicKey toKeyCommitted = transactionInfoCommitted.getTransaction().getMessage().getAccountKeys().get(1); - assertEquals("HHntUXQbUBdx8HZQQaT7W1ZSgKRitMtForz4YJXc6qF6", fromKeyCommitted); - assertEquals("6QcgNYEqHeUohoJWR5ppuRg9Ugh6scMzJY4j4tFnrZMu", toKeyCommitted); + assertEquals("HHntUXQbUBdx8HZQQaT7W1ZSgKRitMtForz4YJXc6qF6", fromKeyCommitted.toBase58()); + assertEquals("6QcgNYEqHeUohoJWR5ppuRg9Ugh6scMzJY4j4tFnrZMu", toKeyCommitted.toBase58()); } @Test public void isBlockhashValidTest() throws RpcException, InterruptedException { - String recentBlockHash = client.getApi().getRecentBlockhash(); + String recentBlockHash = client.getApi().getLatestBlockhash().getValue().getBlockhash(); Thread.sleep(500L); assertTrue(client.getApi().isBlockhashValid(recentBlockHash)); } diff --git a/src/test/java/org/p2p/solanaj/core/TransactionBuilderTest.java b/src/test/java/org/p2p/solanaj/core/TransactionBuilderTest.java new file mode 100644 index 00000000..2438b1b5 --- /dev/null +++ b/src/test/java/org/p2p/solanaj/core/TransactionBuilderTest.java @@ -0,0 +1,150 @@ +package org.p2p.solanaj.core; + +import org.bitcoinj.core.Base58; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +import org.p2p.solanaj.programs.SystemProgram; +import org.p2p.solanaj.programs.MemoProgram; +import org.p2p.solanaj.rpc.Cluster; +import org.p2p.solanaj.rpc.RpcApi; +import org.p2p.solanaj.rpc.RpcClient; +import org.p2p.solanaj.rpc.RpcException; +import org.p2p.solanaj.rpc.types.ConfirmedTransaction; + +import java.util.Arrays; +import java.util.Base64; +import java.util.List; + +/** + * Unit tests for TransactionBuilder. + */ +public class TransactionBuilderTest { + + private static final Account signer = new Account(Base58.decode("4Z7cXSyeFR8wNGMVXUE1TwtKn5D5Vu7FzEv69dokLv7KrQk7h6pu4LF8ZRR9yQBhc7uSM6RTTZtU1fmaxiNrxXrs")); + + /** + * Tests building a legacy transaction using TransactionBuilder. + */ + @Test + public void buildLegacyTransaction() { + PublicKey fromPublicKey = new PublicKey("QqCCvshxtqMAL2CVALqiJB7uEeE5mjSPsseQdDzsRUo"); + PublicKey toPublicKey = new PublicKey("GrDMoeqMLFjeXQ24H56S1RLgT4R76jsuWCd6SvXyGPQ5"); + int lamports = 5000; + + Transaction transaction = new TransactionBuilder() + .addInstruction(SystemProgram.transfer(fromPublicKey, toPublicKey, lamports)) + .setRecentBlockHash("GrDMoeqMLFjeXQ24H56S1RLgT4R76jsuWCd6SvXyGPQ5") + .setSigners(List.of(signer)) + .build(); + + byte[] serialized = transaction.serialize(); + String serializedBase64 = Base64.getEncoder().encodeToString(serialized); + + assertNotNull(serializedBase64); + assertFalse(serializedBase64.isEmpty()); + + // Additional assertions can be added based on expected serialized output + } + + /** + * Tests adding an instruction to the TransactionBuilder. + */ + @Test + public void addInstructionTest() { + PublicKey fromPublicKey = new PublicKey("QqCCvshxtqMAL2CVALqiJB7uEeE5mjSPsseQdDzsRUo"); + PublicKey toPublicKey = new PublicKey("GrDMoeqMLFjeXQ24H56S1RLgT4R76jsuWCd6SvXyGPQ5"); + int lamports = 10000; + + TransactionInstruction transferInstruction = SystemProgram.transfer(fromPublicKey, toPublicKey, lamports); + TransactionInstruction memoInstruction = MemoProgram.writeUtf8(signer.getPublicKey(), "Test Memo"); + + Transaction transaction = new TransactionBuilder() + .addInstruction(transferInstruction) + .addInstruction(memoInstruction) + .setRecentBlockHash("QqCCvshxtqMAL2CVALqiJB7uEeE5mjSPsseQdDzsRUo") + .setSigners(List.of(signer)) + .build(); + + byte[] serialized = transaction.serialize(); + String serializedBase64 = Base64.getEncoder().encodeToString(serialized); + + assertNotNull(serializedBase64); + assertFalse(serializedBase64.isEmpty()); + + // Example assertions to verify both instructions are included + // These would need to match the expected serialized format + } + + /** + * Tests building a versioned transaction with Address Lookup Tables using TransactionBuilder. + */ + @Test + public void buildV0TransactionWithALT() { + PublicKey fromPublicKey = new PublicKey("QqCCvshxtqMAL2CVALqiJB7uEeE5mjSPsseQdDzsRUo"); + PublicKey toPublicKey = new PublicKey("GrDMoeqMLFjeXQ24H56S1RLgT4R76jsuWCd6SvXyGPQ5"); + int lamports = 7000; + + PublicKey lookupTableAddress = new PublicKey("BPFLoaderUpgradeab1e11111111111111111111111"); + List writableIndexes = Arrays.asList((byte) 0, (byte) 1); + List readonlyIndexes = Arrays.asList((byte) 2); + + Transaction transaction = new TransactionBuilder() + .addInstruction(SystemProgram.transfer(fromPublicKey, toPublicKey, lamports)) + .setRecentBlockHash("Eit7RCyhUixAe2hGBS8oqnw59QK3kgMMjfLME5bm9wRn") + .setSigners(List.of(signer)) + .setVersion((byte) 0) + .addAddressTableLookup(lookupTableAddress, writableIndexes, readonlyIndexes) + .build(); + + byte[] serializedTransaction = transaction.serialize(); + + // Assert that the serialized transaction starts with version 0 + assertEquals(0, serializedTransaction[0]); + + // Verify the presence of address table lookup data + assertTrue(serializedTransaction.length > 200); // Approximate length check + } + + /** + * Tests retrieving a version 0 transaction with an Address Lookup Table from the RPC API. + * + * @throws RpcException if there is an error during the RPC call. + */ + @Test + public void testRetrieveV0TransactionWithALT() throws RpcException { + RpcClient rpcClient = new RpcClient(Cluster.MAINNET); + RpcApi api = rpcClient.getApi(); + + // This is a known v0 transaction with ALT on mainnet + String signature = "3t4B38bCZWRxYktRjMEmzE6YdyaZaq2rX74QUHGU5sSxQmxsTL2guuQ6Nf9cfsQFavhpJNJDeDK6D9MKx3ojTw16"; + + ConfirmedTransaction confirmedTx = api.getTransaction(signature); + assertNotNull(confirmedTx); + + ConfirmedTransaction.Message message = confirmedTx.getTransaction().getMessage(); + assertNotNull(message); + + // Verify that this is a v0 transaction + assertEquals(0, message.getVersion()); + + // Verify the presence of account keys + List accountKeyStrings = message.getAccountKeyStrings(); + assertNotNull(accountKeyStrings); + assertFalse(accountKeyStrings.isEmpty()); + + // Verify the presence of address table lookups + List addressTableLookups = message.getAddressTableLookups(); + assertNotNull(addressTableLookups); + assertFalse(addressTableLookups.isEmpty()); + + // Verify the first address table lookup + AddressTableLookup firstLookup = addressTableLookups.get(0); + assertNotNull(firstLookup); + assertNotNull(firstLookup.getAccountKey()); + assertFalse(firstLookup.getWritableIndexes().isEmpty()); + assertFalse(firstLookup.getReadonlyIndexes().isEmpty()); + + // Additional validations or logs can be added here + } +} \ No newline at end of file diff --git a/src/test/java/org/p2p/solanaj/core/TransactionTest.java b/src/test/java/org/p2p/solanaj/core/TransactionTest.java index eae14086..65b240bb 100644 --- a/src/test/java/org/p2p/solanaj/core/TransactionTest.java +++ b/src/test/java/org/p2p/solanaj/core/TransactionTest.java @@ -1,38 +1,52 @@ package org.p2p.solanaj.core; +import org.bitcoinj.core.Base58; +import org.junit.jupiter.api.Test; import org.p2p.solanaj.programs.MemoProgram; import org.p2p.solanaj.programs.SystemProgram; +import org.p2p.solanaj.rpc.Cluster; +import org.p2p.solanaj.rpc.RpcApi; +import org.p2p.solanaj.rpc.RpcClient; +import org.p2p.solanaj.rpc.RpcException; +import org.p2p.solanaj.rpc.types.ConfirmedTransaction; -import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; +import java.util.Arrays; import java.util.Base64; import java.util.List; -import org.bitcoinj.core.Base58; - +/** + * Unit tests for Transaction. + */ public class TransactionTest { - private final static Account signer = new Account(Base58 - .decode("4Z7cXSyeFR8wNGMVXUE1TwtKn5D5Vu7FzEv69dokLv7KrQk7h6pu4LF8ZRR9yQBhc7uSM6RTTZtU1fmaxiNrxXrs")); + private static final Account signer = new Account(Base58.decode("4Z7cXSyeFR8wNGMVXUE1TwtKn5D5Vu7FzEv69dokLv7KrQk7h6pu4LF8ZRR9yQBhc7uSM6RTTZtU1fmaxiNrxXrs")); + /** + * Tests signing and serializing a transaction. + */ @Test public void signAndSerialize() { PublicKey fromPublicKey = new PublicKey("QqCCvshxtqMAL2CVALqiJB7uEeE5mjSPsseQdDzsRUo"); - PublicKey toPublickKey = new PublicKey("GrDMoeqMLFjeXQ24H56S1RLgT4R76jsuWCd6SvXyGPQ5"); + PublicKey toPublicKey = new PublicKey("GrDMoeqMLFjeXQ24H56S1RLgT4R76jsuWCd6SvXyGPQ5"); int lamports = 3000; Transaction transaction = new Transaction(); - transaction.addInstruction(SystemProgram.transfer(fromPublicKey, toPublickKey, lamports)); + transaction.addInstruction(SystemProgram.transfer(fromPublicKey, toPublicKey, lamports)); transaction.setRecentBlockHash("Eit7RCyhUixAe2hGBS8oqnw59QK3kgMMjfLME5bm9wRn"); transaction.sign(signer); byte[] serializedTransaction = transaction.serialize(); assertEquals( "ASdDdWBaKXVRA+6flVFiZokic9gK0+r1JWgwGg/GJAkLSreYrGF4rbTCXNJvyut6K6hupJtm72GztLbWNmRF1Q4BAAEDBhrZ0FOHFUhTft4+JhhJo9+3/QL6vHWyI8jkatuFPQzrerzQ2HXrwm2hsYGjM5s+8qMWlbt6vbxngnO8rc3lqgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAy+KIwZmU8DLmYglP3bPzrlpDaKkGu6VIJJwTOYQmRfUBAgIAAQwCAAAAuAsAAAAAAAA=", - Base64.getEncoder().encodeToString(serializedTransaction)); + Base64.getEncoder().encodeToString(serializedTransaction) + ); } + /** + * Tests building a transaction with the TransactionBuilder. + */ @Test public void transactionBuilderTest() { final String memo = "Test memo"; @@ -53,4 +67,105 @@ public void transactionBuilderTest() { ); } -} + /** + * Tests creating a version 0 transaction with an Address Lookup Table. + */ + @Test + public void testV0TransactionWithAddressLookupTable() { + Transaction transaction = new Transaction(); + transaction.setVersion((byte) 0); + + PublicKey fromPublicKey = new PublicKey("QqCCvshxtqMAL2CVALqiJB7uEeE5mjSPsseQdDzsRUo"); + PublicKey toPublicKey = new PublicKey("GrDMoeqMLFjeXQ24H56S1RLgT4R76jsuWCd6SvXyGPQ5"); + int lamports = 3000; + + transaction.addInstruction(SystemProgram.transfer(fromPublicKey, toPublicKey, lamports)); + transaction.setRecentBlockHash("Eit7RCyhUixAe2hGBS8oqnw59QK3kgMMjfLME5bm9wRn"); + + PublicKey lookupTableAddress = new PublicKey("BPFLoaderUpgradeab1e11111111111111111111111"); + List writableIndexes = Arrays.asList((byte) 0, (byte) 1); + List readonlyIndexes = Arrays.asList((byte) 2); + transaction.addAddressTableLookup(lookupTableAddress, writableIndexes, readonlyIndexes); + + transaction.sign(signer); + byte[] serializedTransaction = transaction.serialize(); + + // Assert that the serialized transaction starts with version 0 + assertEquals(0, serializedTransaction[0]); + + // Verify the presence of address table lookup data + assertTrue(serializedTransaction.length > 200); // Approximate length check + } + + /** + * Tests building a version 0 transaction using TransactionBuilder with an Address Lookup Table. + */ + @Test + public void testV0TransactionBuilder() { + PublicKey fromPublicKey = new PublicKey("QqCCvshxtqMAL2CVALqiJB7uEeE5mjSPsseQdDzsRUo"); + PublicKey toPublicKey = new PublicKey("GrDMoeqMLFjeXQ24H56S1RLgT4R76jsuWCd6SvXyGPQ5"); + int lamports = 3000; + + PublicKey lookupTableAddress = new PublicKey("BPFLoaderUpgradeab1e11111111111111111111111"); + List writableIndexes = Arrays.asList((byte) 0, (byte) 1); + List readonlyIndexes = Arrays.asList((byte) 2); + + Transaction transaction = new TransactionBuilder() + .addInstruction(SystemProgram.transfer(fromPublicKey, toPublicKey, lamports)) + .setRecentBlockHash("Eit7RCyhUixAe2hGBS8oqnw59QK3kgMMjfLME5bm9wRn") + .setSigners(List.of(signer)) + .setVersion((byte) 0) + .addAddressTableLookup(lookupTableAddress, writableIndexes, readonlyIndexes) + .build(); + + byte[] serializedTransaction = transaction.serialize(); + + // Assert that the serialized transaction starts with version 0 + assertEquals(0, serializedTransaction[0]); + + // Verify the presence of address table lookup data + assertTrue(serializedTransaction.length > 200); // Approximate length check + } + + /** + * Tests retrieving a version 0 transaction with an Address Lookup Table from the RPC API. + * + * @throws RpcException if there is an error during the RPC call. + */ + @Test + public void testRetrieveV0TransactionWithALT() throws RpcException { + RpcClient rpcClient = new RpcClient(Cluster.MAINNET); + RpcApi api = rpcClient.getApi(); + + // This is a known v0 transaction with ALT on mainnet + String signature = "3t4B38bCZWRxYktRjMEmzE6YdyaZaq2rX74QUHGU5sSxQmxsTL2guuQ6Nf9cfsQFavhpJNJDeDK6D9MKx3ojTw16"; + + ConfirmedTransaction confirmedTx = api.getTransaction(signature); + assertNotNull(confirmedTx); + + ConfirmedTransaction.Message message = confirmedTx.getTransaction().getMessage(); + assertNotNull(message); + + // Verify that this is a v0 transaction + assertEquals(0, message.getVersion()); + + // Verify the presence of account keys + List accountKeyStrings = message.getAccountKeyStrings(); + assertNotNull(accountKeyStrings); + assertFalse(accountKeyStrings.isEmpty()); + + // Verify the presence of address table lookups + List addressTableLookups = message.getAddressTableLookups(); + assertNotNull(addressTableLookups); + assertFalse(addressTableLookups.isEmpty()); + + // Verify the first address table lookup + AddressTableLookup firstLookup = addressTableLookups.get(0); + assertNotNull(firstLookup); + assertNotNull(firstLookup.getAccountKey()); + assertFalse(firstLookup.getWritableIndexes().isEmpty()); + assertFalse(firstLookup.getReadonlyIndexes().isEmpty()); + + // Additional validations or logs can be added here + } +} \ No newline at end of file diff --git a/src/test/java/org/p2p/solanaj/programs/AddressLookupTableProgramTest.java b/src/test/java/org/p2p/solanaj/programs/AddressLookupTableProgramTest.java index 75871ffd..4bf6674e 100644 --- a/src/test/java/org/p2p/solanaj/programs/AddressLookupTableProgramTest.java +++ b/src/test/java/org/p2p/solanaj/programs/AddressLookupTableProgramTest.java @@ -3,27 +3,39 @@ import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; +import org.p2p.solanaj.core.AccountMeta; import org.p2p.solanaj.core.PublicKey; import org.p2p.solanaj.core.TransactionInstruction; -import java.util.Collections; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.List; +/** + * Unit tests for AddressLookupTableProgram. + */ public class AddressLookupTableProgramTest { - private static final PublicKey AUTHORITY = new PublicKey("BPFLoaderUpgradeab1e11111111111111111111111"); - private static final PublicKey PAYER = new PublicKey("11111111111111111111111111111111"); - private static final PublicKey LOOKUP_TABLE = new PublicKey("AddressLookupTab1e1111111111111111111111111"); - private static final long RECENT_SLOT = 123456; - /** * Test for creating a lookup table. */ @Test public void testCreateLookupTable() { - TransactionInstruction instruction = AddressLookupTableProgram.createLookupTable(AUTHORITY, PAYER, RECENT_SLOT); + PublicKey authority = new PublicKey("QqCCvshxtqMAL2CVALqiJB7uEeE5mjSPsseQdDzsRUo"); + PublicKey payer = new PublicKey("GrDMoeqMLFjeXQ24H56S1RLgT4R76jsuWCd6SvXyGPQ5"); + long recentSlot = 123456789L; + + TransactionInstruction instruction = AddressLookupTableProgram.createLookupTable(authority, payer, recentSlot); assertNotNull(instruction); assertEquals(AddressLookupTableProgram.PROGRAM_ID, instruction.getProgramId()); assertEquals(4, instruction.getKeys().size()); // Check number of keys + + // Validate data + byte[] expectedData = new byte[9]; + expectedData[0] = 0; // CREATE_LOOKUP_TABLE + ByteBuffer buffer = ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN).putLong(recentSlot); + System.arraycopy(buffer.array(), 0, expectedData, 1, 8); + assertArrayEquals(expectedData, instruction.getData()); } /** @@ -31,10 +43,17 @@ public void testCreateLookupTable() { */ @Test public void testFreezeLookupTable() { - TransactionInstruction instruction = AddressLookupTableProgram.freezeLookupTable(LOOKUP_TABLE, AUTHORITY); + PublicKey authority = new PublicKey("QqCCvshxtqMAL2CVALqiJB7uEeE5mjSPsseQdDzsRUo"); + PublicKey lookupTable = new PublicKey("BPFLoaderUpgradeab1e11111111111111111111111"); + + TransactionInstruction instruction = AddressLookupTableProgram.freezeLookupTable(lookupTable, authority); assertNotNull(instruction); assertEquals(AddressLookupTableProgram.PROGRAM_ID, instruction.getProgramId()); assertEquals(2, instruction.getKeys().size()); // Check number of keys + + // Validate data + byte[] expectedData = new byte[]{1}; // FREEZE_LOOKUP_TABLE + assertArrayEquals(expectedData, instruction.getData()); } /** @@ -42,11 +61,30 @@ public void testFreezeLookupTable() { */ @Test public void testExtendLookupTable() { - PublicKey addressToAdd = new PublicKey("SysvarC1ock11111111111111111111111111111111"); - TransactionInstruction instruction = AddressLookupTableProgram.extendLookupTable(LOOKUP_TABLE, AUTHORITY, PAYER, Collections.singletonList(addressToAdd)); + PublicKey lookupTable = new PublicKey("skynetDj29GH6o6bAqoixCpDuYtWqi1rm8ZNx1hB3vq"); + PublicKey payer = new PublicKey("GrDMoeqMLFjeXQ24H56S1RLgT4R76jsuWCd6SvXyGPQ5"); + PublicKey authority = new PublicKey("QqCCvshxtqMAL2CVALqiJB7uEeE5mjSPsseQdDzsRUo"); + List addresses = List.of( + new PublicKey("GrDMoeqMLFjeXQ24H56S1RLgT4R76jsuWCd6SvXyGPQ5"), + new PublicKey("QqCCvshxtqMAL2CVALqiJB7uEeE5mjSPsseQdDzsRUo") + ); + + TransactionInstruction instruction = AddressLookupTableProgram.extendLookupTable(lookupTable, payer, authority, addresses); assertNotNull(instruction); assertEquals(AddressLookupTableProgram.PROGRAM_ID, instruction.getProgramId()); - assertEquals(4, instruction.getKeys().size()); // Check number of keys + assertEquals(3, instruction.getKeys().size()); + assertEquals(1 + 4 + addresses.size() * 32, instruction.getData().length); + + // Validate data + ByteBuffer data = ByteBuffer.wrap(instruction.getData()).order(ByteOrder.LITTLE_ENDIAN); + assertEquals(2, data.get()); // EXTEND_LOOKUP_TABLE + assertEquals(addresses.size(), data.getInt()); + + for (PublicKey address : addresses) { + byte[] addrBytes = new byte[32]; + data.get(addrBytes); + assertArrayEquals(address.toByteArray(), addrBytes); + } } /** @@ -54,10 +92,17 @@ public void testExtendLookupTable() { */ @Test public void testDeactivateLookupTable() { - TransactionInstruction instruction = AddressLookupTableProgram.deactivateLookupTable(LOOKUP_TABLE, AUTHORITY); + PublicKey lookupTable = new PublicKey("skynetDj29GH6o6bAqoixCpDuYtWqi1rm8ZNx1hB3vq"); + PublicKey authority = new PublicKey("skynetDj29GH6o6bAqoixCpDuYtWqi1rm8ZNx1hB3vq"); + + TransactionInstruction instruction = AddressLookupTableProgram.deactivateLookupTable(lookupTable, authority); assertNotNull(instruction); assertEquals(AddressLookupTableProgram.PROGRAM_ID, instruction.getProgramId()); - assertEquals(2, instruction.getKeys().size()); // Check number of keys + assertEquals(2, instruction.getKeys().size()); + + // Validate data + byte[] expectedData = new byte[]{3}; // DEACTIVATE_LOOKUP_TABLE + assertArrayEquals(expectedData, instruction.getData()); } /** @@ -65,10 +110,17 @@ public void testDeactivateLookupTable() { */ @Test public void testCloseLookupTable() { - PublicKey recipient = new PublicKey("SysvarRent111111111111111111111111111111111"); - TransactionInstruction instruction = AddressLookupTableProgram.closeLookupTable(LOOKUP_TABLE, AUTHORITY, recipient); + PublicKey lookupTable = new PublicKey("skynetDj29GH6o6bAqoixCpDuYtWqi1rm8ZNx1hB3vq"); + PublicKey authority = new PublicKey("skynetDj29GH6o6bAqoixCpDuYtWqi1rm8ZNx1hB3vq"); + PublicKey recipient = new PublicKey("skynetDj29GH6o6bAqoixCpDuYtWqi1rm8ZNx1hB3vq"); + + TransactionInstruction instruction = AddressLookupTableProgram.closeLookupTable(lookupTable, authority, recipient); assertNotNull(instruction); assertEquals(AddressLookupTableProgram.PROGRAM_ID, instruction.getProgramId()); - assertEquals(3, instruction.getKeys().size()); // Check number of keys + assertEquals(3, instruction.getKeys().size()); + + // Validate data + byte[] expectedData = new byte[]{4}; // CLOSE_LOOKUP_TABLE + assertArrayEquals(expectedData, instruction.getData()); } } \ No newline at end of file