TadamonService.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.service;
import co.nstant.in.cbor.model.Map;
import com.bloxbean.cardano.client.address.Address;
import com.bloxbean.cardano.client.api.exception.ApiException;
import com.bloxbean.cardano.client.api.model.Result;
import com.bloxbean.cardano.client.common.cbor.CborSerializationUtil;
import com.bloxbean.cardano.client.crypto.Blake2bUtil;
import com.bloxbean.cardano.client.exception.CborDeserializationException;
import com.bloxbean.cardano.client.exception.CborSerializationException;
import com.bloxbean.cardano.client.transaction.spec.Transaction;
import com.bloxbean.cardano.client.transaction.spec.TransactionOutput;
import com.bloxbean.cardano.client.transaction.spec.TransactionWitnessSet;
import com.bloxbean.cardano.client.util.HexUtil;
import com.bloxbean.cardano.yaci.store.common.domain.AddressUtxo;
import io.uverify.backend.extension.UVerifyServiceExtension;
import io.uverify.backend.extension.dto.TadamonTransactionRequest;
import io.uverify.backend.extension.entity.TadamonTransactionEntity;
import io.uverify.backend.extension.repository.TadamonTransactionRepository;
import io.uverify.backend.model.StateDatum;
import io.uverify.backend.model.UVerifyCertificate;
import io.uverify.backend.service.CardanoBlockchainService;
import io.uverify.backend.util.ValidatorUtils;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
@Service
@Slf4j
@AllArgsConstructor
@ConditionalOnProperty(value = "extensions.tadamon.enabled", havingValue = "true")
public class TadamonService implements UVerifyServiceExtension {
@Autowired
private final CardanoBlockchainService cardanoBlockchainService;
@Autowired
private final TadamonTransactionRepository tadamonTransactionRepository;
@Autowired
private final TadamonGoogleSheetsService tadamonGoogleSheetsService;
@Value("${extensions.tadamon.allowed-addresses}")
private final List<String> allowedAddresses;
@Override
public void processAddressUtxos(List<AddressUtxo> addressUtxos) {
}
@Override
public void handleRollbackToSlot(long slot) {
List<TadamonTransactionEntity> transactionEntities = tadamonTransactionRepository.findBySlotGreaterThan(slot);
for (TadamonTransactionEntity transactionEntity : transactionEntities) {
log.info("Handle rollback for tadamon certificate {} to slot {} for transaction {}",
transactionEntity.getCertificateDataHash(),
slot,
transactionEntity.getTransactionId());
try {
Transaction transaction = Transaction.deserialize(HexUtil.decodeHexString(transactionEntity.getTransactionHex()));
Result<String> result = cardanoBlockchainService.submitTransaction(transaction);
if (result.isSuccessful()) {
log.info("Transaction {} rolled back successfully and is now {}", transactionEntity.getTransactionId(), result.getResponse());
transactionEntity.setTransactionId(result.getValue());
transactionEntity.setCertificateCreationDate(LocalDateTime.now());
transactionEntity.setSlot(cardanoBlockchainService.getLatestSlot());
tadamonTransactionRepository.save(transactionEntity);
int row = tadamonGoogleSheetsService.findRowByDataHash(transactionEntity.getCertificateDataHash());
tadamonGoogleSheetsService.writeRowToSheet(transactionEntity, row);
} else {
log.error("Failed to roll back transaction {}: {}", transactionEntity.getTransactionId(), result.getResponse());
}
} catch (Exception e) {
log.error("Error while rolling back transaction {}: {}", transactionEntity.getTransactionId(), e.getMessage());
}
}
}
public Result<?> submit(TadamonTransactionRequest request) throws CborDeserializationException, CborSerializationException, ApiException {
Transaction transaction = Transaction.deserialize(HexUtil.decodeHexString(request.getTransaction()));
if (request.getWitnessSet() != null && !request.getWitnessSet().isEmpty()) {
TransactionWitnessSet witnessSet = TransactionWitnessSet.deserialize((Map) CborSerializationUtil.deserialize(HexUtil.decodeHexString(request.getWitnessSet())));
transaction.getWitnessSet().setVkeyWitnesses(witnessSet.getVkeyWitnesses());
}
if (transaction.getWitnessSet() == null || transaction.getWitnessSet().getVkeyWitnesses().isEmpty()) {
return Result.error("Transaction witness set is empty. Make sure either the transaction is signed or the witness set is provided");
}
boolean signedByAllowedAddress = transaction.getWitnessSet().getVkeyWitnesses().stream().anyMatch(
vkeyWitness -> {
String vkeyHash = HexUtil.encodeHexString(Blake2bUtil.blake2bHash224(vkeyWitness.getVkey()));
for (String address : allowedAddresses) {
Optional<byte[]> paymentCredential = new Address(address).getPaymentCredentialHash();
if (paymentCredential.isPresent()) {
String paymentCredentialHash = HexUtil.encodeHexString(paymentCredential.get());
if (paymentCredentialHash.equalsIgnoreCase(vkeyHash)) {
return true;
}
}
}
return false;
}
);
if (!signedByAllowedAddress) {
return Result.error("Transaction not signed by whitelisted address");
}
TadamonTransactionEntity tadamonTransactionEntity = new TadamonTransactionEntity();
tadamonTransactionEntity.setTransactionHex(transaction.serializeToHex());
tadamonTransactionEntity.setCsoName(request.getCso().getName());
tadamonTransactionEntity.setCsoEmail(request.getCso().getEmail());
tadamonTransactionEntity.setCsoOrganizationType(request.getCso().getOrganizationType());
tadamonTransactionEntity.setCsoStatusApproved(request.getCso().getStatusApproved());
tadamonTransactionEntity.setCsoRegistrationCountry(request.getCso().getRegistrationCountry());
tadamonTransactionEntity.setCsoEstablishmentDate(request.getCso().getEstablishmentDate());
tadamonTransactionEntity.setTadamonId(request.getTadamonId());
tadamonTransactionEntity.setVeridianAid(request.getVeridianAid());
tadamonTransactionEntity.setUndpSigningDate(request.getUndpSigningDate());
tadamonTransactionEntity.setBeneficiarySigningDate(request.getBeneficiarySigningDate());
TransactionOutput transactionOutput = transaction.getBody().getOutputs().stream().filter(
ValidatorUtils::includesStateToken
).findFirst().orElse(null);
if (transactionOutput == null) {
return Result.error("Transaction output does not contain a state token nor UVerify certificates");
}
StateDatum stateDatum = StateDatum.fromUtxoDatum(transactionOutput.getInlineDatum().serializeToHex());
if (stateDatum.getUVerifyCertificates().isEmpty()) {
return Result.error("Transaction output does not contain UVerify certificates");
} else if (stateDatum.getUVerifyCertificates().size() > 1) {
return Result.error("Transaction output contains multiple UVerify certificates. Only one is allowed" +
" for the Tadamon extension");
}
UVerifyCertificate certificate = stateDatum.getUVerifyCertificates().get(0);
tadamonTransactionEntity.setCertificateDataHash(certificate.getHash());
Result<String> result = cardanoBlockchainService.submitTransaction(transaction);
if (result.isSuccessful()) {
tadamonTransactionEntity.setTransactionId(result.getValue());
tadamonTransactionEntity.setCertificateCreationDate(LocalDateTime.now());
tadamonTransactionEntity.setSlot(cardanoBlockchainService.getLatestSlot());
tadamonTransactionRepository.save(tadamonTransactionEntity);
int row = tadamonGoogleSheetsService.findRowByDataHash(tadamonTransactionEntity.getCertificateDataHash());
tadamonGoogleSheetsService.writeRowToSheet(tadamonTransactionEntity, row);
}
return result;
}
}