Compare commits

..

No commits in common. "218482034d6d59ea100f93df276c3c35e30207fe" and "7a2a366dd51cba4368530dfe9c2158da382cfc2c" have entirely different histories.

17 changed files with 82 additions and 446 deletions

View File

@ -22,9 +22,7 @@ import djmil.cordacheckers.cordaclient.dao.VirtualNode;
import djmil.cordacheckers.cordaclient.dao.VirtualNodeList;
import djmil.cordacheckers.cordaclient.dao.flow.RequestBody;
import djmil.cordacheckers.cordaclient.dao.flow.ResponseBody;
import djmil.cordacheckers.cordaclient.dao.flow.arguments.CreateGameProposal;
import djmil.cordacheckers.cordaclient.dao.flow.arguments.Empty;
import djmil.cordacheckers.cordaclient.dao.flow.arguments.GameProposalAction;
@Service
public class CordaClient {
@ -65,63 +63,23 @@ public class CordaClient {
"list-" + UUID.randomUUID(),
"djmil.cordacheckers.gameproposal.ListFlow",
new Empty()
);
);
final String gameProposalsJsonString = cordaFlowExecute(
holdingIdentity,
requestBody
);
);
return gameProposalsJsonString;
}
public String createGameProposal(
public String sendGameProposal(
HoldingIdentity sender,
HoldingIdentity receiver,
Color receiverColor,
String message
) {
final CreateGameProposal createGameProposal = new CreateGameProposal(
receiver.x500Name(),
receiverColor,
message
);
final RequestBody requestBody = new RequestBody(
"create-" + UUID.randomUUID(),
"djmil.cordacheckers.gameproposal.CreateFlow",
createGameProposal
);
final String createdGameProposalUuid = cordaFlowExecute(
sender,
requestBody
);
return createdGameProposalUuid;
}
public String rejectGameProposal(
HoldingIdentity self,
String gameProposalUuid
) {
final GameProposalAction rejectGameProposal = new GameProposalAction(
gameProposalUuid,
GameProposalAction.Action.REJECT
);
final RequestBody requestBody = new RequestBody(
"reject-" + UUID.randomUUID(),
"djmil.cordacheckers.gameproposal.ActionFlow",
rejectGameProposal
);
final String createdGameProposalUuid = cordaFlowExecute(
self,
requestBody
);
return createdGameProposalUuid;
return "";
}
private String cordaFlowExecute(HoldingIdentity holdingIdentity, RequestBody requestBody) {

View File

@ -1,7 +1,5 @@
package djmil.cordacheckers.cordaclient.dao.flow.arguments;
import djmil.cordacheckers.cordaclient.dao.Color;
public record CreateGameProposal(String opponentName, Color opponentColor, String message) {
public record CreateGameProposal(String opponentName, String opponentColor, String additionalMessage) {
}

View File

@ -1,9 +0,0 @@
package djmil.cordacheckers.cordaclient.dao.flow.arguments;
public record GameProposalAction(String gameProposalUuid, Action action) {
public enum Action {
ACCEPT,
REJECT,
CANCEL
}
}

View File

@ -0,0 +1,12 @@
package djmil.cordacheckers.gameproposal;
import djmil.cordacheckers.cordaclient.dao.Color;
public record GameProposal(
String sender,
String recipient,
Color recipientColor,
String message)
{
}

View File

@ -1,6 +1,7 @@
package djmil.cordacheckers.gameproposal;
import java.net.URI;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
@ -11,9 +12,7 @@ import org.springframework.web.util.UriComponentsBuilder;
import djmil.cordacheckers.cordaclient.CordaClient;
import djmil.cordacheckers.cordaclient.dao.HoldingIdentity;
import djmil.cordacheckers.cordaclient.dao.flow.arguments.CreateGameProposal;
import djmil.cordacheckers.cordaclient.dao.Color;
import djmil.cordacheckers.cordaclient.dao.GameProposal;
import djmil.cordacheckers.user.HoldingIdentityResolver;
import djmil.cordacheckers.user.User;
@ -50,15 +49,17 @@ public class GameProposalController {
@PostMapping()
public ResponseEntity<Void> createGameProposal(
@AuthenticationPrincipal User sender,
@RequestBody CreateGameProposal gpRequest,
@RequestBody GameProposal gpRequest,
UriComponentsBuilder ucb
) {
//sender.get
final HoldingIdentity gpSender = sender.getHoldingIdentity();
// TODO: throw execption with custom type
final HoldingIdentity gpReceiver = holdingIdentityResolver.getByUsername(gpRequest.opponentName());
final Color gpReceiverColor = gpRequest.opponentColor();
final HoldingIdentity gpReceiver = holdingIdentityResolver.getByUsername(gpRequest.recipient());
final Color gpReceiverColor = gpRequest.recipientColor();
String newGameProposalUuid = cordaClient.createGameProposal(
String newGameProposalUuid = cordaClient.sendGameProposal(
gpSender,
gpReceiver,
gpReceiverColor,

View File

@ -1,10 +0,0 @@
package djmil.cordacheckers;
import org.junit.jupiter.api.Test;
public class GameProposalControllerTests {
@Test
void testFindAllUnconsumed() {
}
}

View File

@ -7,9 +7,7 @@ import org.springframework.boot.test.context.SpringBootTest;
import com.fasterxml.jackson.core.JsonProcessingException;
import djmil.cordacheckers.cordaclient.dao.Color;
import djmil.cordacheckers.cordaclient.dao.VirtualNode;
import djmil.cordacheckers.user.HoldingIdentityResolver;
import static org.assertj.core.api.Assertions.assertThat;
@ -33,67 +31,11 @@ public class CordaClientTest {
}
@Test
void testGameProposalList() throws JsonProcessingException {
void testListGameProposals() throws JsonProcessingException {
String resp = cordaClient.listGameProposals(
holdingIdentityResolver.getByUsername("alice"));
holdingIdentityResolver.getByCommonName("alice"));
System.out.println("testListGameProposals "+ resp);
}
@Test
void testGemeProposalCreate() {
final String gpSender = "alice";
final String gpReceiver = "bob";
final Color gpReceiverColor = Color.WHITE;
final String gpMessage = "GameProposal create test";
final String gpUuid = cordaClient.createGameProposal(
holdingIdentityResolver.getByUsername(gpSender),
holdingIdentityResolver.getByUsername(gpReceiver),
gpReceiverColor,
gpMessage
);
String listResSender = cordaClient.listGameProposals(
holdingIdentityResolver.getByUsername(gpSender));
String listResReceiver = cordaClient.listGameProposals(
holdingIdentityResolver.getByUsername(gpReceiver));
assertThat(listResSender).contains(gpUuid);
assertThat(listResReceiver).contains(gpUuid);
}
@Test
void testGemeProposalReject() {
final String gpSender = "alice";
final String gpReceiver = "bob";
final Color gpReceiverColor = Color.WHITE;
final String gpMessage = "GameProposal create test";
final String gpUuid = cordaClient.createGameProposal(
holdingIdentityResolver.getByUsername(gpSender),
holdingIdentityResolver.getByUsername(gpReceiver),
gpReceiverColor,
gpMessage
);
System.out.println("Create GP UUID "+ gpUuid);
String listResSender = cordaClient.listGameProposals(
holdingIdentityResolver.getByUsername(gpSender));
String listResReceiver = cordaClient.listGameProposals(
holdingIdentityResolver.getByUsername(gpReceiver));
assertThat(listResSender).contains(gpUuid);
assertThat(listResReceiver).contains(gpUuid);
final String rejectRes = cordaClient.rejectGameProposal(
holdingIdentityResolver.getByUsername(gpReceiver),
"1ed70601-c79a-486d-b907-8537f317083a"
);
assertThat(rejectRes).isEqualToIgnoringCase("REJECTED");
}
}

View File

@ -3,7 +3,6 @@ package djmil.cordacheckers.contracts;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import djmil.cordacheckers.states.GameProposalResolutionState;
import djmil.cordacheckers.states.GameProposalState;
import net.corda.v5.base.exceptions.CordaRuntimeException;
import net.corda.v5.ledger.utxo.Command;
@ -16,7 +15,7 @@ public class GameProposalContract implements net.corda.v5.ledger.utxo.Contract {
public static class Create implements Command { }
public static class Accept implements Command { }
public static class Reject implements Command { }
public static class Cancel implements Command { }
public static class Cancle implements Command { }
@Override
public void verify(UtxoLedgerTransaction trx) {
@ -32,7 +31,7 @@ public class GameProposalContract implements net.corda.v5.ledger.utxo.Contract {
GameProposalState outputState = trx.getOutputStates(GameProposalState.class).get(0);
requireThat(outputState != null, CREATE_OUTPUT_STATE);
requireThat(outputState.getRecipientColor() != null, NON_NULL_RECIPIENT_COLOR);
requireThat(outputState.getYouPlayAs() != null, CREATE_NOT_NULL_YOU_PLAY_AS);
} else
if (command instanceof Accept) {
// TODO outputState -> Game
@ -40,27 +39,14 @@ public class GameProposalContract implements net.corda.v5.ledger.utxo.Contract {
} else
if (command instanceof Reject) {
requireThat(trx.getInputContractStates().size() == 1, REJECT_INPUT_STATE);
requireThat(trx.getOutputContractStates().size() == 1, REJECT_OUTPUT_STATE);
requireThat(trx.getOutputContractStates().isEmpty(), REJECT_OUTPUT_STATE);
GameProposalState inputState = trx.getInputStates(GameProposalState.class).get(0);
requireThat(inputState != null, REJECT_INPUT_STATE);
GameProposalResolutionState outputState = trx.getOutputStates(GameProposalResolutionState.class).get(0);
requireThat(outputState != null, REJECT_OUTPUT_STATE);
requireThat(outputState.outcome == GameProposalResolutionState.Resolution.REJECT, REJECT_OUTPUT_OUTCOME);
} else
if (command instanceof Cancel) {
requireThat(trx.getInputContractStates().size() == 1, CANCEL_INPUT_STATE);
requireThat(trx.getOutputContractStates().size() == 1, CANCEL_OUTPUT_STATE);
GameProposalState inputState = trx.getInputStates(GameProposalState.class).get(0);
requireThat(inputState != null, CANCEL_INPUT_STATE);
GameProposalResolutionState outputState = trx.getOutputStates(GameProposalResolutionState.class).get(0);
requireThat(outputState != null, CANCEL_OUTPUT_STATE);
requireThat(outputState.outcome == GameProposalResolutionState.Resolution.CANCEL, CANCEL_OUTPUT_OUTCOME);
if (command instanceof Cancle) {
// TODO cancle game state
throw new CordaRuntimeException("Unimplemented!");
} else {
throw new CordaRuntimeException(UNKNOWN_COMMAND);
}
@ -77,13 +63,9 @@ public class GameProposalContract implements net.corda.v5.ledger.utxo.Contract {
static final String CREATE_INPUT_STATE = "Create command should have no input states";
static final String CREATE_OUTPUT_STATE = "Create command should output exactly one GameProposal state";
static final String NON_NULL_RECIPIENT_COLOR = "GameProposal.recipientColor field can not be null";
static final String CREATE_NOT_NULL_YOU_PLAY_AS = "GameProposal.youPlayAs field can not be null";
static final String REJECT_INPUT_STATE = "Reject command should have exactly one GameProposal input state";
static final String REJECT_OUTPUT_STATE = "Reject command should have exactly one GameProposalResolution output states";
static final String REJECT_OUTPUT_OUTCOME = "Reject output state should have Resolution value set to REJECT";
static final String REJECT_INPUT_STATE = "Reject command should have exactly one GameProposal state";
static final String REJECT_OUTPUT_STATE = "Reject command should have no output states";
static final String CANCEL_INPUT_STATE = "Cancel command should have exactly one GameProposal input state";
static final String CANCEL_OUTPUT_STATE = "Cancel command should have exactly one GameProposalResolution output states";
static final String CANCEL_OUTPUT_OUTCOME = "Cancel output state should have Resolution value set to CANCEL";
}

View File

@ -1,42 +0,0 @@
package djmil.cordacheckers.states;
import java.security.PublicKey;
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.ledger.utxo.BelongsToContract;
import net.corda.v5.ledger.utxo.ContractState;
@BelongsToContract(GameProposalContract.class)
public class GameProposalResolutionState implements ContractState {
@CordaSerializable
public enum Resolution {
ACCEPT,
REJECT,
CANCEL
}
public final Resolution outcome;
public final List<PublicKey> participants;
@ConstructorForDeserialization
public GameProposalResolutionState(
Resolution outcome,
List<PublicKey> participants
) {
this.outcome = outcome;
this.participants = participants;
}
public Resolution getOutcome() {
return outcome;
}
public List<PublicKey> getParticipants() {
return this.participants;
}
}

View File

@ -22,25 +22,24 @@ public class GameProposalState implements ContractState {
public final MemberX500Name sender;
public final MemberX500Name recipient;
public final Color recipientColor;
public final String message;
public final Color youPlayAs;
public final String additionalMessage;
public final UUID id;
public final List<PublicKey> participants;
// Allows serialisation and to use a specified UUID
@ConstructorForDeserialization
public GameProposalState(
MemberX500Name sender,
MemberX500Name recipient,
Color recipientColor,
String message,
UUID id,
List<PublicKey> participants
) {
MemberX500Name sender,
MemberX500Name recipient,
Color youPlayAs,
String additionalMessage,
UUID id,
List<PublicKey> participants) {
this.sender = sender;
this.recipient = recipient;
this.recipientColor = recipientColor;
this.message = message;
this.youPlayAs = youPlayAs;
this.additionalMessage = additionalMessage;
this.id = id;
this.participants = participants;
}
@ -53,12 +52,12 @@ public class GameProposalState implements ContractState {
return recipient;
}
public Color getRecipientColor() {
return recipientColor;
public Color getYouPlayAs() {
return youPlayAs;
}
public String getMessage() {
return message;
public String getAdditionalMessage() {
return additionalMessage;
}
public UUID getId() {
@ -68,4 +67,12 @@ public class GameProposalState implements ContractState {
public List<PublicKey> getParticipants() {
return participants;
}
public String getRecipientCommonName() {
return recipient == null ? "" : recipient.getCommonName();
}
public String getSenderCommonName() {
return sender == null ? "" : sender.getCommonName();
}
}

View File

@ -1,108 +0,0 @@
package djmil.cordacheckers.gameproposal;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
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;
import net.corda.v5.application.flows.InitiatingFlow;
import net.corda.v5.application.marshalling.JsonMarshallingService;
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.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 {
@CordaInject
public JsonMarshallingService jsonMarshallingService;
@CordaInject
public UtxoLedgerService ledgerService;
@CordaInject
public FlowMessaging flowMessaging;
@CordaInject
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())
);
@Override
@Suspendable
public String call(ClientRequestBody requestBody) {
ActionFlowArgs args = requestBody.getRequestBodyAs(jsonMarshallingService, ActionFlowArgs.class);
/*
* 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
*/
List<StateAndRef<GameProposalState>> stateAndRefs = this.ledgerService
.findUnconsumedStatesByType(GameProposalState.class);
List<StateAndRef<GameProposalState>> stateAndRefsWithId = stateAndRefs
.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");
StateAndRef<GameProposalState> stateAndRef = stateAndRefsWithId.get(0);
TransactionState<GameProposalState> trxState = stateAndRef.getState();
GameProposalState state = trxState.getContractState();
GameProposalResolutionState outputState = new GameProposalResolutionState(
GameProposalResolutionState.Resolution.valueOf(args.action),
state.getParticipants()
);
/*
* Build draft trx
*/
UtxoTransactionBuilder txBuilder = ledgerService.createTransactionBuilder()
.setNotary(trxState.getNotaryName())
.setTimeWindowBetween(Instant.now(), Instant.now().plusMillis(Duration.ofDays(1).toMillis()))
.addInputState(stateAndRef.getRef())
.addOutputState(outputState)
.addCommand(resoultion2command.get(outputState.outcome))
.addSignatories(state.getParticipants());
UtxoSignedTransaction signedTransaction = txBuilder.toSignedTransaction();
FlowSession session = flowMessaging.initiateFlow(
outputState.outcome == Resolution.CANCEL ? state.getRecipient() : state.getSender() // TODO: readability
);
List<FlowSession> sessionsList = Arrays.asList(session);
ledgerService.finalize(signedTransaction, sessionsList);
return args.action+"ED"; // REJECT+ED
}
}

View File

@ -1,15 +0,0 @@
package djmil.cordacheckers.gameproposal;
import java.util.UUID;
public class ActionFlowArgs {
public final UUID gameProposalUuid;
public final String action;
// Serialisation service requires a default constructor
public ActionFlowArgs() {
this.gameProposalUuid = null;
this.action = null;
}
}

View File

@ -1,86 +0,0 @@
package djmil.cordacheckers.gameproposal;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import djmil.cordacheckers.contracts.GameProposalContract.Accept;
import djmil.cordacheckers.contracts.GameProposalContract.Cancel;
import djmil.cordacheckers.contracts.GameProposalContract.Reject;
import djmil.cordacheckers.states.GameProposalState;
import net.corda.v5.application.flows.CordaInject;
import net.corda.v5.application.flows.InitiatedBy;
import net.corda.v5.application.flows.ResponderFlow;
import net.corda.v5.application.membership.MemberLookup;
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.UtxoLedgerService;
import net.corda.v5.ledger.utxo.transaction.UtxoSignedTransaction;
import net.corda.v5.ledger.utxo.transaction.UtxoTransactionValidator;
@InitiatedBy(protocol = "game-proposal-action")
public class ActionResponder implements ResponderFlow {
private final static Logger log = LoggerFactory.getLogger(CreateResponder.class);
@CordaInject
public MemberLookup memberLookup;
@CordaInject
public UtxoLedgerService utxoLedgerService;
@Suspendable
@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)) {
throw new CordaRuntimeException("Failed verification");
}
log.info("Verified the transaction - " + ledgerTransaction.getId());
};
// Calls receiveFinality() function which provides the responder to the finalise() function
// in the Initiating Flow. Accepts a lambda validator containing the business logic to decide whether
// responder should sign the Transaction.
UtxoSignedTransaction finalizedSignedTransaction = utxoLedgerService
.receiveFinality(session, txValidator)
.getTransaction();
log.info("Finished responder flow - " + finalizedSignedTransaction.getId());
}
// Soft fails the flow and log the exception.
catch(Exception e)
{
log.warn("Exceptionally finished responder flow", e);
}
}
@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)
return true;
}
if (command instanceof Cancel) {
if (gameProposal.getRecipient().compareTo(myName) == 0 &&
gameProposal.getSender().compareTo(counterpartyName) == 0)
return true;
}
return false;
}
}

View File

@ -92,8 +92,8 @@ public class CreateFlow implements ClientStartableFlow{
return new GameProposalState(
myInfo.getName(),
opponentInfo.getName(),
opponentColor,
args.message,
GameProposalState.Color.valueOf(args.opponentColor),
args.additionalMessage,
UUID.randomUUID(),
Arrays.asList(myInfo.getLedgerKeys().get(0), opponentInfo.getLedgerKeys().get(0))
);
@ -120,9 +120,6 @@ public class CreateFlow implements ClientStartableFlow{
.finalize(signedTransaction, sessionsList)
.getTransaction();
// final String trxId = finalizedSignedTransaction.getId().toString();
return gameProposal.id.toString();
return finalizedSignedTransaction.getId().toString();
}
}

View File

@ -3,12 +3,16 @@ package djmil.cordacheckers.gameproposal;
public class CreateFlowArgs {
public final String opponentName;
public final String opponentColor;
public final String message;
public final String additionalMessage;
public CreateFlowArgs(String opponentName, String opponentColor, String additionalMessage) {
this.opponentName = opponentName;
this.opponentColor = opponentColor;
this.additionalMessage = additionalMessage;
}
// Serialisation service requires a default constructor
public CreateFlowArgs() {
opponentName = null;
opponentColor = null;
message = null;
public CreateFlowArgs() {
this(null, null, null);
}
}

View File

@ -12,24 +12,24 @@ import djmil.cordacheckers.states.GameProposalState;
public class ListItem {
public final String sender;
public final String recipient;
public final String recipientColor;
public final String message;
public final String youPlayAs;
public final String additionalMessage;
public final UUID id;
// Serialisation service requires a default constructor
public ListItem() {
this.sender = null;
this.recipient = null;
this.recipientColor = null;
this.message = null;
this.youPlayAs = null;
this.additionalMessage = null;
this.id = null;
}
public ListItem(GameProposalState state) {
this.sender = state.getSender().getCommonName();
this.recipient = state.getRecipient().getCommonName();
this.recipientColor = state.getRecipientColor().name();
this.message = state.getMessage();
this.sender = state.getSenderCommonName();
this.recipient = state.getRecipientCommonName();
this.youPlayAs = state.getYouPlayAs().name();
this.additionalMessage = state.getAdditionalMessage();
this.id = state.getId();
}
}

View File

@ -0,0 +1,5 @@
package djmil.cordacheckers.gameproposal;
public class RejectFlow {
}