Create a gist now

Instantly share code, notes, and snippets.

What would you like to do?
Sample EthereumJ transaction sending
package com.mypackage.ethereumsync;
import java.io.File;
import java.math.BigInteger;
import java.util.Base64;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import org.apache.commons.codec.binary.Hex;
import org.ethereum.config.SystemProperties;
import org.ethereum.core.Transaction;
import org.ethereum.core.TransactionReceipt;
import org.ethereum.crypto.ECKey;
import org.ethereum.db.ByteArrayWrapper;
import org.ethereum.facade.Ethereum;
import org.ethereum.facade.EthereumFactory;
import org.ethereum.listener.EthereumListenerAdapter;
import org.ethereum.util.ByteUtil;
import org.ethereum.util.RLP;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.type.AnnotatedTypeMetadata;
import org.springframework.scheduling.annotation.AsyncResult;
import org.springframework.stereotype.Service;
import com.google.common.annotations.VisibleForTesting;
@Service
public class EthereumService {
private static final Logger logger = LoggerFactory.getLogger(EthereumService.class);
private Map<ByteArrayWrapper, TransactionReceipt> txWaiters =
Collections.synchronizedMap(new HashMap<ByteArrayWrapper, TransactionReceipt>());
/**
* We have two nodes so that we can send ETH from one to the other and vice versa
*/
private Ethereum ethereum1;
private Ethereum ethereum2;
private CountDownLatch readyLatch = new CountDownLatch(2);
private ExecutorService starter;
private Random random = new Random();
private Future<?> initFuture;
@PostConstruct
public void init() throws Exception {
starter = Executors.newSingleThreadExecutor();
initFuture = starter.submit(this::start);
}
@PreDestroy
public void destroy() {
starter.shutdown();
ethereum1.close();
ethereum2.close();
}
public void start() {
ethereum1 = EthereumFactory.createEthereum(EthereumConfig1.class);
ethereum1.addListener(new EthereumListener(this));
ethereum2 = EthereumFactory.createEthereum(EthereumConfig2.class);
ethereum2.addListener(new EthereumListener(this));
}
public Future<TransactionReceipt> storeHashInEthereum(String hash) {
try {
// both clients must be synced before we proceed with storing anything
if (EthereumConfig1.PROPERTIES.isSyncEnabled() && EthereumConfig2.PROPERTIES.isSyncEnabled()) {
readyLatch.await();
}
// we have 2 ethereum clients, and we choose randomly the sender and the receiver
SystemProperties senderProps;
SystemProperties receiverProps;
Ethereum sender;
if (random.nextBoolean()) {
logger.info("Selected client1 as sender");
sender = ethereum1;
senderProps = EthereumConfig1.PROPERTIES;
receiverProps = EthereumConfig2.PROPERTIES;
} else {
logger.info("Selected client2 as sender");
sender = ethereum2;
senderProps = EthereumConfig2.PROPERTIES;
receiverProps = EthereumConfig1.PROPERTIES;
}
byte[] senderKey = senderProps.getMyKey().getPrivKeyBytes();
byte[] receiverAddress = receiverProps.getMyKey().getAddress();
TransactionReceipt receipt = sendTxAndWait(senderKey, receiverAddress, Base64.getUrlDecoder().decode(hash), sender);
return AsyncResult.forValue(receipt);
} catch (Exception ex) {
logger.error("Failed to store hash in Ethereum", ex);
return AsyncResult.forExecutionException(ex);
}
}
private Future<TransactionReceipt> sendTxAndWait(byte[] senderPrivateKey, byte[] receiveAddress, byte[] data, Ethereum senderNode) throws InterruptedException {
byte[] fromAddress = ECKey.fromPrivate(senderPrivateKey).getAddress();
ether
BigInteger nonce = senderNode.getRepository().getNonce(fromAddress);
Integer chainId = senderNode.getChainIdForNextBlock();
Transaction tx = new Transaction(
ByteUtil.bigIntegerToBytes(nonce),
ByteUtil.longToBytesNoLeadZeroes(senderNode.getGasPrice()),
ByteUtil.longToBytesNoLeadZeroes(200000),
receiveAddress,
ByteUtil.bigIntegerToBytes(BigInteger.valueOf(1)), // 1 gwei
data,
chainId);
tx.sign(ECKey.fromPrivate(senderPrivateKey));
logger.info("Raw transaction: 0x{}", Hex.encodeHexString(tx.getEncoded()));
new Transaction(getEncodedSignedRaw(tx)).verify();
senderNode.submitTransaction(tx);
logger.info("<=== Sending transaction: " + tx);
return waitForTx(tx.getHash(), senderNode);
}
private TransactionReceipt waitForTx(byte[] txHash, Ethereum senderNode) throws InterruptedException {
ByteArrayWrapper txHashW = new ByteArrayWrapper(txHash);
txWaiters.put(txHashW, null);
long startBlock = senderNode.getBlockchain().getBestBlock().getNumber();
while(true) {
TransactionReceipt receipt = txWaiters.get(txHashW);
if (receipt != null) {
return receipt;
} else {
long curBlock = senderNode.getBlockchain().getBestBlock().getNumber();
if (curBlock > startBlock + 16) {
throw new RuntimeException("The transaction was not included during last 16 blocks: " + txHashW.toString().substring(0,8));
} else {
logger.info("Waiting for block with transaction 0x" + txHashW.toString().substring(0,8) +
" included (" + (curBlock - startBlock) + " blocks received so far) ...");
}
}
synchronized (txHashW) {
txHashW.wait(20000);
}
}
}
public Ethereum getEthereum1() {
return ethereum1;
}
public Ethereum getEthereum2() {
return ethereum2;
}
@VisibleForTesting
public void awaitInitialization() throws InterruptedException, ExecutionException {
initFuture.get();
}
public static class EthereumListener extends EthereumListenerAdapter {
private EthereumService service;
public EthereumListener(EthereumService ethereumService) {
this.service = ethereumService;
}
@Override
public void onSyncDone(SyncState state) {
logger.info("Sync done: state={}", state);
service.readyLatch.countDown();
}
}
/**
* Configuring two clients requires separate configuration classes with the same functionality
*
* @author bozho
*
*/
@Configuration
@Conditional(value=ConfigurationRegistrationCondition.class)
@PropertySource("classpath:/ethereumsync.properties")
public static class EthereumConfig1 {
public static SystemProperties PROPERTIES;
@Value("${ethereumj.config.path}")
private String configPath;
@Bean
public SystemProperties systemProperties() {
if (PROPERTIES == null) {
PROPERTIES = new SystemProperties(new File(configPath + "/user1.conf"));
logger.info("Address for client1: 0x{}", Hex.encodeHexString(PROPERTIES.getMyKey().getAddress()));
}
return PROPERTIES;
}
}
@Configuration
@Conditional(value=ConfigurationRegistrationCondition.class)
@PropertySource("classpath:/ethereumsync.properties")
public static class EthereumConfig2 {
public static SystemProperties PROPERTIES;
@Value("${ethereumj.config.path}")
private String configPath;
@Bean
public SystemProperties systemProperties() {
if (PROPERTIES == null) {
PROPERTIES = new SystemProperties(new File(configPath + "/user2.conf"));
logger.info("Address for client2: 0x{}", Hex.encodeHexString(PROPERTIES.getMyKey().getAddress()));
}
return PROPERTIES;
}
}
// needed in order to not register EthereumConfigs with the main context, just with the nested ethereumj ones
public static class ConfigurationRegistrationCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
if (context.getRegistry().containsBeanDefinition("ethereumService")) {
return false;
}
return true;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment