Skip to content

Commit

Permalink
feat: initial implementation
Browse files Browse the repository at this point in the history
Signed-off-by: Ivan Ivanov <[email protected]>
  • Loading branch information
0xivanov committed Nov 11, 2024
1 parent a6369b9 commit 8b7c126
Show file tree
Hide file tree
Showing 5 changed files with 343 additions and 32 deletions.
73 changes: 55 additions & 18 deletions sdk/src/main/java/com/hedera/hashgraph/sdk/EntityIdHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,14 @@
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.HttpTimeoutException;
import java.nio.ByteBuffer;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.regex.Pattern;
import javax.annotation.Nullable;
import org.bouncycastle.util.encoders.DecoderException;
Expand Down Expand Up @@ -290,7 +292,7 @@ public static boolean isLongZeroAddress(byte[] address) {
*/
public static CompletableFuture<Long> getAccountNumFromMirrorNodeAsync(Client client, String evmAddress) {
String apiEndpoint = "/accounts/" + evmAddress;
return performQueryToMirrorNodeAsync(client, apiEndpoint)
return performQueryToMirrorNodeAsync(client, apiEndpoint, null, false)
.thenApply(response ->
parseNumFromMirrorNodeResponse(response, "account"));
}
Expand All @@ -309,7 +311,7 @@ public static CompletableFuture<Long> getAccountNumFromMirrorNodeAsync(Client cl
*/
public static CompletableFuture<EvmAddress> getEvmAddressFromMirrorNodeAsync(Client client, long num) {
String apiEndpoint = "/accounts/" + num;
return performQueryToMirrorNodeAsync(client, apiEndpoint)
return performQueryToMirrorNodeAsync(client, apiEndpoint, null, false)
.thenApply(response ->
EvmAddress.fromString(parseEvmAddressFromMirrorNodeResponse(response, "evm_address")));
}
Expand All @@ -329,13 +331,34 @@ public static CompletableFuture<EvmAddress> getEvmAddressFromMirrorNodeAsync(Cli
public static CompletableFuture<Long> getContractNumFromMirrorNodeAsync(Client client, String evmAddress) {
String apiEndpoint = "/contracts/" + evmAddress;

CompletableFuture<String> responseFuture = performQueryToMirrorNodeAsync(client, apiEndpoint);
CompletableFuture<String> responseFuture = performQueryToMirrorNodeAsync(client, apiEndpoint, null, false);

return responseFuture.thenApply(response ->
parseNumFromMirrorNodeResponse(response, "contract_id"));
}

private static CompletableFuture<String> performQueryToMirrorNodeAsync(Client client, String apiEndpoint) {




private static long parseNumFromMirrorNodeResponse(String responseBody, String memberName) {
JsonParser jsonParser = new JsonParser();
JsonObject jsonObject = jsonParser.parse(responseBody).getAsJsonObject();

String num = jsonObject.get(memberName).getAsString();

return Long.parseLong(num.substring(num.lastIndexOf(".") + 1));
}

public static CompletableFuture<String> getContractAddressFromMirrorNodeAsync(Client client, String id) {
String apiEndpoint = "/contracts/" + id;
CompletableFuture<String> responseFuture = performQueryToMirrorNodeAsync(client, apiEndpoint, null, false);

return responseFuture.thenApply(response ->
parseEvmAddressFromMirrorNodeResponse(response, "evm_address"));
}

static CompletableFuture<String> performQueryToMirrorNodeAsync(Client client, String apiEndpoint, String jsonBody, boolean isContractCall) {
Optional<String> mirrorUrl = client.getMirrorNetwork().stream()
.map(url -> url.substring(0, url.indexOf(":")))
.findFirst();
Expand All @@ -347,26 +370,40 @@ private static CompletableFuture<String> performQueryToMirrorNodeAsync(Client cl
String apiUrl = "https://" + mirrorUrl.get() + "/api/v1" + apiEndpoint;

if (client.getLedgerId() == null) {
apiUrl = "http://" + mirrorUrl.get() + ":5551/api/v1" + apiEndpoint;
if (isContractCall) {
apiUrl = "http://" + mirrorUrl.get() + ":8545/api/v1" + apiEndpoint;
} else {
apiUrl = "http://" + mirrorUrl.get() + ":5551/api/v1" + apiEndpoint;
}
}

HttpClient httpClient = HttpClient.newHttpClient();
HttpRequest httpRequest = HttpRequest.newBuilder()
var httpBuilder = HttpRequest.newBuilder()
.timeout(MIRROR_NODE_CONNECTION_TIMEOUT)
.uri(URI.create(apiUrl))
.build();
.uri(URI.create(apiUrl));

return httpClient.sendAsync(httpRequest, HttpResponse.BodyHandlers.ofString())
.thenApply(HttpResponse::body);
}

private static long parseNumFromMirrorNodeResponse(String responseBody, String memberName) {
JsonParser jsonParser = new JsonParser();
JsonObject jsonObject = jsonParser.parse(responseBody).getAsJsonObject();

String num = jsonObject.get(memberName).getAsString();
if (jsonBody != null) {
httpBuilder.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(jsonBody));
}
var httpRequest = httpBuilder.build();

return Long.parseLong(num.substring(num.lastIndexOf(".") + 1));
return httpClient.sendAsync(httpRequest, HttpResponse.BodyHandlers.ofString())
.handle((response, ex) -> {
if (ex != null) {
if (ex instanceof HttpTimeoutException) {
throw new CompletionException(new RuntimeException("Request to Mirror Node timed out", ex));
} else {
throw new CompletionException(new RuntimeException("Failed to send request to Mirror Node", ex));
}
}

int statusCode = response.statusCode();
if (statusCode != 200) {
throw new CompletionException(new RuntimeException("Received non-200 response from Mirror Node: " + response.body()));
}
return response.body();
});
}

private static String parseEvmAddressFromMirrorNodeResponse(String responseBody, String memberName) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
/*-
*
* Hedera Java SDK
*
* Copyright (C) 2020 - 2024 Hedera Hashgraph, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/

package com.hedera.hashgraph.sdk;

import static com.hedera.hashgraph.sdk.EntityIdHelper.getContractAddressFromMirrorNodeAsync;
import static com.hedera.hashgraph.sdk.EntityIdHelper.performQueryToMirrorNodeAsync;

import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.protobuf.ByteString;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutionException;
import org.bouncycastle.util.encoders.Hex;

/**
* MirrorNodeContractQuery returns a result from EVM execution such as cost-free execution of read-only smart contract
* queries, gas estimation, and transient simulation of read-write operations.
*/
public class MirrorNodeContractQuery {
private ContractId contractId = null;
private String contractEvmAddress = null;
private byte[] functionParameters;
private long blockNumber;

public ContractId getContractId() {
return this.contractId;
}

/**
* Sets the contract instance to call.
*
* @param contractId The ContractId to be set
* @return {@code this}
*/
public MirrorNodeContractQuery setContractId(ContractId contractId) {
Objects.requireNonNull(contractId);
this.contractId = contractId;
return this;
}

public String getContractEvmAddress() {
return this.contractEvmAddress;
}

/**
* Set the 20-byte EVM address of the contract to call.
*
* @param contractEvmAddress
* @return {@code this}
*/
public MirrorNodeContractQuery setContractEvmAddress(String contractEvmAddress) {
Objects.requireNonNull(contractEvmAddress);
this.contractEvmAddress = contractEvmAddress;
this.contractId = null;
return this;
}

public byte[] getFunctionParameters() {
return functionParameters;
}

/**
* Sets the function to call, and the parameters to pass to the function.
*
* @param name The String to be set as the function name
* @param params The function parameters to be set
* @return {@code this}
*/
public MirrorNodeContractQuery setFunction(String name, ContractFunctionParameters params) {
Objects.requireNonNull(params);
return setFunctionParameters(params.toBytes(name));
}

/**
* Sets the function name to call.
* <p>
* The function will be called with no parameters. Use {@link #setFunction(String, ContractFunctionParameters)} to
* call a function with parameters.
*
* @param name The String to be set as the function name
* @return {@code this}
*/
public MirrorNodeContractQuery setFunction(String name) {
return setFunction(name, new ContractFunctionParameters());
}

/**
* Sets the function parameters as their raw bytes.
* <p>
* Use this instead of {@link #setFunction(String, ContractFunctionParameters)} if you have already pre-encoded a
* solidity function call.
*
* @param functionParameters The function parameters to be set
* @return {@code this}
*/
public MirrorNodeContractQuery setFunctionParameters(ByteString functionParameters) {
Objects.requireNonNull(functionParameters);
this.functionParameters = functionParameters.toByteArray();
return this;
}

public long getBlockNumber() {
return blockNumber;
}

public void setBlockNumber(long blockNumber) {
this.blockNumber = blockNumber;
}

/**
* Returns gas estimation for the EVM execution
*
* @param client
* @throws ExecutionException
* @throws InterruptedException
*/
public long estimate(Client client) throws ExecutionException, InterruptedException {
if (this.contractEvmAddress == null) {
Objects.requireNonNull(this.contractId);
this.contractEvmAddress = getContractAddressFromMirrorNodeAsync(client, this.contractId.toString()).get();
}
return getEstimateGasFromMirrorNodeAsync(client, this.functionParameters, this.contractEvmAddress).get();
}

/**
* Does transient simulation of read-write operations and returns the result in hexadecimal string format
*
* @param client
* @throws ExecutionException
* @throws InterruptedException
*/
public String call(Client client) throws ExecutionException, InterruptedException {
if (this.contractEvmAddress == null) {
Objects.requireNonNull(this.contractId);
this.contractEvmAddress = getContractAddressFromMirrorNodeAsync(client, this.contractId.toString()).get();
}

var blockNum = this.blockNumber == 0 ? "" : String.valueOf(this.blockNumber);
return getContractCallResultFromMirrorNodeAsync(client, this.functionParameters, this.contractEvmAddress,
blockNum).get();
}

private static CompletableFuture<String> getContractCallResultFromMirrorNodeAsync(Client client, byte[] data,
String contractAddress, String blockNumber) {
String apiEndpoint = "/contracts/call";
String jsonPayload = createJsonPayload(data, contractAddress, blockNumber, false);
return performQueryToMirrorNodeAsync(client, apiEndpoint, jsonPayload, true)
.exceptionally(ex -> {
client.getLogger().error("Error in while performing post request to Mirror Node: " + ex.getMessage());
throw new CompletionException(ex);
})
.thenApply(MirrorNodeContractQuery::parseContractCallResult);
}

public static CompletableFuture<Long> getEstimateGasFromMirrorNodeAsync(Client client, byte[] data,
String contractAddress) {
String apiEndpoint = "/contracts/call";
String jsonPayload = createJsonPayload(data, contractAddress, "latest", true);
return performQueryToMirrorNodeAsync(client, apiEndpoint, jsonPayload, true)
.exceptionally(ex -> {
client.getLogger().error("Error in while performing post request to Mirror Node: " + ex.getMessage());
throw new CompletionException(ex);
})
.thenApply(MirrorNodeContractQuery::parseHexEstimateToLong);
}

private static String createJsonPayload(byte[] data, String contractAddress, String blockNumber, boolean estimate) {
String hexData = Hex.toHexString(data);
return String.format("""
{
"data": "%s",
"to": "%s",
"estimate": %b,
"blockNumber": "%s"
}
""", hexData, contractAddress, estimate, blockNumber);
}

private static String parseContractCallResult(String responseBody) {
JsonParser jsonParser = new JsonParser();
JsonObject jsonObject = jsonParser.parse(responseBody).getAsJsonObject();

return jsonObject.get("result").getAsString();
}

private static long parseHexEstimateToLong(String responseBody) {
return Integer.parseInt(parseContractCallResult(responseBody).substring(2), 16);
}
}
Loading

0 comments on commit 8b7c126

Please sign in to comment.