Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(HIP-657): Mutable metadata fields for dynamic NFTs #1748

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
197 changes: 197 additions & 0 deletions examples/src/main/java/UpdateNftsMetadataExample.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
/*-
*
* Hedera Java SDK
*
* Copyright (C) 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.
*
*/

import com.hedera.hashgraph.sdk.AccountCreateTransaction;
import com.hedera.hashgraph.sdk.AccountId;
import com.hedera.hashgraph.sdk.Client;
import com.hedera.hashgraph.sdk.NftId;
import com.hedera.hashgraph.sdk.PrivateKey;
import com.hedera.hashgraph.sdk.TokenAssociateTransaction;
import com.hedera.hashgraph.sdk.TokenCreateTransaction;
import com.hedera.hashgraph.sdk.TokenId;
import com.hedera.hashgraph.sdk.TokenInfoQuery;
import com.hedera.hashgraph.sdk.TokenMintTransaction;
import com.hedera.hashgraph.sdk.TokenNftInfoQuery;
import com.hedera.hashgraph.sdk.TokenType;
import com.hedera.hashgraph.sdk.TokenUpdateNftsTransaction;
import com.hedera.hashgraph.sdk.TransferTransaction;
import io.github.cdimascio.dotenv.Dotenv;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;

public class UpdateNftsMetadataExample {

// 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 static final PrivateKey METADATA_KEY = PrivateKey.generateED25519();

private static final byte[] INITIAL_METADATA = new byte[]{1};

private static final byte[] UPDATED_METADATA = new byte[]{1, 2};

private Client client;

public static void main(String[] args) throws Exception {
UpdateNftsMetadataExample example = new UpdateNftsMetadataExample();

// demonstrate with a mutable token (the one that has an admin key)
example.updateNftsMetadata(example.getMutableTokenCreateTransaction());

// demonstrate with an immutable token (the one that doesn't have an admin key)
example.updateNftsMetadata(example.getImmutableTokenCreateTransaction());

example.cleanUp();
}

private UpdateNftsMetadataExample() throws Exception {
client = ClientHelper.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);
}

private void updateNftsMetadata(TokenCreateTransaction tokenCreateTransaction) throws Exception {
// Create a non-fungible token (NFT) with the metadata key field set
var tokenCreateResponse = tokenCreateTransaction.sign(OPERATOR_KEY).execute(client);
var tokenCreateReceipt = tokenCreateResponse.getReceipt(client);
System.out.println("Status of token create transaction: " + tokenCreateReceipt.status);

// Get the token ID of the token that was created
var tokenId = tokenCreateReceipt.tokenId;
System.out.println("Token id: " + tokenId);

// Query for the token information stored in consensus node state to see that the metadata key is set
var tokenInfo = new TokenInfoQuery()
.setTokenId(tokenId)
.execute(client);

System.out.println("Token metadata key: " + tokenInfo.metadataKey);

// Mint the first NFT and set the initial metadata for the NFT
var tokenMintTransaction = new TokenMintTransaction()
.setMetadata(List.of(INITIAL_METADATA))
.setTokenId(tokenId);

tokenMintTransaction.getMetadata().forEach(metadata -> {
System.out.println("Set metadata: " + Arrays.toString(metadata));
});

var tokenMintResponse = tokenMintTransaction.execute(client);

// Get receipt for mint token transaction
var tokenMintReceipt = tokenMintResponse.getReceipt(client);
System.out.println("Status of token mint transaction: " + tokenMintReceipt.status);

var nftSerials = tokenMintReceipt.serials;
// Check that metadata on the NFT was set correctly
getMetadataList(client, tokenId, nftSerials).forEach(metadata -> {
System.out.println("Metadata after mint: " + Arrays.toString(metadata));
});

// Create an account to send the NFT to
var accountCreateTransaction = new AccountCreateTransaction()
.setKey(OPERATOR_KEY)
.setMaxAutomaticTokenAssociations(10) // If the account does not have any automatic token association slots open ONLY then associate the NFT to the account
.execute(client);

var newAccountId = accountCreateTransaction.getReceipt(client).accountId;

// Transfer the NFT to the new account
new TransferTransaction()
.addNftTransfer(tokenId.nft(nftSerials.get(0)), OPERATOR_ID, newAccountId)
.execute(client);

// Update nft's metadata
var tokenUpdateNftsTransaction = new TokenUpdateNftsTransaction()
.setTokenId(tokenId)
.setSerials(nftSerials)
.setMetadata(UPDATED_METADATA)
.freezeWith(client);

System.out.println("Updated metadata: " + Arrays.toString(tokenUpdateNftsTransaction.getMetadata()));
var tokenUpdateNftsResponse = tokenUpdateNftsTransaction.sign(METADATA_KEY).execute(client);

// Get receipt for update nfts metadata transaction
var tokenUpdateNftsReceipt = tokenUpdateNftsResponse.getReceipt(client);
System.out.println("Status of token update nfts metadata transaction: " + tokenUpdateNftsReceipt.status);

// Check that metadata for the NFT was updated correctly
getMetadataList(client, tokenId, nftSerials).forEach(metadata -> {
System.out.println("Metadata after update: " + Arrays.toString(metadata));
});
}

