49: Hide transaction handling behind high-level service operations
This commit is contained in:
-235
@@ -1,235 +0,0 @@
|
|||||||
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,13 +5,10 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
|||||||
import com.r35157.cryptowallet.solana.SolanaWallet;
|
import com.r35157.cryptowallet.solana.SolanaWallet;
|
||||||
import com.r35157.libs.solana.SPLTokenHolding;
|
import com.r35157.libs.solana.SPLTokenHolding;
|
||||||
import com.r35157.libs.solana.SolanaBlockChain;
|
import com.r35157.libs.solana.SolanaBlockChain;
|
||||||
import com.r35157.libs.solana.SolanaLatestBlockhash;
|
|
||||||
import com.r35157.libs.solana.SolanaSignedTransaction;
|
import com.r35157.libs.solana.SolanaSignedTransaction;
|
||||||
import com.r35157.libs.solana.SolanaUnsignedTransaction;
|
import com.r35157.libs.solana.SolanaUnsignedTransaction;
|
||||||
import com.r35157.libs.solana.valuetypes.economic.SolanaSPLTokenProgram;
|
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.MoneyAmount;
|
||||||
import com.r35157.libs.valuetypes.basic.WellKnownCurrencyTypes;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
@@ -60,16 +57,10 @@ public class SolanaWalletImpl implements SolanaWallet {
|
|||||||
@NotNull ΩSolanaWalletIdΩ recipient,
|
@NotNull ΩSolanaWalletIdΩ recipient,
|
||||||
@NotNull ΩSolanaAmountΩ amount
|
@NotNull ΩSolanaAmountΩ amount
|
||||||
) throws IOException, InterruptedException {
|
) throws IOException, InterruptedException {
|
||||||
validateRecipient(recipient);
|
return solanaBlockChain.buildSolanaTransferTransaction(
|
||||||
|
address,
|
||||||
ΩlamportsΩ lamports = toLamports(amount);
|
|
||||||
SolanaLatestBlockhash latestBlockhash =
|
|
||||||
solanaBlockChain.getLatestBlockhash();
|
|
||||||
|
|
||||||
return buildSolanaTransferTransaction(
|
|
||||||
recipient,
|
recipient,
|
||||||
latestBlockhash,
|
amount
|
||||||
lamports
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,56 +68,9 @@ public class SolanaWalletImpl implements SolanaWallet {
|
|||||||
public @NotNull SolanaUnsignedTransaction buildSolanaTransferAllTransaction(
|
public @NotNull SolanaUnsignedTransaction buildSolanaTransferAllTransaction(
|
||||||
@NotNull ΩSolanaWalletIdΩ recipient
|
@NotNull ΩSolanaWalletIdΩ recipient
|
||||||
) throws IOException, InterruptedException {
|
) throws IOException, InterruptedException {
|
||||||
validateRecipient(recipient);
|
return solanaBlockChain.buildSolanaTransferAllTransaction(
|
||||||
|
address,
|
||||||
ΩlamportsΩ balance =
|
recipient
|
||||||
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
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,75 +159,6 @@ public class SolanaWalletImpl implements SolanaWallet {
|
|||||||
return sst;
|
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(
|
private record SignerResponse(
|
||||||
String signer,
|
String signer,
|
||||||
ΩBase64StringΩ signedTransaction
|
ΩBase64StringΩ signedTransaction
|
||||||
@@ -291,10 +166,6 @@ public class SolanaWalletImpl implements SolanaWallet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
|
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 ΩSolanaWalletIdΩ address;
|
||||||
private final String signerKeyName;
|
private final String signerKeyName;
|
||||||
|
|||||||
@@ -66,6 +66,40 @@ public interface SolanaBlockChain {
|
|||||||
ΩBase64StringΩ serializedMessage
|
ΩBase64StringΩ serializedMessage
|
||||||
) throws IOException, InterruptedException;
|
) throws IOException, InterruptedException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds an unsigned transaction that transfers a specific amount of SOL.
|
||||||
|
*
|
||||||
|
* @param sender the address that sends the SOL and pays the fee
|
||||||
|
* @param recipient the address that receives the SOL
|
||||||
|
* @param amount the amount of SOL to transfer
|
||||||
|
* @return the unsigned serialized transfer transaction
|
||||||
|
* @throws IllegalArgumentException if an address or amount is invalid
|
||||||
|
* @throws IOException if a recent blockhash cannot be fetched
|
||||||
|
* @throws InterruptedException if the calling thread is interrupted
|
||||||
|
*/
|
||||||
|
SolanaUnsignedTransaction buildSolanaTransferTransaction(
|
||||||
|
ΩSolanaAddressΩ sender,
|
||||||
|
ΩSolanaAddressΩ recipient,
|
||||||
|
ΩSolanaAmountΩ amount
|
||||||
|
) throws IOException, InterruptedException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds an unsigned transaction that transfers the sender's complete
|
||||||
|
* native SOL balance after paying the exact transaction fee.
|
||||||
|
*
|
||||||
|
* @param sender the address that sends the SOL and pays the fee
|
||||||
|
* @param recipient the address that receives the SOL
|
||||||
|
* @return the unsigned serialized transfer transaction
|
||||||
|
* @throws IllegalArgumentException if an address 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
|
||||||
|
*/
|
||||||
|
SolanaUnsignedTransaction buildSolanaTransferAllTransaction(
|
||||||
|
ΩSolanaAddressΩ sender,
|
||||||
|
ΩSolanaAddressΩ recipient
|
||||||
|
) throws IOException, InterruptedException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches SPL token holdings owned by a Solana address for a specific token program.
|
* Fetches SPL token holdings owned by a Solana address for a specific token program.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import com.r35157.libs.solana.SolanaLatestBlockhash;
|
|||||||
import com.r35157.libs.solana.SolanaProgramAccountMemcmpFilter;
|
import com.r35157.libs.solana.SolanaProgramAccountMemcmpFilter;
|
||||||
import com.r35157.libs.solana.SolanaProgramAddressSeed;
|
import com.r35157.libs.solana.SolanaProgramAddressSeed;
|
||||||
import com.r35157.libs.solana.SolanaSignedTransaction;
|
import com.r35157.libs.solana.SolanaSignedTransaction;
|
||||||
|
import com.r35157.libs.solana.SolanaUnsignedTransaction;
|
||||||
import com.r35157.libs.solana.valuetypes.SolanaProgramDerivedAddress;
|
import com.r35157.libs.solana.valuetypes.SolanaProgramDerivedAddress;
|
||||||
import com.r35157.libs.solana.valuetypes.economic.SolanaSPLTokenProgram;
|
import com.r35157.libs.solana.valuetypes.economic.SolanaSPLTokenProgram;
|
||||||
import com.r35157.libs.valuetypes.basic.MoneyAmount;
|
import com.r35157.libs.valuetypes.basic.MoneyAmount;
|
||||||
@@ -58,6 +59,30 @@ public final class CachedSolanaBlockChain implements SolanaBlockChain {
|
|||||||
return delegate.getFeeForMessage(serializedMessage);
|
return delegate.getFeeForMessage(serializedMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SolanaUnsignedTransaction buildSolanaTransferTransaction(
|
||||||
|
ΩSolanaAddressΩ sender,
|
||||||
|
ΩSolanaAddressΩ recipient,
|
||||||
|
ΩSolanaAmountΩ amount
|
||||||
|
) throws IOException, InterruptedException {
|
||||||
|
return delegate.buildSolanaTransferTransaction(
|
||||||
|
sender,
|
||||||
|
recipient,
|
||||||
|
amount
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SolanaUnsignedTransaction buildSolanaTransferAllTransaction(
|
||||||
|
ΩSolanaAddressΩ sender,
|
||||||
|
ΩSolanaAddressΩ recipient
|
||||||
|
) throws IOException, InterruptedException {
|
||||||
|
return delegate.buildSolanaTransferAllTransaction(
|
||||||
|
sender,
|
||||||
|
recipient
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Map<ΩSPLMintAddressΩ, SPLTokenHolding> getSPLTokenHoldings(
|
public Map<ΩSPLMintAddressΩ, SPLTokenHolding> getSPLTokenHoldings(
|
||||||
ΩSolanaAddressΩ ownerAddress,
|
ΩSolanaAddressΩ ownerAddress,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import com.r35157.libs.valuetypes.basic.CurrencyType;
|
|||||||
import com.r35157.libs.valuetypes.basic.MoneyAmount;
|
import com.r35157.libs.valuetypes.basic.MoneyAmount;
|
||||||
import com.r35157.libs.valuetypes.basic.WellKnownCurrencyTypes;
|
import com.r35157.libs.valuetypes.basic.WellKnownCurrencyTypes;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
@@ -18,6 +19,8 @@ import java.net.URI;
|
|||||||
import java.net.http.HttpClient;
|
import java.net.http.HttpClient;
|
||||||
import java.net.http.HttpRequest;
|
import java.net.http.HttpRequest;
|
||||||
import java.net.http.HttpResponse;
|
import java.net.http.HttpResponse;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.ByteOrder;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
@@ -215,6 +218,79 @@ public class SolanaBlockChainImpl implements SolanaBlockChain {
|
|||||||
return valueNode.longValue();
|
return valueNode.longValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SolanaUnsignedTransaction buildSolanaTransferTransaction(
|
||||||
|
ΩSolanaAddressΩ sender,
|
||||||
|
ΩSolanaAddressΩ recipient,
|
||||||
|
ΩSolanaAmountΩ amount
|
||||||
|
) throws IOException, InterruptedException {
|
||||||
|
validateSolanaTransferAddresses(sender, recipient);
|
||||||
|
|
||||||
|
return buildSolanaTransferTransaction(
|
||||||
|
sender,
|
||||||
|
recipient,
|
||||||
|
getLatestBlockhash(),
|
||||||
|
toLamports(amount)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SolanaUnsignedTransaction buildSolanaTransferAllTransaction(
|
||||||
|
ΩSolanaAddressΩ sender,
|
||||||
|
ΩSolanaAddressΩ recipient
|
||||||
|
) throws IOException, InterruptedException {
|
||||||
|
validateSolanaTransferAddresses(sender, recipient);
|
||||||
|
|
||||||
|
ΩlamportsΩ balance = getBalanceInLamport(sender);
|
||||||
|
|
||||||
|
if (balance <= 0) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"Wallet does not contain any SOL to transfer"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
SolanaLatestBlockhash latestBlockhash = getLatestBlockhash();
|
||||||
|
|
||||||
|
// The transfer amount is always serialized as one fixed-width u64.
|
||||||
|
// Replacing it with balance - fee therefore cannot change the fee.
|
||||||
|
SerializedSolanaTransfer feeCandidate =
|
||||||
|
serializeSolanaTransferTransaction(
|
||||||
|
sender,
|
||||||
|
recipient,
|
||||||
|
latestBlockhash,
|
||||||
|
balance
|
||||||
|
);
|
||||||
|
|
||||||
|
ΩlamportsΩ fee = 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(
|
||||||
|
sender,
|
||||||
|
recipient,
|
||||||
|
latestBlockhash,
|
||||||
|
transferableLamports
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Map<ΩSPLMintAddressΩ, SPLTokenHolding> getSPLTokenHoldings(
|
public Map<ΩSPLMintAddressΩ, SPLTokenHolding> getSPLTokenHoldings(
|
||||||
ΩSolanaAddressΩ ownerAddress,
|
ΩSolanaAddressΩ ownerAddress,
|
||||||
@@ -617,6 +693,215 @@ public class SolanaBlockChainImpl implements SolanaBlockChain {
|
|||||||
return objectMapper.writeValueAsString(request);
|
return objectMapper.writeValueAsString(request);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private SolanaUnsignedTransaction buildSolanaTransferTransaction(
|
||||||
|
ΩSolanaAddressΩ sender,
|
||||||
|
ΩSolanaAddressΩ recipient,
|
||||||
|
SolanaLatestBlockhash latestBlockhash,
|
||||||
|
ΩlamportsΩ lamports
|
||||||
|
) {
|
||||||
|
SerializedSolanaTransfer transfer =
|
||||||
|
serializeSolanaTransferTransaction(
|
||||||
|
sender,
|
||||||
|
recipient,
|
||||||
|
latestBlockhash,
|
||||||
|
lamports
|
||||||
|
);
|
||||||
|
|
||||||
|
return new SolanaUnsignedTransaction(
|
||||||
|
transfer.serializedTransaction(),
|
||||||
|
latestBlockhash.blockhash(),
|
||||||
|
latestBlockhash.lastValidBlockHeight()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private SerializedSolanaTransfer serializeSolanaTransferTransaction(
|
||||||
|
ΩSolanaAddressΩ sender,
|
||||||
|
ΩSolanaAddressΩ recipient,
|
||||||
|
SolanaLatestBlockhash latestBlockhash,
|
||||||
|
ΩlamportsΩ lamports
|
||||||
|
) {
|
||||||
|
if (lamports <= 0) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"SOL transfer amount must be greater than zero lamports"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] senderBytes = decodeSolanaTransferAddress(
|
||||||
|
"sender",
|
||||||
|
sender
|
||||||
|
);
|
||||||
|
byte[] recipientBytes = decodeSolanaTransferAddress(
|
||||||
|
"recipient",
|
||||||
|
recipient
|
||||||
|
);
|
||||||
|
byte[] systemProgramBytes = decodeSolanaTransferAddress(
|
||||||
|
"System Program",
|
||||||
|
SYSTEM_PROGRAM_ADDRESS
|
||||||
|
);
|
||||||
|
byte[] blockhashBytes = decodeSolanaTransferAddress(
|
||||||
|
"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 SerializedSolanaTransfer(
|
||||||
|
serializedMessage,
|
||||||
|
serializedTransaction
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ΩlamportsΩ toLamports(ΩSolanaAmountΩ amount) {
|
||||||
|
if (amount == null || amount.amount() == null) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"SOL transfer amount must not be null"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!WellKnownCurrencyTypes.SOLANA
|
||||||
|
.getCurrencyType()
|
||||||
|
.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 validateSolanaTransferAddresses(
|
||||||
|
ΩSolanaAddressΩ sender,
|
||||||
|
ΩSolanaAddressΩ recipient
|
||||||
|
) {
|
||||||
|
if (sender == null || sender.isBlank()) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Solana sender must not be blank"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (recipient == null || recipient.isBlank()) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"SOL transfer recipient must not be blank"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (recipient.equals(sender)) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"SOL transfer recipient must be different from the sender"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] decodeSolanaTransferAddress(
|
||||||
|
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 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 String createGetProgramAccountsBody(
|
private String createGetProgramAccountsBody(
|
||||||
ΩSolanaProgramIdΩ programId,
|
ΩSolanaProgramIdΩ programId,
|
||||||
Set<SolanaProgramAccountMemcmpFilter> filters
|
Set<SolanaProgramAccountMemcmpFilter> filters
|
||||||
@@ -948,6 +1233,17 @@ public class SolanaBlockChainImpl implements SolanaBlockChain {
|
|||||||
return indexes;
|
return indexes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private record SerializedSolanaTransfer(
|
||||||
|
Ω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 ΩAmountΩ LAMPORTS_PER_SOL = new BigDecimal("1000000000");
|
private static final ΩAmountΩ LAMPORTS_PER_SOL = new BigDecimal("1000000000");
|
||||||
private static final ΩmilliSecondsΩ MINIMUM_REMOTE_CALL_INTERVAL = 5000L;
|
private static final ΩmilliSecondsΩ MINIMUM_REMOTE_CALL_INTERVAL = 5000L;
|
||||||
private static final byte[] PROGRAM_DERIVED_ADDRESS_MARKER = "ProgramDerivedAddress".getBytes(StandardCharsets.UTF_8);
|
private static final byte[] PROGRAM_DERIVED_ADDRESS_MARKER = "ProgramDerivedAddress".getBytes(StandardCharsets.UTF_8);
|
||||||
|
|||||||
+51
-44
@@ -1,12 +1,10 @@
|
|||||||
package com.r35157.cryptowallet.solana.impl.ref;
|
package com.r35157.libs.solana.impl.ref;
|
||||||
|
|
||||||
import com.r35157.libs.solana.SolanaBlockChain;
|
|
||||||
import com.r35157.libs.solana.SolanaLatestBlockhash;
|
import com.r35157.libs.solana.SolanaLatestBlockhash;
|
||||||
import com.r35157.libs.solana.SolanaUnsignedTransaction;
|
import com.r35157.libs.solana.SolanaUnsignedTransaction;
|
||||||
import com.r35157.libs.valuetypes.basic.MoneyAmount;
|
import com.r35157.libs.valuetypes.basic.MoneyAmount;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
import java.lang.reflect.Proxy;
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.nio.ByteOrder;
|
import java.nio.ByteOrder;
|
||||||
@@ -19,17 +17,18 @@ import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
|||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
|
||||||
class SolanaWalletImplTest {
|
class SolanaBlockChainImplTest {
|
||||||
@Test
|
@Test
|
||||||
void buildsExpectedLegacySolTransferTransaction() throws Exception {
|
void buildsExpectedLegacySolTransferTransaction() throws Exception {
|
||||||
SolanaWalletImpl wallet = new SolanaWalletImpl(
|
SolanaBlockChainImpl blockChain = createBlockChain(
|
||||||
SENDER,
|
0,
|
||||||
"unused-test-key",
|
0,
|
||||||
createBlockChain(0, 0, new AtomicReference<>())
|
new AtomicReference<>()
|
||||||
);
|
);
|
||||||
|
|
||||||
SolanaUnsignedTransaction transaction =
|
SolanaUnsignedTransaction transaction =
|
||||||
wallet.buildSolanaTransferTransaction(
|
blockChain.buildSolanaTransferTransaction(
|
||||||
|
SENDER,
|
||||||
RECIPIENT,
|
RECIPIENT,
|
||||||
new MoneyAmount(
|
new MoneyAmount(
|
||||||
new BigDecimal("0.123456789"),
|
new BigDecimal("0.123456789"),
|
||||||
@@ -48,14 +47,17 @@ class SolanaWalletImplTest {
|
|||||||
@Test
|
@Test
|
||||||
void transferAllSubtractsExactFeeFromBalance() throws Exception {
|
void transferAllSubtractsExactFeeFromBalance() throws Exception {
|
||||||
AtomicReference<String> feeMessage = new AtomicReference<>();
|
AtomicReference<String> feeMessage = new AtomicReference<>();
|
||||||
SolanaWalletImpl wallet = new SolanaWalletImpl(
|
SolanaBlockChainImpl blockChain = createBlockChain(
|
||||||
SENDER,
|
10_000,
|
||||||
"unused-test-key",
|
5_000,
|
||||||
createBlockChain(10_000, 5_000, feeMessage)
|
feeMessage
|
||||||
);
|
);
|
||||||
|
|
||||||
SolanaUnsignedTransaction transaction =
|
SolanaUnsignedTransaction transaction =
|
||||||
wallet.buildSolanaTransferAllTransaction(RECIPIENT);
|
blockChain.buildSolanaTransferAllTransaction(
|
||||||
|
SENDER,
|
||||||
|
RECIPIENT
|
||||||
|
);
|
||||||
|
|
||||||
byte[] transactionBytes = Base64.getDecoder().decode(
|
byte[] transactionBytes = Base64.getDecoder().decode(
|
||||||
transaction.serializedTransaction()
|
transaction.serializedTransaction()
|
||||||
@@ -98,15 +100,18 @@ class SolanaWalletImplTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void transferAllRejectsBalanceThatOnlyCoversFee() {
|
void transferAllRejectsBalanceThatOnlyCoversFee() {
|
||||||
SolanaWalletImpl wallet = new SolanaWalletImpl(
|
SolanaBlockChainImpl blockChain = createBlockChain(
|
||||||
SENDER,
|
5_000,
|
||||||
"unused-test-key",
|
5_000,
|
||||||
createBlockChain(5_000, 5_000, new AtomicReference<>())
|
new AtomicReference<>()
|
||||||
);
|
);
|
||||||
|
|
||||||
IllegalStateException exception = assertThrows(
|
IllegalStateException exception = assertThrows(
|
||||||
IllegalStateException.class,
|
IllegalStateException.class,
|
||||||
() -> wallet.buildSolanaTransferAllTransaction(RECIPIENT)
|
() -> blockChain.buildSolanaTransferAllTransaction(
|
||||||
|
SENDER,
|
||||||
|
RECIPIENT
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
assertEquals(
|
assertEquals(
|
||||||
@@ -119,15 +124,16 @@ class SolanaWalletImplTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void rejectsAmountsSmallerThanOneLamport() {
|
void rejectsAmountsSmallerThanOneLamport() {
|
||||||
SolanaWalletImpl wallet = new SolanaWalletImpl(
|
SolanaBlockChainImpl blockChain = createBlockChain(
|
||||||
SENDER,
|
0,
|
||||||
"unused-test-key",
|
0,
|
||||||
createBlockChain(0, 0, new AtomicReference<>())
|
new AtomicReference<>()
|
||||||
);
|
);
|
||||||
|
|
||||||
assertThrows(
|
assertThrows(
|
||||||
IllegalArgumentException.class,
|
IllegalArgumentException.class,
|
||||||
() -> wallet.buildSolanaTransferTransaction(
|
() -> blockChain.buildSolanaTransferTransaction(
|
||||||
|
SENDER,
|
||||||
RECIPIENT,
|
RECIPIENT,
|
||||||
new MoneyAmount(
|
new MoneyAmount(
|
||||||
new BigDecimal("0.0000000001"),
|
new BigDecimal("0.0000000001"),
|
||||||
@@ -137,30 +143,31 @@ class SolanaWalletImplTest {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static SolanaBlockChain createBlockChain(
|
private static SolanaBlockChainImpl createBlockChain(
|
||||||
long balance,
|
long balance,
|
||||||
long fee,
|
long fee,
|
||||||
AtomicReference<String> feeMessage
|
AtomicReference<String> feeMessage
|
||||||
) {
|
) {
|
||||||
return (SolanaBlockChain) Proxy.newProxyInstance(
|
return new SolanaBlockChainImpl() {
|
||||||
SolanaBlockChain.class.getClassLoader(),
|
@Override
|
||||||
new Class<?>[] { SolanaBlockChain.class },
|
public long getBalanceInLamport(String address) {
|
||||||
(proxy, method, arguments) -> switch (method.getName()) {
|
return balance;
|
||||||
case "getBalanceInLamport" -> balance;
|
}
|
||||||
case "getLatestBlockhash" -> new SolanaLatestBlockhash(
|
|
||||||
BLOCKHASH,
|
@Override
|
||||||
LAST_VALID_BLOCK_HEIGHT
|
public SolanaLatestBlockhash getLatestBlockhash() {
|
||||||
);
|
return new SolanaLatestBlockhash(
|
||||||
case "getFeeForMessage" -> {
|
BLOCKHASH,
|
||||||
feeMessage.set((String) arguments[0]);
|
LAST_VALID_BLOCK_HEIGHT
|
||||||
yield fee;
|
);
|
||||||
}
|
}
|
||||||
default -> throw new AssertionError(
|
|
||||||
"Unexpected SolanaBlockChain call: "
|
@Override
|
||||||
+ method.getName()
|
public long getFeeForMessage(String serializedMessage) {
|
||||||
);
|
feeMessage.set(serializedMessage);
|
||||||
}
|
return fee;
|
||||||
);
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static long readLastLongLittleEndian(byte[] bytes) {
|
private static long readLastLongLittleEndian(byte[] bytes) {
|
||||||
Reference in New Issue
Block a user