Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions src/main/java/org/p2p/solanaj/core/AddressTableLookup.java
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* ALTs allow transactions to reference additional addresses required for execution,
* enabling transactions that exceed the maximum number of accounts.
* </p>
*/
@AllArgsConstructor
public class AddressTableLookup {
@Json(name = "accountKey")
private String accountKey;

@Getter
@Json(name = "writableIndexes")
private List<Byte> writableIndexes;

@Getter
@Json(name = "readonlyIndexes")
private List<Byte> 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<Byte> writableIndexes, List<Byte> 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();
}
}
19 changes: 19 additions & 0 deletions src/main/java/org/p2p/solanaj/core/Message.java
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,12 @@ int getLength() {
private AccountKeysList accountKeys;
private List<TransactionInstruction> instructions;
private Account feePayer;
private List<AddressTableLookup> addressTableLookups;

public Message() {
this.accountKeys = new AccountKeysList();
this.instructions = new ArrayList<TransactionInstruction>();
this.addressTableLookups = new ArrayList<>();
}

public Message addInstruction(TransactionInstruction instruction) {
Expand Down Expand Up @@ -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();
}

Expand Down Expand Up @@ -178,4 +189,12 @@ private int findAccountIndex(List<AccountMeta> accountMetaList, PublicKey key) {

throw new RuntimeException("unable to find account index");
}

public void addAddressTableLookup(AddressTableLookup lookup) {
addressTableLookups.add(lookup);
}

public List<AddressTableLookup> getAddressTableLookups() {
return addressTableLookups;
}
}
58 changes: 50 additions & 8 deletions src/main/java/org/p2p/solanaj/core/Transaction.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
* <p>
* This class allows for building, signing, and serializing transactions.
* It supports both legacy and versioned transactions with Address Lookup Tables (ALTs).
* </p>
*/
public class Transaction {

Expand All @@ -22,12 +26,18 @@ public class Transaction {
private final List<String> signatures;
private byte[] serializedMessage;

@Getter
@Setter
private byte version = (byte) 0xFF; // Default to legacy version

private final List<AddressTableLookup> 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<>();
}

/**
Expand All @@ -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;
}
Expand All @@ -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);
}

Expand All @@ -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")));
}

/**
Expand All @@ -86,7 +96,7 @@ public void sign(List<Account> 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);
}
}
}
Expand All @@ -97,13 +107,26 @@ public void sign(List<Account> 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) {
Expand All @@ -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<Byte> writableIndexes, List<Byte> readonlyIndexes) {
addressTableLookups.add(new AddressTableLookup(tablePubkey, writableIndexes, readonlyIndexes));
}
}
56 changes: 40 additions & 16 deletions src/main/java/org/p2p/solanaj/core/TransactionBuilder.java
Original file line number Diff line number Diff line change
@@ -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();
Expand Down Expand Up @@ -44,25 +46,24 @@ public TransactionBuilder addInstructions(List<TransactionInstruction> 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<Account> signers) {
Objects.requireNonNull(signers, "Signers list cannot be null");
Expand All @@ -73,6 +74,30 @@ public TransactionBuilder setSigners(List<Account> 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<Byte> writableIndexes, List<Byte> readonlyIndexes) {
transaction.addAddressTableLookup(tablePubkey, writableIndexes, readonlyIndexes);
return this;
}

/**
* Builds and returns the constructed Transaction object.
*
Expand All @@ -81,5 +106,4 @@ public TransactionBuilder setSigners(List<Account> signers) {
public Transaction build() {
return transaction;
}

}
}
Loading