blob: ee0e08d2f4b4232198095fea7147bd65a094cd68 [file] [log] [blame]
/*
* Copyright (c) Robert Bosch GmbH. All rights reserved.
*/
package org.eclipse.blockchain.core;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.reflect.InvocationTargetException;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import org.apache.commons.io.FileUtils;
import org.eclipse.blockchain.classloader.DynamicClassLoader;
import org.web3j.crypto.Credentials;
import org.web3j.crypto.WalletUtils;
import org.web3j.protocol.Web3j;
import org.web3j.protocol.admin.Admin;
import org.web3j.protocol.admin.methods.response.NewAccountIdentifier;
import org.web3j.protocol.core.DefaultBlockParameterName;
import org.web3j.protocol.core.DefaultBlockParameterNumber;
import org.web3j.protocol.core.RemoteCall;
import org.web3j.protocol.core.methods.response.EthGetTransactionReceipt;
import org.web3j.protocol.core.methods.response.Transaction;
import org.web3j.protocol.core.methods.response.TransactionReceipt;
import org.web3j.protocol.ipc.WindowsIpcService;
import org.web3j.tx.gas.ContractGasProvider;
import org.web3j.tx.gas.DefaultGasProvider;
import org.web3j.utils.Convert;
import org.web3j.utils.Convert.Unit;
import org.web3j.utils.Numeric;
/**
* @author ADG5COB
*/
public class Web3jHandler {
private final Account personalAccount = new Account();
private static Web3jHandler web3j;
private final List<String> accountPassword = new ArrayList<>();
private final Map<String, Class<?>> contractMap = new HashMap<>();
private final Map<String, Object> classInstanceMap = new HashMap<>();
private Admin admin;
private final List<Class<?>> deployMethodArgTypes =
Arrays.asList(Web3j.class, Credentials.class, ContractGasProvider.class);
/**
* @return the admin
*/
public Admin getAdmin() {
return this.admin;
}
/**
* @param admin the admin to set
*/
public void setAdmin(final Admin admin) {
this.admin = admin;
}
private Web3jHandler() {}
/**
* @return -
*/
public static Web3jHandler getInstance() {
if (web3j == null) {
web3j = new Web3jHandler();
}
if (web3j.accountPassword.isEmpty()) {
web3j.accountPassword.add("123");
web3j.accountPassword.add("123");
web3j.accountPassword.add("123");
}
return web3j;
}
/**
* Before calling this method make sure Geth server is started
*
* @throws IOException -
* @throws InterruptedException -
* @throws ExecutionException -
*/
public String createInitialAccounts() throws IOException, InterruptedException, ExecutionException {
while (true) {
if (CoreCommandExecutor.getInstance().isGethStarted()) {
break;
}
if (!CoreCommandExecutor.getInstance().getGethServerStartError().replace("Geth", "").isEmpty()) {
return CoreCommandExecutor.getInstance().getGethServerStartError().replace("Geth", "");
}
Thread.sleep(500);
}
this.admin = getAdminInstance();
setAdmin(this.admin);
List<String> result = this.admin.personalListAccounts().send().getResult();
if (result.isEmpty()) {
// Then no accounts in the data directory create new accounts
NewAccountIdentifier account1 = this.admin.personalNewAccount("123").send();
NewAccountIdentifier account2 = this.admin.personalNewAccount("123").send();
NewAccountIdentifier account3 = this.admin.personalNewAccount("123").send();
Map<String, String> accountsMap = new LinkedHashMap<>();
accountsMap.put(account1.getAccountId(), "123");
accountsMap.put(account2.getAccountId(), "123");
accountsMap.put(account3.getAccountId(), "123");
this.personalAccount.setDataDir(CoreCommandExecutor.getInstance().getDataDir());
this.personalAccount.setAccounts(accountsMap);
// Terminate geth server
CoreCommandExecutor.getInstance().terminateGethServer();
while (true) {
if (!CoreCommandExecutor.getInstance().isGethStarted()) {
break;
}
Thread.sleep(500);
}
// Start new geth server with new accounts created above
CoreCommandExecutor.getInstance().startGethServer(CoreCommandExecutor.getInstance().getDataDir(),
createGethInitFile(), CoreCommandExecutor.getInstance().getGethOptions());
while (true) {
if (CoreCommandExecutor.getInstance().isGethStarted()) {
break;
}
if (!CoreCommandExecutor.getInstance().getGethServerStartError().replace("Geth", "").isEmpty()) {
return CoreCommandExecutor.getInstance().getGethServerStartError().replace("Geth", "");
}
Thread.sleep(500);
}
Map<String, String> balanceMap = new LinkedHashMap<>();
balanceMap.put(account1.getAccountId(), convertToEther(this.admin
.ethGetBalance(account1.getAccountId(), new DefaultBlockParameterNumber(0)).send().getBalance().toString()));
balanceMap.put(account2.getAccountId(), convertToEther(this.admin
.ethGetBalance(account2.getAccountId(), new DefaultBlockParameterNumber(0)).send().getBalance().toString()));
balanceMap.put(account3.getAccountId(), convertToEther(this.admin
.ethGetBalance(account3.getAccountId(), new DefaultBlockParameterNumber(0)).send().getBalance().toString()));
this.personalAccount.setAccountBalance(balanceMap);
// Unlock newly created accounts
this.admin.personalUnlockAccount(account1.getAccountId(), "123");
this.admin.personalUnlockAccount(account2.getAccountId(), "123");
this.admin.personalUnlockAccount(account3.getAccountId(), "123");
}
else {
// Accounts are already present load them
Map<String, String> accountsMap = new LinkedHashMap<>();
Map<String, String> balanceMap = new LinkedHashMap<>();
String[] account = new String[3];
for (int i = 0; i < result.size(); i++) {
accountsMap.put(result.get(i), this.accountPassword.get(i));
account[i] = result.get(i);
}
balanceMap.put(account[0], convertToEther(this.admin.ethGetBalance(account[0], DefaultBlockParameterName.LATEST)
.sendAsync().get().getBalance().toString()));
balanceMap.put(account[1], convertToEther(this.admin.ethGetBalance(account[1], DefaultBlockParameterName.LATEST)
.sendAsync().get().getBalance().toString()));
balanceMap.put(account[2], convertToEther(this.admin.ethGetBalance(account[2], DefaultBlockParameterName.LATEST)
.sendAsync().get().getBalance().toString()));
this.personalAccount.setAccountBalance(balanceMap);
this.personalAccount.setDataDir(CoreCommandExecutor.getInstance().getDataDir());
this.personalAccount.setAccounts(accountsMap);
this.admin.personalUnlockAccount(account[0], "123");
this.admin.personalUnlockAccount(account[1], "123");
this.admin.personalUnlockAccount(account[2], "123");
}
// add listener for tranactions
addListenerForTransactions(); // - Commented this since it throwing some exception
return "";
}
/**
* @param projectName -
* @param scName -
* @param solOutputDir -
* @param solidityFileAbsPath -
* @param values -
* @param accountDetails -
* @return -
* @throws Exception -
*/
public String deploySmartContract(final String projectName, final String scName, final String solOutputDir,
final String solidityFileAbsPath, final String values, final String accountDetails)
throws Exception {
try {
if (!CoreCommandExecutor.getInstance().isGethStarted()) {
return "Geth Server is not started";
}
Class<?> contract;
Object classInstance;
Web3j web3jLocal = getWeb3jInstance();
Map<String, String> accounts = this.personalAccount.getAccounts();
Iterator<String> iterator = accounts.keySet().iterator();
String accountInfo = null;
while (iterator.hasNext()) {
String account = iterator.next();
if (account.equals(accountDetails)) {
accountInfo = account;
break;
}
}
if (accountInfo != null) {
String[] credentials = getWalletFile(accountInfo.replace("0x", ""),
this.personalAccount.getDataDir() + File.separator + "keystore").split("~");
Credentials creds = WalletUtils.loadCredentials(credentials[1], credentials[0]);
ContractGasProvider contractGasProvider = new DefaultGasProvider();
DynamicClassLoader dcl = new DynamicClassLoader(this.getClass().getClassLoader(), projectName, solOutputDir);
contract = dcl.loadClass("com.bosch." + scName);
SolidityABI[] solABI = CoreCommandExecutor.getInstance().getSolABIMap(solidityFileAbsPath);
Class<?>[] cls = getConstructorArgTypes(solABI);
Object[] objs = getConstructorArgsAsObject(projectName, solABI, web3jLocal, creds, contractGasProvider, values);
RemoteCall<?> dep = (RemoteCall<?>) contract.getMethod("deploy", cls).invoke(contract, objs);
CoreCommandExecutor.getInstance().mine(true);
classInstance = dep.send();
EthereumProject ethProject = ProjectCreator.getInstance().getEthProject(projectName);
ethProject
.createDeploymentModel(
contract.getMethod("getContractAddress").invoke(classInstance).toString(), ethProject.getProject()
.getFile(solidityFileAbsPath.replace(ethProject.getProject().getLocation().toOSString(), "")),
accountInfo);
this.contractMap.put(solidityFileAbsPath, contract);
this.classInstanceMap.put(solidityFileAbsPath, classInstance);
}
}
finally {
updateAccBalance();
CoreCommandExecutor.getInstance().mine(false);
}
return "";
}
/**
* @throws IOException
* @throws ExecutionException
* @throws InterruptedException
*/
private void updateAccBalance() throws InterruptedException, ExecutionException {
Map<String, String> accountBalance = this.personalAccount.getAccountBalance();
Iterator<String> iterator = accountBalance.keySet().iterator();
while (iterator.hasNext()) {
String accountID = iterator.next();
accountBalance.put(accountID, convertToEther(this.admin.ethGetBalance(accountID, DefaultBlockParameterName.LATEST)
.sendAsync().get().getBalance().toString()));
}
this.personalAccount.setAccountBalance(accountBalance);
}
private Class<?>[] getConstructorArgTypes(final SolidityABI[] solABI) {
Class<?>[] classType = null;
for (SolidityABI sAbi : solABI) {
if (sAbi.getType().equalsIgnoreCase("constructor")) {
int inpsLength = sAbi.getInputs().length;
classType = new Class<?>[inpsLength + 3];
classType[0] = Web3j.class;
classType[1] = Credentials.class;
classType[2] = ContractGasProvider.class;
for (int i = 3; i < (inpsLength + 3); i++) {
classType[i] = sAbi.getInputs()[(i - 3)].getJavaType();
}
}
}
if (classType == null) {// Contracts w/o constructor
classType = new Class<?>[3];
classType[0] = Web3j.class;
classType[1] = Credentials.class;
classType[2] = ContractGasProvider.class;
}
return classType;
}
private Object[] getConstructorArgsAsObject(final String projectName, final SolidityABI[] solABI,
final Web3j web3jLocal, final Credentials creds, final ContractGasProvider contractGasProvider,
final String values) {
Object[] objs = null;
String[] inputVals = values.split(";");
for (SolidityABI sAbi : solABI) {
if (sAbi.getType().equalsIgnoreCase("constructor")) {
int inpsLength = sAbi.getInputs().length;
objs = new Object[inpsLength + 3];
objs[0] = web3jLocal;
objs[1] = creds;
objs[2] = contractGasProvider;
for (int i = 3; i < (inpsLength + 3); i++) {
objs[i] = getVariable(projectName, inputVals[(i - 3)], sAbi.getInputs()[(i - 3)].getJavaType(),
sAbi.getInputs()[(i - 3)].getCastType(), sAbi.getInputs()[(i - 3)].getIsArray());
}
}
}
if (objs == null) {// Contracts w/o constructor
objs = new Object[3];
objs[0] = web3jLocal;
objs[1] = creds;
objs[2] = contractGasProvider;
}
return objs;
}
private Object getVariable(final String projectName, final String value, final Class<?> type, final Class<?> castType,
final boolean isArray) {
Object castValue = null;
try {
castValue = SolidityDynamicValueCasterHandler.getValue(projectName, value, (isArray ? castType : type), isArray);
}
catch (ClassNotFoundException | IllegalAccessException | InvocationTargetException | NoSuchMethodException
| IOException | InterruptedException e) {
BlockchainCore.getInstance().logException("", e.getMessage(), e);
}
return castValue;
}
/**
* This method is used to invoke the smart contract function...
*
* @param projectName -
* @param funcName
* @param scAbsPath
* @param values - The input params for the function to be invoked, should be a comma separated value.
* @param accountDetails -
* @return
* @throws Exception
*/
public String invokeSCFunction(final String projectName, final String funcName, final String scAbsPath,
final String values, final String accountDetails)
throws Exception {
String response = "";
Object responseObject = null;
boolean returnPresent = false;
Class<?> contract = this.contractMap.get(scAbsPath);
Object classInstance = this.classInstanceMap.get(scAbsPath);
try {
String[] funcDetails = funcName.split("-");
String argList = "";
if (funcDetails.length > 1) {
argList = funcDetails[1];
}
SolidityABI[] solABI = CoreCommandExecutor.getInstance().getSolABIMap(scAbsPath);
String[] inputVals = new String[0];
if (!values.isEmpty()) {
inputVals = values.split(";");
}
SolidityABI masterSolABI = getSolABI(solABI, funcDetails[0], argList, inputVals);
if (masterSolABI != null) {
Class<?>[] funcArgs = getFuncArgs(masterSolABI);
EthereumProject ethProject = ProjectCreator.getInstance().getEthProject(projectName);
// Re-Load contract with new credentials - if different account is chosen
String lastUsedAccount = ethProject.getLastUsedAccount(
ethProject.getProject().getFile(scAbsPath.replace(ethProject.getProject().getLocation().toOSString(), "")));
if (!accountDetails.equals(lastUsedAccount)) {
String[] credentials = getWalletFile(accountDetails.replace("0x", ""),
this.personalAccount.getDataDir() + File.separator + "keystore").split("~");
Web3j web3jInstance = getWeb3jInstance();
Credentials creds = WalletUtils.loadCredentials(credentials[1], credentials[0]);
ContractGasProvider contractGasProvider = new DefaultGasProvider();
classInstance =
contract.getMethod("load", String.class, Web3j.class, Credentials.class, ContractGasProvider.class)
.invoke(classInstance, ethProject.getContractAddress(ethProject.getProject().getFile(scAbsPath)),
web3jInstance, creds, contractGasProvider);
ethProject.updateLastUsedAccount(ethProject.getProject().getFile(scAbsPath), accountDetails);
}
RemoteCall<?> dep = (RemoteCall<?>) contract.getMethod(funcDetails[0], funcArgs).invoke(classInstance,
getFuncValues(projectName, values, masterSolABI.getInputs()));
returnPresent = doesFunctionReturnSomething(masterSolABI);
CoreCommandExecutor.getInstance().mine(true);
// response = dep.send().toString();
responseObject = dep.send();
// responseObject = SolidityDynamicValueCasterHandler.getValue(projectName, valueToCast, actualType, isArray)
if (returnPresent && masterSolABI.getOutputs()[0].getJavaType().equals(List.class) &&
masterSolABI.getOutputs()[0].getCastType().getSimpleName().contains("byte")) {
List<byte[]> bytes = (List<byte[]>) responseObject;
for (byte[] b : bytes) {
response += Numeric.toHexString(b) + ";";
}
}
else if (returnPresent && masterSolABI.getOutputs()[0].getJavaType().getSimpleName().contains("byte")) {
response = Numeric.toHexString((byte[]) responseObject);
}
else {
response = responseObject.toString();
}
}
else {
returnPresent = true;
response = "Something is not rite. Please make sure you pass the correct arguments";
}
}
finally {
updateAccBalance();
CoreCommandExecutor.getInstance().mine(false);
}
return (returnPresent) ? response : "DONE";
}
/**
* As of now restricted to String check....
*
* @param masterSolABI
* @param funcName
* @return
*/
private boolean doesFunctionReturnSomething(final SolidityABI masterSolABI) {
if (masterSolABI.getOutputs().length > 0) {
for (IOABI iob : masterSolABI.getOutputs()) {
if ((iob.getJavaType() != null) && !iob.getJavaType().getSimpleName().isEmpty()) {
return true;
}
}
return false;
}
return false;
}
private Class<?>[] getFuncArgs(final SolidityABI solABI) {
Class<?>[] argTypes = null;
IOABI[] inputs = solABI.getInputs();
argTypes = new Class[inputs.length];
for (int i = 0; i < inputs.length; i++) {
argTypes[i] = inputs[i].getJavaType();
}
return argTypes;
}
private SolidityABI getSolABI(final SolidityABI solABI[], final String funcName, final String argsList,
final String[] inputVals) {
List<String> inputArgs = new ArrayList<>();
if (argsList.contains(";")) {
inputArgs = Arrays.asList(argsList.split(";"));
}
else if (!argsList.isEmpty()) {
inputArgs.add(argsList);
}
for (SolidityABI sAbi : solABI) {
if (sAbi.getName().equalsIgnoreCase(funcName) && (sAbi.getInputs().length == inputVals.length)) {
List<String> argVals = new ArrayList<>();
for (IOABI input : sAbi.getInputs()) {
argVals.add(input.getJavaType().getSimpleName());
}
if (inputArgs.equals(argVals)) {
return sAbi;
}
}
}
return null;
}
private Object[] getFuncValues(final String projectName, final String values, final IOABI[] ioabi) {
if (values.isEmpty()) {
return null;
}
String[] inputVals = values.split(";");
Object[] vals = new Object[inputVals.length];
for (int i = 0; i < inputVals.length; i++) {
vals[i] =
getVariable(projectName, inputVals[i], ioabi[i].getJavaType(), ioabi[i].getCastType(), ioabi[i].getIsArray());
}
return vals;
}
private String convertToEther(final String wei) {
return Convert.fromWei(wei, Unit.ETHER).toString();
}
private String createGethInitFile() throws IOException {
String dataDir = CoreCommandExecutor.getInstance().getDataDir();
clearOldDataDir(dataDir);
File gethInitFile = new File(dataDir + File.separator + "genesis.json");
gethInitFile.createNewFile();// NOSONAR
List<String> accounts = new ArrayList<>(this.personalAccount.accounts.keySet());
try (
BufferedReader br = new BufferedReader(
new InputStreamReader(CoreCommandExecutor.class.getResourceAsStream("genesis2.template")));
BufferedWriter bw = new BufferedWriter(new FileWriter(gethInitFile));) {
String genesisContent = "";
while ((genesisContent = br.readLine()) != null) {
if (genesisContent.contains("acc")) {
int index = Integer
.parseInt(genesisContent.substring(genesisContent.indexOf("acc") + 3, genesisContent.indexOf("acc") + 4));
genesisContent = genesisContent.replace("acc" + index, accounts.get(index));
}
bw.append(genesisContent);
bw.append(System.lineSeparator());
}
}
return gethInitFile.getAbsolutePath();
}
private static void clearOldDataDir(final String dataDir) throws IOException {
FileUtils.deleteDirectory(new File(dataDir + File.separator + "geth"));
new File(dataDir + File.separator + "genesis.json").delete();// NOSONAR
}
/**
* Returns account wallet file along with its password walletFile~password
*
* @param accId
* @param keyStore
* @return
*/
private String getWalletFile(final String accId, final String keyStore) {
File directory = new File(keyStore);
File[] listFiles = directory.listFiles();
for (File f : listFiles) {
if (f.getName().contains(accId)) {
return f.getAbsolutePath() + "~" + this.personalAccount.accounts.get("0x" + accId);
}
}
return "";
}
/**
* @return - The personal accounts
*/
public Account getAccount() {
return this.personalAccount;
}
private Web3j getWeb3jInstance() {
return Web3j.build(new WindowsIpcService(CoreCommandExecutor.getInstance().getIPCPath()));
}
private Admin getAdminInstance() {
return Admin.build(new WindowsIpcService(CoreCommandExecutor.getInstance().getIPCPath()));
}
/**
* @return -
*/
public BigInteger getGasPrice() {
BigInteger gasPrice = null;
try {
gasPrice = getAdmin().ethGasPrice().send().getGasPrice();
}
catch (IOException e) {
BlockchainCore.getInstance().logException("", e.getMessage(), e);
}
return gasPrice;
}
private void addListenerForTransactions() {
getWeb3jInstance().transactionFlowable().subscribe(tx -> {
addToGlobalTransactionMap(tx);
});
}
/**
* Get transaction receipt for already mined transaction
*
* @param transactionHash - The transaction hash
* @return - The transaction receipt for the requested transaction
* @throws IOException - Is thrown if socket connection get terminated
*/
public String getTransactionReceiptAsString(final String transactionHash) throws IOException {
EthGetTransactionReceipt transactionReceipt = getWeb3jInstance().ethGetTransactionReceipt(transactionHash).send();
return getTransactionReceiptMessage(transactionReceipt.getResult());
}
private String getTransactionReceiptMessage(final TransactionReceipt transactionReceipt) {
return "Transaction Hash :" + transactionReceipt.getTransactionHash() + System.lineSeparator() +
"Transaction Index :" + transactionReceipt.getTransactionIndex() + System.lineSeparator() + "Block Hash :" +
transactionReceipt.getBlockHash() + System.lineSeparator() + "Block Number :" +
transactionReceipt.getBlockNumber() + System.lineSeparator() + "Cumulative Gas Used :" +
transactionReceipt.getCumulativeGasUsed() + System.lineSeparator() + "Gas Used :" +
transactionReceipt.getGasUsed() + System.lineSeparator() + "Contract Address :" +
transactionReceipt.getContractAddress() + System.lineSeparator() + "Root :" + transactionReceipt.getRoot() +
System.lineSeparator() + "Status :" + transactionReceipt.getStatus() + System.lineSeparator() + "From :" +
transactionReceipt.getFrom() + System.lineSeparator() + "To :" + transactionReceipt.getTo() +
System.lineSeparator() + "Logs :" + transactionReceipt.getLogs().toString() + System.lineSeparator() +
"Logs Bloom :" + transactionReceipt.getLogsBloom() + System.lineSeparator();
}
/**
* @param tx
*/
private void addToGlobalTransactionMap(final Transaction tx) {
// create an intermediate transaction model with the actual transaction data
TransactionModel transactionModel = new TransactionModel();
transactionModel.setValues(tx);
// refresh the transaction view with data
BlockchainViewsRegistry.getListOFViews().stream().forEach(view -> view.updateView(transactionModel));
}
}