Skip to content

Commit

Permalink
JUP-setup jupiter program to swap solana tokens and added functionali…
Browse files Browse the repository at this point in the history
…ty to deserialize transactions
  • Loading branch information
Chintan Patel authored and Chintan Patel committed Aug 13, 2024
1 parent a5173cb commit f196779
Show file tree
Hide file tree
Showing 12 changed files with 480 additions and 89 deletions.
12 changes: 12 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,18 @@
<artifactId>jackson-databind</artifactId>
<version>2.17.2</version>
</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>
</dependencies>

<distributionManagement>
Expand Down
6 changes: 6 additions & 0 deletions src/main/java/org/p2p/solanaj/core/AccountKeysList.java
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,10 @@ public int compare(AccountMeta am1, AccountMeta am2) {
}
};

@Override
public String toString() {
return "AccountKeysList{" +
"accounts=" + accounts +
'}';
}
}
9 changes: 9 additions & 0 deletions src/main/java/org/p2p/solanaj/core/AccountMeta.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,13 @@ public class AccountMeta {
private boolean isSigner;

private boolean isWritable;

@Override
public String toString() {
return "AccountMeta{" +
"publicKey=" + publicKey +
", isSigner=" + isSigner +
", isWritable=" + isWritable +
'}';
}
}
249 changes: 192 additions & 57 deletions src/main/java/org/p2p/solanaj/core/Message.java

Large diffs are not rendered by default.

58 changes: 49 additions & 9 deletions src/main/java/org/p2p/solanaj/core/Transaction.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,31 @@
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import org.bitcoinj.core.Base58;
import org.p2p.solanaj.utils.ShortvecEncoding;
import org.p2p.solanaj.utils.ByteUtils;
import org.p2p.solanaj.utils.GuardedArrayUtils;
import org.p2p.solanaj.utils.Shortvec;
import org.p2p.solanaj.utils.TweetNaclFast;