private TokenCreateTransaction getMutableTokenCreateTransaction() {
System.out.println("Creating a mutable token..");

// Create a mutable token with a metadata key
return new TokenCreateTransaction()
.setTokenName("Mutable")
.setTokenSymbol("MUT")
.setTokenType(TokenType.NON_FUNGIBLE_UNIQUE)
.setTreasuryAccountId(OPERATOR_ID)
.setAdminKey(OPERATOR_KEY)
.setSupplyKey(OPERATOR_KEY)
.setMetadataKey(METADATA_KEY)
.freezeWith(client);
}

private TokenCreateTransaction getImmutableTokenCreateTransaction() {
System.out.println("Creating an immutable token..");

// Create an immutable token with a metadata key
return new TokenCreateTransaction()
.setTokenName("Immutable")
.setTokenSymbol("IMUT")
.setTokenType(TokenType.NON_FUNGIBLE_UNIQUE)
.setTreasuryAccountId(OPERATOR_ID)
.setSupplyKey(OPERATOR_KEY)
.setMetadataKey(METADATA_KEY)
.freezeWith(client);
}

private void cleanUp() throws Exception {
client.close();
}

private static List<byte[]> getMetadataList(Client client, TokenId tokenId, List<Long> nftSerials) {
return nftSerials.stream()
.map(serial -> new NftId(tokenId, serial))
.flatMap(nftId -> {
try {
return new TokenNftInfoQuery()
.setNftId(nftId)
.execute(client).stream();
} catch (Exception e) {
throw new RuntimeException(e);
}
})
.map(tokenNftInfo -> tokenNftInfo.metadata)
.toList();
}
}
8 changes: 8 additions & 0 deletions sdk/src/integrationTest/java/NftMetadataGenerator.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

