Skip to content

Commit

Permalink
feat: Implement MirrorNodeContractQueries (#2073)
Browse files Browse the repository at this point in the history
Signed-off-by: Ivan Ivanov <[email protected]>
  • Loading branch information
0xivanov authored Dec 11, 2024
1 parent 472693d commit 88d1dcc
Show file tree
Hide file tree
Showing 14 changed files with 1,376 additions and 51 deletions.
5 changes: 5 additions & 0 deletions examples/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ mainModuleInfo {
runtimeOnly("org.slf4j.simple")
}

dependencies {
implementation("org.bouncycastle:bcpkix-jdk18on:1.78.1")
implementation("org.bouncycastle:bcprov-jdk18on:1.78.1")
}

dependencies.constraints {
implementation("com.hedera.hashgraph:sdk:2.45.0")
implementation("com.hedera.hashgraph:sdk-full:2.45.0")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
/*-
*
* Hedera Java SDK
*
* Copyright (C) 2023 - 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.examples;

import com.hedera.hashgraph.sdk.AccountId;
import com.hedera.hashgraph.sdk.Client;
import com.hedera.hashgraph.sdk.ContractCallQuery;
import com.hedera.hashgraph.sdk.ContractCreateTransaction;
import com.hedera.hashgraph.sdk.Hbar;
import com.hedera.hashgraph.sdk.MirrorNodeContractCallQuery;
import com.hedera.hashgraph.sdk.MirrorNodeContractEstimateGasQuery;
import com.hedera.hashgraph.sdk.PrivateKey;
import com.hedera.hashgraph.sdk.logger.LogLevel;
import com.hedera.hashgraph.sdk.logger.Logger;
import io.github.cdimascio.dotenv.Dotenv;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
import org.bouncycastle.util.encoders.Hex;

public class MirrorNodeContractQueriesExample {
/*
* See .env.sample in the examples folder root for how to specify values below
* or set environment variables with the same names.
*/

/**
* Operator's account ID. Used to sign and pay for operations on Hedera.
*/
private static final AccountId OPERATOR_ID = AccountId.fromString(
Objects.requireNonNull(Dotenv.load().get("OPERATOR_ID")));

/**
* Operator's private key.
*/
private static final PrivateKey OPERATOR_KEY = PrivateKey.fromString(
Objects.requireNonNull(Dotenv.load().get("OPERATOR_KEY")));

/**
* HEDERA_NETWORK defaults to testnet if not specified in dotenv file. Network can be: localhost, testnet,
* previewnet or mainnet.
*/
private static final String HEDERA_NETWORK = Dotenv.load().get("HEDERA_NETWORK", "testnet");

/**
* SDK_LOG_LEVEL defaults to SILENT if not specified in dotenv file. Log levels can be: TRACE, DEBUG, INFO, WARN,
* ERROR, SILENT.
* <p>
* Important pre-requisite: set simple logger log level to same level as the SDK_LOG_LEVEL, for example via VM
* options: -Dorg.slf4j.simpleLogger.log.com.hedera.hashgraph=trace
*/
private static final String SDK_LOG_LEVEL = Dotenv.load().get("SDK_LOG_LEVEL", "SILENT");

private static final String SMART_CONTRACT_BYTECODE = "60806040526040518060400160405280600581526020017f68656c6c6f0000000000000000000000000000000000000000000000000000008152505f90816100479190610293565b50348015610053575f80fd5b50610362565b5f81519050919050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b7f4e487b71000000000000000000000000000000000000000000000000000000005f52602260045260245ffd5b5f60028204905060018216806100d457607f821691505b6020821081036100e7576100e6610090565b5b50919050565b5f819050815f5260205f209050919050565b5f6020601f8301049050919050565b5f82821b905092915050565b5f600883026101497fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8261010e565b610153868361010e565b95508019841693508086168417925050509392505050565b5f819050919050565b5f819050919050565b5f61019761019261018d8461016b565b610174565b61016b565b9050919050565b5f819050919050565b6101b08361017d565b6101c46101bc8261019e565b84845461011a565b825550505050565b5f90565b6101d86101cc565b6101e38184846101a7565b505050565b5b81811015610206576101fb5f826101d0565b6001810190506101e9565b5050565b601f82111561024b5761021c816100ed565b610225846100ff565b81016020851015610234578190505b610248610240856100ff565b8301826101e8565b50505b505050565b5f82821c905092915050565b5f61026b5f1984600802610250565b1980831691505092915050565b5f610283838361025c565b9150826002028217905092915050565b61029c82610059565b67ffffffffffffffff8111156102b5576102b4610063565b5b6102bf82546100bd565b6102ca82828561020a565b5f60209050601f8311600181146102fb575f84156102e9578287015190505b6102f38582610278565b86555061035a565b601f198416610309866100ed565b5f5b828110156103305784890151825560018201915060208501945060208101905061030b565b8683101561034d5784890151610349601f89168261025c565b8355505b6001600288020188555050505b505050505050565b6102178061036f5f395ff3fe608060405234801561000f575f80fd5b5060043610610029575f3560e01c8063ce6d41de1461002d575b5f80fd5b61003561004b565b6040516100429190610164565b60405180910390f35b60605f8054610059906101b1565b80601f0160208091040260200160405190810160405280929190818152602001828054610085906101b1565b80156100d05780601f106100a7576101008083540402835291602001916100d0565b820191905f5260205f20905b8154815290600101906020018083116100b357829003601f168201915b5050505050905090565b5f81519050919050565b5f82825260208201905092915050565b5f5b838110156101115780820151818401526020810190506100f6565b5f8484015250505050565b5f601f19601f8301169050919050565b5f610136826100da565b61014081856100e4565b93506101508185602086016100f4565b6101598161011c565b840191505092915050565b5f6020820190508181035f83015261017c818461012c565b905092915050565b7f4e487b71000000000000000000000000000000000000000000000000000000005f52602260045260245ffd5b5f60028204905060018216806101c857607f821691505b6020821081036101db576101da610184565b5b5091905056fea26469706673582212202a86c27939bfab6d4a2c61ebbf096d8424e17e22dfdd42320f6e2654863581e964736f6c634300081a0033";

public static void main(String[] args) throws Exception {
System.out.println("Mirror Node contract queries Example Start!");
/*
* Step 0:
* Create and configure the SDK Client.
*/
Client client = ClientHelper.forName(HEDERA_NETWORK);
// All generated transactions will be paid by this account and signed by this key.
client.setOperator(OPERATOR_ID, OPERATOR_KEY);
// Attach logger to the SDK Client.
client.setLogger(new Logger(LogLevel.valueOf(SDK_LOG_LEVEL)));

/*
* Step 1:
* Create the contract
*/
var response = new ContractCreateTransaction()
.setGas(200_000)
.setBytecode(Hex.decode(SMART_CONTRACT_BYTECODE))
.setContractMemo("Simple contract with string field")
.execute(client);

var contractId = Objects.requireNonNull(response.getReceipt(client).contractId);
System.out.println("Created new contract with ID: " + contractId);

/*
* Step 3:
* Wait for mirror node to import data
*/
Thread.sleep(5000);

/*
* Step 4:
* Estimate the gas needed
*/
var gas = new MirrorNodeContractEstimateGasQuery()
.setContractId(contractId)
.setSender(client.getOperatorAccountId())
.setGasLimit(30_000)
.setGasPrice(1234)
.setFunction("getMessage")
.execute(client);

System.out.println("Gas needed for this query: " + gas);

/*
* Step 5:
* Do the query against the consensus node using the estimated gas
*/
var callQuery = new ContractCallQuery()
.setContractId(contractId)
.setGas(gas)
.setFunction("getMessage")
.setQueryPayment(new Hbar(1));

var result = callQuery
.execute(client);

/*
* Step 6:
* Simulate the transaction for free, using the mirror node
*/
var simulationResult = new MirrorNodeContractCallQuery()
.setContractId(contractId)
.setSender(client.getOperatorAccountId())
.setGasLimit(30_000)
.setBlockNumber(10000)
.setGasPrice(1234)
.setFunction("getMessage")
.execute(client);

// Decode the result since it's coming in ABI Hex format from the Mirror Node
var decodedResult = decodeABIHexString(simulationResult);
System.out.println("Simulation result: " + decodedResult);
System.out.println("Contract call result: " + result.getString(0));
}

/**
* Decodes a hex-encoded ABI (Application Binary Interface) string into a UTF-8 string.
* <p>
* The function assumes the input follows the ABI encoding standard for dynamic data. Specifically, it parses the
* length of the dynamic data and extracts the corresponding substring.
* <p>
* The structure of the input hex string is as follows: - The first 64 characters represent metadata, such as
* offsets and other header information. - Characters from index 64 to 128 encode the length of the dynamic data in
* bytes. - Characters from index 128 onward represent the actual dynamic data.
* <p>
* This method removes the `0x` prefix if present, parses the length, and decodes the dynamic data into UTF-8.
*
* @param hex the hex string to decode, which follows the ABI encoding standard
* @return the decoded UTF-8 string
*/
private static String decodeABIHexString(String hex) {
// Trim 0x at the beginning
if (hex.startsWith("0x")) {
hex = hex.substring(2);
}

// Extract the length of the data by parsing the substring from position 64 to 128 as a hexadecimal integer
// This section represents the length of the dynamic data, specifically the number of bytes in the string or array
int length = Integer.parseInt(hex.substring(64, 128), 16);

// Using the extracted length, the code calculates the substring containing the actual data starting from position 128.
String hexStringData = hex.substring(128, 128 + length * 2);

byte[] bytes = new byte[length];
// Iterate through the extracted hex data, two characters at a time, converting each pair to a byte and storing it in a byte array.
for (int i = 0; i < length; i++) {
bytes[i] = (byte) Integer.parseInt(hexStringData.substring(i * 2, i * 2 + 2), 16);
}

// Convert to UTF 8
return new String(bytes, StandardCharsets.UTF_8);
}
}
1 change: 1 addition & 0 deletions examples/src/main/java/module-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,5 @@

requires static java.annotation;
requires com.google.protobuf;
requires org.bouncycastle.provider;
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ testing.suites {
testType = TestSuiteType.INTEGRATION_TEST
targets.all {
testTask {
failFast = true
group = "build"
systemProperty("CONFIG_FILE", providers.gradleProperty("CONFIG_FILE").getOrElse(""))
systemProperty("HEDERA_NETWORK", providers.gradleProperty("HEDERA_NETWORK").getOrElse(""))
Expand Down
91 changes: 54 additions & 37 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 All @@ -40,7 +42,7 @@
/**
* Utility class used internally by the sdk.
*/
class EntityIdHelper {
public class EntityIdHelper {
/**
* The length of a Solidity address in bytes.
*/
Expand Down Expand Up @@ -112,7 +114,7 @@ private static <R> R fromSolidityAddress(byte[] address, WithIdNums<R> withAddre
* @param address the string representation
* @return the decoded address
*/
public static byte[] decodeSolidityAddress(String address) {
static byte[] decodeSolidityAddress(String address) {
address = address.startsWith("0x") ? address.substring(2) : address;

if (address.length() != SOLIDITY_ADDRESS_LEN_HEX) {
Expand Down Expand Up @@ -267,7 +269,7 @@ static String toStringWithChecksum(long shard, long realm, long num, Client clie
* @param address
* @return
*/
public static boolean isLongZeroAddress(byte[] address) {
static boolean isLongZeroAddress(byte[] address) {
for (int i = 0; i < 12; i++) {
if (address[i] != 0) {
return false;
Expand All @@ -284,13 +286,10 @@ public static boolean isLongZeroAddress(byte[] address) {
*
* @param client
* @param evmAddress
* @return
* @throws IOException
* @throws InterruptedException
*/
public static CompletableFuture<Long> getAccountNumFromMirrorNodeAsync(Client client, String evmAddress) {
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 @@ -303,15 +302,12 @@ public static CompletableFuture<Long> getAccountNumFromMirrorNodeAsync(Client cl
*
* @param client
* @param num
* @return
* @throws IOException
* @throws InterruptedException
*/
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")));
EvmAddress.fromString(parseStringMirrorNodeResponse(response, "evm_address")));
}

/**
Expand All @@ -322,20 +318,26 @@ public static CompletableFuture<EvmAddress> getEvmAddressFromMirrorNodeAsync(Cli
*
* @param client
* @param evmAddress
* @return
* @throws IOException
* @throws InterruptedException
*/
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) {

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 ->
parseStringMirrorNodeResponse(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,37 +349,52 @@ 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();

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();
.uri(URI.create(apiUrl));

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) {
JsonParser jsonParser = new JsonParser();
JsonObject jsonObject = jsonParser.parse(responseBody).getAsJsonObject();

private static String parseStringMirrorNodeResponse(String responseBody, String memberName) {
JsonObject jsonObject = JsonParser.parseString(responseBody).getAsJsonObject();
String evmAddress = jsonObject.get(memberName).getAsString();

return evmAddress.substring(evmAddress.lastIndexOf(".") + 1);
}

private static long parseNumFromMirrorNodeResponse(String responseBody, String memberName) {
return Long.parseLong(parseStringMirrorNodeResponse(responseBody, memberName));
}

@FunctionalInterface
interface WithIdNums<R> {
R apply(long shard, long realm, long num, @Nullable String checksum);
Expand Down
Loading

0 comments on commit 88d1dcc

Please sign in to comment.