Skip to content

Commit

Permalink
feat: Add TokenUpdateNFTs as a smart contract operation v2 (#15445)
Browse files Browse the repository at this point in the history
Signed-off-by: Stanimir Stoyanov <stanimir.stoyanov@limechain.tech>
  • Loading branch information
stoyanov-st authored Sep 13, 2024
1 parent f51f651 commit aaff32c
Show file tree
Hide file tree
Showing 21 changed files with 725 additions and 72 deletions.
1 change: 1 addition & 0 deletions hedera-node/configuration/dev/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ upgrade.artifacts.path=data/upgrade
contracts.chainId=298
contracts.maxGasPerSec=15000000000
contracts.systemContract.tokenInfo.v2.enabled=true
contracts.systemContract.updateNFTsMetadata.enabled=true
# Needed for end-end tests running on mod-service code
staking.periodMins=1
staking.fees.nodeRewardPercentage=10
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@
#Overrides that differ based on the network
bootstrap.genesisPublicKey=c249a323c878f5b5e2daccda6d731e6fdc32f870228d1cd4fae559d947dbc36c
contracts.chainId=297
contracts.systemContract.updateNFTsMetadata.enabled=true
ledger.id=0x02
entities.unlimitedAutoAssociationsEnabled=true
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ public record ContractsConfig(
boolean isGasPrecisionLossFixEnabled,
@ConfigProperty(value = "systemContract.canonicalViewGas.enabled", defaultValue = "true") @NetworkProperty
boolean isCanonicalViewGasEnabled,
@ConfigProperty(value = "systemContract.updateNFTsMetadata.enabled", defaultValue = "false") @NetworkProperty
boolean systemContractUpdateNFTsMetadataEnabled,
@ConfigProperty(value = "evm.version.dynamic", defaultValue = "false") @NetworkProperty
boolean evmVersionDynamic,
@ConfigProperty(value = "evm.allowCallsToNonContractAccounts", defaultValue = "true") @NetworkProperty
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ public enum DispatchType {
WIPE_FUNGIBLE(HederaFunctionality.TOKEN_ACCOUNT_WIPE, TOKEN_FUNGIBLE_COMMON),
WIPE_NFT(HederaFunctionality.TOKEN_ACCOUNT_WIPE, TOKEN_NON_FUNGIBLE_UNIQUE),
UPDATE(HederaFunctionality.TOKEN_UPDATE, DEFAULT),
TOKEN_UPDATE_NFTS(HederaFunctionality.TOKEN_UPDATE_NFTS, DEFAULT),
UTIL_PRNG(HederaFunctionality.UTIL_PRNG, DEFAULT),
TOKEN_INFO(HederaFunctionality.TOKEN_GET_INFO, DEFAULT),
UPDATE_TOKEN_CUSTOM_FEES(HederaFunctionality.TOKEN_FEE_SCHEDULE_UPDATE, DEFAULT);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.transfer.Erc721TransferFromTranslator;
import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.update.UpdateExpiryTranslator;
import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.update.UpdateKeysTranslator;
import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.update.UpdateNFTsMetadataTranslator;
import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.update.UpdateTranslator;
import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.updatetokencustomfees.UpdateTokenCustomFeesTranslator;
import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.wipe.WipeTranslator;
Expand Down Expand Up @@ -428,4 +429,13 @@ static CallTranslator<HtsCallAttempt> provideUpdateTokenCustomFeesTranslator(
@NonNull final UpdateTokenCustomFeesTranslator translator) {
return translator;
}

@Provides
@Singleton
@IntoSet
@Named("HtsTranslators")
static CallTranslator<HtsCallAttempt> provideUpdateNFTsMetadataTranslator(
@NonNull final UpdateNFTsMetadataTranslator translator) {
return translator;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,13 @@

import com.esaulpaugh.headlong.abi.Address;
import com.esaulpaugh.headlong.abi.Tuple;
import com.google.common.primitives.Longs;
import com.hedera.hapi.node.base.Duration;
import com.hedera.hapi.node.base.Key;
import com.hedera.hapi.node.base.Timestamp;
import com.hedera.hapi.node.base.TokenID;
import com.hedera.hapi.node.state.token.Token;
import com.hedera.hapi.node.token.TokenUpdateNftsTransactionBody;
import com.hedera.hapi.node.token.TokenUpdateTransactionBody;
import com.hedera.hapi.node.transaction.TransactionBody;
import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.AddressIdConverter;
Expand All @@ -39,6 +41,7 @@
import com.hedera.node.app.service.contract.impl.exec.utils.TokenExpiryWrapper;
import com.hedera.node.app.service.contract.impl.exec.utils.TokenKeyWrapper;
import com.hedera.node.app.service.contract.impl.utils.ConversionUtils;
import com.hedera.pbj.runtime.io.buffer.Bytes;
import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.Nullable;
import java.math.BigInteger;
Expand Down Expand Up @@ -82,6 +85,8 @@ public class UpdateDecoder {

private static final int KEY_TYPE = 0;
private static final int KEY_VALUE = 1;
private static final int SERIAL_NUMBERS = 1;
private static final int METADATA = 2;

private static final int INHERIT_ACCOUNT_KEY = 0;
private static final int CONTRACT_ID = 1;
Expand Down Expand Up @@ -222,6 +227,22 @@ public TransactionBody decodeTokenUpdateKeys(@NonNull final HtsCallAttempt attem
}
}

public TransactionBody decodeUpdateNFTsMetadata(@NonNull final HtsCallAttempt attempt) {
final var call = UpdateNFTsMetadataTranslator.UPDATE_NFTs_METADATA.decodeCall(
attempt.input().toArrayUnsafe());

final var tokenId = ConversionUtils.asTokenId(call.get(TOKEN_ADDRESS));
final List<Long> serialNumbers = Longs.asList(call.get(SERIAL_NUMBERS));
final byte[] metadata = call.get(METADATA);

final var txnBodyBuilder = TokenUpdateNftsTransactionBody.newBuilder()
.token(tokenId)
.serialNumbers(serialNumbers)
.metadata(Bytes.wrap(metadata));

return TransactionBody.newBuilder().tokenUpdateNfts(txnBodyBuilder).build();
}

private TransactionBody bodyWith(
final List<TokenKeyWrapper> tokenKeys, final TokenUpdateTransactionBody.Builder builder) {
tokenKeys.forEach(tokenKeyWrapper -> {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* Copyright (C) 2023-2024 Hedera Hashgraph, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.update;

import com.esaulpaugh.headlong.abi.Function;
import com.hedera.hapi.node.base.AccountID;
import com.hedera.hapi.node.transaction.TransactionBody;
import com.hedera.node.app.service.contract.impl.exec.gas.DispatchType;
import com.hedera.node.app.service.contract.impl.exec.gas.SystemContractGasCalculator;
import com.hedera.node.app.service.contract.impl.exec.systemcontracts.common.AbstractCallTranslator;
import com.hedera.node.app.service.contract.impl.exec.systemcontracts.common.Call;
import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.DispatchForResponseCodeHtsCall;
import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.HtsCallAttempt;
import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.ReturnTypes;
import com.hedera.node.app.service.contract.impl.hevm.HederaWorldUpdater;
import com.hedera.node.config.data.ContractsConfig;
import edu.umd.cs.findbugs.annotations.NonNull;
import javax.inject.Inject;

public class UpdateNFTsMetadataTranslator extends AbstractCallTranslator<HtsCallAttempt> {
public static final Function UPDATE_NFTs_METADATA =
new Function("updateNFTsMetadata(address,int64[],bytes)", ReturnTypes.INT);

private final UpdateDecoder decoder;

@Inject
public UpdateNFTsMetadataTranslator(@NonNull final UpdateDecoder decoder) {
this.decoder = decoder;
}

@Override
public boolean matches(@NonNull final HtsCallAttempt attempt) {
return attempt.configuration().getConfigData(ContractsConfig.class).systemContractUpdateNFTsMetadataEnabled()
&& attempt.isSelector(UPDATE_NFTs_METADATA);
}

@Override
public Call callFrom(@NonNull final HtsCallAttempt attempt) {
return new DispatchForResponseCodeHtsCall(
attempt, decoder.decodeUpdateNFTsMetadata(attempt), UpdateNFTsMetadataTranslator::gasRequirement);
}

public static long gasRequirement(
@NonNull final TransactionBody body,
@NonNull final SystemContractGasCalculator systemContractGasCalculator,
@NonNull final HederaWorldUpdater.Enhancement enhancement,
@NonNull final AccountID payerId) {
return systemContractGasCalculator.gasRequirement(body, DispatchType.TOKEN_UPDATE_NFTS, payerId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@
package com.hedera.node.app.service.contract.impl.test.exec.systemcontracts.hts.update;

import static com.hedera.node.app.service.contract.impl.test.TestHelpers.FUNGIBLE_TOKEN_HEADLONG_ADDRESS;
import static com.hedera.node.app.service.contract.impl.test.TestHelpers.NON_FUNGIBLE_TOKEN_HEADLONG_ADDRESS;
import static com.hedera.node.app.service.contract.impl.test.TestHelpers.OWNER_HEADLONG_ADDRESS;
import static com.hedera.node.app.service.contract.impl.test.TestHelpers.OWNER_ID;
import static java.util.Objects.requireNonNull;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.BDDMockito.given;
Expand All @@ -28,10 +30,12 @@
import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.HtsCallAttempt;
import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.update.UpdateDecoder;
import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.update.UpdateExpiryTranslator;
import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.update.UpdateNFTsMetadataTranslator;
import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.update.UpdateTranslator;
import java.time.Instant;
import org.apache.tuweni.bytes.Bytes;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
Expand Down Expand Up @@ -63,76 +67,95 @@ class UpdateDecoderTest {
// Expiry
expiry);

@BeforeEach
void setUp() {
given(attempt.addressIdConverter()).willReturn(addressIdConverter);
given(addressIdConverter.convert(OWNER_HEADLONG_ADDRESS)).willReturn(OWNER_ID);
@Nested
class UpdateTokenInfoAndExpiry {
@BeforeEach
void setUp() {
given(attempt.addressIdConverter()).willReturn(addressIdConverter);
given(addressIdConverter.convert(OWNER_HEADLONG_ADDRESS)).willReturn(OWNER_ID);
}

@Test
void updateV1Works() {
final var encoded = Bytes.wrapByteBuffer(UpdateTranslator.TOKEN_UPDATE_INFO_FUNCTION_V1.encodeCallWithArgs(
FUNGIBLE_TOKEN_HEADLONG_ADDRESS, hederaToken));
given(attempt.input()).willReturn(encoded);

final var body = subject.decodeTokenUpdateV1(attempt);
final var tokenUpdate = body.tokenUpdateOrThrow();
assertEquals(tokenUpdate.name(), newName);
}

@Test
void updateV2Works() {
final var encoded = Bytes.wrapByteBuffer(UpdateTranslator.TOKEN_UPDATE_INFO_FUNCTION_V2.encodeCallWithArgs(
FUNGIBLE_TOKEN_HEADLONG_ADDRESS, hederaToken));
given(attempt.input()).willReturn(encoded);

final var body = subject.decodeTokenUpdateV2(attempt);
final var tokenUpdate = body.tokenUpdateOrThrow();
assertEquals(tokenUpdate.name(), newName);
}

@Test
void updateV3Works() {
final var encoded = Bytes.wrapByteBuffer(UpdateTranslator.TOKEN_UPDATE_INFO_FUNCTION_V3.encodeCallWithArgs(
FUNGIBLE_TOKEN_HEADLONG_ADDRESS, hederaToken));
given(attempt.input()).willReturn(encoded);

final var body = subject.decodeTokenUpdateV3(attempt);
final var tokenUpdate = body.tokenUpdateOrThrow();
assertEquals(tokenUpdate.name(), newName);
}

@Test
void updateExpiryV1Works() {
final var encoded =
Bytes.wrapByteBuffer(UpdateExpiryTranslator.UPDATE_TOKEN_EXPIRY_INFO_V1.encodeCallWithArgs(
FUNGIBLE_TOKEN_HEADLONG_ADDRESS, expiry));
given(attempt.input()).willReturn(encoded);

final var body = subject.decodeTokenUpdateExpiryV1(attempt);
final var tokenUpdate = body.tokenUpdateOrThrow();

assertNotNull(tokenUpdate.expiry());
assertNotNull(tokenUpdate.autoRenewPeriod());

assertEquals(EXPIRY_TIMESTAMP, tokenUpdate.expiry().seconds());
assertEquals(AUTO_RENEW_PERIOD, tokenUpdate.autoRenewPeriod().seconds());
assertEquals(OWNER_ID, tokenUpdate.autoRenewAccount());
}

@Test
void updateExpiryV2Works() {
final var encoded =
Bytes.wrapByteBuffer(UpdateExpiryTranslator.UPDATE_TOKEN_EXPIRY_INFO_V2.encodeCallWithArgs(
FUNGIBLE_TOKEN_HEADLONG_ADDRESS, expiry));
given(attempt.input()).willReturn(encoded);

final var body = subject.decodeTokenUpdateExpiryV2(attempt);
final var tokenUpdate = body.tokenUpdateOrThrow();

assertNotNull(tokenUpdate.expiry());
assertNotNull(tokenUpdate.autoRenewPeriod());

assertEquals(EXPIRY_TIMESTAMP, tokenUpdate.expiry().seconds());
assertEquals(AUTO_RENEW_PERIOD, tokenUpdate.autoRenewPeriod().seconds());
assertEquals(OWNER_ID, tokenUpdate.autoRenewAccount());
}
}

@Test
void updateV1Works() {
final var encoded = Bytes.wrapByteBuffer(UpdateTranslator.TOKEN_UPDATE_INFO_FUNCTION_V1.encodeCallWithArgs(
FUNGIBLE_TOKEN_HEADLONG_ADDRESS, hederaToken));
void updateNFTsMetadataWorks() {
final var encoded = Bytes.wrapByteBuffer(UpdateNFTsMetadataTranslator.UPDATE_NFTs_METADATA.encodeCallWithArgs(
NON_FUNGIBLE_TOKEN_HEADLONG_ADDRESS, new long[] {1, 2, 3}, "Jerry".getBytes()));
given(attempt.input()).willReturn(encoded);

final var body = subject.decodeTokenUpdateV1(attempt);
final var tokenUpdate = body.tokenUpdateOrThrow();
assertEquals(tokenUpdate.name(), newName);
}

@Test
void updateV2Works() {
final var encoded = Bytes.wrapByteBuffer(UpdateTranslator.TOKEN_UPDATE_INFO_FUNCTION_V2.encodeCallWithArgs(
FUNGIBLE_TOKEN_HEADLONG_ADDRESS, hederaToken));
given(attempt.input()).willReturn(encoded);

final var body = subject.decodeTokenUpdateV2(attempt);
final var tokenUpdate = body.tokenUpdateOrThrow();
assertEquals(tokenUpdate.name(), newName);
}

@Test
void updateV3Works() {
final var encoded = Bytes.wrapByteBuffer(UpdateTranslator.TOKEN_UPDATE_INFO_FUNCTION_V3.encodeCallWithArgs(
FUNGIBLE_TOKEN_HEADLONG_ADDRESS, hederaToken));
given(attempt.input()).willReturn(encoded);

final var body = subject.decodeTokenUpdateV3(attempt);
final var tokenUpdate = body.tokenUpdateOrThrow();
assertEquals(tokenUpdate.name(), newName);
}

@Test
void updateExpiryV1Works() {
final var encoded = Bytes.wrapByteBuffer(UpdateExpiryTranslator.UPDATE_TOKEN_EXPIRY_INFO_V1.encodeCallWithArgs(
FUNGIBLE_TOKEN_HEADLONG_ADDRESS, expiry));
given(attempt.input()).willReturn(encoded);

final var body = subject.decodeTokenUpdateExpiryV1(attempt);
final var tokenUpdate = body.tokenUpdateOrThrow();

assertNotNull(tokenUpdate.expiry());
assertNotNull(tokenUpdate.autoRenewPeriod());

assertEquals(EXPIRY_TIMESTAMP, tokenUpdate.expiry().seconds());
assertEquals(AUTO_RENEW_PERIOD, tokenUpdate.autoRenewPeriod().seconds());
assertEquals(OWNER_ID, tokenUpdate.autoRenewAccount());
}

@Test
void updateExpiryV2Works() {
final var encoded = Bytes.wrapByteBuffer(UpdateExpiryTranslator.UPDATE_TOKEN_EXPIRY_INFO_V2.encodeCallWithArgs(
FUNGIBLE_TOKEN_HEADLONG_ADDRESS, expiry));
given(attempt.input()).willReturn(encoded);

final var body = subject.decodeTokenUpdateExpiryV2(attempt);
final var tokenUpdate = body.tokenUpdateOrThrow();

assertNotNull(tokenUpdate.expiry());
assertNotNull(tokenUpdate.autoRenewPeriod());
final var body = subject.decodeUpdateNFTsMetadata(attempt);
final var tokenUpdate = requireNonNull(body).tokenUpdateNftsOrThrow();

assertEquals(EXPIRY_TIMESTAMP, tokenUpdate.expiry().seconds());
assertEquals(AUTO_RENEW_PERIOD, tokenUpdate.autoRenewPeriod().seconds());
assertEquals(OWNER_ID, tokenUpdate.autoRenewAccount());
assertNotNull(tokenUpdate.metadata());
assertEquals("Jerry", tokenUpdate.metadata().asUtf8String());
assertEquals(3, tokenUpdate.serialNumbers().size());
}
}
Loading

0 comments on commit aaff32c

Please sign in to comment.