48: Add SOL transfer support to SolanaWallet

This commit is contained in:
2026-08-05 10:12:39 +02:00
parent 12843e1501
commit 280024c340
9 changed files with 801 additions and 4 deletions
+6
View File
@@ -51,6 +51,8 @@ dependencies {
implementation("org.apache.commons:commons-collections4:4.5.0")
implementation("org.apache.commons:commons-lang3:3.20.0")
implementation("org.slf4j:slf4j-api:2.0.18")
testImplementation("org.junit.jupiter:junit-jupiter:5.13.4")
}
java {
@@ -69,6 +71,10 @@ tasks.withType<JavaCompile>().configureEach {
)
}
tasks.withType<Test>().configureEach {
useJUnitPlatform()
}
val generatedDetagMain = layout.buildDirectory.dir("generated/sources/detag/main/java")
val cleanGeneratedDetagMain = tasks.register<Delete>("cleanGeneratedDetagMain") {
@@ -52,6 +52,44 @@ public interface SolanaWallet {
@NotNull SolanaSPLTokenProgram splProgram
) throws IOException, InterruptedException;
/**
* Builds an unsigned transaction that transfers a specific amount of SOL.
*
* <p>The current wallet is used as fee payer and required signer. This
* method does not sign or submit the transaction.</p>
*
* @param recipient the wallet that should receive the SOL
* @param amount the amount of SOL to transfer
* @return the unsigned serialized transfer transaction
* @throws IllegalArgumentException if the recipient or amount is invalid
* @throws IOException if a recent blockhash cannot be fetched
* @throws InterruptedException if the calling thread is interrupted
*/
@NotNull
SolanaUnsignedTransaction buildSolanaTransferTransaction(
@NotNull ΩSolanaWalletIdΩ recipient,
@NotNull ΩSolanaAmountΩ amount
) throws IOException, InterruptedException;
/**
* Builds an unsigned transaction that transfers the wallet's complete
* native SOL balance after paying the exact transaction fee.
*
* <p>The current wallet is used as fee payer and required signer. This
* method does not sign or submit the transaction.</p>
*
* @param recipient the wallet that should receive the SOL
* @return the unsigned serialized transfer transaction
* @throws IllegalArgumentException if the recipient is invalid
* @throws IllegalStateException if the balance cannot cover a transfer
* @throws IOException if the balance, blockhash or fee cannot be fetched
* @throws InterruptedException if the calling thread is interrupted
*/
@NotNull
SolanaUnsignedTransaction buildSolanaTransferAllTransaction(
@NotNull ΩSolanaWalletIdΩ recipient
) throws IOException, InterruptedException;
/**
* Signs an unsigned Solana transaction.
*
@@ -70,4 +108,4 @@ public interface SolanaWallet {
SolanaSignedTransaction signTransaction(
@NotNull SolanaUnsignedTransaction transaction
) throws IOException, InterruptedException;
}
}
@@ -0,0 +1,235 @@
package com.r35157.cryptowallet.solana.impl.ref;
import com.r35157.libs.solana.SolanaLatestBlockhash;
import java.io.ByteArrayOutputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Arrays;
import java.util.Base64;
final class SolanaTransferTransactionBuilder {
private SolanaTransferTransactionBuilder() {
}
static SerializedTransfer build(
ΩSolanaWalletIdΩ sender,
ΩSolanaWalletIdΩ recipient,
SolanaLatestBlockhash latestBlockhash,
ΩlamportsΩ lamports
) {
if (lamports <= 0) {
throw new IllegalArgumentException(
"SOL transfer amount must be greater than zero lamports"
);
}
byte[] senderBytes = decodeAddress("sender", sender);
byte[] recipientBytes = decodeAddress("recipient", recipient);
byte[] systemProgramBytes = decodeAddress(
"System Program",
SYSTEM_PROGRAM_ADDRESS
);
byte[] blockhashBytes = decodeAddress(
"blockhash",
latestBlockhash.blockhash()
);
ByteArrayOutputStream message = new ByteArrayOutputStream();
message.write(1);
message.write(0);
message.write(1);
writeCompactU16(message, 3);
message.writeBytes(senderBytes);
message.writeBytes(recipientBytes);
message.writeBytes(systemProgramBytes);
message.writeBytes(blockhashBytes);
writeCompactU16(message, 1);
message.write(2);
writeCompactU16(message, 2);
message.write(0);
message.write(1);
byte[] instructionData = ByteBuffer
.allocate(12)
.order(ByteOrder.LITTLE_ENDIAN)
.putInt(SYSTEM_TRANSFER_INSTRUCTION)
.putLong(lamports)
.array();
writeCompactU16(message, instructionData.length);
message.writeBytes(instructionData);
byte[] messageBytes = message.toByteArray();
ByteArrayOutputStream transaction = new ByteArrayOutputStream();
writeCompactU16(transaction, 1);
transaction.writeBytes(new byte[SOLANA_SIGNATURE_LENGTH]);
transaction.writeBytes(messageBytes);
ΩBase64StringΩ serializedMessage =
Base64.getEncoder().encodeToString(messageBytes);
ΩBase64StringΩ serializedTransaction =
Base64.getEncoder().encodeToString(
transaction.toByteArray()
);
return new SerializedTransfer(
serializedMessage,
serializedTransaction
);
}
private static byte[] decodeAddress(
String description,
String address
) {
if (address == null || address.isBlank()) {
throw new IllegalArgumentException(
"Solana " + description + " must not be blank"
);
}
byte[] decoded = base58Decode(address);
if (decoded.length != SOLANA_ADDRESS_LENGTH) {
throw new IllegalArgumentException(
"Solana "
+ description
+ " must decode to "
+ SOLANA_ADDRESS_LENGTH
+ " bytes, but was "
+ decoded.length
);
}
return decoded;
}
private static void writeCompactU16(
ByteArrayOutputStream output,
int value
) {
if (value < 0 || value > 0xffff) {
throw new IllegalArgumentException(
"Compact-u16 value is outside the supported range: "
+ value
);
}
int remaining = value;
do {
int nextByte = remaining & 0x7f;
remaining >>>= 7;
if (remaining != 0) {
nextByte |= 0x80;
}
output.write(nextByte);
} while (remaining != 0);
}
private static byte[] base58Decode(String input) {
byte[] input58 = new byte[input.length()];
for (int i = 0; i < input.length(); i++) {
char character = input.charAt(i);
if (character >= BASE58_INDEXES.length
|| BASE58_INDEXES[character] < 0) {
throw new IllegalArgumentException(
"Invalid Base58 character: " + character
);
}
input58[i] = (byte) BASE58_INDEXES[character];
}
int zeros = 0;
while (zeros < input58.length && input58[zeros] == 0) {
zeros++;
}
byte[] decoded = new byte[input.length()];
int outputStart = decoded.length;
int inputStart = zeros;
while (inputStart < input58.length) {
int remainder = divmod(
input58,
inputStart,
58,
256
);
if (input58[inputStart] == 0) {
inputStart++;
}
decoded[--outputStart] = (byte) remainder;
}
while (outputStart < decoded.length
&& decoded[outputStart] == 0) {
outputStart++;
}
return Arrays.copyOfRange(
decoded,
outputStart - zeros,
decoded.length
);
}
private static int divmod(
byte[] number,
int firstDigit,
int base,
int divisor
) {
int remainder = 0;
for (int i = firstDigit; i < number.length; i++) {
int digit = number[i] & 0xff;
int temporary = remainder * base + digit;
number[i] = (byte) (temporary / divisor);
remainder = temporary % divisor;
}
return remainder;
}
private static int[] createBase58Indexes() {
int[] indexes = new int[128];
Arrays.fill(indexes, -1);
for (int i = 0; i < BASE58_ALPHABET.length; i++) {
indexes[BASE58_ALPHABET[i]] = i;
}
return indexes;
}
record SerializedTransfer(
ΩBase64StringΩ serializedMessage,
ΩBase64StringΩ serializedTransaction
) {
}
private static final String SYSTEM_PROGRAM_ADDRESS =
"11111111111111111111111111111111";
private static final int SYSTEM_TRANSFER_INSTRUCTION = 2;
private static final int SOLANA_ADDRESS_LENGTH = 32;
private static final int SOLANA_SIGNATURE_LENGTH = 64;
private static final char[] BASE58_ALPHABET =
"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
.toCharArray();
private static final int[] BASE58_INDEXES = createBase58Indexes();
}
@@ -5,10 +5,13 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.r35157.cryptowallet.solana.SolanaWallet;
import com.r35157.libs.solana.SPLTokenHolding;
import com.r35157.libs.solana.SolanaBlockChain;
import com.r35157.libs.solana.SolanaLatestBlockhash;
import com.r35157.libs.solana.SolanaSignedTransaction;
import com.r35157.libs.solana.SolanaUnsignedTransaction;
import com.r35157.libs.solana.valuetypes.economic.SolanaSPLTokenProgram;
import com.r35157.libs.valuetypes.basic.CurrencyType;
import com.r35157.libs.valuetypes.basic.MoneyAmount;
import com.r35157.libs.valuetypes.basic.WellKnownCurrencyTypes;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -52,6 +55,81 @@ public class SolanaWalletImpl implements SolanaWallet {
: tokenHolding.uiAmount();
}
@Override
public @NotNull SolanaUnsignedTransaction buildSolanaTransferTransaction(
@NotNull ΩSolanaWalletIdΩ recipient,
@NotNull ΩSolanaAmountΩ amount
) throws IOException, InterruptedException {
validateRecipient(recipient);
ΩlamportsΩ lamports = toLamports(amount);
SolanaLatestBlockhash latestBlockhash =
solanaBlockChain.getLatestBlockhash();
return buildSolanaTransferTransaction(
recipient,
latestBlockhash,
lamports
);
}
@Override
public @NotNull SolanaUnsignedTransaction buildSolanaTransferAllTransaction(
@NotNull ΩSolanaWalletIdΩ recipient
) throws IOException, InterruptedException {
validateRecipient(recipient);
ΩlamportsΩ balance =
solanaBlockChain.getBalanceInLamport(address);
if (balance <= 0) {
throw new IllegalStateException(
"Wallet does not contain any SOL to transfer"
);
}
SolanaLatestBlockhash latestBlockhash =
solanaBlockChain.getLatestBlockhash();
// The transfer amount is always serialized as one fixed-width u64.
// Replacing it with balance - fee therefore cannot change the fee.
SolanaTransferTransactionBuilder.SerializedTransfer feeCandidate =
SolanaTransferTransactionBuilder.build(
address,
recipient,
latestBlockhash,
balance
);
ΩlamportsΩ fee = solanaBlockChain.getFeeForMessage(
feeCandidate.serializedMessage()
);
if (fee < 0) {
throw new IllegalStateException(
"Solana transaction fee must not be negative"
);
}
ΩlamportsΩ transferableLamports = balance - fee;
if (transferableLamports <= 0) {
throw new IllegalStateException(
"Wallet balance of "
+ balance
+ " lamports cannot cover the transaction fee of "
+ fee
+ " lamports and a positive transfer amount"
);
}
return buildSolanaTransferTransaction(
recipient,
latestBlockhash,
transferableLamports
);
}
@Override
public @NotNull SolanaSignedTransaction signTransaction(
@NotNull SolanaUnsignedTransaction transaction
@@ -137,6 +215,75 @@ public class SolanaWalletImpl implements SolanaWallet {
return sst;
}
private SolanaUnsignedTransaction buildSolanaTransferTransaction(
ΩSolanaWalletIdΩ recipient,
SolanaLatestBlockhash latestBlockhash,
ΩlamportsΩ lamports
) {
SolanaTransferTransactionBuilder.SerializedTransfer transfer =
SolanaTransferTransactionBuilder.build(
address,
recipient,
latestBlockhash,
lamports
);
return new SolanaUnsignedTransaction(
transfer.serializedTransaction(),
latestBlockhash.blockhash(),
latestBlockhash.lastValidBlockHeight()
);
}
private ΩlamportsΩ toLamports(ΩSolanaAmountΩ amount) {
if (amount == null || amount.amount() == null) {
throw new IllegalArgumentException(
"SOL transfer amount must not be null"
);
}
if (!SOLANA_CURRENCY.equals(amount.currencyType())) {
throw new IllegalArgumentException(
"SOL transfer amount must use the SOL currency type"
);
}
BigDecimal lamports =
amount.amount().multiply(LAMPORTS_PER_SOL);
try {
ΩlamportsΩ result = lamports.longValueExact();
if (result <= 0) {
throw new IllegalArgumentException(
"SOL transfer amount must be greater than zero"
);
}
return result;
} catch (ArithmeticException e) {
throw new IllegalArgumentException(
"SOL transfer amount must be expressible as a whole "
+ "number of lamports",
e
);
}
}
private void validateRecipient(ΩSolanaWalletIdΩ recipient) {
if (recipient == null || recipient.isBlank()) {
throw new IllegalArgumentException(
"SOL transfer recipient must not be blank"
);
}
if (recipient.equals(address)) {
throw new IllegalArgumentException(
"SOL transfer recipient must be different from the sender"
);
}
}
private record SignerResponse(
String signer,
ΩBase64StringΩ signedTransaction
@@ -144,8 +291,12 @@ public class SolanaWalletImpl implements SolanaWallet {
}
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private static final BigDecimal LAMPORTS_PER_SOL =
new BigDecimal("1000000000");
private static final CurrencyType SOLANA_CURRENCY =
WellKnownCurrencyTypes.SOLANA.getCurrencyType();
private final ΩSolanaWalletIdΩ address;
private final String signerKeyName;
private final SolanaBlockChain solanaBlockChain;
}
}
@@ -43,6 +43,29 @@ public interface SolanaBlockChain {
*/
ΩlamportsΩ getBalanceInLamport(ΩSolanaAddressΩ address) throws IOException, InterruptedException;
/**
* Fetches a recent blockhash for building a new Solana transaction.
*
* @return the recent blockhash and its last valid block height
* @throws IOException if the blockhash could not be fetched or parsed
* @throws InterruptedException if the calling thread is interrupted
*/
SolanaLatestBlockhash getLatestBlockhash()
throws IOException, InterruptedException;
/**
* Fetches the transaction fee for a serialized Solana message.
*
* @param serializedMessage the Base64 encoded legacy or versioned message
* @return the fee in lamports
* @throws IllegalArgumentException if the serialized message is blank
* @throws IOException if the fee could not be fetched or the blockhash has expired
* @throws InterruptedException if the calling thread is interrupted
*/
ΩlamportsΩ getFeeForMessage(
ΩBase64StringΩ serializedMessage
) throws IOException, InterruptedException;
/**
* Fetches SPL token holdings owned by a Solana address for a specific token program.
*
@@ -183,4 +206,4 @@ public interface SolanaBlockChain {
ΩSolanaTransactionSignatureΩ sendTransaction(
SolanaSignedTransaction transaction
) throws IOException, InterruptedException;
}
}
@@ -0,0 +1,13 @@
package com.r35157.libs.solana;
/**
* A recent Solana blockhash and the last block height at which it is valid.
*
* @param blockhash the recent blockhash
* @param lastValidBlockHeight the last block height at which the blockhash is valid
*/
public record SolanaLatestBlockhash(
ΩSolanaBlockhashΩ blockhash,
long lastValidBlockHeight
) {
}
@@ -6,6 +6,7 @@ import com.r35157.libs.solana.SPLTokenHolding;
import com.r35157.libs.solana.SPLTokenSupply;
import com.r35157.libs.solana.SolanaAccountInfo;
import com.r35157.libs.solana.SolanaBlockChain;
import com.r35157.libs.solana.SolanaLatestBlockhash;
import com.r35157.libs.solana.SolanaProgramAccountMemcmpFilter;
import com.r35157.libs.solana.SolanaProgramAddressSeed;
import com.r35157.libs.solana.SolanaSignedTransaction;
@@ -44,6 +45,19 @@ public final class CachedSolanaBlockChain implements SolanaBlockChain {
return delegate.getBalanceInLamport(address);
}
@Override
public SolanaLatestBlockhash getLatestBlockhash()
throws IOException, InterruptedException {
return delegate.getLatestBlockhash();
}
@Override
public ΩlamportsΩ getFeeForMessage(
ΩBase64StringΩ serializedMessage
) throws IOException, InterruptedException {
return delegate.getFeeForMessage(serializedMessage);
}
@Override
public Map<ΩSPLMintAddressΩ, SPLTokenHolding> getSPLTokenHoldings(
ΩSolanaAddressΩ ownerAddress,
@@ -238,4 +252,4 @@ public final class CachedSolanaBlockChain implements SolanaBlockChain {
private final SolanaBlockChain delegate;
private final ObjectCache objectCache;
}
}
@@ -88,6 +88,133 @@ public class SolanaBlockChainImpl implements SolanaBlockChain {
return valueNode.longValue();
}
@Override
public SolanaLatestBlockhash getLatestBlockhash()
throws IOException, InterruptedException {
String jsonBody = """
{
"jsonrpc": "2.0",
"id": 1,
"method": "getLatestBlockhash",
"params": [
{
"commitment": "confirmed"
}
]
}
""";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(RPC_URL))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(jsonBody))
.build();
HttpResponse<String> response = sendThrottled(request);
if (response.statusCode() != 200) {
throw new IOException(
"Solana getLatestBlockhash RPC call failed: HTTP "
+ response.statusCode()
+ "\n"
+ response.body()
);
}
JsonNode root = objectMapper.readTree(response.body());
JsonNode errorNode = root.get("error");
if (errorNode != null && !errorNode.isNull()) {
throw new IOException(
"Solana getLatestBlockhash RPC error: "
+ errorNode.toPrettyString()
);
}
JsonNode valueNode = root.path("result").path("value");
JsonNode blockhashNode = valueNode.path("blockhash");
JsonNode lastValidBlockHeightNode =
valueNode.path("lastValidBlockHeight");
if (!blockhashNode.isTextual()
|| blockhashNode.asText().isBlank()
|| !lastValidBlockHeightNode.isIntegralNumber()) {
throw new IOException(
"Solana getLatestBlockhash response did not contain "
+ "a blockhash and last valid block height: "
+ response.body()
);
}
ΩSolanaBlockhashΩ blockhash = blockhashNode.asText();
return new SolanaLatestBlockhash(
blockhash,
lastValidBlockHeightNode.longValue()
);
}
@Override
public ΩlamportsΩ getFeeForMessage(
ΩBase64StringΩ serializedMessage
) throws IOException, InterruptedException {
if (serializedMessage == null || serializedMessage.isBlank()) {
throw new IllegalArgumentException(
"Serialized Solana message must not be blank"
);
}
ObjectNode rpcRequest = objectMapper.createObjectNode();
rpcRequest.put("jsonrpc", "2.0");
rpcRequest.put("id", 1);
rpcRequest.put("method", "getFeeForMessage");
ArrayNode params = rpcRequest.putArray("params");
params.add(serializedMessage);
params.addObject().put("commitment", "confirmed");
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(RPC_URL))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(
objectMapper.writeValueAsString(rpcRequest)
))
.build();
HttpResponse<String> response = sendThrottled(request);
if (response.statusCode() != 200) {
throw new IOException(
"Solana getFeeForMessage RPC call failed: HTTP "
+ response.statusCode()
+ "\n"
+ response.body()
);
}
JsonNode root = objectMapper.readTree(response.body());
JsonNode errorNode = root.get("error");
if (errorNode != null && !errorNode.isNull()) {
throw new IOException(
"Solana getFeeForMessage RPC error: "
+ errorNode.toPrettyString()
);
}
JsonNode valueNode = root.path("result").path("value");
if (!valueNode.isIntegralNumber()) {
throw new IOException(
"Solana getFeeForMessage response did not contain a fee; "
+ "the transaction blockhash may have expired: "
+ response.body()
);
}
return valueNode.longValue();
}
@Override
public Map<ΩSPLMintAddressΩ, SPLTokenHolding> getSPLTokenHoldings(
ΩSolanaAddressΩ ownerAddress,
@@ -0,0 +1,190 @@
package com.r35157.cryptowallet.solana.impl.ref;
import com.r35157.libs.solana.SolanaBlockChain;
import com.r35157.libs.solana.SolanaLatestBlockhash;
import com.r35157.libs.solana.SolanaUnsignedTransaction;
import com.r35157.libs.valuetypes.basic.MoneyAmount;
import org.junit.jupiter.api.Test;
import java.lang.reflect.Proxy;
import java.math.BigDecimal;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Arrays;
import java.util.Base64;
import java.util.concurrent.atomic.AtomicReference;
import static com.r35157.libs.valuetypes.basic.WellKnownCurrencyTypes.SOLANA;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
class SolanaWalletImplTest {
@Test
void buildsExpectedLegacySolTransferTransaction() throws Exception {
SolanaWalletImpl wallet = new SolanaWalletImpl(
SENDER,
"unused-test-key",
createBlockChain(0, 0, new AtomicReference<>())
);
SolanaUnsignedTransaction transaction =
wallet.buildSolanaTransferTransaction(
RECIPIENT,
new MoneyAmount(
new BigDecimal("0.123456789"),
SOLANA.getCurrencyType()
)
);
assertEquals(EXPECTED_TRANSACTION, transaction.serializedTransaction());
assertEquals(BLOCKHASH, transaction.blockhash());
assertEquals(
LAST_VALID_BLOCK_HEIGHT,
transaction.lastValidBlockHeight()
);
}
@Test
void transferAllSubtractsExactFeeFromBalance() throws Exception {
AtomicReference<String> feeMessage = new AtomicReference<>();
SolanaWalletImpl wallet = new SolanaWalletImpl(
SENDER,
"unused-test-key",
createBlockChain(10_000, 5_000, feeMessage)
);
SolanaUnsignedTransaction transaction =
wallet.buildSolanaTransferAllTransaction(RECIPIENT);
byte[] transactionBytes = Base64.getDecoder().decode(
transaction.serializedTransaction()
);
long transferredLamports = ByteBuffer
.wrap(
transactionBytes,
transactionBytes.length - Long.BYTES,
Long.BYTES
)
.order(ByteOrder.LITTLE_ENDIAN)
.getLong();
assertEquals(5_000, transferredLamports);
byte[] feeMessageBytes = Base64.getDecoder().decode(
feeMessage.get()
);
byte[] finalMessageBytes = Arrays.copyOfRange(
transactionBytes,
1 + 64,
transactionBytes.length
);
assertArrayEquals(
Arrays.copyOf(
feeMessageBytes,
feeMessageBytes.length - Long.BYTES
),
Arrays.copyOf(
finalMessageBytes,
finalMessageBytes.length - Long.BYTES
)
);
assertEquals(
10_000,
readLastLongLittleEndian(feeMessageBytes)
);
}
@Test
void transferAllRejectsBalanceThatOnlyCoversFee() {
SolanaWalletImpl wallet = new SolanaWalletImpl(
SENDER,
"unused-test-key",
createBlockChain(5_000, 5_000, new AtomicReference<>())
);
IllegalStateException exception = assertThrows(
IllegalStateException.class,
() -> wallet.buildSolanaTransferAllTransaction(RECIPIENT)
);
assertEquals(
"Wallet balance of 5000 lamports cannot cover the "
+ "transaction fee of 5000 lamports and a positive "
+ "transfer amount",
exception.getMessage()
);
}
@Test
void rejectsAmountsSmallerThanOneLamport() {
SolanaWalletImpl wallet = new SolanaWalletImpl(
SENDER,
"unused-test-key",
createBlockChain(0, 0, new AtomicReference<>())
);
assertThrows(
IllegalArgumentException.class,
() -> wallet.buildSolanaTransferTransaction(
RECIPIENT,
new MoneyAmount(
new BigDecimal("0.0000000001"),
SOLANA.getCurrencyType()
)
)
);
}
private static SolanaBlockChain createBlockChain(
long balance,
long fee,
AtomicReference<String> feeMessage
) {
return (SolanaBlockChain) Proxy.newProxyInstance(
SolanaBlockChain.class.getClassLoader(),
new Class<?>[] { SolanaBlockChain.class },
(proxy, method, arguments) -> switch (method.getName()) {
case "getBalanceInLamport" -> balance;
case "getLatestBlockhash" -> new SolanaLatestBlockhash(
BLOCKHASH,
LAST_VALID_BLOCK_HEIGHT
);
case "getFeeForMessage" -> {
feeMessage.set((String) arguments[0]);
yield fee;
}
default -> throw new AssertionError(
"Unexpected SolanaBlockChain call: "
+ method.getName()
);
}
);
}
private static long readLastLongLittleEndian(byte[] bytes) {
return ByteBuffer
.wrap(
bytes,
bytes.length - Long.BYTES,
Long.BYTES
)
.order(ByteOrder.LITTLE_ENDIAN)
.getLong();
}
private static final String SENDER =
"So11111111111111111111111111111111111111112";
private static final String RECIPIENT =
"SysvarRent111111111111111111111111111111111";
private static final String BLOCKHASH =
"11111111111111111111111111111111";
private static final long LAST_VALID_BLOCK_HEIGHT = 123_456;
private static final String EXPECTED_TRANSACTION =
"AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+ "AAAAAAAAAAAAAAAAAAAAAAABAAEDBpuIV/6rgYT7aH9jRhjANdrEOdwa6ztVmKDw"
+ "AAAAAAEGp9UXGSxcUSGMyUw9SvF/WNruCJuh/UTj29mKAAAAAAAAAAAAAAAAAAAA"
+ "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+ "AAAAAAABAgIAAQwCAAAAFc1bBwAAAAA=";
}