public class NftMetadataGenerator {
private NftMetadataGenerator() {
Expand All @@ -15,6 +17,12 @@ public static List<byte[]> generate(byte metadataCount) {
return metadatas;
}

public static List<byte[]> generate(byte[] metadata, int count) {
return IntStream.range(0, count)
.mapToObj(i -> metadata.clone())
.collect(Collectors.toList());
}

public static List<byte[]> generateOneLarge() {
return Collections.singletonList(new byte[101]);
}
Expand Down
13 changes: 13 additions & 0 deletions sdk/src/integrationTest/java/TokenInfoIntegrationTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;

class TokenInfoIntegrationTest {

@Test
@DisplayName("Can query token info when all keys are different")
void canQueryTokenInfoWhenAllKeysAreDifferent() throws Exception {
Expand All @@ -28,6 +29,8 @@ void canQueryTokenInfoWhenAllKeysAreDifferent() throws Exception {
var key3 = PrivateKey.generateED25519();
var key4 = PrivateKey.generateED25519();
var key5 = PrivateKey.generateED25519();
var key6 = PrivateKey.generateED25519();
var key7 = PrivateKey.generateED25519();

var response = new TokenCreateTransaction()
.setTokenName("ffff")
Expand All @@ -40,6 +43,8 @@ void canQueryTokenInfoWhenAllKeysAreDifferent() throws Exception {
.setWipeKey(key3)
.setKycKey(key4)
.setSupplyKey(key5)
.setPauseKey(key6)
.setMetadataKey(key7)
.setFreezeDefault(false)
.freezeWith(testEnv.client)
.sign(key1)
Expand All @@ -61,11 +66,15 @@ void canQueryTokenInfoWhenAllKeysAreDifferent() throws Exception {
assertThat(info.wipeKey).isNotNull();
assertThat(info.kycKey).isNotNull();
assertThat(info.supplyKey).isNotNull();
assertThat(info.pauseKey).isNotNull();
assertThat(info.metadataKey).isNotNull();
assertThat(info.adminKey.toString()).isEqualTo(key1.getPublicKey().toString());
assertThat(info.freezeKey.toString()).isEqualTo(key2.getPublicKey().toString());
assertThat(info.wipeKey.toString()).isEqualTo(key3.getPublicKey().toString());
assertThat(info.kycKey.toString()).isEqualTo(key4.getPublicKey().toString());
assertThat(info.supplyKey.toString()).isEqualTo(key5.getPublicKey().toString());
assertThat(info.pauseKey.toString()).isEqualTo(key6.getPublicKey().toString());
assertThat(info.metadataKey.toString()).isEqualTo(key7.getPublicKey().toString());
assertThat(info.defaultFreezeStatus).isNotNull();
assertThat(info.defaultFreezeStatus).isFalse();
assertThat(info.defaultKycStatus).isNotNull();
Expand Down Expand Up @@ -111,6 +120,8 @@ void canQueryTokenInfoWhenTokenIsCreatedWithMinimalProperties() throws Exception
assertThat(info.wipeKey).isNull();
assertThat(info.kycKey).isNull();
assertThat(info.supplyKey).isNull();
assertThat(info.pauseKey).isNull();
assertThat(info.metadataKey).isNull();
assertThat(info.defaultFreezeStatus).isNull();
assertThat(info.defaultKycStatus).isNull();
assertThat(info.tokenType).isEqualTo(TokenType.FUNGIBLE_COMMON);
Expand Down Expand Up @@ -161,6 +172,8 @@ void canQueryNfts() throws Exception {
assertThat(info.wipeKey).isNull();
assertThat(info.kycKey).isNull();
assertThat(info.supplyKey).isNotNull();
assertThat(info.pauseKey).isNull();
assertThat(info.metadataKey).isNull();
assertThat(info.defaultFreezeStatus).isNull();
assertThat(info.defaultKycStatus).isNull();
assertThat(info.tokenType).isEqualTo(TokenType.NON_FUNGIBLE_UNIQUE);
Expand Down
17 changes: 11 additions & 6 deletions sdk/src/integrationTest/java/TokenUpdateIntegrationTest.java
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;

import com.google.errorprone.annotations.Var;
import com.hedera.hashgraph.sdk.Hbar;
import com.hedera.hashgraph.sdk.ReceiptStatusException;
import com.hedera.hashgraph.sdk.Status;
import com.hedera.hashgraph.sdk.TokenCreateTransaction;
import com.hedera.hashgraph.sdk.TokenInfoQuery;
import com.hedera.hashgraph.sdk.TokenUpdateTransaction;
import java.util.Objects;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import java.util.Objects;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;

class TokenUpdateIntegrationTest {

@Test
@DisplayName("Can update token")
void canUpdateToken() throws Exception {
Expand All @@ -30,6 +29,8 @@ void canUpdateToken() throws Exception {
.setWipeKey(testEnv.operatorKey)
.setKycKey(testEnv.operatorKey)
.setSupplyKey(testEnv.operatorKey)
.setPauseKey(testEnv.operatorKey)
.setMetadataKey(testEnv.operatorKey)
.setFreezeDefault(false)
.execute(testEnv.client);

Expand All @@ -54,6 +55,8 @@ void canUpdateToken() throws Exception {
assertThat(info.wipeKey.toString()).isEqualTo(testEnv.operatorKey.toString());
assertThat(info.kycKey.toString()).isEqualTo(testEnv.operatorKey.toString());
assertThat(info.supplyKey.toString()).isEqualTo(testEnv.operatorKey.toString());
assertThat(info.pauseKey.toString()).isEqualTo(testEnv.operatorKey.toString());
assertThat(info.metadataKey.toString()).isEqualTo(testEnv.operatorKey.toString());
assertThat(info.defaultFreezeStatus).isNotNull().isFalse();
assertThat(info.defaultKycStatus).isNotNull().isFalse();

Expand Down Expand Up @@ -83,6 +86,8 @@ void canUpdateToken() throws Exception {
assertThat(info.wipeKey.toString()).isEqualTo(testEnv.operatorKey.toString());
assertThat(info.kycKey.toString()).isEqualTo(testEnv.operatorKey.toString());
assertThat(info.supplyKey.toString()).isEqualTo(testEnv.operatorKey.toString());
assertThat(info.pauseKey.toString()).isEqualTo(testEnv.operatorKey.toString());
assertThat(info.metadataKey.toString()).isEqualTo(testEnv.operatorKey.toString());
assertThat(info.defaultFreezeStatus).isNotNull();
assertThat(info.defaultFreezeStatus).isFalse();
assertThat(info.defaultKycStatus).isNotNull();
Expand Down
Loading
Loading