Skip to content

Commit 43fd320

Browse files
Mateusz Czeladkaclaude
andcommitted
refactor: extract OperationService interface and add comprehensive documentation
Separate OperationService into interface and implementation (OperationServiceImpl) following best practices for dependency injection and testability. Add detailed JavaDoc documentation explaining operation processing flow, signer determination logic, and governance operations support. Changes: - Create OperationService interface with public method signatures - Move implementation to OperationServiceImpl with detailed method documentation - Document governance operations processing (DRep vote delegation, pool governance votes) - Document signer extraction logic for pool, staking, and regular operations - Rename OperationServiceTest → OperationServiceImplTest for consistency - Update test instantiation to use OperationServiceImpl 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 27004ba commit 43fd320

File tree

5 files changed

+643
-388
lines changed

5 files changed

+643
-388
lines changed

api/src/main/java/org/cardanofoundation/rosetta/api/construction/service/OperationService.java

Lines changed: 46 additions & 177 deletions
Original file line numberDiff line numberDiff line change
@@ -2,191 +2,60 @@
22

33
import co.nstant.in.cbor.CborException;
44
import com.bloxbean.cardano.client.common.model.Network;
5-
import com.bloxbean.cardano.client.crypto.bip32.key.HdPublicKey;
65
import com.bloxbean.cardano.client.exception.CborDeserializationException;
76
import com.bloxbean.cardano.client.exception.CborSerializationException;
8-
import com.bloxbean.cardano.client.transaction.spec.TransactionBody;
9-
import com.bloxbean.cardano.client.transaction.spec.TransactionInput;
10-
import com.bloxbean.cardano.client.transaction.spec.TransactionOutput;
11-
import lombok.extern.slf4j.Slf4j;
12-
import org.apache.commons.lang3.ObjectUtils;
13-
import org.cardanofoundation.rosetta.common.enumeration.OperationType;
14-
import org.cardanofoundation.rosetta.common.exception.ExceptionFactory;
15-
import org.cardanofoundation.rosetta.common.model.cardano.pool.PoolRegistrationCertReturn;
167
import org.cardanofoundation.rosetta.common.model.cardano.transaction.TransactionData;
17-
import org.cardanofoundation.rosetta.common.model.cardano.transaction.TransactionExtraData;
18-
import org.cardanofoundation.rosetta.common.util.CardanoAddressUtils;
19-
import org.cardanofoundation.rosetta.common.util.Constants;
20-
import org.cardanofoundation.rosetta.common.util.ParseConstructionUtil;
21-
import org.cardanofoundation.rosetta.common.util.ValidateParseUtil;
228
import org.openapitools.client.model.Operation;
23-
import org.openapitools.client.model.OperationIdentifier;
24-
import org.openapitools.client.model.OperationMetadata;
25-
import org.springframework.stereotype.Service;
269

27-
import java.util.ArrayList;
28-
import java.util.Collections;
2910
import java.util.List;
30-
import java.util.Optional;
31-
32-
import static org.cardanofoundation.rosetta.common.util.Constants.OPERATION_TYPE_POOL_REGISTRATION;
33-
import static org.cardanofoundation.rosetta.common.util.Constants.OPERATION_TYPE_POOL_REGISTRATION_WITH_CERT;
3411

