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