UVerifyTransactionController.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.controller;
import com.bloxbean.cardano.client.api.model.Result;
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.swagger.v3.oas.annotations.tags.Tag;
import io.uverify.backend.dto.BuildTransactionRequest;
import io.uverify.backend.dto.BuildTransactionResponse;
import io.uverify.backend.dto.ProxyInitResponse;
import io.uverify.backend.dto.SubmitTransactionRequest;
import io.uverify.backend.enums.BuildStatusCode;
import io.uverify.backend.enums.TransactionType;
import io.uverify.backend.service.UVerifyTransactionService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@SuppressWarnings("unused")
@RequestMapping("/api/v1/transaction")
@Slf4j
@Tag(name = "Transaction Management", description = "Endpoints for building and submitting UVerify certificate transactions to the Cardano blockchain.")
public class UVerifyTransactionController {
@Autowired
private UVerifyTransactionService transactionService;
@GetMapping("/confirm/{hash}")
@Operation(
summary = "Check if a transaction has been confirmed on-chain",
description = "Returns 200 if the transaction is confirmed on the Cardano blockchain, 404 if it is not yet confirmed."
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Transaction confirmed on-chain"),
@ApiResponse(responseCode = "404", description = "Transaction not yet confirmed"),
@ApiResponse(responseCode = "500", description = "Internal server error")
})
public ResponseEntity<?> confirmTransaction(@PathVariable String hash) {
try {
boolean confirmed = transactionService.isTransactionConfirmed(hash);
return confirmed ? ResponseEntity.ok().build() : ResponseEntity.notFound().build();
} catch (Exception e) {
log.error("Error confirming transaction: {}", e.getMessage(), e);
return ResponseEntity.internalServerError().build();
}
}
@PostMapping("/build")
@Operation(
summary = "Build a transaction",
description = """
Builds a transaction for the Cardano blockchain based on the provided request. Supports the following transaction types:
- **DEFAULT**: Submits UVerify certificates to the blockchain using the cheapest options. If no state is initialized, it forks a new state from the bootstrap datum with the best service fee conditions. If a user state exists with a valid transaction countdown and no service fee is required, it will be reused.
- **BOOTSTRAP**: Initializes a new bootstrap token and datum for forking states. Requires a whitelisted credential to sign the transaction.
- **INIT**: Init a new proxy script for UVerify certificate management.
- **CUSTOM**: Allows the user to specify a bootstrap datum to fork or consume a state related to a specific bootstrap datum. This is useful for use cases requiring a 'partner datum' and may result in a different certificate UI on the client side."""
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Transaction built successfully",
content = @Content(mediaType = "application/json", schema = @Schema(implementation = BuildTransactionResponse.class))),
@ApiResponse(responseCode = "400", description = "Invalid transaction type or request data"),
@ApiResponse(responseCode = "500", description = "Internal server error")
})
public ResponseEntity<?> buildTransaction(@RequestBody BuildTransactionRequest request) {
try {
if (request.getType().equals(TransactionType.DEFAULT)) {
BuildTransactionResponse buildTransactionResponse = transactionService.buildUVerifyTransaction(request.getCertificates(), request.getAddress());
if (buildTransactionResponse.getStatus().getCode().equals(BuildStatusCode.SUCCESS)) {
return ResponseEntity.ok(buildTransactionResponse);
} else {
return ResponseEntity.badRequest().body(buildTransactionResponse);
}
} else if (request.getType().equals(TransactionType.BOOTSTRAP)) {
BuildTransactionResponse buildTransactionResponse = transactionService.buildBootstrapDatum(request.getBootstrapDatum());
if (buildTransactionResponse.getStatus().getCode().equals(BuildStatusCode.SUCCESS)) {
return ResponseEntity.ok(buildTransactionResponse);
} else {
return ResponseEntity.badRequest().body(buildTransactionResponse);
}
} else if (request.getType().equals(TransactionType.CUSTOM)) {
BuildTransactionResponse buildTransactionResponse = transactionService.buildCustomTransaction(request.getCertificates(), request.getAddress(), request.getBootstrapDatum().getName());
if (buildTransactionResponse.getStatus().getCode().equals(BuildStatusCode.SUCCESS)) {
return ResponseEntity.ok(buildTransactionResponse);
} else {
return ResponseEntity.badRequest().body(buildTransactionResponse);
}
} else if (request.getType().equals(TransactionType.INIT)) {
ProxyInitResponse proxyInitResponse = transactionService.buildInitProxyTx();
if (proxyInitResponse.getStatus().getCode().equals(BuildStatusCode.SUCCESS)) {
return ResponseEntity.ok(proxyInitResponse);
} else {
return ResponseEntity.badRequest().body(proxyInitResponse);
}
} else {
return ResponseEntity.badRequest().body("Unknown transaction type. Allowed types are: DEFAULT, BOOTSTRAP, CUSTOM.");
}
} catch (Exception e) {
log.error("Error building transaction: {}", e.getMessage(), e);
return ResponseEntity.internalServerError().build();
}
}
@PostMapping("/submit")
@Operation(
summary = "Submit a transaction",
description = "Submits a transaction to the Cardano blockchain using the provided transaction data and witness set. "
+ "Returns the result of the submission or a 500 status code in case of server errors."
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Transaction submitted successfully",
content = @Content(mediaType = "application/json")),
@ApiResponse(responseCode = "500", description = "Internal server error")
})
public ResponseEntity<?> submitTransaction(@RequestBody SubmitTransactionRequest request) {
try {
Result<String> result = transactionService.submit(request.getTransaction(), request.getWitnessSet());
if (result.isSuccessful()) {
return ResponseEntity.ok(java.util.Map.of("transactionHash", result.getValue()));
}
return ResponseEntity.badRequest().body(result.getResponse());
} catch (Exception e) {
log.error("Error submitting transaction: {}", e.getMessage(), e);
return ResponseEntity.internalServerError().build();
}
}
}