ContractService.java
/*
* @copyright defined in LICENSE.txt
*/
package ship.build.web.service;
import static java.util.Arrays.stream;
import static java.util.UUID.randomUUID;
import static ship.util.Messages.bind;
import hera.api.AccountOperation;
import hera.api.AergoApi;
import hera.api.ContractOperation;
import hera.api.encode.Base58;
import hera.api.encode.Base58WithCheckSum;
import hera.api.model.Account;
import hera.api.model.AccountAddress;
import hera.api.model.AccountState;
import hera.api.model.Authentication;
import hera.api.model.ContractAddress;
import hera.api.model.ContractDefinition;
import hera.api.model.ContractFunction;
import hera.api.model.ContractInterface;
import hera.api.model.ContractInvocation;
import hera.api.model.ContractResult;
import hera.api.model.ContractTxHash;
import hera.api.model.ContractTxReceipt;
import hera.api.model.Fee;
import hera.exception.RpcConnectionException;
import hera.exception.RpcException;
import hera.util.Pair;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.inject.Named;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import org.springframework.beans.factory.annotation.Value;
import ship.build.web.exception.AergoNodeException;
import ship.build.web.exception.ResourceNotFoundException;
import ship.build.web.model.BuildDetails;
import ship.build.web.model.DeploymentResult;
import ship.build.web.model.ExecutionResult;
import ship.build.web.model.QueryResult;
import ship.test.LuaBinary;
import ship.test.LuaCompiler;
import ship.util.AergoPool;
import ship.util.ResourcePool;
@NoArgsConstructor
@Named
public class ContractService extends AbstractService {
protected static final String NL_0 = ContractService.class.getName() + ".0";
@Getter
@Setter
@Value("${project.endpoint}")
protected String endpoint;
protected String password = randomUUID().toString();
protected Account account;
protected Fee fee = new Fee(1000, 1000);
protected final LuaCompiler luaCompiler = new LuaCompiler();
protected final List<DeploymentResult> deployHistory = new ArrayList<>();
protected final Map<String, DeploymentResult> encodedContractTxHash2contractAddresses =
new HashMap<>();
@Setter
protected ResourcePool<AergoApi> aergoPool;
@RequiredArgsConstructor
class SimpleBase58 implements Base58 {
@NonNull
protected final String encoded;
@Override
public String getEncodedValue() {
return encoded;
}
}
protected synchronized void ensureAccount() {
if (null != account) {
return;
}
if (null == aergoPool) {
aergoPool = new AergoPool(endpoint);
}
final AergoApi aergoApi = aergoPool.borrowResource();
try {
final AccountOperation accountOperation = aergoApi.getAccountOperation();
logger.trace("Password: {}", password);
account = accountOperation.create(password);
final AccountAddress accountAddress = account.getAddress();
final Authentication authentication = new Authentication(accountAddress, password);
accountOperation.unlock(authentication);
logger.debug("{} unlocked", authentication);
} catch (final RpcConnectionException ex) {
throw new AergoNodeException(
"Fail to connect aergo[" + endpoint + "]. Check your aergo node.", ex);
} catch (final RpcException ex) {
throw new AergoNodeException("Fail to deploy contract", ex);
} finally {
aergoPool.returnResource(aergoApi);
}
}
/**
* Deploy {@code buildDetails}'s result.
*
* @param buildDetails build result
*
* @return deployment result
*
* @throws Exception Fail to deploy
*/
public DeploymentResult deploy(final BuildDetails buildDetails) throws Exception {
ensureAccount();
final AergoApi aergoApi = aergoPool.borrowResource();
try {
final byte[] buildResult = buildDetails.getResult().getBytes();
final LuaBinary luaBinary = luaCompiler.compile(() -> new ByteArrayInputStream(buildResult));
logger.trace("Successful to compile:\n{}", luaBinary.getPayload());
final AccountOperation accountOperation = aergoApi.getAccountOperation();
final AccountState syncedAccount = accountOperation.getState(account);
final ContractOperation contractOperation = aergoApi.getContractOperation();
final Base58WithCheckSum encodedPayload = () -> luaBinary.getPayload().getEncodedValue();
account.setNonce(syncedAccount.getNonce() + 1);
final ContractDefinition contractDefinition = ContractDefinition.of(encodedPayload);
final ContractTxHash contractTransactionHash =
contractOperation.deploy(account, contractDefinition, fee);
logger.debug("Contract transaction hash: {}", contractTransactionHash);
final String encodedContractTxHash = contractTransactionHash.toString();
final DeploymentResult deploymentResult = new DeploymentResult();
deploymentResult.setBuildUuid(buildDetails.getUuid());
deploymentResult.setEncodedContractTransactionHash(encodedContractTxHash);
encodedContractTxHash2contractAddresses.put(encodedContractTxHash, deploymentResult);
deployHistory.add(deploymentResult);
return deploymentResult;
} catch (final RpcConnectionException ex) {
throw new AergoNodeException(
"Fail to connect aergo[" + endpoint + "]. Check your aergo node.", ex);
} catch (final RpcException ex) {
throw new AergoNodeException("Fail to deploy contract", ex);
} finally {
aergoPool.returnResource(aergoApi);
}
}
protected Pair<ContractTxHash, ContractFunction> find(
final String encodedContractTxHash, final String functionName) {
logger.trace("Encoded tx hash: {}", encodedContractTxHash);
final Base58 encoded = new SimpleBase58(encodedContractTxHash);
final ContractTxHash contractTxHash = new ContractTxHash(encoded);
final DeploymentResult deploymentResult =
encodedContractTxHash2contractAddresses.get(encodedContractTxHash);
final ContractInterface contractInterface = deploymentResult.getContractInterface();
final ContractFunction contractFunction = contractInterface.findFunction(functionName)
.orElseThrow(() -> new ResourceNotFoundException("No " + functionName + " function."));
return new Pair<>(contractTxHash, contractFunction);
}
/**
* Execute smart contract.
*
* @param encodedContractTxHash contract transaction hash
* @param functionName function's name to execute
* @param args function's arguments to execute
*
* @return execution result
*/
public ExecutionResult tryExecute(final String encodedContractTxHash, final String functionName,
final String... args) {
logger.trace("Encoded tx hash: {}", encodedContractTxHash);
final Pair<ContractTxHash, ContractFunction> pair = find(encodedContractTxHash, functionName);
return execute(pair.v1, pair.v2, args);
}
protected ExecutionResult execute(
final ContractTxHash contractTxHash,
final ContractFunction contractFunction,
final String... args) {
ensureAccount();
final AergoApi aergoApi = aergoPool.borrowResource();
try {
final AccountOperation accountOperation = aergoApi.getAccountOperation();
final ContractOperation contractOperation = aergoApi.getContractOperation();
final ContractTxReceipt contractTxReceipt =
contractOperation.getReceipt(contractTxHash);
logger.debug("Receipt: {}", contractTxReceipt);
final AccountState syncedAccount = accountOperation.getState(account);
final ContractAddress contractAddress = contractTxReceipt.getContractAddress();
logger.trace("Executing...");
final ContractInvocation contractCall =
new ContractInvocation(contractAddress, contractFunction, stream(args).toArray());
account.setNonce(syncedAccount.getNonce() + 1);
final ContractTxHash executionContractHash = contractOperation.execute(
account,
contractCall,
fee
);
final ExecutionResult executionResult = new ExecutionResult();
executionResult.setContractTransactionHash(executionContractHash.toString());
return executionResult;
} finally {
aergoPool.returnResource(aergoApi);
}
}
/**
* Query smart contract.
*
* @param encodedContractTxHash contract transaction hash
* @param functionName function's name to execute
* @param args function's arguments to execute
*
* @return query result
*/
public QueryResult tryQuery(final String encodedContractTxHash, final String functionName,
final String... args) {
logger.trace("Encoded tx hash: {}", encodedContractTxHash);
final Pair<ContractTxHash, ContractFunction> pair = find(encodedContractTxHash, functionName);
return query(pair.v1, pair.v2, args);
}
protected QueryResult query(
final ContractTxHash contractTxHash,
final ContractFunction contractFunction,
final String... args) {
ensureAccount();
final AergoApi aergoApi = aergoPool.borrowResource();
try {
final ContractOperation contractOperation = aergoApi.getContractOperation();
final ContractTxReceipt contractTxReceipt =
contractOperation.getReceipt(contractTxHash);
logger.debug("Receipt: {}", contractTxReceipt);
final ContractAddress contractAddress = contractTxReceipt.getContractAddress();
final ContractInvocation contractCall =
new ContractInvocation(contractAddress, contractFunction, stream(args).toArray());
logger.trace("Querying...");
final ContractResult contractResult = contractOperation.query(contractCall);
final String resultString = new String(contractResult.getResultInRawBytes().getValue());
return new QueryResult(resultString);
} finally {
aergoPool.returnResource(aergoApi);
}
}
/**
* Get latest contract.
*
* @return latest deployed contract
*/
public DeploymentResult getLatestContractInformation() {
if (deployHistory.isEmpty()) {
throw new ResourceNotFoundException("No deployment!! Deploy your contract first.");
}
final DeploymentResult latest = deployHistory.get(deployHistory.size() - 1);
logger.debug("Latest deployment: {}", latest);
if (null == latest.getContractInterface()) {
final String encodedContractTxHash = latest.getEncodedContractTransactionHash();
logger.trace("Encoded tx hash: {}", encodedContractTxHash);
final Base58 base58 = new SimpleBase58(encodedContractTxHash);
final ContractTxHash contractTxHash = new ContractTxHash(base58);
final ContractInterface contractInterface = getInterface(contractTxHash);
latest.setContractInterface(contractInterface);
}
return latest;
}
/**
* Get application blockchain interface for {@code encodedContractTransactionHash}
* from {@code endpoint}.
*
* @param contractTxHash contract's transaction hash
*
* @return abi set
*/
public ContractInterface getInterface(final ContractTxHash contractTxHash) {
final AergoApi aergoApi = aergoPool.borrowResource();
try {
final ContractOperation contractOperation = aergoApi.getContractOperation();
ContractTxReceipt receipt = contractOperation.getReceipt(contractTxHash);
final ContractAddress address = receipt.getContractAddress();
final ContractInterface contractInterface = contractOperation.getContractInterface(address);
if (null == contractInterface) {
throw new ResourceNotFoundException(bind(NL_0, contractTxHash));
}
return contractInterface;
} finally {
aergoPool.returnResource(aergoApi);
}
}
}