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();
        }
    }
}