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
22 changes: 13 additions & 9 deletions src/main/java/org/p2p/solanaj/core/Message.java
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ int getLength() {
private String recentBlockhash;
private AccountKeysList accountKeys;
private List<TransactionInstruction> instructions;
private Account feePayer;

public Message() {
this.accountKeys = new AccountKeysList();
Expand All @@ -66,7 +65,7 @@ public byte[] serialize() {
throw new IllegalArgumentException("recentBlockhash required");
}

if (instructions.size() == 0) {
if (instructions.isEmpty()) {
throw new IllegalArgumentException("No instructions provided");
}

Expand Down Expand Up @@ -143,15 +142,20 @@ public byte[] serialize() {
return out.array();
}

protected void setFeePayer(Account feePayer) {
this.feePayer = feePayer;
public void setFeePayer(Account feePayer) {
setFeePayer(feePayer.getPublicKey());
}

/** Prepend specific account as fee payer (place it as first entry on account list). */
public void setFeePayer(PublicKey feePayer) {
AccountKeysList old = this.accountKeys;
this.accountKeys = new AccountKeysList();
this.accountKeys.add(new AccountMeta(feePayer, true, true));
this.accountKeys.addAll(old);
}

public List<AccountMeta> getAccountKeys() {
AccountKeysList accounts = new AccountKeysList();
accounts.add(new AccountMeta(feePayer.getPublicKey(), true, true));
accounts.addAll(accountKeys);
return accounts.getList();
return this.accountKeys.getList();
}

private int findAccountIndex(List<AccountMeta> accountMetaList, PublicKey key) {
Expand All @@ -161,6 +165,6 @@ private int findAccountIndex(List<AccountMeta> accountMetaList, PublicKey key) {
}
}

throw new RuntimeException("unable to find account index");
throw new RuntimeException("unable to find index for account " + key.toBase58());
}
}
77 changes: 44 additions & 33 deletions src/main/java/org/p2p/solanaj/core/Transaction.java
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
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 java.util.*;
import java.util.stream.Stream;

import org.bitcoinj.core.Base58;
import org.p2p.solanaj.utils.ShortvecEncoding;
Expand All @@ -19,15 +17,14 @@ public class Transaction {
public static final int SIGNATURE_LENGTH = 64;

private final Message message;
private final List<String> signatures;
private byte[] serializedMessage;
private final List<Account> signers; // TODO: more like Map[PublicKey,Account]

/**
* Constructs a new Transaction instance.
*/
public Transaction() {
this.message = new Message();
this.signatures = new ArrayList<>(); // Use diamond operator
this.signers = new ArrayList<>();
}

/**
Expand All @@ -54,14 +51,24 @@ public void setRecentBlockHash(String recentBlockhash) {
message.setRecentBlockHash(recentBlockhash);
}

/** Use specific account as transaction fee payer (first signer) */
public void setFeePayer(PublicKey feePayer) {
Objects.requireNonNull(feePayer, "FeePayer cannot be null");
message.setFeePayer(feePayer);
}

/**
* Signs the transaction with a single signer.
* Signs the transaction with a specific signer.
*
* Note - this method does not affect signatures order. If specific account is to be used
* for first signature, `setFeePayer` method should be used additionally.
*
* @param signer The account to sign the transaction
* @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
Objects.requireNonNull(signer, "Signer cannot be null");
this.signers.add(signer);
}

/**
Expand All @@ -71,32 +78,40 @@ public void sign(Account signer) {
* @throws IllegalArgumentException if no signers are provided
*/
public void sign(List<Account> signers) {
if (signers == null || signers.isEmpty()) {
throw new IllegalArgumentException("No signers provided");
}

Account feePayer = signers.get(0);
message.setFeePayer(feePayer);

serializedMessage = message.serialize();

for (Account signer : signers) {
try {
TweetNaclFast.Signature signatureProvider = new TweetNaclFast.Signature(new byte[0], signer.getSecretKey());
byte[] signature = signatureProvider.detached(serializedMessage);
signatures.add(Base58.encode(signature));
} catch (Exception e) {
throw new RuntimeException("Error signing transaction", e); // Improve exception handling
}
}
Objects.requireNonNull(signers, "Signer cannot be null");
this.signers.addAll(signers);
}

/**
* Serializes the transaction into a byte array.
* Signs and serializes the transaction into a byte array.
*
* @return The serialized transaction as a byte array
*/
public byte[] serialize() {

byte[] serializedMessage = message.serialize();

// TODO: use signers lookup, fail on excessive signers
Stream<Account> requiredSigners = message.getAccountKeys()
.stream()
.filter(AccountMeta::isSigner)
.map(AccountMeta::getPublicKey)
.map(publicKey ->
signers.stream()
.filter(account -> account.getPublicKey().equals(publicKey))
.findFirst()
.orElseThrow(() -> new RuntimeException("Missing signer for account "+publicKey)));

// TODO: validate signature length
List<byte[]> signatures = requiredSigners.map(signer -> {
try {
TweetNaclFast.Signature signatureProvider = new TweetNaclFast.Signature(new byte[0], signer.getSecretKey());
return signatureProvider.detached(serializedMessage);
} catch (Exception e) {
throw new RuntimeException("Error signing transaction", e);
}}).toList();


int signaturesSize = signatures.size();
byte[] signaturesLength = ShortvecEncoding.encodeLength(signaturesSize);

Expand All @@ -105,11 +120,7 @@ public byte[] serialize() {
ByteBuffer out = ByteBuffer.allocate(totalSize);

out.put(signaturesLength);

for (String signature : signatures) {
byte[] rawSignature = Base58.decode(signature);
out.put(rawSignature);
}
signatures.forEach(out::put);

out.put(serializedMessage);

Expand Down
5 changes: 5 additions & 0 deletions src/main/java/org/p2p/solanaj/core/TransactionBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ public TransactionBuilder setSigners(List<Account> signers) {
return this;
}

public TransactionBuilder setFeePayer(PublicKey feePayer) {
transaction.setFeePayer(feePayer);
return this;
}

/**
* Builds and returns the constructed Transaction object.
*
Expand Down
1 change: 1 addition & 0 deletions src/test/java/org/p2p/solanaj/core/TransactionTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ public void transactionBuilderTest() {
)
)
.setRecentBlockHash("Eit7RCyhUixAe2hGBS8oqnw59QK3kgMMjfLME5bm9wRn")
.setFeePayer(signer.getPublicKey()) // note the memo program does not encode fee payer account, so it needs to be specified manually
.setSigners(List.of(signer))
.build();

Expand Down