From 7df57cb4d291ccc5ad2842ef20363d8096dc0bf0 Mon Sep 17 00:00:00 2001 From: djmil Date: Tue, 5 Sep 2023 17:41:13 +0200 Subject: [PATCH] Corda: add FlowResult class refactor ActionFlow --- .../states/GameProposalResolutionState.java | 21 +++++ .../java/djmil/cordacheckers/FlowResult.java | 23 +++++ .../gameproposal/ActionFlow.java | 91 +++++++++++-------- .../gameproposal/ActionFlowArgs.java | 14 ++- .../gameproposal/ActionResponder.java | 7 +- .../gameproposal/CreateFlow.java | 18 ++-- .../cordacheckers/gameproposal/ListFlow.java | 28 ++++-- 7 files changed, 138 insertions(+), 64 deletions(-) create mode 100644 corda/workflows/src/main/java/djmil/cordacheckers/FlowResult.java diff --git a/corda/contracts/src/main/java/djmil/cordacheckers/states/GameProposalResolutionState.java b/corda/contracts/src/main/java/djmil/cordacheckers/states/GameProposalResolutionState.java index 9a80b07..96573e6 100644 --- a/corda/contracts/src/main/java/djmil/cordacheckers/states/GameProposalResolutionState.java +++ b/corda/contracts/src/main/java/djmil/cordacheckers/states/GameProposalResolutionState.java @@ -6,6 +6,7 @@ import java.util.List; import djmil.cordacheckers.contracts.GameProposalContract; import net.corda.v5.base.annotations.ConstructorForDeserialization; import net.corda.v5.base.annotations.CordaSerializable; +import net.corda.v5.base.types.MemberX500Name; import net.corda.v5.ledger.utxo.BelongsToContract; import net.corda.v5.ledger.utxo.ContractState; @@ -22,6 +23,14 @@ public class GameProposalResolutionState implements ContractState { public final Resolution outcome; public final List participants; + public GameProposalResolutionState( + Resolution outcome, + GameProposalState gameProposal + ) { + this.outcome = outcome; + this.participants = gameProposal.getParticipants(); + } + @ConstructorForDeserialization public GameProposalResolutionState( Resolution outcome, @@ -38,5 +47,17 @@ public class GameProposalResolutionState implements ContractState { public List getParticipants() { return this.participants; } + + public MemberX500Name getRecipient(GameProposalState gameProposal) { + switch (outcome) { + case ACCEPT: + case REJECT: + return gameProposal.getSender(); + case CANCEL: + return gameProposal.getRecipient(); + default: + throw new RuntimeException("Unknown Resolution value: "+outcome.toString()); + } + } } diff --git a/corda/workflows/src/main/java/djmil/cordacheckers/FlowResult.java b/corda/workflows/src/main/java/djmil/cordacheckers/FlowResult.java new file mode 100644 index 0000000..5c995dc --- /dev/null +++ b/corda/workflows/src/main/java/djmil/cordacheckers/FlowResult.java @@ -0,0 +1,23 @@ +package djmil.cordacheckers; + +import net.corda.v5.application.marshalling.JsonMarshallingService; + +public class FlowResult { + public final Object successStatus; + public final String failureStatus; + + public FlowResult(Object success) { + this.successStatus = success; + this.failureStatus = null; + } + + public FlowResult(Exception exception) { + this.successStatus = null; + this.failureStatus = exception.getMessage(); + } + + public String toJsonEncodedString(JsonMarshallingService jsonMarshallingService) { + return jsonMarshallingService.format(this); + } + +} \ No newline at end of file diff --git a/corda/workflows/src/main/java/djmil/cordacheckers/gameproposal/ActionFlow.java b/corda/workflows/src/main/java/djmil/cordacheckers/gameproposal/ActionFlow.java index e1ac9b7..1d1e237 100644 --- a/corda/workflows/src/main/java/djmil/cordacheckers/gameproposal/ActionFlow.java +++ b/corda/workflows/src/main/java/djmil/cordacheckers/gameproposal/ActionFlow.java @@ -3,11 +3,16 @@ package djmil.cordacheckers.gameproposal; import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.UUID; +import java.util.stream.Collectors; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import djmil.cordacheckers.FlowResult; import djmil.cordacheckers.contracts.GameProposalContract; import djmil.cordacheckers.states.GameProposalResolutionState; import djmil.cordacheckers.states.GameProposalState; -import djmil.cordacheckers.states.GameProposalResolutionState.Resolution; import net.corda.v5.application.flows.ClientRequestBody; import net.corda.v5.application.flows.ClientStartableFlow; import net.corda.v5.application.flows.CordaInject; @@ -18,22 +23,21 @@ import net.corda.v5.application.messaging.FlowMessaging; import net.corda.v5.application.messaging.FlowSession; import net.corda.v5.base.annotations.Suspendable; import net.corda.v5.base.exceptions.CordaRuntimeException; +import net.corda.v5.base.types.MemberX500Name; import net.corda.v5.ledger.utxo.Command; import net.corda.v5.ledger.utxo.StateAndRef; -import net.corda.v5.ledger.utxo.TransactionState; import net.corda.v5.ledger.utxo.UtxoLedgerService; import net.corda.v5.ledger.utxo.transaction.UtxoSignedTransaction; import net.corda.v5.ledger.utxo.transaction.UtxoTransactionBuilder; -import static java.util.Map.entry; -import static java.util.stream.Collectors.toList; - import java.time.Duration; import java.time.Instant; @InitiatingFlow(protocol = "game-proposal-action") public class ActionFlow implements ClientStartableFlow { + private final static Logger log = LoggerFactory.getLogger(CreateFlow.class); + @CordaInject public JsonMarshallingService jsonMarshallingService; @@ -47,62 +51,77 @@ public class ActionFlow implements ClientStartableFlow { public MemberLookup memberLookup; private final static Map resoultion2command = Map.ofEntries( - entry(GameProposalResolutionState.Resolution.CANCEL, new GameProposalContract.Cancel()), - entry(GameProposalResolutionState.Resolution.REJECT, new GameProposalContract.Reject()), - entry(GameProposalResolutionState.Resolution.ACCEPT, new GameProposalContract.Accept()) + Map.entry(GameProposalResolutionState.Resolution.CANCEL, new GameProposalContract.Cancel()), + Map.entry(GameProposalResolutionState.Resolution.REJECT, new GameProposalContract.Reject()), + Map.entry(GameProposalResolutionState.Resolution.ACCEPT, new GameProposalContract.Accept()) ); @Override @Suspendable public String call(ClientRequestBody requestBody) { - ActionFlowArgs args = requestBody.getRequestBodyAs(jsonMarshallingService, ActionFlowArgs.class); + try { + ActionFlowArgs args = requestBody.getRequestBodyAs(jsonMarshallingService, ActionFlowArgs.class); + StateAndRef inputState = findUnconsumedGameProposalState(args.getGameProposalUuid()); + + GameProposalResolutionState outputState = new GameProposalResolutionState( + args.getAction(), + inputState.getState().getContractState() + ); + + String trxResult = doTrunsaction(inputState, outputState); + + return new FlowResult(trxResult).toJsonEncodedString(jsonMarshallingService); + } + catch (Exception e) { + log.warn("GameProposalAction flow failed to process utxo request body " + requestBody + + " because: " + e.getMessage()); + return new FlowResult(e).toJsonEncodedString(jsonMarshallingService); + } + } + + @Suspendable + private StateAndRef findUnconsumedGameProposalState (UUID gameProposalUuid) { /* - * Look up the latest unconsumed ChatState with the given id. - * Note, this code brings all unconsumed states back, then filters them. This is an - * inefficient way to perform this operation when there are a large number of chats + * Get list of all unconsumed aka 'actuve' GameProposalStates, then filter by UUID. + * Note, this is an inefficient way to perform this operation if there are a large + * number of 'active' GameProposals exists in storage. */ List> stateAndRefs = this.ledgerService - .findUnconsumedStatesByType(GameProposalState.class); - - List> stateAndRefsWithId = stateAndRefs + .findUnconsumedStatesByType(GameProposalState.class) .stream() - .filter(sar -> sar.getState().getContractState().getId().equals(args.gameProposalUuid)) - .collect(toList()); - if (stateAndRefsWithId.size() != 1) - throw new CordaRuntimeException("Multiple or zero GameProposal states with id " + args.gameProposalUuid + " found"); + .filter(sar -> sar.getState().getContractState().getId().equals(gameProposalUuid)) + .collect(Collectors.toList()); + + if (stateAndRefs.size() != 1) { + throw new CordaRuntimeException("Expected only one GameProposal state with id " + gameProposalUuid + + ", but found " + stateAndRefs.size()); + } - StateAndRef stateAndRef = stateAndRefsWithId.get(0); - TransactionState trxState = stateAndRef.getState(); - GameProposalState state = trxState.getContractState(); + return stateAndRefs.get(0); + } - GameProposalResolutionState outputState = new GameProposalResolutionState( - GameProposalResolutionState.Resolution.valueOf(args.action), - state.getParticipants() - ); - - /* - * Build draft trx - */ + @Suspendable + private String doTrunsaction(StateAndRef inputState, GameProposalResolutionState outputState) { UtxoTransactionBuilder txBuilder = ledgerService.createTransactionBuilder() - .setNotary(trxState.getNotaryName()) + .setNotary(inputState.getState().getNotaryName()) .setTimeWindowBetween(Instant.now(), Instant.now().plusMillis(Duration.ofDays(1).toMillis())) - .addInputState(stateAndRef.getRef()) + .addInputState(inputState.getRef()) .addOutputState(outputState) .addCommand(resoultion2command.get(outputState.outcome)) - .addSignatories(state.getParticipants()); + .addSignatories(outputState.getParticipants()); UtxoSignedTransaction signedTransaction = txBuilder.toSignedTransaction(); FlowSession session = flowMessaging.initiateFlow( - outputState.outcome == Resolution.CANCEL ? state.getRecipient() : state.getSender() // TODO: readability + outputState.getRecipient(inputState.getState().getContractState()) ); List sessionsList = Arrays.asList(session); ledgerService.finalize(signedTransaction, sessionsList); - return args.action+"ED"; // REJECT+ED + return outputState.getOutcome()+"ED"; // REJECT+ED } - + } diff --git a/corda/workflows/src/main/java/djmil/cordacheckers/gameproposal/ActionFlowArgs.java b/corda/workflows/src/main/java/djmil/cordacheckers/gameproposal/ActionFlowArgs.java index e15d9aa..38cd8e7 100644 --- a/corda/workflows/src/main/java/djmil/cordacheckers/gameproposal/ActionFlowArgs.java +++ b/corda/workflows/src/main/java/djmil/cordacheckers/gameproposal/ActionFlowArgs.java @@ -2,9 +2,12 @@ package djmil.cordacheckers.gameproposal; import java.util.UUID; +import djmil.cordacheckers.states.GameProposalResolutionState; +import djmil.cordacheckers.states.GameProposalResolutionState.Resolution; + public class ActionFlowArgs { - public final UUID gameProposalUuid; - public final String action; + private UUID gameProposalUuid; + private String action; // Serialisation service requires a default constructor public ActionFlowArgs() { @@ -12,4 +15,11 @@ public class ActionFlowArgs { this.action = null; } + public Resolution getAction() { + return GameProposalResolutionState.Resolution.valueOf(this.action); + } + + public UUID getGameProposalUuid() { + return this.gameProposalUuid; + } } diff --git a/corda/workflows/src/main/java/djmil/cordacheckers/gameproposal/ActionResponder.java b/corda/workflows/src/main/java/djmil/cordacheckers/gameproposal/ActionResponder.java index 1578922..c5fc06e 100644 --- a/corda/workflows/src/main/java/djmil/cordacheckers/gameproposal/ActionResponder.java +++ b/corda/workflows/src/main/java/djmil/cordacheckers/gameproposal/ActionResponder.java @@ -34,12 +34,9 @@ public class ActionResponder implements ResponderFlow { @Override public void call(FlowSession session) { try { - // Defines the lambda validator used in receiveFinality below. UtxoTransactionValidator txValidator = ledgerTransaction -> { GameProposalState gameProposal = (GameProposalState) ledgerTransaction.getInputContractStates().get(0); - // Uses checkForBannedWords() and checkMessageFromMatchesCounterparty() functions - // to check whether to sign the transaction. - + Command command = ledgerTransaction.getCommands(Command.class).get(0); if (!checkParticipants(gameProposal, session.getCounterparty(), command)) { @@ -68,7 +65,7 @@ public class ActionResponder implements ResponderFlow { @Suspendable Boolean checkParticipants(GameProposalState gameProposal, MemberX500Name counterpartyName, Command command) { MemberX500Name myName = memberLookup.myInfo().getName(); -log.info("Responder validation:\n command "+command+"\n me " + myName.toString() + "\n opponent " + counterpartyName.toString()); + if (command instanceof Reject || command instanceof Accept) { if (gameProposal.getRecipient().compareTo(counterpartyName) == 0 && gameProposal.getSender().compareTo(myName) == 0) diff --git a/corda/workflows/src/main/java/djmil/cordacheckers/gameproposal/CreateFlow.java b/corda/workflows/src/main/java/djmil/cordacheckers/gameproposal/CreateFlow.java index d346258..e29c6cf 100644 --- a/corda/workflows/src/main/java/djmil/cordacheckers/gameproposal/CreateFlow.java +++ b/corda/workflows/src/main/java/djmil/cordacheckers/gameproposal/CreateFlow.java @@ -3,6 +3,7 @@ package djmil.cordacheckers.gameproposal; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import djmil.cordacheckers.FlowResult; import djmil.cordacheckers.contracts.GameProposalContract; import djmil.cordacheckers.states.GameProposalState; import net.corda.v5.application.flows.ClientRequestBody; @@ -15,7 +16,6 @@ import net.corda.v5.application.membership.MemberLookup; import net.corda.v5.application.messaging.FlowMessaging; import net.corda.v5.application.messaging.FlowSession; import net.corda.v5.base.annotations.Suspendable; -import net.corda.v5.base.exceptions.CordaRuntimeException; import net.corda.v5.base.types.MemberX500Name; import net.corda.v5.ledger.common.NotaryLookup; import net.corda.v5.ledger.utxo.UtxoLedgerService; @@ -62,13 +62,14 @@ public class CreateFlow implements ClientStartableFlow{ log.info("flow: Create Game Proposal"); GameProposalState gameProposal = buildGameProposalStateFrom(requestBody); - String result = doTrunsaction(gameProposal); + String trxResult = doTrunsaction(gameProposal); - return result; + return new FlowResult(trxResult).toJsonEncodedString(jsonMarshallingService); } catch (Exception e) { - log.warn("CreateGameProposal flow failed to process utxo request body " + requestBody + " because: " + e.getMessage()); - throw new CordaRuntimeException(e.getMessage()); + log.warn("CreateGameProposal flow failed to process utxo request body " + requestBody + + " because: " + e.getMessage()); + return new FlowResult(e).toJsonEncodedString(jsonMarshallingService); } } @@ -116,12 +117,7 @@ public class CreateFlow implements ClientStartableFlow{ List sessionsList = Arrays.asList(session); - UtxoSignedTransaction finalizedSignedTransaction = ledgerService - .finalize(signedTransaction, sessionsList) - .getTransaction(); - - - // final String trxId = finalizedSignedTransaction.getId().toString(); + ledgerService.finalize(signedTransaction, sessionsList); return gameProposal.id.toString(); } diff --git a/corda/workflows/src/main/java/djmil/cordacheckers/gameproposal/ListFlow.java b/corda/workflows/src/main/java/djmil/cordacheckers/gameproposal/ListFlow.java index 21741aa..6dfb7cc 100644 --- a/corda/workflows/src/main/java/djmil/cordacheckers/gameproposal/ListFlow.java +++ b/corda/workflows/src/main/java/djmil/cordacheckers/gameproposal/ListFlow.java @@ -1,16 +1,18 @@ package djmil.cordacheckers.gameproposal; +import java.util.List; import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import djmil.cordacheckers.FlowResult; import djmil.cordacheckers.states.GameProposalState; import net.corda.v5.application.flows.ClientRequestBody; import net.corda.v5.application.flows.ClientStartableFlow; import net.corda.v5.application.flows.CordaInject; import net.corda.v5.application.marshalling.JsonMarshallingService; -import net.corda.v5.ledger.utxo.StateAndRef; +import net.corda.v5.base.annotations.Suspendable; import net.corda.v5.ledger.utxo.UtxoLedgerService; public class ListFlow implements ClientStartableFlow { @@ -23,19 +25,25 @@ public class ListFlow implements ClientStartableFlow { @CordaInject public JsonMarshallingService jsonMarshallingService; + @Suspendable @Override public String call(ClientRequestBody requestBody) { - log.info("ListChatsFlow.call() called"); + try { + log.info("ListChatsFlow.call() called"); - // Queries the VNode's vault for unconsumed states and converts the result to a serializable DTO. - java.util.List> states = utxoLedgerService.findUnconsumedStatesByType(GameProposalState.class); + // Queries the VNode's vault for unconsumed states and converts the resulting + // List> to a _serializable_ List DTO + List unconsumedGameProposaList = utxoLedgerService + .findUnconsumedStatesByType(GameProposalState.class) + .stream() + .map( stateAndRef -> new ListItem(stateAndRef.getState().getContractState()) ) + .collect(Collectors.toList()); - java.util.List results = states.stream().map( stateAndRef -> - new ListItem(stateAndRef.getState().getContractState()) - ).collect(Collectors.toList()); - - // Uses the JsonMarshallingService's format() function to serialize the DTO to Json. - return jsonMarshallingService.format(results); + return new FlowResult(unconsumedGameProposaList).toJsonEncodedString(jsonMarshallingService); + } catch (Exception e) { + log.warn("CreateGameProposal flow failed to process utxo request body " + requestBody + " because: " + e.getMessage()); + return new FlowResult(e).toJsonEncodedString(jsonMarshallingService); + } } }