Corda: add FlowResult class

refactor ActionFlow
This commit is contained in:
djmil 2023-09-05 17:41:13 +02:00
parent 218482034d
commit 7df57cb4d2
7 changed files with 138 additions and 64 deletions

View File

@ -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<PublicKey> participants;
public GameProposalResolutionState(
Resolution outcome,
GameProposalState gameProposal
) {
this.outcome = outcome;
this.participants = gameProposal.getParticipants();
}
@ConstructorForDeserialization
public GameProposalResolutionState(
Resolution outcome,
@ -39,4 +48,16 @@ public class GameProposalResolutionState implements ContractState {
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());
}
}
}

View File

@ -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);
}
}

View File

@ -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<GameProposalResolutionState.Resolution, Command> 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<GameProposalState> 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<GameProposalState> 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<StateAndRef<GameProposalState>> stateAndRefs = this.ledgerService
.findUnconsumedStatesByType(GameProposalState.class);
List<StateAndRef<GameProposalState>> 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());
StateAndRef<GameProposalState> stateAndRef = stateAndRefsWithId.get(0);
TransactionState<GameProposalState> trxState = stateAndRef.getState();
GameProposalState state = trxState.getContractState();
if (stateAndRefs.size() != 1) {
throw new CordaRuntimeException("Expected only one GameProposal state with id " + gameProposalUuid +
", but found " + stateAndRefs.size());
}
GameProposalResolutionState outputState = new GameProposalResolutionState(
GameProposalResolutionState.Resolution.valueOf(args.action),
state.getParticipants()
);
return stateAndRefs.get(0);
}
/*
* Build draft trx
*/
@Suspendable
private String doTrunsaction(StateAndRef<GameProposalState> 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<FlowSession> sessionsList = Arrays.asList(session);
ledgerService.finalize(signedTransaction, sessionsList);
return args.action+"ED"; // REJECT+ED
return outputState.getOutcome()+"ED"; // REJECT+ED
}
}

View File

@ -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;
}
}

View File

@ -34,11 +34,8 @@ 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);
@ -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)

View File

@ -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<FlowSession> 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();
}

View File

@ -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<StateAndRef<GameProposalState>> states = utxoLedgerService.findUnconsumedStatesByType(GameProposalState.class);
// Queries the VNode's vault for unconsumed states and converts the resulting
// List<StateAndRef<GameProposalState>> to a _serializable_ List<ListItem> DTO
List<ListItem> unconsumedGameProposaList = utxoLedgerService
.findUnconsumedStatesByType(GameProposalState.class)
.stream()
.map( stateAndRef -> new ListItem(stateAndRef.getState().getContractState()) )
.collect(Collectors.toList());
java.util.List<ListItem> 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);
}
}
}