diff --git a/backend/src/main/java/djmil/cordacheckers/cordaclient/CordaClient.java b/backend/src/main/java/djmil/cordacheckers/cordaclient/CordaClient.java index 7476243..685f7b9 100644 --- a/backend/src/main/java/djmil/cordacheckers/cordaclient/CordaClient.java +++ b/backend/src/main/java/djmil/cordacheckers/cordaclient/CordaClient.java @@ -23,6 +23,7 @@ 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; @@ -32,7 +33,7 @@ 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.GameBoardCommandRes; +import djmil.cordacheckers.cordaclient.dao.flow.arguments.GameBoardResGameResult; import djmil.cordacheckers.cordaclient.dao.flow.arguments.GameBoardListRes; import djmil.cordacheckers.cordaclient.dao.flow.arguments.GameProposalCommandAcceptRes; import djmil.cordacheckers.cordaclient.dao.flow.arguments.GameProposalCommandReq; @@ -199,29 +200,28 @@ public class CordaClient { return listFlowResult.successStatus(); } - public GameBoard gameBoardCommand( + public GameResult gameBoardSurrender( HoldingIdentity myHoldingIdentity, - UUID gameBoardUuid, - GameBoardCommand command + UUID gameBoardUuid ) { final RequestBody requestBody = new RequestBody( - "gb.command-" +command.getType() +UUID.randomUUID(), + "gb.surrender-" +UUID.randomUUID(), "djmil.cordacheckers.gameboard.CommandFlow", new GameBoardCommandReq( gameBoardUuid, - command + new GameBoardCommand(GameBoardCommand.Type.SURRENDER) ) ); - final GameBoardCommandRes moveResult = cordaFlowExecute( + final GameBoardResGameResult moveResult = cordaFlowExecute( myHoldingIdentity, requestBody, - GameBoardCommandRes.class + GameBoardResGameResult.class ); if (moveResult.failureStatus() != null) { - System.out.println("GameBoard.MoveFlow failed: " + moveResult.failureStatus()); - throw new RuntimeException("GameBoard: MoveFlow execution has failed"); + System.out.println("GameBoard.CommandFlow failed: " + moveResult.failureStatus()); + throw new RuntimeException("GameBoard: CommandFlow execution has failed"); } return moveResult.successStatus(); diff --git a/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/GameBoard.java b/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/GameBoard.java index 6c586f9..ec70639 100644 --- a/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/GameBoard.java +++ b/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/GameBoard.java @@ -13,6 +13,8 @@ public record GameBoard( Map board, GameBoardCommand previousCommand, String message, - UUID id) implements CordaState { + UUID id) + +implements CordaState { } diff --git a/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/GameBoardCommand.java b/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/GameBoardCommand.java index 68a83af..0eaf30b 100644 --- a/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/GameBoardCommand.java +++ b/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/GameBoardCommand.java @@ -6,9 +6,9 @@ public class GameBoardCommand { public static enum Type { MOVE, SURRENDER, - REQUEST_DRAW, - REQUEST_VICTORY, - FINISH; // aka accept DRAW or VICTORY request + DRAW, + VICTORY, + ACCEPT; } private final Type type; diff --git a/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/GameResult.java b/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/GameResult.java new file mode 100644 index 0000000..91ef80e --- /dev/null +++ b/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/GameResult.java @@ -0,0 +1,16 @@ +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 id) + +implements CordaState { + +} diff --git a/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/arguments/GameBoardCommandRes.java b/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/arguments/GameBoardCommandRes.java deleted file mode 100644 index 888ad14..0000000 --- a/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/arguments/GameBoardCommandRes.java +++ /dev/null @@ -1,7 +0,0 @@ -package djmil.cordacheckers.cordaclient.dao.flow.arguments; - -import djmil.cordacheckers.cordaclient.dao.GameBoard; - -public record GameBoardCommandRes(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 new file mode 100644 index 0000000..6f494fb --- /dev/null +++ b/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/arguments/GameBoardResGameResult.java @@ -0,0 +1,7 @@ +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/test/java/djmil/cordacheckers/cordaclient/CordaClientTest.java b/backend/src/test/java/djmil/cordacheckers/cordaclient/CordaClientTest.java index 0b35701..f328a98 100644 --- a/backend/src/test/java/djmil/cordacheckers/cordaclient/CordaClientTest.java +++ b/backend/src/test/java/djmil/cordacheckers/cordaclient/CordaClientTest.java @@ -17,8 +17,8 @@ import com.fasterxml.jackson.databind.JsonMappingException; import djmil.cordacheckers.cordaclient.dao.CordaState; import djmil.cordacheckers.cordaclient.dao.GameBoard; -import djmil.cordacheckers.cordaclient.dao.GameBoardCommand; import djmil.cordacheckers.cordaclient.dao.GameProposal; +import djmil.cordacheckers.cordaclient.dao.GameResult; import djmil.cordacheckers.cordaclient.dao.Piece; import djmil.cordacheckers.cordaclient.dao.VirtualNode; import djmil.cordacheckers.user.HoldingIdentityResolver; @@ -184,7 +184,7 @@ public class CordaClientTest { } @Test - void testGameBoardSurrender() throws JsonMappingException, JsonProcessingException { + void testGameBoardSurrender() throws JsonMappingException, JsonProcessingException, InvalidNameException { final var hiAlice = holdingIdentityResolver.getByUsername("alice"); final var hiBob = holdingIdentityResolver.getByUsername("bob"); final var bobColor = Piece.Color.WHITE; @@ -202,12 +202,17 @@ public class CordaClientTest { System.out.println("New GameBoard UUID "+ gbState.id()); - final GameBoard gbSurrender = cordaClient.gameBoardCommand( - hiBob, gbState.id(), - new GameBoardCommand(GameBoardCommand.Type.SURRENDER) - ); + assertThatThrownBy(() -> { // Alice can not surrender, since it is Bob's turn + cordaClient.gameBoardSurrender( + hiAlice, gbState.id()); + }); - System.out.println("SURRENDER GB: "+gbSurrender); + final GameResult gameResult = cordaClient.gameBoardSurrender( + hiBob, gbState.id()); + + assertThat(gameResult.whitePlayerName()).isEqualTo(hiBob.getName()); + assertThat(gameResult.blackPlayerName()).isEqualTo(hiAlice.getName()); + assertThat(gameResult.victoryColor()).isEqualByComparingTo(Piece.Color.BLACK); } private T findByUuid(List statesList, UUID uuid) { diff --git a/corda/contracts/src/main/java/djmil/cordacheckers/contracts/GameBoardCommand.java b/corda/contracts/src/main/java/djmil/cordacheckers/contracts/GameBoardCommand.java index a19dde8..254a2e0 100644 --- a/corda/contracts/src/main/java/djmil/cordacheckers/contracts/GameBoardCommand.java +++ b/corda/contracts/src/main/java/djmil/cordacheckers/contracts/GameBoardCommand.java @@ -1,11 +1,19 @@ 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 { @@ -54,5 +62,39 @@ public class GameBoardCommand implements Command { 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.getWhitePlayerName()) == 0, IN_OUT_PARTICIPANTS); + requireThat(inGameBoardState.getBlackPlayerName().compareTo(outGameResultState.getBlackPlayerName()) == 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 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 4945bfb..2e659ea 100644 --- a/corda/contracts/src/main/java/djmil/cordacheckers/contracts/GameBoardContract.java +++ b/corda/contracts/src/main/java/djmil/cordacheckers/contracts/GameBoardContract.java @@ -1,9 +1,8 @@ package djmil.cordacheckers.contracts; import static djmil.cordacheckers.contracts.UtxoLedgerTransactionUtil.getSingleCommand; -import static djmil.cordacheckers.contracts.UtxoLedgerTransactionUtil.getSingleInputState; -import static djmil.cordacheckers.contracts.UtxoLedgerTransactionUtil.getSingleOutputState; -import static djmil.cordacheckers.contracts.UtxoLedgerTransactionUtil.getSingleReferenceState; +import static djmil.cordacheckers.contracts.UtxoLedgerTransactionUtil.getSingleInputSar; +import static djmil.cordacheckers.contracts.UtxoLedgerTransactionUtil.getSingleReferenceSar; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -12,6 +11,7 @@ import djmil.cordacheckers.states.GameProposalState; import net.corda.v5.base.annotations.Suspendable; import net.corda.v5.base.exceptions.CordaRuntimeException; import net.corda.v5.ledger.utxo.Command; +import net.corda.v5.ledger.utxo.StateAndRef; import net.corda.v5.ledger.utxo.transaction.UtxoLedgerTransaction; public class GameBoardContract implements net.corda.v5.ledger.utxo.Contract { @@ -33,7 +33,7 @@ public class GameBoardContract implements net.corda.v5.ledger.utxo.Contract { break; default: - throw new CordaRuntimeException(UNKNOWN_COMMAND); + throw new CordaRuntimeException("UNKNOWN_COMMAND"); } } else if (command instanceof GameBoardCommand) { @@ -43,7 +43,8 @@ public class GameBoardContract implements net.corda.v5.ledger.utxo.Contract { break; case SURRENDER: - break; + GameBoardCommand.validateSurrenderTrx(trx); + break; case DRAW: break; @@ -51,25 +52,14 @@ public class GameBoardContract implements net.corda.v5.ledger.utxo.Contract { break; case ACCEPT: break; + default: + throw new GameBoardCommand.CommandTypeException(); } } else { - throw new CordaRuntimeException(UNKNOWN_COMMAND); + throw new RuntimeException("Bad utxo command type"); } } - @Suspendable - public static GameProposalState getReferanceGameProposalState(UtxoLedgerTransaction trx) { - final Command command = getSingleCommand(trx, Command.class); - - if (command instanceof GameProposalCommand && (GameProposalCommand)command == GameProposalCommand.ACCEPT) { - return getSingleInputState(trx, GameProposalState.class); - } else - if (command instanceof GameBoardCommand) { - return getSingleReferenceState(trx, GameProposalState.class); - } - - throw new IllegalStateException(NO_REFERANCE_GAMEPROPOSAL_STATE_FOR_TRXID +trx.getId()); - } private static void requireThat(boolean asserted, String errorMessage) { if(!asserted) { @@ -78,7 +68,6 @@ public class GameBoardContract implements net.corda.v5.ledger.utxo.Contract { } static final String REQUIRE_SINGLE_COMMAND = "Require a single command"; - static final String UNKNOWN_COMMAND = "Unsupported 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/GameProposalCommand.java b/corda/contracts/src/main/java/djmil/cordacheckers/contracts/GameProposalCommand.java index 54e0fca..3ea36dd 100644 --- a/corda/contracts/src/main/java/djmil/cordacheckers/contracts/GameProposalCommand.java +++ b/corda/contracts/src/main/java/djmil/cordacheckers/contracts/GameProposalCommand.java @@ -43,12 +43,12 @@ public enum GameProposalCommand implements Command { } } - public MemberX500Name getInitiator(StateAndRef utxoGameProposal) { - return getInitiator(utxoGameProposal.getState().getContractState()); + public MemberX500Name getInitiator(StateAndRef gameProposalSar) { + return getInitiator(gameProposalSar.getState().getContractState()); } - public MemberX500Name getRespondent(StateAndRef utxoGameProposal) { - return getRespondent(utxoGameProposal.getState().getContractState()); + public MemberX500Name getRespondent(StateAndRef gameProposalSar) { + return getRespondent(gameProposalSar.getState().getContractState()); } public static void validateCreateTrx(UtxoLedgerTransaction trx) { diff --git a/corda/contracts/src/main/java/djmil/cordacheckers/contracts/GameResultContract.java b/corda/contracts/src/main/java/djmil/cordacheckers/contracts/GameResultContract.java new file mode 100644 index 0000000..74ec8b7 --- /dev/null +++ b/corda/contracts/src/main/java/djmil/cordacheckers/contracts/GameResultContract.java @@ -0,0 +1,39 @@ +package djmil.cordacheckers.contracts; + +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); + + @Override + 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); + + switch (command.getType()) { + case SURRENDER: + GameBoardCommand.validateSurrenderTrx(trx); + break; + + // case ACCEPT: + // break; + + default: + throw new CordaRuntimeException(UNKNOWN_COMMAND); + } + } + + 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 741b611..16a71af 100644 --- a/corda/contracts/src/main/java/djmil/cordacheckers/contracts/UtxoLedgerTransactionUtil.java +++ b/corda/contracts/src/main/java/djmil/cordacheckers/contracts/UtxoLedgerTransactionUtil.java @@ -5,6 +5,7 @@ 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 net.corda.v5.ledger.utxo.transaction.UtxoLedgerTransaction; public class UtxoLedgerTransactionUtil { @@ -12,14 +13,18 @@ public class UtxoLedgerTransactionUtil { return single(utxoTrx.getCommands(clazz), clazz); } - public static T getSingleReferenceState(UtxoLedgerTransaction utxoTrx, Class clazz) { - return single(utxoTrx.getReferenceStates(clazz), clazz); + public static StateAndRef getSingleReferenceSar(UtxoLedgerTransaction utxoTrx, Class clazz) { + return singleSar(utxoTrx.getReferenceStateAndRefs(clazz), 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 T getSingleOutputState(UtxoLedgerTransaction utxoTrx, Class clazz) { return single(utxoTrx.getOutputStates(clazz), clazz); } @@ -57,6 +62,13 @@ 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/Counterparty.java b/corda/contracts/src/main/java/djmil/cordacheckers/states/Counterparty.java new file mode 100644 index 0000000..539bca7 --- /dev/null +++ b/corda/contracts/src/main/java/djmil/cordacheckers/states/Counterparty.java @@ -0,0 +1,21 @@ +package djmil.cordacheckers.states; + +import java.util.UUID; + +import org.jetbrains.annotations.NotNull; + +import net.corda.v5.base.annotations.CordaSerializable; +import net.corda.v5.base.types.MemberX500Name; + +@CordaSerializable +public interface Counterparty { + + @NotNull + MemberX500Name getCounterpartyName(MemberX500Name myName) throws NotInvolved; + + public static class NotInvolved extends RuntimeException { + public NotInvolved(MemberX500Name myName, Class clazz, UUID uuid) { + super(myName +" not involved in " +clazz.getSimpleName() +" UUID " +uuid); + } + } +} diff --git a/corda/contracts/src/main/java/djmil/cordacheckers/states/GameBoardState.java b/corda/contracts/src/main/java/djmil/cordacheckers/states/GameBoardState.java index 856446e..30555c3 100644 --- a/corda/contracts/src/main/java/djmil/cordacheckers/states/GameBoardState.java +++ b/corda/contracts/src/main/java/djmil/cordacheckers/states/GameBoardState.java @@ -9,47 +9,67 @@ import java.util.UUID; import djmil.cordacheckers.contracts.GameBoardContract; import djmil.cordacheckers.states.Piece.Color; import net.corda.v5.base.annotations.ConstructorForDeserialization; +import net.corda.v5.base.types.MemberX500Name; import net.corda.v5.ledger.utxo.BelongsToContract; import net.corda.v5.ledger.utxo.ContractState; -import net.corda.v5.ledger.utxo.StateAndRef; @BelongsToContract(GameBoardContract.class) -public class GameBoardState implements ContractState { +public class GameBoardState implements ContractState, Counterparty { + + private final MemberX500Name whitePlayerName; + private final MemberX500Name blackPlayerName; + + private final Piece.Color moveColor; + private final Map board; + private final String message; - Map board; - Piece.Color moveColor; - String message; - UUID id; - List participants; + private final UUID id; + private final List participants; public GameBoardState( - StateAndRef utxoGameBoard + GameProposalState gameBoard ) { - this.board = new LinkedHashMap(initialBoard); + this.whitePlayerName = gameBoard.getWhitePlayerName(); + this.blackPlayerName = gameBoard.getBlackPlayerName(); + + // Initial GameBoard state this.moveColor = Piece.Color.WHITE; + this.board = new LinkedHashMap(initialBoard); this.message = null; + this.id = UUID.randomUUID(); - this.participants = utxoGameBoard.getState().getContractState().getParticipants(); + this.participants = gameBoard.getParticipants(); } @ConstructorForDeserialization - public GameBoardState(Map board, Color moveColor, String message, UUID id, - List participants) { - this.board = board; + public GameBoardState(MemberX500Name whitePlayerName, MemberX500Name blackPlayerName, + Color moveColor, Map board, String message, + UUID id, List participants) { + this.whitePlayerName = whitePlayerName; + this.blackPlayerName = blackPlayerName; this.moveColor = moveColor; + this.board = board; this.message = message; this.id = id; this.participants = participants; } - public Map getBoard() { - return board; + public MemberX500Name getWhitePlayerName() { + return whitePlayerName; + } + + public MemberX500Name getBlackPlayerName() { + return blackPlayerName; } public Piece.Color getMoveColor() { return moveColor; } + public Map getBoard() { + return board; + } + public String getMessage() { return message; } @@ -62,6 +82,34 @@ public class GameBoardState implements ContractState { return participants; } + @Override + public MemberX500Name getCounterpartyName(MemberX500Name myName) throws NotInvolved { + if (whitePlayerName.compareTo(myName) == 0) + return blackPlayerName; + + if (blackPlayerName.compareTo(myName) == 0) + return whitePlayerName; + + throw new Counterparty.NotInvolved(myName, GameBoardState.class, this.id); + } + + public Piece.Color getCounterpartyColor(MemberX500Name myName) throws NotInvolved { + final MemberX500Name opponentName = getCounterpartyName(myName); + + return getWhitePlayerName().compareTo(opponentName) == 0 ? Piece.Color.WHITE : Piece.Color.BLACK; + } + + public MemberX500Name getMovePlayerName() { + switch (moveColor) { + case WHITE: + return whitePlayerName; + case BLACK: + return blackPlayerName; + default: + throw new Piece.Color.UnknownException(); + } + } + public final static Map initialBoard = Map.ofEntries( // Inspired by Checkers notation rules: https://www.bobnewell.net/nucleus/checkers.php Map.entry( 1, new Piece(Piece.Color.BLACK, Piece.Type.MAN)), diff --git a/corda/contracts/src/main/java/djmil/cordacheckers/states/GameProposalState.java b/corda/contracts/src/main/java/djmil/cordacheckers/states/GameProposalState.java index 9ccf4d6..f396f47 100644 --- a/corda/contracts/src/main/java/djmil/cordacheckers/states/GameProposalState.java +++ b/corda/contracts/src/main/java/djmil/cordacheckers/states/GameProposalState.java @@ -11,14 +11,14 @@ import net.corda.v5.ledger.utxo.BelongsToContract; import net.corda.v5.ledger.utxo.ContractState; @BelongsToContract(GameProposalContract.class) -public class GameProposalState implements ContractState { +public class GameProposalState implements ContractState, Counterparty { - MemberX500Name issuer; - MemberX500Name acquier; - Piece.Color acquierColor; - String message; - UUID id; - List participants; + private final MemberX500Name issuer; + private final MemberX500Name acquier; + private final Piece.Color acquierColor; + private final String message; + private final UUID id; + private final List participants; @ConstructorForDeserialization public GameProposalState( @@ -69,20 +69,15 @@ public class GameProposalState implements ContractState { return acquierColor == Piece.Color.BLACK ? getAcquier() : getIssuer(); } - public MemberX500Name getOpponentName(MemberX500Name myName) { + @Override + public MemberX500Name getCounterpartyName(MemberX500Name myName) throws NotInvolved { if (issuer.compareTo(myName) == 0) return acquier; - + if (acquier.compareTo(myName) == 0) return issuer; - throw new RuntimeException(myName +" seems to be not involved in " + id +" game"); - } - - public Piece.Color getOpponentColor(MemberX500Name myName) { - final MemberX500Name opponentName = getOpponentName(myName); - - return getWhitePlayerName().compareTo(opponentName) == 0 ? Piece.Color.WHITE : Piece.Color.BLACK; + throw new Counterparty.NotInvolved(myName, GameProposalState.class, this.id); } } diff --git a/corda/contracts/src/main/java/djmil/cordacheckers/states/GameResultState.java b/corda/contracts/src/main/java/djmil/cordacheckers/states/GameResultState.java new file mode 100644 index 0000000..2435c0f --- /dev/null +++ b/corda/contracts/src/main/java/djmil/cordacheckers/states/GameResultState.java @@ -0,0 +1,75 @@ +package djmil.cordacheckers.states; + +import java.security.PublicKey; +import java.util.List; +import java.util.UUID; + +import djmil.cordacheckers.contracts.GameResultContract; +import net.corda.v5.base.annotations.ConstructorForDeserialization; +import net.corda.v5.base.types.MemberX500Name; +import net.corda.v5.ledger.utxo.BelongsToContract; +import net.corda.v5.ledger.utxo.ContractState; + +@BelongsToContract(GameResultContract.class) +public class GameResultState implements ContractState, Counterparty { + + private final MemberX500Name whitePlayerName; + private final MemberX500Name blackPlayerName; + + private final Piece.Color victoryColor; + + private final UUID id; + private final List participants; + + @ConstructorForDeserialization + public GameResultState(MemberX500Name whitePlayerName, MemberX500Name blackPlayerName, Piece.Color victoryColor, + UUID id, List participants) { + this.whitePlayerName = whitePlayerName; + this.blackPlayerName = blackPlayerName; + this.victoryColor = victoryColor; + this.id = id; + this.participants = participants; + } + + public GameResultState(GameBoardState stateGameBoard, Piece.Color victoryColor) { + this.whitePlayerName = stateGameBoard.getWhitePlayerName(); + this.blackPlayerName = stateGameBoard.getBlackPlayerName(); + this.victoryColor = victoryColor; + + this.id = UUID.randomUUID(); + this.participants = stateGameBoard.getParticipants(); + } + + public MemberX500Name getWhitePlayerName() { + return whitePlayerName; + } + + public MemberX500Name getBlackPlayerName() { + return blackPlayerName; + } + + public Piece.Color getVictoryColor() { + return victoryColor; + } + + public UUID getId() { + return id; + } + + @Override + public List getParticipants() { + return participants; + } + + @Override + public MemberX500Name getCounterpartyName(MemberX500Name myName) throws NotInvolved { + if (whitePlayerName.compareTo(myName) == 0) + return blackPlayerName; + + if (blackPlayerName.compareTo(myName) == 0) + return whitePlayerName; + + throw new Counterparty.NotInvolved(myName, GameResultState.class, this.id); + } + +} diff --git a/corda/contracts/src/main/java/djmil/cordacheckers/states/Piece.java b/corda/contracts/src/main/java/djmil/cordacheckers/states/Piece.java index e549590..f766f0f 100644 --- a/corda/contracts/src/main/java/djmil/cordacheckers/states/Piece.java +++ b/corda/contracts/src/main/java/djmil/cordacheckers/states/Piece.java @@ -9,17 +9,34 @@ public class Piece { @CordaSerializable public enum Type { MAN, - KING, + KING; } @CordaSerializable public enum Color { WHITE, - BLACK, + BLACK; + + public static Color oppositOf(Color color) { + switch (color) { + case WHITE: + return BLACK; + case BLACK: + return WHITE; + default: + throw new UnknownException(); + } + } + + public static class UnknownException extends RuntimeException { + public UnknownException() { + super("Unknown Color value"); + } + } } - Color color; - Type type; + private final Color color; + private final Type type; @ConstructorForDeserialization public Piece(Color color, Type type) { diff --git a/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/CommandFlow.java b/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/CommandFlow.java index 0031fcc..17def4e 100644 --- a/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/CommandFlow.java +++ b/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/CommandFlow.java @@ -1,5 +1,12 @@ 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; @@ -7,16 +14,19 @@ import org.slf4j.LoggerFactory; import djmil.cordacheckers.FlowResult; import djmil.cordacheckers.contracts.GameBoardCommand; -import djmil.cordacheckers.contracts.GameBoardContract; -import djmil.cordacheckers.contracts.GameProposalCommand; +import djmil.cordacheckers.contracts.GameBoardCommand.CommandTypeException; +import djmil.cordacheckers.gameresult.GameResultView; import djmil.cordacheckers.states.GameBoardState; -import djmil.cordacheckers.states.GameProposalState; +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.FlowEngine; +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; @@ -26,9 +36,7 @@ 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; - +@InitiatingFlow(protocol = "game-board") public class CommandFlow implements ClientStartableFlow { private final static Logger log = LoggerFactory.getLogger(CommandFlow.class); @@ -40,37 +48,26 @@ public class CommandFlow implements ClientStartableFlow { public UtxoLedgerService utxoLedgerService; @CordaInject - public FlowEngine flowEngine; + public MemberLookup memberLookup; @CordaInject - public MemberLookup memberLookup; + public FlowMessaging flowMessaging; @Override @Suspendable public String call(ClientRequestBody requestBody) { + log.info("GameBoardCommandFlow started"); try { final CommandFlowArgs args = requestBody.getRequestBodyAs(jsonMarshallingService, CommandFlowArgs.class); - //final GameCommand command = args.getCommand(); -log.info("GameBoardCommandFlow: findUnconsumedGameBoardState"); - final StateAndRef gbStateAndRef = findUnconsumedGameBoardState(args.getGameBoardUuid()); - // final UtxoSignedTransaction trx = prepareSignedTransaction(command, utxoGameProposal); + final StateAndRef utxoSarGameBoard = findUtxoGameBoard(args.getGameBoardUuid()); + + final SecureHash trxId = doTransaction(args.getCommand(), utxoSarGameBoard); + final Object trxResult = getTrxResult(trxId); - // final SecureHash trxId = this.flowEngine - // .subFlow( new CommitSubFlow(trx, command.getRespondent(utxoGameProposal)) ); - - // if (command == GameProposalCommand.ACCEPT) { - // GameBoardState newGb = (GameBoardState)trx.getOutputStateAndRefs().get(0).getState().getContractState(); - - // return new FlowResult(newGb.getId(), trxId) - // .toJsonEncodedString(jsonMarshallingService); - // } - log.info("GameBoardCommandFlow: prepareGameBoardView"); - GameBoardView gbView = prepareGameBoardView(gbStateAndRef); - - return new FlowResult(gbView ) + return new FlowResult(trxResult, trxId) .toJsonEncodedString(jsonMarshallingService); - } + } catch (Exception e) { log.warn("GameProposalAction flow failed to process utxo request body " + requestBody + " because: " + e.getMessage()); @@ -79,7 +76,7 @@ log.info("GameBoardCommandFlow: findUnconsumedGameBoardState"); } @Suspendable - private StateAndRef findUnconsumedGameBoardState (UUID gameBoardUuid) { + 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 @@ -93,43 +90,111 @@ log.info("GameBoardCommandFlow: findUnconsumedGameBoardState"); .get(); } - // @Suspendable - // private UtxoSignedTransaction prepareSignedTransaction( - // GameProposalCommand command, - // StateAndRef utxoGameProposal - // ) { - // UtxoTransactionBuilder trxBuilder = ledgerService.createTransactionBuilder() - // .setNotary(utxoGameProposal.getState().getNotaryName()) - // .setTimeWindowBetween(Instant.now(), Instant.now().plusMillis(Duration.ofDays(1).toMillis())) - // .addInputState(utxoGameProposal.getRef()) - // .addCommand(command) - // .addSignatories(utxoGameProposal.getState().getContractState().getParticipants()); + @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); - // if (command == GameProposalCommand.ACCEPT) { - // trxBuilder = trxBuilder - // .addOutputState(new GameBoardState(utxoGameProposal)); - // //A state cannot be both an input and a reference input in the same transaction - // //.addReferenceState(utxoGameProposal.getRef()); - // } + UtxoTransactionBuilder trxBuilder = utxoLedgerService.createTransactionBuilder() + .setNotary(utxoSarGameBoard.getState().getNotaryName()) + .setTimeWindowBetween(Instant.now(), Instant.now().plusMillis(Duration.ofDays(1).toMillis())) + .addInputState(utxoSarGameBoard.getRef()) + .addCommand(command) + .addSignatories(stateGameBoard.getParticipants()); - // return trxBuilder.toSignedTransaction(); - // } + switch (command.getType()) { + case SURRENDER: + case ACCEPT: + final Piece.Color winnerColor = winnerColor(command, stateGameBoard, myName); + trxBuilder = trxBuilder + .addOutputState( new GameResultState(stateGameBoard, winnerColor) ); + break; + + default: + throw new CommandTypeException("GameBoard.CommandFlow doTransaction()"); + } + + return commit( + trxBuilder.toSignedTransaction(), + opponentName); + } @Suspendable - private GameBoardView prepareGameBoardView(StateAndRef stateAndRef) { + Piece.Color winnerColor(GameBoardCommand command, GameBoardState stateGameBoard, MemberX500Name myName) { + + switch (command.getType()) { + case SURRENDER: + return stateGameBoard.getCounterpartyColor(myName); + + case ACCEPT: + //stateGameBoard.getMoveColor(); + throw new RuntimeException("Unimplemented"); + + default: + throw new CommandTypeException("GameBoard.CommandFlow winnerColor()"); + } + } + + @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: + case ACCEPT: + return viewGameResult(utxoTrx); + + case MOVE: + case VICTORY: // request, shall be accepted by opponent + case DRAW: // request, shall be accepted by opponent + 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(); - final SecureHash trxId = stateAndRef.getRef().getTransactionId(); - - final UtxoLedgerTransaction utxoGameBoard = utxoLedgerService - .findLedgerTransaction(trxId); -log.info("GameBoardCommandFlow: createw gbView"); - GameBoardView gbView = new GameBoardView(myName, utxoGameBoard); - - gbView.previousCommand = new GameBoardCommand(GameBoardCommand.Type.SURRENDER); - - return gbView; - // return new GameBoardView(myName, utxoGameBoard); + return new GameBoardView(utxoGameBoard, myName); } } diff --git a/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/CommandResponderFlow.java b/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/CommandResponderFlow.java new file mode 100644 index 0000000..69283f9 --- /dev/null +++ b/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/CommandResponderFlow.java @@ -0,0 +1,93 @@ +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 ACCEPT: + return getSingleInputState(utxoGameBoard, GameBoardState.class); + + case MOVE: + case VICTORY: + case DRAW: + 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/CommitResponderFlow.java b/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/CommitResponderFlow.java deleted file mode 100644 index 454aa8f..0000000 --- a/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/CommitResponderFlow.java +++ /dev/null @@ -1,97 +0,0 @@ -package djmil.cordacheckers.gameboard; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import djmil.cordacheckers.contracts.GameBoardCommand; -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; - -@InitiatedBy(protocol = "game-board") -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) { - try { - UtxoTransactionValidator txValidator = ledgerTransaction -> { - final GameBoardCommand command = ledgerTransaction.getCommands(GameBoardCommand.class).get(0); - - // TODO - //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, - GameBoardCommand command - ) { - final MemberX500Name myName = memberLookup.myInfo().getName(); - final MemberX500Name otherName = session.getCounterparty(); - - // TODO - // 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 Action action = trx.getCommands(Action.class).get(0); - - // switch (action) { - // case CREATE: - // return (GameProposalState)trx.getOutputContractStates().get(0); - - // case ACCEPT: - // case REJECT: - // case CANCEL: - // return (GameProposalState)trx.getInputContractStates().get(0); - - // default: - // throw new RuntimeException(Action.UNSUPPORTED_ACTION_VALUE_OF +action); - // } - // } - -} diff --git a/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/CommitSubFlow.java b/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/CommitSubFlow.java deleted file mode 100644 index 9d70fca..0000000 --- a/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/CommitSubFlow.java +++ /dev/null @@ -1,60 +0,0 @@ -package djmil.cordacheckers.gameboard; - -import java.util.Arrays; -import java.util.List; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import net.corda.v5.application.flows.CordaInject; -import net.corda.v5.application.flows.InitiatingFlow; -import net.corda.v5.application.flows.SubFlow; -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.UtxoLedgerService; -import net.corda.v5.ledger.utxo.transaction.UtxoSignedTransaction; - -@InitiatingFlow(protocol = "game-board") -public class CommitSubFlow implements SubFlow { - - private final static Logger log = LoggerFactory.getLogger(CommitSubFlow.class); - private final UtxoSignedTransaction signedTransaction; - - public CommitSubFlow(UtxoSignedTransaction signedTransaction) { - this.signedTransaction = signedTransaction; - } - - @CordaInject - public UtxoLedgerService ledgerService; - - @CordaInject - public FlowMessaging flowMessaging; - - @Override - @Suspendable - public SecureHash call() { - log.info("GameBoard commit started"); - - final MemberX500Name respondenName = null; // TODO - - final FlowSession session = flowMessaging.initiateFlow(respondenName); - - /* - * 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 = ledgerService - .finalize(this.signedTransaction, sessionsList) - .getTransaction() - .getId(); - - log.info("GameBoard commit " +trxId); - return trxId; - } -} diff --git a/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/GameBoardView.java b/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/GameBoardView.java index 0e97c04..8d50d54 100644 --- a/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/GameBoardView.java +++ b/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/GameBoardView.java @@ -4,10 +4,9 @@ import java.util.Map; import java.util.UUID; import djmil.cordacheckers.contracts.GameBoardCommand; -import djmil.cordacheckers.contracts.GameBoardContract; import djmil.cordacheckers.contracts.UtxoLedgerTransactionUtil; +import djmil.cordacheckers.states.Counterparty.NotInvolved; import djmil.cordacheckers.states.GameBoardState; -import djmil.cordacheckers.states.GameProposalState; import djmil.cordacheckers.states.Piece; import net.corda.v5.base.types.MemberX500Name; import net.corda.v5.ledger.utxo.transaction.UtxoLedgerTransaction; @@ -19,7 +18,7 @@ public class GameBoardView { public final Piece.Color opponentColor; public final Boolean opponentMove; public final Map board; - public /*final*/ GameBoardCommand previousCommand; + public final GameBoardCommand previousCommand; public final String message; public final UUID id; @@ -36,17 +35,14 @@ public class GameBoardView { // A view from a perspective of a concrete player, on a ledger transaction that has // produced new GameBoardState - public GameBoardView(MemberX500Name myName, UtxoLedgerTransaction utxoGameBoard) { - - final GameProposalState referanceGameProposal = GameBoardContract - .getReferanceGameProposalState(utxoGameBoard); - - this.opponentName = referanceGameProposal.getOpponentName(myName).getCommonName(); - this.opponentColor = referanceGameProposal.getOpponentColor(myName); + public GameBoardView(UtxoLedgerTransaction utxoGameBoard, MemberX500Name myName) throws NotInvolved { 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.board = stateGameBoard.getBoard(); this.message = stateGameBoard.getMessage(); @@ -54,7 +50,7 @@ public class GameBoardView { this.previousCommand = UtxoLedgerTransactionUtil .getOptionalCommand(utxoGameBoard, GameBoardCommand.class) - .orElseGet(() -> null); + .orElseGet(() -> null); // there is no previous command for GameProposal.Accept case } } diff --git a/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/ListFlow.java b/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/ListFlow.java index 7b4b846..8b5921b 100644 --- a/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/ListFlow.java +++ b/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/ListFlow.java @@ -7,9 +7,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import djmil.cordacheckers.FlowResult; -import djmil.cordacheckers.contracts.GameBoardContract; import djmil.cordacheckers.states.GameBoardState; -import djmil.cordacheckers.states.GameProposalState; +import djmil.cordacheckers.states.Counterparty.NotInvolved; import net.corda.v5.application.flows.ClientRequestBody; import net.corda.v5.application.flows.ClientStartableFlow; import net.corda.v5.application.flows.CordaInject; @@ -40,14 +39,11 @@ public class ListFlow implements ClientStartableFlow { public String call(ClientRequestBody requestBody) { try { - final var unconsumedGameBoardList = utxoLedgerService + final var utxoGameBoardList = utxoLedgerService .findUnconsumedStatesByType(GameBoardState.class); - - List res = new LinkedList(); - for (StateAndRef gbState :unconsumedGameBoardList) { // NOTE: lambda operations (aka .map) can not be used with UtxoLedgerService instances - res.add(prepareGameBoardView(gbState)); - } + + final List res = prepareGameBoardViewList(utxoGameBoardList); return new FlowResult(res) .toJsonEncodedString(jsonMarshallingService); @@ -59,15 +55,29 @@ public class ListFlow implements ClientStartableFlow { } @Suspendable - private GameBoardView prepareGameBoardView(StateAndRef stateAndRef) { + private List prepareGameBoardViewList(List> utxoGamaBoardList) { final MemberX500Name myName = memberLookup.myInfo().getName(); + List res = new LinkedList(); - final SecureHash trxId = stateAndRef.getRef().getTransactionId(); + 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(myName, utxoGameBoard); + return new GameBoardView(utxoGameBoard, myName); } } diff --git a/corda/workflows/src/main/java/djmil/cordacheckers/gameproposal/CommandFlow.java b/corda/workflows/src/main/java/djmil/cordacheckers/gameproposal/CommandFlow.java index e5d3aa1..3a52791 100644 --- a/corda/workflows/src/main/java/djmil/cordacheckers/gameproposal/CommandFlow.java +++ b/corda/workflows/src/main/java/djmil/cordacheckers/gameproposal/CommandFlow.java @@ -6,11 +6,11 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import djmil.cordacheckers.FlowResult; -import djmil.cordacheckers.contracts.GameBoardCommand; import djmil.cordacheckers.contracts.GameProposalCommand; import djmil.cordacheckers.gameboard.GameBoardView; import djmil.cordacheckers.states.GameBoardState; import djmil.cordacheckers.states.GameProposalState; +import djmil.cordacheckers.states.Counterparty.NotInvolved; import net.corda.v5.application.flows.ClientRequestBody; import net.corda.v5.application.flows.ClientStartableFlow; import net.corda.v5.application.flows.CordaInject; @@ -52,21 +52,21 @@ public class CommandFlow implements ClientStartableFlow { final CommandFlowArgs args = requestBody.getRequestBodyAs(jsonMarshallingService, CommandFlowArgs.class); final GameProposalCommand command = args.getCommand(); - final StateAndRef gpStateAndRef = findUnconsumedGameProposalState(args.getGameProposalUuid()); + final StateAndRef utxoSarGameProposal = findUtxoGameProposal(args.getGameProposalUuid()); - final UtxoSignedTransaction trxCandidate = prepareSignedTransaction(command, gpStateAndRef); + final UtxoSignedTransaction utxoDraft = prepareSignedTransaction(command, utxoSarGameProposal); - final SecureHash trxId = this.flowEngine - .subFlow( new CommitSubFlow(trxCandidate, command.getRespondent(gpStateAndRef)) ); + final SecureHash utxoTrxId = this.flowEngine + .subFlow( new CommitSubFlow(utxoDraft, command.getRespondent(utxoSarGameProposal)) ); if (command == GameProposalCommand.ACCEPT) { - final GameBoardView gbView = prepareGameBoardView(trxId); + final GameBoardView viewGameBoard = prepareGameBoardView(utxoTrxId); - return new FlowResult(gbView, trxId) + return new FlowResult(viewGameBoard, utxoTrxId) .toJsonEncodedString(jsonMarshallingService); } - return new FlowResult(command+"ED", trxId) // REJECT+ED + return new FlowResult(command+"ED", utxoTrxId) // REJECT+ED .toJsonEncodedString(jsonMarshallingService); } catch (Exception e) { @@ -77,7 +77,7 @@ public class CommandFlow implements ClientStartableFlow { } @Suspendable - private StateAndRef findUnconsumedGameProposalState (UUID gpUuid) { + 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 @@ -86,7 +86,7 @@ public class CommandFlow implements ClientStartableFlow { return this.utxoLedgerService .findUnconsumedStatesByType(GameProposalState.class) .stream() - .filter(sar -> sar.getState().getContractState().getId().equals(gpUuid)) + .filter(sar -> sar.getState().getContractState().getId().equals(uuidGameProposal)) .reduce((a, b) -> {throw new IllegalStateException("Multiple states: " +a +", " +b);}) .get(); } @@ -94,33 +94,31 @@ public class CommandFlow implements ClientStartableFlow { @Suspendable private UtxoSignedTransaction prepareSignedTransaction( GameProposalCommand command, - StateAndRef gpStateAndRef + StateAndRef sarGameProposal ) { UtxoTransactionBuilder trxBuilder = utxoLedgerService.createTransactionBuilder() - .setNotary(gpStateAndRef.getState().getNotaryName()) + .setNotary(sarGameProposal.getState().getNotaryName()) .setTimeWindowBetween(Instant.now(), Instant.now().plusMillis(Duration.ofDays(1).toMillis())) - .addInputState(gpStateAndRef.getRef()) + .addInputState(sarGameProposal.getRef()) .addCommand(command) - .addSignatories(gpStateAndRef.getState().getContractState().getParticipants()); + .addSignatories(sarGameProposal.getState().getContractState().getParticipants()); if (command == GameProposalCommand.ACCEPT) { trxBuilder = trxBuilder - .addOutputState(new GameBoardState(gpStateAndRef)); - //A state cannot be both an input and a reference input in the same transaction - //.addReferenceState(utxoGameProposal.getRef()); + .addOutputState(new GameBoardState(sarGameProposal.getState().getContractState())); } return trxBuilder.toSignedTransaction(); } @Suspendable - private GameBoardView prepareGameBoardView(SecureHash gbUtxoTrxId) { + private GameBoardView prepareGameBoardView(SecureHash utxoTrxId) throws NotInvolved { final MemberX500Name myName = memberLookup.myInfo().getName(); final UtxoLedgerTransaction utxoGameBoard = utxoLedgerService - .findLedgerTransaction(gbUtxoTrxId); + .findLedgerTransaction(utxoTrxId); - return new GameBoardView(myName, utxoGameBoard); + return new GameBoardView(utxoGameBoard, myName); } } diff --git a/corda/workflows/src/main/java/djmil/cordacheckers/gameproposal/CommitResponderFlow.java b/corda/workflows/src/main/java/djmil/cordacheckers/gameproposal/CommitResponderFlow.java index 83c68a7..9dd668b 100644 --- a/corda/workflows/src/main/java/djmil/cordacheckers/gameproposal/CommitResponderFlow.java +++ b/corda/workflows/src/main/java/djmil/cordacheckers/gameproposal/CommitResponderFlow.java @@ -18,6 +18,8 @@ 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 { diff --git a/corda/workflows/src/main/java/djmil/cordacheckers/gameresult/GameResultView.java b/corda/workflows/src/main/java/djmil/cordacheckers/gameresult/GameResultView.java new file mode 100644 index 0000000..9f0d710 --- /dev/null +++ b/corda/workflows/src/main/java/djmil/cordacheckers/gameresult/GameResultView.java @@ -0,0 +1,29 @@ +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.getWhitePlayerName().getCommonName(); + this.blackPlayerName = gameResultState.getBlackPlayerName().getCommonName(); + this.victoryColor = gameResultState.getVictoryColor(); + this.id = gameResultState.getId(); + } + +}