3512
/**
36-
* Service class to operate on Operations
13+
* Service interface for processing and extracting Rosetta operations from Cardano transactions.
14+
* <p>
15+
* This service handles the conversion between Cardano transaction structures and Rosetta API
16+
* operations, including inputs, outputs, certificates (staking, pool, governance), and withdrawals.
17+
* </p>
3718
*/
38-
@Slf4j
39-
@Service
40-
public class OperationService {
41-
42-
public List<Operation> getOperationsFromTransactionData(TransactionData data, Network network)
43-
throws CborDeserializationException, CborException, CborSerializationException {
44-
TransactionBody transactionBody = data.transactionBody();
45-
TransactionExtraData extraData = data.transactionExtraData();
46-
47-
List<Operation> operations = new ArrayList<>();
48-
fillInputOperations(transactionBody, extraData, operations);
49-
fillOutputOperations(transactionBody, operations);
50-
fillCertOperations(transactionBody, extraData, network, operations);
51-
fillWithdrawalOperations(transactionBody, extraData, network, operations);
52-
fillGovOperations(extraData, operations, network);
53-
54-
return operations;
55-
}
56-
57-
private void fillGovOperations(TransactionExtraData transactionBody, List<Operation> operations, Network network) {
58-
List<Operation> governanceOperations = transactionBody.operations().stream()
59-
.filter(o -> Constants.GOVERNANCE_OPERATIONS.contains(o.getType())
60-
).toList();
61-
62-
operations.addAll(governanceOperations);
63-
}
64-
65-
public List<String> getSignerFromOperation(Network network, Operation operation) {
66-
if (Constants.POOL_OPERATIONS.contains(operation.getType())) {
67-
return getPoolSigners(network, operation);
68-
}
69-
if (operation.getAccount() != null) {
70-
// org.openapitools.client.model.AccountIdentifier.getAddress() is always not null
71-
return Collections.singletonList(operation.getAccount().getAddress());
72-
}
73-
validateMetadataForStakingCredential(operation);
74-
HdPublicKey hdPublicKey =
75-
CardanoAddressUtils.publicKeyToHdPublicKey(operation.getMetadata().getStakingCredential());
76-
77-
return Collections.singletonList(
78-
CardanoAddressUtils.generateRewardAddress(network, hdPublicKey));
79-
}
80-
81-
private void fillInputOperations(TransactionBody transactionBody,
82-
TransactionExtraData extraData,
83-
List<Operation> operations) {
84-
List<TransactionInput> inputs = transactionBody.getInputs();
85-
log.info("[fillInputOperations] About to parse {} inputs", inputs.size());
86-
List<Operation> extraDataInputOperations = extraData.operations().stream()
87-
.filter(o -> o.getType().equals(OperationType.INPUT.getValue()))
88-
.toList();
89-
90-
for (int i = 0; i < inputs.size(); i++) {
91-
if (!extraDataInputOperations.isEmpty() && extraDataInputOperations.size() <= inputs.size()) {
92-
operations.add(extraDataInputOperations.get(i));
93-
} else { // fallback in case of no extra data input operations
94-
TransactionInput input = inputs.get(i);
95-
96-
Operation inputParsed = ParseConstructionUtil.transactionInputToOperation(input,
97-
(long) operations.size());
98-
99-
operations.add(inputParsed);
100-
}
101-
}
102-
}
103-
104-
private void fillOutputOperations(TransactionBody transactionBody, List<Operation> operations) {
105-
List<TransactionOutput> outputs = transactionBody.getOutputs();
106-
List<OperationIdentifier> relatedOperations = ParseConstructionUtil.getRelatedOperationsFromInputs(
107-
operations);
108-
109-
log.info("[parseOperationsFromTransactionBody] About to parse {} outputs", outputs.size());
110-
111-
for (TransactionOutput output : outputs) {
112-
Operation outputParsed = ParseConstructionUtil.transActionOutputToOperation(output,
113-
(long) operations.size(),
114-
relatedOperations);
115-
operations.add(outputParsed);
116-
}
117-
}
118-
119-
private void fillCertOperations(TransactionBody transactionBody, TransactionExtraData extraData,
120-
Network network, List<Operation> operations)
121-
throws CborException, CborSerializationException {
122-
List<Operation> certOps = extraData.operations().stream()
123-
.filter(o -> {
124-
return Constants.STAKE_POOL_OPERATIONS.contains(o.getType())
125-
|| OperationType.VOTE_DREP_DELEGATION.getValue().equals(o.getType());
126-
}
127-
).toList();
128-
129-
List<Operation> parsedCertOperations = ParseConstructionUtil.parseCertsToOperations(
130-
transactionBody, certOps, network);
131-
132-
operations.addAll(parsedCertOperations);
133-
}
134-
135-
private void fillWithdrawalOperations(TransactionBody transactionBody,
136-
TransactionExtraData extraData,
137-
Network network, List<Operation> operations) {
138-
List<Operation> withdrawalOps = extraData.operations().stream()
139-
.filter(o -> o.getType().equals(OperationType.WITHDRAWAL.getValue()))
140-
.toList();
141-
int withdrawalsCount = ObjectUtils.isEmpty(transactionBody.getWithdrawals()) ? 0
142-
: transactionBody.getWithdrawals().size();
143-
List<Operation> withdrawalsOperations = ParseConstructionUtil.parseWithdrawalsToOperations(
144-
withdrawalOps, withdrawalsCount, network);
145-
operations.addAll(withdrawalsOperations);
146-
}
147-
148-
private List<String> getPoolSigners(Network network, Operation operation) {
149-
List<String> signers = new ArrayList<>();
150-
switch (operation.getType()) {
151-
case OPERATION_TYPE_POOL_REGISTRATION -> {
152-
if (ValidateParseUtil.validateAddressPresence(operation)) {
153-
signers.add(operation.getAccount().getAddress());
154-
}
155-
Optional.ofNullable(operation.getMetadata())
156-
.map(OperationMetadata::getPoolRegistrationParams)
157-
.ifPresent(poolRegistrationParameters -> {
158-
signers.add(poolRegistrationParameters.getRewardAddress());
159-
signers.addAll(poolRegistrationParameters.getPoolOwners());
160-
});
161-
}
162-
case OPERATION_TYPE_POOL_REGISTRATION_WITH_CERT -> {
163-
String poolCertAsHex = Optional.ofNullable(operation.getMetadata())
164-
.map(OperationMetadata::getPoolRegistrationCert)
165-
.orElse(null);
166-
PoolRegistrationCertReturn dto = ValidateParseUtil.validateAndParsePoolRegistrationCert(
167-
network,
168-
poolCertAsHex,
169-
operation.getAccount() == null ? null : operation.getAccount().getAddress()
170-
);
171-
signers.addAll(dto.getAddress());
172-
}
173-
174-
// pool retirement case
175-
default -> {
176-
if (ValidateParseUtil.validateAddressPresence(operation)) {
177-
signers.add(operation.getAccount().getAddress());
178-
}
179-
}
180-
}
181-
log.info("[getPoolSigners] About to return {} signers for {} operation", signers.size(),
182-
operation.getType());
183-
184-
return signers;
185-
}
19+
public interface OperationService {
20+
21+
/**
22+
* Extracts and constructs a complete list of Rosetta operations from transaction data.
23+
* <p>
24+
* This method processes all operation types from a Cardano transaction, including:
25+
* <ul>
26+
* <li>Input operations - UTxO consumption</li>
27+
* <li>Output operations - UTxO creation</li>
28+
* <li>Certificate operations - Staking registrations, delegations, pool operations</li>
29+
* <li>Governance operations - DRep vote delegations, pool governance votes</li>
30+
* <li>Withdrawal operations - Staking reward withdrawals</li>
31+
* </ul>
32+
* </p>
33+
*
34+
* @param data the transaction data containing the transaction body and extra metadata
35+
* @param network the Cardano network (mainnet/testnet) for address generation
36+
* @return a list of Rosetta operations representing all transaction activities
37+
* @throws CborDeserializationException if CBOR deserialization fails
38+
* @throws CborException if CBOR processing encounters an error
39+
* @throws CborSerializationException if CBOR serialization fails
40+
*/
41+
List<Operation> getOperationsFromTransactionData(TransactionData data, Network network)
42+
throws CborDeserializationException, CborException, CborSerializationException;
43+
44+
/**
45+
* Determines the required signers for a given operation.
46+
* <p>
47+
* Different operation types require different signers:
48+
* <ul>
49+
* <li>Pool operations - Pool owners, reward address, and optionally payment address</li>
50+
* <li>Staking operations - Stake address derived from staking credential</li>
51+
* <li>Regular operations - Account address from the operation</li>
52+
* </ul>
53+
* </p>
54+
*
55+
* @param network the Cardano network for address generation
56+
* @param operation the operation to extract signers from
57+
* @return list of addresses that must sign this operation
58+
*/
59+
List<String> getSignerFromOperation(Network network, Operation operation);
18660

187-
private void validateMetadataForStakingCredential(Operation operation) {
188-
if (operation.getMetadata() == null) {
189-
throw ExceptionFactory.missingStakingKeyError();
190-
}
191-
}
19261
}

0 commit comments

Comments
 (0)