31: Add a minimal Solana wallet API and external-signer implementation
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
package com.r35157.cryptowallet.solana;
|
||||
|
||||
import com.r35157.libs.solana.SolanaSignedTransaction;
|
||||
import com.r35157.libs.solana.SolanaUnsignedTransaction;
|
||||
import com.r35157.libs.valuetypes.basic.MoneyAmount;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* A Solana wallet capable of identifying itself and signing transactions.
|
||||
*
|
||||
* <p>Signing a transaction does not submit it to the Solana blockchain.
|
||||
* Submission must be performed separately through {@code SolanaBlockChain}.</p>
|
||||
*/
|
||||
public interface SolanaWallet {
|
||||
/**
|
||||
* Returns the public Solana address controlled by this wallet.
|
||||
*
|
||||
* @return the wallet's public address
|
||||
*/
|
||||
@NotNull
|
||||
ΩSolanaWalletIdΩ getAddress();
|
||||
|
||||
/**
|
||||
* Returns the native SOL balance of this wallet.
|
||||
*
|
||||
* @return the wallet's native SOL balance
|
||||
* @throws IOException if the balance cannot be fetched or decoded
|
||||
* @throws InterruptedException if the calling thread is interrupted
|
||||
*/
|
||||
@NotNull
|
||||
ΩSolanaAmountΩ getSolanaBalance()
|
||||
throws IOException, InterruptedException;
|
||||
|
||||
/**
|
||||
* Signs an unsigned Solana transaction.
|
||||
*
|
||||
* <p>The configured wallet must be a required signer for the supplied
|
||||
* transaction. This method does not submit the resulting transaction
|
||||
* to the Solana blockchain.</p>
|
||||
*
|
||||
* @param transaction the unsigned transaction to sign
|
||||
* @return the transaction containing the wallet's required signature
|
||||
* @throws IllegalArgumentException if the transaction is missing or blank
|
||||
* @throws IOException if the transaction cannot be signed or the signer
|
||||
* returns an invalid response
|
||||
* @throws InterruptedException if the signing process is interrupted
|
||||
*/
|
||||
@NotNull
|
||||
SolanaSignedTransaction signTransaction(
|
||||
@NotNull SolanaUnsignedTransaction transaction
|
||||
) throws IOException, InterruptedException;
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
package com.r35157.cryptowallet.solana.impl.ref;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.r35157.cryptowallet.solana.SolanaWallet;
|
||||
import com.r35157.libs.solana.SolanaBlockChain;
|
||||
import com.r35157.libs.solana.SolanaSignedTransaction;
|
||||
import com.r35157.libs.solana.SolanaUnsignedTransaction;
|
||||
import com.r35157.libs.valuetypes.basic.MoneyAmount;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public class SolanaWalletImpl implements SolanaWallet {
|
||||
public SolanaWalletImpl(
|
||||
ΩSolanaWalletIdΩ address,
|
||||
String signerKeyName,
|
||||
SolanaBlockChain solanaBlockChain
|
||||
) {
|
||||
this.address = address;
|
||||
this.signerKeyName = signerKeyName;
|
||||
this.solanaBlockChain = solanaBlockChain;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull ΩSolanaWalletIdΩ getAddress() {
|
||||
return address;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull ΩSolanaAmountΩ getSolanaBalance()
|
||||
throws IOException, InterruptedException {
|
||||
return solanaBlockChain.getBalanceInSolana(address);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull SolanaSignedTransaction signTransaction(
|
||||
@NotNull SolanaUnsignedTransaction transaction
|
||||
) throws IOException, InterruptedException {
|
||||
if (transaction.serializedTransaction() == null
|
||||
|| transaction.serializedTransaction().isBlank()) {
|
||||
throw new IllegalArgumentException(
|
||||
"Unsigned serialized transaction must not be blank"
|
||||
);
|
||||
}
|
||||
|
||||
Process process = new ProcessBuilder(
|
||||
"jup",
|
||||
"sign",
|
||||
"-f",
|
||||
"json",
|
||||
"--key",
|
||||
signerKeyName,
|
||||
"--tx",
|
||||
transaction.serializedTransaction()
|
||||
).start();
|
||||
|
||||
String standardOutput = new String(
|
||||
process.getInputStream().readAllBytes(),
|
||||
StandardCharsets.UTF_8
|
||||
);
|
||||
|
||||
String standardError = new String(
|
||||
process.getErrorStream().readAllBytes(),
|
||||
StandardCharsets.UTF_8
|
||||
);
|
||||
|
||||
int exitCode = process.waitFor();
|
||||
|
||||
if (exitCode != 0) {
|
||||
throw new IOException(
|
||||
"Solana transaction signing failed with exit code "
|
||||
+ exitCode
|
||||
+ ": "
|
||||
+ standardError
|
||||
);
|
||||
}
|
||||
|
||||
SignerResponse response;
|
||||
|
||||
try {
|
||||
response = OBJECT_MAPPER.readValue(
|
||||
standardOutput,
|
||||
SignerResponse.class
|
||||
);
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new IOException(
|
||||
"Unable to decode signer response: " + standardOutput,
|
||||
e
|
||||
);
|
||||
}
|
||||
|
||||
if (response.signer() == null || response.signer().isBlank()) {
|
||||
throw new IOException(
|
||||
"Signer response did not contain a signer address"
|
||||
);
|
||||
}
|
||||
|
||||
if (!address.equals(response.signer())) {
|
||||
throw new IOException(
|
||||
"Unexpected signer address. Expected "
|
||||
+ address
|
||||
+ " but received "
|
||||
+ response.signer()
|
||||
);
|
||||
}
|
||||
|
||||
if (response.signedTransaction() == null
|
||||
|| response.signedTransaction().isBlank()) {
|
||||
throw new IOException(
|
||||
"Signer response did not contain a signed transaction"
|
||||
);
|
||||
}
|
||||
|
||||
ΩBase64StringΩ signedTransaction = response.signedTransaction();
|
||||
SolanaSignedTransaction sst = new SolanaSignedTransaction(signedTransaction);
|
||||
|
||||
return sst;
|
||||
}
|
||||
|
||||
private record SignerResponse(
|
||||
String signer,
|
||||
ΩBase64StringΩ signedTransaction
|
||||
) {
|
||||
}
|
||||
|
||||
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
|
||||
|
||||
private final ΩSolanaWalletIdΩ address;
|
||||
private final String signerKeyName;
|
||||
private final SolanaBlockChain solanaBlockChain;
|
||||
}
|
||||
Reference in New Issue
Block a user