All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.hedera.hashgraph.sdk.EntityIdHelper Maven / Gradle / Ivy

The newest version!
/*-
 *
 * 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 com.google.errorprone.annotations.Var;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
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.regex.Pattern;
import javax.annotation.Nullable;
import org.bouncycastle.util.encoders.DecoderException;
import org.bouncycastle.util.encoders.Hex;

/**
 * Utility class used internally by the sdk.
 */
class EntityIdHelper {
    /**
     * The length of a Solidity address in bytes.
     */
    static final int SOLIDITY_ADDRESS_LEN = 20;

    /**
     * The length of a hexadecimal-encoded Solidity address, in ASCII characters (bytes).
     */
    static final int SOLIDITY_ADDRESS_LEN_HEX = SOLIDITY_ADDRESS_LEN * 2;

    private static final Pattern ENTITY_ID_REGEX = Pattern.compile(
        "(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-([a-z]{5}))?$");

    static final Duration MIRROR_NODE_CONNECTION_TIMEOUT = Duration.ofSeconds(30);

    /**
     * Constructor.
     */
    private EntityIdHelper() {
    }

    /**
     * Generate an R object from a string.
     *
     * @param idString                  the id string
     * @param constructObjectWithIdNums the R object generator
     * @param 
     * @return the R type object
     */
    static  R fromString(String idString, WithIdNums constructObjectWithIdNums) {
        var match = ENTITY_ID_REGEX.matcher(idString);
        if (!match.find()) {
            throw new IllegalArgumentException(
                "Invalid ID \"" + idString + "\": format should look like 0.0.123 or 0.0.123-vfmkw"
            );
        }
        return constructObjectWithIdNums.apply(
            Long.parseLong(match.group(1)),
            Long.parseLong(match.group(2)),
            Long.parseLong(match.group(3)),
            match.group(4));
    }

    /**
     * Generate an R object from a solidity address.
     *
     * @param address     the string representation
     * @param withAddress the R object generator
     * @param 
     * @return the R type object
     */
    static  R fromSolidityAddress(String address, WithIdNums withAddress) {
        return fromSolidityAddress(decodeSolidityAddress(address), withAddress);
    }

    private static  R fromSolidityAddress(byte[] address, WithIdNums withAddress) {
        if (address.length != SOLIDITY_ADDRESS_LEN) {
            throw new IllegalArgumentException(
                "Solidity addresses must be 20 bytes or 40 hex chars");
        }

        var buf = ByteBuffer.wrap(address);
        return withAddress.apply(buf.getInt(), buf.getLong(), buf.getLong(), null);
    }

    /**
     * Decode the solidity address from a string.
     *
     * @param address the string representation
     * @return the decoded address
     */
    public static byte[] decodeSolidityAddress(@Var String address) {
        address = address.startsWith("0x") ? address.substring(2) : address;

        if (address.length() != SOLIDITY_ADDRESS_LEN_HEX) {
            throw new IllegalArgumentException(
                "Solidity addresses must be 20 bytes or 40 hex chars");
        }

        try {
            return Hex.decode(address);
        } catch (DecoderException e) {
            throw new IllegalArgumentException("failed to decode Solidity address as hex", e);
        }
    }

    /**
     * Generate a solidity address.
     *
     * @param shard the shard part
     * @param realm the realm part
     * @param num   the num part
     * @return the solidity address
     */
    static String toSolidityAddress(long shard, long realm, long num) {
        if (Long.highestOneBit(shard) > 32) {
            throw new IllegalStateException("shard out of 32-bit range " + shard);
        }

        return Hex.toHexString(
            ByteBuffer.allocate(20)
                .putInt((int) shard)
                .putLong(realm)
                .putLong(num)
                .array());
    }

    /**
     * Generate a checksum.
     *
     * @param ledgerId the ledger id
     * @param addr     the address
     * @return the checksum
     */
    static String checksum(LedgerId ledgerId, String addr) {
        StringBuilder answer = new StringBuilder();
        List d = new ArrayList<>(); // Digits with 10 for ".", so if addr == "0.0.123" then d == [0, 10, 0, 10, 1, 2, 3]
        @Var
        long s0 = 0; // Sum of even positions (mod 11)
        @Var
        long s1 = 0; // Sum of odd positions (mod 11)
        @Var
        long s = 0; // Weighted sum of all positions (mod p3)
        @Var
        long sh = 0; // Hash of the ledger ID
        @SuppressWarnings("UnusedVariable")
        @Var
        long c = 0; // The checksum, as a single number
        long p3 = 26 * 26 * 26; // 3 digits in base 26
        long p5 = 26 * 26 * 26 * 26 * 26; // 5 digits in base 26
        long asciiA = Character.codePointAt("a", 0); // 97
        long m = 1_000_003; //min prime greater than a million. Used for the final permutation.
        long w = 31; // Sum s of digit values weights them by powers of w. Should be coprime to p5.

        List h = new ArrayList<>(ledgerId.toBytes().length + 6);
        for (byte b : ledgerId.toBytes()) {
            h.add(b);
        }
        for (int i = 0; i < 6; i++) {
            h.add((byte) 0);
        }
        for (var i = 0; i < addr.length(); i++) {
            d.add(addr.charAt(i) == '.' ? 10 : Integer.parseInt(String.valueOf(addr.charAt(i)), 10));
        }
        for (var i = 0; i < d.size(); i++) {
            s = (w * s + d.get(i)) % p3;
            if (i % 2 == 0) {
                s0 = (s0 + d.get(i)) % 11;
            } else {
                s1 = (s1 + d.get(i)) % 11;
            }
        }
        for (byte b : h) {
            // byte is signed in java, have to fake it to make bytes act like they're unsigned
            sh = (w * sh + (b < 0 ? 256 + b : b)) % p5;
        }
        c = ((((addr.length() % 5) * 11 + s0) * 11 + s1) * p3 + s + sh) % p5;
        c = (c * m) % p5;

        for (var i = 0; i < 5; i++) {
            answer.append((char) (asciiA + (c % 26)));
            c /= 26;
        }

        return answer.reverse().toString();
    }

