All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.quorum.tessera.q2t.TransactionResource Maven / Gradle / Ivy

package com.quorum.tessera.q2t;

import static jakarta.ws.rs.core.MediaType.*;

import com.quorum.tessera.api.*;
import com.quorum.tessera.api.constraint.PrivacyValid;
import com.quorum.tessera.config.constraints.ValidBase64;
import com.quorum.tessera.data.MessageHash;
import com.quorum.tessera.enclave.PrivacyGroup;
import com.quorum.tessera.enclave.PrivacyMode;
import com.quorum.tessera.encryption.PublicKey;
import com.quorum.tessera.privacygroup.PrivacyGroupManager;
import com.quorum.tessera.transaction.TransactionManager;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.ExampleObject;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.UriBuilder;
import java.net.URI;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Provides endpoints for dealing with transactions, including:
 *
 * 

- creating new transactions and distributing them - deleting transactions - fetching * transactions - resending old transactions */ @Tag(name = "quorum-to-tessera") @Path("/") public class TransactionResource { private static final Logger LOGGER = LoggerFactory.getLogger(TransactionResource.class); private final TransactionManager transactionManager; private final PrivacyGroupManager privacyGroupManager; private final Base64.Decoder base64Decoder = Base64.getDecoder(); private final Base64.Encoder base64Encoder = Base64.getEncoder(); public TransactionResource( TransactionManager transactionManager, PrivacyGroupManager privacyGroupManager) { this.transactionManager = Objects.requireNonNull(transactionManager); this.privacyGroupManager = Objects.requireNonNull(privacyGroupManager); } // hide this operation from swagger generation; the /send operation is overloaded and must be // documented in a single place @Hidden @POST @Path("send") @Consumes(APPLICATION_JSON) @Produces(APPLICATION_JSON) public Response send(@NotNull @Valid @PrivacyValid final SendRequest sendRequest) { final PublicKey sender = Optional.ofNullable(sendRequest.getFrom()) .map(base64Decoder::decode) .map(PublicKey::from) .orElseGet(transactionManager::defaultPublicKey); final Optional optionalPrivacyGroup = Optional.ofNullable(sendRequest.getPrivacyGroupId()).map(PrivacyGroup.Id::fromBase64String); final List recipientList = optionalPrivacyGroup .map(privacyGroupManager::retrievePrivacyGroup) .map(PrivacyGroup::getMembers) .orElse( Stream.of(sendRequest) .filter(sr -> Objects.nonNull(sr.getTo())) .flatMap(s -> Stream.of(s.getTo())) .map(base64Decoder::decode) .map(PublicKey::from) .collect(Collectors.toList())); final Set affectedTransactions = Stream.ofNullable(sendRequest.getAffectedContractTransactions()) .flatMap(Arrays::stream) .map(base64Decoder::decode) .map(MessageHash::new) .collect(Collectors.toSet()); final byte[] execHash = Optional.ofNullable(sendRequest.getExecHash()).map(String::getBytes).orElse(new byte[0]); final PrivacyMode privacyMode = PrivacyMode.fromFlag(sendRequest.getPrivacyFlag()); final com.quorum.tessera.transaction.SendRequest.Builder requestBuilder = com.quorum.tessera.transaction.SendRequest.Builder.create() .withRecipients(recipientList) .withSender(sender) .withPayload(sendRequest.getPayload()) .withExecHash(execHash) .withPrivacyMode(privacyMode) .withAffectedContractTransactions(affectedTransactions); optionalPrivacyGroup.ifPresent(requestBuilder::withPrivacyGroupId); final com.quorum.tessera.transaction.SendResponse response = transactionManager.send(requestBuilder.build()); final String encodedKey = Optional.of(response) .map(com.quorum.tessera.transaction.SendResponse::getTransactionHash) .map(MessageHash::getHashBytes) .map(base64Encoder::encodeToString) .get(); final SendResponse sendResponse = Optional.of(response) .map(com.quorum.tessera.transaction.SendResponse::getTransactionHash) .map(MessageHash::getHashBytes) .map(base64Encoder::encodeToString) .map(messageHash -> new SendResponse(messageHash, null, null)) .get(); final URI location = UriBuilder.fromPath("transaction") .path(URLEncoder.encode(encodedKey, StandardCharsets.UTF_8)) .build(); return Response.status(Response.Status.CREATED) .type(APPLICATION_JSON) .location(location) .entity(sendResponse) .build(); } // hide this operation from swagger generation; the /sendsignedtx operation is overloaded and must // be documented in a single place @Hidden @POST @Path("sendsignedtx") @Consumes(APPLICATION_OCTET_STREAM) @Produces(TEXT_PLAIN) public Response sendSignedTransactionStandard( @Parameter( description = "comma-separated list of recipient public keys (for application/octet-stream requests)", schema = @Schema(format = "base64")) @HeaderParam("c11n-to") final String recipientKeys, @Valid @NotNull @Size(min = 1) final byte[] signedTransaction) { final List recipients = Stream.ofNullable(recipientKeys) .filter(s -> !Objects.equals("", s)) .map(v -> v.split(",")) .flatMap(Arrays::stream) .map(base64Decoder::decode) .map(PublicKey::from) .collect(Collectors.toList()); final com.quorum.tessera.transaction.SendSignedRequest request = com.quorum.tessera.transaction.SendSignedRequest.Builder.create() .withRecipients(recipients) .withSignedData(signedTransaction) .withPrivacyMode(PrivacyMode.STANDARD_PRIVATE) .withAffectedContractTransactions(Collections.emptySet()) .withExecHash(new byte[0]) .build(); final com.quorum.tessera.transaction.SendResponse response = transactionManager.sendSignedTransaction(request); final String encodedTransactionHash = base64Encoder.encodeToString(response.getTransactionHash().getHashBytes()); LOGGER.debug("Encoded key: {}", encodedTransactionHash); URI location = UriBuilder.fromPath("transaction") .path(URLEncoder.encode(encodedTransactionHash, StandardCharsets.UTF_8)) .build(); return Response.status(Response.Status.OK) .entity(encodedTransactionHash) .location(location) .build(); } // hide this operation from swagger generation; the /sendsignedtx operation is overloaded and must // be documented in a single place @Hidden @POST @Path("sendsignedtx") @Consumes(APPLICATION_JSON) @Produces(APPLICATION_JSON) public Response sendSignedTransactionEnhanced( @NotNull @Valid @PrivacyValid final SendSignedRequest sendSignedRequest) { final Optional privacyGroupId = Optional.ofNullable(sendSignedRequest.getPrivacyGroupId()) .map(PrivacyGroup.Id::fromBase64String); final List recipients = privacyGroupId .map(privacyGroupManager::retrievePrivacyGroup) .map(PrivacyGroup::getMembers) .orElse( Optional.ofNullable(sendSignedRequest.getTo()) .map(Arrays::stream) .orElse(Stream.empty()) .map(base64Decoder::decode) .map(PublicKey::from) .collect(Collectors.toList())); final PrivacyMode privacyMode = PrivacyMode.fromFlag(sendSignedRequest.getPrivacyFlag()); final Set affectedTransactions = Stream.ofNullable(sendSignedRequest.getAffectedContractTransactions()) .flatMap(Arrays::stream) .map(base64Decoder::decode) .map(MessageHash::new) .collect(Collectors.toSet()); final byte[] execHash = Optional.ofNullable(sendSignedRequest.getExecHash()) .map(String::getBytes) .orElse(new byte[0]); final com.quorum.tessera.transaction.SendSignedRequest.Builder requestBuilder = com.quorum.tessera.transaction.SendSignedRequest.Builder.create() .withSignedData(sendSignedRequest.getHash()) .withRecipients(recipients) .withPrivacyMode(privacyMode) .withAffectedContractTransactions(affectedTransactions) .withExecHash(execHash); privacyGroupId.ifPresent(requestBuilder::withPrivacyGroupId); final com.quorum.tessera.transaction.SendResponse response = transactionManager.sendSignedTransaction(requestBuilder.build()); final String endcodedTransactionHash = Optional.of(response) .map(com.quorum.tessera.transaction.SendResponse::getTransactionHash) .map(MessageHash::getHashBytes) .map(base64Encoder::encodeToString) .get(); LOGGER.debug("Encoded key: {}", endcodedTransactionHash); URI location = UriBuilder.fromPath("transaction") .path(URLEncoder.encode(endcodedTransactionHash, StandardCharsets.UTF_8)) .build(); SendResponse sendResponse = new SendResponse(); sendResponse.setKey(endcodedTransactionHash); return Response.status(Response.Status.CREATED) .type(APPLICATION_JSON) .location(location) .entity(sendResponse) .build(); } @Operation( summary = "/sendraw", operationId = "encryptStoreAndSendOctetStream", description = "encrypts a payload, stores result in database, and publishes result to recipients") @ApiResponse( responseCode = "200", description = "encrypted payload hash", content = @Content( schema = @Schema( type = "string", format = "base64", description = "encrypted payload hash"))) @POST @Path("sendraw") @Consumes(APPLICATION_OCTET_STREAM) @Produces(TEXT_PLAIN) public Response sendRaw( @HeaderParam("c11n-from") @Parameter( description = "public key identifying the server's key pair that will be used in the encryption; if not set, default used", schema = @Schema(format = "base64")) @Valid @ValidBase64 final String sender, @HeaderParam("c11n-to") @Parameter( description = "comma-separated list of recipient public keys", schema = @Schema(format = "base64")) final String recipientKeys, @Schema(description = "data to be encrypted") @NotNull @Size(min = 1) @Valid final byte[] payload) { final PublicKey senderKey = Optional.ofNullable(sender) .filter(Predicate.not(String::isEmpty)) .map(base64Decoder::decode) .map(PublicKey::from) .orElseGet(transactionManager::defaultPublicKey); final List recipients = Stream.of(recipientKeys) .filter(Objects::nonNull) .filter(s -> !Objects.equals("", s)) .map(v -> v.split(",")) .flatMap(Arrays::stream) .map(base64Decoder::decode) .map(PublicKey::from) .collect(Collectors.toList()); final com.quorum.tessera.transaction.SendRequest request = com.quorum.tessera.transaction.SendRequest.Builder.create() .withSender(senderKey) .withRecipients(recipients) .withPayload(payload) .withPrivacyMode(PrivacyMode.STANDARD_PRIVATE) .withAffectedContractTransactions(Collections.emptySet()) .withExecHash(new byte[0]) .build(); final com.quorum.tessera.transaction.SendResponse sendResponse = transactionManager.send(request); final String encodedTransactionHash = Optional.of(sendResponse) .map(com.quorum.tessera.transaction.SendResponse::getTransactionHash) .map(MessageHash::getHashBytes) .map(base64Encoder::encodeToString) .get(); LOGGER.debug("Encoded key: {}", encodedTransactionHash); URI location = UriBuilder.fromPath("transaction") .path(URLEncoder.encode(encodedTransactionHash, StandardCharsets.UTF_8)) .build(); return Response.status(Response.Status.OK) .entity(encodedTransactionHash) .location(location) .build(); } // hide this operation from swagger generation; the /transaction/{hash} operation is overloaded // and must be documented in a single place @Hidden @GET @Path("/transaction/{hash}") @Produces(APPLICATION_JSON) public Response receive( @Parameter( description = "hash indicating encrypted payload to retrieve from database", schema = @Schema(format = "base64")) @Valid @ValidBase64 @PathParam("hash") final String hash, @Parameter( description = "(optional) public key of recipient of the encrypted payload; used in decryption; if not provided, decryption is attempted with all known recipient keys in turn", schema = @Schema(format = "base64")) @QueryParam("to") final String toStr, @Parameter( description = "(optional) indicates whether the payload is raw; determines which database the payload is retrieved from; possible values\n* true - for pre-stored payloads in the \"raw\" database\n* false (default) - for already sent payloads in \"standard\" database") @Valid @Pattern(flags = Pattern.Flag.CASE_INSENSITIVE, regexp = "^(true|false)$") @QueryParam("isRaw") final String isRaw) { final PublicKey recipient = Optional.ofNullable(toStr) .filter(Predicate.not(String::isEmpty)) .map(base64Decoder::decode) .map(PublicKey::from) .orElse(null); final MessageHash transactionHash = Optional.of(hash).map(base64Decoder::decode).map(MessageHash::new).get(); final com.quorum.tessera.transaction.ReceiveRequest request = com.quorum.tessera.transaction.ReceiveRequest.Builder.create() .withRecipient(recipient) .withTransactionHash(transactionHash) .withRaw(Boolean.valueOf(isRaw)) .build(); com.quorum.tessera.transaction.ReceiveResponse response = transactionManager.receive(request); final ReceiveResponse receiveResponse = new ReceiveResponse(); receiveResponse.setPayload(response.getUnencryptedTransactionData()); receiveResponse.setAffectedContractTransactions( response.getAffectedTransactions().stream() .map(MessageHash::getHashBytes) .map(base64Encoder::encodeToString) .toArray(String[]::new)); Optional.ofNullable(response.getExecHash()) .map(String::new) .ifPresent(receiveResponse::setExecHash); receiveResponse.setPrivacyFlag(response.getPrivacyMode().getPrivacyFlag()); response .getPrivacyGroupId() .map(PrivacyGroup.Id::getBase64) .ifPresent(receiveResponse::setPrivacyGroupId); return Response.status(Response.Status.OK) .type(APPLICATION_JSON) .entity(receiveResponse) .build(); } @Operation( summary = "/receiveraw", operationId = "getDecryptedPayloadOctetStream", description = "get payload from database, decrypt, and return") @ApiResponse( responseCode = "200", description = "decrypted ciphertext payload", content = @Content( array = @ArraySchema( schema = @Schema( type = "string", format = "byte", description = "decrypted ciphertext payload")))) @GET @Path("receiveraw") @Consumes(APPLICATION_OCTET_STREAM) @Produces(APPLICATION_OCTET_STREAM) public Response receiveRaw( @Schema( description = "hash indicating encrypted payload to retrieve from database", format = "base64") @ValidBase64 @NotNull @HeaderParam(value = "c11n-key") String hash, @Schema( description = "(optional) public key of recipient of the encrypted payload; used in decryption; if not provided, decryption is attempted with all known recipient keys in turn", format = "base64") @ValidBase64 @HeaderParam(value = "c11n-to") String recipientKey) { LOGGER.debug("Received receiveraw request for hash : {}, recipientKey: {}", hash, recipientKey); MessageHash transactionHash = Optional.of(hash).map(base64Decoder::decode).map(MessageHash::new).get(); PublicKey recipient = Optional.ofNullable(recipientKey) .map(base64Decoder::decode) .map(PublicKey::from) .orElse(null); com.quorum.tessera.transaction.ReceiveRequest request = com.quorum.tessera.transaction.ReceiveRequest.Builder.create() .withTransactionHash(transactionHash) .withRecipient(recipient) .build(); com.quorum.tessera.transaction.ReceiveResponse receiveResponse = transactionManager.receive(request); byte[] payload = receiveResponse.getUnencryptedTransactionData(); return Response.status(Response.Status.OK).entity(payload).build(); } @Deprecated @Operation( summary = "/delete", operationId = "deleteDeprecated", description = "delete payload from database") @ApiResponse( responseCode = "200", description = "delete successful", content = @Content( schema = @Schema(type = "string"), examples = @ExampleObject(value = "Delete successful"))) @POST @Path("delete") @Consumes(APPLICATION_JSON) @Produces(TEXT_PLAIN) public Response delete(@Valid final DeleteRequest deleteRequest) { LOGGER.debug("Received deprecated delete request"); MessageHash messageHash = Optional.of(deleteRequest) .map(DeleteRequest::getKey) .map(base64Decoder::decode) .map(MessageHash::new) .get(); transactionManager.delete(messageHash); return Response.status(Response.Status.OK).entity("Delete successful").build(); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy