diff --git a/hiero-enterprise-spring-sample/pom.xml b/hiero-enterprise-spring-sample/pom.xml index 224bd11b..42e36a56 100644 --- a/hiero-enterprise-spring-sample/pom.xml +++ b/hiero-enterprise-spring-sample/pom.xml @@ -42,6 +42,11 @@ io.grpc grpc-inprocess + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.6.0 + diff --git a/hiero-enterprise-spring-sample/src/main/java/org/hiero/spring/sample/HieroEndpoint.java b/hiero-enterprise-spring-sample/src/main/java/org/hiero/spring/sample/HieroEndpoint.java deleted file mode 100644 index de2e2b44..00000000 --- a/hiero-enterprise-spring-sample/src/main/java/org/hiero/spring/sample/HieroEndpoint.java +++ /dev/null @@ -1,42 +0,0 @@ -package org.hiero.spring.sample; - -import java.util.Objects; -import org.hiero.base.AccountClient; -import org.hiero.base.data.Account; -import org.hiero.base.data.Block; -import org.hiero.base.data.Page; -import org.hiero.base.mirrornode.BlockRepository; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RestController; - -@RestController -public class HieroEndpoint { - - private final AccountClient client; - private final BlockRepository blockRepository; - - public HieroEndpoint(final AccountClient client, final BlockRepository blockRepository) { - this.client = Objects.requireNonNull(client, "client must not be null"); - this.blockRepository = - Objects.requireNonNull(blockRepository, "blockRepository must not be null"); - } - - @GetMapping("/") - public String createAccount() { - try { - final Account account = client.createAccount(); - return "Account " + account.accountId() + " created!"; - } catch (final Exception e) { - throw new RuntimeException("Error in Hedera call", e); - } - } - - @GetMapping("/blocks") - public Page getBlocks() { - try { - return blockRepository.findAll(); - } catch (final Exception e) { - throw new RuntimeException("Error querying blocks", e); - } - } -} diff --git a/hiero-enterprise-spring-sample/src/main/java/org/hiero/spring/sample/config/OpenApiConfig.java b/hiero-enterprise-spring-sample/src/main/java/org/hiero/spring/sample/config/OpenApiConfig.java new file mode 100644 index 00000000..8747b8da --- /dev/null +++ b/hiero-enterprise-spring-sample/src/main/java/org/hiero/spring/sample/config/OpenApiConfig.java @@ -0,0 +1,65 @@ +package org.hiero.spring.sample.config; + +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Contact; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.info.License; +import io.swagger.v3.oas.models.tags.Tag; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import org.springdoc.core.customizers.OpenApiCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class OpenApiConfig { + + @Bean + public OpenAPI hieroEnterpriseOpenAPI() { + return new OpenAPI() + .info( + new Info() + .title("Hiero Enterprise Spring Sample API") + .description( + "Interactive REST API for Hiero Enterprise Java integration. " + + "Provides endpoints for Accounts, Tokens, NFTs, Consensus Topics, and Files.") + .version("v0.20.0") + .contact( + new Contact() + .name("Hiero Enterprise Team") + .url("https://github.com/hiero-ledger/hiero-enterprise-java")) + .license( + new License() + .name("Apache 2.0") + .url("http://www.apache.org/licenses/LICENSE-2.0.html"))); + } + + @Bean + public OpenApiCustomizer sortTagsCustomizer() { + return openApi -> { + final List order = + List.of( + "Accounts", + "Fungible Tokens", + "Non-Fungible Tokens", + "Consensus Topics", + "Blocks", + "Network", + "Files"); + + List tags = openApi.getTags(); + if (tags != null) { + // Create a copy to avoid modification issues during sorting if it's a fixed-size list + List sortedTags = new ArrayList<>(tags); + sortedTags.sort( + Comparator.comparingInt( + tag -> { + int index = order.indexOf(tag.getName()); + return index == -1 ? order.size() : index; + })); + openApi.setTags(sortedTags); + } + }; + } +} diff --git a/hiero-enterprise-spring-sample/src/main/java/org/hiero/spring/sample/controller/AccountController.java b/hiero-enterprise-spring-sample/src/main/java/org/hiero/spring/sample/controller/AccountController.java new file mode 100644 index 00000000..dec0506d --- /dev/null +++ b/hiero-enterprise-spring-sample/src/main/java/org/hiero/spring/sample/controller/AccountController.java @@ -0,0 +1,184 @@ +package org.hiero.spring.sample.controller; + +import com.hedera.hashgraph.sdk.AccountId; +import com.hedera.hashgraph.sdk.Hbar; +import com.hedera.hashgraph.sdk.PrivateKey; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.Objects; +import org.hiero.base.AccountClient; +import org.hiero.base.data.Account; +import org.hiero.base.data.AccountInfo; +import org.hiero.base.mirrornode.AccountRepository; +import org.hiero.spring.sample.dto.account.AccountCreateRequest; +import org.hiero.spring.sample.dto.account.AccountDeleteRequest; +import org.hiero.spring.sample.dto.account.AccountResponse; +import org.hiero.spring.sample.dto.account.AccountUpdateRequest; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * REST controller for Hiero account operations. This controller provides endpoints for account + * lifecycle management and queries. + */ +@Tag( + name = "Accounts", + description = "Operations related to Hiero account lifecycle and balance queries") +@RestController +@RequestMapping("/api/v1/hiero/accounts") +public class AccountController { + + private final AccountClient accountClient; + private final AccountRepository accountRepository; + + public AccountController( + final AccountClient accountClient, final AccountRepository accountRepository) { + this.accountClient = Objects.requireNonNull(accountClient, "accountClient must not be null"); + this.accountRepository = + Objects.requireNonNull(accountRepository, "accountRepository must not be null"); + } + + /** + * Creates a new Hiero account. + * + * @param request The account creation request containing optional initial balance. + * @return Success message with the new account ID. + */ + @Operation( + summary = "Create a new Hiero account", + description = "Creates a new Hiero account with an optional initial balance.") + @PostMapping + public AccountResponse createAccount( + @RequestBody(required = false) final AccountCreateRequest request) { + try { + final Hbar initialBalance = + (request != null && request.initialBalance() != null) + ? Hbar.from(request.initialBalance()) + : Hbar.ZERO; + + final Account account = accountClient.createAccount(initialBalance); + return new AccountResponse( + account.accountId().toString(), + account.publicKey().toString(), + account.privateKey().toString()); + } catch (final Exception e) { + throw new RuntimeException("Failed to create account", e); + } + } + + /** + * Updates an existing Hiero account. + * + * @param request The account update request containing new key or memo. + * @return Success message. + */ + @Operation( + summary = "Update an existing Hiero account", + description = "Updates an account's metadata such as keys or memo.") + @PutMapping + public String updateAccount(@RequestBody final AccountUpdateRequest request) { + try { + if (request.accountId() == null || request.privateKey() == null) { + throw new IllegalArgumentException( + "Missing required fields: accountId and privateKey are mandatory."); + } + final AccountId accountId = AccountId.fromString(request.accountId().trim()); + final PrivateKey currentKey = PrivateKey.fromString(request.privateKey().trim()); + final Account account = Account.of(accountId, currentKey); + + if (request.newPrivateKey() != null && request.memo() != null) { + accountClient.updateAccount( + account, PrivateKey.fromString(request.newPrivateKey().trim()), request.memo()); + } else if (request.newPrivateKey() != null) { + accountClient.updateAccountKey( + account, PrivateKey.fromString(request.newPrivateKey().trim())); + } else if (request.memo() != null) { + accountClient.updateAccountMemo(account, request.memo()); + } + + return "Account " + request.accountId() + " updated successfully!"; + } catch (final Exception e) { + throw new RuntimeException("Failed to update account", e); + } + } + + /** + * Deletes a Hiero account. + * + * @param request The account deletion request. + */ + @Operation( + summary = "Delete a Hiero account", + description = + "Deletes an account and optionally transfers remaining balance to another account.") + @DeleteMapping + public String deleteAccount(@RequestBody final AccountDeleteRequest request) { + try { + if (request.accountId() == null || request.privateKey() == null) { + throw new IllegalArgumentException( + "Missing required fields: accountId and privateKey are mandatory."); + } + final AccountId accountId = AccountId.fromString(request.accountId().trim()); + final PrivateKey privateKey = PrivateKey.fromString(request.privateKey().trim()); + final Account account = Account.of(accountId, privateKey); + + if (request.transferToAccountId() != null) { + final AccountId transferTo = AccountId.fromString(request.transferToAccountId().trim()); + final Account transferTarget = Account.of(transferTo, PrivateKey.generateED25519()); + accountClient.deleteAccount(account, transferTarget); + } else { + accountClient.deleteAccount(account); + } + + return "Account " + request.accountId() + " deleted successfully!"; + } catch (final Exception e) { + throw new RuntimeException("Failed to delete account", e); + } + } + + /** + * Retrieves the balance of a Hiero account. + * + * @param accountId The ID of the account to query. + * @return The balance in Hbar. + */ + @Operation( + summary = "Get account balance", + description = "Retrieves the current balance of a Hiero account in Hbar.") + @GetMapping("/balance/{accountId}") + public String getBalance(@PathVariable("accountId") final String accountId) { + try { + final Hbar balance = accountClient.getAccountBalance(accountId.trim()); + return balance.toString(); + } catch (final Exception e) { + throw new RuntimeException("Failed to retrieve balance for account " + accountId, e); + } + } + + /** + * Retrieves detailed information about a Hiero account from the mirror node. + * + * @param accountId The ID of the account to query. + * @return The AccountInfo object. + */ + @Operation( + summary = "Get account information", + description = "Retrieves detailed account information from the Hiero mirror node.") + @GetMapping("/info/{accountId}") + public AccountInfo getInfo(@PathVariable("accountId") final String accountId) { + try { + final String trimmedAccountId = accountId.trim(); + return accountRepository + .findById(trimmedAccountId) + .orElseThrow(() -> new RuntimeException("Account not found: " + trimmedAccountId)); + } catch (final Exception e) { + throw new RuntimeException("Failed to retrieve info for account " + accountId, e); + } + } +} diff --git a/hiero-enterprise-spring-sample/src/main/java/org/hiero/spring/sample/controller/BlockController.java b/hiero-enterprise-spring-sample/src/main/java/org/hiero/spring/sample/controller/BlockController.java new file mode 100644 index 00000000..5bbced33 --- /dev/null +++ b/hiero-enterprise-spring-sample/src/main/java/org/hiero/spring/sample/controller/BlockController.java @@ -0,0 +1,66 @@ +package org.hiero.spring.sample.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.Objects; +import org.hiero.base.data.Block; +import org.hiero.base.data.Page; +import org.hiero.base.mirrornode.BlockRepository; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * REST controller for Hiero block operations. This controller provides endpoints for querying block + * information from the mirror node. + */ +@Tag(name = "Blocks", description = "Operations related to Hiero network blocks (Mirror Node)") +@RestController +@RequestMapping("/api/v1/hiero/blocks") +public class BlockController { + + private final BlockRepository blockRepository; + + public BlockController(final BlockRepository blockRepository) { + this.blockRepository = + Objects.requireNonNull(blockRepository, "blockRepository must not be null"); + } + + /** + * Retrieves a paginated list of all blocks. + * + * @return A page of blocks. + */ + @Operation( + summary = "Get all blocks", + description = "Retrieves a paginated list of blocks from the Hiero mirror node.") + @GetMapping + public Page getBlocks() { + try { + return blockRepository.findAll(); + } catch (final Exception e) { + throw new RuntimeException("Failed to query blocks", e); + } + } + + /** + * Retrieves a specific block by its number. + * + * @param number The block number. + * @return The block details. + */ + @Operation( + summary = "Get block by number", + description = "Retrieves details of a specific block by its block number.") + @GetMapping("/{number}") + public Block getBlockByNumber(@PathVariable("number") final long number) { + try { + return blockRepository + .findByNumber(number) + .orElseThrow(() -> new RuntimeException("Block not found: " + number)); + } catch (final Exception e) { + throw new RuntimeException("Failed to query block by number: " + number, e); + } + } +} diff --git a/hiero-enterprise-spring-sample/src/main/java/org/hiero/spring/sample/controller/FileController.java b/hiero-enterprise-spring-sample/src/main/java/org/hiero/spring/sample/controller/FileController.java new file mode 100644 index 00000000..4e6f8ad0 --- /dev/null +++ b/hiero-enterprise-spring-sample/src/main/java/org/hiero/spring/sample/controller/FileController.java @@ -0,0 +1,113 @@ +package org.hiero.spring.sample.controller; + +import com.hedera.hashgraph.sdk.FileId; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.Base64; +import java.util.Objects; +import org.hiero.base.FileClient; +import org.hiero.spring.sample.dto.file.FileCreateRequest; +import org.hiero.spring.sample.dto.file.FileUpdateRequest; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.util.HtmlUtils; + +/** + * REST controller for Hiero file operations. Provides endpoints for creating, deleting, updating + * and querying files on the network. + */ +@Tag(name = "Files", description = "Operations related to Hiero File Service (HFS)") +@RestController +@RequestMapping("/api/v1/hiero/files") +public class FileController { + + private final FileClient fileClient; + + public FileController(final FileClient fileClient) { + this.fileClient = Objects.requireNonNull(fileClient, "fileClient must not be null"); + } + + /** Creates a new file. */ + @Operation( + summary = "Create a new file", + description = "Creates a new file on the Hiero network with the provided content.") + @PostMapping + public String createFile(@RequestBody final FileCreateRequest request) { + try { + final FileId fileId; + if (request.expirationTime() != null) { + fileId = fileClient.createFile(request.getDecodedContent(), request.getExpirationInstant()); + } else { + fileId = fileClient.createFile(request.getDecodedContent()); + } + return "File " + fileId + " created successfully!"; + } catch (final Exception e) { + throw new RuntimeException("Failed to create file", e); + } + } + + /** Deletes a file. */ + @Operation( + summary = "Delete a file", + description = "Deletes an existing file from the Hiero network.") + @DeleteMapping("/{fileId}") + public String deleteFile(@PathVariable("fileId") final String fileId) { + try { + fileClient.deleteFile(fileId.trim()); + return "File " + HtmlUtils.htmlEscape(fileId) + " deleted successfully!"; + } catch (final Exception e) { + throw new RuntimeException("Failed to delete file: " + fileId, e); + } + } + + /** Updates an existing file (Content and/or Expiration Time). */ + @Operation( + summary = "Update a file", + description = "Updates the content or expiration time of an existing file.") + @PostMapping("/{fileId}") + public String updateFile( + @PathVariable("fileId") final String fileId, @RequestBody final FileUpdateRequest request) { + try { + final FileId fid = FileId.fromString(fileId.trim()); + if (request.content() != null) { + fileClient.updateFile(fid, request.getDecodedContent()); + } + if (request.expirationTime() != null) { + fileClient.updateExpirationTime(fid, request.getExpirationInstant()); + } + return "File " + HtmlUtils.htmlEscape(fileId) + " updated successfully!"; + } catch (final Exception e) { + throw new RuntimeException("Failed to update file: " + fileId, e); + } + } + + /** Retrieves the content of a file (returned as Base64 string). */ + @Operation( + summary = "Get file content", + description = "Retrieves the byte content of a file, encoded as a Base64 string.") + @GetMapping("/{fileId}/content") + public String getFileContent(@PathVariable("fileId") final String fileId) { + try { + final byte[] content = fileClient.readFile(fileId.trim()); + return Base64.getEncoder().encodeToString(content); + } catch (final Exception e) { + throw new RuntimeException("Failed to read file content: " + fileId, e); + } + } + + /** Retrieves the size of a file in bytes. */ + @Operation(summary = "Get file size", description = "Retrieves the size of a file in bytes.") + @GetMapping("/{fileId}/size") + public Integer getFileSize(@PathVariable("fileId") final String fileId) { + try { + return fileClient.getSize(FileId.fromString(fileId.trim())); + } catch (final Exception e) { + throw new RuntimeException("Failed to get file size: " + fileId, e); + } + } +} diff --git a/hiero-enterprise-spring-sample/src/main/java/org/hiero/spring/sample/controller/NetworkController.java b/hiero-enterprise-spring-sample/src/main/java/org/hiero/spring/sample/controller/NetworkController.java new file mode 100644 index 00000000..324a350e --- /dev/null +++ b/hiero-enterprise-spring-sample/src/main/java/org/hiero/spring/sample/controller/NetworkController.java @@ -0,0 +1,94 @@ +package org.hiero.spring.sample.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.List; +import java.util.Objects; +import org.hiero.base.mirrornode.NetworkRepository; +import org.hiero.spring.sample.dto.network.ExchangeRatesResponse; +import org.hiero.spring.sample.dto.network.NetworkFeeResponse; +import org.hiero.spring.sample.dto.network.NetworkStakeResponse; +import org.hiero.spring.sample.dto.network.NetworkSuppliesResponse; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * REST controller for Hiero network operations. Provides endpoints for querying network-wide + * information like exchange rates, fees, and staking. + */ +@Tag( + name = "Network", + description = "Operations related to Hiero network status (rates, fees, staking)") +@RestController +@RequestMapping("/api/v1/hiero/network") +public class NetworkController { + + private final NetworkRepository networkRepository; + + public NetworkController(final NetworkRepository networkRepository) { + this.networkRepository = + Objects.requireNonNull(networkRepository, "networkRepository must not be null"); + } + + /** Retrieves the current and next exchange rates. */ + @Operation( + summary = "Get exchange rates", + description = "Retrieves the current and next Hbar-to-USD exchange rates.") + @GetMapping("/exchange-rate") + public ExchangeRatesResponse getExchangeRates() { + try { + return networkRepository + .exchangeRates() + .map(ExchangeRatesResponse::fromDomain) + .orElseThrow(() -> new RuntimeException("Exchange rates not available")); + } catch (final Exception e) { + throw new RuntimeException("Failed to query exchange rates", e); + } + } + + /** Retrieves the network fees. */ + @Operation( + summary = "Get network fees", + description = "Retrieves a list of Hiero network transaction fees.") + @GetMapping("/fee") + public List getFees() { + try { + return networkRepository.fees().stream().map(NetworkFeeResponse::fromDomain).toList(); + } catch (final Exception e) { + throw new RuntimeException("Failed to query network fees", e); + } + } + + /** Retrieves network staking information. */ + @Operation( + summary = "Get network staking info", + description = "Retrieves current staking parameters and status for the Hiero network.") + @GetMapping("/stake") + public NetworkStakeResponse getStake() { + try { + return networkRepository + .stake() + .map(NetworkStakeResponse::fromDomain) + .orElseThrow(() -> new RuntimeException("Network stake info not available")); + } catch (final Exception e) { + throw new RuntimeException("Failed to query network stake", e); + } + } + + /** Retrieves network supply information. */ + @Operation( + summary = "Get network supplies", + description = "Retrieves the total and circulating supply of Hbar.") + @GetMapping("/supplies") + public NetworkSuppliesResponse getSupplies() { + try { + return networkRepository + .supplies() + .map(NetworkSuppliesResponse::fromDomain) + .orElseThrow(() -> new RuntimeException("Network supply info not available")); + } catch (final Exception e) { + throw new RuntimeException("Failed to query network supplies", e); + } + } +} diff --git a/hiero-enterprise-spring-sample/src/main/java/org/hiero/spring/sample/controller/NftController.java b/hiero-enterprise-spring-sample/src/main/java/org/hiero/spring/sample/controller/NftController.java new file mode 100644 index 00000000..b4947c9e --- /dev/null +++ b/hiero-enterprise-spring-sample/src/main/java/org/hiero/spring/sample/controller/NftController.java @@ -0,0 +1,189 @@ +package org.hiero.spring.sample.controller; + +import com.hedera.hashgraph.sdk.AccountId; +import com.hedera.hashgraph.sdk.PrivateKey; +import com.hedera.hashgraph.sdk.TokenId; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.nio.charset.StandardCharsets; +import java.util.Objects; +import org.hiero.base.NftClient; +import org.hiero.base.data.Account; +import org.hiero.base.data.Nft; +import org.hiero.base.data.TokenInfo; +import org.hiero.base.mirrornode.NftRepository; +import org.hiero.base.mirrornode.TokenRepository; +import org.hiero.spring.sample.dto.nft.NftAssociateRequest; +import org.hiero.spring.sample.dto.nft.NftCreateRequest; +import org.hiero.spring.sample.dto.nft.NftMintRequest; +import org.hiero.spring.sample.dto.nft.NftTransferRequest; +import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.util.HtmlUtils; + +/** REST controller for Hiero NFT operations. */ +@Tag( + name = "Non-Fungible Tokens", + description = "Operations related to Hiero Non-Fungible Token Service (NFTs)") +@RestController +@CrossOrigin +@RequestMapping("/api/v1/hiero/nfts") +public class NftController { + + private final NftClient nftClient; + private final NftRepository nftRepository; + private final TokenRepository tokenRepository; + + public NftController( + final NftClient nftClient, + final NftRepository nftRepository, + final TokenRepository tokenRepository) { + this.nftClient = Objects.requireNonNull(nftClient, "nftClient must not be null"); + this.nftRepository = Objects.requireNonNull(nftRepository, "nftRepository must not be null"); + this.tokenRepository = + Objects.requireNonNull(tokenRepository, "tokenRepository must not be null"); + } + + /** Creates a new NFT type. */ + @Operation(summary = "Create an NFT type", description = "Creates a new HTS NFT collection.") + @PostMapping + public String createNft(@RequestBody final NftCreateRequest request) { + try { + if (request.name() == null || request.symbol() == null) { + throw new IllegalArgumentException( + "Missing required fields: name and symbol are mandatory."); + } + final TokenId tokenId = nftClient.createNftType(request.name(), request.symbol()); + return "NFT Type " + tokenId + " created successfully!"; + } catch (final Exception e) { + throw new RuntimeException("Failed to create NFT type", e); + } + } + + /** Retrieves detailed information about an NFT type. */ + @Operation( + summary = "Get NFT type information", + description = "Retrieves detailed information about an NFT collection from the mirror node.") + @GetMapping("/{nftId}") + public TokenInfo getNftById(@PathVariable("nftId") final String nftId) { + try { + final String trimmedNftId = nftId.trim(); + return tokenRepository + .findById(trimmedNftId) + .orElseThrow(() -> new RuntimeException("NFT Type not found: " + trimmedNftId)); + } catch (final Exception e) { + throw new RuntimeException("Failed to retrieve NFT info for " + nftId, e); + } + } + + /** Mints a new NFT instance. */ + @Operation( + summary = "Mint an NFT", + description = "Mints a new serial number for an existing NFT collection.") + @PostMapping("/{nftId}/mint") + public String mintNft( + @PathVariable("nftId") final String nftId, @RequestBody final NftMintRequest request) { + try { + if (request.metadata() == null) { + throw new IllegalArgumentException("Missing required field: metadata is mandatory."); + } + final String trimmedNftId = nftId.trim(); + final byte[] metadata = request.metadata().getBytes(StandardCharsets.UTF_8); + final long serialNumber = nftClient.mintNft(trimmedNftId, metadata); + return "Minted NFT instance of " + + HtmlUtils.htmlEscape(nftId) + + " with serial number " + + serialNumber; + } catch (final Exception e) { + throw new RuntimeException("Failed to mint NFT for " + nftId, e); + } + } + + /** Retrieves a specific NFT instance by ID and serial number. */ + @Operation( + summary = "Get NFT instance", + description = + "Retrieves information about a specific NFT instance (serial number) from the mirror node.") + @GetMapping("/{nftId}/serial/{serial}") + public Nft getNftBySerial( + @PathVariable("nftId") final String nftId, @PathVariable("serial") final long serial) { + try { + final String trimmedNftId = nftId.trim(); + return nftRepository + .findByTypeAndSerial(trimmedNftId, serial) + .orElseThrow( + () -> new RuntimeException("NFT not found: " + trimmedNftId + " Serial: " + serial)); + } catch (final Exception e) { + throw new RuntimeException("Failed to retrieve NFT instance", e); + } + } + + /** Transfers an NFT instance between accounts. */ + @Operation( + summary = "Transfer an NFT", + description = "Transfers a specific NFT serial number between two Hiero accounts.") + @PostMapping("/{nftId}/transfer") + public String transferNft( + @PathVariable("nftId") final String nftId, @RequestBody final NftTransferRequest request) { + try { + if (request.fromAccountId() == null + || request.fromPrivateKey() == null + || request.toAccountId() == null) { + throw new IllegalArgumentException( + "Missing required fields: fromAccountId, fromPrivateKey, and toAccountId are mandatory."); + } + final TokenId tokenId = TokenId.fromString(nftId.trim()); + final Account fromAccount = + Account.of( + AccountId.fromString(request.fromAccountId().trim()), + PrivateKey.fromString(request.fromPrivateKey().trim())); + final AccountId toAccountId = AccountId.fromString(request.toAccountId().trim()); + + nftClient.transferNft(tokenId, request.serialNumber(), fromAccount, toAccountId); + return "Transferred NFT " + + HtmlUtils.htmlEscape(nftId) + + " (Serial: " + + request.serialNumber() + + ") to " + + HtmlUtils.htmlEscape(request.toAccountId()); + } catch (final Exception e) { + throw new RuntimeException("Failed to transfer NFT", e); + } + } + + /** Associates an account with a Hiero NFT collection. */ + @Operation( + summary = "Associate account with NFT", + description = + "Explicitly associates a Hiero account with an NFT collection. Required before receiving NFTs.") + @PostMapping("/{nftId}/associate") + public String associateNft( + @PathVariable("nftId") final String nftId, @RequestBody final NftAssociateRequest request) { + try { + if (request.accountId() == null || request.privateKey() == null) { + throw new IllegalArgumentException( + "Missing required fields: accountId and privateKey are mandatory."); + } + final TokenId id = TokenId.fromString(nftId.trim()); + final Account account = + Account.of( + AccountId.fromString(request.accountId().trim()), + PrivateKey.fromString(request.privateKey().trim())); + + nftClient.associateNft(id, account); + return "Account " + + HtmlUtils.htmlEscape(request.accountId()) + + " associated with NFT collection " + + HtmlUtils.htmlEscape(nftId) + + " successfully!"; + } catch (final Exception e) { + throw new RuntimeException( + "Failed to associate account " + request.accountId() + " with NFT " + nftId, e); + } + } +} diff --git a/hiero-enterprise-spring-sample/src/main/java/org/hiero/spring/sample/controller/TokenController.java b/hiero-enterprise-spring-sample/src/main/java/org/hiero/spring/sample/controller/TokenController.java new file mode 100644 index 00000000..907add59 --- /dev/null +++ b/hiero-enterprise-spring-sample/src/main/java/org/hiero/spring/sample/controller/TokenController.java @@ -0,0 +1,163 @@ +package org.hiero.spring.sample.controller; + +import com.hedera.hashgraph.sdk.AccountId; +import com.hedera.hashgraph.sdk.PrivateKey; +import com.hedera.hashgraph.sdk.TokenId; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.Objects; +import org.hiero.base.FungibleTokenClient; +import org.hiero.base.data.Account; +import org.hiero.base.data.TokenInfo; +import org.hiero.base.mirrornode.TokenRepository; +import org.hiero.spring.sample.dto.token.TokenAssociateRequest; +import org.hiero.spring.sample.dto.token.TokenCreateRequest; +import org.hiero.spring.sample.dto.token.TokenMintRequest; +import org.hiero.spring.sample.dto.token.TokenTransferRequest; +import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.util.HtmlUtils; + +@Tag(name = "Fungible Tokens", description = "Operations related to Hiero fungible tokens (HTS)") +@RestController +@CrossOrigin +@RequestMapping("/api/v1/hiero/fungible-token") +public class TokenController { + + private final FungibleTokenClient tokenClient; + private final TokenRepository tokenRepository; + + public TokenController( + final FungibleTokenClient tokenClient, final TokenRepository tokenRepository) { + this.tokenClient = Objects.requireNonNull(tokenClient, "tokenClient must not be null"); + this.tokenRepository = + Objects.requireNonNull(tokenRepository, "tokenRepository must not be null"); + } + + /** Creates a new Hiero fungible token. */ + @Operation( + summary = "Create a new fungible token", + description = "Creates a new Hiero fungible token (HTS).") + @PostMapping + public String createToken(@RequestBody final TokenCreateRequest request) { + try { + if (request.name() == null || request.symbol() == null) { + throw new IllegalArgumentException( + "Missing required fields: name and symbol are mandatory."); + } + final TokenId tokenId = tokenClient.createToken(request.name(), request.symbol()); + return "Token " + tokenId + " created successfully!"; + } catch (final Exception e) { + throw new RuntimeException("Failed to create token", e); + } + } + + /** Retrieves detailed information about a Hiero fungible token. */ + @Operation( + summary = "Get token information", + description = "Retrieves detailed information about a fungible token from the mirror node.") + @GetMapping("/{tokenId}") + public TokenInfo getToken(@PathVariable("tokenId") final String tokenId) { + try { + final String trimmedTokenId = tokenId.trim(); + return tokenRepository + .findById(trimmedTokenId) + .orElseThrow(() -> new RuntimeException("Token not found: " + trimmedTokenId)); + } catch (final Exception e) { + throw new RuntimeException("Failed to retrieve token info for " + tokenId, e); + } + } + + /** Mints more of a Hiero fungible token. */ + @Operation( + summary = "Mint tokens", + description = "Mints additional units of a fungible token to the treasury account.") + @PostMapping("/{tokenId}/mint") + public String mintToken( + @PathVariable("tokenId") final String tokenId, @RequestBody final TokenMintRequest request) { + try { + final String trimmedTokenId = tokenId.trim(); + final long newTotalSupply; + if (request.supplyKey() != null) { + newTotalSupply = + tokenClient.mintToken(trimmedTokenId, request.supplyKey(), request.amount()); + } else { + newTotalSupply = tokenClient.mintToken(trimmedTokenId, request.amount()); + } + return "Minted " + request.amount() + " units. New total supply: " + newTotalSupply; + } catch (final Exception e) { + throw new RuntimeException("Failed to mint token " + tokenId, e); + } + } + + /** Transfers Hiero fungible tokens between accounts. */ + @Operation( + summary = "Transfer tokens", + description = "Transfers fungible tokens between two Hiero accounts.") + @PostMapping("/{tokenId}/transfer") + public String transferToken( + @PathVariable("tokenId") final String tokenId, + @RequestBody final TokenTransferRequest request) { + try { + if (request.fromAccountId() == null + || request.fromPrivateKey() == null + || request.toAccountId() == null) { + throw new IllegalArgumentException( + "Missing required fields: fromAccountId, fromPrivateKey, and toAccountId are mandatory."); + } + final TokenId id = TokenId.fromString(tokenId.trim()); + final Account fromAccount = + Account.of( + AccountId.fromString(request.fromAccountId().trim()), + PrivateKey.fromString(request.fromPrivateKey().trim())); + final AccountId toAccount = AccountId.fromString(request.toAccountId().trim()); + + tokenClient.transferToken(id, fromAccount, toAccount, request.amount()); + return "Transferred " + + request.amount() + + " tokens from " + + HtmlUtils.htmlEscape(request.fromAccountId()) + + " to " + + HtmlUtils.htmlEscape(request.toAccountId()); + } catch (final Exception e) { + throw new RuntimeException("Failed to transfer token " + tokenId, e); + } + } + + /** Associates an account with a Hiero fungible token. */ + @Operation( + summary = "Associate account with token", + description = + "Explicitly associates a Hiero account with a fungible token. Required before receiving tokens.") + @PostMapping("/{tokenId}/associate") + public String associateToken( + @PathVariable("tokenId") final String tokenId, + @RequestBody final TokenAssociateRequest request) { + try { + if (request.accountId() == null || request.privateKey() == null) { + throw new IllegalArgumentException( + "Missing required fields: accountId and privateKey are mandatory."); + } + final TokenId id = TokenId.fromString(tokenId.trim()); + final Account account = + Account.of( + AccountId.fromString(request.accountId().trim()), + PrivateKey.fromString(request.privateKey().trim())); + + tokenClient.associateToken(id, account); + return "Account " + + HtmlUtils.htmlEscape(request.accountId()) + + " associated with token " + + HtmlUtils.htmlEscape(tokenId) + + " successfully!"; + } catch (final Exception e) { + throw new RuntimeException( + "Failed to associate account " + request.accountId() + " with token " + tokenId, e); + } + } +} diff --git a/hiero-enterprise-spring-sample/src/main/java/org/hiero/spring/sample/controller/TopicController.java b/hiero-enterprise-spring-sample/src/main/java/org/hiero/spring/sample/controller/TopicController.java new file mode 100644 index 00000000..e067a7e8 --- /dev/null +++ b/hiero-enterprise-spring-sample/src/main/java/org/hiero/spring/sample/controller/TopicController.java @@ -0,0 +1,196 @@ +package org.hiero.spring.sample.controller; + +import com.hedera.hashgraph.sdk.PrivateKey; +import com.hedera.hashgraph.sdk.TopicId; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.Objects; +import org.hiero.base.TopicClient; +import org.hiero.base.data.Page; +import org.hiero.base.data.Topic; +import org.hiero.base.data.TopicMessage; +import org.hiero.base.mirrornode.TopicRepository; +import org.hiero.spring.sample.dto.topic.TopicCreateRequest; +import org.hiero.spring.sample.dto.topic.TopicMessageRequest; +import org.hiero.spring.sample.dto.topic.TopicUpdateRequest; +import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.util.HtmlUtils; + +/** REST controller for Hiero Consensus Service (Topic) operations. */ +@Tag(name = "Consensus Topics", description = "Operations related to Hiero Consensus Service (HCS)") +@RestController +@RequestMapping("/api/v1/hiero/topics") +@CrossOrigin(origins = "*") +public class TopicController { + + private final TopicClient topicClient; + private final TopicRepository topicRepository; + + public TopicController(final TopicClient topicClient, final TopicRepository topicRepository) { + this.topicClient = Objects.requireNonNull(topicClient, "topicClient must not be null"); + this.topicRepository = + Objects.requireNonNull(topicRepository, "topicRepository must not be null"); + } + + /** Creates a new topic. */ + @Operation( + summary = "Create a new topic", + description = "Creates a new public or private HCS topic.") + @PostMapping + public String createTopic(@RequestBody final TopicCreateRequest request) { + try { + final TopicId topicId; + final PrivateKey adminKey = + request.adminKey() != null ? PrivateKey.fromString(request.adminKey()) : null; + final PrivateKey submitKey = + request.submitKey() != null ? PrivateKey.fromString(request.submitKey()) : null; + final String memo = request.memo() != null ? request.memo() : ""; + + if (submitKey != null) { + if (adminKey != null) { + topicId = topicClient.createPrivateTopic(adminKey, submitKey, memo); + } else { + topicId = topicClient.createPrivateTopic(submitKey, memo); + } + } else { + if (adminKey != null) { + topicId = topicClient.createTopic(adminKey, memo); + } else { + topicId = topicClient.createTopic(memo); + } + } + return "Topic " + topicId.toString() + " created successfully!"; + } catch (final Exception e) { + throw new RuntimeException("Failed to create topic", e); + } + } + + /** Updates an existing topic. */ + @Operation( + summary = "Update a topic", + description = "Updates an existing HCS topic's memo or keys.") + @PutMapping + public String updateTopic(@RequestBody final TopicUpdateRequest request) { + try { + if (request.topicId() == null) { + throw new IllegalArgumentException("Missing required field: topicId is mandatory."); + } + final TopicId topicId = TopicId.fromString(request.topicId().trim()); + final String memo = request.memo() != null ? request.memo() : ""; + + if (request.adminKey() != null + && request.updatedAdminKey() != null + && request.submitKey() != null) { + topicClient.updateTopic( + topicId, + PrivateKey.fromString(request.adminKey().trim()), + PrivateKey.fromString(request.updatedAdminKey().trim()), + PrivateKey.fromString(request.submitKey().trim()), + memo); + } else if (request.adminKey() != null) { + topicClient.updateTopic(topicId, PrivateKey.fromString(request.adminKey().trim()), memo); + } else { + topicClient.updateTopic(topicId, memo); + } + return "Topic " + topicId.toString() + " updated successfully!"; + } catch (final Exception e) { + throw new RuntimeException("Failed to update topic", e); + } + } + + /** Deletes a topic. */ + @Operation(summary = "Delete a topic", description = "Deletes an existing HCS topic.") + @DeleteMapping("/{topicId}") + public String deleteTopic(@PathVariable("topicId") final String topicId) { + try { + topicClient.deleteTopic(topicId.trim()); + return "Topic " + HtmlUtils.htmlEscape(topicId) + " deleted successfully!"; + } catch (final Exception e) { + throw new RuntimeException("Failed to delete topic", e); + } + } + + /** Submits a message to a topic. */ + @Operation( + summary = "Submit a message", + description = "Submits a message to a specific HCS topic.") + @PostMapping("/message") + public String submitMessage(@RequestBody final TopicMessageRequest request) { + try { + if (request.topicId() == null || request.message() == null) { + throw new IllegalArgumentException( + "Missing required fields: topicId and message are mandatory."); + } + final String trimmedTopicId = request.topicId().trim(); + if (request.submitKey() != null) { + topicClient.submitMessage(trimmedTopicId, request.submitKey().trim(), request.message()); + } else { + topicClient.submitMessage(trimmedTopicId, request.message()); + } + return "Message submitted successfully to topic " + HtmlUtils.htmlEscape(request.topicId()); + } catch (final Exception e) { + throw new RuntimeException("Failed to submit message", e); + } + } + + /** Retrieves info for a specific topic. */ + @Operation( + summary = "Get topic information", + description = "Retrieves detailed information about an HCS topic from the mirror node.") + @GetMapping("/{topicId}/info") + public Topic getTopicInfo(@PathVariable("topicId") final String topicId) { + try { + final String trimmedTopicId = topicId.trim(); + return topicRepository + .findTopicById(trimmedTopicId) + .orElseThrow(() -> new RuntimeException("Topic not found: " + trimmedTopicId)); + } catch (final Exception e) { + throw new RuntimeException("Failed to retrieve topic info", e); + } + } + + /** Retrieves messages for a specific topic. */ + @Operation( + summary = "Get topic messages", + description = "Retrieves a list of messages submitted to a specific HCS topic.") + @GetMapping("/{topicId}/message") + public Page getTopicMessages(@PathVariable("topicId") final String topicId) { + try { + return topicRepository.getMessages(topicId.trim()); + } catch (final Exception e) { + throw new RuntimeException("Failed to retrieve topic messages", e); + } + } + + /** Retrieves a specific message by its sequence number. */ + @Operation( + summary = "Get topic message by sequence", + description = "Retrieves a specific message from an HCS topic by its sequence number.") + @GetMapping("/{topicId}/message/{sequenceNumber}") + public TopicMessage getTopicMessageBySequenceNumber( + @PathVariable("topicId") final String topicId, + @PathVariable("sequenceNumber") final long sequenceNumber) { + try { + final String trimmedTopicId = topicId.trim(); + return topicRepository + .getMessageBySequenceNumber(trimmedTopicId, sequenceNumber) + .orElseThrow( + () -> + new RuntimeException( + "Message not found for Topic: " + + trimmedTopicId + + " Sequence: " + + sequenceNumber)); + } catch (final Exception e) { + throw new RuntimeException("Failed to retrieve topic message", e); + } + } +} diff --git a/hiero-enterprise-spring-sample/src/main/java/org/hiero/spring/sample/dto/account/AccountCreateRequest.java b/hiero-enterprise-spring-sample/src/main/java/org/hiero/spring/sample/dto/account/AccountCreateRequest.java new file mode 100644 index 00000000..a3910002 --- /dev/null +++ b/hiero-enterprise-spring-sample/src/main/java/org/hiero/spring/sample/dto/account/AccountCreateRequest.java @@ -0,0 +1,11 @@ +package org.hiero.spring.sample.dto.account; + +import io.swagger.v3.oas.annotations.media.Schema; + +/** Request to create a new Hiero account. */ +@Schema( + name = "Account: Create Request", + description = "Request DTO for creating a new Hiero account.") +public record AccountCreateRequest( + @Schema(description = "The initial balance in Hbar (optional, defaults to 0).", example = "100") + Long initialBalance) {} diff --git a/hiero-enterprise-spring-sample/src/main/java/org/hiero/spring/sample/dto/account/AccountDeleteRequest.java b/hiero-enterprise-spring-sample/src/main/java/org/hiero/spring/sample/dto/account/AccountDeleteRequest.java new file mode 100644 index 00000000..788f81bb --- /dev/null +++ b/hiero-enterprise-spring-sample/src/main/java/org/hiero/spring/sample/dto/account/AccountDeleteRequest.java @@ -0,0 +1,23 @@ +package org.hiero.spring.sample.dto.account; + +import io.swagger.v3.oas.annotations.media.Schema; + +/** Request to delete a Hiero account. */ +@Schema(name = "Account: Delete Request", description = "Request DTO for deleting a Hiero account.") +public record AccountDeleteRequest( + @Schema( + description = "The ID of the account to delete.", + example = "0.0.1234", + requiredMode = Schema.RequiredMode.REQUIRED) + String accountId, + @Schema( + description = "The private key of the account to delete.", + example = "302e020100300506032b657004220420...", + requiredMode = Schema.RequiredMode.REQUIRED) + String privateKey, + @Schema( + description = + "The ID of the account to transfer remaining funds to (optional, defaults to operator).", + example = "0.0.5678", + requiredMode = Schema.RequiredMode.NOT_REQUIRED) + String transferToAccountId) {} diff --git a/hiero-enterprise-spring-sample/src/main/java/org/hiero/spring/sample/dto/account/AccountResponse.java b/hiero-enterprise-spring-sample/src/main/java/org/hiero/spring/sample/dto/account/AccountResponse.java new file mode 100644 index 00000000..eb2c50a3 --- /dev/null +++ b/hiero-enterprise-spring-sample/src/main/java/org/hiero/spring/sample/dto/account/AccountResponse.java @@ -0,0 +1,16 @@ +package org.hiero.spring.sample.dto.account; + +import io.swagger.v3.oas.annotations.media.Schema; + +/** + * Response containing Hiero account details. + * + * @param accountId The ID of the account. + * @param publicKey The public key of the account. + * @param privateKey The private key of the account. + */ +@Schema(name = "Account: Response", description = "Response containing Hiero account details.") +public record AccountResponse( + @Schema(description = "The ID of the account.") String accountId, + @Schema(description = "The public key of the account.") String publicKey, + @Schema(description = "The private key of the account.") String privateKey) {} diff --git a/hiero-enterprise-spring-sample/src/main/java/org/hiero/spring/sample/dto/account/AccountUpdateRequest.java b/hiero-enterprise-spring-sample/src/main/java/org/hiero/spring/sample/dto/account/AccountUpdateRequest.java new file mode 100644 index 00000000..f51e7569 --- /dev/null +++ b/hiero-enterprise-spring-sample/src/main/java/org/hiero/spring/sample/dto/account/AccountUpdateRequest.java @@ -0,0 +1,29 @@ +package org.hiero.spring.sample.dto.account; + +import io.swagger.v3.oas.annotations.media.Schema; + +/** Request to update an existing Hiero account. */ +@Schema( + name = "Account: Update Request", + description = "Request DTO for updating an existing Hiero account.") +public record AccountUpdateRequest( + @Schema( + description = "The ID of the account to update.", + example = "0.0.1234", + requiredMode = Schema.RequiredMode.REQUIRED) + String accountId, + @Schema( + description = "The current private key of the account.", + example = "302e020100300506032b657004220420...", + requiredMode = Schema.RequiredMode.REQUIRED) + String privateKey, + @Schema( + description = "The new private key to set (optional).", + example = "302e020100300506032b657004220420...", + requiredMode = Schema.RequiredMode.NOT_REQUIRED) + String newPrivateKey, + @Schema( + description = "The new memo to set (optional).", + example = "Updated account memo", + requiredMode = Schema.RequiredMode.NOT_REQUIRED) + String memo) {} diff --git a/hiero-enterprise-spring-sample/src/main/java/org/hiero/spring/sample/dto/file/FileCreateRequest.java b/hiero-enterprise-spring-sample/src/main/java/org/hiero/spring/sample/dto/file/FileCreateRequest.java new file mode 100644 index 00000000..f9a34506 --- /dev/null +++ b/hiero-enterprise-spring-sample/src/main/java/org/hiero/spring/sample/dto/file/FileCreateRequest.java @@ -0,0 +1,24 @@ +package org.hiero.spring.sample.dto.file; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.time.Instant; + +/** Request DTO for creating a Hiero file. */ +@Schema( + name = "File: Create Request", + description = "Request DTO for creating a new file on the Hiero File Service (HFS).") +public record FileCreateRequest( + @Schema(description = "Base64 encoded content of the file.", example = "SGVsbG8gSGllcm8h") + String content, + @Schema( + description = "Optional expiration time in seconds since epoch.", + example = "1735689600") + Long expirationTime) { + public byte[] getDecodedContent() { + return java.util.Base64.getDecoder().decode(content); + } + + public Instant getExpirationInstant() { + return expirationTime != null ? Instant.ofEpochSecond(expirationTime) : null; + } +} diff --git a/hiero-enterprise-spring-sample/src/main/java/org/hiero/spring/sample/dto/file/FileUpdateRequest.java b/hiero-enterprise-spring-sample/src/main/java/org/hiero/spring/sample/dto/file/FileUpdateRequest.java new file mode 100644 index 00000000..7c990c18 --- /dev/null +++ b/hiero-enterprise-spring-sample/src/main/java/org/hiero/spring/sample/dto/file/FileUpdateRequest.java @@ -0,0 +1,26 @@ +package org.hiero.spring.sample.dto.file; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.time.Instant; + +/** Request DTO for updating a Hiero file. */ +@Schema( + name = "File: Update Request", + description = "Request DTO for updating the content or expiration of a Hiero file.") +public record FileUpdateRequest( + @Schema( + description = "Optional Base64 encoded content to update.", + example = "VXBkYXRlZCBjb250ZW50") + String content, + @Schema( + description = "Optional new expiration time in seconds since epoch.", + example = "1767225600") + Long expirationTime) { + public byte[] getDecodedContent() { + return content != null ? java.util.Base64.getDecoder().decode(content) : null; + } + + public Instant getExpirationInstant() { + return expirationTime != null ? Instant.ofEpochSecond(expirationTime) : null; + } +} diff --git a/hiero-enterprise-spring-sample/src/main/java/org/hiero/spring/sample/dto/network/ExchangeRatesResponse.java b/hiero-enterprise-spring-sample/src/main/java/org/hiero/spring/sample/dto/network/ExchangeRatesResponse.java new file mode 100644 index 00000000..f3723d8c --- /dev/null +++ b/hiero-enterprise-spring-sample/src/main/java/org/hiero/spring/sample/dto/network/ExchangeRatesResponse.java @@ -0,0 +1,25 @@ +package org.hiero.spring.sample.dto.network; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.time.Instant; +import org.hiero.base.data.ExchangeRates; + +/** Response DTO for Network Exchange Rates. */ +@Schema( + name = "Network: Exchange Rates", + description = "Response DTO containing current and next network exchange rates.") +public record ExchangeRatesResponse(RateInfo currentRate, RateInfo nextRate) { + public record RateInfo(int centEquivalent, int hbarEquivalent, Instant expirationTime) {} + + public static ExchangeRatesResponse fromDomain(ExchangeRates rates) { + return new ExchangeRatesResponse( + new RateInfo( + rates.currentRate().centEquivalent(), + rates.currentRate().hbarEquivalent(), + rates.currentRate().expirationTime()), + new RateInfo( + rates.nextRate().centEquivalent(), + rates.nextRate().hbarEquivalent(), + rates.nextRate().expirationTime())); + } +} diff --git a/hiero-enterprise-spring-sample/src/main/java/org/hiero/spring/sample/dto/network/NetworkFeeResponse.java b/hiero-enterprise-spring-sample/src/main/java/org/hiero/spring/sample/dto/network/NetworkFeeResponse.java new file mode 100644 index 00000000..488bbbe5 --- /dev/null +++ b/hiero-enterprise-spring-sample/src/main/java/org/hiero/spring/sample/dto/network/NetworkFeeResponse.java @@ -0,0 +1,16 @@ +package org.hiero.spring.sample.dto.network; + +import io.swagger.v3.oas.annotations.media.Schema; +import org.hiero.base.data.NetworkFee; + +/** Response DTO for Network Fees. */ +@Schema( + name = "Network: Fee", + description = "Response DTO containing network transaction fee information.") +public record NetworkFeeResponse( + @Schema(description = "The gas cost associated with the transaction.") long gas, + @Schema(description = "The type of transaction.") String transactionType) { + public static NetworkFeeResponse fromDomain(NetworkFee fee) { + return new NetworkFeeResponse(fee.gas(), fee.transactionType()); + } +} diff --git a/hiero-enterprise-spring-sample/src/main/java/org/hiero/spring/sample/dto/network/NetworkStakeResponse.java b/hiero-enterprise-spring-sample/src/main/java/org/hiero/spring/sample/dto/network/NetworkStakeResponse.java new file mode 100644 index 00000000..f97dab54 --- /dev/null +++ b/hiero-enterprise-spring-sample/src/main/java/org/hiero/spring/sample/dto/network/NetworkStakeResponse.java @@ -0,0 +1,40 @@ +package org.hiero.spring.sample.dto.network; + +import io.swagger.v3.oas.annotations.media.Schema; +import org.hiero.base.data.NetworkStake; + +/** Response DTO for Network Stake Information. */ +@Schema( + name = "Network: Stake", + description = "Response DTO containing network staking status and parameters.") +public record NetworkStakeResponse( + long maxStakeReward, + long maxStakeRewardPerHbar, + long maxTotalReward, + double nodeRewardFeeFraction, + long reservedStakingRewards, + long rewardBalanceThreshold, + long stakeTotal, + long stakingPeriodDuration, + long stakingPeriodsStored, + double stakingRewardFeeFraction, + long stakingRewardRate, + long stakingStartThreshold, + long unreservedStakingRewardBalance) { + public static NetworkStakeResponse fromDomain(NetworkStake stake) { + return new NetworkStakeResponse( + stake.maxStakeReward(), + stake.maxStakeRewardPerHbar(), + stake.maxTotalReward(), + stake.nodeRewardFeeFraction(), + stake.reservedStakingRewards(), + stake.rewardBalanceThreshold(), + stake.stakeTotal(), + stake.stakingPeriodDuration(), + stake.stakingPeriodsStored(), + stake.stakingRewardFeeFraction(), + stake.stakingRewardRate(), + stake.stakingStartThreshold(), + stake.unreservedStakingRewardBalance()); + } +} diff --git a/hiero-enterprise-spring-sample/src/main/java/org/hiero/spring/sample/dto/network/NetworkSuppliesResponse.java b/hiero-enterprise-spring-sample/src/main/java/org/hiero/spring/sample/dto/network/NetworkSuppliesResponse.java new file mode 100644 index 00000000..467d3f0b --- /dev/null +++ b/hiero-enterprise-spring-sample/src/main/java/org/hiero/spring/sample/dto/network/NetworkSuppliesResponse.java @@ -0,0 +1,16 @@ +package org.hiero.spring.sample.dto.network; + +import io.swagger.v3.oas.annotations.media.Schema; +import org.hiero.base.data.NetworkSupplies; + +/** Response DTO for Network Supplies. */ +@Schema( + name = "Network: Supplies", + description = "Response DTO containing Hbar supply information.") +public record NetworkSuppliesResponse( + @Schema(description = "The released supply of Hbar.") String releasedSupply, + @Schema(description = "The total supply of Hbar.") String totalSupply) { + public static NetworkSuppliesResponse fromDomain(NetworkSupplies supplies) { + return new NetworkSuppliesResponse(supplies.releasedSupply(), supplies.totalSupply()); + } +} diff --git a/hiero-enterprise-spring-sample/src/main/java/org/hiero/spring/sample/dto/nft/NftAssociateRequest.java b/hiero-enterprise-spring-sample/src/main/java/org/hiero/spring/sample/dto/nft/NftAssociateRequest.java new file mode 100644 index 00000000..dd233b82 --- /dev/null +++ b/hiero-enterprise-spring-sample/src/main/java/org/hiero/spring/sample/dto/nft/NftAssociateRequest.java @@ -0,0 +1,19 @@ +package org.hiero.spring.sample.dto.nft; + +import io.swagger.v3.oas.annotations.media.Schema; + +/** Request DTO for associating an account with an NFT collection. */ +@Schema( + name = "Non-Fungible Token: Associate Request", + description = "Request DTO for explicitly associating a Hiero account with an NFT collection.") +public record NftAssociateRequest( + @Schema( + description = "The ID of the account to associate.", + example = "0.0.1234", + requiredMode = Schema.RequiredMode.REQUIRED) + String accountId, + @Schema( + description = "The private key of the account to associate.", + example = "302e020100300506032b657004220420...", + requiredMode = Schema.RequiredMode.REQUIRED) + String privateKey) {} diff --git a/hiero-enterprise-spring-sample/src/main/java/org/hiero/spring/sample/dto/nft/NftCreateRequest.java b/hiero-enterprise-spring-sample/src/main/java/org/hiero/spring/sample/dto/nft/NftCreateRequest.java new file mode 100644 index 00000000..c48824f6 --- /dev/null +++ b/hiero-enterprise-spring-sample/src/main/java/org/hiero/spring/sample/dto/nft/NftCreateRequest.java @@ -0,0 +1,11 @@ +package org.hiero.spring.sample.dto.nft; + +import io.swagger.v3.oas.annotations.media.Schema; + +/** Request DTO for creating a new NFT type. */ +@Schema( + name = "Non-Fungible Token: Create Request", + description = "Request DTO for creating a new Hiero NFT collection.") +public record NftCreateRequest( + @Schema(description = "The name of the NFT collection.", example = "Hiero Heroes") String name, + @Schema(description = "The symbol of the NFT collection.", example = "HERO") String symbol) {} diff --git a/hiero-enterprise-spring-sample/src/main/java/org/hiero/spring/sample/dto/nft/NftMintRequest.java b/hiero-enterprise-spring-sample/src/main/java/org/hiero/spring/sample/dto/nft/NftMintRequest.java new file mode 100644 index 00000000..10bc2709 --- /dev/null +++ b/hiero-enterprise-spring-sample/src/main/java/org/hiero/spring/sample/dto/nft/NftMintRequest.java @@ -0,0 +1,13 @@ +package org.hiero.spring.sample.dto.nft; + +import io.swagger.v3.oas.annotations.media.Schema; + +/** Request DTO for minting a new NFT instance. */ +@Schema( + name = "Non-Fungible Token: Mint Request", + description = "Request DTO for minting a new NFT instance into a collection.") +public record NftMintRequest( + @Schema( + description = "The metadata for the NFT instance (e.g., IPFS CID).", + example = "ipfs://Qm...") + String metadata) {} diff --git a/hiero-enterprise-spring-sample/src/main/java/org/hiero/spring/sample/dto/nft/NftTransferRequest.java b/hiero-enterprise-spring-sample/src/main/java/org/hiero/spring/sample/dto/nft/NftTransferRequest.java new file mode 100644 index 00000000..d37b7477 --- /dev/null +++ b/hiero-enterprise-spring-sample/src/main/java/org/hiero/spring/sample/dto/nft/NftTransferRequest.java @@ -0,0 +1,29 @@ +package org.hiero.spring.sample.dto.nft; + +import io.swagger.v3.oas.annotations.media.Schema; + +/** Request DTO for transferring an NFT instance. */ +@Schema( + name = "Non-Fungible Token: Transfer Request", + description = "Request DTO for transferring a specific NFT serial number between accounts.") +public record NftTransferRequest( + @Schema( + description = "The ID of the account transferring the NFT.", + example = "0.0.1234", + requiredMode = Schema.RequiredMode.REQUIRED) + String fromAccountId, + @Schema( + description = "The private key of the account transferring the NFT.", + example = "302e020100300506032b657004220420...", + requiredMode = Schema.RequiredMode.REQUIRED) + String fromPrivateKey, + @Schema( + description = "The ID of the account receiving the NFT.", + example = "0.0.5678", + requiredMode = Schema.RequiredMode.REQUIRED) + String toAccountId, + @Schema( + description = "The serial number of the NFT instance to transfer.", + example = "1", + requiredMode = Schema.RequiredMode.REQUIRED) + long serialNumber) {} diff --git a/hiero-enterprise-spring-sample/src/main/java/org/hiero/spring/sample/dto/token/TokenAssociateRequest.java b/hiero-enterprise-spring-sample/src/main/java/org/hiero/spring/sample/dto/token/TokenAssociateRequest.java new file mode 100644 index 00000000..3fd92238 --- /dev/null +++ b/hiero-enterprise-spring-sample/src/main/java/org/hiero/spring/sample/dto/token/TokenAssociateRequest.java @@ -0,0 +1,19 @@ +package org.hiero.spring.sample.dto.token; + +import io.swagger.v3.oas.annotations.media.Schema; + +/** Data Transfer Object for token association requests. */ +@Schema( + name = "Fungible Token: Associate Request", + description = "Request DTO for explicitly associating a Hiero account with a fungible token.") +public record TokenAssociateRequest( + @Schema( + description = "The ID of the account to associate with the token", + example = "0.0.12345", + requiredMode = Schema.RequiredMode.REQUIRED) + String accountId, + @Schema( + description = "The private key of the account to sign the association", + example = "302e...", + requiredMode = Schema.RequiredMode.REQUIRED) + String privateKey) {} diff --git a/hiero-enterprise-spring-sample/src/main/java/org/hiero/spring/sample/dto/token/TokenCreateRequest.java b/hiero-enterprise-spring-sample/src/main/java/org/hiero/spring/sample/dto/token/TokenCreateRequest.java new file mode 100644 index 00000000..67c2482f --- /dev/null +++ b/hiero-enterprise-spring-sample/src/main/java/org/hiero/spring/sample/dto/token/TokenCreateRequest.java @@ -0,0 +1,19 @@ +package org.hiero.spring.sample.dto.token; + +import io.swagger.v3.oas.annotations.media.Schema; + +/** Request to create a new Hiero fungible token. */ +@Schema( + name = "Fungible Token: Create Request", + description = "Request DTO for creating a new Hiero fungible token.") +public record TokenCreateRequest( + @Schema(description = "The name of the token.", example = "Hiero Gold") String name, + @Schema(description = "The symbol of the token.", example = "GOLD") String symbol, + @Schema( + description = "The number of decimals for the token (optional, defaults to 0).", + example = "8") + Integer decimals, + @Schema( + description = "The initial supply of the token (optional, defaults to 0).", + example = "1000000") + Long initialSupply) {} diff --git a/hiero-enterprise-spring-sample/src/main/java/org/hiero/spring/sample/dto/token/TokenMintRequest.java b/hiero-enterprise-spring-sample/src/main/java/org/hiero/spring/sample/dto/token/TokenMintRequest.java new file mode 100644 index 00000000..f0c86aed --- /dev/null +++ b/hiero-enterprise-spring-sample/src/main/java/org/hiero/spring/sample/dto/token/TokenMintRequest.java @@ -0,0 +1,14 @@ +package org.hiero.spring.sample.dto.token; + +import io.swagger.v3.oas.annotations.media.Schema; + +/** Request to mint more of a Hiero fungible token. */ +@Schema( + name = "Fungible Token: Mint Request", + description = "Request DTO for minting additional units of a fungible token.") +public record TokenMintRequest( + @Schema(description = "The amount of tokens to mint.", example = "500000") long amount, + @Schema( + description = "The supply key of the token (required if the token has a supply key).", + example = "302e020100300506032b657004220420...") + String supplyKey) {} diff --git a/hiero-enterprise-spring-sample/src/main/java/org/hiero/spring/sample/dto/token/TokenTransferRequest.java b/hiero-enterprise-spring-sample/src/main/java/org/hiero/spring/sample/dto/token/TokenTransferRequest.java new file mode 100644 index 00000000..c4c0cc8f --- /dev/null +++ b/hiero-enterprise-spring-sample/src/main/java/org/hiero/spring/sample/dto/token/TokenTransferRequest.java @@ -0,0 +1,29 @@ +package org.hiero.spring.sample.dto.token; + +import io.swagger.v3.oas.annotations.media.Schema; + +/** Request to transfer Hiero fungible tokens between accounts. */ +@Schema( + name = "Fungible Token: Transfer Request", + description = "Request DTO for transferring units of a fungible token between accounts.") +public record TokenTransferRequest( + @Schema( + description = "The ID of the account to transfer tokens from.", + example = "0.0.1234", + requiredMode = Schema.RequiredMode.REQUIRED) + String fromAccountId, + @Schema( + description = "The private key of the sender account.", + example = "302e020100300506032b657004220420...", + requiredMode = Schema.RequiredMode.REQUIRED) + String fromPrivateKey, + @Schema( + description = "The ID of the account to transfer tokens to.", + example = "0.0.5678", + requiredMode = Schema.RequiredMode.REQUIRED) + String toAccountId, + @Schema( + description = "The amount of tokens to transfer.", + example = "1000", + requiredMode = Schema.RequiredMode.REQUIRED) + long amount) {} diff --git a/hiero-enterprise-spring-sample/src/main/java/org/hiero/spring/sample/dto/topic/TopicCreateRequest.java b/hiero-enterprise-spring-sample/src/main/java/org/hiero/spring/sample/dto/topic/TopicCreateRequest.java new file mode 100644 index 00000000..523ffcb8 --- /dev/null +++ b/hiero-enterprise-spring-sample/src/main/java/org/hiero/spring/sample/dto/topic/TopicCreateRequest.java @@ -0,0 +1,19 @@ +package org.hiero.spring.sample.dto.topic; + +import io.swagger.v3.oas.annotations.media.Schema; + +/** Request DTO for creating a new Consensus Topic. */ +@Schema( + name = "Consensus Topic: Create Request", + description = "Request DTO for creating a new Hiero Consensus Service (HCS) topic.") +public record TopicCreateRequest( + @Schema(description = "Optional memo for the topic.", example = "Project alerts topic") + String memo, + @Schema( + description = "Optional admin key for the topic.", + example = "302e020100300506032b657004220420...") + String adminKey, + @Schema( + description = "Optional submit key for the topic.", + example = "302e020100300506032b657004220420...") + String submitKey) {} diff --git a/hiero-enterprise-spring-sample/src/main/java/org/hiero/spring/sample/dto/topic/TopicMessageRequest.java b/hiero-enterprise-spring-sample/src/main/java/org/hiero/spring/sample/dto/topic/TopicMessageRequest.java new file mode 100644 index 00000000..483842bd --- /dev/null +++ b/hiero-enterprise-spring-sample/src/main/java/org/hiero/spring/sample/dto/topic/TopicMessageRequest.java @@ -0,0 +1,24 @@ +package org.hiero.spring.sample.dto.topic; + +import io.swagger.v3.oas.annotations.media.Schema; + +/** Request DTO for submitting a message to a Consensus Topic. */ +@Schema( + name = "Consensus Topic: Message Request", + description = "Request DTO for submitting a message to a Hiero Consensus Service (HCS) topic.") +public record TopicMessageRequest( + @Schema( + description = "The ID of the topic to submit the message to.", + example = "0.0.1234", + requiredMode = Schema.RequiredMode.REQUIRED) + String topicId, + @Schema( + description = "The message content to submit.", + example = "Hello Hiero Consensus Service!", + requiredMode = Schema.RequiredMode.REQUIRED) + String message, + @Schema( + description = "The submit key (required if the topic is private).", + example = "302e020100300506032b657004220420...", + requiredMode = Schema.RequiredMode.NOT_REQUIRED) + String submitKey) {} diff --git a/hiero-enterprise-spring-sample/src/main/java/org/hiero/spring/sample/dto/topic/TopicUpdateRequest.java b/hiero-enterprise-spring-sample/src/main/java/org/hiero/spring/sample/dto/topic/TopicUpdateRequest.java new file mode 100644 index 00000000..fa2a38a2 --- /dev/null +++ b/hiero-enterprise-spring-sample/src/main/java/org/hiero/spring/sample/dto/topic/TopicUpdateRequest.java @@ -0,0 +1,34 @@ +package org.hiero.spring.sample.dto.topic; + +import io.swagger.v3.oas.annotations.media.Schema; + +/** Request DTO for updating an existing Consensus Topic. */ +@Schema( + name = "Consensus Topic: Update Request", + description = "Request DTO for updating an existing Hiero Consensus Service (HCS) topic.") +public record TopicUpdateRequest( + @Schema( + description = "The ID of the topic to update.", + example = "0.0.1234", + requiredMode = Schema.RequiredMode.REQUIRED) + String topicId, + @Schema( + description = "The new memo for the topic.", + example = "Updated topic memo", + requiredMode = Schema.RequiredMode.NOT_REQUIRED) + String memo, + @Schema( + description = "The current admin key (required for most updates).", + example = "302e020100300506032b657004220420...", + requiredMode = Schema.RequiredMode.NOT_REQUIRED) + String adminKey, + @Schema( + description = "The new admin key to set (optional).", + example = "302e020100300506032b657004220420...", + requiredMode = Schema.RequiredMode.NOT_REQUIRED) + String updatedAdminKey, + @Schema( + description = "The new submit key to set (optional).", + example = "302e020100300506032b657004220420...", + requiredMode = Schema.RequiredMode.NOT_REQUIRED) + String submitKey) {}