diff --git a/CHANGELOG.md b/CHANGELOG.md index 90e80e7e5f..b9ca852c24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - `TRANSACTION_HAS_UNKNOWN_FIELDS` and `ACCOUNT_IS_IMMUTABLE` in `Status` + - `TransactionRecord.evmAddress` + - `PublicKeyECDSA.toEvmAddress()` + - `AccountCreateTransaction.setEvmAddress()` + - `AccountId.fromEvmAddress()` + - `AccountId.fromString()` now supports EVM address + - `TransferTransaction.addHbarTransfer()` now supports EVM address + - `AccountCreateEvmAddressExample` + - `CreateAccountWithAliasExample` + - `LazyCreateAccountTransactionExample` + - `LazyCreateTransferTransactionExample` + - `TransferUsingEvmAddressExample` + - `AccountCreationWaysExample` + +### Deprecated + +- `AccountCreateTransaction.setAliasEvmAddress()` use `AccountCreateTransaction.setEvmAddress()` instead ### Fixed - Misleading logging when an unhealthy node is hit diff --git a/examples/src/main/java/AccountCreateEvmAddressExample.java b/examples/src/main/java/AccountCreateEvmAddressExample.java new file mode 100644 index 0000000000..15f72fcc1e --- /dev/null +++ b/examples/src/main/java/AccountCreateEvmAddressExample.java @@ -0,0 +1,134 @@ +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.hedera.hashgraph.sdk.*; +import io.github.cdimascio.dotenv.Dotenv; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Objects; +import java.util.Collections; +import java.util.concurrent.TimeoutException; + +public class AccountCreateEvmAddressExample { + // see `.env.sample` in the repository root for how to specify these values + // or set environment variables with the same names + private static final AccountId OPERATOR_ID = AccountId.fromString(Objects.requireNonNull(Dotenv.load().get("OPERATOR_ID"))); + 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 + private static final String HEDERA_NETWORK = Dotenv.load().get("HEDERA_NETWORK", "testnet"); + + private AccountCreateEvmAddressExample() { + } + + /* + Create an account and set an EVM address using the `AccountCreateTransaction` + Reference: [HIP-583 Expand alias support in CryptoCreate & CryptoTransfer Transactions](https://hips.hedera.com/hip/hip-583) + ## Example 1 + - Create an ECSDA private key + - Extract the ECDSA public key + - Extract the Ethereum public address + - Add function in the SDK to calculate the Ethereum Address + - Ethereum account address / public-address - This is the rightmost 20 bytes of the 32 byte Keccak-256 hash of the ECDSA public key of the account. This calculation is in the manner described by the Ethereum Yellow Paper. + - Use the `AccountCreateTransaction` and set the EVM address field to the Ethereum public address + - Sign the transaction with the key that us paying for the transaction + - Get the account ID from the receipt + - Get the `AccountInfo` and return the account details + - Verify the evm address provided for the account matches what is in the mirror node + */ + public static void main(String[] args) throws PrecheckStatusException, TimeoutException, ReceiptStatusException, InterruptedException, IOException { + Client client = Client.forName(HEDERA_NETWORK); + + // Defaults the operator account ID and key such that all generated transactions will be paid for + // by this account and be signed by this key + client.setOperator(OPERATOR_ID, OPERATOR_KEY); + + /* + * Step 1 + * Create an ECSDA private key + */ + PrivateKey privateKey = PrivateKey.generateECDSA(); + + /* + * Step 2 + * Extract the ECDSA public key + */ + PublicKey publicKey = privateKey.getPublicKey(); + + /* + * Step 3 + * Extract the Ethereum public address + */ + EvmAddress evmAddress = publicKey.toEvmAddress(); + System.out.println(evmAddress); + + /* + * Step 4 + * Use the `AccountCreateTransaction` and set the EVM address field to the Ethereum public address + */ + AccountCreateTransaction accountCreateTransaction = new AccountCreateTransaction() + .setEvmAddress(evmAddress) + .setInitialBalance(new Hbar(10)); + + /* + * Step 5 + * Sign the transaction with the key that is paying for the transaction + */ + TransactionResponse response = accountCreateTransaction.execute(client); + + /* + * Step 6 + * Get the account ID from the receipt + */ + AccountId newAccountId = response.getReceipt(client).accountId; + System.out.println(newAccountId); + + /* + * Step 7 + * Get the `AccountInfo` and return the account details + */ + AccountInfo info = new AccountInfoQuery() + .setAccountId(newAccountId) + .execute(client); + + if (info.contractAccountId.equals(evmAddress.toString())) { + System.out.println("The addresses match"); + } else { + System.out.println("The addresses don't match"); + } + + /* + * Step 8 + * Verify the evm address provided for the account matches what is in the mirror node + */ + Thread.sleep(5000); + String link = "https://" + HEDERA_NETWORK + ".mirrornode.hedera.com/api/v1/accounts?account.id=" + newAccountId; + URL url = new URL(link); + HttpURLConnection con = (HttpURLConnection)url.openConnection(); + con.setRequestMethod("GET"); + con.setRequestProperty("Accept", "application/json"); + try(BufferedReader br = new BufferedReader(new InputStreamReader(con.getInputStream(), StandardCharsets.UTF_8))) { + StringBuilder builder = new StringBuilder(); + String responseLine; + while ((responseLine = br.readLine()) != null) { + builder.append(responseLine.trim()); + } + JsonObject jsonObject = JsonParser.parseString(builder.toString()).getAsJsonObject(); + String mirrorNodeEvmAddress = jsonObject + .getAsJsonArray("accounts") + .get(0).getAsJsonObject() + .get("evm_address") + .getAsString(); + + if (mirrorNodeEvmAddress.equals("0x" + evmAddress)) { + System.out.println("The addresses match"); + } else { + System.out.println("The addresses don't match"); + } + } + } +} diff --git a/examples/src/main/java/AccountCreationWaysExample.java b/examples/src/main/java/AccountCreationWaysExample.java new file mode 100644 index 0000000000..423b4d2f07 --- /dev/null +++ b/examples/src/main/java/AccountCreationWaysExample.java @@ -0,0 +1,47 @@ +import com.hedera.hashgraph.sdk.*; + +public class AccountCreationWaysExample { + + private AccountCreationWaysExample() { + } + + /* + Reference: [HIP-583 Expand alias support in CryptoCreate & CryptoTransfer Transactions](https://hips.hedera.com/hip/hip-583) + ## In Hedera we have the concept of 4 different account representations + - an account can have an account ID in shard.realm.accountNumber format (0.0.10) + - an account can have a public key alias in 0.0.CIQNOWUYAGBLCCVX2VF75U6JMQDTUDXBOLZ5VJRDEWXQEGTI64DVCGQ format + - an account can have an AccountId that is represented in 0x000000000000000000000000000000000000000a (for account ID 0.0.10) long zero format + - an account can be represented by an Ethereum public address 0xb794f5ea0ba39494ce839613fffba74279579268 + */ + public static void main(String[] args) { + /* + * Account ID - shard.realm.number format, i.e. `0.0.10` with the corresponding `0x000000000000000000000000000000000000000a` ethereum address + */ + AccountId hederaFormat = AccountId.fromString("0.0.10"); + System.out.println("Account ID: " + hederaFormat); + System.out.println("Account 0.0.10 corresponding Long-Zero address: " + hederaFormat.toSolidityAddress()); + + /* + * Hedera Long-Form Account ID - 0.0.aliasPublicKey, i.e. `0.0.CIQNOWUYAGBLCCVX2VF75U6JMQDTUDXBOLZ5VJRDEWXQEGTI64DVCGQ` + */ + PrivateKey privateKey = PrivateKey.generateECDSA(); + PublicKey publicKey = privateKey.getPublicKey(); + + // Assuming that the target shard and realm are known. + // For now they are virtually always 0. + AccountId aliasAccountId = publicKey.toAccountId(0, 0); + System.out.println("Hedera Long-Form Account ID: " + aliasAccountId.toString()); + + /* + * Hedera Account Long-Zero address - 0x000000000000000000000000000000000000000a (for accountId 0.0.10) + */ + AccountId longZeroAddress = AccountId.fromString("0x000000000000000000000000000000000000000a"); + System.out.println("Hedera Account Long-Zero address: " + longZeroAddress); + + /* + * Ethereum Account Address / public-address - 0xb794f5ea0ba39494ce839613fffba74279579268 + */ + AccountId evmAddress = AccountId.fromString("0xb794f5ea0ba39494ce839613fffba74279579268"); + System.out.println("Ethereum Account Address / public-address: " + evmAddress); + } +} diff --git a/examples/src/main/java/CreateAccountWithAliasExample.java b/examples/src/main/java/CreateAccountWithAliasExample.java new file mode 100644 index 0000000000..e4a6ad480b --- /dev/null +++ b/examples/src/main/java/CreateAccountWithAliasExample.java @@ -0,0 +1,226 @@ +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.hedera.hashgraph.sdk.*; +import io.github.cdimascio.dotenv.Dotenv; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.TimeoutException; + +public class CreateAccountWithAliasExample { + // see `.env.sample` in the repository root for how to specify these values + // or set environment variables with the same names + private static final AccountId OPERATOR_ID = AccountId.fromString(Objects.requireNonNull(Dotenv.load().get("OPERATOR_ID"))); + 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 + private static final String HEDERA_NETWORK = Dotenv.load().get("HEDERA_NETWORK", "testnet"); + + private CreateAccountWithAliasExample() { + } + + /* + Create an account and set a public key alias. + Reference: [HIP-583 Expand alias support in CryptoCreate & CryptoTransfer Transactions](https://hips.hedera.com/hip/hip-583) + ## Example 1: + - Create an ECDSA private key + - Get the ECDSA public key + - Use the `AccountCreateTransaction` and populate the `setAliasKey` field + - Sign the `AccountCreateTransaction` using an existing Hedera account and key to pay for the transaction fee + - Execute the transaction + - Return the Hedera account ID from the receipt of the transaction + - Get the `AccountInfo` using the new account ID + - Get the `AccountInfo` using the account public key in `0.0.aliasPublicKey` format + - Show the public key and the public key alias are the same on the account + - Show this account has a corresponding EVM address in the mirror node + ## Example 2: + - Create an ED2519 private key + - Get the ED2519 public key + - Use the `AccountCreateTransaction` and populate the `setAliasKey` field + - Sign the `AccountCreateTransaction` using an existing Hedera account and key to pay for the transaction fee + - Execute the transaction + - Return the Hedera account ID from the receipt of the transaction + - Get the `AccountInfo` using the new account ID + - Get the `AccountInfo` using the account public key in `0.0.aliasPublicKey` format + - Show the public key and the public key alias are the same on the account + */ + public static void main(String[] args) throws PrecheckStatusException, TimeoutException, ReceiptStatusException, InterruptedException, IOException { + Client client = Client.forName(HEDERA_NETWORK); + + // Defaults the operator account ID and key such that all generated transactions will be paid for + // by this account and be signed by this key + client.setOperator(OPERATOR_ID, OPERATOR_KEY); + + /* + * Example 1 + */ + System.out.println("Example 1"); + + /* + * Step 1 + * Create an ECSDA private key + */ + PrivateKey privateKey = PrivateKey.generateECDSA(); + + /* + * Step 2 + * Extract the ECDSA public key + */ + PublicKey publicKey = privateKey.getPublicKey(); + + /* + * Step 3 + * Use the `AccountCreateTransaction` and populate the `setAliasKey` field + */ + AccountCreateTransaction accountCreateTransaction = new AccountCreateTransaction() + .setAliasKey(publicKey) + .setInitialBalance(new Hbar(10)); + + /* + * Step 4 + * Execute the transaction + */ + TransactionResponse response = accountCreateTransaction.execute(client); + + /* + * Step 5 + * Return the Hedera account ID from the receipt of the transaction + */ + AccountId newAccountId = response.getReceipt(client).accountId; + System.out.println(newAccountId); + + /* + * Step 6 + * Get the `AccountInfo` using the new account ID + */ + AccountInfo accountInfo = new AccountInfoQuery() + .setAccountId(newAccountId) + .execute(client); + + /* + * Step 7 + * Get the `AccountInfo` using the account public key in `0.0.aliasPublicKey` format + */ + AccountId aliasPublicKey = publicKey.toAccountId(0, 0); + AccountInfo accountInfoAlias = new AccountInfoQuery() + .setAccountId(aliasPublicKey) + .execute(client); + + /* + * Step 8 + * Show the public key and the public key alias are the same on the account + */ + if (accountInfo.key.equals(accountInfo.aliasKey) + && accountInfo.key.equals(accountInfoAlias.key) + && accountInfoAlias.key.equals(accountInfoAlias.aliasKey) + ) { + System.out.println("The public key and the public key alias are the same"); + } else { + System.out.println("The public key and the public key alias differ"); + } + + /* + * Step 9 + * Show this account has a corresponding EVM address in the mirror node + */ + Thread.sleep(5000); + String link = "https://" + HEDERA_NETWORK + ".mirrornode.hedera.com/api/v1/accounts?account.id=" + newAccountId; + URL url = new URL(link); + HttpURLConnection con = (HttpURLConnection)url.openConnection(); + con.setRequestMethod("GET"); + con.setRequestProperty("Accept", "application/json"); + try(BufferedReader br = new BufferedReader(new InputStreamReader(con.getInputStream(), StandardCharsets.UTF_8))) { + StringBuilder builder = new StringBuilder(); + String responseLine; + while ((responseLine = br.readLine()) != null) { + builder.append(responseLine.trim()); + } + JsonObject jsonObject = JsonParser.parseString(builder.toString()).getAsJsonObject(); + String mirrorNodeEvmAddress = jsonObject + .getAsJsonArray("accounts") + .get(0).getAsJsonObject() + .get("evm_address") + .toString(); + + if (mirrorNodeEvmAddress != null) { + System.out.println("The account has a corresponding EVM address in the mirror node"); + } else { + System.out.println("The EVM address of the account is missing in the mirror node"); + } + } + + /* + * Example 2 + */ + System.out.println("Example 2"); + + /* + * Step 1 + * Create an ECSDA private key + */ + PrivateKey privateKey2 = PrivateKey.generateED25519(); + + /* + * Step 2 + * Extract the ECDSA public key + */ + PublicKey publicKey2 = privateKey2.getPublicKey(); + + /* + * Step 3 + * Use the `AccountCreateTransaction` and populate the `setAliasKey` field + */ + AccountCreateTransaction accountCreateTransaction2 = new AccountCreateTransaction() + .setAliasKey(publicKey2) + .setInitialBalance(new Hbar(10)); + + /* + * Step 4 + * Execute the transaction + */ + TransactionResponse response2 = accountCreateTransaction2.execute(client); + + /* + * Step 5 + * Return the Hedera account ID from the receipt of the transaction + */ + AccountId newAccountId2 = response2.getReceipt(client).accountId; + System.out.println(newAccountId2); + + /* + * Step 6 + * Get the `AccountInfo` using the new account ID + */ + AccountInfo accountInfo2 = new AccountInfoQuery() + .setAccountId(newAccountId2) + .execute(client); + + /* + * Step 7 + * Get the `AccountInfo` using the account public key in `0.0.aliasPublicKey` format + */ + AccountId aliasPublicKey2 = publicKey2.toAccountId(0, 0); + AccountInfo accountInfoAlias2 = new AccountInfoQuery() + .setAccountId(aliasPublicKey2) + .execute(client); + + /* + * Step 8 + * Show the public key and the public key alias are the same on the account + */ + if (accountInfo2.key.equals(accountInfo2.aliasKey) + && accountInfo2.key.equals(accountInfoAlias2.key) + && accountInfoAlias2.key.equals(accountInfoAlias2.aliasKey) + ) { + System.out.println("The public key and the public key alias are the same"); + } else { + System.out.println("The public key and the public key alias differ"); + } + } +} diff --git a/examples/src/main/java/LazyCreateAccountTransactionExample.java b/examples/src/main/java/LazyCreateAccountTransactionExample.java new file mode 100644 index 0000000000..b574bec1f3 --- /dev/null +++ b/examples/src/main/java/LazyCreateAccountTransactionExample.java @@ -0,0 +1,129 @@ +import com.hedera.hashgraph.sdk.*; +import io.github.cdimascio.dotenv.Dotenv; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.TimeoutException; + +public class LazyCreateAccountTransactionExample { + // see `.env.sample` in the repository root for how to specify these values + // or set environment variables with the same names + private static final AccountId OPERATOR_ID = AccountId.fromString(Objects.requireNonNull(Dotenv.load().get("OPERATOR_ID"))); + 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 + private static final String HEDERA_NETWORK = Dotenv.load().get("HEDERA_NETWORK", "testnet"); + + private LazyCreateAccountTransactionExample() { + } + + /* + Lazy-create a new account using a public-address via the `AccountCreateTransaction` transaction. + Reference: [HIP-583 Expand alias support in CryptoCreate & CryptoTransfer Transactions](https://hips.hedera.com/hip/hip-583) + ## Example 1: + - Create an ECSDA private key + - Extract the ECDSA public key + - Extract the Ethereum public address + - Add function in the SDK to calculate the Ethereum Address + - Ethereum account address / public-address - This is the rightmost 20 bytes of the 32 byte Keccak-256 hash of the ECDSA public key of the account. This calculation is in the manner described by the Ethereum Yellow Paper. + - Use the `AccountCreateTransaction` and populate `setEvmAddress(publicAddress)` field with the Ethereum public address + - Sign the `AccountCreateTransaction` transaction using an existing Hedera account and key to pay for the transaction fee + - The Hedera account that was created has a public address the user specified in the `AccountCreateTransaction` + - Will not have a Hedera account public key at this stage + - The account can only receive tokens or hbars + - This is referred to as a hollow account + - The alias property of the account will not have the public address + - Get the `AccountInfo` of the account and show that it is a hollow account i.e. does not have a public key + - To enhance the hollow account to have a public key the hollow account needs to be specified as a transaction fee payer in a HAPI transaction + - Any HAPI transaction can be used to apply the public key to the hollow account and create a complete Hedera account + - Use a HAPI transaction and set the hollow account as the transaction fee payer + - Sign with the ECDSA private key that corresponds to the public address on the hollow account + - Execute the transaction + - Get the `AccountInfo` and show that the account is now a complete account i.e. returns a public key of the account + */ + public static void main(String[] args) throws PrecheckStatusException, TimeoutException, ReceiptStatusException, InterruptedException, IOException { + Client client = Client.forName(HEDERA_NETWORK); + + // Defaults the operator account ID and key such that all generated transactions will be paid for + // by this account and be signed by this key + client.setOperator(OPERATOR_ID, OPERATOR_KEY); + + /* + * Step 1 + * Create an ECSDA private key + */ + PrivateKey privateKey = PrivateKey.generateECDSA(); + + /* + * Step 2 + * Extract the ECDSA public key + */ + PublicKey publicKey = privateKey.getPublicKey(); + + /* + * Step 3 + * Extract the Ethereum public address + */ + EvmAddress evmAddress = publicKey.toEvmAddress(); + System.out.println(evmAddress); + + /* + * Step 4 + * Use the `AccountCreateTransaction` and set the EVM address field to the Ethereum public address + */ + AccountCreateTransaction accountCreateTransaction = new AccountCreateTransaction() + .setEvmAddress(evmAddress) + .setInitialBalance(new Hbar(10)); + + /* + * Step 5 + * Sign the transaction with the key that is paying for the transaction + */ + TransactionResponse response = accountCreateTransaction.execute(client); + + /* + * Step 6 + * Get the `AccountInfo` of the account and show that it is a hollow account i.e. does not have a public key + */ + AccountId newAccountId = response.getReceipt(client).accountId; + System.out.println(newAccountId); + + AccountInfo accountInfo = new AccountInfoQuery() + .setAccountId(newAccountId) + .execute(client); + + if (((KeyList) accountInfo.key).isEmpty()) { + System.out.println("The newly created account is a hollow account"); + } else { + System.out.println("Not a hollow account"); + } + + /* + * Step 7 + * Use a HAPI transaction and set the hollow account as the transaction fee payer + * - To enhance the hollow account to have a public key the hollow account needs to be specified as a transaction fee payer in a HAPI transaction + * - Any HAPI transaction can be used to apply the public key to the hollow account and create a complete Hedera account + */ + + TransactionReceipt receipt = new TopicCreateTransaction() + .setTransactionId(TransactionId.generate(newAccountId)) + .setTopicMemo("Memo") + .freezeWith(client) + .sign(privateKey) + .execute(client) + .getReceipt(client); + System.out.println("Topic id = " + receipt.topicId); + + /* + * Step 8 + * Get the `AccountInfo` and show that the account is now a complete account i.e. returns a public key of the account + */ + AccountInfo accountInfo2 = new AccountInfoQuery() + .setAccountId(newAccountId) + .execute(client); + + System.out.println("The public key of the newly created and now complete account: " + accountInfo2.key); + + } +} diff --git a/examples/src/main/java/LazyCreateTransferTransactionExample.java b/examples/src/main/java/LazyCreateTransferTransactionExample.java new file mode 100644 index 0000000000..f14aa56737 --- /dev/null +++ b/examples/src/main/java/LazyCreateTransferTransactionExample.java @@ -0,0 +1,146 @@ +import com.hedera.hashgraph.sdk.*; +import io.github.cdimascio.dotenv.Dotenv; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.TimeoutException; + +public class LazyCreateTransferTransactionExample { + // see `.env.sample` in the repository root for how to specify these values + // or set environment variables with the same names + private static final AccountId OPERATOR_ID = AccountId.fromString(Objects.requireNonNull(Dotenv.load().get("OPERATOR_ID"))); + 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 + private static final String HEDERA_NETWORK = Dotenv.load().get("HEDERA_NETWORK", "testnet"); + + private LazyCreateTransferTransactionExample() { + } + + /* + Lazy-create a new account using a public-address via a `TransferTransaction`. + Reference: [HIP-583 Expand alias support in CryptoCreate & CryptoTransfer Transactions](https://hips.hedera.com/hip/hip-583) + ## Example 2 + - Create an ECSDA private key + - Extract the ECDSA public key + - Extract the Ethereum public address + - Add function to calculate the Ethereum Address to in SDK + - Ethereum account address / public-address - This is the rightmost 20 bytes of the 32 byte Keccak-256 hash of the ECDSA public key of the account. This calculation is in the manner described by the Ethereum Yellow Paper. + - Use the `TransferTransaction` + - Populate the `FromAddress` with the sender Hedera AccountID + - Populate the `ToAddress` with Ethereum public address + - Note: Can transfer from public address to public address in the `TransferTransaction` for complete accounts. Transfers from hollow accounts will not work because the hollow account does not have a public key assigned to authorize transfers out of the account + - Sign the `TransferTransaction` transaction using an existing Hedera account and key paying for the transaction fee + - The `AccountCreateTransaction` is executed as a child transaction triggered by the `TransferTransaction` + - The Hedera Account that was created has a public address the user specified in the TransferTransaction ToAddress + - Will not have a public key at this stage + - Cannot do anything besides receive tokens or hbars + - The alias property of the account does not have the public address + - Referred to as a hollow account + - To get the new account ID ask for the child receipts or child records for the parent transaction ID of the `TransferTransaction` + - Get the `AccountInfo` and verify the account is a hollow account with the supplied public address (may need to verify with mirror node API) + - To enhance the hollow account to have a public key the hollow account needs to be specified as a transaction fee payer in a HAPI transaction + - Create a HAPI transaction and assign the new hollow account as the transaction fee payer + - Sign with the private key that corresponds to the public key on the hollow account + - Get the `AccountInfo` for the account and return the public key on the account to show it is a complete account + */ + public static void main(String[] args) throws PrecheckStatusException, TimeoutException, ReceiptStatusException, InterruptedException, IOException { + Client client = Client.forName(HEDERA_NETWORK); + + // Defaults the operator account ID and key such that all generated transactions will be paid for + // by this account and be signed by this key + client.setOperator(OPERATOR_ID, OPERATOR_KEY); + + /* + * Step 1 + * Create an ECSDA private key + */ + PrivateKey privateKey = PrivateKey.generateECDSA(); + + /* + * Step 2 + * Extract the ECDSA public key + */ + PublicKey publicKey = privateKey.getPublicKey(); + + /* + * Step 3 + * Extract the Ethereum public address + */ + EvmAddress evmAddress = publicKey.toEvmAddress(); + System.out.println(evmAddress); + + /* + * Step 4 + * Use the `AccountCreateTransaction` and set the EVM address field to the Ethereum public address + */ + TransferTransaction transferTransaction = new TransferTransaction() + .addHbarTransfer(OPERATOR_ID, Hbar.from(10).negated()) + .addHbarTransfer(AccountId.fromEvmAddress(evmAddress), Hbar.from(10)) + .freezeWith(client); + + /* + * Step 5 + * Sign the `TransferTransaction` transaction using an existing Hedera account and key paying for the transaction fee + */ + TransactionResponse response = transferTransaction.execute(client); + + /* + * Step 6 + * To get the new account ID ask for the child receipts or child records for the parent transaction ID of the `TransferTransaction` + * - The `AccountCreateTransaction` is executed as a child transaction triggered by the `TransferTransaction` + */ + TransactionReceipt receipt = new TransactionReceiptQuery() + .setTransactionId(response.transactionId) + .setIncludeChildren(true) + .execute(client); + + AccountId newAccountId = receipt.children.get(0).accountId; + System.out.println(newAccountId); + + /* + * Step 7 + * Get the `AccountInfo` and verify the account is a hollow account with the supplied public address (may need to verify with mirror node API) + * The Hedera Account that was created has a public address the user specified in the TransferTransaction ToAddress + - Will not have a public key at this stage + - Cannot do anything besides receive tokens or hbars + - The alias property of the account does not have the public address + - Referred to as a hollow account + */ + AccountInfo accountInfo = new AccountInfoQuery() + .setAccountId(newAccountId) + .execute(client); + + if (((KeyList) accountInfo.key).isEmpty()) { + System.out.println("The newly created account is a hollow account"); + } else { + System.out.println("Not a hollow account"); + } + + /* + * Step 8 + * Create a HAPI transaction and assign the new hollow account as the transaction fee payer + * - To enhance the hollow account to have a public key the hollow account needs to be specified as a transaction fee payer in a HAPI transaction + */ + TransactionReceipt receipt2 = new TopicCreateTransaction() + .setTransactionId(TransactionId.generate(newAccountId)) + .setTopicMemo("Memo") + .freezeWith(client) + .sign(privateKey) + .execute(client) + .getReceipt(client); + System.out.println("Topic id = " + receipt2.topicId); + + /* + * + * Step 9 + * Get the `AccountInfo` for the account and return the public key on the account to show it is a complete account + */ + AccountInfo accountInfo2 = new AccountInfoQuery() + .setAccountId(newAccountId) + .execute(client); + + System.out.println("The public key of the newly created and now complete account: " + accountInfo2.key); + } +} diff --git a/examples/src/main/java/TransferUsingEvmAddressExample.java b/examples/src/main/java/TransferUsingEvmAddressExample.java new file mode 100644 index 0000000000..47d21cdfa3 --- /dev/null +++ b/examples/src/main/java/TransferUsingEvmAddressExample.java @@ -0,0 +1,129 @@ +import com.hedera.hashgraph.sdk.*; +import io.github.cdimascio.dotenv.Dotenv; + +import java.io.IOException; +import java.util.Objects; +import java.util.concurrent.TimeoutException; + +public class TransferUsingEvmAddressExample { + // see `.env.sample` in the repository root for how to specify these values + // or set environment variables with the same names + private static final AccountId OPERATOR_ID = AccountId.fromString(Objects.requireNonNull(Dotenv.load().get("OPERATOR_ID"))); + 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 + private static final String HEDERA_NETWORK = Dotenv.load().get("HEDERA_NETWORK", "testnet"); + + private TransferUsingEvmAddressExample() { + } + + /* + Transfer HBAR or tokens to a Hedera account using their public-address. + Reference: [HIP-583 Expand alias support in CryptoCreate & CryptoTransfer Transactions](https://hips.hedera.com/hip/hip-583) + ## Example 1 + - Create an ECSDA private key + - Extract the ECDSA public key + - Extract the Ethereum public address + - Add function to calculate the Ethereum Address to example in SDK + - Ethereum account address / public-address - This is the rightmost 20 bytes of the 32 byte Keccak-256 hash of the ECDSA public key of the account. This calculation is in the manner described by the Ethereum Yellow Paper. + - Transfer tokens using the `TransferTransaction` to the Etherum Account Address + - The From field should be a complete account that has a public address + - The To field should be to a public address (to create a new account) + - Get the child receipt or child record to return the Hedera Account ID for the new account that was created + - Get the `AccountInfo` on the new account and show it is a hollow account by not having a public key + - This is a hollow account in this state + - Use the hollow account as a transaction fee payer in a HAPI transaction + - Sign the transaction with ECDSA private key + - Get the `AccountInfo` of the account and show the account is now a complete account by returning the public key on the account + */ + public static void main(String[] args) throws PrecheckStatusException, TimeoutException, ReceiptStatusException, InterruptedException, IOException { + Client client = Client.forName(HEDERA_NETWORK); + + // Defaults the operator account ID and key such that all generated transactions will be paid for by this account and be signed by this key + client.setOperator(OPERATOR_ID, OPERATOR_KEY); + + /* + * Step 1 + * Create an ECSDA private key + */ + PrivateKey privateKey = PrivateKey.generateECDSA(); + + /* + * Step 2 + * Extract the ECDSA public key + */ + PublicKey publicKey = privateKey.getPublicKey(); + + /* + * Step 3 + * Extract the Ethereum public address + */ + EvmAddress evmAddress = publicKey.toEvmAddress(); + System.out.println("Corresponding evm address: " + evmAddress); + + /* + * Step 4 + * Transfer tokens using the `TransferTransaction` to the Etherum Account Address + * - The From field should be a complete account that has a public address + * - The To field should be to a public address (to create a new account) + */ + TransferTransaction transferTx = new TransferTransaction() + .addHbarTransfer(OPERATOR_ID, Hbar.from(10).negated()) + .addHbarTransfer(evmAddress, Hbar.from(10)) + .freezeWith(client); + + TransferTransaction transferTxSign = transferTx.sign(OPERATOR_KEY); + TransactionResponse transferTxSubmit = transferTxSign.execute(client); + + /* + * Step 5 + * Get the child receipt or child record to return the Hedera Account ID for the new account that was created + */ + TransactionReceipt receipt = new TransactionReceiptQuery() + .setTransactionId(transferTxSubmit.transactionId) + .setIncludeChildren(true) + .execute(client); + + AccountId newAccountId = receipt.children.get(0).accountId; + System.out.println(newAccountId); + + /* + * Step 6 + * Get the `AccountInfo` on the new account and show it is a hollow account by not having a public key + */ + AccountInfo accountInfo = new AccountInfoQuery() + .setAccountId(newAccountId) + .execute(client); + + System.out.println("accountInfo: " + accountInfo); + + /* + * Step 7 + * Use the hollow account as a transaction fee payer in a HAPI transaction + */ + client.setOperator(newAccountId, privateKey); + PublicKey newPublicKey = PrivateKey.generateED25519().getPublicKey(); + + AccountCreateTransaction transaction = new AccountCreateTransaction() + .setKey(newPublicKey) + .freezeWith(client); + + /* + * Step 8 + * Sign the transaction with ECDSA private key + */ + AccountCreateTransaction transactionSign = transaction.sign(privateKey); + TransactionResponse transactionSubmit = transactionSign.execute(client); + TransactionReceipt status = transactionSubmit.getReceipt(client); + System.out.println(status); + + /* + * Step 9 + * Get the `AccountInfo` of the account and show the account is now a complete account by returning the public key on the account + */ + AccountInfo accountInfo2 = new AccountInfoQuery() + .setAccountId(newAccountId) + .execute(client); + + System.out.println("The public key of the newly created and now complete account: " + accountInfo2.key); + } +} diff --git a/sdk/src/main/java/com/hedera/hashgraph/sdk/AccountCreateTransaction.java b/sdk/src/main/java/com/hedera/hashgraph/sdk/AccountCreateTransaction.java index 7213129fab..b2185d4f5b 100644 --- a/sdk/src/main/java/com/hedera/hashgraph/sdk/AccountCreateTransaction.java +++ b/sdk/src/main/java/com/hedera/hashgraph/sdk/AccountCreateTransaction.java @@ -60,7 +60,7 @@ public final class AccountCreateTransaction extends Transaction * The ethereum account 20-byte EVM address to be used as the account's alias. This EVM address may be either * the encoded form of the shard.realm.num or the keccak-256 hash of a ECDSA_SECP256K1 primitive key. @@ -392,15 +391,41 @@ public EvmAddress getAliasEvmAddress() { * If a transaction creates an account using an alias, any further crypto transfers to that alias will * simply be deposited in that account, without creating anything, and with no creation fee being charged. * - * @param aliasEvmAddress The ethereum account 20-byte EVM address + * @param evmAddress The ethereum account 20-byte EVM address * @return {@code this} */ - public AccountCreateTransaction setAliasEvmAddress(EvmAddress aliasEvmAddress) { + public AccountCreateTransaction setEvmAddress(EvmAddress evmAddress) { requireNotFrozen(); - this.aliasEvmAddress = aliasEvmAddress; + this.evmAddress = evmAddress; return this; } + /** + * NOT YET SUPPORTED ON MAINNET AS OF DEC/23/2022 + *

