com.citahub.cita.tx.Contract Maven / Gradle / Ivy
package com.citahub.cita.tx;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import com.citahub.cita.protocol.CITAj;
import com.citahub.cita.protocol.core.DefaultBlockParameterName;
import com.citahub.cita.protocol.core.RemoteCall;
import com.citahub.cita.protocol.core.methods.request.Call;
import com.citahub.cita.protocol.core.methods.response.AppCall;
import com.citahub.cita.protocol.core.methods.response.AppGetCode;
import com.citahub.cita.protocol.core.methods.response.Log;
import com.citahub.cita.protocol.core.methods.response.TransactionReceipt;
import com.citahub.cita.tx.exceptions.ContractCallException;
import com.citahub.cita.abi.EventEncoder;
import com.citahub.cita.abi.EventValues;
import com.citahub.cita.abi.FunctionEncoder;
import com.citahub.cita.abi.FunctionReturnDecoder;
import com.citahub.cita.abi.TypeReference;
import com.citahub.cita.abi.datatypes.Address;
import com.citahub.cita.abi.datatypes.Event;
import com.citahub.cita.abi.datatypes.Function;
import com.citahub.cita.abi.datatypes.Type;
import com.citahub.cita.protocol.exceptions.TransactionException;
import com.citahub.cita.utils.Numeric;
/**
* Solidity contract type abstraction for interacting with smart contracts via native Java types.
*/
@SuppressWarnings("WeakerAccess")
public abstract class Contract extends ManagedTransaction {
// https://www.reddit.com/r/ethereum/comments/5g8ia6/attention_miners_we_recommend_raising_gas_limit/
private static final String ABI_ADDRESS = "ffffffffffffffffffffffffffffffffff010001";
protected final String contractBinary;
protected String contractAddress;
protected String nonce;
protected long quota;
protected TransactionReceipt transactionReceipt;
protected Map deployedAddresses;
protected Contract(String contractBinary, String contractAddress,
CITAj citaj, TransactionManager transactionManager,
String nonce, long quota) {
super(citaj, transactionManager);
//this.contractAddress = ensResolver.resolve(contractAddress);
this.contractAddress = contractAddress;
this.contractBinary = contractBinary;
this.nonce = nonce;
this.quota = quota;
}
protected Contract(String contractBinary, String contractAddress,
CITAj citaj, TransactionManager transactionManager) {
this(contractBinary, contractAddress, citaj,
transactionManager, "", 0);
}
@Deprecated
protected Contract(String contractAddress,
CITAj citaj, TransactionManager transactionManager) {
this("", contractAddress, citaj,
transactionManager, "", 0);
}
public void setContractAddress(String contractAddress) {
this.contractAddress = contractAddress;
}
public String getContractAddress() {
return contractAddress;
}
public void setTransactionReceipt(TransactionReceipt transactionReceipt) {
this.transactionReceipt = transactionReceipt;
}
public String getContractBinary() {
return contractBinary;
}
/**
* Check that the contract deployed at the address associated with this smart contract wrapper
* is in fact the contract you believe it is.
*
* This method uses the
* eth_getCode method
* to get the contract byte code and validates it against the byte code stored in this smart
* contract wrapper.
*
* @return true if the contract is valid
* @throws IOException if unable to connect to citaj node
*/
public boolean isValid() throws IOException {
if (contractAddress.equals("")) {
throw new UnsupportedOperationException(
"Contract binary not present, you will need to regenerate your smart "
+ "contract wrapper with citaj v2.2.0+");
}
AppGetCode ethGetCode = citaj
.appGetCode(contractAddress, DefaultBlockParameterName.PENDING)
.send();
if (ethGetCode.hasError()) {
return false;
}
String code = Numeric.cleanHexPrefix(ethGetCode.getCode());
// There may be multiple contracts in the Solidity bytecode, hence we only check for a
// match with a subset
return !code.isEmpty() && contractBinary.contains(code);
}
/**
* If this Contract instance was created at deployment, the TransactionReceipt associated
* with the initial creation will be provided, e.g. via a deploy method. This will
* not persist for Contracts instances constructed via a load method.
*
* @return the TransactionReceipt generated at contract deployment
*/
public TransactionReceipt getTransactionReceipt() {
return transactionReceipt;
}
/**
* Execute constant function call - i.e. a call that does not change state of the contract
*
* @param function to call
* @return {@link List} of values returned by function call
*/
private List executeCall(
Function function) throws IOException {
String encodedFunction = FunctionEncoder.encode(function);
AppCall ethCall =
citaj.appCall(
new Call(transactionManager.getFromAddress(),
contractAddress, encodedFunction),
DefaultBlockParameterName.PENDING).send();
String value = ethCall.getValue();
return FunctionReturnDecoder.decode(value, function.getOutputParameters());
}
@SuppressWarnings("unchecked")
protected T executeCallSingleValueReturn(
Function function) throws IOException {
List values = executeCall(function);
if (!values.isEmpty()) {
return (T) values.get(0);
} else {
return null;
}
}
@SuppressWarnings("unchecked")
protected R executeCallSingleValueReturn(
Function function, Class returnType) throws IOException {
T result = executeCallSingleValueReturn(function);
if (result == null) {
throw new ContractCallException("Empty value (0x) returned from contract");
}
Object value = result.getValue();
if (returnType.isAssignableFrom(value.getClass())) {
return (R) value;
} else if (result.getClass().equals(Address.class) && returnType.equals(String.class)) {
return (R) result.toString(); // cast isn't necessary
} else {
throw new ContractCallException(
"Unable to convert response: " + value
+ " to expected type: " + returnType.getSimpleName());
}
}
protected List executeCallMultipleValueReturn(
Function function) throws IOException {
return executeCall(function);
}
protected TransactionReceipt executeTransaction(
Function function)
throws IOException, TransactionException {
return executeTransaction(function, "0");
}
private TransactionReceipt executeTransaction(
Function function, String weiValue)
throws IOException, TransactionException {
return executeTransaction(FunctionEncoder.encode(function), weiValue);
}
/**
* Given the duration required to execute a transaction.
*
* @param data to send in transaction
* @return containing our transaction receipt
* @throws IOException if the call to the node fails
* @throws TransactionException if the transaction was not mined while waiting
*/
TransactionReceipt executeTransaction(
String data, String weiValue)
throws TransactionException, IOException {
return send(contractAddress, data, quota, nonce, 0, 1, BigInteger.ONE, weiValue);
}
TransactionReceipt executeTransaction(
String data, long quota, String nonce, long validUntilBlock,
int version , BigInteger chainId, String value)
throws TransactionException, IOException {
return send(
contractAddress, data, quota, nonce, validUntilBlock, version, chainId, value);
}
TransactionReceipt uploadAbi(
String abi, long quota, String nonce, long validUntilBlock,
int version , BigInteger chainId, String value)
throws TransactionException, IOException {
String data = hex_remove_0x(contractAddress) + hex_remove_0x(bytesToHexStr(abi.getBytes()));
return send(
ABI_ADDRESS, data, quota, nonce, validUntilBlock,
version, chainId, value);
}
private String hex_remove_0x(String hex) {
if (hex.contains("0x")) {
return hex.substring(2);
}
return hex;
}
private String bytesToHexStr(byte[] byteArr) {
if (null == byteArr || byteArr.length < 1) {
return "";
}
StringBuilder sb = new StringBuilder();
for (byte t : byteArr) {
if ((t & 0xF0) == 0) {
sb.append("0");
}
sb.append(Integer.toHexString(t & 0xFF));
}
return sb.toString();
}
protected RemoteCall executeRemoteCallSingleValueReturn(Function function) {
return new RemoteCall<>(
() -> executeCallSingleValueReturn(function));
}
protected RemoteCall executeRemoteCallSingleValueReturn(
Function function, Class returnType) {
return new RemoteCall<>(
() -> executeCallSingleValueReturn(function, returnType));
}
protected RemoteCall> executeRemoteCallMultipleValueReturn(Function function) {
return new RemoteCall<>(
() -> executeCallMultipleValueReturn(function));
}
protected RemoteCall executeRemoteCallTransaction(Function function) {
return new RemoteCall<>(() -> executeTransaction(function));
}
protected RemoteCall executeRemoteCallTransaction(
Function function, String weiValue) {
return new RemoteCall<>(() -> executeTransaction(function, weiValue));
}
protected RemoteCall executeRemoteCallTransaction(
Function function, long quota, String nonce,
long validUntilBlock, int version,
BigInteger chainId, String value) {
return new RemoteCall<>(
() -> executeTransaction(
FunctionEncoder.encode(function),
quota, nonce, validUntilBlock, version, chainId, value));
}
protected RemoteCall executeUploadAbi(
String abi, long quota, String nonce,
long validUntilBlock, int version,
BigInteger chainId, String value) {
return new RemoteCall<>(
() -> executeTransaction(
abi, quota, nonce, validUntilBlock, version, chainId, value));
}
private static T create(
T contract, String binary, String encodedConstructor,
long quota, String nonce, long validUntilBlock,
int version, BigInteger chainId, String value)
throws IOException, TransactionException {
TransactionReceipt transactionReceipt =
contract.executeTransaction(
binary + encodedConstructor, quota, nonce,
validUntilBlock, version, chainId, value);
String contractAddress = transactionReceipt.getContractAddress();
if (contractAddress == null) {
throw new RuntimeException("Empty contract address returned");
}
contract.setContractAddress(contractAddress);
contract.setTransactionReceipt(transactionReceipt);
return contract;
}
private static T create(
T contract, String binary, String encodedConstructor,
long quota, String nonce, long validUntilBlock,
int version, BigInteger chainId, String value, String abi)
throws IOException, TransactionException {
TransactionReceipt transactionReceipt =
contract.executeTransaction(
binary + encodedConstructor, quota, nonce,
validUntilBlock, version, chainId, value);
String contractAddress = transactionReceipt.getContractAddress();
if (contractAddress == null) {
throw new RuntimeException("Empty contract address returned");
}
contract.setContractAddress(contractAddress);
contract.setTransactionReceipt(transactionReceipt);
if(abi != null && abi.length() > 0) {
contract.uploadAbi(abi, quota, nonce,
validUntilBlock + 88, version, chainId, value);
}
return contract;
}
protected static T deploy(
Class type,
CITAj citaj, TransactionManager transactionManager,
long quota, String nonce, long validUntilBlock,
int version, String binary, BigInteger chainId,
String value, String encodedConstructor)
throws IOException, TransactionException {
try {
Constructor constructor = type.getDeclaredConstructor(
String.class,
CITAj.class, TransactionManager.class);
constructor.setAccessible(true);
// we want to use null here to ensure that "to" parameter on message is not populated
// Unfortunately, we need empty string(not null) that represent create contract
T contract = constructor.newInstance(
"", citaj, transactionManager);
return create(contract, binary, encodedConstructor, quota, nonce,
validUntilBlock, version, chainId, value);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
protected static T deploy(
Class type,
CITAj citaj, TransactionManager transactionManager,
long quota, String nonce, long validUntilBlock,
int version, String binary, BigInteger chainId,
String value, String encodedConstructor, String abi)
throws IOException, TransactionException {
try {
Constructor constructor = type.getDeclaredConstructor(
String.class,
CITAj.class, TransactionManager.class);
constructor.setAccessible(true);
// we want to use null here to ensure that "to" parameter on message is not populated
// Unfortunately, we need empty string(not null) that represent create contract
T contract = constructor.newInstance(
"", citaj, transactionManager);
return create(contract, binary, encodedConstructor, quota, nonce,
validUntilBlock, version, chainId, value, abi);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
protected static RemoteCall deployRemoteCall(
Class type, CITAj citaj, TransactionManager transactionManager,
long quota, String nonce, long validUntilBlock,
int version, BigInteger chainId, String value,
String binary, String encodedConstructor) {
return new RemoteCall<>(() -> deploy(
type, citaj, transactionManager, quota, nonce, validUntilBlock,
version, binary, chainId, value, encodedConstructor));
}
protected static RemoteCall deployRemoteCall(
Class type, CITAj citaj, TransactionManager transactionManager,
long quota, String nonce, long validUntilBlock,
int version, BigInteger chainId, String value,
String binary, String encodedConstructor, String abi) {
return new RemoteCall<>(() -> deploy(
type, citaj, transactionManager, quota, nonce, validUntilBlock,
version, binary, chainId, value, encodedConstructor, abi));
}
public static EventValues staticExtractEventParameters(
Event event, Log log) {
List topics = log.getTopics();
String encodedEventSignature = EventEncoder.encode(event);
if (!topics.get(0).equals(encodedEventSignature)) {
return null;
}
List indexedValues = new ArrayList<>();
List nonIndexedValues = FunctionReturnDecoder.decode(
log.getData(), event.getNonIndexedParameters());
List> indexedParameters = event.getIndexedParameters();
for (int i = 0; i < indexedParameters.size(); i++) {
Type value = FunctionReturnDecoder.decodeIndexedValue(
topics.get(i + 1), indexedParameters.get(i));
indexedValues.add(value);
}
return new EventValues(indexedValues, nonIndexedValues);
}
protected EventValues extractEventParameters(Event event, Log log) {
return staticExtractEventParameters(event, log);
}
protected List extractEventParameters(
Event event, TransactionReceipt transactionReceipt) {
List logs = transactionReceipt.getLogs();
List values = new ArrayList<>();
for (Log log : logs) {
EventValues eventValues = extractEventParameters(event, log);
if (eventValues != null) {
values.add(eventValues);
}
}
return values;
}
/**
* Subclasses should implement this method to return pre-existing addresses for deployed
* contracts.
*
* @param networkId the network id, for example "1" for the main-net, "3" for ropsten, etc.
* @return the deployed address of the contract, if known, and null otherwise.
*/
protected String getStaticDeployedAddress(String networkId) {
return null;
}
public final void setDeployedAddress(String networkId, String address) {
if (deployedAddresses == null) {
deployedAddresses = new HashMap<>();
}
deployedAddresses.put(networkId, address);
}
public final String getDeployedAddress(String networkId) {
String addr = null;
if (deployedAddresses != null) {
addr = deployedAddresses.get(networkId);
}
return addr == null ? getStaticDeployedAddress(networkId) : addr;
}
@SuppressWarnings("unchecked")
protected static List convertToNative(List arr) {
List out = new ArrayList();
for (Iterator it = arr.iterator(); it.hasNext(); ) {
out.add((T)it.next().getValue());
}
return out;
}
}