    /**
     * Validate the configured client.
     *
     * @param shard    the shard part
     * @param realm    the realm part
     * @param num      the num part
     * @param client   the configured client
     * @param checksum the checksum
     * @throws BadEntityIdException
     */
    static void validate(long shard, long realm, long num, Client client, @Nullable String checksum)
        throws BadEntityIdException {
        if (client.getNetworkName() == null) {
            throw new IllegalStateException(
                "Can't validate checksum without knowing which network the ID is for.  Ensure client's network name is set.");
        }
        if (checksum != null) {
            String expectedChecksum = EntityIdHelper.checksum(
                client.getLedgerId(),
                EntityIdHelper.toString(shard, realm, num)
            );
            if (!checksum.equals(expectedChecksum)) {
                throw new BadEntityIdException(shard, realm, num, checksum, expectedChecksum);
            }
        }
    }

    /**
     * Generate a string representation.
     *
     * @param shard the shard part
     * @param realm the realm part
     * @param num   the num part
     * @return the string representation
     */
    static String toString(long shard, long realm, long num) {
        return "" + shard + "." + realm + "." + num;
    }

    /**
     * Generate a string representation with a checksum.
     *
     * @param shard    the shard part
     * @param realm    the realm part
     * @param num      the num part
     * @param client   the configured client
     * @param checksum the checksum
     * @return the string representation with checksum
     */
    static String toStringWithChecksum(long shard, long realm, long num, Client client, @Nullable String checksum) {
        if (client.getLedgerId() != null) {
            return "" + shard + "." + realm + "." + num + "-" + checksum(client.getLedgerId(),
                EntityIdHelper.toString(shard, realm, num));
        } else {
            throw new IllegalStateException(
                "Can't derive checksum for ID without knowing which network the ID is for.  Ensure client's ledgerId is set.");
        }
    }

    /**
     * Takes an address as `byte[]` and returns whether this is a long-zero address
     * @param address
     * @return
     */
    public static boolean isLongZeroAddress(byte[] address) {
        for (int i = 0; i < 12; i++) {
            if (address[i] != 0) {
                return false;
            }
        }
        return true;
    }

    /**
     * Get AccountId num from mirror node using evm address.
     *
     * @param client
     * @param evmAddress
     * @return
     * @throws IOException
     * @throws InterruptedException
     */
    public static CompletableFuture getAccountNumFromMirrorNodeAsync(Client client, String evmAddress) {
        String apiEndpoint = "/accounts/" + evmAddress;
        return performQueryToMirrorNodeAsync(client, apiEndpoint)
            .thenApply(response ->
                parseNumFromMirrorNodeResponse(response, "account"));
    }

    /**
     * Get EvmAddress from mirror node using account num.
     *
     * @param client
     * @param num
     * @return
     * @throws IOException
     * @throws InterruptedException
     */
    public static CompletableFuture getEvmAddressFromMirrorNodeAsync(Client client, long num) {
        String apiEndpoint = "/accounts/" + num;
        return performQueryToMirrorNodeAsync(client, apiEndpoint)
            .thenApply(response ->
                EvmAddress.fromString(parseEvmAddressFromMirrorNodeResponse(response, "evm_address")));
    }

    /**
     * Get ContractId num from mirror node using evm address.
     *
     * @param client
     * @param evmAddress
     * @return
     * @throws IOException
     * @throws InterruptedException
     */
    public static CompletableFuture getContractNumFromMirrorNodeAsync(Client client, String evmAddress) {
        String apiEndpoint = "/contracts/" + evmAddress;

        CompletableFuture responseFuture = performQueryToMirrorNodeAsync(client, apiEndpoint);

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

    private static CompletableFuture performQueryToMirrorNodeAsync(Client client, String apiEndpoint) {
        Optional mirrorUrl = client.getMirrorNetwork().stream()
            .map(url -> url.substring(0, url.indexOf(":")))
            .findFirst();

        if (mirrorUrl.isEmpty()) {
            return CompletableFuture.failedFuture(new IllegalArgumentException("Mirror URL not found"));
        }

        String apiUrl = "https://" + mirrorUrl.get() + "/api/v1" + apiEndpoint;

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

        HttpClient httpClient = HttpClient.newHttpClient();
        HttpRequest httpRequest = 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();

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

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

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

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

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

    @FunctionalInterface
    interface WithIdNums {
        R apply(long shard, long realm, long num, @Nullable String checksum);
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy