diff --git a/backend/src/main/java/djmil/cordacheckers/cordaclient/CordaClient.java b/backend/src/main/java/djmil/cordacheckers/cordaclient/CordaClient.java index b022588..43c2560 100644 --- a/backend/src/main/java/djmil/cordacheckers/cordaclient/CordaClient.java +++ b/backend/src/main/java/djmil/cordacheckers/cordaclient/CordaClient.java @@ -21,6 +21,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import djmil.cordacheckers.cordaclient.dao.HoldingIdentity; import djmil.cordacheckers.cordaclient.dao.Piece; import djmil.cordacheckers.cordaclient.dao.GameBoard; +import djmil.cordacheckers.cordaclient.dao.GameBoardCommand; import djmil.cordacheckers.cordaclient.dao.GameProposal; import djmil.cordacheckers.cordaclient.dao.VirtualNode; import djmil.cordacheckers.cordaclient.dao.VirtualNodeList; @@ -29,14 +30,13 @@ import djmil.cordacheckers.cordaclient.dao.flow.ResponseBody; import djmil.cordacheckers.cordaclient.dao.flow.arguments.GameProposalCreateReq; import djmil.cordacheckers.cordaclient.dao.flow.arguments.GameProposalCreateRes; import djmil.cordacheckers.cordaclient.dao.flow.arguments.GameProposalListRes; -import djmil.cordacheckers.cordaclient.dao.flow.arguments.GameProposalActionReq.Command; import djmil.cordacheckers.cordaclient.dao.flow.arguments.Empty; +import djmil.cordacheckers.cordaclient.dao.flow.arguments.GameBoardCommandReq; +import djmil.cordacheckers.cordaclient.dao.flow.arguments.GameBoardCommandRes; import djmil.cordacheckers.cordaclient.dao.flow.arguments.GameBoardListRes; -import djmil.cordacheckers.cordaclient.dao.flow.arguments.GameBoardMoveReq; -import djmil.cordacheckers.cordaclient.dao.flow.arguments.GameBoardMoveRes; -import djmil.cordacheckers.cordaclient.dao.flow.arguments.GameProposalActionAcceptRes; -import djmil.cordacheckers.cordaclient.dao.flow.arguments.GameProposalActionReq; -import djmil.cordacheckers.cordaclient.dao.flow.arguments.GameProposalActionRes; +import djmil.cordacheckers.cordaclient.dao.flow.arguments.GameProposalCommandAcceptRes; +import djmil.cordacheckers.cordaclient.dao.flow.arguments.GameProposalCommandReq; +import djmil.cordacheckers.cordaclient.dao.flow.arguments.GameProposalCommandRes; @Service public class CordaClient { @@ -130,16 +130,16 @@ public class CordaClient { final RequestBody requestBody = new RequestBody( "gp.reject-" +UUID.randomUUID(), "djmil.cordacheckers.gameproposal.CommandFlow", - new GameProposalActionReq( + new GameProposalCommandReq( gameProposalUuid.toString(), - Command.REJECT + GameProposalCommandReq.Command.REJECT ) ); - final GameProposalActionRes actionResult = cordaFlowExecute( + final GameProposalCommandRes actionResult = cordaFlowExecute( myHoldingIdentity, requestBody, - GameProposalActionRes.class + GameProposalCommandRes.class ); if (actionResult.failureStatus() != null) { @@ -150,23 +150,23 @@ public class CordaClient { return actionResult.successStatus(); } - public UUID gameProposalAccept( + public UUID gameProposalAccept( // TODO shall return newGameBoard HoldingIdentity myHoldingIdentity, UUID gameProposalUuid ) { final RequestBody requestBody = new RequestBody( "gp.accept-" +UUID.randomUUID(), "djmil.cordacheckers.gameproposal.CommandFlow", - new GameProposalActionReq( + new GameProposalCommandReq( gameProposalUuid.toString(), - Command.ACCEPT + GameProposalCommandReq.Command.ACCEPT ) ); - final GameProposalActionAcceptRes actionResult = cordaFlowExecute( + final GameProposalCommandAcceptRes actionResult = cordaFlowExecute( myHoldingIdentity, requestBody, - GameProposalActionAcceptRes.class + GameProposalCommandAcceptRes.class ); if (actionResult.failureStatus() != null) { @@ -199,24 +199,24 @@ public class CordaClient { return listFlowResult.successStatus(); } - public GameBoard gameBoardMove( + public GameBoard gameBoardCommand( HoldingIdentity myHoldingIdentity, UUID gameBoardUuid, - int from, int to + GameBoardCommand command ) { final RequestBody requestBody = new RequestBody( - "gb.move-" +UUID.randomUUID(), - "djmil.cordacheckers.gameboard.MoveFlow", - new GameBoardMoveReq( + "gb.command-" +command.getType() +UUID.randomUUID(), + "djmil.cordacheckers.gameboard.CommandFlow", + new GameBoardCommandReq( gameBoardUuid, - from, to + command ) ); - final GameBoardMoveRes moveResult = cordaFlowExecute( + final GameBoardCommandRes moveResult = cordaFlowExecute( myHoldingIdentity, requestBody, - GameBoardMoveRes.class + GameBoardCommandRes.class ); if (moveResult.failureStatus() != null) { diff --git a/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/GameBoard.java b/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/GameBoard.java index 5b76bc9..6c586f9 100644 --- a/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/GameBoard.java +++ b/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/GameBoard.java @@ -11,6 +11,7 @@ public record GameBoard( Piece.Color opponentColor, Boolean opponentMove, Map board, + GameBoardCommand previousCommand, String message, UUID id) implements CordaState { diff --git a/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/GameBoardCommand.java b/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/GameBoardCommand.java new file mode 100644 index 0000000..68a83af --- /dev/null +++ b/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/GameBoardCommand.java @@ -0,0 +1,48 @@ +package djmil.cordacheckers.cordaclient.dao; + +import java.util.List; + +public class GameBoardCommand { + public static enum Type { + MOVE, + SURRENDER, + REQUEST_DRAW, + REQUEST_VICTORY, + FINISH; // aka accept DRAW or VICTORY request + } + + private final Type type; + private final List move; // [0] from, [1] to + + public GameBoardCommand() { + this.type = null; + this.move = null; + } + + public GameBoardCommand(Type type) { + this.type = type; + this.move = null; + } + + public GameBoardCommand(List move) { + this.type = Type.MOVE; + this.move = move; + } + + public Type getType() { + return type; + } + + public List getMove() { + return move; + } + + @Override + public String toString() { + if (type == Type.MOVE) + return move.get(0) +"->" +move.get(1); + else + return type.name(); + } + +} diff --git a/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/arguments/GameBoardCommandReq.java b/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/arguments/GameBoardCommandReq.java new file mode 100644 index 0000000..025f4f4 --- /dev/null +++ b/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/arguments/GameBoardCommandReq.java @@ -0,0 +1,8 @@ +package djmil.cordacheckers.cordaclient.dao.flow.arguments; + +import java.util.UUID; +import djmil.cordacheckers.cordaclient.dao.GameBoardCommand; + +public record GameBoardCommandReq(UUID gameBoardUuid, GameBoardCommand command) { + +} diff --git a/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/arguments/GameBoardMoveRes.java b/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/arguments/GameBoardCommandRes.java similarity index 57% rename from backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/arguments/GameBoardMoveRes.java rename to backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/arguments/GameBoardCommandRes.java index 906bb9a..888ad14 100644 --- a/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/arguments/GameBoardMoveRes.java +++ b/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/arguments/GameBoardCommandRes.java @@ -2,6 +2,6 @@ package djmil.cordacheckers.cordaclient.dao.flow.arguments; import djmil.cordacheckers.cordaclient.dao.GameBoard; -public record GameBoardMoveRes (GameBoard successStatus, String failureStatus) { - +public record GameBoardCommandRes(GameBoard successStatus, String failureStatus) { + } diff --git a/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/arguments/GameBoardMoveReq.java b/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/arguments/GameBoardMoveReq.java deleted file mode 100644 index 00e9ac6..0000000 --- a/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/arguments/GameBoardMoveReq.java +++ /dev/null @@ -1,7 +0,0 @@ -package djmil.cordacheckers.cordaclient.dao.flow.arguments; - -import java.util.UUID; - -public record GameBoardMoveReq(UUID gameBoard, int from, int to) { - -} diff --git a/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/arguments/GameProposalActionAcceptRes.java b/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/arguments/GameProposalActionAcceptRes.java deleted file mode 100644 index 13a608b..0000000 --- a/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/arguments/GameProposalActionAcceptRes.java +++ /dev/null @@ -1,7 +0,0 @@ -package djmil.cordacheckers.cordaclient.dao.flow.arguments; - -import java.util.UUID; - -public record GameProposalActionAcceptRes(UUID successStatus, String transactionId, String failureStatus) { - -} diff --git a/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/arguments/GameProposalActionRes.java b/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/arguments/GameProposalActionRes.java deleted file mode 100644 index e290878..0000000 --- a/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/arguments/GameProposalActionRes.java +++ /dev/null @@ -1,5 +0,0 @@ -package djmil.cordacheckers.cordaclient.dao.flow.arguments; - -public record GameProposalActionRes(String successStatus, String transactionId, String failureStatus) { - -} diff --git a/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/arguments/GameProposalCommandAcceptRes.java b/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/arguments/GameProposalCommandAcceptRes.java new file mode 100644 index 0000000..45c19ce --- /dev/null +++ b/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/arguments/GameProposalCommandAcceptRes.java @@ -0,0 +1,7 @@ +package djmil.cordacheckers.cordaclient.dao.flow.arguments; + +import java.util.UUID; + +public record GameProposalCommandAcceptRes(UUID successStatus, String transactionId, String failureStatus) { + +} diff --git a/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/arguments/GameProposalActionReq.java b/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/arguments/GameProposalCommandReq.java similarity index 63% rename from backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/arguments/GameProposalActionReq.java rename to backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/arguments/GameProposalCommandReq.java index 5434581..1b7a92e 100644 --- a/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/arguments/GameProposalActionReq.java +++ b/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/arguments/GameProposalCommandReq.java @@ -1,6 +1,6 @@ package djmil.cordacheckers.cordaclient.dao.flow.arguments; -public record GameProposalActionReq(String gameProposalUuid, Command command) { +public record GameProposalCommandReq(String gameProposalUuid, Command command) { public enum Command { ACCEPT, REJECT, diff --git a/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/arguments/GameProposalCommandRes.java b/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/arguments/GameProposalCommandRes.java new file mode 100644 index 0000000..e98abdc --- /dev/null +++ b/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/arguments/GameProposalCommandRes.java @@ -0,0 +1,5 @@ +package djmil.cordacheckers.cordaclient.dao.flow.arguments; + +public record GameProposalCommandRes(String successStatus, String transactionId, String failureStatus) { + +} diff --git a/backend/src/test/java/djmil/cordacheckers/cordaclient/CordaClientTest.java b/backend/src/test/java/djmil/cordacheckers/cordaclient/CordaClientTest.java index 78d84ec..9de5fe0 100644 --- a/backend/src/test/java/djmil/cordacheckers/cordaclient/CordaClientTest.java +++ b/backend/src/test/java/djmil/cordacheckers/cordaclient/CordaClientTest.java @@ -17,6 +17,7 @@ import com.fasterxml.jackson.databind.JsonMappingException; import djmil.cordacheckers.cordaclient.dao.CordaState; import djmil.cordacheckers.cordaclient.dao.GameBoard; +import djmil.cordacheckers.cordaclient.dao.GameBoardCommand; import djmil.cordacheckers.cordaclient.dao.GameProposal; import djmil.cordacheckers.cordaclient.dao.Piece; import djmil.cordacheckers.cordaclient.dao.VirtualNode; @@ -56,8 +57,7 @@ public class CordaClientTest { holdingIdentityResolver.getByUsername(gpIssuer), holdingIdentityResolver.getByUsername(gpAcquier), gpAcquierColor, - gpMessage - ); + gpMessage); List gpListSender = cordaClient.gameProposalList( holdingIdentityResolver.getByUsername(gpIssuer)); @@ -87,8 +87,7 @@ public class CordaClientTest { holdingIdentityResolver.getByUsername(gpIssuer), holdingIdentityResolver.getByUsername(gpAcquier), gpReceiverColor, - gpMessage - ); + gpMessage); System.out.println("Create GP UUID "+ gpUuid); @@ -100,8 +99,7 @@ public class CordaClientTest { final String rejectRes = cordaClient.gameProposalReject( holdingIdentityResolver.getByUsername(gpAcquier), - gpUuid - ); + gpUuid); assertThat(rejectRes).isEqualToIgnoringCase("REJECTED"); @@ -147,8 +145,7 @@ public class CordaClientTest { final UUID newGameBoardId = cordaClient.gameProposalAccept( holdingIdentityResolver.getByUsername(gpAcquier), - gpUuid - ); + gpUuid); System.out.println("New GameBoard UUID "+newGameBoardId); @@ -186,6 +183,33 @@ public class CordaClientTest { System.out.println("GB list: " +gbList); } + @Test + void testGameBoardSurrender() throws JsonMappingException, JsonProcessingException { + final var hiAlice = holdingIdentityResolver.getByUsername("alice"); + final var hiBob = holdingIdentityResolver.getByUsername("bob"); + final var bobColor = Piece.Color.WHITE; + + final UUID gpUuid = cordaClient.gameProposalCreate( + hiAlice, hiBob, + bobColor, "GameBoard SURRENDER test" + ); + + System.out.println("New GameProposal UUID "+ gpUuid); + + final UUID newGameBoardId = cordaClient.gameProposalAccept( + hiBob, gpUuid + ); + + System.out.println("New GameBoard UUID "+ newGameBoardId); + + GameBoard gbSurrender = cordaClient.gameBoardCommand( + hiBob, newGameBoardId, + new GameBoardCommand(GameBoardCommand.Type.SURRENDER) + ); + + System.out.println("SURRENDER GB: "+gbSurrender); + } + private T findByUuid(List statesList, UUID uuid) { for (T state : statesList) { if (state.id().compareTo(uuid) == 0) diff --git a/corda/contracts/src/main/java/djmil/cordacheckers/UtxoLedgerTransactionUtil.java b/corda/contracts/src/main/java/djmil/cordacheckers/UtxoLedgerTransactionUtil.java deleted file mode 100644 index 3c9e341..0000000 --- a/corda/contracts/src/main/java/djmil/cordacheckers/UtxoLedgerTransactionUtil.java +++ /dev/null @@ -1,43 +0,0 @@ -package djmil.cordacheckers; - -import net.corda.v5.ledger.utxo.Command; -import net.corda.v5.ledger.utxo.ContractState; -import net.corda.v5.ledger.utxo.transaction.UtxoLedgerTransaction; - -public class UtxoLedgerTransactionUtil { - private final UtxoLedgerTransaction trx; - - public UtxoLedgerTransactionUtil(UtxoLedgerTransaction trx) { - this.trx = trx; - } - - public T getSingleCommand(Class clazz) { - return trx.getCommands(clazz) - .stream() - .reduce( (a, b) -> {throw new IllegalStateException(trx.getId() +EXPECTED_SINGLE_CLAZZ +clazz);} ) - .get(); - } - - public T getSingleReferenceState(Class clazz) { - return trx.getReferenceStates(clazz) - .stream() - .reduce( (a, b) -> {throw new IllegalStateException(trx.getId() +EXPECTED_SINGLE_CLAZZ +clazz);} ) - .get(); - } - - public T getSingleInputState(Class clazz) { - return trx.getInputStates(clazz) - .stream() - .reduce( (a, b) -> {throw new IllegalStateException(trx.getId() +EXPECTED_SINGLE_CLAZZ +clazz);} ) - .get(); - } - - public T getSingleOutputState(Class clazz) { - return trx.getOutputStates(clazz) - .stream() - .reduce( (a, b) -> {throw new IllegalStateException(trx.getId() +EXPECTED_SINGLE_CLAZZ +clazz);} ) - .get(); - } - - private static String EXPECTED_SINGLE_CLAZZ = ": expected single "; -} diff --git a/corda/contracts/src/main/java/djmil/cordacheckers/contracts/GameBoardCommand.java b/corda/contracts/src/main/java/djmil/cordacheckers/contracts/GameBoardCommand.java index 8e44d79..51f9640 100644 --- a/corda/contracts/src/main/java/djmil/cordacheckers/contracts/GameBoardCommand.java +++ b/corda/contracts/src/main/java/djmil/cordacheckers/contracts/GameBoardCommand.java @@ -7,15 +7,22 @@ import net.corda.v5.ledger.utxo.Command; public class GameBoardCommand implements Command { public static enum Type { + MOVE, SURRENDER, - DRAW, - VICTORY, - MOVE; + REQUEST_DRAW, + REQUEST_VICTORY, + FINISH; // aka accept DRAW or VICTORY request } private final Type type; private final List move; // [0] from, [1] to + // Serialisation service requires a default constructor + public GameBoardCommand() { + this.type = null; + this.move = null; + } + public GameBoardCommand(Type type) { if (type == Type.MOVE) throw new CordaRuntimeException (BAD_ACTIONMOVE_CONSTRUCTOR); @@ -34,12 +41,8 @@ public class GameBoardCommand implements Command { } public List getMove() { - if (type != Type.MOVE) - throw new CordaRuntimeException (NO_MOVES_FOR_ACTIONTYPE +type); - return this.move; } static final String BAD_ACTIONMOVE_CONSTRUCTOR = "Bad constructor for Action.MOVE"; - static final String NO_MOVES_FOR_ACTIONTYPE = ".getMove() not possible for "; } diff --git a/corda/contracts/src/main/java/djmil/cordacheckers/contracts/GameBoardContract.java b/corda/contracts/src/main/java/djmil/cordacheckers/contracts/GameBoardContract.java index a7eb889..63fac64 100644 --- a/corda/contracts/src/main/java/djmil/cordacheckers/contracts/GameBoardContract.java +++ b/corda/contracts/src/main/java/djmil/cordacheckers/contracts/GameBoardContract.java @@ -1,9 +1,13 @@ package djmil.cordacheckers.contracts; +import static djmil.cordacheckers.contracts.UtxoLedgerTransactionUtil.getSingleCommand; +import static djmil.cordacheckers.contracts.UtxoLedgerTransactionUtil.getSingleInputState; +import static djmil.cordacheckers.contracts.UtxoLedgerTransactionUtil.getSingleOutputState; +import static djmil.cordacheckers.contracts.UtxoLedgerTransactionUtil.getSingleReferenceState; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import djmil.cordacheckers.UtxoLedgerTransactionUtil; import djmil.cordacheckers.states.GameProposalState; import net.corda.v5.base.annotations.Suspendable; import net.corda.v5.base.exceptions.CordaRuntimeException; @@ -18,8 +22,7 @@ public class GameBoardContract implements net.corda.v5.ledger.utxo.Contract { public void verify(UtxoLedgerTransaction trx) { log.info("GameBoardContract.verify() called"); - final UtxoLedgerTransactionUtil trxUtil = new UtxoLedgerTransactionUtil(trx); - final Command command = trxUtil.getSingleCommand(Command.class); + final Command command = getSingleCommand(trx, Command.class); if (command instanceof GameProposalCommand) { log.info("GameBoardContract.verify() as GameProposalCommand "+(GameProposalCommand)command); @@ -27,13 +30,15 @@ public class GameBoardContract implements net.corda.v5.ledger.utxo.Contract { if (command instanceof GameBoardCommand) { log.info("GameBoardContract.verify() as GameBoardCommand "+((GameBoardCommand)command).getType()); switch (((GameBoardCommand)command).getType()) { + case MOVE: + break; case SURRENDER: break; - case DRAW: + case REQUEST_DRAW: break; - case VICTORY: + case REQUEST_VICTORY: break; - case MOVE: + case FINISH: break; } } else { @@ -43,14 +48,13 @@ public class GameBoardContract implements net.corda.v5.ledger.utxo.Contract { @Suspendable public static GameProposalState getReferanceGameProposalState(UtxoLedgerTransaction trx) { - final UtxoLedgerTransactionUtil trxUtil = new UtxoLedgerTransactionUtil(trx); - final Command command = trxUtil.getSingleCommand(Command.class); + final Command command = getSingleCommand(trx, Command.class); if (command instanceof GameProposalCommand && (GameProposalCommand)command == GameProposalCommand.ACCEPT) { - return trxUtil.getSingleInputState(GameProposalState.class); + return getSingleInputState(trx, GameProposalState.class); } else if (command instanceof GameBoardCommand) { - return trxUtil.getSingleReferenceState(GameProposalState.class); + return getSingleReferenceState(trx, GameProposalState.class); } throw new IllegalStateException(NO_REFERANCE_GAMEPROPOSAL_STATE_FOR_TRXID +trx.getId()); diff --git a/corda/contracts/src/main/java/djmil/cordacheckers/contracts/GameProposalContract.java b/corda/contracts/src/main/java/djmil/cordacheckers/contracts/GameProposalContract.java index 553e2b2..01e3dbe 100644 --- a/corda/contracts/src/main/java/djmil/cordacheckers/contracts/GameProposalContract.java +++ b/corda/contracts/src/main/java/djmil/cordacheckers/contracts/GameProposalContract.java @@ -1,9 +1,12 @@ package djmil.cordacheckers.contracts; +import static djmil.cordacheckers.contracts.UtxoLedgerTransactionUtil.getSingleCommand; +import static djmil.cordacheckers.contracts.UtxoLedgerTransactionUtil.getSingleInputState; +import static djmil.cordacheckers.contracts.UtxoLedgerTransactionUtil.getSingleOutputState; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import djmil.cordacheckers.UtxoLedgerTransactionUtil; import djmil.cordacheckers.states.GameBoardState; import djmil.cordacheckers.states.GameProposalState; import net.corda.v5.base.exceptions.CordaRuntimeException; @@ -17,15 +20,14 @@ public class GameProposalContract implements net.corda.v5.ledger.utxo.Contract { public void verify(UtxoLedgerTransaction trx) { log.info("GameProposalContract.verify() called"); - final UtxoLedgerTransactionUtil trxUtil = new UtxoLedgerTransactionUtil(trx); - final GameProposalCommand command = trxUtil.getSingleCommand(GameProposalCommand.class); + final GameProposalCommand command = getSingleCommand(trx, GameProposalCommand.class); switch (command) { case CREATE: { requireThat(trx.getInputContractStates().isEmpty(), CREATE_INPUT_STATE); requireThat(trx.getOutputContractStates().size() == 1, CREATE_OUTPUT_STATE); - GameProposalState outputState = trxUtil.getSingleOutputState(GameProposalState.class); + GameProposalState outputState = getSingleOutputState(trx, GameProposalState.class); requireThat(outputState.getAcquierColor() != null, NON_NULL_RECIPIENT_COLOR); break; } @@ -34,8 +36,8 @@ public class GameProposalContract implements net.corda.v5.ledger.utxo.Contract { requireThat(trx.getInputContractStates().size() == 1, ACCEPT_INPUT_STATE); requireThat(trx.getOutputContractStates().size() == 1, ACCEPT_OUTPUT_STATE); - GameProposalState inGameProposal = trxUtil.getSingleInputState(GameProposalState.class); - GameBoardState outGameBoard = trxUtil.getSingleOutputState(GameBoardState.class); + GameProposalState inGameProposal = getSingleInputState(trx, GameProposalState.class); + GameBoardState outGameBoard = getSingleOutputState(trx, GameBoardState.class); requireThat(outGameBoard.getParticipants().containsAll(inGameProposal.getParticipants()), ACCEPT_PARTICIPANTS); break; } @@ -44,14 +46,14 @@ public class GameProposalContract implements net.corda.v5.ledger.utxo.Contract { requireThat(trx.getInputContractStates().size() == 1, REJECT_INPUT_STATE); requireThat(trx.getOutputContractStates().isEmpty(), REJECT_OUTPUT_STATE); - trxUtil.getSingleInputState(GameProposalState.class); + getSingleInputState(trx, GameProposalState.class); break; } case CANCEL: requireThat(trx.getInputContractStates().size() == 1, CANCEL_INPUT_STATE); requireThat(trx.getOutputContractStates().isEmpty(), CANCEL_OUTPUT_STATE); - trxUtil.getSingleInputState(GameProposalState.class); + getSingleInputState(trx, GameProposalState.class); break; default: diff --git a/corda/contracts/src/main/java/djmil/cordacheckers/contracts/UtxoLedgerTransactionUtil.java b/corda/contracts/src/main/java/djmil/cordacheckers/contracts/UtxoLedgerTransactionUtil.java new file mode 100644 index 0000000..7e2416d --- /dev/null +++ b/corda/contracts/src/main/java/djmil/cordacheckers/contracts/UtxoLedgerTransactionUtil.java @@ -0,0 +1,56 @@ +package djmil.cordacheckers.contracts; + +import java.util.List; +import java.util.Optional; + +import net.corda.v5.ledger.utxo.Command; +import net.corda.v5.ledger.utxo.ContractState; +import net.corda.v5.ledger.utxo.transaction.UtxoLedgerTransaction; + +public class UtxoLedgerTransactionUtil { + public static T getSingleCommand(UtxoLedgerTransaction utxoTrx, Class clazz) { + return single(utxoTrx.getCommands(clazz), clazz); + } + + public static T getSingleReferenceState(UtxoLedgerTransaction utxoTrx, Class clazz) { + return single(utxoTrx.getReferenceStates(clazz), clazz); + } + + public static T getSingleInputState(UtxoLedgerTransaction utxoTrx, Class clazz) { + return single(utxoTrx.getInputStates(clazz), clazz); + } + + public static T getSingleOutputState(UtxoLedgerTransaction utxoTrx, Class clazz) { + return single(utxoTrx.getOutputStates(clazz), clazz); + } + + public static Optional getOptionalCommand(UtxoLedgerTransaction utxoTrx, Class clazz) { + return optional(utxoTrx.getCommands(clazz), clazz); + } + + public static Optional getOptionalReferenceState(UtxoLedgerTransaction utxoTrx, Class clazz) { + return optional(utxoTrx.getReferenceStates(clazz), clazz); + } + + public static Optional getOptionalInputState(UtxoLedgerTransaction utxoTrx, Class clazz) { + return optional(utxoTrx.getInputStates(clazz), clazz); + } + + public static Optional getOptionalOutputState(UtxoLedgerTransaction utxoTrx, Class clazz) { + return optional(utxoTrx.getOutputStates(clazz), clazz); + } + + private static Optional optional(List list, Class clazz) { + return list + .stream() + .reduce((a, b) -> {throw new IllegalStateException(MULTIPLE_INSTANCES_OF +clazz.getName());}); + } + + private static T single(List list, Class clazz) { + return optional(list, clazz) + .orElseThrow( () -> new IllegalStateException(NO_INSTANCES_OF +clazz.getName()) ); + } + + private static String MULTIPLE_INSTANCES_OF = "Multiple instances of "; + private static String NO_INSTANCES_OF = "No instances of "; +} diff --git a/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/CommandFlow.java b/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/CommandFlow.java new file mode 100644 index 0000000..0031fcc --- /dev/null +++ b/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/CommandFlow.java @@ -0,0 +1,135 @@ +package djmil.cordacheckers.gameboard; + +import java.util.UUID; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import djmil.cordacheckers.FlowResult; +import djmil.cordacheckers.contracts.GameBoardCommand; +import djmil.cordacheckers.contracts.GameBoardContract; +import djmil.cordacheckers.contracts.GameProposalCommand; +import djmil.cordacheckers.states.GameBoardState; +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.flows.FlowEngine; +import net.corda.v5.application.marshalling.JsonMarshallingService; +import net.corda.v5.application.membership.MemberLookup; +import net.corda.v5.base.annotations.Suspendable; +import net.corda.v5.base.types.MemberX500Name; +import net.corda.v5.crypto.SecureHash; +import net.corda.v5.ledger.utxo.StateAndRef; +import net.corda.v5.ledger.utxo.UtxoLedgerService; +import net.corda.v5.ledger.utxo.transaction.UtxoLedgerTransaction; +import net.corda.v5.ledger.utxo.transaction.UtxoSignedTransaction; +import net.corda.v5.ledger.utxo.transaction.UtxoTransactionBuilder; + +import java.time.Duration; +import java.time.Instant; + +public class CommandFlow implements ClientStartableFlow { + + private final static Logger log = LoggerFactory.getLogger(CommandFlow.class); + + @CordaInject + public JsonMarshallingService jsonMarshallingService; + + @CordaInject + public UtxoLedgerService utxoLedgerService; + + @CordaInject + public FlowEngine flowEngine; + + @CordaInject + public MemberLookup memberLookup; + + @Override + @Suspendable + public String call(ClientRequestBody requestBody) { + try { + final CommandFlowArgs args = requestBody.getRequestBodyAs(jsonMarshallingService, CommandFlowArgs.class); + //final GameCommand command = args.getCommand(); +log.info("GameBoardCommandFlow: findUnconsumedGameBoardState"); + final StateAndRef gbStateAndRef = findUnconsumedGameBoardState(args.getGameBoardUuid()); + + // final UtxoSignedTransaction trx = prepareSignedTransaction(command, utxoGameProposal); + + // final SecureHash trxId = this.flowEngine + // .subFlow( new CommitSubFlow(trx, command.getRespondent(utxoGameProposal)) ); + + // if (command == GameProposalCommand.ACCEPT) { + // GameBoardState newGb = (GameBoardState)trx.getOutputStateAndRefs().get(0).getState().getContractState(); + + // return new FlowResult(newGb.getId(), trxId) + // .toJsonEncodedString(jsonMarshallingService); + // } + log.info("GameBoardCommandFlow: prepareGameBoardView"); + GameBoardView gbView = prepareGameBoardView(gbStateAndRef); + + return new FlowResult(gbView ) + .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 findUnconsumedGameBoardState (UUID gameBoardUuid) { + /* + * Get list of all unconsumed aka 'active' 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. + */ + return this.utxoLedgerService + .findUnconsumedStatesByType(GameBoardState.class) + .stream() + .filter(sar -> sar.getState().getContractState().getId().equals(gameBoardUuid)) + .reduce((a, b) -> {throw new IllegalStateException("Multiple states: " +a +", " +b);}) + .get(); + } + + // @Suspendable + // private UtxoSignedTransaction prepareSignedTransaction( + // GameProposalCommand command, + // StateAndRef utxoGameProposal + // ) { + // UtxoTransactionBuilder trxBuilder = ledgerService.createTransactionBuilder() + // .setNotary(utxoGameProposal.getState().getNotaryName()) + // .setTimeWindowBetween(Instant.now(), Instant.now().plusMillis(Duration.ofDays(1).toMillis())) + // .addInputState(utxoGameProposal.getRef()) + // .addCommand(command) + // .addSignatories(utxoGameProposal.getState().getContractState().getParticipants()); + + // if (command == GameProposalCommand.ACCEPT) { + // trxBuilder = trxBuilder + // .addOutputState(new GameBoardState(utxoGameProposal)); + // //A state cannot be both an input and a reference input in the same transaction + // //.addReferenceState(utxoGameProposal.getRef()); + // } + + // return trxBuilder.toSignedTransaction(); + // } + + @Suspendable + private GameBoardView prepareGameBoardView(StateAndRef stateAndRef) { + final MemberX500Name myName = memberLookup.myInfo().getName(); + + final SecureHash trxId = stateAndRef.getRef().getTransactionId(); + + final UtxoLedgerTransaction utxoGameBoard = utxoLedgerService + .findLedgerTransaction(trxId); +log.info("GameBoardCommandFlow: createw gbView"); + GameBoardView gbView = new GameBoardView(myName, utxoGameBoard); + + gbView.previousCommand = new GameBoardCommand(GameBoardCommand.Type.SURRENDER); + + return gbView; + // return new GameBoardView(myName, utxoGameBoard); + } + +} diff --git a/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/MoveFlowArgs.java b/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/CommandFlowArgs.java similarity index 52% rename from corda/workflows/src/main/java/djmil/cordacheckers/gameboard/MoveFlowArgs.java rename to corda/workflows/src/main/java/djmil/cordacheckers/gameboard/CommandFlowArgs.java index ca0a3e0..4ed671a 100644 --- a/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/MoveFlowArgs.java +++ b/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/CommandFlowArgs.java @@ -1,24 +1,24 @@ package djmil.cordacheckers.gameboard; -import java.util.List; import java.util.UUID; -public class MoveFlowArgs { +import djmil.cordacheckers.contracts.GameBoardCommand; + +public class CommandFlowArgs { private UUID gameBoardUuid; - private List move; + private GameBoardCommand command; // Serialisation service requires a default constructor - public MoveFlowArgs() { + public CommandFlowArgs() { this.gameBoardUuid = null; - this.move = null; + this.command = null; + } + + public GameBoardCommand getCommand() { + return this.command; } public UUID getGameBoardUuid() { return this.gameBoardUuid; } - - public List getMove() { - return this.move; - } - } diff --git a/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/GameBoardView.java b/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/GameBoardView.java new file mode 100644 index 0000000..0e97c04 --- /dev/null +++ b/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/GameBoardView.java @@ -0,0 +1,60 @@ +package djmil.cordacheckers.gameboard; + +import java.util.Map; +import java.util.UUID; + +import djmil.cordacheckers.contracts.GameBoardCommand; +import djmil.cordacheckers.contracts.GameBoardContract; +import djmil.cordacheckers.contracts.UtxoLedgerTransactionUtil; +import djmil.cordacheckers.states.GameBoardState; +import djmil.cordacheckers.states.GameProposalState; +import djmil.cordacheckers.states.Piece; +import net.corda.v5.base.types.MemberX500Name; +import net.corda.v5.ledger.utxo.transaction.UtxoLedgerTransaction; + + +// GameBoard from the player's point of view +public class GameBoardView { + public final String opponentName; + public final Piece.Color opponentColor; + public final Boolean opponentMove; + public final Map board; + public /*final*/ GameBoardCommand previousCommand; + public final String message; + public final UUID id; + + // Serialisation service requires a default constructor + public GameBoardView() { + this.opponentName = null; + this.opponentColor = null; + this.opponentMove = null; + this.board = null; + this.previousCommand = null; + this.message = null; + this.id = null; + } + + // A view from a perspective of a concrete player, on a ledger transaction that has + // produced new GameBoardState + public GameBoardView(MemberX500Name myName, UtxoLedgerTransaction utxoGameBoard) { + + final GameProposalState referanceGameProposal = GameBoardContract + .getReferanceGameProposalState(utxoGameBoard); + + this.opponentName = referanceGameProposal.getOpponentName(myName).getCommonName(); + this.opponentColor = referanceGameProposal.getOpponentColor(myName); + + final GameBoardState stateGameBoard = UtxoLedgerTransactionUtil + .getSingleOutputState(utxoGameBoard, GameBoardState.class); + + this.opponentMove = this.opponentColor == stateGameBoard.getMoveColor(); + this.board = stateGameBoard.getBoard(); + this.message = stateGameBoard.getMessage(); + this.id = stateGameBoard.getId(); + + this.previousCommand = UtxoLedgerTransactionUtil + .getOptionalCommand(utxoGameBoard, GameBoardCommand.class) + .orElseGet(() -> null); + } + +} diff --git a/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/ListFlow.java b/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/ListFlow.java index cedd1e8..41ceac2 100644 --- a/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/ListFlow.java +++ b/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/ListFlow.java @@ -43,10 +43,10 @@ public class ListFlow implements ClientStartableFlow { final var unconsumedGameBoardList = utxoLedgerService .findUnconsumedStatesByType(GameBoardState.class); - List res = new LinkedList(); + List res = new LinkedList(); for (StateAndRef gbState :unconsumedGameBoardList) { // NOTE: lambda operations (aka .map) can not be used with UtxoLedgerService instances - res.add(prepareListItem(gbState)); + res.add(prepareGameBoardView(gbState)); } return new FlowResult(res) @@ -59,7 +59,7 @@ public class ListFlow implements ClientStartableFlow { } @Suspendable - private ListItem prepareListItem(StateAndRef stateAndRef) { + private GameBoardView prepareGameBoardView(StateAndRef stateAndRef) { final MemberX500Name myName = memberLookup.myInfo().getName(); final SecureHash trxId = stateAndRef.getRef().getTransactionId(); @@ -67,14 +67,9 @@ public class ListFlow implements ClientStartableFlow { final UtxoLedgerTransaction utxoGameBoard = utxoLedgerService .findLedgerTransaction(trxId); - final GameProposalState referanceGameProposal = GameBoardContract - .getReferanceGameProposalState(utxoGameBoard); + var newGbView = new GameBoardView(myName, utxoGameBoard); - return new ListItem( - stateAndRef.getState().getContractState(), - referanceGameProposal.getOpponentName(myName), - referanceGameProposal.getOpponentColor(myName) - ); + return newGbView; } } diff --git a/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/ListItem.java b/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/ListItem.java deleted file mode 100644 index 70ecadb..0000000 --- a/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/ListItem.java +++ /dev/null @@ -1,41 +0,0 @@ -package djmil.cordacheckers.gameboard; - -import java.util.UUID; - -import djmil.cordacheckers.states.GameBoardState; -import djmil.cordacheckers.states.Piece; -import net.corda.v5.base.types.MemberX500Name; - -// Class to hold results of the List flow. -// JsonMarshallingService can only serialize simple classes that the underlying Jackson serializer recognises, -// hence creating a DTO style object which consists only of Strings and a UUIDs. It is possible to create custom -// serializers for the JsonMarshallingService in the future. - -public class ListItem { - public final String opponentName; - public final Piece.Color opponentColor; - public final Boolean opponentMove; - public final Object board; - public final String message; - public final UUID id; - - // Serialisation service requires a default constructor - public ListItem() { - this.opponentName = null; - this.opponentColor = null; - this.opponentMove = null; - this.board = null; - this.message = null; - this.id = null; - } - - public ListItem(GameBoardState gameBoard, MemberX500Name opponentName, Piece.Color opponentColor) { - this.opponentName = opponentName.getCommonName(); - this.opponentColor = opponentColor; - this.opponentMove = opponentColor == gameBoard.getMoveColor(); - this.board = gameBoard.getBoard(); - this.message = gameBoard.getMessage(); - this.id = gameBoard.getId(); - } - -} diff --git a/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/MoveFlow.java b/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/MoveFlow.java deleted file mode 100644 index 2de6503..0000000 --- a/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/MoveFlow.java +++ /dev/null @@ -1,97 +0,0 @@ -package djmil.cordacheckers.gameboard; - -import java.util.UUID; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import djmil.cordacheckers.FlowResult; -import djmil.cordacheckers.contracts.GameBoardCommand; -import djmil.cordacheckers.states.GameBoardState; -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.FlowEngine; -import net.corda.v5.application.marshalling.JsonMarshallingService; -import net.corda.v5.base.annotations.Suspendable; -import net.corda.v5.crypto.SecureHash; -import net.corda.v5.ledger.utxo.StateAndRef; -import net.corda.v5.ledger.utxo.UtxoLedgerService; -import net.corda.v5.ledger.utxo.transaction.UtxoSignedTransaction; -import net.corda.v5.ledger.utxo.transaction.UtxoTransactionBuilder; - -import java.time.Duration; -import java.time.Instant; - -public class MoveFlow implements ClientStartableFlow { - - private final static Logger log = LoggerFactory.getLogger(MoveFlow.class); - - @CordaInject - public JsonMarshallingService jsonMarshallingService; - - @CordaInject - public UtxoLedgerService ledgerService; - - @CordaInject - public FlowEngine flowEngine; - - @Override - @Suspendable - public String call(ClientRequestBody requestBody) { - try { - final MoveFlowArgs args = requestBody.getRequestBodyAs(jsonMarshallingService, MoveFlowArgs.class); - final GameBoardCommand command = new GameBoardCommand(args.getMove()); - - final StateAndRef utxoGameBoard = findUnconsumedGameBoardState(args.getGameBoardUuid()); - - final GameBoardState newGameBoard = null; // TODO - - final UtxoSignedTransaction trx = prepareSignedTransaction(command, utxoGameBoard, newGameBoard); - - final SecureHash trxId = this.flowEngine - .subFlow( new CommitSubFlow(trx) ); - - return new FlowResult(newGameBoard, trxId) // TODO return players perspective GB - .toJsonEncodedString(jsonMarshallingService); - } - catch (Exception e) { - log.warn("GameBoardAction flow failed to process utxo request body " + requestBody + - " because: " + e.getMessage()); - return new FlowResult(e).toJsonEncodedString(jsonMarshallingService); - } - } - - @Suspendable - private StateAndRef findUnconsumedGameBoardState (UUID gameProposalUuid) { - /* - * Get list of all unconsumed aka 'active' GameBoardState, 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. - */ - return this.ledgerService - .findUnconsumedStatesByType(GameBoardState.class) - .stream() - .filter(sar -> sar.getState().getContractState().getId().equals(gameProposalUuid)) - .reduce((a, b) -> {throw new IllegalStateException("Multiple states: " +a +", " +b);}) - .get(); - } - - @Suspendable - private UtxoSignedTransaction prepareSignedTransaction( - GameBoardCommand command, - StateAndRef utxoGameProposal, - GameBoardState newGameBoard - ) { - UtxoTransactionBuilder trxBuilder = ledgerService.createTransactionBuilder() - .setNotary(utxoGameProposal.getState().getNotaryName()) - .setTimeWindowBetween(Instant.now(), Instant.now().plusMillis(Duration.ofDays(1).toMillis())) - .addInputState(utxoGameProposal.getRef()) - .addOutputState(newGameBoard) - .addCommand(command) - .addSignatories(utxoGameProposal.getState().getContractState().getParticipants()); - - return trxBuilder.toSignedTransaction(); - } - -} diff --git a/corda/workflows/src/main/java/djmil/cordacheckers/gameproposal/CommandFlow.java b/corda/workflows/src/main/java/djmil/cordacheckers/gameproposal/CommandFlow.java index 4ed511b..61cd069 100644 --- a/corda/workflows/src/main/java/djmil/cordacheckers/gameproposal/CommandFlow.java +++ b/corda/workflows/src/main/java/djmil/cordacheckers/gameproposal/CommandFlow.java @@ -44,8 +44,6 @@ public class CommandFlow implements ClientStartableFlow { final CommandFlowArgs args = requestBody.getRequestBodyAs(jsonMarshallingService, CommandFlowArgs.class); final GameProposalCommand command = args.getCommand(); - System.out.println("Game Proposal command" + command); - final StateAndRef utxoGameProposal = findUnconsumedGameProposalState(args.getGameProposalUuid()); final UtxoSignedTransaction trx = prepareSignedTransaction(command, utxoGameProposal);