diff --git a/backend/src/main/java/djmil/cordacheckers/cordaclient/CordaClient.java b/backend/src/main/java/djmil/cordacheckers/cordaclient/CordaClient.java index c95ae5f..327f83c 100644 --- a/backend/src/main/java/djmil/cordacheckers/cordaclient/CordaClient.java +++ b/backend/src/main/java/djmil/cordacheckers/cordaclient/CordaClient.java @@ -14,31 +14,21 @@ import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.ObjectMapper; +import djmil.cordacheckers.cordaclient.dao.GameState; 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.GameResult; 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.GameProposalCreateReq; -import djmil.cordacheckers.cordaclient.dao.flow.arguments.GameProposalCreateRes; -import djmil.cordacheckers.cordaclient.dao.flow.arguments.GameProposalListRes; -import djmil.cordacheckers.cordaclient.dao.flow.arguments.Empty; -import djmil.cordacheckers.cordaclient.dao.flow.arguments.GameBoardCommandReq; -import djmil.cordacheckers.cordaclient.dao.flow.arguments.GameBoardResGameResult; -import djmil.cordacheckers.cordaclient.dao.flow.arguments.GameBoardListRes; -import djmil.cordacheckers.cordaclient.dao.flow.arguments.GameBoardResGameBoard; -import djmil.cordacheckers.cordaclient.dao.flow.arguments.GameProposalCommandAcceptRes; -import djmil.cordacheckers.cordaclient.dao.flow.arguments.GameProposalCommandReq; -import djmil.cordacheckers.cordaclient.dao.flow.arguments.GameProposalCommandRes; +import djmil.cordacheckers.cordaclient.dao.flow.arguments.Req; +import djmil.cordacheckers.cordaclient.dao.flow.arguments.Rsp; +import djmil.cordacheckers.cordaclient.dao.flow.arguments.ReqGameBoardMove; +import djmil.cordacheckers.cordaclient.dao.flow.arguments.ReqGameProposalCreate; +import djmil.cordacheckers.cordaclient.dao.flow.arguments.RspGameState; +import djmil.cordacheckers.cordaclient.dao.flow.arguments.RspGameStateList; @Service public class CordaClient { @@ -69,205 +59,110 @@ public class CordaClient { } /** - * Obtain list of unconsumed (active) GameProposals - * @param myHoldingIdentity - * @return GameProposals list in JSON form + * @param holdingIdentity + * @return list of unconsumed (active) GameStateViews */ - public List gameProposalList(HoldingIdentity myHoldingIdentity) { - + public List gameStateList(HoldingIdentity holdingIdentity) { final RequestBody requestBody = new RequestBody( - "gp.list-" + UUID.randomUUID(), - "djmil.cordacheckers.gameproposal.ListFlow", - new Empty() - ); + "gs.list-" + UUID.randomUUID(), + "djmil.cordacheckers.gamestate.ListFlow", + new Req()); - final GameProposalListRes listFlowResult = cordaFlowExecute( - myHoldingIdentity, - requestBody, - GameProposalListRes.class - ); - - if (listFlowResult.failureStatus() != null) { - System.out.println("GameProposalCreateFlow failed: " + listFlowResult.failureStatus()); - throw new RuntimeException("GameProsal: CreateFlow execution has failed"); - } - - return listFlowResult.successStatus(); + return cordaFlowExecute(holdingIdentity, requestBody, RspGameStateList.class) + .getResponce(requestBody); } - public UUID gameProposalCreate( + public GameState gameStateGet(HoldingIdentity holdingIdentity, UUID gameStateUuid) { + final RequestBody requestBody = new RequestBody( + "gs.get-" + UUID.randomUUID(), + "djmil.cordacheckers.gamestate.GetFlow", + gameStateUuid); + + return cordaFlowExecute(holdingIdentity, requestBody, RspGameState.class) + .getResponce(requestBody); + } + + public GameState gameProposalCreate( HoldingIdentity issuer, HoldingIdentity acquier, Piece.Color acquierColor, String message - ) throws JsonMappingException, JsonProcessingException { + ) { final RequestBody requestBody = new RequestBody( "gp.create-" + UUID.randomUUID(), "djmil.cordacheckers.gameproposal.CreateFlow", - new GameProposalCreateReq( + new ReqGameProposalCreate( acquier.x500Name(), acquierColor, - message - ) - ); + message)); - final GameProposalCreateRes createResult = cordaFlowExecute( - issuer, - requestBody, - GameProposalCreateRes.class - ); - - if (createResult.failureStatus() != null) { - System.out.println("GameProposal.CreateFlow failed: " + createResult.failureStatus()); - throw new RuntimeException("GameProsal: CreateFlow execution has failed"); - } - - return createResult.successStatus(); + return cordaFlowExecute(issuer, requestBody,RspGameState.class) + .getResponce(requestBody); } - public String gameProposalReject( - HoldingIdentity myHoldingIdentity, - UUID gameProposalUuid - ) { + public GameState gameProposalReject(HoldingIdentity holdingIdentity, UUID gameProposalUuid) { final RequestBody requestBody = new RequestBody( "gp.reject-" +UUID.randomUUID(), - "djmil.cordacheckers.gameproposal.CommandFlow", - new GameProposalCommandReq( - gameProposalUuid.toString(), - GameProposalCommandReq.Command.REJECT - ) - ); + "djmil.cordacheckers.gameproposal.RejectFlow", + gameProposalUuid); - final GameProposalCommandRes actionResult = cordaFlowExecute( - myHoldingIdentity, - requestBody, - GameProposalCommandRes.class - ); - - if (actionResult.failureStatus() != null) { - System.out.println("GameProposal.CommandFlow failed: " + actionResult.failureStatus()); - throw new RuntimeException("GameProsal: CommandFlow execution has failed"); - } - - return actionResult.successStatus(); + return cordaFlowExecute(holdingIdentity, requestBody, RspGameState.class) + .getResponce(requestBody); } - public GameBoard gameProposalAccept( - HoldingIdentity myHoldingIdentity, - UUID gameProposalUuid - ) { + public GameState gameProposalCancel(HoldingIdentity holdingIdentity, UUID gameProposalUuid) { + final RequestBody requestBody = new RequestBody( + "gp.reject-" +UUID.randomUUID(), + "djmil.cordacheckers.gameproposal.CancelFlow", + gameProposalUuid); + + return cordaFlowExecute(holdingIdentity, requestBody, RspGameState.class) + .getResponce(requestBody); + } + + public GameState gameProposalAccept(HoldingIdentity holdingIdentity, UUID gameProposalUuid) { final RequestBody requestBody = new RequestBody( "gp.accept-" +UUID.randomUUID(), - "djmil.cordacheckers.gameproposal.CommandFlow", - new GameProposalCommandReq( - gameProposalUuid.toString(), - GameProposalCommandReq.Command.ACCEPT - ) - ); + "djmil.cordacheckers.gameproposal.AcceptFlow", + gameProposalUuid); - final GameProposalCommandAcceptRes actionResult = cordaFlowExecute( - myHoldingIdentity, - requestBody, - GameProposalCommandAcceptRes.class - ); - - if (actionResult.failureStatus() != null) { - System.out.println("GameProposal.CommandFlow failed: " + actionResult.failureStatus()); - throw new RuntimeException("GameProsal: CommandFlow execution has failed"); - } - - return actionResult.successStatus(); + return cordaFlowExecute(holdingIdentity, requestBody, RspGameState.class) + .getResponce(requestBody); } - public List gameBoardList(HoldingIdentity holdingIdentity) { - - final RequestBody requestBody = new RequestBody( - "gb.list-" + UUID.randomUUID(), - "djmil.cordacheckers.gameboard.ListFlow", - new Empty() - ); - - final GameBoardListRes listFlowResult = cordaFlowExecute( - holdingIdentity, - requestBody, - GameBoardListRes.class - ); - - if (listFlowResult.failureStatus() != null) { - System.out.println("GameBoard.ListFlow failed: " + listFlowResult.failureStatus()); - throw new RuntimeException("GameBoard: ListFlow execution has failed"); - } - - return listFlowResult.successStatus(); - } - - public GameResult gameBoardSurrender( - HoldingIdentity myHoldingIdentity, - UUID gameBoardUuid - ) { + public GameState gameBoardSurrender(HoldingIdentity holdingIdentity, UUID gameBoardUuid) { final RequestBody requestBody = new RequestBody( "gb.surrender-" +UUID.randomUUID(), - "djmil.cordacheckers.gameboard.CommandFlow", - new GameBoardCommandReq( - gameBoardUuid, - new GameBoardCommand(GameBoardCommand.Type.SURRENDER) - ) - ); + "djmil.cordacheckers.gameboard.SurrenderFlow", + gameBoardUuid); - final GameBoardResGameResult moveResult = cordaFlowExecute( - myHoldingIdentity, - requestBody, - GameBoardResGameResult.class - ); - - if (moveResult.failureStatus() != null) { - System.out.println("GameBoard.CommandFlow failed: " + moveResult.failureStatus()); - throw new RuntimeException("GameBoard: CommandFlow execution has failed"); - } - - return moveResult.successStatus(); + return cordaFlowExecute(holdingIdentity, requestBody, RspGameState.class) + .getResponce(requestBody); } - public GameBoard gameBoardMove( - HoldingIdentity myHoldingIdentity, - UUID gameBoardUuid, - List move - ) { + public GameState gameBoardMove(HoldingIdentity holdingIdentity, UUID gameBoardUuid, List move) { final RequestBody requestBody = new RequestBody( "gb.move-" +UUID.randomUUID(), "djmil.cordacheckers.gameboard.CommandFlow", - new GameBoardCommandReq( + new ReqGameBoardMove( gameBoardUuid, - new GameBoardCommand(move)) - ); + move)); - final GameBoardResGameBoard moveResult = cordaFlowExecute( - myHoldingIdentity, - requestBody, - GameBoardResGameBoard.class - ); - - if (moveResult.failureStatus() != null) { - System.out.println("GameBoard.CommandFlow failed: " + moveResult.failureStatus()); - throw new RuntimeException("GameBoard: CommandFlow execution has failed"); - } - - return moveResult.successStatus(); + return cordaFlowExecute(holdingIdentity, requestBody, RspGameState.class) + .getResponce(requestBody); } - private T cordaFlowExecute(HoldingIdentity holdingIdentity, RequestBody requestBody, Class flowResultType) { - + private > T cordaFlowExecute(HoldingIdentity holdingIdentity, RequestBody requestBody, Class flowReponceType) { try { - final String requestBodyJson = this.jsonMapper.writeValueAsString(requestBody); + final String requestBodyJson = this.jsonMapper + .writeValueAsString(requestBody); - final ResponseBody startedFlow = cordaFlowPost( - holdingIdentity, - requestBodyJson - ); + final ResponseBody startedFlow = cordaFlowPost(holdingIdentity, requestBodyJson); - final String flowResult = cordaFlowPoll(startedFlow); + final String flowResponce = cordaFlowPoll(startedFlow); - return this.jsonMapper.readValue(flowResult, flowResultType); + return this.jsonMapper + .readValue(flowResponce, flowReponceType); } catch (Exception e) { throw new RuntimeException("Unable to perform "+requestBody.flowClassName() @@ -335,7 +230,6 @@ public class CordaClient { } if (responseBody.isFlowCompleted() && responseBody.flowResult() != null) { - System.out.println("resp: "+ responseBody.flowResult()); return responseBody.flowResult(); } } diff --git a/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/GameBoard.java b/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/GameBoard.java deleted file mode 100644 index ff9f1a5..0000000 --- a/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/GameBoard.java +++ /dev/null @@ -1,21 +0,0 @@ -package djmil.cordacheckers.cordaclient.dao; - -import java.util.Map; -import java.util.UUID; - -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; - -@JsonDeserialize -public record GameBoard( - String opponentName, - Piece.Color opponentColor, - Boolean opponentMove, - Integer moveNumber, - Map board, - GameBoardCommand previousCommand, - String message, - UUID gameUuid) - -implements GameState { - -} diff --git a/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/GameBoardCommand.java b/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/GameBoardCommand.java deleted file mode 100644 index b13a393..0000000 --- a/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/GameBoardCommand.java +++ /dev/null @@ -1,46 +0,0 @@ -package djmil.cordacheckers.cordaclient.dao; - -import java.util.List; - -public class GameBoardCommand { - public static enum Type { - MOVE, - SURRENDER, - VICTORY; - } - - 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/GameProposal.java b/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/GameProposal.java deleted file mode 100644 index c13a51b..0000000 --- a/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/GameProposal.java +++ /dev/null @@ -1,17 +0,0 @@ -package djmil.cordacheckers.cordaclient.dao; - -import java.util.UUID; - -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; - -@JsonDeserialize -public record GameProposal( - String issuer, - String acquier, - Piece.Color acquierColor, - String message, - UUID gameUuid) - -implements GameState { - -} diff --git a/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/GameResult.java b/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/GameResult.java deleted file mode 100644 index 2fff804..0000000 --- a/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/GameResult.java +++ /dev/null @@ -1,16 +0,0 @@ -package djmil.cordacheckers.cordaclient.dao; - -import java.util.UUID; - -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; - -@JsonDeserialize -public record GameResult( - String whitePlayerName, - String blackPlayerName, - Piece.Color victoryColor, - UUID gameUuid) - -implements GameState { - -} diff --git a/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/GameState.java b/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/GameState.java index 2743b46..6271a03 100644 --- a/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/GameState.java +++ b/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/GameState.java @@ -1,7 +1,30 @@ package djmil.cordacheckers.cordaclient.dao; +import java.util.List; +import java.util.Map; import java.util.UUID; -public interface GameState { - public UUID gameUuid(); +public record GameState( + Status status, + String opponentName, + Piece.Color opponentColor, + + Map board, + Integer moveNumber, + List previousMove, + + String message, + UUID uuid +) { + public static enum Status { + GAME_PROPOSAL_WAIT_FOR_OPPONENT, + GAME_PROPOSAL_WAIT_FOR_YOU, + GAME_PROPOSAL_REJECTED, + GAME_PROPOSAL_CANCELED, + GAME_BOARD_WAIT_FOR_OPPONENT, + GAME_BOARD_WAIT_FOR_YOU, + GAME_RESULT_YOU_WON, + GAME_RESULT_YOU_LOOSE; + } + } diff --git a/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/Piece.java b/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/Piece.java index 066dc93..84cbeac 100644 --- a/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/Piece.java +++ b/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/Piece.java @@ -4,12 +4,16 @@ public class Piece { public enum Type { MAN, - KING, + KING; } public enum Color { WHITE, - BLACK, + BLACK; + + public Color opposite() { + return this == WHITE ? BLACK : WHITE; + } } Color color; 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 deleted file mode 100644 index 025f4f4..0000000 --- a/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/arguments/GameBoardCommandReq.java +++ /dev/null @@ -1,8 +0,0 @@ -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/GameBoardListRes.java b/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/arguments/GameBoardListRes.java deleted file mode 100644 index abf270d..0000000 --- a/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/arguments/GameBoardListRes.java +++ /dev/null @@ -1,12 +0,0 @@ -package djmil.cordacheckers.cordaclient.dao.flow.arguments; - -import java.util.List; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; - -import djmil.cordacheckers.cordaclient.dao.GameBoard; - -@JsonIgnoreProperties(ignoreUnknown = true) -public record GameBoardListRes(List successStatus, String failureStatus) { - -} diff --git a/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/arguments/GameBoardResGameBoard.java b/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/arguments/GameBoardResGameBoard.java deleted file mode 100644 index c9bd0ae..0000000 --- a/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/arguments/GameBoardResGameBoard.java +++ /dev/null @@ -1,7 +0,0 @@ -package djmil.cordacheckers.cordaclient.dao.flow.arguments; - -import djmil.cordacheckers.cordaclient.dao.GameBoard; - -public record GameBoardResGameBoard(GameBoard successStatus, String failureStatus) { - -} diff --git a/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/arguments/GameBoardResGameResult.java b/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/arguments/GameBoardResGameResult.java deleted file mode 100644 index 6f494fb..0000000 --- a/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/arguments/GameBoardResGameResult.java +++ /dev/null @@ -1,7 +0,0 @@ -package djmil.cordacheckers.cordaclient.dao.flow.arguments; - -import djmil.cordacheckers.cordaclient.dao.GameResult; - -public record GameBoardResGameResult(GameResult successStatus, 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 deleted file mode 100644 index c4b1d3b..0000000 --- a/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/arguments/GameProposalCommandAcceptRes.java +++ /dev/null @@ -1,7 +0,0 @@ -package djmil.cordacheckers.cordaclient.dao.flow.arguments; - -import djmil.cordacheckers.cordaclient.dao.GameBoard; - -public record GameProposalCommandAcceptRes(GameBoard successStatus, String transactionId, String failureStatus) { - -} diff --git a/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/arguments/GameProposalCommandReq.java b/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/arguments/GameProposalCommandReq.java deleted file mode 100644 index 1b7a92e..0000000 --- a/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/arguments/GameProposalCommandReq.java +++ /dev/null @@ -1,9 +0,0 @@ -package djmil.cordacheckers.cordaclient.dao.flow.arguments; - -public record GameProposalCommandReq(String gameProposalUuid, Command command) { - public enum Command { - ACCEPT, - REJECT, - CANCEL - } -} 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 deleted file mode 100644 index e98abdc..0000000 --- a/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/arguments/GameProposalCommandRes.java +++ /dev/null @@ -1,5 +0,0 @@ -package djmil.cordacheckers.cordaclient.dao.flow.arguments; - -public record GameProposalCommandRes(String successStatus, String transactionId, String failureStatus) { - -} diff --git a/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/arguments/GameProposalCreateRes.java b/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/arguments/GameProposalCreateRes.java deleted file mode 100644 index 4246dc6..0000000 --- a/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/arguments/GameProposalCreateRes.java +++ /dev/null @@ -1,10 +0,0 @@ -package djmil.cordacheckers.cordaclient.dao.flow.arguments; - -import java.util.UUID; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; - -@JsonIgnoreProperties(ignoreUnknown = true) -public record GameProposalCreateRes(UUID successStatus, String failureStatus) { - -} diff --git a/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/arguments/GameProposalListRes.java b/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/arguments/GameProposalListRes.java deleted file mode 100644 index b7e134e..0000000 --- a/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/arguments/GameProposalListRes.java +++ /dev/null @@ -1,12 +0,0 @@ -package djmil.cordacheckers.cordaclient.dao.flow.arguments; - -import java.util.List; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; - -import djmil.cordacheckers.cordaclient.dao.GameProposal; - -@JsonIgnoreProperties(ignoreUnknown = true) -public record GameProposalListRes(List successStatus, String failureStatus) { - -} diff --git a/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/arguments/Empty.java b/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/arguments/Req.java similarity index 66% rename from backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/arguments/Empty.java rename to backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/arguments/Req.java index 9af8db8..9e9bde5 100644 --- a/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/arguments/Empty.java +++ b/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/arguments/Req.java @@ -3,6 +3,9 @@ package djmil.cordacheckers.cordaclient.dao.flow.arguments; import com.fasterxml.jackson.databind.annotation.JsonSerialize; @JsonSerialize -public class Empty { +/** + * Representation of a request without arguments + */ +public class Req { } diff --git a/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/arguments/ReqGameBoardMove.java b/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/arguments/ReqGameBoardMove.java new file mode 100644 index 0000000..3069173 --- /dev/null +++ b/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/arguments/ReqGameBoardMove.java @@ -0,0 +1,11 @@ +package djmil.cordacheckers.cordaclient.dao.flow.arguments; + +import java.util.List; +import java.util.UUID; + +public record ReqGameBoardMove( + UUID gameBoardUuid, + List move +) { + +} diff --git a/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/arguments/GameProposalCreateReq.java b/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/arguments/ReqGameProposalCreate.java similarity index 50% rename from backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/arguments/GameProposalCreateReq.java rename to backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/arguments/ReqGameProposalCreate.java index c41d58b..ff76911 100644 --- a/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/arguments/GameProposalCreateReq.java +++ b/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/arguments/ReqGameProposalCreate.java @@ -2,6 +2,10 @@ package djmil.cordacheckers.cordaclient.dao.flow.arguments; import djmil.cordacheckers.cordaclient.dao.Piece; -public record GameProposalCreateReq(String opponentName, Piece.Color opponentColor, String message) { +public record ReqGameProposalCreate( + String opponentName, + Piece.Color opponentColor, + String message +) { } diff --git a/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/arguments/Rsp.java b/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/arguments/Rsp.java new file mode 100644 index 0000000..8b03a2b --- /dev/null +++ b/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/arguments/Rsp.java @@ -0,0 +1,19 @@ +package djmil.cordacheckers.cordaclient.dao.flow.arguments; + +import djmil.cordacheckers.cordaclient.dao.flow.RequestBody; + +public interface Rsp { + public T successStatus(); + public String failureStatus(); + + public default T getResponce(RequestBody requestBody) { + if (failureStatus() != null) { + final String msg = requestBody.flowClassName() +" requestId " +requestBody.clientRequestId() +" has failed: " +failureStatus(); + System.err.println(msg +". Reson " +failureStatus()); + throw new RuntimeException(msg); + } + + return successStatus(); + } + +} diff --git a/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/arguments/RspGameState.java b/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/arguments/RspGameState.java new file mode 100644 index 0000000..758597c --- /dev/null +++ b/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/arguments/RspGameState.java @@ -0,0 +1,22 @@ +package djmil.cordacheckers.cordaclient.dao.flow.arguments; + +import djmil.cordacheckers.cordaclient.dao.GameState; +import djmil.cordacheckers.cordaclient.dao.flow.RequestBody; + +public record RspGameState( + GameState successStatus, + String transactionId, + String failureStatus) implements Rsp { + + @Override + public GameState getResponce(RequestBody requestBody) { + if (failureStatus() != null) { + final String msg = requestBody.flowClassName() +" requestId " +requestBody.clientRequestId() +" has failed"; + System.err.println(msg +". Reson " +failureStatus +". TransactionId " +transactionId); + throw new RuntimeException(msg); + } + + System.out.println("[OK] "+transactionId); + return successStatus(); + } +} diff --git a/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/arguments/RspGameStateList.java b/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/arguments/RspGameStateList.java new file mode 100644 index 0000000..cc88041 --- /dev/null +++ b/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/arguments/RspGameStateList.java @@ -0,0 +1,11 @@ +package djmil.cordacheckers.cordaclient.dao.flow.arguments; + +import java.util.List; + +import djmil.cordacheckers.cordaclient.dao.GameState; + +public record RspGameStateList( + List successStatus, + String failureStatus) implements Rsp> { + +} diff --git a/backend/src/main/java/djmil/cordacheckers/gameproposal/GameProposalController.java b/backend/src/main/java/djmil/cordacheckers/gameproposal/GameProposalController.java index 12aff5e..bdf4b25 100644 --- a/backend/src/main/java/djmil/cordacheckers/gameproposal/GameProposalController.java +++ b/backend/src/main/java/djmil/cordacheckers/gameproposal/GameProposalController.java @@ -2,12 +2,14 @@ package djmil.cordacheckers.gameproposal; import java.net.URI; import java.util.List; -import java.util.UUID; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.UriComponentsBuilder; @@ -15,17 +17,13 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonMappingException; import djmil.cordacheckers.cordaclient.CordaClient; +import djmil.cordacheckers.cordaclient.dao.GameState; import djmil.cordacheckers.cordaclient.dao.HoldingIdentity; -import djmil.cordacheckers.cordaclient.dao.flow.arguments.GameProposalCreateReq; import djmil.cordacheckers.cordaclient.dao.Piece; -import djmil.cordacheckers.cordaclient.dao.GameProposal; +import djmil.cordacheckers.cordaclient.dao.flow.arguments.ReqGameProposalCreate; import djmil.cordacheckers.user.HoldingIdentityResolver; import djmil.cordacheckers.user.User; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; - @RestController @RequestMapping("gameproposal") @@ -41,9 +39,9 @@ public class GameProposalController { public ResponseEntity findAllUnconsumed( @AuthenticationPrincipal User player ) { - List gpList = cordaClient.gameProposalList(player.getHoldingIdentity()); + List gsList = cordaClient.gameStateList(player.getHoldingIdentity()); - return ResponseEntity.ok(gpList.toString()); + return ResponseEntity.ok(gsList.toString()); } // @PostMapping() @@ -55,7 +53,7 @@ public class GameProposalController { @PostMapping() public ResponseEntity createGameProposal( @AuthenticationPrincipal User sender, - @RequestBody GameProposalCreateReq gpRequest, + @RequestBody ReqGameProposalCreate gpRequest, UriComponentsBuilder ucb ) throws JsonMappingException, JsonProcessingException { final HoldingIdentity gpSender = sender.getHoldingIdentity(); @@ -64,7 +62,7 @@ public class GameProposalController { final Piece.Color gpReceiverColor = gpRequest.opponentColor(); // TODO handle expectionns here - UUID newGameProposalUuid = cordaClient.gameProposalCreate( + GameState gameStateView = cordaClient.gameProposalCreate( gpSender, gpReceiver, gpReceiverColor, @@ -73,7 +71,7 @@ public class GameProposalController { URI locationOfNewGameProposal = ucb .path("gameproposal/{id}") - .buildAndExpand(newGameProposalUuid) + .buildAndExpand(gameStateView) .toUri(); return ResponseEntity diff --git a/backend/src/test/java/djmil/cordacheckers/cordaclient/CordaClientTest.java b/backend/src/test/java/djmil/cordacheckers/cordaclient/CordaClientTest.java deleted file mode 100644 index 5c9b843..0000000 --- a/backend/src/test/java/djmil/cordacheckers/cordaclient/CordaClientTest.java +++ /dev/null @@ -1,255 +0,0 @@ -package djmil.cordacheckers.cordaclient; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -import java.util.Arrays; -import java.util.List; -import java.util.UUID; - -import javax.naming.InvalidNameException; - -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonMappingException; - -import djmil.cordacheckers.cordaclient.dao.GameState; -import djmil.cordacheckers.cordaclient.dao.GameBoard; -import djmil.cordacheckers.cordaclient.dao.GameProposal; -import djmil.cordacheckers.cordaclient.dao.GameResult; -import djmil.cordacheckers.cordaclient.dao.Piece; -import djmil.cordacheckers.cordaclient.dao.VirtualNode; -import djmil.cordacheckers.user.HoldingIdentityResolver; - -@SpringBootTest -public class CordaClientTest { - @Autowired - CordaClient cordaClient; - - @Autowired - HoldingIdentityResolver holdingIdentityResolver; - - @Test - void testGetVirtualNodeList() throws InvalidNameException { - List vNodes = cordaClient.getVirtualNodeList(); - - assertThat(vNodes.size()).isEqualTo(5); // default vNode config for CSDE - } - - @Test - void testGameProposalList() throws JsonProcessingException { - List gpList = cordaClient.gameProposalList( - holdingIdentityResolver.getByUsername("Alice")); - - System.out.println("testListGameProposals\n"+ gpList); - } - - @Test - void testGameProposalCreate() throws JsonMappingException, JsonProcessingException { - final String gpIssuer = "alice"; - final String gpAcquier = "bob"; - final Piece.Color gpAcquierColor = Piece.Color.WHITE; - final String gpMessage = "GameProposal create test"; - - final UUID createdGpUuid = cordaClient.gameProposalCreate( - holdingIdentityResolver.getByUsername(gpIssuer), - holdingIdentityResolver.getByUsername(gpAcquier), - gpAcquierColor, - gpMessage); - - List gpListSender = cordaClient.gameProposalList( - holdingIdentityResolver.getByUsername(gpIssuer)); - - assertThat(findByUuid(gpListSender, createdGpUuid)).isNotNull(); - - List gpListReceiver = cordaClient.gameProposalList( - holdingIdentityResolver.getByUsername(gpAcquier)); - - GameProposal gp; - assertThat(gp = findByUuid(gpListReceiver, createdGpUuid)).isNotNull(); - - assertThat(gp.acquier()).isEqualToIgnoringCase(gpAcquier); - assertThat(gp.issuer()).isEqualToIgnoringCase(gpIssuer); - assertThat(gp.acquierColor()).isEqualByComparingTo(gpAcquierColor); - assertThat(gp.message()).isEqualTo(gpMessage); - } - - @Test - void testGameProposalReject() throws JsonMappingException, JsonProcessingException { - final String gpIssuer = "alice"; - final String gpAcquier = "bob"; - final Piece.Color gpReceiverColor = Piece.Color.WHITE; - final String gpMessage = "GameProposal REJECT test"; - - final UUID gpUuid = cordaClient.gameProposalCreate( - holdingIdentityResolver.getByUsername(gpIssuer), - holdingIdentityResolver.getByUsername(gpAcquier), - gpReceiverColor, - gpMessage); - - System.out.println("Create GP UUID "+ gpUuid); - - assertThatThrownBy(() -> { // Issuer can not reject - cordaClient.gameProposalReject( - holdingIdentityResolver.getByUsername(gpIssuer), - gpUuid); - }); - - final String rejectRes = cordaClient.gameProposalReject( - holdingIdentityResolver.getByUsername(gpAcquier), - gpUuid); - - assertThat(rejectRes).isEqualToIgnoringCase("REJECTED"); - - List gpListSender = cordaClient.gameProposalList( - holdingIdentityResolver.getByUsername(gpIssuer)); - - assertThat(findByUuid(gpListSender, gpUuid)).isNull(); - - List gpListReceiver = cordaClient.gameProposalList( - holdingIdentityResolver.getByUsername(gpAcquier)); - - assertThat(findByUuid(gpListReceiver, gpUuid)).isNull(); - - // GameProposal can not be rejected twice - assertThatThrownBy(() -> { - cordaClient.gameProposalReject( - holdingIdentityResolver.getByUsername(gpIssuer), - gpUuid); - }); - } - - @Test - void testGameProposalAccept() throws JsonMappingException, JsonProcessingException { - final String gpIssuer = "alice"; - final String gpAcquier = "bob"; - final Piece.Color gpAcquierColor = Piece.Color.WHITE; - final String gpMessage = "GameProposal ACCEPT test"; - - final UUID gpUuid = cordaClient.gameProposalCreate( - holdingIdentityResolver.getByUsername(gpIssuer), - holdingIdentityResolver.getByUsername(gpAcquier), - gpAcquierColor, - gpMessage - ); - - System.out.println("New GameProposal UUID "+ gpUuid); - - assertThatThrownBy(() -> { // Issuer can not accept - cordaClient.gameProposalAccept( - holdingIdentityResolver.getByUsername(gpIssuer), - gpUuid); - }); - - final GameBoard gbState = cordaClient.gameProposalAccept( - holdingIdentityResolver.getByUsername(gpAcquier), - gpUuid); - - System.out.println("New GameBoard UUID "+gbState); - - List gbListIssuer = cordaClient.gameBoardList( - holdingIdentityResolver.getByUsername(gpIssuer)); - - GameBoard gbAlice = findByUuid(gbListIssuer, gbState.gameUuid()); - assertThat(gbAlice).isNotNull(); - assertThat(gbAlice.opponentName()).isEqualToIgnoringCase(gpAcquier); - assertThat(gbAlice.opponentColor()).isEqualByComparingTo(gpAcquierColor); - assertThat(gbAlice.opponentMove()).isEqualTo(true); - - - List gbListAcquier = cordaClient.gameBoardList( - holdingIdentityResolver.getByUsername(gpAcquier)); - - GameBoard bgBob = findByUuid(gbListAcquier, gbState.gameUuid()); - assertThat(bgBob).isNotNull(); - assertThat(bgBob.opponentName()).isEqualToIgnoringCase(gpIssuer); - assertThat(bgBob.opponentColor()).isEqualByComparingTo(Piece.Color.BLACK); - assertThat(bgBob.opponentMove()).isEqualTo(false); - - assertThatThrownBy(() -> { // GameProposal can not be accepted twice - cordaClient.gameProposalAccept( - holdingIdentityResolver.getByUsername(gpAcquier), - gpUuid); - }); - } - - @Test - void testGameBoardList() throws JsonMappingException, JsonProcessingException { - List gbList = cordaClient.gameBoardList( - holdingIdentityResolver.getByUsername("bob")); - - System.out.println("GB list: " +gbList); - } - - @Test - void testGameBoardSurrender() throws JsonMappingException, JsonProcessingException, InvalidNameException { - 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 GameBoard gbState = cordaClient.gameProposalAccept( - hiBob, gpUuid - ); - - System.out.println("New GameBoard UUID "+ gbState.gameUuid()); - - assertThatThrownBy(() -> { // Alice can not surrender, since it is Bob's turn - cordaClient.gameBoardSurrender( - hiAlice, gbState.gameUuid()); - }); - - final GameResult gameResult = cordaClient.gameBoardSurrender( - hiBob, gbState.gameUuid()); - - assertThat(gameResult.whitePlayerName()).isEqualTo(hiBob.getName()); - assertThat(gameResult.blackPlayerName()).isEqualTo(hiAlice.getName()); - assertThat(gameResult.victoryColor()).isEqualByComparingTo(Piece.Color.BLACK); - } - - @Test - void testGameBoardMove() throws JsonMappingException, JsonProcessingException, InvalidNameException { - 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 MOVE test" - ); - - System.out.println("New GameProposal UUID "+ gpUuid); - - final GameBoard gbState = cordaClient.gameProposalAccept( - hiBob, gpUuid - ); - - System.out.println("New GameBoard UUID "+ gbState.gameUuid()); - - assertThatThrownBy(() -> { // Alice can not move, since it is Bob's turn - cordaClient.gameBoardMove( - hiAlice, gbState.gameUuid(), - Arrays.asList(1, 2)); - }); - - final GameBoard gameBoard = cordaClient.gameBoardMove( - hiBob, gbState.gameUuid(), Arrays.asList(1, 2)); - } - - private T findByUuid(List statesList, UUID uuid) { - for (T state : statesList) { - if (state.gameUuid().compareTo(uuid) == 0) - return state; - }; - return null; - } -} diff --git a/backend/src/test/java/djmil/cordacheckers/cordaclient/GameBoardTests.java b/backend/src/test/java/djmil/cordacheckers/cordaclient/GameBoardTests.java new file mode 100644 index 0000000..6bc74a8 --- /dev/null +++ b/backend/src/test/java/djmil/cordacheckers/cordaclient/GameBoardTests.java @@ -0,0 +1,99 @@ +package djmil.cordacheckers.cordaclient; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import djmil.cordacheckers.cordaclient.dao.GameState; +import djmil.cordacheckers.cordaclient.dao.GameState.Status; +import djmil.cordacheckers.cordaclient.dao.Piece; +import djmil.cordacheckers.user.HoldingIdentityResolver; + +@SpringBootTest +public class GameBoardTests { + @Autowired + CordaClient cordaClient; + + @Autowired + HoldingIdentityResolver holdingIdentityResolver; + + final String whitePlayerName = "alice"; + final String blackPlayerName = "bob"; + + @Test + void testSurrender() { + final var hiWhite = holdingIdentityResolver.getByUsername(whitePlayerName); + final var hiBlack = holdingIdentityResolver.getByUsername(blackPlayerName); + final String message = "GameBoard SURRENDER test"; + + final GameState game = cordaClient.gameProposalCreate( + hiWhite, hiBlack, Piece.Color.BLACK, message); + + System.out.println("Game UUID " +game.uuid()); + + final GameState acceptedGameBlackView = cordaClient.gameProposalAccept( + hiBlack, game.uuid()); + + assertThat(acceptedGameBlackView.opponentColor()).isEqualByComparingTo(Piece.Color.WHITE); + assertThat(acceptedGameBlackView.status()).isEqualByComparingTo(Status.GAME_BOARD_WAIT_FOR_OPPONENT); + + assertThatThrownBy(() -> { // Black can not surrender, since it is opponent's turn + cordaClient.gameBoardSurrender( + hiBlack, game.uuid()); + }); + + final GameState surrendererGameView = cordaClient.gameBoardSurrender( + hiWhite, game.uuid()); + + assertThat(surrendererGameView.opponentName()).isEqualToIgnoringCase(blackPlayerName); + assertThat(surrendererGameView.opponentColor()).isEqualByComparingTo(Piece.Color.BLACK); + assertThat(surrendererGameView.status()).isEqualByComparingTo(Status.GAME_RESULT_YOU_LOOSE); + + final GameState winnerGameView = cordaClient.gameStateGet( + hiBlack, game.uuid()); + + assertThat(winnerGameView.opponentName()).isEqualToIgnoringCase(whitePlayerName); + assertThat(winnerGameView.opponentColor()).isEqualByComparingTo(Piece.Color.WHITE); + assertThat(winnerGameView.status()).isEqualByComparingTo(Status.GAME_RESULT_YOU_WON); + } + + // @Test + // void testGameBoardMove() throws JsonMappingException, JsonProcessingException, InvalidNameException { + // 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 MOVE test" + // ); + + // System.out.println("New GameProposal UUID "+ gpUuid); + + // final GameBoard gbState = cordaClient.gameProposalAccept( + // hiBob, gpUuid + // ); + + // System.out.println("New GameBoard UUID "+ gbState.gameUuid()); + + // assertThatThrownBy(() -> { // Alice can not move, since it is Bob's turn + // cordaClient.gameBoardMove( + // hiAlice, gbState.gameUuid(), + // Arrays.asList(1, 2)); + // }); + + // final GameBoard gameBoard = cordaClient.gameBoardMove( + // hiBob, gbState.gameUuid(), Arrays.asList(1, 2)); + // } + + // private T findByUuid(List statesList, UUID uuid) { + // for (T state : statesList) { + // if (state.uuid().compareTo(uuid) == 0) + // return state; + // }; + // return null; + // } +} diff --git a/backend/src/test/java/djmil/cordacheckers/cordaclient/GameProposalTests.java b/backend/src/test/java/djmil/cordacheckers/cordaclient/GameProposalTests.java new file mode 100644 index 0000000..e8de3f3 --- /dev/null +++ b/backend/src/test/java/djmil/cordacheckers/cordaclient/GameProposalTests.java @@ -0,0 +1,171 @@ +package djmil.cordacheckers.cordaclient; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import djmil.cordacheckers.cordaclient.dao.GameState; +import djmil.cordacheckers.cordaclient.dao.GameState.Status; +import djmil.cordacheckers.cordaclient.dao.Piece; +import djmil.cordacheckers.user.HoldingIdentityResolver; + +@SpringBootTest +public class GameProposalTests { + @Autowired + CordaClient cordaClient; + + @Autowired + HoldingIdentityResolver holdingIdentityResolver; + + final String issuer = "alice"; + final String acquier = "bob"; + final Piece.Color acquierColor = Piece.Color.WHITE; + + @Test + void testCreate() { + final var hiIssuer = holdingIdentityResolver.getByUsername(issuer); + final var hiAcquier = holdingIdentityResolver.getByUsername(acquier); + final String message = "GameProposal CREATE test"; + + final GameState issuerGameView = cordaClient.gameProposalCreate( + hiIssuer, + hiAcquier, + acquierColor, + message); + + System.out.println("Game UUID " +issuerGameView.uuid()); + + /* + * Issuers perspective on a newly created GameProposal + */ + assertThat(issuerGameView.status()).isEqualByComparingTo(Status.GAME_PROPOSAL_WAIT_FOR_OPPONENT); + assertThat(issuerGameView.opponentName()).isEqualToIgnoringCase(acquier); + assertThat(issuerGameView.opponentColor()).isEqualByComparingTo(acquierColor); + assertThat(issuerGameView.message()).isEqualToIgnoringCase(message); + assertThat(issuerGameView.uuid()).isNotNull(); + + /* + * Acquier shall be able to find newly created GameProposal + */ + GameState acquierGameView = assertDoesNotThrow( () -> { + return cordaClient.gameStateList(hiAcquier) + .stream() + .filter(gameView -> gameView.uuid().equals(issuerGameView.uuid())) + .reduce((a, b) -> {throw new IllegalStateException("Duplicate");}) + .get(); + }); + + assertThat(acquierGameView.status()).isEqualByComparingTo(Status.GAME_PROPOSAL_WAIT_FOR_YOU); + assertThat(acquierGameView.opponentName()).isEqualToIgnoringCase(issuer); + assertThat(acquierGameView.opponentColor()).isEqualByComparingTo(acquierColor.opposite()); + assertThat(acquierGameView.message()).isEqualToIgnoringCase(message); + assertThat(acquierGameView.uuid()).isEqualByComparingTo(issuerGameView.uuid()); + } + + @Test + void testReject() { + final var hiIssuer = holdingIdentityResolver.getByUsername(issuer); + final var hiAcquier = holdingIdentityResolver.getByUsername(acquier); + final String message = "GameProposal REJECT test"; + + final GameState game = cordaClient.gameProposalCreate( + hiIssuer, hiAcquier, acquierColor, message); + + System.out.println("Game UUID " +game.uuid()); + + assertThatThrownBy(() -> { // Issuer can not reject + cordaClient.gameProposalReject( + hiIssuer, game.uuid()); + }); + + final GameState pendingGameView = cordaClient.gameStateGet( + hiAcquier, game.uuid()); + assertThat(pendingGameView.status()).isEqualByComparingTo(Status.GAME_PROPOSAL_WAIT_FOR_YOU); + assertThat(pendingGameView.uuid()).isEqualByComparingTo(game.uuid()); + + final GameState rejectedGameView = cordaClient.gameProposalReject( + hiAcquier, game.uuid()); + assertThat(rejectedGameView.status()).isEqualByComparingTo(Status.GAME_PROPOSAL_REJECTED); + assertThat(rejectedGameView.uuid()).isEqualByComparingTo(game.uuid()); + + assertThatThrownBy(() -> { // Issuer has no pending GameProposal + cordaClient.gameStateGet( + hiIssuer, game.uuid()); + }); + } + + @Test + void testCancel() { + final var hiIssuer = holdingIdentityResolver.getByUsername(issuer); + final var hiAcquier = holdingIdentityResolver.getByUsername(acquier); + final String message = "GameProposal CANCEL test"; + + final GameState game = cordaClient.gameProposalCreate( + hiIssuer, hiAcquier, acquierColor, message); + + System.out.println("Game UUID " +game.uuid()); + + assertThatThrownBy(() -> { // Acquier can not cancel + cordaClient.gameProposalCancel( + hiAcquier, game.uuid()); + }); + + final GameState pendingGameView = cordaClient.gameStateGet( + hiIssuer, game.uuid()); + assertThat(pendingGameView.status()).isEqualByComparingTo(Status.GAME_PROPOSAL_WAIT_FOR_OPPONENT); + assertThat(pendingGameView.uuid()).isEqualByComparingTo(game.uuid()); + + final GameState canceledGameView = cordaClient.gameProposalCancel( + hiIssuer, game.uuid()); + assertThat(canceledGameView.status()).isEqualByComparingTo(Status.GAME_PROPOSAL_CANCELED); + assertThat(canceledGameView.uuid()).isEqualByComparingTo(game.uuid()); + + assertThatThrownBy(() -> { // Acquier has no pending GameProposal + cordaClient.gameStateGet( + hiAcquier, game.uuid()); + }); + } + + @Test + void testAccept() { + final var hiIssuer = holdingIdentityResolver.getByUsername(issuer); + final var hiAcquier = holdingIdentityResolver.getByUsername(acquier); + final String message = "GameProposal ACCEPT test"; + + final GameState game = cordaClient.gameProposalCreate( + hiIssuer, hiAcquier, acquierColor, message); + + System.out.println("Game UUID " +game.uuid()); + + assertThatThrownBy(() -> { // Issuer can not accept + cordaClient.gameProposalAccept( + hiIssuer, game.uuid()); + }); + + final GameState acceptedGameAcquierView = cordaClient.gameProposalAccept( + hiAcquier, game.uuid()); + + assertThat(acceptedGameAcquierView.opponentName()).isEqualToIgnoringCase(issuer); + assertThat(acceptedGameAcquierView.opponentColor()).isEqualByComparingTo(acquierColor.opposite()); + assertThat(acceptedGameAcquierView.status()).isEqualByComparingTo(Status.GAME_BOARD_WAIT_FOR_YOU); + assertThat(acceptedGameAcquierView.uuid()).isEqualByComparingTo(game.uuid()); + + assertThatThrownBy(() -> { // same GameProposal can not be accepted twice + cordaClient.gameProposalAccept( + hiIssuer, game.uuid()); + }); + + final GameState acceptedGameIssuerView = cordaClient.gameStateGet( + hiIssuer, game.uuid()); + + assertThat(acceptedGameIssuerView.opponentName()).isEqualToIgnoringCase(acquier); + assertThat(acceptedGameIssuerView.opponentColor()).isEqualByComparingTo(acquierColor); + assertThat(acceptedGameIssuerView.status()).isEqualByComparingTo(Status.GAME_BOARD_WAIT_FOR_OPPONENT); + assertThat(acceptedGameIssuerView.uuid()).isEqualByComparingTo(game.uuid()); + } + +} diff --git a/backend/src/test/java/djmil/cordacheckers/cordaclient/GameStateTests.java b/backend/src/test/java/djmil/cordacheckers/cordaclient/GameStateTests.java new file mode 100644 index 0000000..1c2f740 --- /dev/null +++ b/backend/src/test/java/djmil/cordacheckers/cordaclient/GameStateTests.java @@ -0,0 +1,53 @@ +package djmil.cordacheckers.cordaclient; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import java.util.UUID; + +import javax.naming.InvalidNameException; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import com.fasterxml.jackson.core.JsonProcessingException; + +import djmil.cordacheckers.cordaclient.dao.GameState; +import djmil.cordacheckers.cordaclient.dao.VirtualNode; +import djmil.cordacheckers.user.HoldingIdentityResolver; + +@SpringBootTest +public class GameStateTests { + @Autowired + CordaClient cordaClient; + + @Autowired + HoldingIdentityResolver holdingIdentityResolver; + + @Test + void testGetVirtualNodeList() throws InvalidNameException { + List vNodes = cordaClient.getVirtualNodeList(); + + assertThat(vNodes.size()).isEqualTo(5); // default vNode config for CSDE + } + + @Test + void testList() throws JsonProcessingException { + List gsList = cordaClient.gameStateList( + holdingIdentityResolver.getByUsername("bob")); + + System.out.println(gsList); + } + + @Test + void testGet() throws JsonProcessingException { + GameState gameStateView = cordaClient.gameStateGet( + holdingIdentityResolver.getByUsername("bob"), + UUID.fromString("cf357d0a-8f64-4599-b9b5-d263163812d4") + ); + + System.out.println(gameStateView); + } + +} diff --git a/backend/src/test/java/djmil/cordacheckers/cordaclient/dao/flow/RequestBodyTest.java b/backend/src/test/java/djmil/cordacheckers/cordaclient/dao/flow/RequestBodyTest.java index de1ca2f..688e875 100644 --- a/backend/src/test/java/djmil/cordacheckers/cordaclient/dao/flow/RequestBodyTest.java +++ b/backend/src/test/java/djmil/cordacheckers/cordaclient/dao/flow/RequestBodyTest.java @@ -5,7 +5,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.json.JsonTest; import org.springframework.boot.test.json.JacksonTester; -import djmil.cordacheckers.cordaclient.dao.flow.arguments.Empty; +import djmil.cordacheckers.cordaclient.dao.flow.arguments.Req; import static org.assertj.core.api.Assertions.assertThat; @@ -23,7 +23,7 @@ public class RequestBodyTest { RequestBody requestBody = new RequestBody( "list-2", "djmil.cordacheckers.gameproposal.ListFlow", - new Empty() + new Req() ); assertThat(jTester.write(requestBody)).isEqualToJson("requestBody/ListFlow.json"); diff --git a/corda/contracts/src/main/java/djmil/cordacheckers/contracts/GameBoardCommand.java b/corda/contracts/src/main/java/djmil/cordacheckers/contracts/GameBoardCommand.java deleted file mode 100644 index e81a117..0000000 --- a/corda/contracts/src/main/java/djmil/cordacheckers/contracts/GameBoardCommand.java +++ /dev/null @@ -1,128 +0,0 @@ -package djmil.cordacheckers.contracts; - -import static djmil.cordacheckers.contracts.UtxoLedgerTransactionUtil.getSingleInputState; -import static djmil.cordacheckers.contracts.UtxoLedgerTransactionUtil.getSingleOutputState; -import static djmil.cordacheckers.contracts.UtxoLedgerTransactionUtil.requireThat; - -import java.util.List; - -import djmil.cordacheckers.states.GameBoardState; -import djmil.cordacheckers.states.GameResultState; -import net.corda.v5.base.annotations.ConstructorForDeserialization; -import net.corda.v5.base.annotations.CordaSerializable; -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.transaction.UtxoLedgerTransaction; - -public class GameBoardCommand implements Command { - - @CordaSerializable - public static enum Type { - MOVE, - SURRENDER, - FINISH; - } - - 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; - } - - @ConstructorForDeserialization - public GameBoardCommand(Type type, List move) { - this.type = type; - this.move = move; - } - - public GameBoardCommand(Type type) { - if (type == Type.MOVE) - throw new CordaRuntimeException (BAD_ACTIONMOVE_CONSTRUCTOR); - - this.type = type; - this.move = null; - } - - public GameBoardCommand(List move) { - this.type = Type.MOVE; - this.move = move; - } - - public Type getType() { - return this.type; - } - - public List getMove() { - return this.move; - } - - public MemberX500Name getInitiator(GameBoardState gameBoardState) { - return gameBoardState.getMovePlayerName(); - } - - public MemberX500Name getRespondent(GameBoardState gameBoardState) { - return gameBoardState.getCounterpartyName(gameBoardState.getMovePlayerName()); - } - - public static void validateSurrenderTrx(UtxoLedgerTransaction trx) { - requireThat(trx.getInputContractStates().size() == 1, SURRENDER_INPUT_STATE); - final var inGameBoardState = getSingleInputState(trx, GameBoardState.class); - - requireThat(trx.getOutputContractStates().size() == 1, SURRENDER_OUTPUT_STATE); - final var outGameResultState = getSingleOutputState(trx, GameResultState.class); - - requireThat(inGameBoardState.getWhitePlayerName().compareTo(outGameResultState.getWinnerName()) == 0, IN_OUT_PARTICIPANTS); - requireThat(inGameBoardState.getBlackPlayerName().compareTo(outGameResultState.getOpponentName()) == 0, IN_OUT_PARTICIPANTS); - } - - public static void validateMoveTrx(UtxoLedgerTransaction trx) { - requireThat(trx.getInputContractStates().size() == 1, MOVE_INPUT_STATE); - final var inGameBoardState = getSingleInputState(trx, GameBoardState.class); - - requireThat(trx.getOutputContractStates().size() == 1, MOVE_OUTPUT_STATE); - final var outGameBoardState = getSingleOutputState(trx, GameBoardState.class); - - requireThat(inGameBoardState.getWhitePlayerName().compareTo(outGameBoardState.getWhitePlayerName()) == 0, IN_OUT_PARTICIPANTS); - requireThat(inGameBoardState.getBlackPlayerName().compareTo(outGameBoardState.getBlackPlayerName()) == 0, IN_OUT_PARTICIPANTS); - } - - public static void validateFinishTrx(UtxoLedgerTransaction trx) { - requireThat(trx.getInputContractStates().size() == 1, FINAL_MOVE_INPUT_STATE); - final var inGameBoardState = getSingleInputState(trx, GameBoardState.class); - - requireThat(trx.getOutputContractStates().size() == 1, FINAL_MOVE_OUTPUT_STATE); - final var outGameResultState = getSingleOutputState(trx, GameResultState.class); - - requireThat(inGameBoardState.getWhitePlayerName().compareTo(outGameResultState.getWinnerName()) == 0, IN_OUT_PARTICIPANTS); - requireThat(inGameBoardState.getBlackPlayerName().compareTo(outGameResultState.getOpponentName()) == 0, IN_OUT_PARTICIPANTS); - } - - static final String BAD_ACTIONMOVE_CONSTRUCTOR = "Bad constructor for Action.MOVE"; - - static final String SURRENDER_INPUT_STATE = "SURRENDER command should have exactly one GameBoardState input state"; - static final String SURRENDER_OUTPUT_STATE = "SURRENDER command should have exactly one GameResultState output state"; - - static final String MOVE_INPUT_STATE = "MOVE command should have exactly one GameBoardState input state"; - static final String MOVE_OUTPUT_STATE = "MOVE command should have exactly one GameBoardState output state"; - static final String MOVE_OUT_BOARD = "MOVE command: move checkers rules violation"; - static final String MOVE_OUT_COLOR = "MOVE command: moveColor checkers rules violation"; - - static final String FINAL_MOVE_INPUT_STATE = "FINAL_MOVE command should have exactly one GameBoardState input state"; - static final String FINAL_MOVE_OUTPUT_STATE = "FINAL_MOVE command should have exactly one GameResultState output state"; - - static final String IN_OUT_PARTICIPANTS = "InputState and OutputState participants do not match"; - - public static class CommandTypeException extends RuntimeException { - public CommandTypeException() { - super("Bad GameBoardCommand type"); - } - public CommandTypeException(String message) { - super(message); - } - } - -} 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 55a3b62..83e12f4 100644 --- a/corda/contracts/src/main/java/djmil/cordacheckers/contracts/GameBoardContract.java +++ b/corda/contracts/src/main/java/djmil/cordacheckers/contracts/GameBoardContract.java @@ -1,12 +1,11 @@ package djmil.cordacheckers.contracts; +import static djmil.cordacheckers.contracts.GameCommand.requireThat; import static djmil.cordacheckers.contracts.UtxoLedgerTransactionUtil.getSingleCommand; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import net.corda.v5.base.exceptions.CordaRuntimeException; -import net.corda.v5.ledger.utxo.Command; import net.corda.v5.ledger.utxo.transaction.UtxoLedgerTransaction; public class GameBoardContract implements net.corda.v5.ledger.utxo.Contract { @@ -17,52 +16,25 @@ public class GameBoardContract implements net.corda.v5.ledger.utxo.Contract { public void verify(UtxoLedgerTransaction trx) { log.info("GameBoardContract.verify() called"); - requireThat(trx.getCommands().size() == 1, REQUIRE_SINGLE_COMMAND); - final Command command = getSingleCommand(trx, Command.class); + requireThat(trx.getCommands().size() == 1, GameCommand.REQUIRE_SINGLE_COMMAND); + final GameCommand command = getSingleCommand(trx, GameCommand.class); - if (command instanceof GameProposalCommand) { - log.info("GameBoardContract.verify() as GameProposalCommand "+(GameProposalCommand)command); - switch ((GameProposalCommand)command) { - case ACCEPT: - GameProposalCommand.validateAcceptTrx(trx); - break; + switch (command.action) { + case GAME_PROPOSAL_ACCEPT: + GameCommand.validateGameProposalAccept(trx); + break; + + case GAME_BOARD_MOVE: + GameCommand.validateGameBoardMove(trx); + break; - default: - throw new CordaRuntimeException("UNKNOWN_COMMAND"); - } - } else - if (command instanceof GameBoardCommand) { - log.info("GameBoardContract.verify() as GameBoardCommand "+((GameBoardCommand)command).getType()); - switch (((GameBoardCommand)command).getType()) { - case MOVE: - GameBoardCommand.validateMoveTrx(trx); - break; - - case SURRENDER: - GameBoardCommand.validateSurrenderTrx(trx); - break; + case GAME_BOARD_SURRENDER: + GameCommand.validateGameBoardSurrender(trx); + break; - case FINISH: - GameBoardCommand.validateFinishTrx(trx); - break; - - default: - throw new GameBoardCommand.CommandTypeException(); - } - } else { - throw new RuntimeException("Bad utxo command type"); + default: + throw new GameCommand.ActionException(); } } - - private static void requireThat(boolean asserted, String errorMessage) { - if(!asserted) { - throw new CordaRuntimeException("Failed requirement: " + errorMessage); - } - } - - static final String REQUIRE_SINGLE_COMMAND = "Require a single command"; - - static final String SINGLE_STATE_EXPECTED = "Single state expected"; - static final String NO_REFERANCE_GAMEPROPOSAL_STATE_FOR_TRXID = "No reference GamePropsal state found for trx.id "; } diff --git a/corda/contracts/src/main/java/djmil/cordacheckers/contracts/GameCommand.java b/corda/contracts/src/main/java/djmil/cordacheckers/contracts/GameCommand.java new file mode 100644 index 0000000..caa87fd --- /dev/null +++ b/corda/contracts/src/main/java/djmil/cordacheckers/contracts/GameCommand.java @@ -0,0 +1,243 @@ +package djmil.cordacheckers.contracts; + +import djmil.cordacheckers.states.GameBoardState; +import djmil.cordacheckers.states.GameProposalState; +import djmil.cordacheckers.states.GameResultState; +import djmil.cordacheckers.states.GameState; +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.Command; +import net.corda.v5.ledger.utxo.StateAndRef; +import net.corda.v5.ledger.utxo.transaction.UtxoLedgerTransaction; + +import java.lang.reflect.Array; +import java.util.LinkedList; +import java.util.List; + +import static djmil.cordacheckers.contracts.UtxoLedgerTransactionUtil.getSingleInputState; +import static djmil.cordacheckers.contracts.UtxoLedgerTransactionUtil.getSingleOutputState; + +public class GameCommand implements Command { + @CordaSerializable + public static enum Action { + GAME_PROPOSAL_CREATE, + GAME_PROPOSAL_CANCEL, + GAME_PROPOSAL_ACCEPT, + GAME_PROPOSAL_REJECT, + + GAME_BOARD_MOVE, + GAME_BOARD_SURRENDER, + + GAME_RESULT_CREATE; + } + + public final Action action; + public final List move; // [0] from, [1] to + + public static class ActionException extends RuntimeException { + public ActionException() { + super("Bad GameCommand.Action value"); + } + } + + // Serialisation service requires a default constructor + public GameCommand() { + this.action = null; + this.move = null; + } + + public GameCommand(Action action) { + if (action == Action.GAME_BOARD_MOVE) + throw new ActionException(); + + this.action = action; + this.move = null; + } + + public GameCommand(List move) { + this.action = Action.GAME_BOARD_MOVE; + this.move = move; + } + + @ConstructorForDeserialization + public GameCommand(Action action, List move) { + this.action = action; + this.move = move; + } + + public Action getAction() { + return action; + } + + public List getMove() { + return move; + } + + + /* + * Session initiator/respondent + */ + + public MemberX500Name getObserver(StateAndRef gameStateSar) { + final GameState gameState = gameStateSar.getState().getContractState(); + return gameState.getCounterpartyName(getActor(gameState)); + } + + public MemberX500Name getActor(StateAndRef gameStateSar) { + return getActor(gameStateSar.getState().getContractState()); + } + + public MemberX500Name getObserver(GameState gameState) { + return gameState.getCounterpartyName(getActor(gameState)); + } + + public MemberX500Name getActor(GameState gameState) { + switch (action) { + case GAME_PROPOSAL_CREATE: + case GAME_PROPOSAL_CANCEL: + if (gameState instanceof GameProposalState) + return ((GameProposalState)gameState).getIssuer(); + break; + + case GAME_PROPOSAL_REJECT: + if (gameState instanceof GameProposalState) + return ((GameProposalState)gameState).getAcquier(); + break; + + case GAME_PROPOSAL_ACCEPT: + if (gameState instanceof GameProposalState) // <<-- Session validation perspective + return ((GameProposalState)gameState).getAcquier(); + if (gameState instanceof GameBoardState) // <<-- GameViewBuilder perspective + return ((GameBoardState)gameState).getMovePlayerName(); + break; + + case GAME_BOARD_MOVE: + if (gameState instanceof GameBoardState) + return ((GameBoardState)gameState).getMovePlayerName(); + break; + + case GAME_BOARD_SURRENDER: + if (gameState instanceof GameResultState) + return ((GameResultState)gameState).getOpponentName(); + break; + + case GAME_RESULT_CREATE: + if (gameState instanceof GameResultState) + return ((GameResultState)gameState).getWinnerName(); + break; + + default: + throw new ActionException(); + } + + throw new RuntimeException(action +": unexpected GameState type " +gameState.getClass()); + } + + /* + * Transaction validation + */ + + public static void validateGameProposalCreate(UtxoLedgerTransaction trx) { + requireThat(trx.getInputContractStates().isEmpty(), CREATE_INPUT_STATE); + requireThat(trx.getOutputContractStates().size() == 1, CREATE_OUTPUT_STATE); + + GameProposalState outputState = getSingleOutputState(trx, GameProposalState.class); + + requireThat(outputState.getAcquierColor() != null, NON_NULL_RECIPIENT_COLOR); + } + + public static void validateGameProposalAccept(UtxoLedgerTransaction trx) { + requireThat(trx.getInputContractStates().size() == 1, ACCEPT_INPUT_STATE); + requireThat(trx.getOutputContractStates().size() == 1, ACCEPT_OUTPUT_STATE); + + GameProposalState inGameProposal = getSingleInputState(trx, GameProposalState.class); + GameBoardState outGameBoard = getSingleOutputState(trx, GameBoardState.class); + + requireThat(outGameBoard.getParticipants().containsAll(inGameProposal.getParticipants()), IN_OUT_PARTICIPANTS); + } + + public static void validateGameProposalReject(UtxoLedgerTransaction trx) { + requireThat(trx.getInputContractStates().size() == 1, REJECT_INPUT_STATE); + requireThat(trx.getOutputContractStates().isEmpty(), REJECT_OUTPUT_STATE); + + getSingleInputState(trx, GameProposalState.class); + } + + public static void validateGameProposalCancel(UtxoLedgerTransaction trx) { + requireThat(trx.getInputContractStates().size() == 1, CANCEL_INPUT_STATE); + requireThat(trx.getOutputContractStates().isEmpty(), CANCEL_OUTPUT_STATE); + + getSingleInputState(trx, GameProposalState.class); + } + + public static void validateGameBoardSurrender(UtxoLedgerTransaction trx) { + requireThat(trx.getInputContractStates().size() == 1, SURRENDER_INPUT_STATE); + final var inGameBoardState = getSingleInputState(trx, GameBoardState.class); + + requireThat(trx.getOutputContractStates().size() == 1, SURRENDER_OUTPUT_STATE); + final var outGameResultState = getSingleOutputState(trx, GameResultState.class); + + + List inActors = new LinkedList(List.of( + inGameBoardState.getWhitePlayerName(), + inGameBoardState.getBlackPlayerName()) + ); + + List outActors = new LinkedList(List.of( + outGameResultState.getWinnerName(), + outGameResultState.getOpponentName()) + ); + + requireThat(inActors.containsAll(outActors) && outActors.containsAll(inActors), IN_OUT_PARTICIPANTS); + + final var activePlayerName = outGameResultState.getCounterpartyName(outGameResultState.getWinnerName()); + requireThat(inGameBoardState.getMovePlayerName().compareTo(activePlayerName) == 0, SURRENDER_ACTOR + "expected " +inGameBoardState.getMovePlayerName() +" actual " +activePlayerName); + } + + public static void validateGameBoardMove(UtxoLedgerTransaction trx) { + requireThat(trx.getInputContractStates().size() == 1, MOVE_INPUT_STATE); + final var inGameBoardState = getSingleInputState(trx, GameBoardState.class); + + requireThat(trx.getOutputContractStates().size() == 1, MOVE_OUTPUT_STATE); + final var outGameBoardState = getSingleOutputState(trx, GameBoardState.class); + + requireThat(inGameBoardState.getWhitePlayerName().compareTo(outGameBoardState.getWhitePlayerName()) == 0, IN_OUT_PARTICIPANTS); + requireThat(inGameBoardState.getBlackPlayerName().compareTo(outGameBoardState.getBlackPlayerName()) == 0, IN_OUT_PARTICIPANTS); + } + + public static void validateGameResultCreate(UtxoLedgerTransaction trx) { + throw new RuntimeException("Unimplemented"); + } + + public static void requireThat(boolean asserted, String errorMessage) { + if (!asserted) { + throw new IllegalStateException("Failed requirement: " + errorMessage); + } + } + + static final String REQUIRE_SINGLE_COMMAND = "Require a single command"; + static final String IN_OUT_PARTICIPANTS = "Input participants should represent output participants"; + + 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 REJECT_INPUT_STATE = "Reject command should have exactly one GameProposal input 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 no output states"; + + static final String ACCEPT_INPUT_STATE = "Accept command should have exactly one GameProposal input state"; + static final String ACCEPT_OUTPUT_STATE = "Accept command should have exactly one GameBoard output state"; + + static final String SURRENDER_INPUT_STATE = "SURRENDER command should have exactly one GameBoardState input state"; + static final String SURRENDER_OUTPUT_STATE = "SURRENDER command should have exactly one GameResultState output state"; + static final String SURRENDER_ACTOR = "Only active player can SURRENDER"; + + static final String MOVE_INPUT_STATE = "MOVE command should have exactly one GameBoardState input state"; + static final String MOVE_OUTPUT_STATE = "MOVE command should have exactly one GameBoardState output state"; + static final String MOVE_OUT_BOARD = "MOVE command: move checkers rules violation"; + static final String MOVE_OUT_COLOR = "MOVE command: moveColor checkers rules violation"; +} diff --git a/corda/contracts/src/main/java/djmil/cordacheckers/contracts/GameProposalCommand.java b/corda/contracts/src/main/java/djmil/cordacheckers/contracts/GameProposalCommand.java deleted file mode 100644 index 3ea36dd..0000000 --- a/corda/contracts/src/main/java/djmil/cordacheckers/contracts/GameProposalCommand.java +++ /dev/null @@ -1,102 +0,0 @@ -package djmil.cordacheckers.contracts; - -import djmil.cordacheckers.states.GameBoardState; -import djmil.cordacheckers.states.GameProposalState; -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.transaction.UtxoLedgerTransaction; - -import static djmil.cordacheckers.contracts.UtxoLedgerTransactionUtil.getSingleInputState; -import static djmil.cordacheckers.contracts.UtxoLedgerTransactionUtil.getSingleOutputState; -import static djmil.cordacheckers.contracts.UtxoLedgerTransactionUtil.requireThat; - -public enum GameProposalCommand implements Command { - CREATE, - ACCEPT, - REJECT, - CANCEL; - - public MemberX500Name getInitiator(GameProposalState gameProposalState) { - switch (this) { - case CREATE: - case CANCEL: - return gameProposalState.getIssuer(); - case ACCEPT: - case REJECT: - return gameProposalState.getAcquier(); - default: - throw new RuntimeException(UNSUPPORTED_VALUE_OF + this.name()); - } - } - - public MemberX500Name getRespondent(GameProposalState gameProposalState) { - switch (this) { - case CREATE: - case CANCEL: - return gameProposalState.getAcquier(); - case ACCEPT: - case REJECT: - return gameProposalState.getIssuer(); - default: - throw new RuntimeException(UNSUPPORTED_VALUE_OF + this.name()); - } - } - - public MemberX500Name getInitiator(StateAndRef gameProposalSar) { - return getInitiator(gameProposalSar.getState().getContractState()); - } - - public MemberX500Name getRespondent(StateAndRef gameProposalSar) { - return getRespondent(gameProposalSar.getState().getContractState()); - } - - public static void validateCreateTrx(UtxoLedgerTransaction trx) { - requireThat(trx.getInputContractStates().isEmpty(), CREATE_INPUT_STATE); - requireThat(trx.getOutputContractStates().size() == 1, CREATE_OUTPUT_STATE); - - GameProposalState outputState = getSingleOutputState(trx, GameProposalState.class); - - requireThat(outputState.getAcquierColor() != null, NON_NULL_RECIPIENT_COLOR); - } - - public static void validateAcceptTrx(UtxoLedgerTransaction trx) { - requireThat(trx.getInputContractStates().size() == 1, ACCEPT_INPUT_STATE); - requireThat(trx.getOutputContractStates().size() == 1, ACCEPT_OUTPUT_STATE); - - GameProposalState inGameProposal = getSingleInputState(trx, GameProposalState.class); - GameBoardState outGameBoard = getSingleOutputState(trx, GameBoardState.class); - - requireThat(outGameBoard.getParticipants().containsAll(inGameProposal.getParticipants()), ACCEPT_PARTICIPANTS); - } - - public static void validateRejectTrx(UtxoLedgerTransaction trx) { - requireThat(trx.getInputContractStates().size() == 1, REJECT_INPUT_STATE); - requireThat(trx.getOutputContractStates().isEmpty(), REJECT_OUTPUT_STATE); - - getSingleInputState(trx, GameProposalState.class); - } - - public static void validateCancelTrx(UtxoLedgerTransaction trx) { - requireThat(trx.getInputContractStates().size() == 1, CANCEL_INPUT_STATE); - requireThat(trx.getOutputContractStates().isEmpty(), CANCEL_OUTPUT_STATE); - - getSingleInputState(trx, GameProposalState.class); - } - - public static final String UNSUPPORTED_VALUE_OF = "Unsupported GameProposalCommand value: "; - - 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 REJECT_INPUT_STATE = "Reject command should have exactly one GameProposal input 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 no output states"; - - static final String ACCEPT_INPUT_STATE = "Accept command should have exactly one GameProposal input state"; - static final String ACCEPT_OUTPUT_STATE = "Accept command should have exactly one GameBoard output state"; - static final String ACCEPT_PARTICIPANTS = "Accept command: GameBoard participants should math GameProposal participants"; -} 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 0044774..0cfdc36 100644 --- a/corda/contracts/src/main/java/djmil/cordacheckers/contracts/GameProposalContract.java +++ b/corda/contracts/src/main/java/djmil/cordacheckers/contracts/GameProposalContract.java @@ -1,14 +1,13 @@ package djmil.cordacheckers.contracts; +import static djmil.cordacheckers.contracts.GameCommand.requireThat; +import static djmil.cordacheckers.contracts.UtxoLedgerTransactionUtil.getSingleCommand; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import net.corda.v5.base.exceptions.CordaRuntimeException; import net.corda.v5.ledger.utxo.transaction.UtxoLedgerTransaction; -import static djmil.cordacheckers.contracts.UtxoLedgerTransactionUtil.getSingleCommand; -import static djmil.cordacheckers.contracts.UtxoLedgerTransactionUtil.requireThat; - public class GameProposalContract implements net.corda.v5.ledger.utxo.Contract { private final static Logger log = LoggerFactory.getLogger(GameProposalContract.class); @@ -17,31 +16,29 @@ public class GameProposalContract implements net.corda.v5.ledger.utxo.Contract { public void verify(UtxoLedgerTransaction trx) { log.info("GameProposalContract.verify() called"); - requireThat(trx.getCommands().size() == 1, REQUIRE_SINGLE_COMMAND); - final GameProposalCommand command = getSingleCommand(trx, GameProposalCommand.class); + requireThat(trx.getCommands().size() == 1, GameCommand.REQUIRE_SINGLE_COMMAND); + final GameCommand command = getSingleCommand(trx, GameCommand.class); - switch (command) { - case CREATE: - GameProposalCommand.validateCreateTrx(trx); + switch (command.action) { + case GAME_PROPOSAL_CREATE: + GameCommand.validateGameProposalCreate(trx); break; - case ACCEPT: - GameProposalCommand.validateAcceptTrx(trx); + case GAME_PROPOSAL_ACCEPT: + GameCommand.validateGameProposalAccept(trx); break; - case REJECT: - GameProposalCommand.validateRejectTrx(trx); + case GAME_PROPOSAL_REJECT: + GameCommand.validateGameProposalReject(trx); break; - case CANCEL: - GameProposalCommand.validateCancelTrx(trx); + case GAME_PROPOSAL_CANCEL: + GameCommand.validateGameProposalCancel(trx); break; default: - throw new CordaRuntimeException(UNKNOWN_COMMAND); + throw new GameCommand.ActionException(); } } - static final String REQUIRE_SINGLE_COMMAND = "Require a single command"; - static final String UNKNOWN_COMMAND = "Unsupported command"; - } +} diff --git a/corda/contracts/src/main/java/djmil/cordacheckers/contracts/GameResultContract.java b/corda/contracts/src/main/java/djmil/cordacheckers/contracts/GameResultContract.java index bbcd75b..f9bf240 100644 --- a/corda/contracts/src/main/java/djmil/cordacheckers/contracts/GameResultContract.java +++ b/corda/contracts/src/main/java/djmil/cordacheckers/contracts/GameResultContract.java @@ -1,15 +1,13 @@ package djmil.cordacheckers.contracts; +import static djmil.cordacheckers.contracts.GameCommand.requireThat; import static djmil.cordacheckers.contracts.UtxoLedgerTransactionUtil.getSingleCommand; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import net.corda.v5.base.exceptions.CordaRuntimeException; import net.corda.v5.ledger.utxo.transaction.UtxoLedgerTransaction; -import static djmil.cordacheckers.contracts.UtxoLedgerTransactionUtil.requireThat; - public class GameResultContract implements net.corda.v5.ledger.utxo.Contract { private final static Logger log = LoggerFactory.getLogger(GameResultContract.class); @@ -18,23 +16,21 @@ public class GameResultContract implements net.corda.v5.ledger.utxo.Contract { public void verify(UtxoLedgerTransaction trx) { log.info("GameResultContract.verify() called"); - requireThat(trx.getCommands().size() == 1, REQUIRE_SINGLE_COMMAND); - final GameBoardCommand command = getSingleCommand(trx, GameBoardCommand.class); + requireThat(trx.getCommands().size() == 1, GameCommand.REQUIRE_SINGLE_COMMAND); + final GameCommand command = getSingleCommand(trx, GameCommand.class); - switch (command.getType()) { - case SURRENDER: - GameBoardCommand.validateSurrenderTrx(trx); + switch (command.action) { + case GAME_BOARD_SURRENDER: + GameCommand.validateGameBoardSurrender(trx); break; - case FINISH: - GameBoardCommand.validateFinishTrx(trx); + case GAME_RESULT_CREATE: + GameCommand.validateGameResultCreate(trx); break; default: - throw new CordaRuntimeException(UNKNOWN_COMMAND); + throw new GameCommand.ActionException(); } } - - static final String REQUIRE_SINGLE_COMMAND = "Require a single command"; - static final String UNKNOWN_COMMAND = "Unsupported command"; + } diff --git a/corda/contracts/src/main/java/djmil/cordacheckers/contracts/UtxoLedgerTransactionUtil.java b/corda/contracts/src/main/java/djmil/cordacheckers/contracts/UtxoLedgerTransactionUtil.java index 16a71af..469014a 100644 --- a/corda/contracts/src/main/java/djmil/cordacheckers/contracts/UtxoLedgerTransactionUtil.java +++ b/corda/contracts/src/main/java/djmil/cordacheckers/contracts/UtxoLedgerTransactionUtil.java @@ -3,53 +3,45 @@ 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.StateAndRef; +import djmil.cordacheckers.states.GameState; import net.corda.v5.ledger.utxo.transaction.UtxoLedgerTransaction; public class UtxoLedgerTransactionUtil { - public static T getSingleCommand(UtxoLedgerTransaction utxoTrx, Class clazz) { + public static T getSingleCommand(UtxoLedgerTransaction utxoTrx, Class clazz) { return single(utxoTrx.getCommands(clazz), clazz); } - public static StateAndRef getSingleReferenceSar(UtxoLedgerTransaction utxoTrx, Class clazz) { - return singleSar(utxoTrx.getReferenceStateAndRefs(clazz), clazz); - } + // public static StateAndRef getSingleReferenceSar(UtxoLedgerTransaction utxoTrx, Class clazz) { + // return singleSar(utxoTrx.getReferenceStateAndRefs(clazz), clazz); + // } - public static T getSingleInputState(UtxoLedgerTransaction utxoTrx, Class clazz) { + public static T getSingleInputState(UtxoLedgerTransaction utxoTrx, Class clazz) { return single(utxoTrx.getInputStates(clazz), clazz); } - public static StateAndRef getSingleInputSar(UtxoLedgerTransaction utxoTrx, Class clazz) { - return singleSar(utxoTrx.getInputStateAndRefs(clazz), clazz); - } + // public static StateAndRef getSingleInputSar(UtxoLedgerTransaction utxoTrx, Class clazz) { + // return singleSar(utxoTrx.getInputStateAndRefs(clazz), clazz); + // } - public static T getSingleOutputState(UtxoLedgerTransaction utxoTrx, Class 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 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 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 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); - } - - public static void requireThat(boolean asserted, String errorMessage) { - if (!asserted) { - throw new IllegalStateException("Failed requirement: " + errorMessage); - } - } + // public static Optional getOptionalOutputState(UtxoLedgerTransaction utxoTrx, Class clazz) { + // return optional(utxoTrx.getOutputStates(clazz), clazz); + // } private static Optional optional(List list, Class clazz) { return list @@ -62,13 +54,6 @@ public class UtxoLedgerTransactionUtil { .orElseThrow( () -> new IllegalStateException(NO_INSTANCES_OF +clazz.getName()) ); } - private static StateAndRef singleSar(List> list, Class clazz) { - return list - .stream() - .reduce((a, b) -> {throw new IllegalStateException(MULTIPLE_INSTANCES_OF +clazz.getName());}) - .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/contracts/src/main/java/djmil/cordacheckers/states/GameResultState.java b/corda/contracts/src/main/java/djmil/cordacheckers/states/GameResultState.java index 3ea7414..d15e283 100644 --- a/corda/contracts/src/main/java/djmil/cordacheckers/states/GameResultState.java +++ b/corda/contracts/src/main/java/djmil/cordacheckers/states/GameResultState.java @@ -26,11 +26,11 @@ public class GameResultState extends GameState { this.winnerColor = winnerColor; } - public GameResultState(GameBoardState gameBoardState, Piece.Color victoryColor) { + public GameResultState(GameBoardState gameBoardState, MemberX500Name winnerName) { super(gameBoardState.gameUuid, null, gameBoardState.participants); - this.winnerName = gameBoardState.getWhitePlayerName(); - this.opponentName = gameBoardState.getBlackPlayerName(); - this.winnerColor = victoryColor; + this.opponentName = gameBoardState.getCounterpartyName(winnerName); + this.winnerName = winnerName; + this.winnerColor = gameBoardState.getWhitePlayerName().compareTo(winnerName) == 0 ? Piece.Color.WHITE : Piece.Color.BLACK; } public MemberX500Name getWinnerName() { @@ -57,7 +57,7 @@ public class GameResultState extends GameState { } @Override - MemberX500Name getWhitePlayerName() { + public MemberX500Name getWhitePlayerName() { return winnerColor == Piece.Color.WHITE ? winnerName : opponentName; } diff --git a/corda/contracts/src/main/java/djmil/cordacheckers/states/GameState.java b/corda/contracts/src/main/java/djmil/cordacheckers/states/GameState.java index 61a3b9d..3d71c6e 100644 --- a/corda/contracts/src/main/java/djmil/cordacheckers/states/GameState.java +++ b/corda/contracts/src/main/java/djmil/cordacheckers/states/GameState.java @@ -17,10 +17,10 @@ public abstract class GameState implements ContractState { final List participants; @NotNull - abstract MemberX500Name getWhitePlayerName(); + public abstract MemberX500Name getWhitePlayerName(); @NotNull - abstract MemberX500Name getCounterpartyName(MemberX500Name myName) throws NotInvolved; + public abstract MemberX500Name getCounterpartyName(MemberX500Name myName) throws NotInvolved; GameState(UUID gameUuid, String message, List participants) { this.gameUuid = gameUuid; @@ -49,7 +49,7 @@ public abstract class GameState implements ContractState { return getWhitePlayerName().compareTo(opponentName) == 0 ? Piece.Color.WHITE : Piece.Color.BLACK; } - + public static class NotInvolved extends RuntimeException { public NotInvolved(MemberX500Name name, Class clazz, UUID uuid) { super(name +" is not involved in " +clazz.getSimpleName() +" UUID " +uuid); diff --git a/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/CommandFlow.java b/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/CommandFlow.java deleted file mode 100644 index 35b84da..0000000 --- a/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/CommandFlow.java +++ /dev/null @@ -1,198 +0,0 @@ -package djmil.cordacheckers.gameboard; - -import static djmil.cordacheckers.contracts.UtxoLedgerTransactionUtil.getSingleCommand; -import static djmil.cordacheckers.contracts.UtxoLedgerTransactionUtil.getSingleOutputState; - -import java.time.Duration; -import java.time.Instant; -import java.util.Arrays; -import java.util.List; -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.GameBoardCommand.CommandTypeException; -import djmil.cordacheckers.gameresult.GameResultView; -import djmil.cordacheckers.states.GameBoardState; -import djmil.cordacheckers.states.GameResultState; -import djmil.cordacheckers.states.Piece; -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.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; - -@InitiatingFlow(protocol = "game-board") -public class CommandFlow implements ClientStartableFlow { - - private final static Logger log = LoggerFactory.getLogger(CommandFlow.class); - - @CordaInject - public JsonMarshallingService jsonMarshallingService; - - @CordaInject - public UtxoLedgerService utxoLedgerService; - - @CordaInject - public MemberLookup memberLookup; - - @CordaInject - public FlowMessaging flowMessaging; - - @Override - @Suspendable - public String call(ClientRequestBody requestBody) { - log.info("GameBoardCommandFlow started"); - try { - final CommandFlowArgs args = requestBody.getRequestBodyAs(jsonMarshallingService, CommandFlowArgs.class); - - final StateAndRef utxoSarGameBoard = findUtxoGameBoard(args.getGameBoardUuid()); - - final SecureHash trxId = doTransaction(args.getCommand(), utxoSarGameBoard); - final Object trxResult = getTrxResult(trxId); - - return new FlowResult(trxResult, trxId) - .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 - StateAndRef findUtxoGameBoard (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().getGameUuid().equals(gameBoardUuid)) - .reduce((a, b) -> {throw new IllegalStateException("Multiple states: " +a +", " +b);}) - .get(); - } - - @Suspendable - SecureHash doTransaction( - GameBoardCommand command, - StateAndRef utxoSarGameBoard - ) { - final GameBoardState stateGameBoard = utxoSarGameBoard.getState().getContractState(); - final MemberX500Name myName = memberLookup.myInfo().getName(); - final MemberX500Name opponentName = stateGameBoard.getCounterpartyName(myName); - - UtxoTransactionBuilder trxBuilder = utxoLedgerService.createTransactionBuilder() - .setNotary(utxoSarGameBoard.getState().getNotaryName()) - .setTimeWindowBetween(Instant.now(), Instant.now().plusMillis(Duration.ofDays(1).toMillis())) - .addCommand(command) - .addInputState(utxoSarGameBoard.getRef()) - .addSignatories(stateGameBoard.getParticipants()); - - switch (command.getType()) { - case SURRENDER: - trxBuilder = trxBuilder - .addOutputState( new GameResultState( - stateGameBoard, - stateGameBoard.getCounterpartyColor(myName)) // winner color - ); - break; - - case MOVE: - trxBuilder = trxBuilder - .addOutputState( new GameBoardState(stateGameBoard, stateGameBoard.getBoard(), stateGameBoard.getMoveColor())); // TODO: advance color - break; - - case FINISH: - throw new CommandTypeException("GameBoard.CommandFlow can not process externaly issued FINISH commnd"); - - default: - throw new CommandTypeException("GameBoard.CommandFlow doTransaction(): Unknown command"); - } - - return commit( - trxBuilder.toSignedTransaction(), - opponentName); - } - - @Suspendable - SecureHash commit(UtxoSignedTransaction candidateTrx, MemberX500Name counterpartyName) { - log.info("About to commit " +candidateTrx.getId()); - - final FlowSession session = flowMessaging.initiateFlow(counterpartyName); - - /* - * Calls the Corda provided finalise() function which gather signatures from the counterparty, - * notarises the transaction and persists the transaction to each party's vault. - */ - - final List sessionsList = Arrays.asList(session); - - final SecureHash trxId = utxoLedgerService - .finalize(candidateTrx, sessionsList) - .getTransaction() - .getId(); - - log.info("GameBoard utxo trx id " +trxId); - return trxId; - } - - @Suspendable - Object getTrxResult(SecureHash trxId) { - final UtxoLedgerTransaction utxoTrx = utxoLedgerService - .findLedgerTransaction(trxId); - - final var command = getSingleCommand(utxoTrx, GameBoardCommand.class); - - switch (command.getType()) { - case SURRENDER: - return viewGameResult(utxoTrx); - - case MOVE: - // 1. create initial GameBoardView - // 2. check for winning conditions - // - run subcommnd FINISH that will consume current gbUtxo and will produce GameResult state - // - update GameBoardView to have FINISH command - // 3. return GameBoardView - return viewGameBoard(utxoTrx); - - case FINISH: - return viewGameBoard(utxoTrx); - - default: - throw new CommandTypeException(); - } - } - - @Suspendable - GameResultView viewGameResult(UtxoLedgerTransaction utxoGameResult) { - final GameResultState grState = getSingleOutputState(utxoGameResult, GameResultState.class); - - return new GameResultView(grState); - } - - @Suspendable - GameBoardView viewGameBoard(UtxoLedgerTransaction utxoGameBoard) { - final MemberX500Name myName = memberLookup.myInfo().getName(); - - return new GameBoardView(utxoGameBoard, myName); - } - -} diff --git a/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/CommandFlowArgs.java b/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/CommandFlowArgs.java deleted file mode 100644 index 4ed671a..0000000 --- a/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/CommandFlowArgs.java +++ /dev/null @@ -1,24 +0,0 @@ -package djmil.cordacheckers.gameboard; - -import java.util.UUID; - -import djmil.cordacheckers.contracts.GameBoardCommand; - -public class CommandFlowArgs { - private UUID gameBoardUuid; - private GameBoardCommand command; - - // Serialisation service requires a default constructor - public CommandFlowArgs() { - this.gameBoardUuid = null; - this.command = null; - } - - public GameBoardCommand getCommand() { - return this.command; - } - - public UUID getGameBoardUuid() { - return this.gameBoardUuid; - } -} diff --git a/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/CommandResponderFlow.java b/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/CommandResponderFlow.java deleted file mode 100644 index 1857f30..0000000 --- a/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/CommandResponderFlow.java +++ /dev/null @@ -1,91 +0,0 @@ -package djmil.cordacheckers.gameboard; - -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.contracts.GameBoardCommand; -import djmil.cordacheckers.contracts.GameBoardCommand.CommandTypeException; -import djmil.cordacheckers.states.GameBoardState; -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.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.UtxoTransactionValidator; - -@InitiatedBy(protocol = "game-board") -public class CommandResponderFlow implements ResponderFlow { - - private final static Logger log = LoggerFactory.getLogger(CommandResponderFlow.class); - - @CordaInject - public MemberLookup memberLookup; - - @CordaInject - public UtxoLedgerService utxoLedgerService; - - @Suspendable - @Override - public void call(FlowSession session) { - try { - UtxoTransactionValidator txValidator = utxoGameBoard -> { - final var command = getSingleCommand(utxoGameBoard, GameBoardCommand.class); - final var stateGameBoard = getGameBoardState(utxoGameBoard, command); - - checkParticipants(session, stateGameBoard, command); - - /* - * Other checks / actions ? - */ - - log.info("Verified the transaction - " + utxoGameBoard.getId()); - }; - - UtxoSignedTransaction finalizedSignedTransaction = utxoLedgerService - .receiveFinality(session, txValidator) - .getTransaction(); - - log.info("Finished responder flow - " + finalizedSignedTransaction.getId()); - } - catch(Exception e) { - log.warn("Responder flow failed: ", e); - } - } - - @Suspendable - GameBoardState getGameBoardState(UtxoLedgerTransaction utxoGameBoard, GameBoardCommand command) { - switch (command.getType()) { - case SURRENDER: - case FINISH: - return getSingleInputState(utxoGameBoard, GameBoardState.class); - - case MOVE: - return getSingleOutputState(utxoGameBoard, GameBoardState.class); - - default: - throw new CommandTypeException(); - } - } - - @Suspendable - void checkParticipants(FlowSession session, GameBoardState stateGameBoard, GameBoardCommand command) { - final var myName = memberLookup.myInfo().getName(); - final var counterpartyName = session.getCounterparty(); - - if (command.getRespondent(stateGameBoard).compareTo(myName) != 0) - throw new CordaRuntimeException("Bad respondent"); - - if (command.getInitiator(stateGameBoard).compareTo(counterpartyName) !=0) - throw new CordaRuntimeException("Bad initiator"); - } - -} diff --git a/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/GameBoardView.java b/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/GameBoardView.java deleted file mode 100644 index 46a04a9..0000000 --- a/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/GameBoardView.java +++ /dev/null @@ -1,68 +0,0 @@ -package djmil.cordacheckers.gameboard; - -import java.util.Map; -import java.util.UUID; - -import djmil.cordacheckers.contracts.GameBoardCommand; -import djmil.cordacheckers.contracts.UtxoLedgerTransactionUtil; -import djmil.cordacheckers.states.GameState.NotInvolved; -import djmil.cordacheckers.states.GameBoardState; -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 Integer moveNumber; - public final Map board; - public final GameBoardCommand previousCommand; - public final String message; - public final UUID gameUuid; - - /* - * GameStatus enum: - * - YOUR_TURN - * - WAIT_FOR_OPPONENT - * - VICTORY - * - YOU_LOOSE - */ - - // Serialisation service requires a default constructor - public GameBoardView() { - this.opponentName = null; - this.opponentColor = null; - this.opponentMove = null; - this.moveNumber = null; - this.board = null; - this.previousCommand = null; - this.message = null; - this.gameUuid = null; - } - - // A view from a perspective of a concrete player, on a ledger transaction that has - // produced new GameBoardState - public GameBoardView(UtxoLedgerTransaction utxoGameBoard, MemberX500Name myName) throws NotInvolved { - // TODO check command type: MOVE vs FINNISH | SURRENDER - // SingleOutPut vs SingleInput - this.previousCommand = UtxoLedgerTransactionUtil - .getOptionalCommand(utxoGameBoard, GameBoardCommand.class) - .orElseGet(() -> null); // there is no previous command for GameProposal.Accept case - - final GameBoardState stateGameBoard = UtxoLedgerTransactionUtil - .getSingleOutputState(utxoGameBoard, GameBoardState.class); - - this.opponentName = stateGameBoard.getCounterpartyName(myName).getCommonName(); - this.opponentColor = stateGameBoard.getCounterpartyColor(myName); - - this.opponentMove = this.opponentColor == stateGameBoard.getMoveColor(); - this.moveNumber = stateGameBoard.getMoveNumber(); - this.board = stateGameBoard.getBoard(); - this.message = stateGameBoard.getMessage(); - this.gameUuid = stateGameBoard.getGameUuid(); - } - -} diff --git a/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/ListFlow.java b/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/ListFlow.java deleted file mode 100644 index 05c9ae1..0000000 --- a/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/ListFlow.java +++ /dev/null @@ -1,83 +0,0 @@ -package djmil.cordacheckers.gameboard; - -import java.util.LinkedList; -import java.util.List; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import djmil.cordacheckers.FlowResult; -import djmil.cordacheckers.states.GameBoardState; -import djmil.cordacheckers.states.GameState.NotInvolved; -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.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; - -public class ListFlow implements ClientStartableFlow { - - private final static Logger log = LoggerFactory.getLogger(ListFlow.class); - - @CordaInject - public MemberLookup memberLookup; - - @CordaInject - public UtxoLedgerService utxoLedgerService; - - @CordaInject - public JsonMarshallingService jsonMarshallingService; - - @Suspendable - @Override - public String call(ClientRequestBody requestBody) { - - try { - final var utxoGameBoardList = utxoLedgerService - .findUnconsumedStatesByType(GameBoardState.class); - // NOTE: lambda operations (aka .map) can not be used with UtxoLedgerService instances - - final List res = prepareGameBoardViewList(utxoGameBoardList); - - return new FlowResult(res) - .toJsonEncodedString(jsonMarshallingService); - } - catch (Exception e) { - log.warn("GameBoard.ListFlow failed to process utxo request body " + requestBody + " because: " + e.getMessage()); - return new FlowResult(e).toJsonEncodedString(jsonMarshallingService); - } - } - - @Suspendable - private List prepareGameBoardViewList(List> utxoGamaBoardList) { - final MemberX500Name myName = memberLookup.myInfo().getName(); - List res = new LinkedList(); - - for (StateAndRef sarGameBoard :utxoGamaBoardList) { - try { - res.add(prepareGameBoardView(sarGameBoard, myName)); - } catch (NotInvolved e) { - log.warn(e.getMessage()); - } - } - - return res; - } - - @Suspendable - private GameBoardView prepareGameBoardView(StateAndRef sarGameBoard, MemberX500Name myName) throws NotInvolved { - final SecureHash trxId = sarGameBoard.getRef().getTransactionId(); - - final UtxoLedgerTransaction utxoGameBoard = utxoLedgerService - .findLedgerTransaction(trxId); - - return new GameBoardView(utxoGameBoard, myName); - } - -} diff --git a/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/SurrenderFlow.java b/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/SurrenderFlow.java new file mode 100644 index 0000000..d79ab7c --- /dev/null +++ b/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/SurrenderFlow.java @@ -0,0 +1,104 @@ +package djmil.cordacheckers.gameboard; + +import java.time.Duration; +import java.time.Instant; +import java.util.UUID; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import djmil.cordacheckers.contracts.GameCommand; +import djmil.cordacheckers.gamestate.CommitSubFlow; +import djmil.cordacheckers.gamestate.FlowResponce; +import djmil.cordacheckers.gamestate.GetFlow; +import djmil.cordacheckers.gamestate.View; +import djmil.cordacheckers.gamestate.ViewBuilder; +import djmil.cordacheckers.states.GameBoardState; +import djmil.cordacheckers.states.GameResultState; +import djmil.cordacheckers.states.GameState; +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.common.NotaryLookup; +import net.corda.v5.ledger.utxo.StateAndRef; +import net.corda.v5.ledger.utxo.UtxoLedgerService; +import net.corda.v5.ledger.utxo.transaction.UtxoSignedTransaction; + +public class SurrenderFlow implements ClientStartableFlow{ + + private final static Logger log = LoggerFactory.getLogger(SurrenderFlow.class); + + @CordaInject + public JsonMarshallingService jsonMarshallingService; + + @CordaInject + public MemberLookup memberLookup; + + @CordaInject + public NotaryLookup notaryLookup; + + @CordaInject + public UtxoLedgerService utxoLedgerService; + + @CordaInject + public FlowEngine flowEngine; + + @Suspendable + @Override + public String call(ClientRequestBody requestBody) { + SecureHash gameStateUtxoTrxId = null; + + try { + final GameCommand command = new GameCommand(GameCommand.Action.GAME_BOARD_SURRENDER); + + final UUID gameStateUuid = UUID.fromString(requestBody.getRequestBody()); + + final StateAndRef inputStateSar = this.flowEngine + .subFlow(new GetFlow(gameStateUuid)); + + final GameResultState outputState = prepareGameResultState(inputStateSar); + + final UtxoSignedTransaction transactionCandidate = utxoLedgerService.createTransactionBuilder() + .addCommand(command) + .addInputState(inputStateSar.getRef()) + .addOutputState(outputState) + .addSignatories(outputState.getParticipants()) + .setNotary(inputStateSar.getState().getNotaryName()) + .setTimeWindowUntil(Instant.now().plusMillis(Duration.ofDays(1).toMillis())) + .toSignedTransaction(); + + gameStateUtxoTrxId = this.flowEngine + .subFlow(new CommitSubFlow(transactionCandidate, command.getObserver(outputState))); + + final View gameStateView = this.flowEngine + .subFlow(new ViewBuilder(gameStateUtxoTrxId)); + + return new FlowResponce(gameStateView, gameStateUtxoTrxId) + .toJsonEncodedString(jsonMarshallingService); + } + catch (Exception e) { + log.warn("Exception during processing " + requestBody + " request: " + e.getMessage()); + e.printStackTrace(System.out); + return new FlowResponce(e, gameStateUtxoTrxId) + .toJsonEncodedString(jsonMarshallingService); + } + } + + @Suspendable + GameResultState prepareGameResultState(StateAndRef gameStateSar) { + final GameState gameState = gameStateSar.getState().getContractState(); + final GameBoardState gameBoard = (GameBoardState) gameState; + + final MemberX500Name myName = memberLookup.myInfo().getName(); + final MemberX500Name winnerName = gameBoard.getCounterpartyName(myName); + + return new GameResultState(gameBoard, winnerName); + } + +} diff --git a/corda/workflows/src/main/java/djmil/cordacheckers/gameproposal/AcceptFlow.java b/corda/workflows/src/main/java/djmil/cordacheckers/gameproposal/AcceptFlow.java new file mode 100644 index 0000000..9847bbe --- /dev/null +++ b/corda/workflows/src/main/java/djmil/cordacheckers/gameproposal/AcceptFlow.java @@ -0,0 +1,84 @@ +package djmil.cordacheckers.gameproposal; + +import java.time.Duration; +import java.time.Instant; +import java.util.UUID; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import djmil.cordacheckers.contracts.GameCommand; +import djmil.cordacheckers.gamestate.CommitSubFlow; +import djmil.cordacheckers.gamestate.FlowResponce; +import djmil.cordacheckers.gamestate.GetFlow; +import djmil.cordacheckers.gamestate.View; +import djmil.cordacheckers.gamestate.ViewBuilder; +import djmil.cordacheckers.states.GameBoardState; +import djmil.cordacheckers.states.GameProposalState; +import djmil.cordacheckers.states.GameState; +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; + +public class AcceptFlow implements ClientStartableFlow{ + + private final static Logger log = LoggerFactory.getLogger(AcceptFlow.class); + + @CordaInject + public JsonMarshallingService jsonMarshallingService; + + @CordaInject + public UtxoLedgerService utxoLedgerService; + + @CordaInject + public FlowEngine flowEngine; + + @Suspendable + @Override + public String call(ClientRequestBody requestBody) { + SecureHash gameStateUtxoTrxId = null; + + try { + final GameCommand command = new GameCommand(GameCommand.Action.GAME_PROPOSAL_ACCEPT); + + final UUID gameStateUuid = UUID.fromString(requestBody.getRequestBody()); + + final StateAndRef inputState = this.flowEngine + .subFlow(new GetFlow(gameStateUuid)); + + final GameBoardState outputState = new GameBoardState((GameProposalState)inputState.getState().getContractState()); + + final UtxoSignedTransaction gameStateRejectTrx = utxoLedgerService.createTransactionBuilder() + .addCommand(command) + .addInputState(inputState.getRef()) + .addOutputState(outputState) + .addSignatories(inputState.getState().getContractState().getParticipants()) + .setNotary(inputState.getState().getNotaryName()) + .setTimeWindowUntil(Instant.now().plusMillis(Duration.ofDays(1).toMillis())) + .toSignedTransaction(); + + gameStateUtxoTrxId = this.flowEngine + .subFlow(new CommitSubFlow(gameStateRejectTrx, command.getObserver(inputState))); + + final View gameStateView = this.flowEngine + .subFlow(new ViewBuilder(gameStateUtxoTrxId)); + + return new FlowResponce(gameStateView, gameStateUtxoTrxId) + .toJsonEncodedString(jsonMarshallingService); + } + catch (Exception e) { + log.warn("Exception during processing " + requestBody + " request: " + e.getMessage()); + e.printStackTrace(System.out); + return new FlowResponce(e, gameStateUtxoTrxId) + .toJsonEncodedString(jsonMarshallingService); + } + } + +} diff --git a/corda/workflows/src/main/java/djmil/cordacheckers/gameproposal/CancelFlow.java b/corda/workflows/src/main/java/djmil/cordacheckers/gameproposal/CancelFlow.java new file mode 100644 index 0000000..9829234 --- /dev/null +++ b/corda/workflows/src/main/java/djmil/cordacheckers/gameproposal/CancelFlow.java @@ -0,0 +1,78 @@ +package djmil.cordacheckers.gameproposal; + +import java.time.Duration; +import java.time.Instant; +import java.util.UUID; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import djmil.cordacheckers.contracts.GameCommand; +import djmil.cordacheckers.gamestate.CommitSubFlow; +import djmil.cordacheckers.gamestate.FlowResponce; +import djmil.cordacheckers.gamestate.GetFlow; +import djmil.cordacheckers.gamestate.View; +import djmil.cordacheckers.gamestate.ViewBuilder; +import djmil.cordacheckers.states.GameState; +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; + +public class CancelFlow implements ClientStartableFlow{ + + private final static Logger log = LoggerFactory.getLogger(CancelFlow.class); + + @CordaInject + public JsonMarshallingService jsonMarshallingService; + + @CordaInject + public UtxoLedgerService utxoLedgerService; + + @CordaInject + public FlowEngine flowEngine; + + @Suspendable + @Override + public String call(ClientRequestBody requestBody) { + SecureHash gameStateUtxoTrxId = null; + + try { + final GameCommand command = new GameCommand(GameCommand.Action.GAME_PROPOSAL_CANCEL); + + final UUID gameStateUuid = UUID.fromString(requestBody.getRequestBody()); + + final StateAndRef inputState = this.flowEngine + .subFlow(new GetFlow(gameStateUuid)); + + final UtxoSignedTransaction gameStateRejectTrx = utxoLedgerService.createTransactionBuilder() + .addCommand(command) + .addInputState(inputState.getRef()) + .addSignatories(inputState.getState().getContractState().getParticipants()) + .setNotary(inputState.getState().getNotaryName()) + .setTimeWindowUntil(Instant.now().plusMillis(Duration.ofDays(1).toMillis())) + .toSignedTransaction(); + + gameStateUtxoTrxId = this.flowEngine + .subFlow(new CommitSubFlow(gameStateRejectTrx, command.getObserver(inputState))); + + final View gameStateView = this.flowEngine + .subFlow(new ViewBuilder(gameStateUtxoTrxId)); + + return new FlowResponce(gameStateView, gameStateUtxoTrxId) + .toJsonEncodedString(jsonMarshallingService); + } + catch (Exception e) { + log.warn("Exception during processing " + requestBody + " request: " + e.getMessage()); + return new FlowResponce(e, gameStateUtxoTrxId) + .toJsonEncodedString(jsonMarshallingService); + } + } + +} diff --git a/corda/workflows/src/main/java/djmil/cordacheckers/gameproposal/CommandFlow.java b/corda/workflows/src/main/java/djmil/cordacheckers/gameproposal/CommandFlow.java deleted file mode 100644 index 3261dcb..0000000 --- a/corda/workflows/src/main/java/djmil/cordacheckers/gameproposal/CommandFlow.java +++ /dev/null @@ -1,124 +0,0 @@ -package djmil.cordacheckers.gameproposal; - -import java.util.UUID; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import djmil.cordacheckers.FlowResult; -import djmil.cordacheckers.contracts.GameProposalCommand; -import djmil.cordacheckers.gameboard.GameBoardView; -import djmil.cordacheckers.states.GameBoardState; -import djmil.cordacheckers.states.GameProposalState; -import djmil.cordacheckers.states.GameState.NotInvolved; -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 GameProposalCommand command = args.getCommand(); - - final StateAndRef utxoSarGameProposal = findUtxoGameProposal(args.getGameProposalUuid()); - - final UtxoSignedTransaction utxoDraft = prepareSignedTransaction(command, utxoSarGameProposal); - - final SecureHash utxoTrxId = this.flowEngine - .subFlow( new CommitSubFlow(utxoDraft, command.getRespondent(utxoSarGameProposal)) ); - - if (command == GameProposalCommand.ACCEPT) { - final GameBoardView viewGameBoard = prepareGameBoardView(utxoTrxId); - - return new FlowResult(viewGameBoard, utxoTrxId) - .toJsonEncodedString(jsonMarshallingService); - } - - return new FlowResult(command+"ED", utxoTrxId) // REJECT+ED - .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 findUtxoGameProposal (UUID uuidGameProposal) { - /* - * 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(GameProposalState.class) - .stream() - .filter(sar -> sar.getState().getContractState().getGameUuid().equals(uuidGameProposal)) - .reduce((a, b) -> {throw new IllegalStateException("Multiple states: " +a +", " +b);}) - .get(); - } - - @Suspendable - private UtxoSignedTransaction prepareSignedTransaction( - GameProposalCommand command, - StateAndRef sarGameProposal - ) { - UtxoTransactionBuilder trxBuilder = utxoLedgerService.createTransactionBuilder() - .setNotary(sarGameProposal.getState().getNotaryName()) - .setTimeWindowBetween(Instant.now(), Instant.now().plusMillis(Duration.ofDays(1).toMillis())) - .addInputState(sarGameProposal.getRef()) - .addCommand(command) - .addSignatories(sarGameProposal.getState().getContractState().getParticipants()); - - if (command == GameProposalCommand.ACCEPT) { - trxBuilder = trxBuilder - .addOutputState(new GameBoardState(sarGameProposal.getState().getContractState())); - } - - return trxBuilder.toSignedTransaction(); - } - - @Suspendable - private GameBoardView prepareGameBoardView(SecureHash utxoTrxId) throws NotInvolved { - final MemberX500Name myName = memberLookup.myInfo().getName(); - - final UtxoLedgerTransaction utxoGameBoard = utxoLedgerService - .findLedgerTransaction(utxoTrxId); - - return new GameBoardView(utxoGameBoard, myName); - } - -} diff --git a/corda/workflows/src/main/java/djmil/cordacheckers/gameproposal/CommandFlowArgs.java b/corda/workflows/src/main/java/djmil/cordacheckers/gameproposal/CommandFlowArgs.java deleted file mode 100644 index e3fc351..0000000 --- a/corda/workflows/src/main/java/djmil/cordacheckers/gameproposal/CommandFlowArgs.java +++ /dev/null @@ -1,24 +0,0 @@ -package djmil.cordacheckers.gameproposal; - -import java.util.UUID; - -import djmil.cordacheckers.contracts.GameProposalCommand; - -public class CommandFlowArgs { - private UUID gameProposalUuid; - private String command; - - // Serialisation service requires a default constructor - public CommandFlowArgs() { - this.gameProposalUuid = null; - this.command = null; - } - - public GameProposalCommand getCommand() { - return GameProposalCommand.valueOf(this.command); - } - - public UUID getGameProposalUuid() { - return this.gameProposalUuid; - } -} diff --git a/corda/workflows/src/main/java/djmil/cordacheckers/gameproposal/CommitResponderFlow.java b/corda/workflows/src/main/java/djmil/cordacheckers/gameproposal/CommitResponderFlow.java deleted file mode 100644 index 15b5efb..0000000 --- a/corda/workflows/src/main/java/djmil/cordacheckers/gameproposal/CommitResponderFlow.java +++ /dev/null @@ -1,98 +0,0 @@ -package djmil.cordacheckers.gameproposal; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import djmil.cordacheckers.contracts.GameProposalCommand; -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.UtxoLedgerService; -import net.corda.v5.ledger.utxo.transaction.UtxoLedgerTransaction; -import net.corda.v5.ledger.utxo.transaction.UtxoSignedTransaction; -import net.corda.v5.ledger.utxo.transaction.UtxoTransactionValidator; - -import static djmil.cordacheckers.contracts.UtxoLedgerTransactionUtil.getSingleCommand; - -@InitiatedBy(protocol = "game-proposal") -public class CommitResponderFlow implements ResponderFlow { - - private final static Logger log = LoggerFactory.getLogger(CommitResponderFlow.class); - - @CordaInject - public MemberLookup memberLookup; - - @CordaInject - public UtxoLedgerService utxoLedgerService; - - @Suspendable - @Override - public void call(FlowSession session) { - log.info("GameProposal: Commit responder flow"); - try { - UtxoTransactionValidator txValidator = ledgerTransaction -> { - final GameProposalCommand command = ledgerTransaction.getCommands(GameProposalCommand.class).get(0); - - final GameProposalState gameProposal = getGameProposal(ledgerTransaction); - - checkParticipants(session, gameProposal, command); - - /* - * Other checks / actions ? - */ - - log.info("Verified the transaction - " + ledgerTransaction.getId()); - }; - - UtxoSignedTransaction finalizedSignedTransaction = utxoLedgerService - .receiveFinality(session, txValidator) - .getTransaction(); - - log.info("Finished responder flow - " + finalizedSignedTransaction.getId()); - } - catch(Exception e) { - log.warn("Responder flow failed: ", e); - } - } - - @Suspendable - void checkParticipants( - FlowSession session, - GameProposalState gameProposal, - GameProposalCommand command - ) { - final MemberX500Name myName = memberLookup.myInfo().getName(); - final MemberX500Name otherName = session.getCounterparty(); - - if (command.getRespondent(gameProposal).compareTo(myName) != 0) - throw new CordaRuntimeException("Bad GameProposal acquirer: expected '"+myName+"', actual '" +gameProposal.getAcquier() +"'"); - - if (command.getInitiator(gameProposal).compareTo(otherName) != 0) - throw new CordaRuntimeException("Bad GameProposal issuer: expected '"+otherName+"', actual '"+gameProposal.getIssuer()+"'"); - } - - @Suspendable - GameProposalState getGameProposal(UtxoLedgerTransaction trx) { - final GameProposalCommand command = trx.getCommands(GameProposalCommand.class).get(0); - - switch (command) { - case CREATE: - return (GameProposalState)trx.getOutputContractStates().get(0); - - case ACCEPT: - case REJECT: - case CANCEL: - return (GameProposalState)trx.getInputContractStates().get(0); - - default: - throw new RuntimeException(GameProposalCommand.UNSUPPORTED_VALUE_OF +command); - } - } - -} 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 8b8110c..8e377c5 100644 --- a/corda/workflows/src/main/java/djmil/cordacheckers/gameproposal/CreateFlow.java +++ b/corda/workflows/src/main/java/djmil/cordacheckers/gameproposal/CreateFlow.java @@ -1,10 +1,20 @@ package djmil.cordacheckers.gameproposal; +import static java.util.Objects.requireNonNull; + +import java.time.Duration; +import java.time.Instant; +import java.util.Arrays; +import java.util.UUID; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import djmil.cordacheckers.FlowResult; -import djmil.cordacheckers.contracts.GameProposalCommand; +import djmil.cordacheckers.contracts.GameCommand; +import djmil.cordacheckers.gamestate.CommitSubFlow; +import djmil.cordacheckers.gamestate.FlowResponce; +import djmil.cordacheckers.gamestate.View; +import djmil.cordacheckers.gamestate.ViewBuilder; import djmil.cordacheckers.states.GameProposalState; import djmil.cordacheckers.states.Piece; import net.corda.v5.application.flows.ClientRequestBody; @@ -20,14 +30,6 @@ import net.corda.v5.ledger.common.NotaryLookup; import net.corda.v5.ledger.utxo.UtxoLedgerService; import net.corda.v5.ledger.utxo.transaction.UtxoSignedTransaction; import net.corda.v5.membership.MemberInfo; -import net.corda.v5.membership.NotaryInfo; - -import static java.util.Objects.requireNonNull; - -import java.time.Duration; -import java.time.Instant; -import java.util.Arrays; -import java.util.UUID; public class CreateFlow implements ClientStartableFlow{ @@ -43,7 +45,7 @@ public class CreateFlow implements ClientStartableFlow{ public NotaryLookup notaryLookup; @CordaInject - public UtxoLedgerService ledgerService; + public UtxoLedgerService utxoLedgerService; @CordaInject public FlowEngine flowEngine; @@ -51,24 +53,34 @@ public class CreateFlow implements ClientStartableFlow{ @Suspendable @Override public String call(ClientRequestBody requestBody) { + SecureHash gameStateUtxoTrxId = null; + try { - log.info("flow: Create Game Proposal"); - final GameProposalCommand command = GameProposalCommand.CREATE; + final GameCommand command = new GameCommand(GameCommand.Action.GAME_PROPOSAL_CREATE); final GameProposalState newGameProposal = buildGameProposalStateFrom(requestBody); - final UtxoSignedTransaction trx = prepareSignedTransaction(newGameProposal); + final UtxoSignedTransaction utxoCandidate = utxoLedgerService.createTransactionBuilder() + .addCommand(command) + .addOutputState(newGameProposal) + .addSignatories(newGameProposal.getParticipants()) + .setNotary(notaryLookup.getNotaryServices().iterator().next().getName()) + .setTimeWindowUntil(Instant.now().plusMillis(Duration.ofDays(1).toMillis())) + .toSignedTransaction(); - final SecureHash trxId = this.flowEngine - .subFlow(new CommitSubFlow(trx, command.getRespondent(newGameProposal))); + gameStateUtxoTrxId = this.flowEngine + .subFlow(new CommitSubFlow(utxoCandidate, command.getObserver(newGameProposal))); - return new FlowResult(newGameProposal.getGameUuid(), trxId) + final View gameStateView = this.flowEngine + .subFlow(new ViewBuilder(gameStateUtxoTrxId)); + + return new FlowResponce(gameStateView, gameStateUtxoTrxId) .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); + log.warn("Exception during processing " + requestBody + " request: " + e.getMessage()); + return new FlowResponce(e, gameStateUtxoTrxId) + .toJsonEncodedString(jsonMarshallingService); } } @@ -99,16 +111,4 @@ public class CreateFlow implements ClientStartableFlow{ ); } - @Suspendable - private UtxoSignedTransaction prepareSignedTransaction(GameProposalState outputGameProposalState) { - final NotaryInfo notary = notaryLookup.getNotaryServices().iterator().next(); - - return ledgerService.createTransactionBuilder() - .setNotary(notary.getName()) - .setTimeWindowBetween(Instant.now(), Instant.now().plusMillis(Duration.ofDays(1).toMillis())) - .addOutputState(outputGameProposalState) - .addCommand(GameProposalCommand.CREATE) - .addSignatories(outputGameProposalState.getParticipants()) - .toSignedTransaction(); - } } diff --git a/corda/workflows/src/main/java/djmil/cordacheckers/gameproposal/ListFlow.java b/corda/workflows/src/main/java/djmil/cordacheckers/gameproposal/ListFlow.java deleted file mode 100644 index 1d5f85f..0000000 --- a/corda/workflows/src/main/java/djmil/cordacheckers/gameproposal/ListFlow.java +++ /dev/null @@ -1,49 +0,0 @@ -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.base.annotations.Suspendable; -import net.corda.v5.ledger.utxo.UtxoLedgerService; - -public class ListFlow implements ClientStartableFlow { - - private final static Logger log = LoggerFactory.getLogger(ListFlow.class); - - @CordaInject - public UtxoLedgerService utxoLedgerService; - - @CordaInject - public JsonMarshallingService jsonMarshallingService; - - @Suspendable - @Override - public String call(ClientRequestBody requestBody) { - try { - log.info("ListFlow.call() called"); - - // 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()); - - return new FlowResult(unconsumedGameProposaList).toJsonEncodedString(jsonMarshallingService); - } catch (Exception e) { - log.warn("ListFlow failed to process utxo request body " + requestBody + " because: " + e.getMessage()); - return new FlowResult(e).toJsonEncodedString(jsonMarshallingService); - } - } - -} diff --git a/corda/workflows/src/main/java/djmil/cordacheckers/gameproposal/ListItem.java b/corda/workflows/src/main/java/djmil/cordacheckers/gameproposal/ListItem.java deleted file mode 100644 index a937190..0000000 --- a/corda/workflows/src/main/java/djmil/cordacheckers/gameproposal/ListItem.java +++ /dev/null @@ -1,36 +0,0 @@ -package djmil.cordacheckers.gameproposal; - -import java.util.UUID; - -import djmil.cordacheckers.states.GameProposalState; -import djmil.cordacheckers.states.Piece; - -// 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 issuer; - public final String acquier; - public final Piece.Color acquierColor; - public final String message; - public final UUID id; - - // Serialisation service requires a default constructor - public ListItem() { - this.issuer = null; - this.acquier = null; - this.acquierColor = null; - this.message = null; - this.id = null; - } - - public ListItem(GameProposalState state) { - this.issuer = state.getIssuer().getCommonName(); - this.acquier = state.getAcquier().getCommonName(); - this.acquierColor = state.getAcquierColor(); - this.message = state.getMessage(); - this.id = state.getGameUuid(); - } -} diff --git a/corda/workflows/src/main/java/djmil/cordacheckers/gameproposal/RejectFlow.java b/corda/workflows/src/main/java/djmil/cordacheckers/gameproposal/RejectFlow.java new file mode 100644 index 0000000..a45310f --- /dev/null +++ b/corda/workflows/src/main/java/djmil/cordacheckers/gameproposal/RejectFlow.java @@ -0,0 +1,86 @@ +package djmil.cordacheckers.gameproposal; + +import java.time.Duration; +import java.time.Instant; +import java.util.UUID; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import djmil.cordacheckers.contracts.GameCommand; +import djmil.cordacheckers.gamestate.CommitSubFlow; +import djmil.cordacheckers.gamestate.FlowResponce; +import djmil.cordacheckers.gamestate.GetFlow; +import djmil.cordacheckers.gamestate.View; +import djmil.cordacheckers.gamestate.ViewBuilder; +import djmil.cordacheckers.states.GameState; +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.crypto.SecureHash; +import net.corda.v5.ledger.common.NotaryLookup; +import net.corda.v5.ledger.utxo.StateAndRef; +import net.corda.v5.ledger.utxo.UtxoLedgerService; +import net.corda.v5.ledger.utxo.transaction.UtxoSignedTransaction; + +public class RejectFlow implements ClientStartableFlow{ + + private final static Logger log = LoggerFactory.getLogger(RejectFlow.class); + + @CordaInject + public JsonMarshallingService jsonMarshallingService; + + @CordaInject + public MemberLookup memberLookup; + + @CordaInject + public NotaryLookup notaryLookup; + + @CordaInject + public UtxoLedgerService utxoLedgerService; + + @CordaInject + public FlowEngine flowEngine; + + @Suspendable + @Override + public String call(ClientRequestBody requestBody) { + SecureHash gameStateUtxoTrxId = null; + + try { + final GameCommand command = new GameCommand(GameCommand.Action.GAME_PROPOSAL_REJECT); + + final UUID gameStateUuid = UUID.fromString(requestBody.getRequestBody()); + + final StateAndRef inputState = this.flowEngine + .subFlow(new GetFlow(gameStateUuid)); + + final UtxoSignedTransaction gameStateRejectTrx = utxoLedgerService.createTransactionBuilder() + .addCommand(command) + .addInputState(inputState.getRef()) + .addSignatories(inputState.getState().getContractState().getParticipants()) + .setNotary(inputState.getState().getNotaryName()) + .setTimeWindowUntil(Instant.now().plusMillis(Duration.ofDays(1).toMillis())) + .toSignedTransaction(); + + gameStateUtxoTrxId = this.flowEngine + .subFlow(new CommitSubFlow(gameStateRejectTrx, command.getObserver(inputState))); + + final View gameStateView = this.flowEngine + .subFlow(new ViewBuilder(gameStateUtxoTrxId)); + + return new FlowResponce(gameStateView, gameStateUtxoTrxId) + .toJsonEncodedString(jsonMarshallingService); + } + catch (Exception e) { + log.warn("Exception during processing " + requestBody + " request: " + e.getMessage()); + return new FlowResponce(e, gameStateUtxoTrxId) + .toJsonEncodedString(jsonMarshallingService); + } + } + +} diff --git a/corda/workflows/src/main/java/djmil/cordacheckers/gameresult/GameResultView.java b/corda/workflows/src/main/java/djmil/cordacheckers/gameresult/GameResultView.java deleted file mode 100644 index d106a4b..0000000 --- a/corda/workflows/src/main/java/djmil/cordacheckers/gameresult/GameResultView.java +++ /dev/null @@ -1,29 +0,0 @@ -package djmil.cordacheckers.gameresult; - -import java.util.UUID; - -import djmil.cordacheckers.states.GameResultState; -import djmil.cordacheckers.states.Piece; - -public class GameResultView { - public final String whitePlayerName; - public final String blackPlayerName; - public final Piece.Color victoryColor; - public final UUID id; - - // Serialisation service requires a default constructor - public GameResultView() { - this.whitePlayerName = null; - this.blackPlayerName = null; - this.victoryColor = null; - this.id = null; - } - - public GameResultView(GameResultState gameResultState) { - this.whitePlayerName = gameResultState.getWinnerName().getCommonName(); - this.blackPlayerName = gameResultState.getOpponentName().getCommonName(); - this.victoryColor = gameResultState.getWinnerColor(); - this.id = gameResultState.getGameUuid(); - } - -} diff --git a/corda/workflows/src/main/java/djmil/cordacheckers/gameproposal/CommitSubFlow.java b/corda/workflows/src/main/java/djmil/cordacheckers/gamestate/CommitSubFlow.java similarity index 74% rename from corda/workflows/src/main/java/djmil/cordacheckers/gameproposal/CommitSubFlow.java rename to corda/workflows/src/main/java/djmil/cordacheckers/gamestate/CommitSubFlow.java index 4de9c1f..d513dc3 100644 --- a/corda/workflows/src/main/java/djmil/cordacheckers/gameproposal/CommitSubFlow.java +++ b/corda/workflows/src/main/java/djmil/cordacheckers/gamestate/CommitSubFlow.java @@ -1,4 +1,4 @@ -package djmil.cordacheckers.gameproposal; +package djmil.cordacheckers.gamestate; import java.util.Arrays; import java.util.List; @@ -17,16 +17,16 @@ import net.corda.v5.crypto.SecureHash; import net.corda.v5.ledger.utxo.UtxoLedgerService; import net.corda.v5.ledger.utxo.transaction.UtxoSignedTransaction; -@InitiatingFlow(protocol = "game-proposal") +@InitiatingFlow(protocol = "gamestate-commit") public class CommitSubFlow implements SubFlow { private final static Logger log = LoggerFactory.getLogger(CommitSubFlow.class); - private final UtxoSignedTransaction signedTransaction; - private final MemberX500Name respondenName; + private final UtxoSignedTransaction utxTrxCandidate; + private final MemberX500Name counterpartyName; - public CommitSubFlow(UtxoSignedTransaction signedTransaction, MemberX500Name respondenName) { - this.signedTransaction = signedTransaction; - this.respondenName = respondenName; + public CommitSubFlow(UtxoSignedTransaction signedTransaction, MemberX500Name counterpartyName) { + this.utxTrxCandidate = signedTransaction; + this.counterpartyName = counterpartyName; } @CordaInject @@ -38,9 +38,9 @@ public class CommitSubFlow implements SubFlow { @Override @Suspendable public SecureHash call() { - log.info("GamePropsal commit started"); + log.info("GameState commit started"); - final FlowSession session = flowMessaging.initiateFlow(this.respondenName); + final FlowSession session = flowMessaging.initiateFlow(this.counterpartyName); /* * Calls the Corda provided finalise() function which gather signatures from the counterparty, @@ -50,11 +50,11 @@ public class CommitSubFlow implements SubFlow { final List sessionsList = Arrays.asList(session); final SecureHash trxId = ledgerService - .finalize(this.signedTransaction, sessionsList) + .finalize(this.utxTrxCandidate, sessionsList) .getTransaction() .getId(); - log.info("GamePropsal commit " +trxId); + log.info("GameState commit " +trxId); return trxId; } } diff --git a/corda/workflows/src/main/java/djmil/cordacheckers/gamestate/CommitSubFlowResponder.java b/corda/workflows/src/main/java/djmil/cordacheckers/gamestate/CommitSubFlowResponder.java new file mode 100644 index 0000000..c766145 --- /dev/null +++ b/corda/workflows/src/main/java/djmil/cordacheckers/gamestate/CommitSubFlowResponder.java @@ -0,0 +1,124 @@ +package djmil.cordacheckers.gamestate; + +import static djmil.cordacheckers.contracts.UtxoLedgerTransactionUtil.getSingleCommand; +import static djmil.cordacheckers.contracts.UtxoLedgerTransactionUtil.getSingleOutputState; +import static djmil.cordacheckers.contracts.UtxoLedgerTransactionUtil.getSingleInputState; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import djmil.cordacheckers.contracts.GameCommand; +import djmil.cordacheckers.states.GameBoardState; +import djmil.cordacheckers.states.GameProposalState; +import djmil.cordacheckers.states.GameResultState; +import djmil.cordacheckers.states.GameState; +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.UtxoLedgerService; +import net.corda.v5.ledger.utxo.transaction.UtxoLedgerTransaction; +import net.corda.v5.ledger.utxo.transaction.UtxoTransactionValidator; + +@InitiatedBy(protocol = "gamestate-commit") +public class CommitSubFlowResponder implements ResponderFlow { + + private final static Logger log = LoggerFactory.getLogger(CommitSubFlowResponder.class); + + @CordaInject + public MemberLookup memberLookup; + + @CordaInject + public UtxoLedgerService utxoLedgerService; + + @Suspendable + @Override + public void call(FlowSession session) { + UtxoTransactionValidator txValidator = trxToValidate -> { + try { + final GameCommand gameCommand = getSingleCommand(trxToValidate, GameCommand.class); + final GameState gameState = getGameStateFromTransaction(trxToValidate, gameCommand); + + checkParticipants(session, gameCommand, gameState); + + /* + * Other checks / actions ? + */ + + log.info("Approval for " + trxToValidate.getId()); + } catch (Exception e) { + log.warn("Validation has failed. " +e.getMessage()); + // We have to re-trow an exception to indicate validation failure + throw new CordaRuntimeException(e.getClass().getCanonicalName(), e.getMessage(), e.getCause()); + } + }; + + try { + this.utxoLedgerService + .receiveFinality(session, txValidator); + } catch (Exception e) { + // Log Corda's specific message about finality validation failure + log.warn(e.getMessage()); + } + } + + /** + * + * @param gameStateTransaction an utxo ledger transaction, that involves GameState + * @param command + * @return the most recent (from perspective of session participants validation) GameState for a given transaction + * + * @see djmil.cordacheckers.gamestate.ViewBuilder#getGameStateFromTransaction(UtxoLedgerTransaction, GameCommand) + */ + @Suspendable + GameState getGameStateFromTransaction(UtxoLedgerTransaction gameStateTransaction, GameCommand command) { + switch (command.action) { + case GAME_PROPOSAL_CREATE: + return getSingleOutputState(gameStateTransaction, GameProposalState.class); + + case GAME_PROPOSAL_ACCEPT: + case GAME_PROPOSAL_REJECT: + case GAME_PROPOSAL_CANCEL: + return getSingleInputState(gameStateTransaction, GameProposalState.class); + + case GAME_BOARD_MOVE: + return getSingleOutputState(gameStateTransaction, GameBoardState.class); + + case GAME_BOARD_SURRENDER: + case GAME_RESULT_CREATE: + return getSingleOutputState(gameStateTransaction, GameResultState.class); + } + + throw new GameCommand.ActionException(); + } + + @Suspendable + void checkParticipants(FlowSession session, GameCommand gameCommand, GameState outputGameState) throws ParticipantException { + final var myName = memberLookup.myInfo().getName(); + final var outputStateConterparty = outputGameState.getCounterpartyName(myName); + final var sessionConterparty = session.getCounterparty(); + + final var actor = gameCommand.getActor(outputGameState); + final var observer = gameCommand.getObserver(outputGameState); + + if (outputStateConterparty.compareTo(sessionConterparty) != 0) + throw new ParticipantException("Counterparty", sessionConterparty, outputStateConterparty); + + if (actor.compareTo(sessionConterparty) != 0) + throw new ParticipantException("Actor", sessionConterparty, actor); + + if (observer.compareTo(myName) != 0) + throw new ParticipantException("Observer", myName, observer); + } + + public static class ParticipantException extends Exception { + public ParticipantException(String role, MemberX500Name expected, MemberX500Name actual) { + super("Bad participants. " +role +" role: expected '" +expected +"', actual '" +actual +"'"); + } + } + +} diff --git a/corda/workflows/src/main/java/djmil/cordacheckers/FlowResult.java b/corda/workflows/src/main/java/djmil/cordacheckers/gamestate/FlowResponce.java similarity index 58% rename from corda/workflows/src/main/java/djmil/cordacheckers/FlowResult.java rename to corda/workflows/src/main/java/djmil/cordacheckers/gamestate/FlowResponce.java index 3903379..68fffb6 100644 --- a/corda/workflows/src/main/java/djmil/cordacheckers/FlowResult.java +++ b/corda/workflows/src/main/java/djmil/cordacheckers/gamestate/FlowResponce.java @@ -1,28 +1,22 @@ -package djmil.cordacheckers; +package djmil.cordacheckers.gamestate; import net.corda.v5.application.marshalling.JsonMarshallingService; import net.corda.v5.crypto.SecureHash; -public class FlowResult { - public final Object successStatus; +public class FlowResponce { + public final View successStatus; public final SecureHash transactionId; public final String failureStatus; - public FlowResult(Object success) { - this.successStatus = success; - this.transactionId = null; - this.failureStatus = null; - } - - public FlowResult(Object success, SecureHash transactionId) { + public FlowResponce(View success, SecureHash transactionId) { this.successStatus = success; this.transactionId = transactionId; this.failureStatus = null; } - public FlowResult(Exception exception) { + public FlowResponce(Exception exception, SecureHash transactionId) { this.successStatus = null; - this.transactionId = null; + this.transactionId = transactionId; this.failureStatus = exception.getMessage(); } diff --git a/corda/workflows/src/main/java/djmil/cordacheckers/gamestate/GetFlow.java b/corda/workflows/src/main/java/djmil/cordacheckers/gamestate/GetFlow.java new file mode 100644 index 0000000..b9bc3b9 --- /dev/null +++ b/corda/workflows/src/main/java/djmil/cordacheckers/gamestate/GetFlow.java @@ -0,0 +1,86 @@ +package djmil.cordacheckers.gamestate; + +import java.util.UUID; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import djmil.cordacheckers.states.GameState; +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.flows.SubFlow; +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; + +public class GetFlow implements ClientStartableFlow, SubFlow> { + + private final static Logger log = LoggerFactory.getLogger(GetFlow.class); + private UUID subflowGameStateUuid = null; + + public GetFlow() { + this.subflowGameStateUuid = null; + } + + public GetFlow(UUID gameStateUuid) { + this.subflowGameStateUuid = gameStateUuid; + } + + @CordaInject + public JsonMarshallingService jsonMarshallingService; + + @CordaInject + public UtxoLedgerService utxoLedgerService; + + @CordaInject + public FlowEngine flowEngine; + + @Override + @Suspendable + public StateAndRef call() { // <<-- SubFlow entry + return findGameStateUtxoSar(this.subflowGameStateUuid); + } + + @Override + @Suspendable + public String call(ClientRequestBody requestBody) { // <<-- ClientStartableFlow entry + SecureHash gameStateUtxoTrxId = null; + + try { + final UUID gameStateUuid = UUID.fromString(requestBody.getRequestBody()); + + gameStateUtxoTrxId = findGameStateUtxoSar(gameStateUuid) + .getRef().getTransactionId(); + + final View gameStateView = this.flowEngine + .subFlow(new ViewBuilder(gameStateUtxoTrxId)); + + return new FlowResponce(gameStateView, gameStateUtxoTrxId) + .toJsonEncodedString(jsonMarshallingService); + } + catch (Exception e) { + log.warn("Exception during processing " + requestBody + " request: " + e.getMessage()); + return new FlowResponce(e, gameStateUtxoTrxId).toJsonEncodedString(jsonMarshallingService); + } + } + + @Suspendable + StateAndRef findGameStateUtxoSar(UUID gameStateUuid) { + /* + * Get list of all unconsumed aka 'active' GameStates, 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(GameState.class) + .stream() + .filter(sar -> sar.getState().getContractState().getGameUuid().equals(gameStateUuid)) + .reduce((a, b) -> {throw new IllegalStateException("Multiple GameStates found: " +a +", " +b);}) + .get(); + } + +} diff --git a/corda/workflows/src/main/java/djmil/cordacheckers/gamestate/ListFlow.java b/corda/workflows/src/main/java/djmil/cordacheckers/gamestate/ListFlow.java new file mode 100644 index 0000000..51d7a80 --- /dev/null +++ b/corda/workflows/src/main/java/djmil/cordacheckers/gamestate/ListFlow.java @@ -0,0 +1,67 @@ +package djmil.cordacheckers.gamestate; + +import java.util.LinkedList; +import java.util.List; +import java.util.stream.Collectors; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import djmil.cordacheckers.states.GameState; +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.UtxoLedgerService; + +public class ListFlow implements ClientStartableFlow { + + private final static Logger log = LoggerFactory.getLogger(ListFlow.class); + + @CordaInject + public UtxoLedgerService utxoLedgerService; + + @CordaInject + public JsonMarshallingService jsonMarshallingService; + + @CordaInject + public FlowEngine flowEngine; + + @Suspendable + @Override + public String call(ClientRequestBody requestBody) { + try { + List gameStateUtxoTrxIdList = getGameStateUtxoTrxIdList(); + + // TODO: try in one single list + List gameStateViewList = new LinkedList(); + + for (SecureHash gameStateUtxoTrxId: gameStateUtxoTrxIdList) { + final View gameStateView = this.flowEngine + .subFlow(new ViewBuilder(gameStateUtxoTrxId)); + + gameStateViewList.add(gameStateView); + } + + return new ListFlowResponce(gameStateViewList) + .toJsonEncodedString(jsonMarshallingService); + } catch (Exception e) { + log.warn("Exception during processing " + requestBody + " request: " + e.getMessage()); + return new ListFlowResponce(e) + .toJsonEncodedString(jsonMarshallingService); + } + } + + @Suspendable + List getGameStateUtxoTrxIdList() { + return utxoLedgerService + .findUnconsumedStatesByType(GameState.class) + .stream() + .map( stateAndRef -> stateAndRef.getRef().getTransactionId() ) + .collect(Collectors.toList()); + } + +} diff --git a/corda/workflows/src/main/java/djmil/cordacheckers/gamestate/ListFlowResponce.java b/corda/workflows/src/main/java/djmil/cordacheckers/gamestate/ListFlowResponce.java new file mode 100644 index 0000000..d12f046 --- /dev/null +++ b/corda/workflows/src/main/java/djmil/cordacheckers/gamestate/ListFlowResponce.java @@ -0,0 +1,25 @@ +package djmil.cordacheckers.gamestate; + +import java.util.List; + +import net.corda.v5.application.marshalling.JsonMarshallingService; + +public class ListFlowResponce { + public final List successStatus; + public final String failureStatus; + + public ListFlowResponce(List success) { + this.successStatus = success; + this.failureStatus = null; + } + + public ListFlowResponce(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/gamestate/View.java b/corda/workflows/src/main/java/djmil/cordacheckers/gamestate/View.java new file mode 100644 index 0000000..6a21166 --- /dev/null +++ b/corda/workflows/src/main/java/djmil/cordacheckers/gamestate/View.java @@ -0,0 +1,96 @@ +package djmil.cordacheckers.gamestate; + +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import djmil.cordacheckers.states.GameBoardState; +import djmil.cordacheckers.states.GameProposalState; +import djmil.cordacheckers.states.GameResultState; +import djmil.cordacheckers.states.Piece; +import net.corda.v5.base.types.MemberX500Name; + + +// GameBoard from the player's point of view +public class View { + public static enum Status { + GAME_PROPOSAL_WAIT_FOR_OPPONENT, + GAME_PROPOSAL_WAIT_FOR_YOU, + GAME_PROPOSAL_REJECTED, + GAME_PROPOSAL_CANCELED, + + GAME_BOARD_WAIT_FOR_OPPONENT, + GAME_BOARD_WAIT_FOR_YOU, + + GAME_RESULT_YOU_WON, + GAME_RESULT_YOU_LOOSE; + } + public final Status status; + + public final String opponentName; + public final Piece.Color opponentColor; + + public final Map board; + public final Integer moveNumber; + public final List previousMove; + + public final String message; + public final UUID uuid; + + // Serialisation service requires a default constructor + public View() { + this.status = null; + this.opponentName = null; + this.opponentColor = null; + this.board = null; + this.moveNumber = null; + this.previousMove = null; + this.message = null; + this.uuid = null; + } + + public View(View.Status status, GameProposalState gameProposal, MemberX500Name myName) { + this.status = status; + this.opponentName = gameProposal.getCounterpartyName(myName).getCommonName(); + this.opponentColor = gameProposal.getCounterpartyColor(myName); + this.board = null; + this.moveNumber = null; + this.previousMove = null; + this.message = gameProposal.getMessage(); + this.uuid = gameProposal.getGameUuid(); + } + + public View(View.Status status, GameBoardState gameBoard, MemberX500Name myName) { + this.status = status; + this.opponentName = gameBoard.getCounterpartyName(myName).getCommonName(); + this.opponentColor = gameBoard.getCounterpartyColor(myName); + this.board = gameBoard.getBoard(); + this.moveNumber = gameBoard.getMoveNumber(); + this.previousMove = null; + this.message = gameBoard.getMessage(); + this.uuid = gameBoard.getGameUuid(); + } + + public View(View.Status status, GameBoardState gameBoard, List previousMove, MemberX500Name myName) { + this.status = status; + this.opponentName = gameBoard.getCounterpartyName(myName).getCommonName(); + this.opponentColor = gameBoard.getCounterpartyColor(myName); + this.board = gameBoard.getBoard(); + this.moveNumber = gameBoard.getMoveNumber(); + this.previousMove = previousMove; + this.message = gameBoard.getMessage(); + this.uuid = gameBoard.getGameUuid(); + } + + public View(View.Status status, GameResultState gameResult, MemberX500Name myName) { + this.status = status; + this.opponentName = gameResult.getCounterpartyName(myName).getCommonName(); + this.opponentColor = gameResult.getCounterpartyColor(myName); + this.board = null; + this.moveNumber = null; + this.previousMove = null; + this.message = gameResult.getMessage(); + this.uuid = gameResult.getGameUuid(); + } + +} diff --git a/corda/workflows/src/main/java/djmil/cordacheckers/gamestate/ViewBuilder.java b/corda/workflows/src/main/java/djmil/cordacheckers/gamestate/ViewBuilder.java new file mode 100644 index 0000000..d899136 --- /dev/null +++ b/corda/workflows/src/main/java/djmil/cordacheckers/gamestate/ViewBuilder.java @@ -0,0 +1,147 @@ +package djmil.cordacheckers.gamestate; + +import static djmil.cordacheckers.contracts.UtxoLedgerTransactionUtil.getSingleCommand; +import static djmil.cordacheckers.contracts.UtxoLedgerTransactionUtil.getSingleOutputState; +import static djmil.cordacheckers.contracts.UtxoLedgerTransactionUtil.getSingleInputState; + +import djmil.cordacheckers.contracts.GameCommand; +import djmil.cordacheckers.states.GameBoardState; +import djmil.cordacheckers.states.GameProposalState; +import djmil.cordacheckers.states.GameResultState; +import djmil.cordacheckers.states.GameState; +import net.corda.v5.application.flows.CordaInject; +import net.corda.v5.application.flows.SubFlow; +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.UtxoLedgerService; +import net.corda.v5.ledger.utxo.transaction.UtxoLedgerTransaction; + +/* + * A view on a ledger transaction that has GameState involved in it, from perspective of a concrete player + */ +public class ViewBuilder implements SubFlow { + private final SecureHash gameStateUtxoTrxId; + + public ViewBuilder (SecureHash gameStateUtxoTrxId) { + this.gameStateUtxoTrxId = gameStateUtxoTrxId; + } + + @CordaInject + public UtxoLedgerService utxoLedgerService; + + @CordaInject + public MemberLookup memberLookup; + + @Override + @Suspendable + public View call() { + final UtxoLedgerTransaction gameStateUtxo = this.utxoLedgerService + .findLedgerTransaction(gameStateUtxoTrxId); + + return buildGameStateView(gameStateUtxo); + } + + @Suspendable + View buildGameStateView(UtxoLedgerTransaction gameStateUtxo) { + MemberX500Name myName = memberLookup.myInfo().getName(); + + final GameCommand command = getSingleCommand(gameStateUtxo, GameCommand.class); + final GameState state = getGameStateFromTransaction(gameStateUtxo, command); + final View.Status viewStatus = action2status(command, state, myName); + + switch (command.action) { + case GAME_PROPOSAL_CREATE: + case GAME_PROPOSAL_CANCEL: + case GAME_PROPOSAL_REJECT: + if (state instanceof GameProposalState) + return new View(viewStatus, (GameProposalState)state, myName); + break; + + case GAME_PROPOSAL_ACCEPT: + case GAME_BOARD_MOVE: + if (state instanceof GameBoardState) + return new View(viewStatus, (GameBoardState)state, command.move, myName); + break; + + case GAME_BOARD_SURRENDER: + case GAME_RESULT_CREATE: + if (state instanceof GameResultState) + return new View(viewStatus, (GameResultState)state, myName); + break; + } + + throw new GameCommand.ActionException(); + } + + /** + * + * @param gameStateTransaction an utxo ledger transaction, that involves GameState + * @param command + * @return the most recent (from perspective of building a GameView) GameState for a given transaction + * + * @see djmil.cordacheckers.gamestate.CommitSubFlowResponder#getGameStateFromTransaction(UtxoLedgerTransaction, GameCommand) + */ + @Suspendable + GameState getGameStateFromTransaction(UtxoLedgerTransaction gameStateTransaction, GameCommand command) { + switch (command.action) { + case GAME_PROPOSAL_CREATE: + return getSingleOutputState(gameStateTransaction, GameProposalState.class); + + case GAME_PROPOSAL_REJECT: + case GAME_PROPOSAL_CANCEL: + return getSingleInputState(gameStateTransaction, GameProposalState.class); + + case GAME_PROPOSAL_ACCEPT: + case GAME_BOARD_MOVE: + return getSingleOutputState(gameStateTransaction, GameBoardState.class); + + case GAME_BOARD_SURRENDER: + case GAME_RESULT_CREATE: + return getSingleOutputState(gameStateTransaction, GameResultState.class); + } + + throw new GameCommand.ActionException(); + } + + @Suspendable + View.Status action2status(GameCommand command, GameState state, MemberX500Name myName) { + final boolean myAction = command.getActor(state).compareTo(myName) == 0; + + switch (command.action) { + case GAME_PROPOSAL_CREATE: + if (myAction) + return View.Status.GAME_PROPOSAL_WAIT_FOR_OPPONENT; + else + return View.Status.GAME_PROPOSAL_WAIT_FOR_YOU; + + case GAME_PROPOSAL_REJECT: + return View.Status.GAME_PROPOSAL_REJECTED; + + case GAME_PROPOSAL_CANCEL: + return View.Status.GAME_PROPOSAL_CANCELED; + + case GAME_PROPOSAL_ACCEPT: + case GAME_BOARD_MOVE: + if (myAction) + return View.Status.GAME_BOARD_WAIT_FOR_YOU; + else + return View.Status.GAME_BOARD_WAIT_FOR_OPPONENT; + + case GAME_BOARD_SURRENDER: + if (myAction) + return View.Status.GAME_RESULT_YOU_LOOSE; + else + return View.Status.GAME_RESULT_YOU_WON; + + case GAME_RESULT_CREATE: + if (myAction) + return View.Status.GAME_RESULT_YOU_WON; + else + return View.Status.GAME_RESULT_YOU_LOOSE; + } + + throw new GameCommand.ActionException(); + } +}