com.quorum.tessera.p2p.TransactionResource Maven / Gradle / Ivy
package com.quorum.tessera.p2p;
import static jakarta.ws.rs.core.MediaType.*;
import static java.util.Collections.emptyList;
import com.quorum.tessera.base64.Base64Codec;
import com.quorum.tessera.data.MessageHash;
import com.quorum.tessera.enclave.EncodedPayloadCodec;
import com.quorum.tessera.enclave.PayloadEncoder;
import com.quorum.tessera.encryption.PublicKey;
import com.quorum.tessera.p2p.recovery.ResendBatchRequest;
import com.quorum.tessera.p2p.resend.ResendRequest;
import com.quorum.tessera.recovery.resend.ResendBatchResponse;
import com.quorum.tessera.recovery.workflow.BatchResendManager;
import com.quorum.tessera.recovery.workflow.LegacyResendManager;
import com.quorum.tessera.shared.Constants;
import com.quorum.tessera.transaction.TransactionManager;
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.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.ws.rs.*;
import jakarta.ws.rs.core.Response;
import java.util.*;
import java.util.stream.Collectors;
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 = "peer-to-peer")
@Path("/")
public class TransactionResource {
private static final Logger LOGGER = LoggerFactory.getLogger(TransactionResource.class);
private final TransactionManager transactionManager;
private final BatchResendManager batchResendManager;
private final LegacyResendManager legacyResendManager;
public TransactionResource(
final TransactionManager transactionManager,
final BatchResendManager batchResendManager,
final LegacyResendManager legacyResendManager) {
this.transactionManager = Objects.requireNonNull(transactionManager);
this.batchResendManager = Objects.requireNonNull(batchResendManager);
this.legacyResendManager = Objects.requireNonNull(legacyResendManager);
}
@Operation(
summary = "/resend",
operationId = "requestPayloadResend",
description =
"initiate resend of either an INDIVIDUAL transaction or ALL transactions involving a given public key")
@ApiResponse(
responseCode = "200",
description = "resent payload",
content =
@Content(
array =
@ArraySchema(
schema =
@Schema(
description =
"empty if request was for ALL; else the encoded INDIVIDUAL transaction",
type = "string",
format = "byte"))))
@POST
@Path("resend")
@Consumes(APPLICATION_JSON)
@Produces(TEXT_PLAIN)
public Response resend(@Valid @NotNull final ResendRequest resendRequest) {
LOGGER.debug("Received resend request");
final PayloadEncoder payloadEncoder = PayloadEncoder.create(EncodedPayloadCodec.LEGACY);
final PublicKey recipient =
Optional.of(resendRequest)
.map(ResendRequest::getPublicKey)
.map(Base64Codec.create()::decode)
.map(PublicKey::from)
.get();
final MessageHash transactionHash =
Optional.of(resendRequest)
.map(ResendRequest::getKey)
.map(Base64.getDecoder()::decode)
.map(MessageHash::new)
.orElse(null);
final com.quorum.tessera.recovery.resend.ResendRequest request =
com.quorum.tessera.recovery.resend.ResendRequest.Builder.create()
.withType(
com.quorum.tessera.recovery.resend.ResendRequest.ResendRequestType.valueOf(
resendRequest.getType()))
.withRecipient(recipient)
.withHash(transactionHash)
.build();
final com.quorum.tessera.recovery.resend.ResendResponse response =
legacyResendManager.resend(request);
final Response.ResponseBuilder builder = Response.ok();
Optional.ofNullable(response.getPayload())
.map(payloadEncoder::encode)
.ifPresent(builder::entity);
return builder.build();
}
@Operation(
summary = "/resendBatch",
operationId = "requestPayloadBatchResend",
description = "initiate resend of all transactions for a given public key in batches")
@ApiResponse(
responseCode = "200",
description = "count of total transactions being resent",
content =
@Content(
schema =
@Schema(
implementation = com.quorum.tessera.p2p.recovery.ResendBatchResponse.class)))
@POST
@Path("resendBatch")
@Consumes(APPLICATION_JSON)
@Produces(APPLICATION_JSON)
public Response resendBatch(@Valid @NotNull final ResendBatchRequest resendBatchRequest) {
LOGGER.debug("Received resend request");
final com.quorum.tessera.recovery.resend.ResendBatchRequest request =
com.quorum.tessera.recovery.resend.ResendBatchRequest.Builder.create()
.withPublicKey(resendBatchRequest.getPublicKey())
.withBatchSize(resendBatchRequest.getBatchSize())
.build();
final ResendBatchResponse response = batchResendManager.resendBatch(request);
final com.quorum.tessera.p2p.recovery.ResendBatchResponse responseEntity =
new com.quorum.tessera.p2p.recovery.ResendBatchResponse();
responseEntity.setTotal(response.getTotal());
final Response.ResponseBuilder builder = Response.status(Response.Status.OK);
builder.entity(responseEntity);
return builder.build();
}
// path push is overloaded (RecoveryResource & TransactionResource); swagger cannot handle
// situations like this so this operation documents both
@Operation(
summary = "/push",
operationId = "pushPayload",
description = "store encoded payload to the server's database")
@ApiResponse(
responseCode = "201",
description = "hash of encoded payload",
content =
@Content(
mediaType = TEXT_PLAIN,
schema =
@Schema(
description = "hash of encrypted payload",
type = "string",
format = "base64")))
@ApiResponse(
responseCode = "403",
description =
"server is in recovery mode and encoded payload is not a Standard Private transaction")
@POST
@Path("push")
@Consumes(APPLICATION_OCTET_STREAM)
public Response push(
@Schema(description = "encoded payload") final byte[] payload,
@HeaderParam(Constants.API_VERSION_HEADER)
@Parameter(
description = "client's supported API versions",
array = @ArraySchema(schema = @Schema(type = "string")))
final List headers) {
LOGGER.debug("Received push request");
final Set versions =
Optional.ofNullable(headers).orElse(emptyList()).stream()
.filter(Objects::nonNull)
.flatMap(v -> Arrays.stream(v.split(",")))
.collect(Collectors.toSet());
final EncodedPayloadCodec codec = EncodedPayloadCodec.getPreferredCodec(versions);
final PayloadEncoder payloadEncoder = PayloadEncoder.create(codec);
final MessageHash messageHash = transactionManager.storePayload(payloadEncoder.decode(payload));
LOGGER.debug("Push request generated hash {}", messageHash);
return Response.status(Response.Status.CREATED).entity(Objects.toString(messageHash)).build();
}
}