+ * The ethereum account 20-byte EVM address to be used as the account's alias. This EVM address may be either + * the encoded form of the shard.realm.num or the keccak-256 hash of a ECDSA_SECP256K1 primitive key. + *

+ * A given alias can map to at most one account on the network at a time. This uniqueness will be enforced + * relative to aliases currently on the network at alias assignment. + *

+ * If a transaction creates an account using an alias, any further crypto transfers to that alias will + * simply be deposited in that account, without creating anything, and with no creation fee being charged. + * + * @param evmAddress The ethereum account 20-byte EVM address + * @return {@code this} + * @throws IllegalArgumentException when evmAddress is invalid or doesn't have "0x" prefix + */ + public AccountCreateTransaction setEvmAddress(String evmAddress) { + if (evmAddress.startsWith("0x") || evmAddress.length() == 42) { + // Remove the "0x" prefix + EvmAddress address = EvmAddress.fromString(evmAddress.substring(2)); + return this.setEvmAddress(address); + } else { + throw new IllegalArgumentException("evmAddress must be an a valid EVM address with \"0x\" prefix"); + } + } + /** * Build the transaction body. * @@ -425,8 +450,6 @@ CryptoCreateTransactionBody.Builder build() { if (aliasKey != null) { builder.setAlias(aliasKey.toProtobufKey().toByteString()); - } else if (aliasEvmAddress != null) { - builder.setAlias(ByteString.copyFrom(aliasEvmAddress.toBytes())); } if (stakedAccountId != null) { @@ -435,6 +458,10 @@ CryptoCreateTransactionBody.Builder build() { builder.setStakedNodeId(stakedNodeId); } + if (evmAddress != null) { + builder.setEvmAddress(ByteString.copyFrom(evmAddress.toBytes())); + } + return builder; } @@ -479,7 +506,7 @@ void initFromTransactionBody() { } aliasKey = PublicKey.fromAliasBytes(body.getAlias()); - aliasEvmAddress = EvmAddress.fromAliasBytes(body.getAlias()); + evmAddress = EvmAddress.fromAliasBytes(body.getEvmAddress()); } @Override diff --git a/sdk/src/main/java/com/hedera/hashgraph/sdk/AccountId.java b/sdk/src/main/java/com/hedera/hashgraph/sdk/AccountId.java index 57dc0cd01b..5515f78ba8 100644 --- a/sdk/src/main/java/com/hedera/hashgraph/sdk/AccountId.java +++ b/sdk/src/main/java/com/hedera/hashgraph/sdk/AccountId.java @@ -26,8 +26,7 @@ import javax.annotation.Nonnegative; import javax.annotation.Nullable; -import java.util.Arrays; -import java.util.Objects; +import java.util.*; import java.util.regex.Pattern; /** @@ -58,7 +57,7 @@ public final class AccountId implements Comparable { public final PublicKey aliasKey; @Nullable - public final EvmAddress aliasEvmAddress; + public final EvmAddress evmAddress; @Nullable private final String checksum; @@ -98,7 +97,7 @@ public AccountId(@Nonnegative long shard, @Nonnegative long realm, @Nonnegative this.num = num; this.checksum = checksum; this.aliasKey = null; - this.aliasEvmAddress = null; + this.evmAddress = null; } /** @@ -115,14 +114,14 @@ public AccountId(@Nonnegative long shard, @Nonnegative long realm, @Nonnegative @Nonnegative long num, @Nullable String checksum, @Nullable PublicKey aliasKey, - @Nullable EvmAddress aliasEvmAddress + @Nullable EvmAddress evmAddress ) { this.shard = shard; this.realm = realm; this.num = num; this.checksum = checksum; this.aliasKey = aliasKey; - this.aliasEvmAddress = aliasEvmAddress; + this.evmAddress = evmAddress; } /** @@ -133,6 +132,9 @@ public AccountId(@Nonnegative long shard, @Nonnegative long realm, @Nonnegative * @throws IllegalArgumentException when the account id and checksum are invalid */ public static AccountId fromString(String id) { + if ((id.startsWith("0x") && id.length() == 42) || id.length() == 40) + return fromEvmAddress(id); + try { return EntityIdHelper.fromString(id, AccountId::new); } catch (IllegalArgumentException error) { @@ -156,6 +158,57 @@ public static AccountId fromString(String id) { } } + /** + * Retrieve the account id from an EVM address. + * + * @param evmAddress a string representing the EVM address + * @return the account id object + */ + public static AccountId fromEvmAddress(String evmAddress) { + return fromEvmAddress(evmAddress, 0, 0); + } + + /** + * Retrieve the account id from an EVM address. + * + * @param evmAddress a string representing the EVM address + * @param shard the shard part of the account id + * @param realm the shard realm of the account id + * @return the account id object + */ + public static AccountId fromEvmAddress(String evmAddress, @Nonnegative long shard, @Nonnegative long realm) { + return fromEvmAddress(EvmAddress.fromString(evmAddress), shard, realm); + } + + /** + * Retrieve the account id from an EVM address. + * + * @param evmAddress an EvmAddress instance + * @return the account id object + */ + public static AccountId fromEvmAddress(EvmAddress evmAddress) { + return fromEvmAddress(evmAddress, 0, 0); + } + + /** + * Retrieve the account id from an EVM address. + * + * @param evmAddress an EvmAddress instance + * @param shard the shard part of the account id + * @param realm the shard realm of the account id + * @return the account id object + */ + public static AccountId fromEvmAddress(EvmAddress evmAddress, @Nonnegative long shard, @Nonnegative long realm) { + return new AccountId( + shard, + realm, + 0, + null, + null, + evmAddress + ); + } + /** * Retrieve the account id from a solidity address. * @@ -213,13 +266,15 @@ AccountID toProtobuf() { var accountIdBuilder = AccountID.newBuilder() .setShardNum(shard) .setRealmNum(realm); + if (aliasKey != null) { accountIdBuilder.setAlias(aliasKey.toProtobufKey().toByteString()); - } else if (aliasEvmAddress != null) { - accountIdBuilder.setAlias(ByteString.copyFrom(aliasEvmAddress.toBytes())); + } else if (evmAddress != null) { + accountIdBuilder.setAlias(ByteString.copyFrom(evmAddress.toBytes())); }else { accountIdBuilder.setAccountNum(num); } + return accountIdBuilder.build(); } @@ -240,7 +295,7 @@ public void validate(Client client) throws BadEntityIdException { * @throws BadEntityIdException when the account id and checksum are invalid */ public void validateChecksum(Client client) throws BadEntityIdException { - if (aliasKey == null && aliasEvmAddress == null) { + if (aliasKey == null && evmAddress == null) { EntityIdHelper.validate(shard, realm, num, client, checksum); } } @@ -268,8 +323,8 @@ public byte[] toBytes() { public String toString() { if (aliasKey != null) { return "" + shard + "." + realm + "." + aliasKey.toStringDER(); - } else if (aliasEvmAddress != null) { - return "" + shard + "." + realm + "." + aliasEvmAddress.toString(); + } else if (evmAddress != null) { + return "" + shard + "." + realm + "." + evmAddress.toString(); } else { return EntityIdHelper.toString(shard, realm, num); } @@ -282,8 +337,8 @@ public String toString() { * @return the account id with checksum */ public String toStringWithChecksum(Client client) { - if (aliasKey != null || aliasEvmAddress != null) { - throw new IllegalStateException("toStringWithChecksum cannot be applied to AccountId with aliasKey or aliasEvmAddress"); + if (aliasKey != null || evmAddress != null) { + throw new IllegalStateException("toStringWithChecksum cannot be applied to AccountId with aliasKey or evmAddress"); } else { return EntityIdHelper.toStringWithChecksum(shard, realm, num, client, checksum); } @@ -293,7 +348,7 @@ public String toStringWithChecksum(Client client) { public int hashCode() { return Objects.hash( shard, realm, num, - (aliasKey != null) ? aliasKey.toBytes() : ((aliasEvmAddress != null) ? aliasEvmAddress.toBytes() : null) + (aliasKey != null) ? aliasKey.toBytes() : ((evmAddress != null) ? evmAddress.toBytes() : null) ); } @@ -311,12 +366,12 @@ public boolean equals(Object o) { if ((aliasKey == null) != (otherId.aliasKey == null)) { return false; } - if ((aliasEvmAddress == null) != (otherId.aliasEvmAddress == null)) { + if ((evmAddress == null) != (otherId.evmAddress == null)) { return false; } return shard == otherId.shard && realm == otherId.realm && num == otherId.num && (aliasKey == null || aliasKey.equals(otherId.aliasKey)) && - (aliasEvmAddress == null || aliasEvmAddress.equals(otherId.aliasEvmAddress)); + (evmAddress == null || evmAddress.equals(otherId.evmAddress)); } @Override @@ -340,12 +395,12 @@ public int compareTo(AccountId o) { if (aliasKey != null) { return aliasKey.toStringDER().compareTo(o.aliasKey.toStringDER()); } - if ((aliasEvmAddress == null) != (o.aliasEvmAddress == null)) { - return aliasEvmAddress != null ? 1 : -1; + if ((evmAddress == null) != (o.evmAddress == null)) { + return evmAddress != null ? 1 : -1; } - if (aliasEvmAddress == null) { + if (evmAddress == null) { return 0; } - return aliasEvmAddress.toString().compareTo(o.aliasEvmAddress.toString()); + return evmAddress.toString().compareTo(o.evmAddress.toString()); } } diff --git a/sdk/src/main/java/com/hedera/hashgraph/sdk/AccountInfo.java b/sdk/src/main/java/com/hedera/hashgraph/sdk/AccountInfo.java index 6a4268d2a5..88548f0b60 100644 --- a/sdk/src/main/java/com/hedera/hashgraph/sdk/AccountInfo.java +++ b/sdk/src/main/java/com/hedera/hashgraph/sdk/AccountInfo.java @@ -28,7 +28,6 @@ import org.threeten.bp.Instant; import javax.annotation.Nullable; -import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -158,6 +157,12 @@ public final class AccountInfo { @Nullable public final StakingInfo stakingInfo; + /** + * List of virtual addresses each of which is an EVM address that maps to an ECDSA key pair a user must prove ownership of. + */ + public final List virtualAddresses; + + /** * Constructor. * @@ -202,7 +207,8 @@ private AccountInfo( @Nullable PublicKey aliasKey, LedgerId ledgerId, long ethereumNonce, - @Nullable StakingInfo stakingInfo + @Nullable StakingInfo stakingInfo, + List virtualAddresses ) { this.accountId = accountId; this.contractAccountId = contractAccountId; @@ -228,6 +234,7 @@ private AccountInfo( this.tokenAllowances = Collections.emptyList(); this.tokenNftAllowances = Collections.emptyList(); this.stakingInfo = stakingInfo; + this.virtualAddresses = virtualAddresses; } /** @@ -257,6 +264,10 @@ static AccountInfo fromProtobuf(CryptoGetInfoResponse.AccountInfo accountInfo) { @Nullable var aliasKey = PublicKey.fromAliasBytes(accountInfo.getAlias()); + var virtualAddresses = J8Arrays.stream(accountInfo.getVirtualAddressesList().toArray()) + .map((virtualAddress) -> VirtualAddressInfo.fromProtobuf((com.hedera.hashgraph.sdk.proto.CryptoGetInfoResponse.AccountInfo.VirtualAddressInfo)virtualAddress)) + .collect(Collectors.toList()); + return new AccountInfo( accountId, accountInfo.getContractAccountID(), @@ -278,7 +289,8 @@ static AccountInfo fromProtobuf(CryptoGetInfoResponse.AccountInfo accountInfo) { aliasKey, LedgerId.fromByteString(accountInfo.getLedgerId()), accountInfo.getEthereumNonce(), - accountInfo.hasStakingInfo() ? StakingInfo.fromProtobuf(accountInfo.getStakingInfo()) : null + accountInfo.hasStakingInfo() ? StakingInfo.fromProtobuf(accountInfo.getStakingInfo()) : null, + virtualAddresses ); } @@ -303,6 +315,10 @@ CryptoGetInfoResponse.AccountInfo toProtobuf() { .map((liveHash) -> ((LiveHash) liveHash).toProtobuf()) .collect(Collectors.toList()); + var addresses = J8Arrays.stream(virtualAddresses.toArray()) + .map((virtualAddress) -> ((VirtualAddressInfo)virtualAddress).toProtobuf()) + .collect(Collectors.toList()); + var accountInfoBuilder = CryptoGetInfoResponse.AccountInfo.newBuilder() .setAccountID(accountId.toProtobuf()) .setDeleted(isDeleted) @@ -319,7 +335,8 @@ CryptoGetInfoResponse.AccountInfo toProtobuf() { .setOwnedNfts(ownedNfts) .setMaxAutomaticTokenAssociations(maxAutomaticTokenAssociations) .setLedgerId(ledgerId.toByteString()) - .setEthereumNonce(ethereumNonce); + .setEthereumNonce(ethereumNonce) + .addAllVirtualAddresses(addresses); if (contractAccountId != null) { accountInfoBuilder.setContractAccountID(contractAccountId); @@ -364,6 +381,7 @@ public String toString() { .add("ledgerId", ledgerId) .add("ethereumNonce", ethereumNonce) .add("stakingInfo", stakingInfo) + .add("virtualAddresses", virtualAddresses) .toString(); } diff --git a/sdk/src/main/java/com/hedera/hashgraph/sdk/ContractFunctionResult.java b/sdk/src/main/java/com/hedera/hashgraph/sdk/ContractFunctionResult.java index 3b988d10e4..4b32840bab 100644 --- a/sdk/src/main/java/com/hedera/hashgraph/sdk/ContractFunctionResult.java +++ b/sdk/src/main/java/com/hedera/hashgraph/sdk/ContractFunctionResult.java @@ -82,6 +82,8 @@ public final class ContractFunctionResult { @Nullable public final AccountId senderAccountId; + public final List contractNonces; + /** * Constructor. * @@ -128,6 +130,8 @@ public final class ContractFunctionResult { contractFunctionParametersBytes = inner.getFunctionParameters().toByteArray(); senderAccountId = inner.hasSenderId() ? AccountId.fromProtobuf(inner.getSenderId()) : null; + + contractNonces = StreamSupport.stream(inner.getContractNoncesList()).map(ContractNonce::fromProtobuf).collect(Collectors.toList()); } /** @@ -411,6 +415,10 @@ com.hedera.hashgraph.sdk.proto.ContractFunctionResult toProtobuf() { contractFunctionResult.setSenderId(senderAccountId.toProtobuf()); } + for (var contractNonce : contractNonces) { + contractFunctionResult.addContractNonces(contractNonce.toProtobuf()); + } + // for (var stateChange : stateChanges) { // contractFunctionResult.addStateChanges(stateChange.toProtobuf()); // } @@ -434,6 +442,7 @@ public String toString() { .add("contractFunctionparametersBytes", Hex.toHexString(contractFunctionParametersBytes)) .add("rawResult", Hex.toHexString(rawResult.toByteArray())) .add("senderAccountId", senderAccountId) + .add("contractNonces", contractNonces) .toString(); } } diff --git a/sdk/src/main/java/com/hedera/hashgraph/sdk/ContractNonce.java b/sdk/src/main/java/com/hedera/hashgraph/sdk/ContractNonce.java new file mode 100644 index 0000000000..6eaf6d278a --- /dev/null +++ b/sdk/src/main/java/com/hedera/hashgraph/sdk/ContractNonce.java @@ -0,0 +1,132 @@ +/*- + * + * Hedera Java SDK + * + * Copyright (C) 2020 - 2022 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.common.base.MoreObjects; +import com.google.protobuf.InvalidProtocolBufferException; +import com.hedera.hashgraph.sdk.proto.ContractNonceInfo; + +import java.util.Objects; + +/** + * Info about a contract account's nonce value. + * A nonce of a contract is only incremented when that contract creates another contract. + */ +public final class ContractNonce { + + /** + * The ID of the contract. + */ + public final ContractId contractId; + + /** + * The current value of the contract account's nonce property. + */ + public final long nonce; + + /** + * Constructor. + * + * @param contractId the contract ID + * @param nonce the value of the contract account's nonce + */ + public ContractNonce( + ContractId contractId, + long nonce + ) { + this.contractId = contractId; + this.nonce = nonce; + } + + /** + * Retrieve the contract nonce from a protobuf. + * + * @param contractNonce the contract nonce protobuf + * @return the contract nonce object + */ + static ContractNonce fromProtobuf(ContractNonceInfo contractNonce) { + return new ContractNonce( + ContractId.fromProtobuf(contractNonce.getContractId()), + contractNonce.getNonce() + ); + } + + /** + * Retrieve the contract nonce from a protobuf byte array. + * + * @param bytes a byte array representing the protobuf + * @return the contract nonce object + * @throws InvalidProtocolBufferException when there is an issue with the protobuf + */ + public static ContractNonce fromBytes(byte[] bytes) throws InvalidProtocolBufferException { + return fromProtobuf(ContractNonceInfo.parseFrom(bytes).toBuilder().build()); + } + + /** + * Convert a contract nonce object into a protobuf. + * + * @return the protobuf object + */ + ContractNonceInfo toProtobuf() { + var contractNonceBuilder = ContractNonceInfo.newBuilder() + .setContractId(contractId.toProtobuf()) + .setNonce(nonce); + + return contractNonceBuilder.build(); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("contractId", contractId) + .add("nonce", nonce) + .toString(); + } + + /** + * Extract a byte array representation. + * + * @return a byte array representation + */ + public byte[] toBytes() { + return toProtobuf().toByteArray(); + } + + + @Override + public int hashCode() { + return Objects.hash(contractId, nonce); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + + if (!(o instanceof ContractNonce)) { + return false; + } + + ContractNonce otherNonce = (ContractNonce)o; + + return contractId.equals(otherNonce.contractId) && nonce == otherNonce.nonce; + } +} diff --git a/sdk/src/main/java/com/hedera/hashgraph/sdk/EvmAddress.java b/sdk/src/main/java/com/hedera/hashgraph/sdk/EvmAddress.java index 6d214c14a7..0122b213f8 100644 --- a/sdk/src/main/java/com/hedera/hashgraph/sdk/EvmAddress.java +++ b/sdk/src/main/java/com/hedera/hashgraph/sdk/EvmAddress.java @@ -41,7 +41,8 @@ public EvmAddress(byte[] bytes) { } public static EvmAddress fromString(String text) { - return new EvmAddress(Hex.decode(text)); + String address = text.startsWith("0x") ? text.substring(2) : text; + return new EvmAddress(Hex.decode(address)); } @Nullable diff --git a/sdk/src/main/java/com/hedera/hashgraph/sdk/Key.java b/sdk/src/main/java/com/hedera/hashgraph/sdk/Key.java index d8559a008c..a44346882c 100644 --- a/sdk/src/main/java/com/hedera/hashgraph/sdk/Key.java +++ b/sdk/src/main/java/com/hedera/hashgraph/sdk/Key.java @@ -52,30 +52,32 @@ public abstract class Key { */ static Key fromProtobufKey(com.hedera.hashgraph.sdk.proto.Key key) { switch (key.getKeyCase()) { - case ED25519: + case ED25519 -> { return new PublicKeyED25519(key.getEd25519().toByteArray()); - - case ECDSA_SECP256K1: + } + case ECDSA_SECP256K1 -> { if (key.getECDSASecp256K1().size() == 20) { return new EvmAddress(key.getECDSASecp256K1().toByteArray()); } else { return new PublicKeyECDSA(key.getECDSASecp256K1().toByteArray()); } - - case KEYLIST: + } + case KEYLIST -> { return KeyList.fromProtobuf(key.getKeyList(), null); - - case THRESHOLDKEY: + } + case THRESHOLDKEY -> { return KeyList.fromProtobuf(key.getThresholdKey().getKeys(), key.getThresholdKey().getThreshold()); - - case CONTRACTID: + } + case CONTRACTID -> { return ContractId.fromProtobuf(key.getContractID()); - - case DELEGATABLE_CONTRACT_ID: + } + case DELEGATABLE_CONTRACT_ID -> { return DelegateContractId.fromProtobuf(key.getDelegatableContractId()); - - default: - throw new IllegalStateException("Key#fromProtobuf: unhandled key case: " + key.getKeyCase()); + } + case KEY_NOT_SET -> { + return null; + } + default -> throw new IllegalStateException("Key#fromProtobuf: unhandled key case: " + key.getKeyCase()); } } diff --git a/sdk/src/main/java/com/hedera/hashgraph/sdk/PublicKey.java b/sdk/src/main/java/com/hedera/hashgraph/sdk/PublicKey.java index 744fe8f75c..b8287d9b57 100644 --- a/sdk/src/main/java/com/hedera/hashgraph/sdk/PublicKey.java +++ b/sdk/src/main/java/com/hedera/hashgraph/sdk/PublicKey.java @@ -278,4 +278,11 @@ public AccountId toAccountId(@Nonnegative long shard, @Nonnegative long realm) { * @return is this an ECDSA key */ public abstract boolean isECDSA(); + + /** + * Converts the key to EVM address + * + * @return the EVM address + */ + public abstract EvmAddress toEvmAddress(); } diff --git a/sdk/src/main/java/com/hedera/hashgraph/sdk/PublicKeyECDSA.java b/sdk/src/main/java/com/hedera/hashgraph/sdk/PublicKeyECDSA.java index 53c6ebaadf..06e2f87d15 100644 --- a/sdk/src/main/java/com/hedera/hashgraph/sdk/PublicKeyECDSA.java +++ b/sdk/src/main/java/com/hedera/hashgraph/sdk/PublicKeyECDSA.java @@ -25,12 +25,15 @@ import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; import org.bouncycastle.crypto.params.ECPublicKeyParameters; import org.bouncycastle.crypto.signers.ECDSASigner; +import org.bouncycastle.jcajce.provider.digest.Keccak; +import org.bouncycastle.util.encoders.Hex; -import javax.annotation.Nullable; import java.io.IOException; import java.math.BigInteger; import java.util.Arrays; +import static com.hedera.hashgraph.sdk.Crypto.calcKeccak256; + /** * Encapsulate the ECDSA public key. */ @@ -85,7 +88,7 @@ ByteString extractSignatureFromProtobuf(SignaturePair pair) { @Override public boolean verify(byte[] message, byte[] signature) { - var hash = Crypto.calcKeccak256(message); + var hash = calcKeccak256(message); ECDSASigner signer = new ECDSASigner(); signer.init(false, new ECPublicKeyParameters( @@ -136,6 +139,17 @@ public byte[] toBytesRaw() { return Arrays.copyOf(keyData, keyData.length); } + @Override + public EvmAddress toEvmAddress() { + // Calculate the Keccak-256 hash of the uncompressed key without "04" prefix + byte[] uncompressed = Key.ECDSA_SECP256K1_CURVE + .getCurve().decodePoint(toBytesRaw()).getEncoded(false); + byte[] keccakBytes = calcKeccak256(Arrays.copyOfRange(uncompressed, 1, uncompressed.length)); + + // Return the last 20 bytes + return EvmAddress.fromBytes(Arrays.copyOfRange(keccakBytes, 12, 32)); + } + @Override public boolean equals( Object o) { if (this == o) { diff --git a/sdk/src/main/java/com/hedera/hashgraph/sdk/PublicKeyED25519.java b/sdk/src/main/java/com/hedera/hashgraph/sdk/PublicKeyED25519.java index 6009bb3466..b87780b5d6 100644 --- a/sdk/src/main/java/com/hedera/hashgraph/sdk/PublicKeyED25519.java +++ b/sdk/src/main/java/com/hedera/hashgraph/sdk/PublicKeyED25519.java @@ -117,6 +117,11 @@ public byte[] toBytesRaw() { return keyData; } + @Override + public EvmAddress toEvmAddress() { + throw new IllegalStateException("unsupported operation on Ed25519PublicKey"); + } + @Override public boolean equals( Object o) { if (this == o) { diff --git a/sdk/src/main/java/com/hedera/hashgraph/sdk/TransactionRecord.java b/sdk/src/main/java/com/hedera/hashgraph/sdk/TransactionRecord.java index 2ae1504784..dfd1af486d 100644 --- a/sdk/src/main/java/com/hedera/hashgraph/sdk/TransactionRecord.java +++ b/sdk/src/main/java/com/hedera/hashgraph/sdk/TransactionRecord.java @@ -182,6 +182,12 @@ public final class TransactionRecord { @Nullable public final Integer prngNumber; + /** + * The new default EVM address of the account created by this transaction. + * This field is populated only when the EVM address is not specified in the related transaction body. + */ + public final ByteString evmAddress; + TransactionRecord( TransactionReceipt transactionReceipt, ByteString transactionHash, @@ -204,7 +210,8 @@ public final class TransactionRecord { ByteString ethereumHash, List paidStakingRewards, @Nullable ByteString prngBytes, - @Nullable Integer prngNumber + @Nullable Integer prngNumber, + ByteString evmAddress ) { this.receipt = transactionReceipt; this.transactionHash = transactionHash; @@ -231,6 +238,7 @@ public final class TransactionRecord { this.paidStakingRewards = paidStakingRewards; this.prngBytes = prngBytes; this.prngNumber = prngNumber; + this.evmAddress = evmAddress; } /** @@ -317,7 +325,8 @@ static TransactionRecord fromProtobuf( transactionRecord.getEthereumHash(), paidStakingRewards, transactionRecord.hasPrngBytes() ? transactionRecord.getPrngBytes() : null, - transactionRecord.hasPrngNumber() ? transactionRecord.getPrngNumber() : null + transactionRecord.hasPrngNumber() ? transactionRecord.getPrngNumber() : null, + transactionRecord.getEvmAddress() ); } @@ -373,7 +382,8 @@ com.hedera.hashgraph.sdk.proto.TransactionRecord toProtobuf() { .setMemo(transactionMemo) .setTransactionFee(transactionFee.toTinybars()) .setTransferList(transferList) - .setEthereumHash(ethereumHash); + .setEthereumHash(ethereumHash) + .setEvmAddress(evmAddress); for (var tokenEntry : tokenTransfers.entrySet()) { var tokenTransfersList = TokenTransferList.newBuilder() @@ -468,6 +478,7 @@ public String toString() { .add("paidStakingRewards", paidStakingRewards) .add("prngBytes", prngBytes != null ? Hex.toHexString(prngBytes.toByteArray()) : null) .add("prngNumber", prngNumber) + .add("evmAddress", Hex.toHexString(evmAddress.toByteArray())) .toString(); } diff --git a/sdk/src/main/java/com/hedera/hashgraph/sdk/TransferTransaction.java b/sdk/src/main/java/com/hedera/hashgraph/sdk/TransferTransaction.java index befc019e29..a321592975 100644 --- a/sdk/src/main/java/com/hedera/hashgraph/sdk/TransferTransaction.java +++ b/sdk/src/main/java/com/hedera/hashgraph/sdk/TransferTransaction.java @@ -421,6 +421,18 @@ private TransferTransaction doAddHbarTransfer(AccountId accountId, Hbar value, b return this; } + /** + * Add a non approved hbar transfer to an EVM address. + * + * @param evmAddress the EVM address + * @param value the value + * @return the updated transaction + */ + public TransferTransaction addHbarTransfer(EvmAddress evmAddress, Hbar value) { + AccountId accountId = AccountId.fromEvmAddress(evmAddress); + return doAddHbarTransfer(accountId, value, false); + } + /** * Add a non approved hbar transfer. * diff --git a/sdk/src/main/java/com/hedera/hashgraph/sdk/VirtualAddress.java b/sdk/src/main/java/com/hedera/hashgraph/sdk/VirtualAddress.java new file mode 100644 index 0000000000..ea42f2b434 --- /dev/null +++ b/sdk/src/main/java/com/hedera/hashgraph/sdk/VirtualAddress.java @@ -0,0 +1,132 @@ +/*- + * + * Hedera Java SDK + * + * Copyright (C) 2020 - 2022 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.common.base.MoreObjects; +import com.google.protobuf.ByteString; +import com.google.protobuf.InvalidProtocolBufferException; + +import java.util.Objects; + +/** + * EVM virtual address. + */ +public final class VirtualAddress { + + /** + * The EVM address. + */ + public final String address; + + /** + * If true, then this is the default virtual address for a given account. + */ + public final boolean isDefault; + + /** + * Constructor. + * + * @param address the virtual address + * @param isDefault is it the default address + */ + public VirtualAddress( + String address, + boolean isDefault + ) { + this.address = address; + this.isDefault = isDefault; + } + + /** + * Retrieve the virtual address from a protobuf. + * + * @param virtualAddress the virtual address protobuf + * @return the virtual address object + */ + static VirtualAddress fromProtobuf(com.hedera.hashgraph.sdk.proto.VirtualAddress virtualAddress) { + return new VirtualAddress( + virtualAddress.getAddress().toString(), + virtualAddress.getIsDefault() + ); + } + + /** + * Retrieve the virtual address from a protobuf byte array. + * + * @param bytes a byte array representing the protobuf + * @return the virtual address object + * @throws InvalidProtocolBufferException when there is an issue with the protobuf + */ + public static VirtualAddress fromBytes(byte[] bytes) throws InvalidProtocolBufferException { + return fromProtobuf(com.hedera.hashgraph.sdk.proto.VirtualAddress.parseFrom(bytes).toBuilder().build()); + } + + /** + * Convert a virtual address object into a protobuf. + * + * @return the protobuf object + */ + com.hedera.hashgraph.sdk.proto.VirtualAddress toProtobuf() { + var virtualAddressBuilder = com.hedera.hashgraph.sdk.proto.VirtualAddress.newBuilder() + .setAddress(ByteString.fromHex(address)) + .setIsDefault(isDefault); + + return virtualAddressBuilder.build(); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("address", address) + .add("default", isDefault) + .toString(); + } + + /** + * Extract a byte array representation. + * + * @return a byte array representation + */ + public byte[] toBytes() { + return toProtobuf().toByteArray(); + } + + + @Override + public int hashCode() { + // TODO - should addresses "0x123abc" and "123abc" be considered equal??? + return Objects.hash(address, isDefault); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + + if (!(o instanceof VirtualAddress)) { + return false; + } + + VirtualAddress otherAddress = (VirtualAddress)o; + // TODO - should addresses "0x123abc" and "123abc" be considered equal??? + return address.equals(otherAddress.address) && isDefault == otherAddress.isDefault; + } +} diff --git a/sdk/src/main/java/com/hedera/hashgraph/sdk/VirtualAddressInfo.java b/sdk/src/main/java/com/hedera/hashgraph/sdk/VirtualAddressInfo.java new file mode 100644 index 0000000000..8aeae53a3c --- /dev/null +++ b/sdk/src/main/java/com/hedera/hashgraph/sdk/VirtualAddressInfo.java @@ -0,0 +1,146 @@ +/*- + * + * Hedera Java SDK + * + * Copyright (C) 2020 - 2022 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.common.base.MoreObjects; +import com.google.protobuf.ByteString; +import com.google.protobuf.InvalidProtocolBufferException; + +import java.util.Objects; + +/** + * EVM virtual address. + */ +public final class VirtualAddressInfo { + + /** + * The EVM address. + */ + public final String address; + + /** + * If true, then this is the default virtual address for a given account. + */ + public final boolean isDefault; + + /** + * The EthereumTransaction type nonce associated with this virtual address. + */ + public final long nonce; + + /** + * Constructor. + * + * @param address the virtual address + * @param isDefault is it the default address + * @param nonce nonce associated with this virtual address + */ + public VirtualAddressInfo( + String address, + boolean isDefault, + long nonce + ) { + this.address = address; + this.isDefault = isDefault; + this.nonce = nonce; + } + + /** + * Retrieve the virtual address from a protobuf. + * + * @param virtualAddress the virtual address protobuf + * @return the virtual address object + */ + static VirtualAddressInfo fromProtobuf(com.hedera.hashgraph.sdk.proto.CryptoGetInfoResponse.AccountInfo.VirtualAddressInfo virtualAddress) { + return new VirtualAddressInfo( + virtualAddress.getAddress().toString(), + virtualAddress.getIsDefault(), + virtualAddress.getNonce() + ); + } + + /** + * Retrieve the virtual address from a protobuf byte array. + * + * @param bytes a byte array representing the protobuf + * @return the virtual address object + * @throws InvalidProtocolBufferException when there is an issue with the protobuf + */ + public static VirtualAddressInfo fromBytes(byte[] bytes) throws InvalidProtocolBufferException { + return fromProtobuf(com.hedera.hashgraph.sdk.proto.CryptoGetInfoResponse.AccountInfo.VirtualAddressInfo.parseFrom(bytes).toBuilder().build()); + } + + /** + * Convert a virtual address object into a protobuf. + * + * @return the protobuf object + */ + com.hedera.hashgraph.sdk.proto.CryptoGetInfoResponse.AccountInfo.VirtualAddressInfo toProtobuf() { + var virtualAddressBuilder = com.hedera.hashgraph.sdk.proto.CryptoGetInfoResponse.AccountInfo.VirtualAddressInfo.newBuilder() + .setAddress(ByteString.fromHex(address)) + .setIsDefault(isDefault) + .setNonce(nonce); + + return virtualAddressBuilder.build(); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("address", address) + .add("default", isDefault) + .add("nonce", nonce) + .toString(); + } + + /** + * Extract a byte array representation. + * + * @return a byte array representation + */ + public byte[] toBytes() { + return toProtobuf().toByteArray(); + } + + + @Override + public int hashCode() { + // TODO - should addresses "0x123abc" and "123abc" be considered equal??? + return Objects.hash(address, isDefault, nonce); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + + if (!(o instanceof VirtualAddressInfo)) { + return false; + } + + VirtualAddressInfo otherAddress = (VirtualAddressInfo)o; + // TODO - should addresses "0x123abc" and "123abc" be considered equal??? + return + address.equals(otherAddress.address) && + isDefault == otherAddress.isDefault && + nonce == otherAddress.nonce; + } +} diff --git a/sdk/src/main/proto/basic_types.proto b/sdk/src/main/proto/basic_types.proto index 094edc6bb6..cabfef74f5 100644 --- a/sdk/src/main/proto/basic_types.proto +++ b/sdk/src/main/proto/basic_types.proto @@ -1607,3 +1607,20 @@ message VirtualAddress { */ bool is_default = 2; } + +/** + * Info about a contract account's nonce value. + * A nonce of a contract is only incremented when that contract creates another contract. + */ +message ContractNonceInfo { + + /** + * Id of the contract + */ + ContractID contract_id = 1; + + /** + * The current value of the contract account's nonce property + */ + int64 nonce = 2; +} \ No newline at end of file diff --git a/sdk/src/main/proto/contract_call_local.proto b/sdk/src/main/proto/contract_call_local.proto index 8d4520936e..d9814d95f1 100644 --- a/sdk/src/main/proto/contract_call_local.proto +++ b/sdk/src/main/proto/contract_call_local.proto @@ -162,6 +162,11 @@ message ContractFunctionResult { * ContractCreateTransactionBody or a ContractCallTransactionBody. */ AccountID sender_id = 13; + + /** + * A list of updated contract account nonces containing the new nonce value for each contract account + */ + repeated ContractNonceInfo contract_nonces = 14; } /** diff --git a/sdk/src/main/proto/crypto_get_info.proto b/sdk/src/main/proto/crypto_get_info.proto index 02a9c8f949..773c4e8d02 100644 --- a/sdk/src/main/proto/crypto_get_info.proto +++ b/sdk/src/main/proto/crypto_get_info.proto @@ -182,10 +182,27 @@ message CryptoGetInfoResponse { */ StakingInfo staking_info = 22; + message VirtualAddressInfo { + /** + * The 20-byte EVM address that is derived from the keccak-256 hash of a ECDSA_SECP256K1 primitive key. + */ + bytes address = 1; + + /** + * Flag if this address should now be set or is the default address on the account. + */ + bool is_default = 2; + + /** + * The EthereumTransaction type nonce associated with this virtual address. + */ + int64 nonce = 3; + } + /** - * List of virtual addresses each of which is an EVM address that maps to an ECDSA key pair a user must prove ownership of. + * List of virtual addresses each of which is an EVM address that maps to an ECDSA key pair a user must prove ownership of and their corresponding nonce value. */ - repeated VirtualAddress virtual_addresses = 24; + repeated VirtualAddressInfo virtual_addresses = 24; } /** diff --git a/sdk/src/test/java/com/hedera/hashgraph/sdk/AccountCreateTransactionTest.java b/sdk/src/test/java/com/hedera/hashgraph/sdk/AccountCreateTransactionTest.java index 9d89cadb4a..df4b2415e4 100644 --- a/sdk/src/test/java/com/hedera/hashgraph/sdk/AccountCreateTransactionTest.java +++ b/sdk/src/test/java/com/hedera/hashgraph/sdk/AccountCreateTransactionTest.java @@ -75,7 +75,7 @@ AccountCreateTransaction spawnTestTransaction2() { .setReceiverSignatureRequired(true) .setAutoRenewPeriod(Duration.ofHours(10)) .setStakedNodeId(4L) - .setAliasEvmAddress(EvmAddress.fromString("302a300506032b6570032100114e6abc371b82da")) + .setEvmAddress(EvmAddress.fromString("302a300506032b6570032100114e6abc371b82da")) .setMaxAutomaticTokenAssociations(100) .setMaxTransactionFee(Hbar.fromTinybars(100_000)) .freeze() diff --git a/sdk/src/test/java/com/hedera/hashgraph/sdk/AccountCreateTransactionTest.snap b/sdk/src/test/java/com/hedera/hashgraph/sdk/AccountCreateTransactionTest.snap index 5ba48b5321..74b2c853b7 100644 --- a/sdk/src/test/java/com/hedera/hashgraph/sdk/AccountCreateTransactionTest.snap +++ b/sdk/src/test/java/com/hedera/hashgraph/sdk/AccountCreateTransactionTest.snap @@ -1,8 +1,8 @@ com.hedera.hashgraph.sdk.AccountCreateTransactionTest.shouldSerialize2=[ - "# com.hedera.hashgraph.sdk.proto.TransactionBody\ncrypto_create_account {\n alias: \"0*0\\005\\006\\003+ep\\003!\\000\\021Nj\\2747\\033\\202\\332\"\n auto_renew_period {\n seconds: 36000\n }\n initial_balance: 450\n key {\n ed25519: \"\\340\\310\\354\\'X\\245\\207\\237\\372\\302&\\241<\\fQky\\236r\\343QA\\240\\335\\202\\217\\224\\323y\\210\\244\\267\"\n }\n max_automatic_token_associations: 100\n memo: \"some dumb memo\"\n proxy_account_i_d {\n account_num: 1001\n realm_num: 0\n shard_num: 0\n }\n receive_record_threshold: 0\n receiver_sig_required: true\n send_record_threshold: 0\n staked_node_id: 4\n}\nnode_account_i_d {\n account_num: 5005\n realm_num: 0\n shard_num: 0\n}\ntransaction_fee: 100000\ntransaction_i_d {\n account_i_d {\n account_num: 5006\n realm_num: 0\n shard_num: 0\n }\n transaction_valid_start {\n seconds: 1554158542\n }\n}\ntransaction_valid_duration {\n seconds: 120\n}" + "# com.hedera.hashgraph.sdk.proto.TransactionBody\ncrypto_create_account {\n auto_renew_period {\n seconds: 36000\n }\n evm_address: \"0*0\\005\\006\\003+ep\\003!\\000\\021Nj\\2747\\033\\202\\332\"\n initial_balance: 450\n key {\n ed25519: \"\\340\\310\\354\\'X\\245\\207\\237\\372\\302&\\241<\\fQky\\236r\\343QA\\240\\335\\202\\217\\224\\323y\\210\\244\\267\"\n }\n max_automatic_token_associations: 100\n memo: \"some dumb memo\"\n proxy_account_i_d {\n account_num: 1001\n realm_num: 0\n shard_num: 0\n }\n receive_record_threshold: 0\n receiver_sig_required: true\n send_record_threshold: 0\n staked_node_id: 4\n}\nnode_account_i_d {\n account_num: 5005\n realm_num: 0\n shard_num: 0\n}\ntransaction_fee: 100000\ntransaction_i_d {\n account_i_d {\n account_num: 5006\n realm_num: 0\n shard_num: 0\n }\n transaction_valid_start {\n seconds: 1554158542\n }\n}\ntransaction_valid_duration {\n seconds: 120\n}" ] com.hedera.hashgraph.sdk.AccountCreateTransactionTest.shouldSerialize=[ "# com.hedera.hashgraph.sdk.proto.TransactionBody\ncrypto_create_account {\n alias: \"\\022 \\207v\\306\\2701\\241\\266\\032\\301\\r\\254\\003\\004\\242\\204=\\344qoT\\261\\221\\233\\271\\032&\\205\\320\\376?0H\"\n auto_renew_period {\n seconds: 36000\n }\n initial_balance: 450\n key {\n ed25519: \"\\340\\310\\354\\'X\\245\\207\\237\\372\\302&\\241<\\fQky\\236r\\343QA\\240\\335\\202\\217\\224\\323y\\210\\244\\267\"\n }\n max_automatic_token_associations: 100\n memo: \"some dumb memo\"\n proxy_account_i_d {\n account_num: 1001\n realm_num: 0\n shard_num: 0\n }\n receive_record_threshold: 0\n receiver_sig_required: true\n send_record_threshold: 0\n staked_account_id {\n account_num: 3\n realm_num: 0\n shard_num: 0\n }\n}\nnode_account_i_d {\n account_num: 5005\n realm_num: 0\n shard_num: 0\n}\ntransaction_fee: 100000\ntransaction_i_d {\n account_i_d {\n account_num: 5006\n realm_num: 0\n shard_num: 0\n }\n transaction_valid_start {\n seconds: 1554158542\n }\n}\ntransaction_valid_duration {\n seconds: 120\n}" -] \ No newline at end of file +] diff --git a/sdk/src/test/java/com/hedera/hashgraph/sdk/AccountIdTest.java b/sdk/src/test/java/com/hedera/hashgraph/sdk/AccountIdTest.java index 7f32655f03..db4914b52c 100644 --- a/sdk/src/test/java/com/hedera/hashgraph/sdk/AccountIdTest.java +++ b/sdk/src/test/java/com/hedera/hashgraph/sdk/AccountIdTest.java @@ -143,7 +143,7 @@ void fromStringWithAliasKey() { } @Test - void fromStringWithAliasEvmAddress() { + void fromStringWithEvmAddress() { SnapshotMatcher.expect(AccountId.fromString("0.0.302a300506032b6570032100114e6abc371b82da").toString()).toMatchSnapshot(); } @@ -168,7 +168,7 @@ void toBytesAlias() throws InvalidProtocolBufferException { } @Test - void toBytesAliasEvmAddress() { + void toBytesEvmAddress() { SnapshotMatcher.expect(Hex.toHexString(AccountId.fromString("0.0.302a300506032b6570032100114e6abc371b82da").toBytes())).toMatchSnapshot(); } @@ -204,19 +204,36 @@ void toFromProtobufEcdsaAliasKey() { } @Test - void fromBytesAliasEvmAddress() throws InvalidProtocolBufferException { + void fromBytesEvmAddress() throws InvalidProtocolBufferException { SnapshotMatcher.expect(AccountId.fromBytes(AccountId.fromString("0.0.302a300506032b6570032100114e6abc371b82da").toBytes()).toString()).toMatchSnapshot(); } @Test - void toFromProtobufAliasEvmAddress() { + void toFromProtobufEvmAddress() { var id1 = AccountId.fromString("0.0.302a300506032b6570032100114e6abc371b82da"); var id2 = AccountId.fromProtobuf(id1.toProtobuf()); assertThat(id2).isEqualTo(id1); } + @Test + void toFromProtobufRawEvmAddress() { + var id1 = AccountId.fromString("302a300506032b6570032100114e6abc371b82da"); + var id2 = AccountId.fromProtobuf(id1.toProtobuf()); + assertThat(id2).isEqualTo(id1); + } + @Test void toSolidityAddress() { SnapshotMatcher.expect(new AccountId(5005).toSolidityAddress()).toMatchSnapshot(); } + + @Test + void fromEvmAddress() { + String evmAddress = "302a300506032b6570032100114e6abc371b82da"; + var id = AccountId.fromEvmAddress(evmAddress, 5, 9); + + assertThat(id.evmAddress.toString()).isEqualTo(evmAddress); + assertThat(id.shard).isEqualTo(5); + assertThat(id.realm).isEqualTo(9); + } } diff --git a/sdk/src/test/java/com/hedera/hashgraph/sdk/AccountIdTest.snap b/sdk/src/test/java/com/hedera/hashgraph/sdk/AccountIdTest.snap index e8785a43c5..ba6d16f58b 100644 --- a/sdk/src/test/java/com/hedera/hashgraph/sdk/AccountIdTest.snap +++ b/sdk/src/test/java/com/hedera/hashgraph/sdk/AccountIdTest.snap @@ -8,7 +8,7 @@ com.hedera.hashgraph.sdk.AccountIdTest.fromBytesAlias=[ ] -com.hedera.hashgraph.sdk.AccountIdTest.fromBytesAliasEvmAddress=[ +com.hedera.hashgraph.sdk.AccountIdTest.fromBytesEvmAddress=[ "0.0.302a300506032b6570032100114e6abc371b82da" ] @@ -28,11 +28,6 @@ com.hedera.hashgraph.sdk.AccountIdTest.fromString=[ ] -com.hedera.hashgraph.sdk.AccountIdTest.fromStringWithAliasEvmAddress=[ - "0.0.302a300506032b6570032100114e6abc371b82da" -] - - com.hedera.hashgraph.sdk.AccountIdTest.fromStringWithAliasKey=[ "0.0.302a300506032b6570032100114e6abc371b82dab5c15ea149f02d34a012087b163516dd70f44acafabf7777" ] @@ -53,6 +48,11 @@ com.hedera.hashgraph.sdk.AccountIdTest.fromStringWithChecksumOnTestnet=[ ] +com.hedera.hashgraph.sdk.AccountIdTest.fromStringWithEvmAddress=[ + "0.0.302a300506032b6570032100114e6abc371b82da" +] + + com.hedera.hashgraph.sdk.AccountIdTest.toBytes=[ "188d27" ] @@ -63,7 +63,7 @@ com.hedera.hashgraph.sdk.AccountIdTest.toBytesAlias=[ ] -com.hedera.hashgraph.sdk.AccountIdTest.toBytesAliasEvmAddress=[ +com.hedera.hashgraph.sdk.AccountIdTest.toBytesEvmAddress=[ "2214302a300506032b6570032100114e6abc371b82da" ] diff --git a/sdk/src/test/java/com/hedera/hashgraph/sdk/AccountInfoTest.snap b/sdk/src/test/java/com/hedera/hashgraph/sdk/AccountInfoTest.snap index 2bef9a59af..becb516d70 100644 --- a/sdk/src/test/java/com/hedera/hashgraph/sdk/AccountInfoTest.snap +++ b/sdk/src/test/java/com/hedera/hashgraph/sdk/AccountInfoTest.snap @@ -1,10 +1,10 @@ com.hedera.hashgraph.sdk.AccountInfoTest.fromBytes=[ - "AccountInfo{accountId=0.0.1, contractAccountId=, deleted=true, proxyAccountId=0.0.8, proxyReceived=2 tℏ, key=302a300506032b6570032100e0c8ec2758a5879ffac226a13c0c516b799e72e35141a0dd828f94d37988a4b7, balance=3 tℏ, sendRecordThreshold=4 tℏ, receiveRecordThreshold=5 tℏ, receiverSignatureRequired=true, expirationTime=1970-01-01T00:00:00.006Z, autoRenewPeriod=PT168H, liveHashes=[LiveHash{accountId=0.0.10, hash=[0, 1, 2], keys=KeyList{threshold=null, keys=[302a300506032b6570032100e0c8ec2758a5879ffac226a13c0c516b799e72e35141a0dd828f94d37988a4b7]}, duration=PT264H}], tokenRelationships={}, accountMemo=, ownedNfts=0, maxAutomaticTokenAssociations=0, aliasKey=null, ledgerId=previewnet, ethereumNonce=1001, stakingInfo=null}" + "AccountInfo{accountId=0.0.1, contractAccountId=, deleted=true, proxyAccountId=0.0.8, proxyReceived=2 tℏ, key=302a300506032b6570032100e0c8ec2758a5879ffac226a13c0c516b799e72e35141a0dd828f94d37988a4b7, balance=3 tℏ, sendRecordThreshold=4 tℏ, receiveRecordThreshold=5 tℏ, receiverSignatureRequired=true, expirationTime=1970-01-01T00:00:00.006Z, autoRenewPeriod=PT168H, liveHashes=[LiveHash{accountId=0.0.10, hash=[0, 1, 2], keys=KeyList{threshold=null, keys=[302a300506032b6570032100e0c8ec2758a5879ffac226a13c0c516b799e72e35141a0dd828f94d37988a4b7]}, duration=PT264H}], tokenRelationships={}, accountMemo=, ownedNfts=0, maxAutomaticTokenAssociations=0, aliasKey=null, ledgerId=previewnet, ethereumNonce=1001, stakingInfo=null, virtualAddresses=[]}" ] com.hedera.hashgraph.sdk.AccountInfoTest.fromProtobufWithOtherOptions=[ - "AccountInfo{accountId=0.0.1, contractAccountId=, deleted=true, proxyAccountId=0.0.8, proxyReceived=2 tℏ, key=302a300506032b6570032100e0c8ec2758a5879ffac226a13c0c516b799e72e35141a0dd828f94d37988a4b7, balance=3 tℏ, sendRecordThreshold=4 tℏ, receiveRecordThreshold=5 tℏ, receiverSignatureRequired=true, expirationTime=1970-01-01T00:00:00.006Z, autoRenewPeriod=PT168H, liveHashes=[LiveHash{accountId=0.0.10, hash=[0, 1, 2], keys=KeyList{threshold=null, keys=[302a300506032b6570032100e0c8ec2758a5879ffac226a13c0c516b799e72e35141a0dd828f94d37988a4b7]}, duration=PT264H}], tokenRelationships={}, accountMemo=, ownedNfts=0, maxAutomaticTokenAssociations=0, aliasKey=null, ledgerId=previewnet, ethereumNonce=1001, stakingInfo=null}" + "AccountInfo{accountId=0.0.1, contractAccountId=, deleted=true, proxyAccountId=0.0.8, proxyReceived=2 tℏ, key=302a300506032b6570032100e0c8ec2758a5879ffac226a13c0c516b799e72e35141a0dd828f94d37988a4b7, balance=3 tℏ, sendRecordThreshold=4 tℏ, receiveRecordThreshold=5 tℏ, receiverSignatureRequired=true, expirationTime=1970-01-01T00:00:00.006Z, autoRenewPeriod=PT168H, liveHashes=[LiveHash{accountId=0.0.10, hash=[0, 1, 2], keys=KeyList{threshold=null, keys=[302a300506032b6570032100e0c8ec2758a5879ffac226a13c0c516b799e72e35141a0dd828f94d37988a4b7]}, duration=PT264H}], tokenRelationships={}, accountMemo=, ownedNfts=0, maxAutomaticTokenAssociations=0, aliasKey=null, ledgerId=previewnet, ethereumNonce=1001, stakingInfo=null, virtualAddresses=[]}" ] @@ -15,4 +15,4 @@ com.hedera.hashgraph.sdk.AccountInfoTest.toBytes=[ com.hedera.hashgraph.sdk.AccountInfoTest.toProtobuf=[ "# com.hedera.hashgraph.sdk.proto.CryptoGetInfoResponse$AccountInfo@21386b1e\naccount_i_d {\n account_num: 1\n realm_num: 0\n shard_num: 0\n}\nauto_renew_period {\n seconds: 604800\n}\nbalance: 3\ndeleted: true\nethereum_nonce: 1001\nexpiration_time {\n nanos: 6000000\n seconds: 0\n}\ngenerate_receive_record_threshold: 5\ngenerate_send_record_threshold: 4\nkey {\n ed25519: \"\\340\\310\\354\\'X\\245\\207\\237\\372\\302&\\241<\\fQky\\236r\\343QA\\240\\335\\202\\217\\224\\323y\\210\\244\\267\"\n}\nledger_id: \"\\002\"\nlive_hashes {\n account_id {\n account_num: 10\n realm_num: 0\n shard_num: 0\n }\n duration {\n seconds: 950400\n }\n hash: \"\\000\\001\\002\"\n keys {\n keys {\n ed25519: \"\\340\\310\\354\\'X\\245\\207\\237\\372\\302&\\241<\\fQky\\236r\\343QA\\240\\335\\202\\217\\224\\323y\\210\\244\\267\"\n }\n }\n}\nowned_nfts: 0\nproxy_account_i_d {\n account_num: 8\n realm_num: 0\n shard_num: 0\n}\nproxy_received: 2\nreceiver_sig_required: true" -] +] \ No newline at end of file diff --git a/sdk/src/test/java/com/hedera/hashgraph/sdk/ECDSAPublicKeyTest.java b/sdk/src/test/java/com/hedera/hashgraph/sdk/ECDSAPublicKeyTest.java index 65b8dbd06f..eb63951046 100644 --- a/sdk/src/test/java/com/hedera/hashgraph/sdk/ECDSAPublicKeyTest.java +++ b/sdk/src/test/java/com/hedera/hashgraph/sdk/ECDSAPublicKeyTest.java @@ -140,4 +140,17 @@ void keyIsNotEd25519() { assertThat(key.isED25519()).isFalse(); } + + @Test + @DisplayName("to EVM address") + void toEvmAddress() { + // Generated by https://www.rfctools.com/ethereum-address-test-tool/ + String privateKeyString = "DEBAE3CA62AB3157110DBA79C8DE26540DC320EE9BE73A77D70BA175643A3500"; + String expectedEvmAddress = "d8eb8db03c699faa3f47adcdcd2ae91773b10f8b"; + + PrivateKey privateKey = PrivateKey.fromStringECDSA(privateKeyString); + PublicKey key = privateKey.getPublicKey(); + + assertThat(key.toEvmAddress()).hasToString(expectedEvmAddress); + } } diff --git a/sdk/src/test/java/com/hedera/hashgraph/sdk/TransactionRecordTest.java b/sdk/src/test/java/com/hedera/hashgraph/sdk/TransactionRecordTest.java index 7894cf4273..7afa6c99ed 100644 --- a/sdk/src/test/java/com/hedera/hashgraph/sdk/TransactionRecordTest.java +++ b/sdk/src/test/java/com/hedera/hashgraph/sdk/TransactionRecordTest.java @@ -81,7 +81,8 @@ private static TransactionRecord spawnRecordExample(@Nullable ByteString prngByt ByteString.copyFrom("Some hash", StandardCharsets.UTF_8), List.of(new Transfer(AccountId.fromString("1.2.3"), Hbar.from(8))), prngBytes, - prngNumber + prngNumber, + ByteString.copyFrom("0x00", StandardCharsets.UTF_8) ); } diff --git a/sdk/src/test/java/com/hedera/hashgraph/sdk/TransactionRecordTest.snap b/sdk/src/test/java/com/hedera/hashgraph/sdk/TransactionRecordTest.snap index 3adcdbc1ba..914227851a 100644 --- a/sdk/src/test/java/com/hedera/hashgraph/sdk/TransactionRecordTest.snap +++ b/sdk/src/test/java/com/hedera/hashgraph/sdk/TransactionRecordTest.snap @@ -1,8 +1,8 @@ com.hedera.hashgraph.sdk.TransactionRecordTest.shouldSerialize2=[ - "TransactionRecord{receipt=TransactionReceipt{transactionId=null, status=SCHEDULE_ALREADY_DELETED, exchangeRate=ExchangeRate{hbars=3, cents=4, expirationTime=2019-04-01T22:42:22Z, exchangeRateInCents=1.3333333333333333}, accountId=1.2.3, fileId=4.5.6, contractId=3.2.1, topicId=9.8.7, tokenId=6.5.4, topicSequenceNumber=3, topicRunningHash=[54, 56, 54, 102, 55, 55, 50, 48, 54, 101, 54, 102, 55, 55, 50, 48, 54, 50, 55, 50, 54, 102, 55, 55, 54, 101, 50, 48, 54, 51, 54, 102, 55, 55], totalSupply=30, scheduleId=1.1.1, scheduledTransactionId=3.3.3@1554158542.0, serials=[1, 2, 3], duplicates=[], children=[]}, transactionHash=68656c6c6f, consensusTimestamp=2019-04-01T22:42:22Z, transactionId=3.3.3@1554158542.0, transactionMemo=memo, transactionFee=3000 tℏ, contractFunctionResult=ContractFunctionResult{contractId=1.2.3, evmAddress=1.2.98329e006610472e6b372c080833f6d79ed833cf, errorMessage=null, bloom=, gasUsed=0, logs=[], createdContractIds=[], stateChanges=[], gas=0, hbarAmount=0 tℏ, contractFunctionparametersBytes=, rawResult=00000000000000000000000000000000000000000000000000000000ffffffff7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00000000000000000000000011223344556677889900aabbccddeeff00112233ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000d48656c6c6f2c20776f726c642100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001448656c6c6f2c20776f726c642c20616761696e21000000000000000000000000, senderAccountId=1.2.3}, transfers=[Transfer{accountId=4.4.4, amount=5 ℏ}], tokenTransfers={6.6.6={1.1.1=4}}, tokenNftTransfers={4.4.4=[TokenNftTransfer{tokenId=4.4.4, sender=1.2.3, receiver=3.2.1, serial=4, isApproved=true}]}, scheduleRef=3.3.3, assessedCustomFees=[AssessedCustomFee{amount=4, tokenId=4.5.6, feeCollectorAccountId=8.6.5, payerAccountIdList=[3.3.3]}], automaticTokenAssociations=[TokenAssociation{tokenId=5.4.3, accountId=8.7.6}], aliasKey=302d300706052b8104000a03220002703a9370b0443be6ae7c507b0aec81a55e94e4a863b9655360bd65358caa6588, children=[], duplicates=[], parentConsensusTimestamp=2019-04-01T22:42:22Z, ethereumHash=536f6d652068617368, paidStakingRewards=[Transfer{accountId=1.2.3, amount=8 ℏ}], prngBytes=null, prngNumber=4}" + "TransactionRecord{receipt=TransactionReceipt{transactionId=null, status=SCHEDULE_ALREADY_DELETED, exchangeRate=ExchangeRate{hbars=3, cents=4, expirationTime=2019-04-01T22:42:22Z, exchangeRateInCents=1.3333333333333333}, accountId=1.2.3, fileId=4.5.6, contractId=3.2.1, topicId=9.8.7, tokenId=6.5.4, topicSequenceNumber=3, topicRunningHash=[54, 56, 54, 102, 55, 55, 50, 48, 54, 101, 54, 102, 55, 55, 50, 48, 54, 50, 55, 50, 54, 102, 55, 55, 54, 101, 50, 48, 54, 51, 54, 102, 55, 55], totalSupply=30, scheduleId=1.1.1, scheduledTransactionId=3.3.3@1554158542.0, serials=[1, 2, 3], duplicates=[], children=[]}, transactionHash=68656c6c6f, consensusTimestamp=2019-04-01T22:42:22Z, transactionId=3.3.3@1554158542.0, transactionMemo=memo, transactionFee=3000 tℏ, contractFunctionResult=ContractFunctionResult{contractId=1.2.3, evmAddress=1.2.98329e006610472e6b372c080833f6d79ed833cf, errorMessage=null, bloom=, gasUsed=0, logs=[], createdContractIds=[], stateChanges=[], gas=0, hbarAmount=0 tℏ, contractFunctionparametersBytes=, rawResult=00000000000000000000000000000000000000000000000000000000ffffffff7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00000000000000000000000011223344556677889900aabbccddeeff00112233ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000d48656c6c6f2c20776f726c642100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001448656c6c6f2c20776f726c642c20616761696e21000000000000000000000000, senderAccountId=1.2.3, contractNonces=[]}, transfers=[Transfer{accountId=4.4.4, amount=5 ℏ}], tokenTransfers={6.6.6={1.1.1=4}}, tokenNftTransfers={4.4.4=[TokenNftTransfer{tokenId=4.4.4, sender=1.2.3, receiver=3.2.1, serial=4, isApproved=true}]}, scheduleRef=3.3.3, assessedCustomFees=[AssessedCustomFee{amount=4, tokenId=4.5.6, feeCollectorAccountId=8.6.5, payerAccountIdList=[3.3.3]}], automaticTokenAssociations=[TokenAssociation{tokenId=5.4.3, accountId=8.7.6}], aliasKey=302d300706052b8104000a03220002703a9370b0443be6ae7c507b0aec81a55e94e4a863b9655360bd65358caa6588, children=[], duplicates=[], parentConsensusTimestamp=2019-04-01T22:42:22Z, ethereumHash=536f6d652068617368, paidStakingRewards=[Transfer{accountId=1.2.3, amount=8 ℏ}], prngBytes=null, prngNumber=4, evmAddress=30783030}" ] com.hedera.hashgraph.sdk.TransactionRecordTest.shouldSerialize=[ - "TransactionRecord{receipt=TransactionReceipt{transactionId=null, status=SCHEDULE_ALREADY_DELETED, exchangeRate=ExchangeRate{hbars=3, cents=4, expirationTime=2019-04-01T22:42:22Z, exchangeRateInCents=1.3333333333333333}, accountId=1.2.3, fileId=4.5.6, contractId=3.2.1, topicId=9.8.7, tokenId=6.5.4, topicSequenceNumber=3, topicRunningHash=[54, 56, 54, 102, 55, 55, 50, 48, 54, 101, 54, 102, 55, 55, 50, 48, 54, 50, 55, 50, 54, 102, 55, 55, 54, 101, 50, 48, 54, 51, 54, 102, 55, 55], totalSupply=30, scheduleId=1.1.1, scheduledTransactionId=3.3.3@1554158542.0, serials=[1, 2, 3], duplicates=[], children=[]}, transactionHash=68656c6c6f, consensusTimestamp=2019-04-01T22:42:22Z, transactionId=3.3.3@1554158542.0, transactionMemo=memo, transactionFee=3000 tℏ, contractFunctionResult=ContractFunctionResult{contractId=1.2.3, evmAddress=1.2.98329e006610472e6b372c080833f6d79ed833cf, errorMessage=null, bloom=, gasUsed=0, logs=[], createdContractIds=[], stateChanges=[], gas=0, hbarAmount=0 tℏ, contractFunctionparametersBytes=, rawResult=00000000000000000000000000000000000000000000000000000000ffffffff7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00000000000000000000000011223344556677889900aabbccddeeff00112233ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000d48656c6c6f2c20776f726c642100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001448656c6c6f2c20776f726c642c20616761696e21000000000000000000000000, senderAccountId=1.2.3}, transfers=[Transfer{accountId=4.4.4, amount=5 ℏ}], tokenTransfers={6.6.6={1.1.1=4}}, tokenNftTransfers={4.4.4=[TokenNftTransfer{tokenId=4.4.4, sender=1.2.3, receiver=3.2.1, serial=4, isApproved=true}]}, scheduleRef=3.3.3, assessedCustomFees=[AssessedCustomFee{amount=4, tokenId=4.5.6, feeCollectorAccountId=8.6.5, payerAccountIdList=[3.3.3]}], automaticTokenAssociations=[TokenAssociation{tokenId=5.4.3, accountId=8.7.6}], aliasKey=302d300706052b8104000a03220002703a9370b0443be6ae7c507b0aec81a55e94e4a863b9655360bd65358caa6588, children=[], duplicates=[], parentConsensusTimestamp=2019-04-01T22:42:22Z, ethereumHash=536f6d652068617368, paidStakingRewards=[Transfer{accountId=1.2.3, amount=8 ℏ}], prngBytes=766572792072616e646f6d206279746573, prngNumber=null}" + "TransactionRecord{receipt=TransactionReceipt{transactionId=null, status=SCHEDULE_ALREADY_DELETED, exchangeRate=ExchangeRate{hbars=3, cents=4, expirationTime=2019-04-01T22:42:22Z, exchangeRateInCents=1.3333333333333333}, accountId=1.2.3, fileId=4.5.6, contractId=3.2.1, topicId=9.8.7, tokenId=6.5.4, topicSequenceNumber=3, topicRunningHash=[54, 56, 54, 102, 55, 55, 50, 48, 54, 101, 54, 102, 55, 55, 50, 48, 54, 50, 55, 50, 54, 102, 55, 55, 54, 101, 50, 48, 54, 51, 54, 102, 55, 55], totalSupply=30, scheduleId=1.1.1, scheduledTransactionId=3.3.3@1554158542.0, serials=[1, 2, 3], duplicates=[], children=[]}, transactionHash=68656c6c6f, consensusTimestamp=2019-04-01T22:42:22Z, transactionId=3.3.3@1554158542.0, transactionMemo=memo, transactionFee=3000 tℏ, contractFunctionResult=ContractFunctionResult{contractId=1.2.3, evmAddress=1.2.98329e006610472e6b372c080833f6d79ed833cf, errorMessage=null, bloom=, gasUsed=0, logs=[], createdContractIds=[], stateChanges=[], gas=0, hbarAmount=0 tℏ, contractFunctionparametersBytes=, rawResult=00000000000000000000000000000000000000000000000000000000ffffffff7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00000000000000000000000011223344556677889900aabbccddeeff00112233ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000d48656c6c6f2c20776f726c642100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001448656c6c6f2c20776f726c642c20616761696e21000000000000000000000000, senderAccountId=1.2.3, contractNonces=[]}, transfers=[Transfer{accountId=4.4.4, amount=5 ℏ}], tokenTransfers={6.6.6={1.1.1=4}}, tokenNftTransfers={4.4.4=[TokenNftTransfer{tokenId=4.4.4, sender=1.2.3, receiver=3.2.1, serial=4, isApproved=true}]}, scheduleRef=3.3.3, assessedCustomFees=[AssessedCustomFee{amount=4, tokenId=4.5.6, feeCollectorAccountId=8.6.5, payerAccountIdList=[3.3.3]}], automaticTokenAssociations=[TokenAssociation{tokenId=5.4.3, accountId=8.7.6}], aliasKey=302d300706052b8104000a03220002703a9370b0443be6ae7c507b0aec81a55e94e4a863b9655360bd65358caa6588, children=[], duplicates=[], parentConsensusTimestamp=2019-04-01T22:42:22Z, ethereumHash=536f6d652068617368, paidStakingRewards=[Transfer{accountId=1.2.3, amount=8 ℏ}], prngBytes=766572792072616e646f6d206279746573, prngNumber=null, evmAddress=30783030}" ] \ No newline at end of file