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

JUP-setup jupiter program to swap solana tokens and added functionali… #41

Open
wants to merge 20 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
2d4f179
JUP-setup jupiter program to swap solana tokens and added functionali…
Aug 13, 2024
fbf186b
Adding a capability to call getSignaturesForAddress rpc call with bef…
Aug 16, 2024
dfaa37e
Added an ability to read unsigned 128 bits value
Aug 17, 2024
94b9f3f
Added some properties to parse the data received from raydium lp market
Aug 18, 2024
28c5922
Added logsSubscribeWithId and logsUnSubscribe rpc method calls
Sep 11, 2024
df7f9b7
Updated onClose Subscription websocket so it can reconnect when conne…
Sep 14, 2024
39e6d2e
Refactored Jupiter program swap token local field name to avoid confu…
Nov 12, 2024
3f79b4d
Fixed merged conflict and changed naming for legacy transaction and l…
Nov 12, 2024
6daac37
Fixed new merged conflict created by me force pushing commit that rem…
Nov 12, 2024
c388174
Fixed conflict that was not removed from comments and the compiler di…
Nov 12, 2024
6f77a4c
Updated getSignaturesForAddress Rpc call to include before parameter
Nov 13, 2024
646ec91
Added digital asset standard api getTokenAccounts to fetch all holder…
Nov 30, 2024
9697334
Added digital asset standard api searchAssets method to search assets…
i-contemplator Dec 1, 2024
301a150
Added cursor for searchAssets rpc das call
i-contemplator Dec 3, 2024
c377e2d
Made Holder Asset class public
i-contemplator Dec 3, 2024
0f4f828
Changed enum tokenType to string to be correctly parsed by rpc reques…
i-contemplator Dec 3, 2024
a6531a9
Modified the search asset response dto
i-contemplator Dec 3, 2024
1668103
Modified inner classes of search asset request and response to be ac…
i-contemplator Dec 3, 2024
bff7f38
Updated searchassetsresponse dto type
i-contemplator Dec 16, 2024
55d59a5
Merge branch 'main' into JUP-swap-functionality
i-contemplator Jan 8, 2025
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
14 changes: 7 additions & 7 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
13 changes: 13 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,19 @@
<version>5.14.2</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
<version>5.2.1</version>
</dependency>

<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.15.0</version>
</dependency>

<dependency>
<groupId>com.syntifi.near</groupId>
<artifactId>borshj</artifactId>
Expand Down
7 changes: 7 additions & 0 deletions src/main/java/org/p2p/solanaj/core/AccountKeysList.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@ public ArrayList<AccountMeta> getList() {
return accountKeysList;
}

@Override
public String toString() {
return "AccountKeysList{" +
"accounts=" + accounts +
'}';
}

private static final Comparator<AccountMeta> metaComparator = Comparator
.comparing(AccountMeta::isSigner)
.thenComparing(AccountMeta::isWritable).reversed();
Expand Down
21 changes: 20 additions & 1 deletion src/main/java/org/p2p/solanaj/core/AccountMeta.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,23 @@ 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;
}

@Override
public String toString() {
return "AccountMeta{" +
"publicKey=" + publicKey +
", isSigner=" + isSigner +
", isWritable=" + isWritable +
'}';
}
}
181 changes: 181 additions & 0 deletions src/main/java/org/p2p/solanaj/core/LegacyMessage.java
Original file line number Diff line number Diff line change
@@ -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<TransactionInstruction> 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<AccountMeta> keysList = getAccountKeys();
int accountKeysSize = keysList.size();

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

int compiledInstructionsLength = 0;
List<CompiledInstruction> 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<AccountMeta> getAccountKeys() {
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.getPublicKey());
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) {
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");
}
}
114 changes: 114 additions & 0 deletions src/main/java/org/p2p/solanaj/core/LegacyTransaction.java
Original file line number Diff line number Diff line change
@@ -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<String> 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<Account> 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();
}
}
Loading