public class Transaction {

public static final int SIGNATURE_LENGTH = 64;

private Message message;
private List<String> signatures;
private final Message message;
private final List<String> signatures;
private byte[] serializedMessage;

public Transaction() {
this.message = new Message();
this.signatures = new ArrayList<String>();
this.signatures = new ArrayList<>();
}

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

public Transaction addInstruction(TransactionInstruction instruction) {
Expand All @@ -33,31 +41,38 @@ public void setRecentBlockHash(String recentBlockhash) {
}

public void sign(Account signer) {
sign(Arrays.asList(signer));
sign(Collections.singletonList(signer));
}

public void sign(List<Account> signers) {

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

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

List<AccountMeta> signerPubKeys = List.copyOf(message.getAccountKeys());
signerPubKeys = signerPubKeys.subList(0, signers.size());

serializedMessage = message.serialize();

for (Account signer : signers) {
int signerIndex = message.findAccountIndex(signerPubKeys, signer.getPublicKey());
if (signerIndex < 0) {
throw new IllegalArgumentException("Cannot sign with non signer key: " +
signer.getPublicKey().toBase58());
}
TweetNaclFast.Signature signatureProvider = new TweetNaclFast.Signature(new byte[0], signer.getSecretKey());
byte[] signature = signatureProvider.detached(serializedMessage);

signatures.add(Base58.encode(signature));
this.signatures.set(signerIndex, Base58.encode(signature));
}
}

public byte[] serialize() {
int signaturesSize = signatures.size();
byte[] signaturesLength = ShortvecEncoding.encodeLength(signaturesSize);
byte[] signaturesLength = Shortvec.encodeLength(signaturesSize);

ByteBuffer out = ByteBuffer
.allocate(signaturesLength.length + signaturesSize * SIGNATURE_LENGTH + serializedMessage.length);
Expand All @@ -73,4 +88,29 @@ public byte[] serialize() {

return out.array();
}

public static Transaction deserialize(byte[] serializedTransaction) {
List<Byte> serializedTransactionList = ByteUtils.toByteList(serializedTransaction);

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

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

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

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

@Override
public String toString() {
return "Transaction{" +
"message=" + message +
", signatures=" + signatures +
", serializedMessage=" + Arrays.toString(serializedMessage) +
'}';
}
}
113 changes: 113 additions & 0 deletions src/main/java/org/p2p/solanaj/programs/JupiterSwapProgram.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package org.p2p.solanaj.programs;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.Getter;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.classic.methods.HttpPost;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.core5.http.HttpEntity;
import org.apache.hc.core5.http.io.HttpClientResponseHandler;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.http.io.entity.StringEntity;
import org.apache.hc.core5.net.URIBuilder;
import org.p2p.solanaj.core.Account;
import org.p2p.solanaj.core.Transaction;
import org.p2p.solanaj.rpc.RpcClient;
import org.p2p.solanaj.rpc.RpcException;

import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Base64;

public final class JupiterSwapProgram {

public static final String SOL_QUOTE_TOKEN = "So11111111111111111111111111111111111111112";

private static final CloseableHttpClient httpClient = HttpClients.createDefault();
private static final ObjectMapper objectMapper = new ObjectMapper();

private static final HttpClientResponseHandler<JsonNode> handler = response -> {
int status = response.getCode();
if (status >= 200 && status < 300) {
HttpEntity entity = response.getEntity();
return entity != null ? objectMapper.readTree(EntityUtils.toString(entity)) : null;
} else {
throw new IOException("Unexpected response status: " + status);
}
};

public static URI createQuoteUri(String inputToken, String outputToken, String amount, String slippage) {
try {
return new URIBuilder("https://quote-api.jup.ag/v6/quote")
.addParameter("inputMint", inputToken)
.addParameter("outputMint", outputToken)
.addParameter("amount", amount)
.addParameter("slippageBps", slippage)
.build();
} catch (URISyntaxException e) {
throw new RuntimeException("Failed to create quote URI: ", e);
}
}

public static JsonNode getJupiterQuote(URI quoteUri) {
HttpGet quoteRequest = new HttpGet(quoteUri);

JsonNode quote;
try {
quote = httpClient.execute(quoteRequest, handler);
} catch (IOException e) {
throw new RuntimeException("Failed to retrieve quote from jupiter for " + quoteUri, e);
}
return quote;
}

public static String swapToken(RpcClient rpcClient, Account account, JsonNode quote) {
SwapRequest swapRequestBody = new SwapRequest(quote, account.getPublicKey().toString());
String jsonSwapRequestBody = null;
try {
jsonSwapRequestBody = objectMapper.writeValueAsString(swapRequestBody);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}

HttpPost swapRequest = new HttpPost("https://quote-api.jup.ag/v6/swap");
swapRequest.setHeader("Content-Type", "application/json");
swapRequest.setEntity(new StringEntity(jsonSwapRequestBody));

JsonNode swapRes = null;
try {
swapRes = httpClient.execute(swapRequest, handler);
} catch (IOException e) {
throw new RuntimeException(e);
}

String swapTransaction = swapRes.get("swapTransaction").asText();

byte[] base64Decoded = Base64.getDecoder().decode(swapTransaction);
Transaction transaction = Transaction.deserialize(base64Decoded);

try {
return rpcClient.getApi().sendTransaction(transaction, account);
} catch (RpcException e) {
throw new RuntimeException("Failed to send swap transaction", e);
}
}

public static String swapToken(RpcClient rpcClient, Account account, String inputToken, String outputToken, String amount, String slippage) {
URI quoteUri = createQuoteUri(inputToken, outputToken, amount, slippage);
return swapToken(rpcClient, account, quoteUri);
}

public static String swapToken(RpcClient rpcClient, Account account, URI quoteUri) {
JsonNode quote = getJupiterQuote(quoteUri);
return swapToken(rpcClient, account, quote);
}

@Getter
public record SwapRequest(Object quoteResponse, String userPublicKey) {
}
}
1 change: 0 additions & 1 deletion src/main/java/org/p2p/solanaj/rpc/RpcClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,6 @@ public <T> T call(String method, List<Object> params, Class<T> clazz) throws Rpc
try {
Response response = httpClient.newCall(request).execute();
final String result = response.body().string();
// System.out.println("Response = " + result);
RpcResponse<T> rpcResult = resultAdapter.fromJson(result);

if (rpcResult.getError() != null) {
Expand Down
18 changes: 18 additions & 0 deletions src/main/java/org/p2p/solanaj/utils/ByteUtils.java
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
package org.p2p.solanaj.utils;

import com.google.common.primitives.Bytes;

import static org.bitcoinj.core.Utils.*;

import java.io.IOException;
import java.io.OutputStream;
import java.math.BigInteger;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

public class ByteUtils {
public static final int UINT_32_LENGTH = 4;
Expand Down Expand Up @@ -72,4 +77,17 @@ public static int getBit(byte[] data, int pos) {
return valInt;
}

public static byte[] toByteArray(List<Byte> byteList) {
return Bytes.toArray(byteList);
}

public static List<Byte> toByteList(byte[] bytes) {
return IntStream.range(0, bytes.length)
.mapToObj(i -> bytes[i])
.collect(Collectors.toList());
}

public static byte[] emptyByteArray() {
return new byte[]{};
}
}
43 changes: 43 additions & 0 deletions src/main/java/org/p2p/solanaj/utils/GuardedArrayUtils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package org.p2p.solanaj.utils;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

public class GuardedArrayUtils {

public static Byte guardedShift(List<Byte> byteArray) {
if (byteArray.isEmpty()) {
throw new IllegalArgumentException("Byte array length is 0");
}
return byteArray.remove(0);
}

public static byte[] guardedSplice(
List<Byte> byteList,
Integer start,
Integer deleteCount,
byte... items) {
List<Byte> removedItems;
if (deleteCount != null) {
if (start + deleteCount > byteList.size()) {
throw new Error("Reached end of bytes");
}
removedItems = new ArrayList<>(byteList.subList(start, start + deleteCount));
byteList.subList(start, start + deleteCount).clear();
} else {
if (start > byteList.size()) {
throw new Error("Reached end of bytes");
}
removedItems = Collections.emptyList();
}
List<Byte> itemsToAdd = ByteUtils.toByteList(items);
byteList.addAll(start, itemsToAdd);

if (!removedItems.isEmpty()) {
return ByteUtils.toByteArray(removedItems);
}
return ByteUtils.emptyByteArray();
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package org.p2p.solanaj.utils;

import java.util.List;

import static org.bitcoinj.core.Utils.*;

public class ShortvecEncoding {
public class Shortvec {

public static byte[] encodeLength(int len) {
byte[] out = new byte[10];
Expand All @@ -27,4 +29,18 @@ public static byte[] encodeLength(int len) {

return bytes;
}

public static int decodeLength(List<Byte> dataBytesList) {
int len = 0;
int size = 0;
for (;;) {
int elem = (int) dataBytesList.remove(0);
len |= (elem & 0x7f) << (size * 7);
size += 1;
if ((elem & 0x80) == 0) {
break;
}
}
return len;
}
}
Loading

0 comments on commit f196779

Please sign in to comment.