diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/validation/AttributeValidatorImpl.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/validation/AttributeValidatorImpl.java index fe790e8dec36..64c272c578e3 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/validation/AttributeValidatorImpl.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/validation/AttributeValidatorImpl.java @@ -23,6 +23,7 @@ import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_ZERO_BYTE_IN_STRING; import static com.hedera.hapi.node.base.ResponseCodeEnum.MEMO_TOO_LONG; import static com.hedera.node.app.spi.key.KeyUtils.isValid; +import static com.hedera.node.app.spi.validation.ExpiryMeta.NA; import static com.hedera.node.app.spi.workflows.HandleException.validateTrue; import static java.util.Objects.requireNonNull; @@ -91,7 +92,8 @@ public void validateExpiry(long expiry) { context.configuration().getConfigData(EntitiesConfig.class).maxLifetime(); final var now = context.consensusNow().getEpochSecond(); final var expiryGivenMaxLifetime = now + maxEntityLifetime; - validateTrue(expiry > now && expiry < expiryGivenMaxLifetime, INVALID_EXPIRATION_TIME); + final var impliedExpiry = expiry == NA ? expiryGivenMaxLifetime : expiry; + validateTrue(impliedExpiry > now && impliedExpiry <= expiryGivenMaxLifetime, INVALID_EXPIRATION_TIME); } /** diff --git a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/create/ClassicCreatesCall.java b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/create/ClassicCreatesCall.java index bf13838d7167..73e535fc8e5e 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/create/ClassicCreatesCall.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/create/ClassicCreatesCall.java @@ -18,7 +18,6 @@ import static com.hedera.hapi.node.base.ResponseCodeEnum.INSUFFICIENT_TX_FEE; import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_ACCOUNT_ID; -import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_EXPIRATION_TIME; import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_TRANSACTION_BODY; import static com.hedera.hapi.node.base.ResponseCodeEnum.MISSING_TOKEN_SYMBOL; import static com.hedera.hapi.node.base.ResponseCodeEnum.OK; @@ -180,9 +179,6 @@ private ResponseCodeEnum validityOfSynth(@NonNull final TokenCreateTransactionBo if (treasuryAccount == null) { return INVALID_ACCOUNT_ID; } - if (op.autoRenewAccount() == null) { - return INVALID_EXPIRATION_TIME; - } return OK; } diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/CreatePrecompileSuite.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/CreatePrecompileSuite.java index 12789b7f89b6..95c951b909eb 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/CreatePrecompileSuite.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/contract/precompile/CreatePrecompileSuite.java @@ -17,44 +17,68 @@ package com.hedera.services.bdd.suites.contract.precompile; import static com.hedera.services.bdd.junit.TestTags.SMART_CONTRACT; +import static com.hedera.services.bdd.spec.HapiPropertySource.asTokenString; import static com.hedera.services.bdd.spec.HapiSpec.defaultHapiSpec; import static com.hedera.services.bdd.spec.assertions.AccountInfoAsserts.changeFromSnapshot; +import static com.hedera.services.bdd.spec.keys.KeyShape.CONTRACT; +import static com.hedera.services.bdd.spec.keys.KeyShape.DELEGATE_CONTRACT; import static com.hedera.services.bdd.spec.keys.KeyShape.ED25519; import static com.hedera.services.bdd.spec.keys.KeyShape.SECP256K1; +import static com.hedera.services.bdd.spec.keys.KeyShape.sigs; +import static com.hedera.services.bdd.spec.keys.SigControl.ED25519_ON; +import static com.hedera.services.bdd.spec.keys.SigControl.ON; import static com.hedera.services.bdd.spec.queries.QueryVerbs.getAccountBalance; import static com.hedera.services.bdd.spec.queries.QueryVerbs.getAccountInfo; import static com.hedera.services.bdd.spec.queries.QueryVerbs.getContractInfo; +import static com.hedera.services.bdd.spec.queries.QueryVerbs.getTokenInfo; import static com.hedera.services.bdd.spec.queries.QueryVerbs.getTxnRecord; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.contractCall; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.contractCreate; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoCreate; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoDelete; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoTransfer; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoUpdate; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.uploadInitCode; import static com.hedera.services.bdd.spec.utilops.CustomSpecAssert.allRunFor; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.balanceSnapshot; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.childRecordsCheck; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.emptyChildRecordsCheck; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.newKeyNamed; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.sourcing; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.withOpContext; import static com.hedera.services.bdd.spec.utilops.records.SnapshotMatchMode.NONDETERMINISTIC_FUNCTION_PARAMETERS; import static com.hedera.services.bdd.spec.utilops.records.SnapshotMatchMode.NONDETERMINISTIC_TRANSACTION_FEES; import static com.hedera.services.bdd.suites.contract.Utils.asAddress; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.ACCOUNT_IS_TREASURY; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.CONTRACT_REVERT_EXECUTED; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INSUFFICIENT_TX_FEE; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_ACCOUNT_ID; -import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_EXPIRATION_TIME; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_RENEWAL_PERIOD; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.MISSING_TOKEN_SYMBOL; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.SUCCESS; +import com.esaulpaugh.headlong.abi.Address; import com.hedera.services.bdd.junit.HapiTest; import com.hedera.services.bdd.junit.HapiTestSuite; import com.hedera.services.bdd.spec.HapiSpec; import com.hedera.services.bdd.spec.assertions.ContractFnResultAsserts; +import com.hedera.services.bdd.spec.assertions.ContractInfoAsserts; import com.hedera.services.bdd.spec.assertions.TransactionRecordAsserts; +import com.hedera.services.bdd.spec.keys.KeyShape; import com.hedera.services.bdd.spec.transactions.contract.HapiEthereumCall; import com.hedera.services.bdd.spec.transactions.contract.HapiParserUtil; +import com.hedera.services.bdd.spec.transactions.token.TokenMovement; import com.hedera.services.bdd.suites.HapiSuite; import com.hedera.services.bdd.suites.contract.Utils; import com.hederahashgraph.api.proto.java.ResponseCodeEnum; +import com.hederahashgraph.api.proto.java.TokenFreezeStatus; +import com.hederahashgraph.api.proto.java.TokenID; +import com.hederahashgraph.api.proto.java.TokenPauseStatus; +import com.hederahashgraph.api.proto.java.TokenSupplyType; +import com.hederahashgraph.api.proto.java.TokenType; import java.util.List; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; import org.apache.commons.lang3.ArrayUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -76,6 +100,7 @@ public class CreatePrecompileSuite extends HapiSuite { public static final String CREATE_TOKEN_WITH_ALL_CUSTOM_FEES_AVAILABLE = "createTokenWithAllCustomFeesAvailable"; private static final Logger log = LogManager.getLogger(CreatePrecompileSuite.class); private static final long GAS_TO_OFFER = 1_000_000L; + private static final long GAS_TO_OFFER_2 = 4_000_000L; public static final long AUTO_RENEW_PERIOD = 8_000_000L; public static final String TOKEN_SYMBOL = "tokenSymbol"; public static final String TOKEN_NAME = "tokenName"; @@ -91,6 +116,11 @@ public class CreatePrecompileSuite extends HapiSuite { public static final String EXISTING_TOKEN = "EXISTING_TOKEN"; public static final String EXPLICIT_CREATE_RESULT = "Explicit create result is {}"; private static final String CREATE_NFT_WITH_KEYS_AND_EXPIRY_FUNCTION = "createNFTTokenWithKeysAndExpiry"; + private static final KeyShape THRESHOLD_KEY_SHAPE = KeyShape.threshOf(1, ED25519, CONTRACT); + private static final String THRESHOLD_KEY = "ThreshKey"; + private static final String ADMIN_KEY = "adminKey"; + private static final String TOKEN_MISC_OPERATIONS_CONTRACT = "TokenMiscOperations"; + private static final String CREATE_FUNGIBLE_TOKEN_WITH_KEYS_AND_EXPIRY_FUNCTION = "createTokenWithKeysAndExpiry"; public static void main(String... args) { new CreatePrecompileSuite().runSuiteAsync(); @@ -115,6 +145,13 @@ List positiveSpecs() { List negativeSpecs() { return List.of( + fungibleTokenCreateHappyPath(), + nonFungibleTokenCreateHappyPath(), + fungibleTokenCreateThenQueryAndTransfer(), + nonFungibleTokenCreateThenQuery(), + inheritsSenderAutoRenewAccountIfAnyForNftCreate(), + inheritsSenderAutoRenewAccountForTokenCreate(), + createTokenWithDefaultExpiryAndEmptyKeys(), tokenCreateWithEmptyKeysReverts(), tokenCreateWithKeyWithMultipleKeyValuesReverts(), tokenCreateWithFixedFeeWithMultiplePaymentsReverts(), @@ -122,9 +159,578 @@ List negativeSpecs() { createTokenWithInvalidExpiry(), createTokenWithInvalidTreasury(), createTokenWithInsufficientValueSent(), + createTokenWithFixedFeeThenTransferAndAssessFee(), delegateCallTokenCreateFails()); } + // TEST-001 + @HapiTest + final HapiSpec fungibleTokenCreateHappyPath() { + final var tokenCreateContractAsKeyDelegate = "tokenCreateContractAsKeyDelegate"; + final var createTokenNum = new AtomicLong(); + final AtomicReference ed2551Key = new AtomicReference<>(); + return defaultHapiSpec("fungibleTokenCreateHappyPath") + .given( + newKeyNamed(ECDSA_KEY).shape(SECP256K1), + newKeyNamed(CONTRACT_ADMIN_KEY), + cryptoCreate(ACCOUNT_TO_ASSOCIATE), + uploadInitCode(TOKEN_CREATE_CONTRACT), + cryptoCreate(ACCOUNT).balance(ONE_MILLION_HBARS), + contractCreate(TOKEN_CREATE_CONTRACT) + .autoRenewAccountId(ACCOUNT) + .adminKey(CONTRACT_ADMIN_KEY) + .gas(GAS_TO_OFFER), + newKeyNamed(THRESHOLD_KEY) + .shape(THRESHOLD_KEY_SHAPE.signedWith(sigs(ED25519_ON, TOKEN_CREATE_CONTRACT))) + .exposingKeyTo(k -> ed2551Key.set(k.getThresholdKey() + .getKeys() + .getKeys(0) + .getEd25519() + .toByteArray())), + cryptoUpdate(ACCOUNT).key(THRESHOLD_KEY), + cryptoUpdate(ACCOUNT_TO_ASSOCIATE).key(THRESHOLD_KEY)) + .when(withOpContext((spec, opLog) -> { + spec.registry() + .saveKey( + ED25519KEY, + spec.registry() + .getKey(THRESHOLD_KEY) + .getThresholdKey() + .getKeys() + .getKeys(0)); + allRunFor( + spec, + contractCall( + TOKEN_CREATE_CONTRACT, + CREATE_FUNGIBLE_TOKEN_WITH_KEYS_AND_EXPIRY_FUNCTION, + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getAccountID(ACCOUNT))), + ed2551Key.get(), + spec.registry() + .getKey(ECDSA_KEY) + .getECDSASecp256K1() + .toByteArray(), + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getContractId(TOKEN_CREATE_CONTRACT))), + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getContractId(TOKEN_CREATE_CONTRACT))), + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getAccountID(ACCOUNT))), + AUTO_RENEW_PERIOD, + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getAccountID(ACCOUNT_TO_ASSOCIATE)))) + .via(FIRST_CREATE_TXN) + .gas(GAS_TO_OFFER) + .sending(DEFAULT_AMOUNT_TO_SEND) + .payingWith(ACCOUNT) + .signedBy(THRESHOLD_KEY) + .refusingEthConversion() + .exposingResultTo(result -> { + log.info(EXPLICIT_CREATE_RESULT, result[0]); + final var res = (Address) result[0]; + createTokenNum.set(res.value().longValueExact()); + }) + .hasKnownStatus(SUCCESS), + newKeyNamed(TOKEN_CREATE_CONTRACT_AS_KEY).shape(CONTRACT.signedWith(TOKEN_CREATE_CONTRACT)), + newKeyNamed(tokenCreateContractAsKeyDelegate) + .shape(DELEGATE_CONTRACT.signedWith(TOKEN_CREATE_CONTRACT))); + })) + .then(withOpContext((spec, opLog) -> allRunFor( + spec, + getContractInfo(TOKEN_CREATE_CONTRACT) + .has(ContractInfoAsserts.contractWith().autoRenewAccountId(ACCOUNT)) + .logged(), + getTxnRecord(FIRST_CREATE_TXN).andAllChildRecords().logged(), + getAccountBalance(ACCOUNT).logged(), + getAccountBalance(TOKEN_CREATE_CONTRACT).logged(), + getContractInfo(TOKEN_CREATE_CONTRACT).logged(), + childRecordsCheck( + FIRST_CREATE_TXN, + ResponseCodeEnum.SUCCESS, + TransactionRecordAsserts.recordWith().status(ResponseCodeEnum.SUCCESS), + TransactionRecordAsserts.recordWith().status(ResponseCodeEnum.SUCCESS), + TransactionRecordAsserts.recordWith().status(ResponseCodeEnum.SUCCESS)), + sourcing(() -> + getAccountInfo(ACCOUNT_TO_ASSOCIATE).logged().hasTokenRelationShipCount(1)), + sourcing(() -> getTokenInfo(asTokenString(TokenID.newBuilder() + .setTokenNum(createTokenNum.get()) + .build())) + .logged() + .hasTokenType(TokenType.FUNGIBLE_COMMON) + .hasSymbol(TOKEN_SYMBOL) + .hasName(TOKEN_NAME) + .hasDecimals(8) + .hasTotalSupply(100) + .hasEntityMemo(MEMO) + .hasTreasury(ACCOUNT) + // Token doesn't inherit contract's auto-renew + // account if set in tokenCreate + .hasAutoRenewAccount(ACCOUNT) + .hasAutoRenewPeriod(AUTO_RENEW_PERIOD) + .hasSupplyType(TokenSupplyType.INFINITE) + .searchKeysGlobally() + .hasAdminKey(ED25519KEY) + .hasKycKey(ED25519KEY) + .hasFreezeKey(ECDSA_KEY) + .hasWipeKey(ECDSA_KEY) + .hasSupplyKey(TOKEN_CREATE_CONTRACT_AS_KEY) + .hasFeeScheduleKey(tokenCreateContractAsKeyDelegate) + .hasPauseKey(CONTRACT_ADMIN_KEY) + .hasPauseStatus(TokenPauseStatus.Unpaused)), + cryptoDelete(ACCOUNT).hasKnownStatus(ACCOUNT_IS_TREASURY)))); + } + + // TEST-002 + @HapiTest + final HapiSpec inheritsSenderAutoRenewAccountIfAnyForNftCreate() { + final var createdNftTokenNum = new AtomicLong(); + final AtomicReference ed2551Key = new AtomicReference<>(); + return defaultHapiSpec("inheritsSenderAutoRenewAccountIfAnyForNftCreate") + .given( + newKeyNamed(ED25519KEY).shape(ED25519), + cryptoCreate(ACCOUNT).balance(ONE_MILLION_HBARS), + uploadInitCode(TOKEN_CREATE_CONTRACT), + contractCreate(TOKEN_CREATE_CONTRACT) + .autoRenewAccountId(ACCOUNT) + .gas(GAS_TO_OFFER), + newKeyNamed(THRESHOLD_KEY) + .shape(THRESHOLD_KEY_SHAPE.signedWith(sigs(ED25519_ON, TOKEN_CREATE_CONTRACT))) + .exposingKeyTo(k -> ed2551Key.set(k.getThresholdKey() + .getKeys() + .getKeys(0) + .getEd25519() + .toByteArray())), + cryptoUpdate(ACCOUNT).key(THRESHOLD_KEY)) + .when(withOpContext((spec, opLog) -> allRunFor( + spec, + getContractInfo(TOKEN_CREATE_CONTRACT) + .has(ContractInfoAsserts.contractWith().autoRenewAccountId(ACCOUNT)) + .logged()))) + .then(withOpContext((spec, ignore) -> { + final var subop1 = balanceSnapshot(ACCOUNT_BALANCE, ACCOUNT); + final var subop2 = contractCall( + TOKEN_CREATE_CONTRACT, + CREATE_NFT_WITH_KEYS_AND_EXPIRY_FUNCTION, + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getAccountID(ACCOUNT))), + ed2551Key.get(), + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getAccountID(ACCOUNT))), + AUTO_RENEW_PERIOD) + .via(FIRST_CREATE_TXN) + .gas(GAS_TO_OFFER) + .payingWith(ACCOUNT) + .sending(DEFAULT_AMOUNT_TO_SEND) + .refusingEthConversion() + .exposingResultTo(result -> { + log.info("Explicit create result is" + " {}", result[0]); + final var res = (Address) result[0]; + createdNftTokenNum.set(res.value().longValueExact()); + }) + .hasKnownStatus(SUCCESS); + + allRunFor( + spec, + subop1, + subop2, + childRecordsCheck( + FIRST_CREATE_TXN, + SUCCESS, + TransactionRecordAsserts.recordWith().status(SUCCESS))); + + final var nftInfo = getTokenInfo(asTokenString(TokenID.newBuilder() + .setTokenNum(createdNftTokenNum.get()) + .build())) + .hasAutoRenewAccount(ACCOUNT) + .logged(); + + allRunFor(spec, nftInfo); + })); + } + + // TEST-001 + @HapiTest + final HapiSpec inheritsSenderAutoRenewAccountForTokenCreate() { + final var createTokenNum = new AtomicLong(); + final AtomicReference ed2551Key = new AtomicReference<>(); + return defaultHapiSpec("inheritsSenderAutoRenewAccountForTokenCreate") + .given( + newKeyNamed(ECDSA_KEY).shape(SECP256K1), + newKeyNamed(ACCOUNT_TO_ASSOCIATE_KEY), + newKeyNamed(CONTRACT_ADMIN_KEY), + cryptoCreate(ACCOUNT).balance(ONE_MILLION_HBARS), + cryptoCreate(ACCOUNT_TO_ASSOCIATE).key(ACCOUNT_TO_ASSOCIATE_KEY), + uploadInitCode(TOKEN_CREATE_CONTRACT), + contractCreate(TOKEN_CREATE_CONTRACT) + .gas(GAS_TO_OFFER) + .adminKey(CONTRACT_ADMIN_KEY) + .autoRenewAccountId(ACCOUNT), + newKeyNamed(THRESHOLD_KEY) + .shape(THRESHOLD_KEY_SHAPE.signedWith(sigs(ED25519_ON, TOKEN_CREATE_CONTRACT))) + .exposingKeyTo(k -> ed2551Key.set(k.getThresholdKey() + .getKeys() + .getKeys(0) + .getEd25519() + .toByteArray())), + cryptoUpdate(ACCOUNT).key(THRESHOLD_KEY), + cryptoUpdate(ACCOUNT_TO_ASSOCIATE).key(THRESHOLD_KEY), + getContractInfo(TOKEN_CREATE_CONTRACT) + .has(ContractInfoAsserts.contractWith().autoRenewAccountId(ACCOUNT)) + .logged()) + .when(withOpContext((spec, opLog) -> allRunFor( + spec, + contractCall( + TOKEN_CREATE_CONTRACT, + CREATE_FUNGIBLE_TOKEN_WITH_KEYS_AND_EXPIRY_FUNCTION, + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getAccountID(ACCOUNT))), + ed2551Key.get(), + spec.registry() + .getKey(ECDSA_KEY) + .getECDSASecp256K1() + .toByteArray(), + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getContractId(TOKEN_CREATE_CONTRACT))), + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getContractId(TOKEN_CREATE_CONTRACT))), + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getAccountID(ACCOUNT))), + AUTO_RENEW_PERIOD, + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getAccountID(ACCOUNT_TO_ASSOCIATE)))) + .via(FIRST_CREATE_TXN) + .gas(GAS_TO_OFFER) + .sending(DEFAULT_AMOUNT_TO_SEND) + .payingWith(ACCOUNT) + .alsoSigningWithFullPrefix(THRESHOLD_KEY) + .refusingEthConversion() + .exposingResultTo(result -> { + log.info(EXPLICIT_CREATE_RESULT, result[0]); + final var res = (Address) result[0]; + createTokenNum.set(res.value().longValueExact()); + }) + .hasKnownStatus(SUCCESS)))) + .then(sourcing(() -> getTokenInfo(asTokenString(TokenID.newBuilder() + .setTokenNum(createTokenNum.get()) + .build())) + .logged() + .hasAutoRenewAccount(ACCOUNT) + .hasPauseStatus(TokenPauseStatus.Unpaused))); + } + + // TEST-003 + @HapiTest + final HapiSpec nonFungibleTokenCreateHappyPath() { + final var createdTokenNum = new AtomicLong(); + return defaultHapiSpec("nonFungibleTokenCreateHappyPath") + .given( + newKeyNamed(ED25519KEY).shape(ED25519), + cryptoCreate(ACCOUNT).balance(ONE_MILLION_HBARS).key(ED25519KEY), + uploadInitCode(TOKEN_CREATE_CONTRACT), + getAccountInfo(DEFAULT_CONTRACT_SENDER).savingSnapshot(DEFAULT_CONTRACT_SENDER)) + .when(withOpContext((spec, opLog) -> + allRunFor(spec, contractCreate(TOKEN_CREATE_CONTRACT).gas(GAS_TO_OFFER)))) + .then(withOpContext((spec, ignore) -> { + final var subop1 = balanceSnapshot(ACCOUNT_BALANCE, ACCOUNT); + final var subop2 = contractCall( + TOKEN_CREATE_CONTRACT, + CREATE_NFT_WITH_KEYS_AND_EXPIRY_FUNCTION, + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getAccountID(ACCOUNT))), + spec.registry() + .getKey(ED25519KEY) + .getEd25519() + .toByteArray(), + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getAccountID(ACCOUNT))), + AUTO_RENEW_PERIOD) + .via(FIRST_CREATE_TXN) + .gas(GAS_TO_OFFER) + .payingWith(ACCOUNT) + .sending(DEFAULT_AMOUNT_TO_SEND) + .exposingResultTo(result -> { + log.info("Explicit create result is" + " {}", result[0]); + final var res = (Address) result[0]; + createdTokenNum.set(res.value().longValueExact()); + }) + .refusingEthConversion() + .hasKnownStatus(SUCCESS); + final var subop3 = getTxnRecord(FIRST_CREATE_TXN); + allRunFor( + spec, + subop1, + subop2, + subop3, + childRecordsCheck( + FIRST_CREATE_TXN, + SUCCESS, + TransactionRecordAsserts.recordWith().status(SUCCESS))); + + final var delta = subop3.getResponseRecord().getTransactionFee(); + final var effectivePayer = ACCOUNT; + final var subop4 = getAccountBalance(effectivePayer) + .hasTinyBars(changeFromSnapshot(ACCOUNT_BALANCE, -(delta + DEFAULT_AMOUNT_TO_SEND))); + final var contractBalanceCheck = getContractInfo(TOKEN_CREATE_CONTRACT) + .has(ContractInfoAsserts.contractWith() + .balanceGreaterThan(0L) + .balanceLessThan(DEFAULT_AMOUNT_TO_SEND)); + final var getAccountTokenBalance = getAccountBalance(ACCOUNT) + .hasTokenBalance( + asTokenString(TokenID.newBuilder() + .setTokenNum(createdTokenNum.get()) + .build()), + 0); + final var tokenInfo = getTokenInfo(asTokenString(TokenID.newBuilder() + .setTokenNum(createdTokenNum.get()) + .build())) + .hasTokenType(TokenType.NON_FUNGIBLE_UNIQUE) + .hasSymbol(TOKEN_SYMBOL) + .hasName(TOKEN_NAME) + .hasDecimals(0) + .hasTotalSupply(0) + .hasEntityMemo(MEMO) + .hasTreasury(ACCOUNT) + .hasAutoRenewAccount(ACCOUNT) + .hasAutoRenewPeriod(AUTO_RENEW_PERIOD) + .hasSupplyType(TokenSupplyType.FINITE) + .hasFreezeDefault(TokenFreezeStatus.Frozen) + .hasMaxSupply(10) + .searchKeysGlobally() + .hasAdminKey(ED25519KEY) + .hasSupplyKey(ED25519KEY) + .hasPauseKey(ED25519KEY) + .hasFreezeKey(ED25519KEY) + .hasKycKey(ED25519KEY) + .hasFeeScheduleKey(ED25519KEY) + .hasWipeKey(ED25519KEY) + .hasPauseStatus(TokenPauseStatus.Unpaused) + .logged(); + allRunFor(spec, subop4, getAccountTokenBalance, tokenInfo, contractBalanceCheck); + })); + } + + // TEST-005 + @HapiTest + final HapiSpec fungibleTokenCreateThenQueryAndTransfer() { + final var createdTokenNum = new AtomicLong(); + final AtomicReference ed2551Key = new AtomicReference<>(); + return defaultHapiSpec("fungibleTokenCreateThenQueryAndTransfer") + .given( + uploadInitCode(TOKEN_CREATE_CONTRACT), + contractCreate(TOKEN_CREATE_CONTRACT).gas(GAS_TO_OFFER), + newKeyNamed(TOKEN_CREATE_CONTRACT_AS_KEY).shape(CONTRACT.signedWith(TOKEN_CREATE_CONTRACT)), + newKeyNamed(THRESHOLD_KEY) + .shape(THRESHOLD_KEY_SHAPE.signedWith(sigs(ED25519_ON, TOKEN_CREATE_CONTRACT))) + .exposingKeyTo(k -> ed2551Key.set(k.getThresholdKey() + .getKeys() + .getKeys(0) + .getEd25519() + .toByteArray())), + cryptoCreate(ACCOUNT) + .balance(ONE_MILLION_HBARS) + .key(THRESHOLD_KEY) + .maxAutomaticTokenAssociations(1)) + .when(withOpContext((spec, opLog) -> { + spec.registry() + .saveKey( + ADMIN_KEY, + spec.registry() + .getKey(THRESHOLD_KEY) + .getThresholdKey() + .getKeys() + .getKeys(0)); + allRunFor( + spec, + contractCall( + TOKEN_CREATE_CONTRACT, + "createTokenThenQueryAndTransfer", + ed2551Key.get(), + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getAccountID(ACCOUNT))), + AUTO_RENEW_PERIOD) + .via(FIRST_CREATE_TXN) + .gas(GAS_TO_OFFER) + .sending(DEFAULT_AMOUNT_TO_SEND) + .payingWith(ACCOUNT) + .signedBy(ACCOUNT) + .exposingResultTo(result -> { + log.info(EXPLICIT_CREATE_RESULT, result[0]); + final var res = (Address) result[0]; + createdTokenNum.set(res.value().longValueExact()); + }) + .hasKnownStatus(SUCCESS)); + })) + .then(withOpContext((spec, opLog) -> allRunFor( + spec, + getTxnRecord(FIRST_CREATE_TXN).andAllChildRecords().logged(), + getAccountBalance(ACCOUNT).logged(), + getAccountBalance(TOKEN_CREATE_CONTRACT).logged(), + getContractInfo(TOKEN_CREATE_CONTRACT).logged(), + childRecordsCheck( + FIRST_CREATE_TXN, + ResponseCodeEnum.SUCCESS, + TransactionRecordAsserts.recordWith().status(SUCCESS), + TransactionRecordAsserts.recordWith().status(SUCCESS), + TransactionRecordAsserts.recordWith().status(SUCCESS), + TransactionRecordAsserts.recordWith().status(SUCCESS)), + sourcing(() -> getAccountBalance(ACCOUNT) + .hasTokenBalance( + asTokenString(TokenID.newBuilder() + .setTokenNum(createdTokenNum.get()) + .build()), + 20)), + sourcing(() -> getAccountBalance(TOKEN_CREATE_CONTRACT) + .hasTokenBalance( + asTokenString(TokenID.newBuilder() + .setTokenNum(createdTokenNum.get()) + .build()), + 10)), + sourcing(() -> getTokenInfo(asTokenString(TokenID.newBuilder() + .setTokenNum(createdTokenNum.get()) + .build())) + .hasTokenType(TokenType.FUNGIBLE_COMMON) + .hasSymbol(TOKEN_SYMBOL) + .hasName(TOKEN_NAME) + .hasDecimals(8) + .hasTotalSupply(30) + .hasEntityMemo(MEMO) + .hasTreasury(TOKEN_CREATE_CONTRACT) + .hasAutoRenewAccount(ACCOUNT) + .hasAutoRenewPeriod(AUTO_RENEW_PERIOD) + .hasSupplyType(TokenSupplyType.INFINITE) + .searchKeysGlobally() + .hasAdminKey("adminKey") + .hasSupplyKey(TOKEN_CREATE_CONTRACT_AS_KEY) + .hasPauseKey(TOKEN_CREATE_CONTRACT_AS_KEY) + .hasPauseStatus(TokenPauseStatus.Unpaused) + .logged())))); + } + + // TEST-006 + @HapiTest + final HapiSpec nonFungibleTokenCreateThenQuery() { + final var createdTokenNum = new AtomicLong(); + return defaultHapiSpec("nonFungibleTokenCreateThenQuery") + .given( + cryptoCreate(ACCOUNT).balance(ONE_MILLION_HBARS), + uploadInitCode(TOKEN_CREATE_CONTRACT), + contractCreate(TOKEN_CREATE_CONTRACT) + .autoRenewAccountId(ACCOUNT) + .gas(GAS_TO_OFFER), + newKeyNamed(THRESHOLD_KEY) + .shape(THRESHOLD_KEY_SHAPE.signedWith(sigs(ON, TOKEN_CREATE_CONTRACT))), + cryptoUpdate(ACCOUNT).key(THRESHOLD_KEY)) + .when(withOpContext((spec, opLog) -> allRunFor( + spec, + contractCall( + TOKEN_CREATE_CONTRACT, + "createNonFungibleTokenThenQuery", + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getContractId(TOKEN_CREATE_CONTRACT))), + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getAccountID(ACCOUNT))), + AUTO_RENEW_PERIOD) + .via(FIRST_CREATE_TXN) + .gas(GAS_TO_OFFER) + .sending(DEFAULT_AMOUNT_TO_SEND) + .payingWith(ACCOUNT) + .signedByPayerAnd(THRESHOLD_KEY) + .refusingEthConversion() + .exposingResultTo(result -> { + log.info(EXPLICIT_CREATE_RESULT, result[0]); + final var res = (Address) result[0]; + createdTokenNum.set(res.value().longValueExact()); + }), + newKeyNamed(TOKEN_CREATE_CONTRACT_AS_KEY).shape(CONTRACT.signedWith(TOKEN_CREATE_CONTRACT))))) + .then( + getTxnRecord(FIRST_CREATE_TXN).andAllChildRecords().logged(), + getAccountBalance(ACCOUNT).logged(), + getAccountBalance(TOKEN_CREATE_CONTRACT).logged(), + getContractInfo(TOKEN_CREATE_CONTRACT).logged(), + childRecordsCheck( + FIRST_CREATE_TXN, + ResponseCodeEnum.SUCCESS, + TransactionRecordAsserts.recordWith().status(SUCCESS), + TransactionRecordAsserts.recordWith().status(SUCCESS), + TransactionRecordAsserts.recordWith().status(SUCCESS)), + sourcing(() -> getAccountBalance(TOKEN_CREATE_CONTRACT) + .hasTokenBalance( + asTokenString(TokenID.newBuilder() + .setTokenNum(createdTokenNum.get()) + .build()), + 0)), + sourcing(() -> getTokenInfo(asTokenString(TokenID.newBuilder() + .setTokenNum(createdTokenNum.get()) + .build())) + .hasTokenType(TokenType.NON_FUNGIBLE_UNIQUE) + .hasSymbol(TOKEN_SYMBOL) + .hasName(TOKEN_NAME) + .hasDecimals(0) + .hasTotalSupply(0) + .hasEntityMemo(MEMO) + .hasTreasury(TOKEN_CREATE_CONTRACT) + .hasAutoRenewAccount(ACCOUNT) + .hasAutoRenewPeriod(AUTO_RENEW_PERIOD) + .hasSupplyType(TokenSupplyType.INFINITE) + .searchKeysGlobally() + .hasAdminKey(TOKEN_CREATE_CONTRACT_AS_KEY) + .hasSupplyKey(TOKEN_CREATE_CONTRACT_AS_KEY) + .hasPauseStatus(TokenPauseStatus.PauseNotApplicable) + .logged())); + } + + @HapiTest + final HapiSpec createTokenWithDefaultExpiryAndEmptyKeys() { + final var tokenCreateContractAsKeyDelegate = "createTokenWithDefaultExpiryAndEmptyKeys"; + final var createdTokenNum = new AtomicLong(); + return defaultHapiSpec("createTokenWithDefaultExpiryAndEmptyKeys") + .given( + uploadInitCode(TOKEN_CREATE_CONTRACT), + contractCreate(TOKEN_CREATE_CONTRACT).gas(GAS_TO_OFFER), + newKeyNamed(THRESHOLD_KEY) + .shape(THRESHOLD_KEY_SHAPE.signedWith(sigs(ED25519_ON, TOKEN_CREATE_CONTRACT))), + cryptoCreate(ACCOUNT).balance(ONE_MILLION_HBARS).key(THRESHOLD_KEY)) + .when(withOpContext((spec, opLog) -> allRunFor( + spec, + contractCall(TOKEN_CREATE_CONTRACT, tokenCreateContractAsKeyDelegate) + .via(FIRST_CREATE_TXN) + .gas(GAS_TO_OFFER) + .sending(DEFAULT_AMOUNT_TO_SEND) + .payingWith(ACCOUNT) + .exposingResultTo(result -> { + log.info(EXPLICIT_CREATE_RESULT, result[0]); + final var res = (Address) result[0]; + createdTokenNum.set(res.value().longValueExact()); + }) + .hasKnownStatus(SUCCESS)))) + .then( + getTxnRecord(FIRST_CREATE_TXN).andAllChildRecords().logged(), + getAccountBalance(ACCOUNT).logged(), + getAccountBalance(TOKEN_CREATE_CONTRACT).logged(), + getContractInfo(TOKEN_CREATE_CONTRACT).logged(), + childRecordsCheck( + FIRST_CREATE_TXN, + ResponseCodeEnum.SUCCESS, + TransactionRecordAsserts.recordWith().status(SUCCESS)), + sourcing(() -> getAccountBalance(TOKEN_CREATE_CONTRACT) + .hasTokenBalance( + asTokenString(TokenID.newBuilder() + .setTokenNum(createdTokenNum.get()) + .build()), + 200)), + sourcing(() -> getTokenInfo(asTokenString(TokenID.newBuilder() + .setTokenNum(createdTokenNum.get()) + .build())) + .hasTokenType(TokenType.FUNGIBLE_COMMON) + .hasDecimals(8) + .hasTotalSupply(200) + .hasTreasury(TOKEN_CREATE_CONTRACT) + .hasAutoRenewPeriod(0L) + .searchKeysGlobally() + .hasPauseStatus(TokenPauseStatus.PauseNotApplicable) + .logged())); + } + // TEST-007 & TEST-016 // Should fail on insufficient value sent @HapiTest @@ -303,7 +909,6 @@ final HapiSpec createTokenWithInvalidExpiry() { NONDETERMINISTIC_TRANSACTION_FEES, NONDETERMINISTIC_FUNCTION_PARAMETERS) .given( - newKeyNamed(ECDSA_KEY).shape(SECP256K1), cryptoCreate(ACCOUNT).balance(ONE_MILLION_HBARS), uploadInitCode(TOKEN_CREATE_CONTRACT), contractCreate(TOKEN_CREATE_CONTRACT).gas(GAS_TO_OFFER)) @@ -329,9 +934,9 @@ final HapiSpec createTokenWithInvalidExpiry() { FIRST_CREATE_TXN, ResponseCodeEnum.CONTRACT_REVERT_EXECUTED, TransactionRecordAsserts.recordWith() - .status(INVALID_EXPIRATION_TIME) + .status(INVALID_RENEWAL_PERIOD) .contractCallResult(ContractFnResultAsserts.resultWith() - .error(INVALID_EXPIRATION_TIME.name())))); + .error(INVALID_RENEWAL_PERIOD.name())))); } // TEST-013 @@ -475,6 +1080,81 @@ final HapiSpec delegateCallTokenCreateFails() { getContractInfo(TOKEN_CREATE_CONTRACT)); } + @HapiTest + final HapiSpec createTokenWithFixedFeeThenTransferAndAssessFee() { + final var createTokenNum = new AtomicLong(); + final var FEE_COLLECTOR = "feeCollector"; + final var RECIPIENT = "recipient"; + final var SECOND_RECIPIENT = "secondRecipient"; + return defaultHapiSpec("createTokenWithFixedFeeThenTransferAndAssessFee") + .given( + cryptoCreate(ACCOUNT).balance(ONE_MILLION_HBARS), + cryptoCreate(RECIPIENT).balance(ONE_HUNDRED_HBARS), + cryptoCreate(SECOND_RECIPIENT), + cryptoCreate(FEE_COLLECTOR).balance(0L), + uploadInitCode(TOKEN_MISC_OPERATIONS_CONTRACT), + contractCreate(TOKEN_MISC_OPERATIONS_CONTRACT) + .gas(GAS_TO_OFFER_2) + .autoRenewAccountId(ACCOUNT), + newKeyNamed(THRESHOLD_KEY) + .shape(THRESHOLD_KEY_SHAPE.signedWith(sigs(ON, TOKEN_MISC_OPERATIONS_CONTRACT))), + cryptoUpdate(ACCOUNT).key(THRESHOLD_KEY), + cryptoUpdate(FEE_COLLECTOR).key(THRESHOLD_KEY), + cryptoUpdate(RECIPIENT).key(THRESHOLD_KEY), + cryptoUpdate(SECOND_RECIPIENT).key(THRESHOLD_KEY), + cryptoTransfer(TokenMovement.movingHbar(ONE_HUNDRED_HBARS) + .between(GENESIS, TOKEN_MISC_OPERATIONS_CONTRACT)), + getContractInfo(TOKEN_MISC_OPERATIONS_CONTRACT) + .has(ContractInfoAsserts.contractWith().autoRenewAccountId(ACCOUNT)) + .logged()) + .when(withOpContext((spec, opLog) -> allRunFor( + spec, + contractCall( + TOKEN_MISC_OPERATIONS_CONTRACT, + "createTokenWithHbarsFixedFeeAndTransferIt", + 10L, + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getAccountID(FEE_COLLECTOR))), + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getAccountID(RECIPIENT))), + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getAccountID(SECOND_RECIPIENT))), + HapiParserUtil.asHeadlongAddress( + asAddress(spec.registry().getAccountID(ACCOUNT)))) + .via(FIRST_CREATE_TXN) + .gas(GAS_TO_OFFER_2) + .sending(DEFAULT_AMOUNT_TO_SEND) + .payingWith(ACCOUNT) + .exposingResultTo(result -> { + log.info(EXPLICIT_CREATE_RESULT, result[0]); + final var res = (Address) result[0]; + createTokenNum.set(res.value().longValueExact()); + }) + .hasKnownStatus(SUCCESS)))) + .then(withOpContext((spec, opLog) -> allRunFor( + spec, + getTxnRecord(FIRST_CREATE_TXN).andAllChildRecords().logged(), + getAccountBalance(RECIPIENT) + .hasTokenBalance( + asTokenString(TokenID.newBuilder() + .setTokenNum(createTokenNum.get()) + .build()), + 0L), + getAccountBalance(SECOND_RECIPIENT) + .hasTokenBalance( + asTokenString(TokenID.newBuilder() + .setTokenNum(createTokenNum.get()) + .build()), + 1L), + getAccountBalance(ACCOUNT) + .hasTokenBalance( + asTokenString(TokenID.newBuilder() + .setTokenNum(createTokenNum.get()) + .build()), + 199L), + getAccountBalance(FEE_COLLECTOR).hasTinyBars(10L)))); + } + @Override protected Logger getResultsLogger() { return log; diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/ethereum/EthereumSuite.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/ethereum/EthereumSuite.java index 044fa1758d1e..2a1158f61dfc 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/ethereum/EthereumSuite.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/ethereum/EthereumSuite.java @@ -41,6 +41,7 @@ import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoCreate; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoDelete; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoTransfer; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoUpdate; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoUpdateAliased; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.ethereumCall; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.ethereumCallWithFunctionAbi; @@ -97,6 +98,8 @@ import com.hedera.services.bdd.junit.HapiTest; import com.hedera.services.bdd.junit.HapiTestSuite; import com.hedera.services.bdd.spec.HapiSpec; +import com.hedera.services.bdd.spec.assertions.TransactionRecordAsserts; +import com.hedera.services.bdd.spec.keys.SigControl; import com.hedera.services.bdd.spec.queries.meta.HapiGetTxnRecord; import com.hedera.services.bdd.spec.transactions.TxnUtils; import com.hedera.services.bdd.suites.BddMethodIsNotATest; @@ -114,6 +117,7 @@ import java.util.Arrays; import java.util.Comparator; import java.util.List; +import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Stream; import org.apache.logging.log4j.LogManager; @@ -175,6 +179,7 @@ public List getSpecsInSuite() { callToTokenAddressViaEip2930TxSuccessfully(), transferTokensViaEip2930TxSuccessfully(), accountDeletionResetsTheAliasNonce(), + etx007FungibleTokenCreateWithFeesHappyPath(), legacyUnprotectedEtxBeforeEIP155WithDefaultChainId())) .toList(); } @@ -1174,6 +1179,74 @@ HapiSpec legacyUnprotectedEtxBeforeEIP155WithDefaultChainId() { .hasPriority(recordWith().status(SUCCESS))))); } + @HapiTest + final HapiSpec etx007FungibleTokenCreateWithFeesHappyPath() { + final var createdTokenNum = new AtomicLong(); + final var feeCollectorAndAutoRenew = "feeCollectorAndAutoRenew"; + final var contract = "TokenCreateContract"; + final var EXISTING_TOKEN = "EXISTING_TOKEN"; + final var firstTxn = "firstCreateTxn"; + final long DEFAULT_AMOUNT_TO_SEND = 20 * ONE_HBAR; + return defaultHapiSpec("etx007FungibleTokenCreateWithFeesHappyPath") + .given( + newKeyNamed(SECP_256K1_SOURCE_KEY).shape(SECP_256K1_SHAPE), + cryptoTransfer(tinyBarsFromAccountToAlias(GENESIS, SECP_256K1_SOURCE_KEY, ONE_HUNDRED_HBARS)) + .via(AUTO_ACCOUNT_TRANSACTION_NAME), + cryptoCreate(feeCollectorAndAutoRenew) + .keyShape(SigControl.ED25519_ON) + .balance(ONE_MILLION_HBARS), + uploadInitCode(contract), + contractCreate(contract).gas(GAS_LIMIT), + tokenCreate(EXISTING_TOKEN).decimals(5), + tokenAssociate(feeCollectorAndAutoRenew, EXISTING_TOKEN), + cryptoUpdate(feeCollectorAndAutoRenew).key(SECP_256K1_SOURCE_KEY)) + .when(withOpContext((spec, opLog) -> allRunFor( + spec, + ethereumCall( + contract, + "createTokenWithAllCustomFeesAvailable", + spec.registry() + .getKey(SECP_256K1_SOURCE_KEY) + .getECDSASecp256K1() + .toByteArray(), + asHeadlongAddress( + asAddress(spec.registry().getAccountID(feeCollectorAndAutoRenew))), + asHeadlongAddress( + asAddress(spec.registry().getTokenID(EXISTING_TOKEN))), + asHeadlongAddress( + asAddress(spec.registry().getAccountID(feeCollectorAndAutoRenew))), + 8_000_000L) + .via(firstTxn) + .gasLimit(GAS_LIMIT) + .payingWith(feeCollectorAndAutoRenew) + .sending(DEFAULT_AMOUNT_TO_SEND) + .hasKnownStatus(SUCCESS) + .exposingResultTo(result -> { + log.info("Explicit create result" + " is {}", result[0]); + final var res = (Address) result[0]; + createdTokenNum.set(res.value().longValueExact()); + })))) + .then( + getTxnRecord(firstTxn).andAllChildRecords().logged(), + childRecordsCheck( + firstTxn, + SUCCESS, + TransactionRecordAsserts.recordWith().status(ResponseCodeEnum.SUCCESS)), + withOpContext((spec, ignore) -> { + final var op = getTxnRecord(firstTxn); + allRunFor(spec, op); + + final var callResult = op.getResponseRecord().getContractCallResult(); + final var gasUsed = callResult.getGasUsed(); + final var amount = callResult.getAmount(); + final var gasLimit = callResult.getGas(); + Assertions.assertEquals(DEFAULT_AMOUNT_TO_SEND, amount); + Assertions.assertEquals(GAS_LIMIT, gasLimit); + Assertions.assertTrue(gasUsed > 0L); + Assertions.assertTrue(callResult.hasContractID() && callResult.hasSenderId()); + })); + } + @Override protected Logger getResultsLogger() { return log;