Learn how to interact with Ethereum smart contracts from Java applications using Web3j for DeFi, NFTs, and blockchain applications.
Table of Contents
- Architecture Overview
- Setup & Dependencies
- Smart Contract Integration
- DeFi Protocol Examples
- NFT & ERC721 Integration
- Event Monitoring
- Security & Best Practices
Architecture Overview
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐ │ Java App │───▶│ Web3j Library │───▶│ Ethereum Node │ │ │ │ │ │ │ │ - Contract │ │ - ABIs │ │ - JSON-RPC │ │ Clients │ │ - Wrappers │ │ - WebSocket │ │ - Event │ │ - Codegen │ │ - IPC │ │ Listeners │ │ - Filters │ │ │ │ - Transaction │ │ - Transaction │ └─────────────────┘ │ Managers │ │ Management │ │ └─────────────────┘ └──────────────────┘ │ ▼ ┌─────────────────┐ │ Smart Contract │ │ on Ethereum │ └─────────────────┘
Setup & Dependencies
Maven Configuration
<properties>
<web3j.version>4.9.7</web3j.version>
<okhttp.version>4.10.0</okhttp.version>
<jackson.version>2.15.2</jackson.version>
</properties>
<dependencies>
<!-- Web3j Core -->
<dependency>
<groupId>org.web3j</groupId>
<artifactId>core</artifactId>
<version>${web3j.version}</version>
</dependency>
<!-- Web3j Contracts -->
<dependency>
<groupId>org.web3j</groupId>
<artifactId>contracts</artifactId>
<version>${web3j.version}</version>
</dependency>
<!-- HTTP Client -->
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>${okhttp.version}</version>
</dependency>
<!-- JSON Processing -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<!-- Logging -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.7</version>
</dependency>
<!-- Testing -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.9.2</version>
<scope>test</scope>
</dependency>
</dependencies>
Gradle Configuration
dependencies {
implementation 'org.web3j:core:4.9.7'
implementation 'org.web3j:contracts:4.9.7'
implementation 'com.squareup.okhttp3:okhttp:4.10.0'
implementation 'com.fasterxml.jackson.core:jackson-databind:2.15.2'
testImplementation 'org.junit.jupiter:junit-jupiter:5.9.2'
}
Core Ethereum Service
1. Web3j Configuration Service
@Service
public class Web3jService {
private static final Logger logger = LoggerFactory.getLogger(Web3jService.class);
private final Web3j web3j;
private final TransactionManager transactionManager;
private final Credentials credentials;
private final GasProvider gasProvider;
public Web3jService(@Value("${ethereum.node.url}") String nodeUrl,
@Value("${ethereum.wallet.private-key}") String privateKey) {
this.web3j = buildWeb3jClient(nodeUrl);
this.credentials = Credentials.create(privateKey);
this.transactionManager = new RawTransactionManager(web3j, credentials);
this.gasProvider = new DefaultGasProvider();
logger.info("Web3j service initialized for network: {}", nodeUrl);
}
private Web3j buildWeb3jClient(String nodeUrl) {
OkHttpClient okHttpClient = new OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.build();
return Web3j.build(new HttpService(nodeUrl, okHttpClient));
}
public Web3j getWeb3j() {
return web3j;
}
public Credentials getCredentials() {
return credentials;
}
public TransactionManager getTransactionManager() {
return transactionManager;
}
public GasProvider getGasProvider() {
return gasProvider;
}
public String getCurrentAddress() {
return credentials.getAddress();
}
public EthBlock.Block getLatestBlock() throws Exception {
return web3j.ethGetBlockByNumber(DefaultBlockParameterName.LATEST, false)
.send()
.getBlock();
}
public BigInteger getBalance(String address) throws Exception {
return web3j.ethGetBalance(address, DefaultBlockParameterName.LATEST)
.send()
.getBalance();
}
public BigInteger getGasPrice() throws Exception {
return web3j.ethGasPrice().send().getGasPrice();
}
@PreDestroy
public void shutdown() {
if (web3j != null) {
web3j.shutdown();
logger.info("Web3j service shutdown completed");
}
}
}
2. Contract Loader Service
@Service
public class ContractLoaderService {
private static final Logger logger = LoggerFactory.getLogger(ContractLoaderService.class);
private final Web3jService web3jService;
private final Map<String, String> contractABIs = new ConcurrentHashMap<>();
private final Map<String, String> contractAddresses = new ConcurrentHashMap<>();
public ContractLoaderService(Web3jService web3jService) {
this.web3jService = web3jService;
loadDefaultContracts();
}
/**
* Load contract from ABI string and address
*/
public <T extends Contract> T loadContract(String contractName,
String contractAddress,
Class<T> contractClass) {
try {
String abi = contractABIs.get(contractName);
if (abi == null) {
throw new ContractNotFoundException("ABI not found for contract: " + contractName);
}
return Contract.load(
contractAddress,
web3jService.getWeb3j(),
web3jService.getTransactionManager(),
web3jService.getGasProvider(),
abi
);
} catch (Exception e) {
logger.error("Failed to load contract: {}", contractName, e);
throw new ContractLoadException("Failed to load contract: " + contractName, e);
}
}
/**
* Deploy new contract
*/
public <T extends Contract> String deployContract(String contractName,
Class<T> contractClass,
Object... constructorParams) {
try {
String abi = contractABIs.get(contractName);
if (abi == null) {
throw new ContractNotFoundException("ABI not found for contract: " + contractName);
}
String bin = getContractBinary(contractName);
// Deploy contract
T contract = Contract.deploy(
contractClass,
web3jService.getWeb3j(),
web3jService.getTransactionManager(),
web3jService.getGasProvider(),
bin,
abi,
constructorParams
).send();
String contractAddress = contract.getContractAddress();
contractAddresses.put(contractName, contractAddress);
logger.info("Deployed contract {} at address: {}", contractName, contractAddress);
return contractAddress;
} catch (Exception e) {
logger.error("Failed to deploy contract: {}", contractName, e);
throw new ContractDeploymentException("Failed to deploy contract: " + contractName, e);
}
}
/**
* Register contract ABI
*/
public void registerContract(String contractName, String abi, String address) {
contractABIs.put(contractName, abi);
if (address != null) {
contractAddresses.put(contractName, address);
}
logger.debug("Registered contract: {}", contractName);
}
private void loadDefaultContracts() {
// ERC20 Standard ABI
registerContract("ERC20", ERC20_ABI, null);
registerContract("ERC721", ERC721_ABI, null);
registerContract("UniswapV2Router", UNISWAP_V2_ROUTER_ABI, null);
registerContract("WETH", WETH_ABI, null);
}
private String getContractBinary(String contractName) {
// Load contract binary from resources
// This would typically be loaded from compiled .bin files
return ""; // Implementation specific
}
// Standard Contract ABIs
private static final String ERC20_ABI = "[{\"constant\":true,\"inputs\":[],\"name\":\"name\",\"outputs\":[{\"name\":\"\",\"type\":\"string\"}],\"type\":\"function\"},...]";
private static final String ERC721_ABI = "[...]";
private static final String UNISWAP_V2_ROUTER_ABI = "[...]";
private static final String WETH_ABI = "[...]";
}
Smart Contract Integration
1. ERC20 Token Service
@Service
public class ERC20Service {
private static final Logger logger = LoggerFactory.getLogger(ERC20Service.class);
private final Web3jService web3jService;
private final ContractLoaderService contractLoader;
public ERC20Service(Web3jService web3jService, ContractLoaderService contractLoader) {
this.web3jService = web3jService;
this.contractLoader = contractLoader;
}
/**
* Get ERC20 token balance
*/
public BigInteger getBalance(String tokenAddress, String walletAddress) {
try {
ERC20 token = loadERC20(tokenAddress);
return token.balanceOf(walletAddress).send();
} catch (Exception e) {
logger.error("Failed to get balance for token: {}", tokenAddress, e);
throw new ContractCallException("Failed to get token balance", e);
}
}
/**
* Transfer ERC20 tokens
*/
public TransactionReceipt transfer(String tokenAddress,
String toAddress,
BigInteger amount) {
try {
ERC20 token = loadERC20(tokenAddress);
return token.transfer(toAddress, amount).send();
} catch (Exception e) {
logger.error("Failed to transfer tokens from {} to {}", tokenAddress, toAddress, e);
throw new ContractCallException("Token transfer failed", e);
}
}
/**
* Get token metadata
*/
public TokenInfo getTokenInfo(String tokenAddress) {
try {
ERC20 token = loadERC20(tokenAddress);
String name = token.name().send();
String symbol = token.symbol().send();
BigInteger decimals = token.decimals().send();
BigInteger totalSupply = token.totalSupply().send();
return TokenInfo.builder()
.address(tokenAddress)
.name(name)
.symbol(symbol)
.decimals(decimals.intValue())
.totalSupply(totalSupply)
.build();
} catch (Exception e) {
logger.error("Failed to get token info: {}", tokenAddress, e);
throw new ContractCallException("Failed to get token info", e);
}
}
/**
* Get token allowance
*/
public BigInteger getAllowance(String tokenAddress,
String owner,
String spender) {
try {
ERC20 token = loadERC20(tokenAddress);
return token.allowance(owner, spender).send();
} catch (Exception e) {
logger.error("Failed to get allowance for token: {}", tokenAddress, e);
throw new ContractCallException("Failed to get allowance", e);
}
}
/**
* Approve token spending
*/
public TransactionReceipt approve(String tokenAddress,
String spender,
BigInteger amount) {
try {
ERC20 token = loadERC20(tokenAddress);
return token.approve(spender, amount).send();
} catch (Exception e) {
logger.error("Failed to approve spending for token: {}", tokenAddress, e);
throw new ContractCallException("Token approval failed", e);
}
}
private ERC20 loadERC20(String tokenAddress) {
return contractLoader.loadContract("ERC20", tokenAddress, ERC20.class);
}
@Data
@Builder
public static class TokenInfo {
private String address;
private String name;
private String symbol;
private int decimals;
private BigInteger totalSupply;
}
}
2. DeFi Service (Uniswap Integration)
@Service
public class UniswapService {
private static final Logger logger = LoggerFactory.getLogger(UniswapService.class);
private final Web3jService web3jService;
private final ContractLoaderService contractLoader;
private final ERC20Service erc20Service;
// Mainnet Uniswap V2 Router address
private static final String UNISWAP_V2_ROUTER = "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D";
// WETH address
private static final String WETH = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2";
public UniswapService(Web3jService web3jService,
ContractLoaderService contractLoader,
ERC20Service erc20Service) {
this.web3jService = web3jService;
this.contractLoader = contractLoader;
this.erc20Service = erc20Service;
}
/**
* Swap exact ETH for tokens
*/
public TransactionReceipt swapExactETHForTokens(
String tokenOut,
BigInteger amountOutMin,
BigInteger valueETH) {
try {
// Get router contract
UniswapV2Router02 router = loadRouter();
// Define path (ETH -> WETH -> Token)
List<String> path = Arrays.asList(WETH, tokenOut);
// Define deadline (20 minutes from now)
BigInteger deadline = BigInteger.valueOf(
System.currentTimeMillis() / 1000 + 20 * 60
);
// Execute swap
return router.swapExactETHForTokens(
amountOutMin,
path,
web3jService.getCurrentAddress(),
deadline
).send();
} catch (Exception e) {
logger.error("Failed to swap ETH for tokens", e);
throw new ContractCallException("Swap failed", e);
}
}
/**
* Swap exact tokens for ETH
*/
public TransactionReceipt swapExactTokensForETH(
String tokenIn,
BigInteger amountIn,
BigInteger amountOutMin) {
try {
UniswapV2Router02 router = loadRouter();
// Approve router to spend tokens
erc20Service.approve(tokenIn, UNISWAP_V2_ROUTER, amountIn);
// Define path (Token -> WETH -> ETH)
List<String> path = Arrays.asList(tokenIn, WETH);
// Define deadline
BigInteger deadline = BigInteger.valueOf(
System.currentTimeMillis() / 1000 + 20 * 60
);
// Execute swap
return router.swapExactTokensForETH(
amountIn,
amountOutMin,
path,
web3jService.getCurrentAddress(),
deadline
).send();
} catch (Exception e) {
logger.error("Failed to swap tokens for ETH", e);
throw new ContractCallException("Swap failed", e);
}
}
/**
* Get price quote
*/
public List<BigInteger> getAmountsOut(BigInteger amountIn, List<String> path) {
try {
UniswapV2Router02 router = loadRouter();
return router.getAmountsOut(amountIn, path).send();
} catch (Exception e) {
logger.error("Failed to get price quote", e);
throw new ContractCallException("Price quote failed", e);
}
}
/**
* Add liquidity
*/
public TransactionReceipt addLiquidity(
String tokenA,
String tokenB,
BigInteger amountADesired,
BigInteger amountBDesired,
BigInteger amountAMin,
BigInteger amountBMin) {
try {
UniswapV2Router02 router = loadRouter();
// Approve tokens
erc20Service.approve(tokenA, UNISWAP_V2_ROUTER, amountADesired);
erc20Service.approve(tokenB, UNISWAP_V2_ROUTER, amountBDesired);
// Define deadline
BigInteger deadline = BigInteger.valueOf(
System.currentTimeMillis() / 1000 + 20 * 60
);
// Add liquidity
return router.addLiquidity(
tokenA,
tokenB,
amountADesired,
amountBDesired,
amountAMin,
amountBMin,
web3jService.getCurrentAddress(),
deadline
).send();
} catch (Exception e) {
logger.error("Failed to add liquidity", e);
throw new ContractCallException("Add liquidity failed", e);
}
}
private UniswapV2Router02 loadRouter() {
return contractLoader.loadContract(
"UniswapV2Router",
UNISWAP_V2_ROUTER,
UniswapV2Router02.class
);
}
}
3. NFT Service (ERC721)
@Service
public class NFTService {
private static final Logger logger = LoggerFactory.getLogger(NFTService.class);
private final Web3jService web3jService;
private final ContractLoaderService contractLoader;
public NFTService(Web3jService web3jService, ContractLoaderService contractLoader) {
this.web3jService = web3jService;
this.contractLoader = contractLoader;
}
/**
* Get NFT balance of address
*/
public BigInteger getBalance(String contractAddress, String owner) {
try {
ERC721 nft = loadERC721(contractAddress);
return nft.balanceOf(owner).send();
} catch (Exception e) {
logger.error("Failed to get NFT balance for contract: {}", contractAddress, e);
throw new ContractCallException("Failed to get NFT balance", e);
}
}
/**
* Get NFT owner
*/
public String getOwnerOf(String contractAddress, BigInteger tokenId) {
try {
ERC721 nft = loadERC721(contractAddress);
return nft.ownerOf(tokenId).send();
} catch (Exception e) {
logger.error("Failed to get owner for token {}: {}", tokenId, contractAddress, e);
throw new ContractCallException("Failed to get NFT owner", e);
}
}
/**
* Get NFT metadata URI
*/
public String getTokenURI(String contractAddress, BigInteger tokenId) {
try {
ERC721 nft = loadERC721(contractAddress);
return nft.tokenURI(tokenId).send();
} catch (Exception e) {
logger.error("Failed to get token URI for token {}: {}", tokenId, contractAddress, e);
throw new ContractCallException("Failed to get token URI", e);
}
}
/**
* Transfer NFT
*/
public TransactionReceipt transferFrom(String contractAddress,
String from,
String to,
BigInteger tokenId) {
try {
ERC721 nft = loadERC721(contractAddress);
return nft.transferFrom(from, to, tokenId).send();
} catch (Exception e) {
logger.error("Failed to transfer NFT {} from {} to {}", tokenId, from, to, e);
throw new ContractCallException("NFT transfer failed", e);
}
}
/**
* Safe transfer NFT
*/
public TransactionReceipt safeTransferFrom(String contractAddress,
String from,
String to,
BigInteger tokenId) {
try {
ERC721 nft = loadERC721(contractAddress);
return nft.safeTransferFrom(from, to, tokenId).send();
} catch (Exception e) {
logger.error("Failed to safe transfer NFT {} from {} to {}", tokenId, from, to, e);
throw new ContractCallException("NFT safe transfer failed", e);
}
}
/**
* Check NFT approval
*/
public String getApproved(String contractAddress, BigInteger tokenId) {
try {
ERC721 nft = loadERC721(contractAddress);
return nft.getApproved(tokenId).send();
} catch (Exception e) {
logger.error("Failed to get approved address for token {}: {}", tokenId, contractAddress, e);
throw new ContractCallException("Failed to get approved address", e);
}
}
/**
* Set approval for all
*/
public TransactionReceipt setApprovalForAll(String contractAddress,
String operator,
boolean approved) {
try {
ERC721 nft = loadERC721(contractAddress);
return nft.setApprovalForAll(operator, approved).send();
} catch (Exception e) {
logger.error("Failed to set approval for all for operator: {}", operator, e);
throw new ContractCallException("Set approval for all failed", e);
}
}
private ERC721 loadERC721(String contractAddress) {
return contractLoader.loadContract("ERC721", contractAddress, ERC721.class);
}
}
Event Monitoring
1. Event Listener Service
@Service
public class EventListenerService {
private static final Logger logger = LoggerFactory.getLogger(EventListenerService.class);
private final Web3jService web3jService;
private final Map<String, Disposable> eventSubscriptions = new ConcurrentHashMap<>();
public EventListenerService(Web3jService web3jService) {
this.web3jService = web3jService;
}
/**
* Listen for ERC20 Transfer events
*/
public void listenToERC20Transfers(String tokenAddress,
Consumer<ERC20.TransferEventResponse> handler) {
try {
ERC20 token = ERC20.load(
tokenAddress,
web3jService.getWeb3j(),
web3jService.getTransactionManager(),
web3jService.getGasProvider()
);
Disposable subscription = token.transferEventFlowable(
DefaultBlockParameterName.EARLIEST,
DefaultBlockParameterName.LATEST
).subscribe(event -> {
logger.info("ERC20 Transfer: from {} to {} amount {}",
event.from, event.to, event.value);
handler.accept(event);
});
eventSubscriptions.put("ERC20_" + tokenAddress, subscription);
logger.info("Started listening to ERC20 transfers for: {}", tokenAddress);
} catch (Exception e) {
logger.error("Failed to listen to ERC20 transfers: {}", tokenAddress, e);
throw new EventListenerException("Failed to listen to ERC20 transfers", e);
}
}
/**
* Listen for new blocks
*/
public void listenToNewBlocks(Consumer<EthBlock.Block> handler) {
Disposable subscription = web3jService.getWeb3j()
.blockFlowable(true)
.subscribe(ethBlock -> {
EthBlock.Block block = ethBlock.getBlock();
logger.debug("New block: {}", block.getNumber());
handler.accept(block);
});
eventSubscriptions.put("NEW_BLOCKS", subscription);
logger.info("Started listening to new blocks");
}
/**
* Listen for pending transactions
*/
public void listenToPendingTransactions(Consumer<String> handler) {
Disposable subscription = web3jService.getWeb3j()
.pendingTransactionFlowable()
.subscribe(transaction -> {
logger.debug("Pending transaction: {}", transaction.getHash());
handler.accept(transaction.getHash());
});
eventSubscriptions.put("PENDING_TXS", subscription);
logger.info("Started listening to pending transactions");
}
/**
* Stop event listener
*/
public void stopListener(String listenerId) {
Disposable subscription = eventSubscriptions.get(listenerId);
if (subscription != null && !subscription.isDisposed()) {
subscription.dispose();
eventSubscriptions.remove(listenerId);
logger.info("Stopped listener: {}", listenerId);
}
}
/**
* Stop all listeners
*/
@PreDestroy
public void stopAllListeners() {
eventSubscriptions.forEach((id, subscription) -> {
if (!subscription.isDisposed()) {
subscription.dispose();
}
});
eventSubscriptions.clear();
logger.info("Stopped all event listeners");
}
}
2. Transaction Monitoring Service
@Service
public class TransactionMonitorService {
private static final Logger logger = LoggerFactory.getLogger(TransactionMonitorService.class);
private final Web3jService web3jService;
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2);
public TransactionMonitorService(Web3jService web3jService) {
this.web3jService = web3jService;
}
/**
* Wait for transaction receipt with timeout
*/
public TransactionReceipt waitForTransactionReceipt(String transactionHash,
long timeoutSeconds) {
try {
Optional<TransactionReceipt> receipt = web3jService.getWeb3j()
.ethGetTransactionReceipt(transactionHash)
.send()
.getTransactionReceipt();
if (receipt.isPresent()) {
return receipt.get();
}
// Poll for receipt
long startTime = System.currentTimeMillis();
while (System.currentTimeMillis() - startTime < timeoutSeconds * 1000) {
Thread.sleep(2000); // Wait 2 seconds between checks
receipt = web3jService.getWeb3j()
.ethGetTransactionReceipt(transactionHash)
.send()
.getTransactionReceipt();
if (receipt.isPresent()) {
return receipt.get();
}
logger.debug("Waiting for transaction receipt: {}", transactionHash);
}
throw new TransactionTimeoutException(
"Transaction receipt not received within timeout: " + transactionHash);
} catch (Exception e) {
logger.error("Failed to get transaction receipt: {}", transactionHash, e);
throw new TransactionMonitorException("Failed to monitor transaction", e);
}
}
/**
* Check transaction status
*/
public TransactionStatus getTransactionStatus(String transactionHash) {
try {
Optional<TransactionReceipt> receipt = web3jService.getWeb3j()
.ethGetTransactionReceipt(transactionHash)
.send()
.getTransactionReceipt();
if (receipt.isPresent()) {
TransactionReceipt txReceipt = receipt.get();
return TransactionStatus.builder()
.hash(transactionHash)
.status(txReceipt.isStatusOK() ? "SUCCESS" : "FAILED")
.blockHash(txReceipt.getBlockHash())
.blockNumber(txReceipt.getBlockNumber())
.gasUsed(txReceipt.getGasUsed())
.build();
} else {
return TransactionStatus.builder()
.hash(transactionHash)
.status("PENDING")
.build();
}
} catch (Exception e) {
logger.error("Failed to get transaction status: {}", transactionHash, e);
throw new TransactionMonitorException("Failed to get transaction status", e);
}
}
@Data
@Builder
public static class TransactionStatus {
private String hash;
private String status; // PENDING, SUCCESS, FAILED
private String blockHash;
private BigInteger blockNumber;
private BigInteger gasUsed;
}
}
Configuration & Properties
application.yml
# Ethereum Configuration
ethereum:
node:
url: ${ETHEREUM_NODE_URL:https://mainnet.infura.io/v3/your-project-id}
network: ${ETHEREUM_NETWORK:mainnet}
wallet:
private-key: ${WALLET_PRIVATE_KEY:}
address: ${WALLET_ADDRESS:}
contracts:
uniswap-router: "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D"
weth: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"
gas:
multiplier: 1.1
priority: "fast"
# Application Settings
app:
transaction:
timeout-seconds: 180
confirmation-blocks: 12
monitoring:
enabled: true
block-interval-ms: 15000
# Logging
logging:
level:
org.web3j: INFO
com.yourcompany.ethereum: DEBUG
Configuration Class
@Configuration
@ConfigurationProperties(prefix = "ethereum")
@Data
public class EthereumConfig {
private Node node = new Node();
private Wallet wallet = new Wallet();
private Contracts contracts = new Contracts();
private Gas gas = new Gas();
@Data
public static class Node {
private String url;
private String network;
}
@Data
public static class Wallet {
private String privateKey;
private String address;
}
@Data
public static class Contracts {
private String uniswapRouter;
private String weth;
}
@Data
public static class Gas {
private double multiplier;
private String priority;
}
}
Security & Best Practices
1. Secure Credential Management
@Service
public class SecureCredentialService {
private static final Logger logger = LoggerFactory.getLogger(SecureCredentialService.class);
/**
* Load private key from secure source
*/
public Credentials loadCredentials() {
try {
String privateKey = getPrivateKeyFromSecureSource();
if (privateKey == null || privateKey.trim().isEmpty()) {
throw new SecurityException("Private key not found");
}
// Validate private key format
if (!privateKey.startsWith("0x")) {
privateKey = "0x" + privateKey;
}
if (privateKey.length() != 66) { // 32 bytes + 0x prefix
throw new SecurityException("Invalid private key length");
}
return Credentials.create(privateKey);
} catch (Exception e) {
logger.error("Failed to load credentials", e);
throw new SecurityException("Credential loading failed", e);
}
}
private String getPrivateKeyFromSecureSource() {
// Priority order for private key sources
return Optional.ofNullable(System.getenv("WALLET_PRIVATE_KEY"))
.or(() -> Optional.ofNullable(System.getProperty("wallet.private-key")))
.orElseThrow(() -> new SecurityException("No private key source found"));
}
/**
* Validate address format
*/
public boolean isValidAddress(String address) {
if (address == null || !address.startsWith("0x") || address.length() != 42) {
return false;
}
try {
// Basic hex validation
return address.matches("^0x[0-9a-fA-F]{40}$");
} catch (Exception e) {
return false;
}
}
}
2. Gas Optimization Service
@Service
public class GasOptimizationService {
private final Web3jService web3jService;
public GasOptimizationService(Web3jService web3jService) {
this.web3jService = web3jService;
}
/**
* Calculate optimal gas price based on network conditions
*/
public BigInteger calculateOptimalGasPrice() {
try {
// Get current gas price from network
BigInteger currentGasPrice = web3jService.getGasPrice();
// Apply multiplier based on priority
double multiplier = getGasMultiplier();
BigInteger optimalGasPrice = currentGasPrice
.multiply(BigInteger.valueOf((long) (multiplier * 100)))
.divide(BigInteger.valueOf(100));
logger.debug("Optimal gas price calculated: {} (base: {})",
optimalGasPrice, currentGasPrice);
return optimalGasPrice;
} catch (Exception e) {
logger.warn("Failed to calculate optimal gas price, using default", e);
return new DefaultGasProvider().getGasPrice();
}
}
/**
* Estimate gas limit for transaction
*/
public BigInteger estimateGasLimit(Transaction transaction) {
try {
// This would typically call eth_estimateGas
// For simplicity, returning a safe default
return BigInteger.valueOf(21000); // Base gas limit
} catch (Exception e) {
logger.warn("Failed to estimate gas limit, using default", e);
return new DefaultGasProvider().getGasLimit();
}
}
private double getGasMultiplier() {
// Could be configured based on transaction priority
return 1.1; // 10% above current gas price
}
}
Error Handling & Exceptions
public class EthereumException extends RuntimeException {
public EthereumException(String message) {
super(message);
}
public EthereumException(String message, Throwable cause) {
super(message, cause);
}
}
public class ContractCallException extends EthereumException {
public ContractCallException(String message) {
super(message);
}
public ContractCallException(String message, Throwable cause) {
super(message, cause);
}
}
public class TransactionTimeoutException extends EthereumException {
public TransactionTimeoutException(String message) {
super(message);
}
}
public class InsufficientFundsException extends EthereumException {
public InsufficientFundsException(String message) {
super(message);
}
}
@ControllerAdvice
public class EthereumExceptionHandler {
@ExceptionHandler(ContractCallException.class)
public ResponseEntity<ErrorResponse> handleContractCallException(ContractCallException e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(ErrorResponse.builder()
.error("CONTRACT_CALL_ERROR")
.message(e.getMessage())
.timestamp(Instant.now())
.build());
}
@ExceptionHandler(TransactionTimeoutException.class)
public ResponseEntity<ErrorResponse> handleTransactionTimeout(TransactionTimeoutException e) {
return ResponseEntity.status(HttpStatus.REQUEST_TIMEOUT)
.body(ErrorResponse.builder()
.error("TRANSACTION_TIMEOUT")
.message(e.getMessage())
.timestamp(Instant.now())
.build());
}
@ExceptionHandler(InsufficientFundsException.class)
public ResponseEntity<ErrorResponse> handleInsufficientFunds(InsufficientFundsException e) {
return ResponseEntity.status(HttpStatus.PAYMENT_REQUIRED)
.body(ErrorResponse.builder()
.error("INSUFFICIENT_FUNDS")
.message(e.getMessage())
.timestamp(Instant.now())
.build());
}
@Data
@Builder
public static class ErrorResponse {
private String error;
private String message;
private Instant timestamp;
}
}
This comprehensive implementation provides a robust foundation for interacting with Ethereum smart contracts from Java applications, covering everything from basic token operations to advanced DeFi protocols and NFT management.