ConnectedGoodsController.java
/*
* UVerify Backend
* Copyright (C) 2025 Fabian Bormann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.uverify.backend.extension.controller;
import com.bloxbean.cardano.client.address.Address;
import com.bloxbean.cardano.client.address.Credential;
import com.bloxbean.cardano.client.transaction.spec.Transaction;
import com.bloxbean.cardano.client.util.HexUtil;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.uverify.backend.dto.BuildTransactionResponse;
import io.uverify.backend.enums.CardanoNetwork;
import io.uverify.backend.extension.dto.ClaimUpdateConnectedGoodsRequest;
import io.uverify.backend.extension.dto.MintConnectedGoodsRequest;
import io.uverify.backend.extension.dto.MintConnectedGoodsResponse;
import io.uverify.backend.extension.dto.SocialHub;
import io.uverify.backend.extension.entity.ConnectedGoodEntity;
import io.uverify.backend.extension.entity.ConnectedGoodUpdateEntity;
import io.uverify.backend.extension.entity.SocialHubEntity;
import io.uverify.backend.extension.service.ConnectedGoodsService;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.Optional;
import static io.uverify.backend.extension.dto.SocialHub.fromSocialHubEntity;
import static io.uverify.backend.extension.utils.ConnectedGoodUtils.applySHA3_256;
import static io.uverify.backend.util.CardanoUtils.fromCardanoNetwork;
@Slf4j
@ConditionalOnProperty(value = "extensions.connected-goods.enabled", havingValue = "true")
@RestController
@SuppressWarnings("unused")
@RequestMapping("/api/v1/extension/connected-goods")
public class ConnectedGoodsController {
@Autowired
private final ConnectedGoodsService connectedGoodsService;
private final CardanoNetwork network;
public ConnectedGoodsController(@Value("${cardano.network}") String network, ConnectedGoodsService connectedGoodsService) {
this.connectedGoodsService = connectedGoodsService;
this.network = CardanoNetwork.valueOf(network);
}
// TODO: Remove it in the future and replace it by proper UVerify certificate re-minting strategies
private String correctBatchIds(String batchId) {
return switch (batchId) {
case "0ef210c95e39bbfc1f3bff7354f05e02d6ef751aedb6087df684ad655d2fb13b" ->
"189b6ba68788f9e4806aecac2724ce0cfa92553ea384d2c1607b8af383598860";
case "2898c9849a49f216e0c389040eda20610819c1e4b64b9d41b2137bc54f374455" ->
"c78ef5bed568c856a113e16f559feb73b2cd3c161561fb97ae91d0450c552571";
case "f696dd2126597392fdb4f564af149f14c2df75544e9a844152ccfd054dae57f5" ->
"1d6f25308d25e3d955ced55025c484d178f1fa6b7932e86a74bbd9ee790b6e22";
default -> batchId;
};
}
@GetMapping("/{batchIds}/{itemId}")
public ResponseEntity<?> getConnectedGood(@PathVariable String batchIds, @PathVariable String itemId) {
batchIds = correctBatchIds(batchIds);
SocialHubEntity socialHubEntity = connectedGoodsService.getSocialHubByBatchIdsAndItemId(batchIds, itemId);
try {
return ResponseEntity.ok(
connectedGoodsService.decryptSocialHub(
fromSocialHubEntity(socialHubEntity, fromCardanoNetwork(network)), itemId));
} catch (Exception exception) {
log.error("Error decrypting social hub: {}", exception.getMessage(), exception);
return ResponseEntity.badRequest().body("Error while decrypting social hub. Please check the provided item id and try again.");
}
}
@PostMapping("/mint/batch")
@Operation(
summary = "Returns an unsigned transaction to mint a batch of connected goods",
description = """
Returns an unsigned transaction to mint a batch of connected goods.
The transaction needs to be signed by a user wallet.
The request contains a list of connected goods, each with an asset name and a unique claiming password.
"""
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "transaction successfully created",
content = @Content(mediaType = "application/json", schema = @Schema(implementation = BuildTransactionResponse.class))),
@ApiResponse(responseCode = "400", description = "Invalid request or unknown action type",
content = @Content(mediaType = "application/json", schema = @Schema(implementation = BuildTransactionResponse.class))),
@ApiResponse(responseCode = "500", description = "Internal server error")
})
public ResponseEntity<MintConnectedGoodsResponse> mintConnectedGoods(@RequestBody @NotNull MintConnectedGoodsRequest mintConnectedGoodsRequest) {
try {
return ResponseEntity.ok(connectedGoodsService.mint(mintConnectedGoodsRequest.getTokenName(), mintConnectedGoodsRequest.getItems(), mintConnectedGoodsRequest.getAddress()));
} catch (Exception exception) {
MintConnectedGoodsResponse response = new MintConnectedGoodsResponse();
log.error("Error building transaction: {}", exception.getMessage(), exception);
response.setStatus(HttpStatus.BAD_REQUEST);
response.setError(exception.getMessage());
response.setMessage("Transaction building failed. Please check the request and try again.");
return ResponseEntity.badRequest().body(response);
}
}
@PostMapping("/claim/item")
public ResponseEntity<?> claimItem(@RequestBody @NotNull ClaimUpdateConnectedGoodsRequest claimUpdateConnectedGoodsRequest) {
String batchId = correctBatchIds(claimUpdateConnectedGoodsRequest.getBatchId());
try {
SocialHubEntity socialHubEntity = connectedGoodsService.getSocialHubByBatchIdAndMintHash(
batchId,
applySHA3_256(claimUpdateConnectedGoodsRequest.getPassword())
);
SocialHub socialHub = claimUpdateConnectedGoodsRequest.getSocialHub();
if (socialHub.getOwner().startsWith("addr")) {
Address address = new Address(claimUpdateConnectedGoodsRequest.getUserAddress());
Optional<Credential> optionalCredential = address.getPaymentCredential();
if (optionalCredential.isEmpty()) {
return ResponseEntity.badRequest().body("Invalid user address");
}
socialHub.setOwner(HexUtil.encodeHexString(optionalCredential.get().getBytes()));
}
ConnectedGoodEntity connectedGood = socialHubEntity.getConnectedGood();
ConnectedGoodUpdateEntity latestConnectedGoodUpdate = connectedGoodsService.getLatestUpdateByConnectedGoodId(connectedGood.getId());
Transaction transaction = connectedGoodsService.claim(
claimUpdateConnectedGoodsRequest.getSocialHub().getItemName(),
claimUpdateConnectedGoodsRequest.getPassword(),
latestConnectedGoodUpdate.getTransactionId(),
latestConnectedGoodUpdate.getOutputIndex(),
socialHub.toSocialHubDatum(batchId),
claimUpdateConnectedGoodsRequest.getUserAddress()
);
return ResponseEntity.ok(transaction.serializeToHex());
} catch (Exception exception) {
log.error("Error building transaction: {}", exception.getMessage(), exception);
return ResponseEntity.badRequest().body("Error while building transaction. Please check the request and try again.");
}
}
@PostMapping("/update/item")
public ResponseEntity<?> updateItem(@RequestBody @NotNull ClaimUpdateConnectedGoodsRequest claimUpdateConnectedGoodsRequest) {
String batchId = correctBatchIds(claimUpdateConnectedGoodsRequest.getBatchId());
try {
SocialHubEntity socialHubEntity = connectedGoodsService.getSocialHubByBatchIdAndMintHash(
batchId,
applySHA3_256(claimUpdateConnectedGoodsRequest.getPassword())
);
SocialHub socialHub = claimUpdateConnectedGoodsRequest.getSocialHub();
if (socialHub.getOwner().startsWith("addr")) {
Address address = new Address(claimUpdateConnectedGoodsRequest.getUserAddress());
Optional<Credential> optionalCredential = address.getPaymentCredential();
if (optionalCredential.isEmpty()) {
return ResponseEntity.badRequest().body("Invalid user address");
}
socialHub.setOwner(HexUtil.encodeHexString(optionalCredential.get().getBytes()));
}
Transaction transaction = connectedGoodsService.update(
socialHub.toSocialHubDatum(batchId),
socialHubEntity.getTransactionId(),
socialHubEntity.getOutputIndex(),
claimUpdateConnectedGoodsRequest.getUserAddress(),
claimUpdateConnectedGoodsRequest.getPassword()
);
return ResponseEntity.ok(transaction.serializeToHex());
} catch (Exception exception) {
log.error("Error building transaction: {}", exception.getMessage(), exception);
return ResponseEntity.badRequest().body("Error while building transaction. Please check the request and try again.");
}
}
}