50: Add support for transferring SPL tokens
This commit is contained in:
+18
@@ -0,0 +1,18 @@
|
|||||||
|
package com.r35157.cryptowallet.solana;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decides how an SPL-token transfer handles a missing recipient Associated
|
||||||
|
* Token Account.
|
||||||
|
*/
|
||||||
|
public enum MissingRecipientTokenAccountPolicy {
|
||||||
|
/**
|
||||||
|
* Reject the transfer without submitting a transaction.
|
||||||
|
*/
|
||||||
|
FAIL,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create the recipient account in the transfer transaction, paid by the
|
||||||
|
* sending wallet.
|
||||||
|
*/
|
||||||
|
CREATE_AT_SENDER_EXPENSE
|
||||||
|
}
|
||||||
@@ -89,6 +89,34 @@ public interface SolanaWallet {
|
|||||||
@NotNull ΩSolanaWalletIdΩ recipient
|
@NotNull ΩSolanaWalletIdΩ recipient
|
||||||
) throws IOException, InterruptedException;
|
) throws IOException, InterruptedException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends an SPL token to another wallet.
|
||||||
|
*
|
||||||
|
* <p>The token program is detected from the mint account. The wallet
|
||||||
|
* builds, signs and submits the complete transaction internally.</p>
|
||||||
|
*
|
||||||
|
* @param mintAddress the mint of the token to send
|
||||||
|
* @param recipient the wallet that should receive the token
|
||||||
|
* @param amount the token amount in its human-readable decimal unit
|
||||||
|
* @param missingRecipientTokenAccountPolicy how to handle a missing
|
||||||
|
* recipient Associated Token Account
|
||||||
|
* @return the transaction signature returned by Solana RPC
|
||||||
|
* @throws IllegalArgumentException if an argument or amount is invalid
|
||||||
|
* @throws IllegalStateException if required accounts or balances are
|
||||||
|
* missing or invalid
|
||||||
|
* @throws IOException if blockchain data cannot be read or the transaction
|
||||||
|
* cannot be built, signed or sent
|
||||||
|
* @throws InterruptedException if the calling thread is interrupted
|
||||||
|
*/
|
||||||
|
@NotNull
|
||||||
|
ΩSolanaTransactionSignatureΩ sendSPLToken(
|
||||||
|
@NotNull ΩSPLMintAddressΩ mintAddress,
|
||||||
|
@NotNull ΩSolanaWalletIdΩ recipient,
|
||||||
|
@NotNull ΩAmountΩ amount,
|
||||||
|
@NotNull MissingRecipientTokenAccountPolicy
|
||||||
|
missingRecipientTokenAccountPolicy
|
||||||
|
) throws IOException, InterruptedException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Signs an unsigned Solana transaction.
|
* Signs an unsigned Solana transaction.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -3,10 +3,13 @@ package com.r35157.cryptowallet.solana.impl.ref;
|
|||||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.r35157.cryptowallet.solana.SolanaWallet;
|
import com.r35157.cryptowallet.solana.SolanaWallet;
|
||||||
|
import com.r35157.cryptowallet.solana.MissingRecipientTokenAccountPolicy;
|
||||||
|
import com.r35157.libs.solana.SolanaAccountInfo;
|
||||||
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.SolanaSignedTransaction;
|
import com.r35157.libs.solana.SolanaSignedTransaction;
|
||||||
import com.r35157.libs.solana.SolanaUnsignedTransaction;
|
import com.r35157.libs.solana.SolanaUnsignedTransaction;
|
||||||
|
import com.r35157.libs.solana.SPLTokenSupply;
|
||||||
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;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
@@ -15,6 +18,8 @@ import org.jetbrains.annotations.Nullable;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Base64;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
public class SolanaWalletImpl implements SolanaWallet {
|
public class SolanaWalletImpl implements SolanaWallet {
|
||||||
public SolanaWalletImpl(
|
public SolanaWalletImpl(
|
||||||
@@ -80,6 +85,194 @@ public class SolanaWalletImpl implements SolanaWallet {
|
|||||||
return signAndSendTransaction(unsignedTransaction);
|
return signAndSendTransaction(unsignedTransaction);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull ΩSolanaTransactionSignatureΩ sendSPLToken(
|
||||||
|
@NotNull ΩSPLMintAddressΩ mintAddress,
|
||||||
|
@NotNull ΩSolanaWalletIdΩ recipient,
|
||||||
|
@NotNull ΩAmountΩ amount,
|
||||||
|
@NotNull MissingRecipientTokenAccountPolicy policy
|
||||||
|
) throws IOException, InterruptedException {
|
||||||
|
if (mintAddress == null || mintAddress.isBlank()) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"SPL token mint address must not be blank"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (recipient == null
|
||||||
|
|| recipient.isBlank()
|
||||||
|
|| recipient.equals(address)) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"SPL token recipient must be present and different "
|
||||||
|
+ "from the sender"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (amount == null || amount.signum() <= 0) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"SPL token transfer amount must be greater than zero"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Objects.requireNonNull(
|
||||||
|
policy,
|
||||||
|
"Missing recipient token account policy must not be null"
|
||||||
|
);
|
||||||
|
|
||||||
|
SolanaAccountInfo mintAccount = solanaBlockChain.getAccountInfo(
|
||||||
|
mintAddress
|
||||||
|
);
|
||||||
|
if (mintAccount == null) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"SPL token mint account does not exist: " + mintAddress
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
SolanaSPLTokenProgram splProgram = detectTokenProgram(mintAccount);
|
||||||
|
SPLTokenSupply tokenSupply = solanaBlockChain.getSPLTokenSupply(
|
||||||
|
mintAddress,
|
||||||
|
splProgram
|
||||||
|
);
|
||||||
|
SPLTokenHolding senderHolding = solanaBlockChain
|
||||||
|
.getSPLTokenHoldings(address, splProgram)
|
||||||
|
.get(mintAddress);
|
||||||
|
|
||||||
|
if (senderHolding == null) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"Sender does not have a token account for mint "
|
||||||
|
+ mintAddress
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (senderHolding.decimals() != tokenSupply.decimals()) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"Sender token account decimals do not match the mint"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ΩRawAmountΩ rawAmount;
|
||||||
|
try {
|
||||||
|
rawAmount = amount
|
||||||
|
.movePointRight(tokenSupply.decimals())
|
||||||
|
.toBigIntegerExact()
|
||||||
|
.toString();
|
||||||
|
} catch (ArithmeticException e) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"SPL token transfer amount has more than "
|
||||||
|
+ tokenSupply.decimals()
|
||||||
|
+ " decimal places",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (new java.math.BigInteger(rawAmount).compareTo(
|
||||||
|
new java.math.BigInteger(senderHolding.rawAmount())
|
||||||
|
) > 0) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"Sender has insufficient SPL token balance"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ΩSPLTokenAccountΩ recipientTokenAccount =
|
||||||
|
solanaBlockChain.findAssociatedTokenAccount(
|
||||||
|
recipient,
|
||||||
|
mintAddress,
|
||||||
|
splProgram
|
||||||
|
);
|
||||||
|
SolanaAccountInfo recipientAccount = solanaBlockChain.getAccountInfo(
|
||||||
|
recipientTokenAccount
|
||||||
|
);
|
||||||
|
boolean createRecipientAccount = recipientAccount == null;
|
||||||
|
|
||||||
|
if (createRecipientAccount
|
||||||
|
&& policy == MissingRecipientTokenAccountPolicy.FAIL) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"Recipient Associated Token Account does not exist: "
|
||||||
|
+ recipientTokenAccount
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (recipientAccount != null) {
|
||||||
|
validateRecipientTokenAccount(
|
||||||
|
recipientAccount,
|
||||||
|
mintAddress,
|
||||||
|
recipient,
|
||||||
|
splProgram
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
SolanaUnsignedTransaction transaction =
|
||||||
|
solanaBlockChain.buildSPLTokenTransferTransaction(
|
||||||
|
address,
|
||||||
|
senderHolding.tokenAccount(),
|
||||||
|
mintAddress,
|
||||||
|
recipient,
|
||||||
|
recipientTokenAccount,
|
||||||
|
rawAmount,
|
||||||
|
tokenSupply.decimals(),
|
||||||
|
splProgram,
|
||||||
|
createRecipientAccount
|
||||||
|
);
|
||||||
|
|
||||||
|
return signAndSendTransaction(transaction);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateRecipientTokenAccount(
|
||||||
|
SolanaAccountInfo tokenAccount,
|
||||||
|
ΩSPLMintAddressΩ expectedMint,
|
||||||
|
ΩSolanaWalletIdΩ expectedOwner,
|
||||||
|
SolanaSPLTokenProgram expectedProgram
|
||||||
|
) {
|
||||||
|
if (!expectedProgram.getAddress().equals(tokenAccount.owner())) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"Recipient token account is owned by an unexpected program"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] accountData;
|
||||||
|
try {
|
||||||
|
accountData = Base64.getDecoder().decode(tokenAccount.data());
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"Recipient token account contains invalid Base64 data",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (accountData.length < 64) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"Recipient token account data is too short"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ΩSolanaAddressΩ actualMint = solanaBlockChain.encodeSolanaAddress(
|
||||||
|
java.util.Arrays.copyOfRange(accountData, 0, 32)
|
||||||
|
);
|
||||||
|
ΩSolanaAddressΩ actualOwner = solanaBlockChain.encodeSolanaAddress(
|
||||||
|
java.util.Arrays.copyOfRange(accountData, 32, 64)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!expectedMint.equals(actualMint)) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"Recipient token account belongs to an unexpected mint"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!expectedOwner.equals(actualOwner)) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"Recipient token account belongs to an unexpected owner"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private SolanaSPLTokenProgram detectTokenProgram(
|
||||||
|
SolanaAccountInfo mintAccount
|
||||||
|
) {
|
||||||
|
for (SolanaSPLTokenProgram program
|
||||||
|
: SolanaSPLTokenProgram.values()) {
|
||||||
|
if (program.getAddress().equals(mintAccount.owner())) {
|
||||||
|
return program;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"Mint account is not owned by a supported SPL token program: "
|
||||||
|
+ mintAccount.owner()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public @NotNull SolanaSignedTransaction signTransaction(
|
public @NotNull SolanaSignedTransaction signTransaction(
|
||||||
@NotNull SolanaUnsignedTransaction transaction
|
@NotNull SolanaUnsignedTransaction transaction
|
||||||
|
|||||||
@@ -100,6 +100,43 @@ public interface SolanaBlockChain {
|
|||||||
ΩSolanaAddressΩ recipient
|
ΩSolanaAddressΩ recipient
|
||||||
) throws IOException, InterruptedException;
|
) throws IOException, InterruptedException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds an unsigned checked SPL-token transfer.
|
||||||
|
*
|
||||||
|
* @param sender wallet owner, authority and transaction fee payer
|
||||||
|
* @param senderTokenAccount token account from which tokens are sent
|
||||||
|
* @param mintAddress token mint
|
||||||
|
* @param recipient recipient wallet owner
|
||||||
|
* @param recipientTokenAccount recipient Associated Token Account
|
||||||
|
* @param rawAmount amount expressed in the mint's smallest unit
|
||||||
|
* @param decimals decimal count read from the mint
|
||||||
|
* @param splProgram token program owning the mint and token accounts
|
||||||
|
* @param createRecipientTokenAccount whether the transaction must first
|
||||||
|
* create the recipient Associated Token Account
|
||||||
|
* @return the unsigned serialized transaction
|
||||||
|
*/
|
||||||
|
SolanaUnsignedTransaction buildSPLTokenTransferTransaction(
|
||||||
|
ΩSolanaAddressΩ sender,
|
||||||
|
ΩSPLTokenAccountΩ senderTokenAccount,
|
||||||
|
ΩSPLMintAddressΩ mintAddress,
|
||||||
|
ΩSolanaAddressΩ recipient,
|
||||||
|
ΩSPLTokenAccountΩ recipientTokenAccount,
|
||||||
|
ΩRawAmountΩ rawAmount,
|
||||||
|
ΩamountDecimalsΩ decimals,
|
||||||
|
SolanaSPLTokenProgram splProgram,
|
||||||
|
boolean createRecipientTokenAccount
|
||||||
|
) throws IOException, InterruptedException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Derives the Associated Token Account for an owner, mint and token
|
||||||
|
* program.
|
||||||
|
*/
|
||||||
|
ΩSPLTokenAccountΩ findAssociatedTokenAccount(
|
||||||
|
ΩSolanaAddressΩ owner,
|
||||||
|
ΩSPLMintAddressΩ mintAddress,
|
||||||
|
SolanaSPLTokenProgram splProgram
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -210,6 +210,44 @@ public final class CachedSolanaBlockChain implements SolanaBlockChain {
|
|||||||
return delegate.sendTransaction(transaction);
|
return delegate.sendTransaction(transaction);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SolanaUnsignedTransaction buildSPLTokenTransferTransaction(
|
||||||
|
ΩSolanaAddressΩ sender,
|
||||||
|
ΩSPLTokenAccountΩ senderTokenAccount,
|
||||||
|
ΩSPLMintAddressΩ mintAddress,
|
||||||
|
ΩSolanaAddressΩ recipient,
|
||||||
|
ΩSPLTokenAccountΩ recipientTokenAccount,
|
||||||
|
ΩRawAmountΩ rawAmount,
|
||||||
|
ΩamountDecimalsΩ decimals,
|
||||||
|
SolanaSPLTokenProgram splProgram,
|
||||||
|
boolean createRecipientTokenAccount
|
||||||
|
) throws IOException, InterruptedException {
|
||||||
|
return delegate.buildSPLTokenTransferTransaction(
|
||||||
|
sender,
|
||||||
|
senderTokenAccount,
|
||||||
|
mintAddress,
|
||||||
|
recipient,
|
||||||
|
recipientTokenAccount,
|
||||||
|
rawAmount,
|
||||||
|
decimals,
|
||||||
|
splProgram,
|
||||||
|
createRecipientTokenAccount
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ΩSPLTokenAccountΩ findAssociatedTokenAccount(
|
||||||
|
ΩSolanaAddressΩ owner,
|
||||||
|
ΩSPLMintAddressΩ mintAddress,
|
||||||
|
SolanaSPLTokenProgram splProgram
|
||||||
|
) {
|
||||||
|
return delegate.findAssociatedTokenAccount(
|
||||||
|
owner,
|
||||||
|
mintAddress,
|
||||||
|
splProgram
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private synchronized Set<SolanaAccountInfo> loadProgramAccounts(
|
private synchronized Set<SolanaAccountInfo> loadProgramAccounts(
|
||||||
ObjectCacheKey key,
|
ObjectCacheKey key,
|
||||||
ΩSolanaProgramIdΩ programId,
|
ΩSolanaProgramIdΩ programId,
|
||||||
|
|||||||
@@ -291,6 +291,146 @@ public class SolanaBlockChainImpl implements SolanaBlockChain {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ΩSPLTokenAccountΩ findAssociatedTokenAccount(
|
||||||
|
ΩSolanaAddressΩ owner,
|
||||||
|
ΩSPLMintAddressΩ mintAddress,
|
||||||
|
SolanaSPLTokenProgram splProgram
|
||||||
|
) {
|
||||||
|
Objects.requireNonNull(splProgram, "SPL token program must not be null");
|
||||||
|
|
||||||
|
ΩSPLTokenAccountΩ tokenAccount = findProgramAddress(
|
||||||
|
ASSOCIATED_TOKEN_PROGRAM_ADDRESS,
|
||||||
|
List.of(
|
||||||
|
SolanaProgramAddressSeed.solanaAddress(owner),
|
||||||
|
SolanaProgramAddressSeed.solanaAddress(
|
||||||
|
splProgram.getAddress()
|
||||||
|
),
|
||||||
|
SolanaProgramAddressSeed.solanaAddress(mintAddress)
|
||||||
|
)
|
||||||
|
).address();
|
||||||
|
|
||||||
|
return tokenAccount;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SolanaUnsignedTransaction buildSPLTokenTransferTransaction(
|
||||||
|
ΩSolanaAddressΩ sender,
|
||||||
|
ΩSPLTokenAccountΩ senderTokenAccount,
|
||||||
|
ΩSPLMintAddressΩ mintAddress,
|
||||||
|
ΩSolanaAddressΩ recipient,
|
||||||
|
ΩSPLTokenAccountΩ recipientTokenAccount,
|
||||||
|
ΩRawAmountΩ rawAmount,
|
||||||
|
ΩamountDecimalsΩ decimals,
|
||||||
|
SolanaSPLTokenProgram splProgram,
|
||||||
|
boolean createRecipientTokenAccount
|
||||||
|
) throws IOException, InterruptedException {
|
||||||
|
Objects.requireNonNull(splProgram, "SPL token program must not be null");
|
||||||
|
|
||||||
|
BigInteger transferAmount;
|
||||||
|
try {
|
||||||
|
transferAmount = new BigInteger(rawAmount);
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Raw SPL token amount must be an integer",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (transferAmount.signum() <= 0
|
||||||
|
|| transferAmount.bitLength() > 64) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Raw SPL token amount must fit in an unsigned u64 and "
|
||||||
|
+ "be greater than zero"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (decimals < 0 || decimals > 255) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"SPL token decimals must fit in an unsigned byte"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ΩSolanaAddressΩ> accountKeys = new ArrayList<>();
|
||||||
|
accountKeys.add(sender);
|
||||||
|
accountKeys.add(senderTokenAccount);
|
||||||
|
accountKeys.add(recipientTokenAccount);
|
||||||
|
accountKeys.add(mintAddress);
|
||||||
|
|
||||||
|
int tokenProgramIndex;
|
||||||
|
int readOnlyUnsignedAccounts;
|
||||||
|
if (createRecipientTokenAccount) {
|
||||||
|
accountKeys.add(recipient);
|
||||||
|
accountKeys.add(SYSTEM_PROGRAM_ADDRESS);
|
||||||
|
accountKeys.add(splProgram.getAddress());
|
||||||
|
accountKeys.add(ASSOCIATED_TOKEN_PROGRAM_ADDRESS);
|
||||||
|
tokenProgramIndex = 6;
|
||||||
|
readOnlyUnsignedAccounts = 5;
|
||||||
|
} else {
|
||||||
|
accountKeys.add(splProgram.getAddress());
|
||||||
|
tokenProgramIndex = 4;
|
||||||
|
readOnlyUnsignedAccounts = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<CompiledInstruction> instructions = new ArrayList<>();
|
||||||
|
if (createRecipientTokenAccount) {
|
||||||
|
instructions.add(new CompiledInstruction(
|
||||||
|
7,
|
||||||
|
new byte[] { 0, 2, 4, 3, 5, 6 },
|
||||||
|
new byte[] { 1 }
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] transferCheckedData = ByteBuffer
|
||||||
|
.allocate(10)
|
||||||
|
.order(ByteOrder.LITTLE_ENDIAN)
|
||||||
|
.put((byte) TOKEN_TRANSFER_CHECKED_INSTRUCTION)
|
||||||
|
.putLong(transferAmount.longValue())
|
||||||
|
.put((byte) decimals)
|
||||||
|
.array();
|
||||||
|
instructions.add(new CompiledInstruction(
|
||||||
|
tokenProgramIndex,
|
||||||
|
new byte[] { 1, 3, 2, 0 },
|
||||||
|
transferCheckedData
|
||||||
|
));
|
||||||
|
|
||||||
|
SolanaLatestBlockhash latestBlockhash = getLatestBlockhash();
|
||||||
|
ByteArrayOutputStream message = new ByteArrayOutputStream();
|
||||||
|
message.write(1);
|
||||||
|
message.write(0);
|
||||||
|
message.write(readOnlyUnsignedAccounts);
|
||||||
|
writeCompactU16(message, accountKeys.size());
|
||||||
|
|
||||||
|
for (String accountKey : accountKeys) {
|
||||||
|
message.writeBytes(decodeSolanaTransferAddress(
|
||||||
|
"transaction account",
|
||||||
|
accountKey
|
||||||
|
));
|
||||||
|
}
|
||||||
|
message.writeBytes(decodeSolanaTransferAddress(
|
||||||
|
"blockhash",
|
||||||
|
latestBlockhash.blockhash()
|
||||||
|
));
|
||||||
|
writeCompactU16(message, instructions.size());
|
||||||
|
|
||||||
|
for (CompiledInstruction instruction : instructions) {
|
||||||
|
message.write(instruction.programIndex());
|
||||||
|
writeCompactU16(message, instruction.accountIndexes().length);
|
||||||
|
message.writeBytes(instruction.accountIndexes());
|
||||||
|
writeCompactU16(message, instruction.data().length);
|
||||||
|
message.writeBytes(instruction.data());
|
||||||
|
}
|
||||||
|
|
||||||
|
ByteArrayOutputStream transaction = new ByteArrayOutputStream();
|
||||||
|
writeCompactU16(transaction, 1);
|
||||||
|
transaction.writeBytes(new byte[SOLANA_SIGNATURE_LENGTH]);
|
||||||
|
transaction.writeBytes(message.toByteArray());
|
||||||
|
|
||||||
|
return new SolanaUnsignedTransaction(
|
||||||
|
Base64.getEncoder().encodeToString(transaction.toByteArray()),
|
||||||
|
latestBlockhash.blockhash(),
|
||||||
|
latestBlockhash.lastValidBlockHeight()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Map<ΩSPLMintAddressΩ, SPLTokenHolding> getSPLTokenHoldings(
|
public Map<ΩSPLMintAddressΩ, SPLTokenHolding> getSPLTokenHoldings(
|
||||||
ΩSolanaAddressΩ ownerAddress,
|
ΩSolanaAddressΩ ownerAddress,
|
||||||
@@ -1239,9 +1379,19 @@ public class SolanaBlockChainImpl implements SolanaBlockChain {
|
|||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private record CompiledInstruction(
|
||||||
|
int programIndex,
|
||||||
|
byte[] accountIndexes,
|
||||||
|
byte[] data
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
private static final String SYSTEM_PROGRAM_ADDRESS =
|
private static final String SYSTEM_PROGRAM_ADDRESS =
|
||||||
"11111111111111111111111111111111";
|
"11111111111111111111111111111111";
|
||||||
|
private static final String ASSOCIATED_TOKEN_PROGRAM_ADDRESS =
|
||||||
|
"ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL";
|
||||||
private static final int SYSTEM_TRANSFER_INSTRUCTION = 2;
|
private static final int SYSTEM_TRANSFER_INSTRUCTION = 2;
|
||||||
|
private static final int TOKEN_TRANSFER_CHECKED_INSTRUCTION = 12;
|
||||||
private static final int SOLANA_ADDRESS_LENGTH = 32;
|
private static final int SOLANA_ADDRESS_LENGTH = 32;
|
||||||
private static final int SOLANA_SIGNATURE_LENGTH = 64;
|
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");
|
||||||
|
|||||||
Reference in New Issue
Block a user