Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Deserialize transaction #88

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
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
18 changes: 18 additions & 0 deletions src/main/java/org/p2p/solanaj/core/AccountMeta.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,22 @@ public class AccountMeta {

private boolean isWritable;

/**
* Sorting based on isSigner and isWritable cannot fully meet the requirements. This value can be used for custom sorting, because if the order is incorrect during serialization, it may lead to failed method calls.
*/
private int sort = Integer.MAX_VALUE;

public AccountMeta(PublicKey publicKey, boolean isSigner, boolean isWritable) {
this.publicKey = publicKey;
this.isSigner = isSigner;
this.isWritable = isWritable;
}

public void setSigner(boolean signer) {
isSigner = signer;
}

public void setWritable(boolean writable) {
isWritable = writable;
}
}
199 changes: 184 additions & 15 deletions src/main/java/org/p2p/solanaj/core/Message.java
Original file line number Diff line number Diff line change
@@ -1,28 +1,38 @@
package org.p2p.solanaj.core;

import org.bitcoinj.core.Base58;
import org.p2p.solanaj.utils.ArrayUtils;
import org.p2p.solanaj.utils.ShortvecEncoding;

import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;

public class Message {
private class MessageHeader {
private static class MessageHeader {
static final int HEADER_LENGTH = 3;

byte numRequiredSignatures = 0;
byte numReadonlySignedAccounts = 0;
byte numReadonlyUnsignedAccounts = 0;

public MessageHeader(){}

MessageHeader(byte[] byteArray) {
numRequiredSignatures = byteArray[0];
numReadonlySignedAccounts = byteArray[1];
numReadonlyUnsignedAccounts = byteArray[2];
}

byte[] toByteArray() {
return new byte[] { numRequiredSignatures, numReadonlySignedAccounts, numReadonlyUnsignedAccounts };
}
}

private class CompiledInstruction {
private static class CompiledInstruction {
byte programIdIndex;
byte[] keyIndicesCount;
byte[] keyIndices;
Expand All @@ -39,46 +49,85 @@ int getLength() {

private MessageHeader messageHeader;
private String recentBlockhash;
private AccountKeysList accountKeys;
private List<TransactionInstruction> instructions;
private Account feePayer;
private final AccountKeysList accountKeys;
private final List<TransactionInstruction> instructions;
private PublicKey feePayer;

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

public Message(MessageHeader messageHeader, String recentBlockhash, AccountKeysList accountKeys,
List<TransactionInstruction> compiledInstructions) {
this.messageHeader = messageHeader;
this.recentBlockhash = recentBlockhash;
this.accountKeys = accountKeys;
this.instructions = compiledInstructions;

this.feePayer = accountKeys.getList().get(0).getPublicKey();
}

public Message 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 String getRecentBlockhash() {
return recentBlockhash;
}

public MessageHeader getMessageHeader() {
return messageHeader;
}

public List<TransactionInstruction> getInstructions() {
return instructions;
}

public byte[] serialize() {

if (recentBlockhash == null) {
throw new IllegalArgumentException("recentBlockhash required");
}

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

messageHeader = new MessageHeader();

List<AccountMeta> keysList = getAccountKeys();
/**
* #################################################
* ########## Here's the change. sort ##############
* #################################################
*
*/
Collections.sort(keysList, new Comparator<AccountMeta>() {
@Override
public int compare(AccountMeta o1, AccountMeta o2) {
if(o2.isSigner()){
return 1;
}else if(o1.isSigner()){
return -1;
}else{
return 0;
}
}
});
int accountKeysSize = keysList.size();

byte[] accountAddressesLength = ShortvecEncoding.encodeLength(accountKeysSize);

int compiledInstructionsLength = 0;
List<CompiledInstruction> compiledInstructions = new ArrayList<CompiledInstruction>();
List<CompiledInstruction> compiledInstructions = new ArrayList<>();

for (TransactionInstruction instruction : instructions) {
int keysSize = instruction.getKeys().size();
Expand Down Expand Up @@ -143,18 +192,37 @@ public byte[] serialize() {
return out.array();
}

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

public PublicKey getFeePayer() {
return feePayer;
}

public List<AccountMeta> getAccountKeys() {
AccountKeysList accounts = new AccountKeysList();
accounts.add(new AccountMeta(feePayer.getPublicKey(), true, true));
accounts.addAll(accountKeys);
return accounts.getList();
List<AccountMeta> 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);
List<AccountMeta> newList = new ArrayList<AccountMeta>();
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<AccountMeta> accountMetaList, PublicKey key) {
public int findAccountIndex(List<AccountMeta> accountMetaList, PublicKey key) {
for (int i = 0; i < accountMetaList.size(); i++) {
if (accountMetaList.get(i).getPublicKey().equals(key)) {
return i;
Expand All @@ -163,4 +231,105 @@ private int findAccountIndex(List<AccountMeta> accountMetaList, PublicKey key) {

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

/**
* deserialize Message
* @param serializedMessageList message serialize byte array
* @return Message
* @author jc0803kevin
*/
public static Message deserialize(List<Byte> serializedMessageList) {
// Remove the byte as it is used to indicate legacy Transaction.
// GuardedArrayUtils.guardedShift(serializedMessageList);

// Remove three bytes for header
byte[] messageHeaderBytes = ArrayUtils.guardedSplice(serializedMessageList, 0, MessageHeader.HEADER_LENGTH);
MessageHeader messageHeader = new MessageHeader(messageHeaderBytes);

// Total static account keys
int accountKeysSize = ShortvecEncoding.decodeLength(serializedMessageList);
List<AccountMeta> accountKeys = new ArrayList<>(accountKeysSize);
for (int i = 0; i < accountKeysSize; i++) {
byte[] accountMetaPublicKeyByteArray = ArrayUtils.guardedSplice(serializedMessageList, 0,
PublicKey.PUBLIC_KEY_LENGTH);
PublicKey publicKey = new PublicKey(accountMetaPublicKeyByteArray);
accountKeys.add(new AccountMeta(publicKey, false, false));
}

// setSigner VS setWritable
for (AccountMeta accountKey : accountKeys) {
PublicKey publicKey = accountKey.getPublicKey();
boolean isSigner = isSigner(accountKeys, publicKey, messageHeader);
boolean isWriter = isWriter(accountKeys, publicKey, messageHeader);
accountKey.setSigner(isSigner);
accountKey.setWritable(isWriter);
}

AccountKeysList accountKeysList = new AccountKeysList();
accountKeysList.addAll(accountKeys);

// recent_blockhash
String recentBlockHash = Base58.encode(ArrayUtils.guardedSplice(serializedMessageList, 0,
PublicKey.PUBLIC_KEY_LENGTH));

// Deserialize instructions
int instructionsLength = ShortvecEncoding.decodeLength(serializedMessageList);
List<TransactionInstruction> instructions = new ArrayList<>();
List<CompiledInstruction> compiledInstructions = new ArrayList<>(instructionsLength);
for (int i = 0; i < instructionsLength; i++) {
CompiledInstruction compiledInstruction = new CompiledInstruction();
compiledInstruction.programIdIndex = ArrayUtils.guardedShift(serializedMessageList);
int keysSize = ShortvecEncoding.decodeLength(serializedMessageList); // keysSize
compiledInstruction.keyIndicesCount = ShortvecEncoding.encodeLength(keysSize);
compiledInstruction.keyIndices = ArrayUtils.guardedSplice(serializedMessageList, 0, keysSize);
var dataLength = ShortvecEncoding.decodeLength(serializedMessageList);
compiledInstruction.dataLength = ShortvecEncoding.encodeLength(dataLength);
compiledInstruction.data = ArrayUtils.guardedSplice(serializedMessageList, 0, dataLength);

compiledInstructions.add(compiledInstruction);

PublicKey programId = accountKeys.get(compiledInstruction.programIdIndex).getPublicKey();
List<AccountMeta> keys = new ArrayList<>();
for (int i1 = 0; i1 < compiledInstruction.keyIndices.length; i1++) {
keys.add(accountKeys.get(compiledInstruction.keyIndices[i1]));
}
instructions.add(new TransactionInstruction(programId, keys, compiledInstruction.data));
}

return new Message(messageHeader, recentBlockHash, accountKeysList, instructions);
}

private static boolean isWriter(List<AccountMeta> accountKeys, PublicKey account, MessageHeader messageHeader){

int index = indexOf(accountKeys, account);
if(index == -1){
return false;
}
boolean isSignerWriter= index < messageHeader.numRequiredSignatures - messageHeader.numReadonlySignedAccounts;
boolean isNonSigner = index >= messageHeader.numRequiredSignatures;
boolean isNonSignerReadonly = index >= (accountKeys.size() - messageHeader.numReadonlyUnsignedAccounts);
boolean isNonSignerWriter = isNonSigner && !isNonSignerReadonly;
return isSignerWriter || isNonSignerWriter;
}

private static boolean isSigner(List<AccountMeta> accountKeys, PublicKey account, MessageHeader messageHeader) {
int index = indexOf(accountKeys, account);

if (index == -1) {
return false;
}

return index < messageHeader.numRequiredSignatures;
}

private static int indexOf(List<AccountMeta> accountKeys, PublicKey account){
for (int i = 0; i < accountKeys.size(); i++) {
if(account.toBase58().equals(accountKeys.get(i).getPublicKey().toBase58())){
return i;
}
}

return -1;
}

}
42 changes: 39 additions & 3 deletions src/main/java/org/p2p/solanaj/core/Transaction.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@

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.ByteUtils;
import org.p2p.solanaj.utils.ArrayUtils;
import org.p2p.solanaj.utils.ShortvecEncoding;
import org.p2p.solanaj.utils.TweetNaclFast;

Expand All @@ -30,6 +31,12 @@ public Transaction() {
this.signatures = new ArrayList<>(); // Use diamond operator
}

public Transaction(Message message, List<String> signatures) {
this.message = message;
this.signatures = signatures;
this.serializedMessage = message.serialize();
}

/**
* Adds an instruction to the transaction.
*
Expand Down Expand Up @@ -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
}

/**
Expand All @@ -76,7 +83,7 @@ public void sign(List<Account> signers) {
}

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

serializedMessage = message.serialize();

Expand All @@ -91,6 +98,13 @@ public void sign(List<Account> signers) {
}
}

public String getTxHash(){
if (signatures == null || signatures.isEmpty()){
return null;
}
return signatures.get(0);
}

/**
* Serializes the transaction into a byte array.
*
Expand All @@ -115,4 +129,26 @@ public byte[] serialize() {

return out.array();
}

/**
* deserialize Transaction
* @param serializedTransaction transaction serialize byte array
* @return
* @author jc080kevin
*/
public static Transaction deserialize(byte[] serializedTransaction) {
List<Byte> serializedTransactionList = ByteUtils.toByteList(serializedTransaction);

int signaturesSize = ShortvecEncoding.decodeLength(serializedTransactionList);
List<String> signatures = new ArrayList<>(signaturesSize);

for (int i = 0; i < signaturesSize; i++) {

byte[] signatureBytes = ArrayUtils.guardedSplice(serializedTransactionList, 0, SIGNATURE_LENGTH);
signatures.add(Base58.encode(signatureBytes));
}

Message message = Message.deserialize(serializedTransactionList);
return new Transaction(message, signatures);
}
}
Loading
Loading