GameBoard SURRENDER update

- UtxoLedgerTrxUtil with option
- rename action to command
- lots of minor refectorings
This commit is contained in:
djmil 2023-09-13 15:01:01 +02:00
parent d9b885b550
commit 4c2569810a
24 changed files with 427 additions and 281 deletions

View File

@ -21,6 +21,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import djmil.cordacheckers.cordaclient.dao.HoldingIdentity; import djmil.cordacheckers.cordaclient.dao.HoldingIdentity;
import djmil.cordacheckers.cordaclient.dao.Piece; import djmil.cordacheckers.cordaclient.dao.Piece;
import djmil.cordacheckers.cordaclient.dao.GameBoard; import djmil.cordacheckers.cordaclient.dao.GameBoard;
import djmil.cordacheckers.cordaclient.dao.GameBoardCommand;
import djmil.cordacheckers.cordaclient.dao.GameProposal; import djmil.cordacheckers.cordaclient.dao.GameProposal;
import djmil.cordacheckers.cordaclient.dao.VirtualNode; import djmil.cordacheckers.cordaclient.dao.VirtualNode;
import djmil.cordacheckers.cordaclient.dao.VirtualNodeList; import djmil.cordacheckers.cordaclient.dao.VirtualNodeList;
@ -29,14 +30,13 @@ import djmil.cordacheckers.cordaclient.dao.flow.ResponseBody;
import djmil.cordacheckers.cordaclient.dao.flow.arguments.GameProposalCreateReq; import djmil.cordacheckers.cordaclient.dao.flow.arguments.GameProposalCreateReq;
import djmil.cordacheckers.cordaclient.dao.flow.arguments.GameProposalCreateRes; import djmil.cordacheckers.cordaclient.dao.flow.arguments.GameProposalCreateRes;
import djmil.cordacheckers.cordaclient.dao.flow.arguments.GameProposalListRes; import djmil.cordacheckers.cordaclient.dao.flow.arguments.GameProposalListRes;
import djmil.cordacheckers.cordaclient.dao.flow.arguments.GameProposalActionReq.Command;
import djmil.cordacheckers.cordaclient.dao.flow.arguments.Empty; import djmil.cordacheckers.cordaclient.dao.flow.arguments.Empty;
import djmil.cordacheckers.cordaclient.dao.flow.arguments.GameBoardCommandReq;
import djmil.cordacheckers.cordaclient.dao.flow.arguments.GameBoardCommandRes;
import djmil.cordacheckers.cordaclient.dao.flow.arguments.GameBoardListRes; import djmil.cordacheckers.cordaclient.dao.flow.arguments.GameBoardListRes;
import djmil.cordacheckers.cordaclient.dao.flow.arguments.GameBoardMoveReq; import djmil.cordacheckers.cordaclient.dao.flow.arguments.GameProposalCommandAcceptRes;
import djmil.cordacheckers.cordaclient.dao.flow.arguments.GameBoardMoveRes; import djmil.cordacheckers.cordaclient.dao.flow.arguments.GameProposalCommandReq;
import djmil.cordacheckers.cordaclient.dao.flow.arguments.GameProposalActionAcceptRes; import djmil.cordacheckers.cordaclient.dao.flow.arguments.GameProposalCommandRes;
import djmil.cordacheckers.cordaclient.dao.flow.arguments.GameProposalActionReq;
import djmil.cordacheckers.cordaclient.dao.flow.arguments.GameProposalActionRes;
@Service @Service
public class CordaClient { public class CordaClient {
@ -130,16 +130,16 @@ public class CordaClient {
final RequestBody requestBody = new RequestBody( final RequestBody requestBody = new RequestBody(
"gp.reject-" +UUID.randomUUID(), "gp.reject-" +UUID.randomUUID(),
"djmil.cordacheckers.gameproposal.CommandFlow", "djmil.cordacheckers.gameproposal.CommandFlow",
new GameProposalActionReq( new GameProposalCommandReq(
gameProposalUuid.toString(), gameProposalUuid.toString(),
Command.REJECT GameProposalCommandReq.Command.REJECT
) )
); );
final GameProposalActionRes actionResult = cordaFlowExecute( final GameProposalCommandRes actionResult = cordaFlowExecute(
myHoldingIdentity, myHoldingIdentity,
requestBody, requestBody,
GameProposalActionRes.class GameProposalCommandRes.class
); );
if (actionResult.failureStatus() != null) { if (actionResult.failureStatus() != null) {
@ -150,23 +150,23 @@ public class CordaClient {
return actionResult.successStatus(); return actionResult.successStatus();
} }
public UUID gameProposalAccept( public UUID gameProposalAccept( // TODO shall return newGameBoard
HoldingIdentity myHoldingIdentity, HoldingIdentity myHoldingIdentity,
UUID gameProposalUuid UUID gameProposalUuid
) { ) {
final RequestBody requestBody = new RequestBody( final RequestBody requestBody = new RequestBody(
"gp.accept-" +UUID.randomUUID(), "gp.accept-" +UUID.randomUUID(),
"djmil.cordacheckers.gameproposal.CommandFlow", "djmil.cordacheckers.gameproposal.CommandFlow",
new GameProposalActionReq( new GameProposalCommandReq(
gameProposalUuid.toString(), gameProposalUuid.toString(),
Command.ACCEPT GameProposalCommandReq.Command.ACCEPT
) )
); );
final GameProposalActionAcceptRes actionResult = cordaFlowExecute( final GameProposalCommandAcceptRes actionResult = cordaFlowExecute(
myHoldingIdentity, myHoldingIdentity,
requestBody, requestBody,
GameProposalActionAcceptRes.class GameProposalCommandAcceptRes.class
); );
if (actionResult.failureStatus() != null) { if (actionResult.failureStatus() != null) {
@ -199,24 +199,24 @@ public class CordaClient {
return listFlowResult.successStatus(); return listFlowResult.successStatus();
} }
public GameBoard gameBoardMove( public GameBoard gameBoardCommand(
HoldingIdentity myHoldingIdentity, HoldingIdentity myHoldingIdentity,
UUID gameBoardUuid, UUID gameBoardUuid,
int from, int to GameBoardCommand command
) { ) {
final RequestBody requestBody = new RequestBody( final RequestBody requestBody = new RequestBody(
"gb.move-" +UUID.randomUUID(), "gb.command-" +command.getType() +UUID.randomUUID(),
"djmil.cordacheckers.gameboard.MoveFlow", "djmil.cordacheckers.gameboard.CommandFlow",
new GameBoardMoveReq( new GameBoardCommandReq(
gameBoardUuid, gameBoardUuid,
from, to command
) )
); );
final GameBoardMoveRes moveResult = cordaFlowExecute( final GameBoardCommandRes moveResult = cordaFlowExecute(
myHoldingIdentity, myHoldingIdentity,
requestBody, requestBody,
GameBoardMoveRes.class GameBoardCommandRes.class
); );
if (moveResult.failureStatus() != null) { if (moveResult.failureStatus() != null) {

View File

@ -11,6 +11,7 @@ public record GameBoard(
Piece.Color opponentColor, Piece.Color opponentColor,
Boolean opponentMove, Boolean opponentMove,
Map<Integer, Piece> board, Map<Integer, Piece> board,
GameBoardCommand previousCommand,
String message, String message,
UUID id) implements CordaState { UUID id) implements CordaState {

View File

@ -0,0 +1,48 @@
package djmil.cordacheckers.cordaclient.dao;
import java.util.List;
public class GameBoardCommand {
public static enum Type {
MOVE,
SURRENDER,
REQUEST_DRAW,
REQUEST_VICTORY,
FINISH; // aka accept DRAW or VICTORY request
}
private final Type type;
private final List<Integer> 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<Integer> move) {
this.type = Type.MOVE;
this.move = move;
}
public Type getType() {
return type;
}
public List<Integer> getMove() {
return move;
}
@Override
public String toString() {
if (type == Type.MOVE)
return move.get(0) +"->" +move.get(1);
else
return type.name();
}
}

View File

@ -0,0 +1,8 @@
package djmil.cordacheckers.cordaclient.dao.flow.arguments;
import java.util.UUID;
import djmil.cordacheckers.cordaclient.dao.GameBoardCommand;
public record GameBoardCommandReq(UUID gameBoardUuid, GameBoardCommand command) {
}

View File

@ -2,6 +2,6 @@ package djmil.cordacheckers.cordaclient.dao.flow.arguments;
import djmil.cordacheckers.cordaclient.dao.GameBoard; import djmil.cordacheckers.cordaclient.dao.GameBoard;
public record GameBoardMoveRes (GameBoard successStatus, String failureStatus) { public record GameBoardCommandRes(GameBoard successStatus, String failureStatus) {
} }

View File

@ -1,7 +0,0 @@
package djmil.cordacheckers.cordaclient.dao.flow.arguments;
import java.util.UUID;
public record GameBoardMoveReq(UUID gameBoard, int from, int to) {
}

View File

@ -1,7 +0,0 @@
package djmil.cordacheckers.cordaclient.dao.flow.arguments;
import java.util.UUID;
public record GameProposalActionAcceptRes(UUID successStatus, String transactionId, String failureStatus) {
}

View File

@ -1,5 +0,0 @@
package djmil.cordacheckers.cordaclient.dao.flow.arguments;
public record GameProposalActionRes(String successStatus, String transactionId, String failureStatus) {
}

View File

@ -0,0 +1,7 @@
package djmil.cordacheckers.cordaclient.dao.flow.arguments;
import java.util.UUID;
public record GameProposalCommandAcceptRes(UUID successStatus, String transactionId, String failureStatus) {
}

View File

@ -1,6 +1,6 @@
package djmil.cordacheckers.cordaclient.dao.flow.arguments; package djmil.cordacheckers.cordaclient.dao.flow.arguments;
public record GameProposalActionReq(String gameProposalUuid, Command command) { public record GameProposalCommandReq(String gameProposalUuid, Command command) {
public enum Command { public enum Command {
ACCEPT, ACCEPT,
REJECT, REJECT,

View File

@ -0,0 +1,5 @@
package djmil.cordacheckers.cordaclient.dao.flow.arguments;
public record GameProposalCommandRes(String successStatus, String transactionId, String failureStatus) {
}

View File

@ -17,6 +17,7 @@ import com.fasterxml.jackson.databind.JsonMappingException;
import djmil.cordacheckers.cordaclient.dao.CordaState; import djmil.cordacheckers.cordaclient.dao.CordaState;
import djmil.cordacheckers.cordaclient.dao.GameBoard; import djmil.cordacheckers.cordaclient.dao.GameBoard;
import djmil.cordacheckers.cordaclient.dao.GameBoardCommand;
import djmil.cordacheckers.cordaclient.dao.GameProposal; import djmil.cordacheckers.cordaclient.dao.GameProposal;
import djmil.cordacheckers.cordaclient.dao.Piece; import djmil.cordacheckers.cordaclient.dao.Piece;
import djmil.cordacheckers.cordaclient.dao.VirtualNode; import djmil.cordacheckers.cordaclient.dao.VirtualNode;
@ -56,8 +57,7 @@ public class CordaClientTest {
holdingIdentityResolver.getByUsername(gpIssuer), holdingIdentityResolver.getByUsername(gpIssuer),
holdingIdentityResolver.getByUsername(gpAcquier), holdingIdentityResolver.getByUsername(gpAcquier),
gpAcquierColor, gpAcquierColor,
gpMessage gpMessage);
);
List<GameProposal> gpListSender = cordaClient.gameProposalList( List<GameProposal> gpListSender = cordaClient.gameProposalList(
holdingIdentityResolver.getByUsername(gpIssuer)); holdingIdentityResolver.getByUsername(gpIssuer));
@ -87,8 +87,7 @@ public class CordaClientTest {
holdingIdentityResolver.getByUsername(gpIssuer), holdingIdentityResolver.getByUsername(gpIssuer),
holdingIdentityResolver.getByUsername(gpAcquier), holdingIdentityResolver.getByUsername(gpAcquier),
gpReceiverColor, gpReceiverColor,
gpMessage gpMessage);
);
System.out.println("Create GP UUID "+ gpUuid); System.out.println("Create GP UUID "+ gpUuid);
@ -100,8 +99,7 @@ public class CordaClientTest {
final String rejectRes = cordaClient.gameProposalReject( final String rejectRes = cordaClient.gameProposalReject(
holdingIdentityResolver.getByUsername(gpAcquier), holdingIdentityResolver.getByUsername(gpAcquier),
gpUuid gpUuid);
);
assertThat(rejectRes).isEqualToIgnoringCase("REJECTED"); assertThat(rejectRes).isEqualToIgnoringCase("REJECTED");
@ -147,8 +145,7 @@ public class CordaClientTest {
final UUID newGameBoardId = cordaClient.gameProposalAccept( final UUID newGameBoardId = cordaClient.gameProposalAccept(
holdingIdentityResolver.getByUsername(gpAcquier), holdingIdentityResolver.getByUsername(gpAcquier),
gpUuid gpUuid);
);
System.out.println("New GameBoard UUID "+newGameBoardId); System.out.println("New GameBoard UUID "+newGameBoardId);
@ -186,6 +183,33 @@ public class CordaClientTest {
System.out.println("GB list: " +gbList); System.out.println("GB list: " +gbList);
} }
@Test
void testGameBoardSurrender() throws JsonMappingException, JsonProcessingException {
final var hiAlice = holdingIdentityResolver.getByUsername("alice");
final var hiBob = holdingIdentityResolver.getByUsername("bob");
final var bobColor = Piece.Color.WHITE;
final UUID gpUuid = cordaClient.gameProposalCreate(
hiAlice, hiBob,
bobColor, "GameBoard SURRENDER test"
);
System.out.println("New GameProposal UUID "+ gpUuid);
final UUID newGameBoardId = cordaClient.gameProposalAccept(
hiBob, gpUuid
);
System.out.println("New GameBoard UUID "+ newGameBoardId);
GameBoard gbSurrender = cordaClient.gameBoardCommand(
hiBob, newGameBoardId,
new GameBoardCommand(GameBoardCommand.Type.SURRENDER)
);
System.out.println("SURRENDER GB: "+gbSurrender);
}
private <T extends CordaState> T findByUuid(List<T> statesList, UUID uuid) { private <T extends CordaState> T findByUuid(List<T> statesList, UUID uuid) {
for (T state : statesList) { for (T state : statesList) {
if (state.id().compareTo(uuid) == 0) if (state.id().compareTo(uuid) == 0)

View File

@ -1,43 +0,0 @@
package djmil.cordacheckers;
import net.corda.v5.ledger.utxo.Command;
import net.corda.v5.ledger.utxo.ContractState;
import net.corda.v5.ledger.utxo.transaction.UtxoLedgerTransaction;
public class UtxoLedgerTransactionUtil {
private final UtxoLedgerTransaction trx;
public UtxoLedgerTransactionUtil(UtxoLedgerTransaction trx) {
this.trx = trx;
}
public <T extends Command> T getSingleCommand(Class<T> clazz) {
return trx.getCommands(clazz)
.stream()
.reduce( (a, b) -> {throw new IllegalStateException(trx.getId() +EXPECTED_SINGLE_CLAZZ +clazz);} )
.get();
}
public <T extends ContractState> T getSingleReferenceState(Class<T> clazz) {
return trx.getReferenceStates(clazz)
.stream()
.reduce( (a, b) -> {throw new IllegalStateException(trx.getId() +EXPECTED_SINGLE_CLAZZ +clazz);} )
.get();
}
public <T extends ContractState> T getSingleInputState(Class<T> clazz) {
return trx.getInputStates(clazz)
.stream()
.reduce( (a, b) -> {throw new IllegalStateException(trx.getId() +EXPECTED_SINGLE_CLAZZ +clazz);} )
.get();
}
public <T extends ContractState> T getSingleOutputState(Class<T> clazz) {
return trx.getOutputStates(clazz)
.stream()
.reduce( (a, b) -> {throw new IllegalStateException(trx.getId() +EXPECTED_SINGLE_CLAZZ +clazz);} )
.get();
}
private static String EXPECTED_SINGLE_CLAZZ = ": expected single ";
}

View File

@ -7,15 +7,22 @@ import net.corda.v5.ledger.utxo.Command;
public class GameBoardCommand implements Command { public class GameBoardCommand implements Command {
public static enum Type { public static enum Type {
MOVE,
SURRENDER, SURRENDER,
DRAW, REQUEST_DRAW,
VICTORY, REQUEST_VICTORY,
MOVE; FINISH; // aka accept DRAW or VICTORY request
} }
private final Type type; private final Type type;
private final List<Integer> move; // [0] from, [1] to private final List<Integer> move; // [0] from, [1] to
// Serialisation service requires a default constructor
public GameBoardCommand() {
this.type = null;
this.move = null;
}
public GameBoardCommand(Type type) { public GameBoardCommand(Type type) {
if (type == Type.MOVE) if (type == Type.MOVE)
throw new CordaRuntimeException (BAD_ACTIONMOVE_CONSTRUCTOR); throw new CordaRuntimeException (BAD_ACTIONMOVE_CONSTRUCTOR);
@ -34,12 +41,8 @@ public class GameBoardCommand implements Command {
} }
public List<Integer> getMove() { public List<Integer> getMove() {
if (type != Type.MOVE)
throw new CordaRuntimeException (NO_MOVES_FOR_ACTIONTYPE +type);
return this.move; return this.move;
} }
static final String BAD_ACTIONMOVE_CONSTRUCTOR = "Bad constructor for Action.MOVE"; static final String BAD_ACTIONMOVE_CONSTRUCTOR = "Bad constructor for Action.MOVE";
static final String NO_MOVES_FOR_ACTIONTYPE = ".getMove() not possible for ";
} }

View File

@ -1,9 +1,13 @@
package djmil.cordacheckers.contracts; package djmil.cordacheckers.contracts;
import static djmil.cordacheckers.contracts.UtxoLedgerTransactionUtil.getSingleCommand;
import static djmil.cordacheckers.contracts.UtxoLedgerTransactionUtil.getSingleInputState;
import static djmil.cordacheckers.contracts.UtxoLedgerTransactionUtil.getSingleOutputState;
import static djmil.cordacheckers.contracts.UtxoLedgerTransactionUtil.getSingleReferenceState;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import djmil.cordacheckers.UtxoLedgerTransactionUtil;
import djmil.cordacheckers.states.GameProposalState; import djmil.cordacheckers.states.GameProposalState;
import net.corda.v5.base.annotations.Suspendable; import net.corda.v5.base.annotations.Suspendable;
import net.corda.v5.base.exceptions.CordaRuntimeException; import net.corda.v5.base.exceptions.CordaRuntimeException;
@ -18,8 +22,7 @@ public class GameBoardContract implements net.corda.v5.ledger.utxo.Contract {
public void verify(UtxoLedgerTransaction trx) { public void verify(UtxoLedgerTransaction trx) {
log.info("GameBoardContract.verify() called"); log.info("GameBoardContract.verify() called");
final UtxoLedgerTransactionUtil trxUtil = new UtxoLedgerTransactionUtil(trx); final Command command = getSingleCommand(trx, Command.class);
final Command command = trxUtil.getSingleCommand(Command.class);
if (command instanceof GameProposalCommand) { if (command instanceof GameProposalCommand) {
log.info("GameBoardContract.verify() as GameProposalCommand "+(GameProposalCommand)command); log.info("GameBoardContract.verify() as GameProposalCommand "+(GameProposalCommand)command);
@ -27,13 +30,15 @@ public class GameBoardContract implements net.corda.v5.ledger.utxo.Contract {
if (command instanceof GameBoardCommand) { if (command instanceof GameBoardCommand) {
log.info("GameBoardContract.verify() as GameBoardCommand "+((GameBoardCommand)command).getType()); log.info("GameBoardContract.verify() as GameBoardCommand "+((GameBoardCommand)command).getType());
switch (((GameBoardCommand)command).getType()) { switch (((GameBoardCommand)command).getType()) {
case MOVE:
break;
case SURRENDER: case SURRENDER:
break; break;
case DRAW: case REQUEST_DRAW:
break; break;
case VICTORY: case REQUEST_VICTORY:
break; break;
case MOVE: case FINISH:
break; break;
} }
} else { } else {
@ -43,14 +48,13 @@ public class GameBoardContract implements net.corda.v5.ledger.utxo.Contract {
@Suspendable @Suspendable
public static GameProposalState getReferanceGameProposalState(UtxoLedgerTransaction trx) { public static GameProposalState getReferanceGameProposalState(UtxoLedgerTransaction trx) {
final UtxoLedgerTransactionUtil trxUtil = new UtxoLedgerTransactionUtil(trx); final Command command = getSingleCommand(trx, Command.class);
final Command command = trxUtil.getSingleCommand(Command.class);
if (command instanceof GameProposalCommand && (GameProposalCommand)command == GameProposalCommand.ACCEPT) { if (command instanceof GameProposalCommand && (GameProposalCommand)command == GameProposalCommand.ACCEPT) {
return trxUtil.getSingleInputState(GameProposalState.class); return getSingleInputState(trx, GameProposalState.class);
} else } else
if (command instanceof GameBoardCommand) { if (command instanceof GameBoardCommand) {
return trxUtil.getSingleReferenceState(GameProposalState.class); return getSingleReferenceState(trx, GameProposalState.class);
} }
throw new IllegalStateException(NO_REFERANCE_GAMEPROPOSAL_STATE_FOR_TRXID +trx.getId()); throw new IllegalStateException(NO_REFERANCE_GAMEPROPOSAL_STATE_FOR_TRXID +trx.getId());

View File

@ -1,9 +1,12 @@
package djmil.cordacheckers.contracts; package djmil.cordacheckers.contracts;
import static djmil.cordacheckers.contracts.UtxoLedgerTransactionUtil.getSingleCommand;
import static djmil.cordacheckers.contracts.UtxoLedgerTransactionUtil.getSingleInputState;
import static djmil.cordacheckers.contracts.UtxoLedgerTransactionUtil.getSingleOutputState;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import djmil.cordacheckers.UtxoLedgerTransactionUtil;
import djmil.cordacheckers.states.GameBoardState; import djmil.cordacheckers.states.GameBoardState;
import djmil.cordacheckers.states.GameProposalState; import djmil.cordacheckers.states.GameProposalState;
import net.corda.v5.base.exceptions.CordaRuntimeException; import net.corda.v5.base.exceptions.CordaRuntimeException;
@ -17,15 +20,14 @@ public class GameProposalContract implements net.corda.v5.ledger.utxo.Contract {
public void verify(UtxoLedgerTransaction trx) { public void verify(UtxoLedgerTransaction trx) {
log.info("GameProposalContract.verify() called"); log.info("GameProposalContract.verify() called");
final UtxoLedgerTransactionUtil trxUtil = new UtxoLedgerTransactionUtil(trx); final GameProposalCommand command = getSingleCommand(trx, GameProposalCommand.class);
final GameProposalCommand command = trxUtil.getSingleCommand(GameProposalCommand.class);
switch (command) { switch (command) {
case CREATE: { case CREATE: {
requireThat(trx.getInputContractStates().isEmpty(), CREATE_INPUT_STATE); requireThat(trx.getInputContractStates().isEmpty(), CREATE_INPUT_STATE);
requireThat(trx.getOutputContractStates().size() == 1, CREATE_OUTPUT_STATE); requireThat(trx.getOutputContractStates().size() == 1, CREATE_OUTPUT_STATE);
GameProposalState outputState = trxUtil.getSingleOutputState(GameProposalState.class); GameProposalState outputState = getSingleOutputState(trx, GameProposalState.class);
requireThat(outputState.getAcquierColor() != null, NON_NULL_RECIPIENT_COLOR); requireThat(outputState.getAcquierColor() != null, NON_NULL_RECIPIENT_COLOR);
break; } break; }
@ -34,8 +36,8 @@ public class GameProposalContract implements net.corda.v5.ledger.utxo.Contract {
requireThat(trx.getInputContractStates().size() == 1, ACCEPT_INPUT_STATE); requireThat(trx.getInputContractStates().size() == 1, ACCEPT_INPUT_STATE);
requireThat(trx.getOutputContractStates().size() == 1, ACCEPT_OUTPUT_STATE); requireThat(trx.getOutputContractStates().size() == 1, ACCEPT_OUTPUT_STATE);
GameProposalState inGameProposal = trxUtil.getSingleInputState(GameProposalState.class); GameProposalState inGameProposal = getSingleInputState(trx, GameProposalState.class);
GameBoardState outGameBoard = trxUtil.getSingleOutputState(GameBoardState.class); GameBoardState outGameBoard = getSingleOutputState(trx, GameBoardState.class);
requireThat(outGameBoard.getParticipants().containsAll(inGameProposal.getParticipants()), ACCEPT_PARTICIPANTS); requireThat(outGameBoard.getParticipants().containsAll(inGameProposal.getParticipants()), ACCEPT_PARTICIPANTS);
break; } break; }
@ -44,14 +46,14 @@ public class GameProposalContract implements net.corda.v5.ledger.utxo.Contract {
requireThat(trx.getInputContractStates().size() == 1, REJECT_INPUT_STATE); requireThat(trx.getInputContractStates().size() == 1, REJECT_INPUT_STATE);
requireThat(trx.getOutputContractStates().isEmpty(), REJECT_OUTPUT_STATE); requireThat(trx.getOutputContractStates().isEmpty(), REJECT_OUTPUT_STATE);
trxUtil.getSingleInputState(GameProposalState.class); getSingleInputState(trx, GameProposalState.class);
break; } break; }
case CANCEL: case CANCEL:
requireThat(trx.getInputContractStates().size() == 1, CANCEL_INPUT_STATE); requireThat(trx.getInputContractStates().size() == 1, CANCEL_INPUT_STATE);
requireThat(trx.getOutputContractStates().isEmpty(), CANCEL_OUTPUT_STATE); requireThat(trx.getOutputContractStates().isEmpty(), CANCEL_OUTPUT_STATE);
trxUtil.getSingleInputState(GameProposalState.class); getSingleInputState(trx, GameProposalState.class);
break; break;
default: default:

View File

@ -0,0 +1,56 @@
package djmil.cordacheckers.contracts;
import java.util.List;
import java.util.Optional;
import net.corda.v5.ledger.utxo.Command;
import net.corda.v5.ledger.utxo.ContractState;
import net.corda.v5.ledger.utxo.transaction.UtxoLedgerTransaction;
public class UtxoLedgerTransactionUtil {
public static <T extends Command> T getSingleCommand(UtxoLedgerTransaction utxoTrx, Class<T> clazz) {
return single(utxoTrx.getCommands(clazz), clazz);
}
public static <T extends ContractState> T getSingleReferenceState(UtxoLedgerTransaction utxoTrx, Class<T> clazz) {
return single(utxoTrx.getReferenceStates(clazz), clazz);
}
public static <T extends ContractState> T getSingleInputState(UtxoLedgerTransaction utxoTrx, Class<T> clazz) {
return single(utxoTrx.getInputStates(clazz), clazz);
}
public static <T extends ContractState> T getSingleOutputState(UtxoLedgerTransaction utxoTrx, Class<T> clazz) {
return single(utxoTrx.getOutputStates(clazz), clazz);
}
public static <T extends Command> Optional<T> getOptionalCommand(UtxoLedgerTransaction utxoTrx, Class<T> clazz) {
return optional(utxoTrx.getCommands(clazz), clazz);
}
public static <T extends ContractState> Optional<T> getOptionalReferenceState(UtxoLedgerTransaction utxoTrx, Class<T> clazz) {
return optional(utxoTrx.getReferenceStates(clazz), clazz);
}
public static <T extends ContractState> Optional<T> getOptionalInputState(UtxoLedgerTransaction utxoTrx, Class<T> clazz) {
return optional(utxoTrx.getInputStates(clazz), clazz);
}
public static <T extends ContractState> Optional<T> getOptionalOutputState(UtxoLedgerTransaction utxoTrx, Class<T> clazz) {
return optional(utxoTrx.getOutputStates(clazz), clazz);
}
private static <T> Optional<T> optional(List<T> list, Class<T> clazz) {
return list
.stream()
.reduce((a, b) -> {throw new IllegalStateException(MULTIPLE_INSTANCES_OF +clazz.getName());});
}
private static <T> T single(List<T> list, Class<T> clazz) {
return optional(list, clazz)
.orElseThrow( () -> new IllegalStateException(NO_INSTANCES_OF +clazz.getName()) );
}
private static String MULTIPLE_INSTANCES_OF = "Multiple instances of ";
private static String NO_INSTANCES_OF = "No instances of ";
}

View File

@ -0,0 +1,135 @@
package djmil.cordacheckers.gameboard;
import java.util.UUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import djmil.cordacheckers.FlowResult;
import djmil.cordacheckers.contracts.GameBoardCommand;
import djmil.cordacheckers.contracts.GameBoardContract;
import djmil.cordacheckers.contracts.GameProposalCommand;
import djmil.cordacheckers.states.GameBoardState;
import djmil.cordacheckers.states.GameProposalState;
import net.corda.v5.application.flows.ClientRequestBody;
import net.corda.v5.application.flows.ClientStartableFlow;
import net.corda.v5.application.flows.CordaInject;
import net.corda.v5.application.flows.FlowEngine;
import net.corda.v5.application.marshalling.JsonMarshallingService;
import net.corda.v5.application.membership.MemberLookup;
import net.corda.v5.base.annotations.Suspendable;
import net.corda.v5.base.types.MemberX500Name;
import net.corda.v5.crypto.SecureHash;
import net.corda.v5.ledger.utxo.StateAndRef;
import net.corda.v5.ledger.utxo.UtxoLedgerService;
import net.corda.v5.ledger.utxo.transaction.UtxoLedgerTransaction;
import net.corda.v5.ledger.utxo.transaction.UtxoSignedTransaction;
import net.corda.v5.ledger.utxo.transaction.UtxoTransactionBuilder;
import java.time.Duration;
import java.time.Instant;
public class CommandFlow implements ClientStartableFlow {
private final static Logger log = LoggerFactory.getLogger(CommandFlow.class);
@CordaInject
public JsonMarshallingService jsonMarshallingService;
@CordaInject
public UtxoLedgerService utxoLedgerService;
@CordaInject
public FlowEngine flowEngine;
@CordaInject
public MemberLookup memberLookup;
@Override
@Suspendable
public String call(ClientRequestBody requestBody) {
try {
final CommandFlowArgs args = requestBody.getRequestBodyAs(jsonMarshallingService, CommandFlowArgs.class);
//final GameCommand command = args.getCommand();
log.info("GameBoardCommandFlow: findUnconsumedGameBoardState");
final StateAndRef<GameBoardState> gbStateAndRef = findUnconsumedGameBoardState(args.getGameBoardUuid());
// final UtxoSignedTransaction trx = prepareSignedTransaction(command, utxoGameProposal);
// final SecureHash trxId = this.flowEngine
// .subFlow( new CommitSubFlow(trx, command.getRespondent(utxoGameProposal)) );
// if (command == GameProposalCommand.ACCEPT) {
// GameBoardState newGb = (GameBoardState)trx.getOutputStateAndRefs().get(0).getState().getContractState();
// return new FlowResult(newGb.getId(), trxId)
// .toJsonEncodedString(jsonMarshallingService);
// }
log.info("GameBoardCommandFlow: prepareGameBoardView");
GameBoardView gbView = prepareGameBoardView(gbStateAndRef);
return new FlowResult(gbView )
.toJsonEncodedString(jsonMarshallingService);
}
catch (Exception e) {
log.warn("GameProposalAction flow failed to process utxo request body " + requestBody +
" because: " + e.getMessage());
return new FlowResult(e).toJsonEncodedString(jsonMarshallingService);
}
}
@Suspendable
private StateAndRef<GameBoardState> findUnconsumedGameBoardState (UUID gameBoardUuid) {
/*
* Get list of all unconsumed aka 'active' GameProposalStates, then filter by UUID.
* Note, this is an inefficient way to perform this operation if there are a large
* number of 'active' GameProposals exists in storage.
*/
return this.utxoLedgerService
.findUnconsumedStatesByType(GameBoardState.class)
.stream()
.filter(sar -> sar.getState().getContractState().getId().equals(gameBoardUuid))
.reduce((a, b) -> {throw new IllegalStateException("Multiple states: " +a +", " +b);})
.get();
}
// @Suspendable
// private UtxoSignedTransaction prepareSignedTransaction(
// GameProposalCommand command,
// StateAndRef<GameProposalState> utxoGameProposal
// ) {
// UtxoTransactionBuilder trxBuilder = ledgerService.createTransactionBuilder()
// .setNotary(utxoGameProposal.getState().getNotaryName())
// .setTimeWindowBetween(Instant.now(), Instant.now().plusMillis(Duration.ofDays(1).toMillis()))
// .addInputState(utxoGameProposal.getRef())
// .addCommand(command)
// .addSignatories(utxoGameProposal.getState().getContractState().getParticipants());
// if (command == GameProposalCommand.ACCEPT) {
// trxBuilder = trxBuilder
// .addOutputState(new GameBoardState(utxoGameProposal));
// //A state cannot be both an input and a reference input in the same transaction
// //.addReferenceState(utxoGameProposal.getRef());
// }
// return trxBuilder.toSignedTransaction();
// }
@Suspendable
private GameBoardView prepareGameBoardView(StateAndRef<GameBoardState> stateAndRef) {
final MemberX500Name myName = memberLookup.myInfo().getName();
final SecureHash trxId = stateAndRef.getRef().getTransactionId();
final UtxoLedgerTransaction utxoGameBoard = utxoLedgerService
.findLedgerTransaction(trxId);
log.info("GameBoardCommandFlow: createw gbView");
GameBoardView gbView = new GameBoardView(myName, utxoGameBoard);
gbView.previousCommand = new GameBoardCommand(GameBoardCommand.Type.SURRENDER);
return gbView;
// return new GameBoardView(myName, utxoGameBoard);
}
}

View File

@ -1,24 +1,24 @@
package djmil.cordacheckers.gameboard; package djmil.cordacheckers.gameboard;
import java.util.List;
import java.util.UUID; import java.util.UUID;
public class MoveFlowArgs { import djmil.cordacheckers.contracts.GameBoardCommand;
public class CommandFlowArgs {
private UUID gameBoardUuid; private UUID gameBoardUuid;
private List<Integer> move; private GameBoardCommand command;
// Serialisation service requires a default constructor // Serialisation service requires a default constructor
public MoveFlowArgs() { public CommandFlowArgs() {
this.gameBoardUuid = null; this.gameBoardUuid = null;
this.move = null; this.command = null;
}
public GameBoardCommand getCommand() {
return this.command;
} }
public UUID getGameBoardUuid() { public UUID getGameBoardUuid() {
return this.gameBoardUuid; return this.gameBoardUuid;
} }
public List<Integer> getMove() {
return this.move;
}
} }

View File

@ -0,0 +1,60 @@
package djmil.cordacheckers.gameboard;
import java.util.Map;
import java.util.UUID;
import djmil.cordacheckers.contracts.GameBoardCommand;
import djmil.cordacheckers.contracts.GameBoardContract;
import djmil.cordacheckers.contracts.UtxoLedgerTransactionUtil;
import djmil.cordacheckers.states.GameBoardState;
import djmil.cordacheckers.states.GameProposalState;
import djmil.cordacheckers.states.Piece;
import net.corda.v5.base.types.MemberX500Name;
import net.corda.v5.ledger.utxo.transaction.UtxoLedgerTransaction;
// GameBoard from the player's point of view
public class GameBoardView {
public final String opponentName;
public final Piece.Color opponentColor;
public final Boolean opponentMove;
public final Map<Integer, Piece> board;
public /*final*/ GameBoardCommand previousCommand;
public final String message;
public final UUID id;
// Serialisation service requires a default constructor
public GameBoardView() {
this.opponentName = null;
this.opponentColor = null;
this.opponentMove = null;
this.board = null;
this.previousCommand = null;
this.message = null;
this.id = null;
}
// A view from a perspective of a concrete player, on a ledger transaction that has
// produced new GameBoardState
public GameBoardView(MemberX500Name myName, UtxoLedgerTransaction utxoGameBoard) {
final GameProposalState referanceGameProposal = GameBoardContract
.getReferanceGameProposalState(utxoGameBoard);
this.opponentName = referanceGameProposal.getOpponentName(myName).getCommonName();
this.opponentColor = referanceGameProposal.getOpponentColor(myName);
final GameBoardState stateGameBoard = UtxoLedgerTransactionUtil
.getSingleOutputState(utxoGameBoard, GameBoardState.class);
this.opponentMove = this.opponentColor == stateGameBoard.getMoveColor();
this.board = stateGameBoard.getBoard();
this.message = stateGameBoard.getMessage();
this.id = stateGameBoard.getId();
this.previousCommand = UtxoLedgerTransactionUtil
.getOptionalCommand(utxoGameBoard, GameBoardCommand.class)
.orElseGet(() -> null);
}
}

View File

@ -43,10 +43,10 @@ public class ListFlow implements ClientStartableFlow {
final var unconsumedGameBoardList = utxoLedgerService final var unconsumedGameBoardList = utxoLedgerService
.findUnconsumedStatesByType(GameBoardState.class); .findUnconsumedStatesByType(GameBoardState.class);
List<ListItem> res = new LinkedList<ListItem>(); List<GameBoardView> res = new LinkedList<GameBoardView>();
for (StateAndRef<GameBoardState> gbState :unconsumedGameBoardList) { for (StateAndRef<GameBoardState> gbState :unconsumedGameBoardList) {
// NOTE: lambda operations (aka .map) can not be used with UtxoLedgerService instances // NOTE: lambda operations (aka .map) can not be used with UtxoLedgerService instances
res.add(prepareListItem(gbState)); res.add(prepareGameBoardView(gbState));
} }
return new FlowResult(res) return new FlowResult(res)
@ -59,7 +59,7 @@ public class ListFlow implements ClientStartableFlow {
} }
@Suspendable @Suspendable
private ListItem prepareListItem(StateAndRef<GameBoardState> stateAndRef) { private GameBoardView prepareGameBoardView(StateAndRef<GameBoardState> stateAndRef) {
final MemberX500Name myName = memberLookup.myInfo().getName(); final MemberX500Name myName = memberLookup.myInfo().getName();
final SecureHash trxId = stateAndRef.getRef().getTransactionId(); final SecureHash trxId = stateAndRef.getRef().getTransactionId();
@ -67,14 +67,9 @@ public class ListFlow implements ClientStartableFlow {
final UtxoLedgerTransaction utxoGameBoard = utxoLedgerService final UtxoLedgerTransaction utxoGameBoard = utxoLedgerService
.findLedgerTransaction(trxId); .findLedgerTransaction(trxId);
final GameProposalState referanceGameProposal = GameBoardContract var newGbView = new GameBoardView(myName, utxoGameBoard);
.getReferanceGameProposalState(utxoGameBoard);
return new ListItem( return newGbView;
stateAndRef.getState().getContractState(),
referanceGameProposal.getOpponentName(myName),
referanceGameProposal.getOpponentColor(myName)
);
} }
} }

View File

@ -1,41 +0,0 @@
package djmil.cordacheckers.gameboard;
import java.util.UUID;
import djmil.cordacheckers.states.GameBoardState;
import djmil.cordacheckers.states.Piece;
import net.corda.v5.base.types.MemberX500Name;
// Class to hold results of the List flow.
// JsonMarshallingService can only serialize simple classes that the underlying Jackson serializer recognises,
// hence creating a DTO style object which consists only of Strings and a UUIDs. It is possible to create custom
// serializers for the JsonMarshallingService in the future.
public class ListItem {
public final String opponentName;
public final Piece.Color opponentColor;
public final Boolean opponentMove;
public final Object board;
public final String message;
public final UUID id;
// Serialisation service requires a default constructor
public ListItem() {
this.opponentName = null;
this.opponentColor = null;
this.opponentMove = null;
this.board = null;
this.message = null;
this.id = null;
}
public ListItem(GameBoardState gameBoard, MemberX500Name opponentName, Piece.Color opponentColor) {
this.opponentName = opponentName.getCommonName();
this.opponentColor = opponentColor;
this.opponentMove = opponentColor == gameBoard.getMoveColor();
this.board = gameBoard.getBoard();
this.message = gameBoard.getMessage();
this.id = gameBoard.getId();
}
}

View File

@ -1,97 +0,0 @@
package djmil.cordacheckers.gameboard;
import java.util.UUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import djmil.cordacheckers.FlowResult;
import djmil.cordacheckers.contracts.GameBoardCommand;
import djmil.cordacheckers.states.GameBoardState;
import net.corda.v5.application.flows.ClientRequestBody;
import net.corda.v5.application.flows.ClientStartableFlow;
import net.corda.v5.application.flows.CordaInject;
import net.corda.v5.application.flows.FlowEngine;
import net.corda.v5.application.marshalling.JsonMarshallingService;
import net.corda.v5.base.annotations.Suspendable;
import net.corda.v5.crypto.SecureHash;
import net.corda.v5.ledger.utxo.StateAndRef;
import net.corda.v5.ledger.utxo.UtxoLedgerService;
import net.corda.v5.ledger.utxo.transaction.UtxoSignedTransaction;
import net.corda.v5.ledger.utxo.transaction.UtxoTransactionBuilder;
import java.time.Duration;
import java.time.Instant;
public class MoveFlow implements ClientStartableFlow {
private final static Logger log = LoggerFactory.getLogger(MoveFlow.class);
@CordaInject
public JsonMarshallingService jsonMarshallingService;
@CordaInject
public UtxoLedgerService ledgerService;
@CordaInject
public FlowEngine flowEngine;
@Override
@Suspendable
public String call(ClientRequestBody requestBody) {
try {
final MoveFlowArgs args = requestBody.getRequestBodyAs(jsonMarshallingService, MoveFlowArgs.class);
final GameBoardCommand command = new GameBoardCommand(args.getMove());
final StateAndRef<GameBoardState> utxoGameBoard = findUnconsumedGameBoardState(args.getGameBoardUuid());
final GameBoardState newGameBoard = null; // TODO
final UtxoSignedTransaction trx = prepareSignedTransaction(command, utxoGameBoard, newGameBoard);
final SecureHash trxId = this.flowEngine
.subFlow( new CommitSubFlow(trx) );
return new FlowResult(newGameBoard, trxId) // TODO return players perspective GB
.toJsonEncodedString(jsonMarshallingService);
}
catch (Exception e) {
log.warn("GameBoardAction flow failed to process utxo request body " + requestBody +
" because: " + e.getMessage());
return new FlowResult(e).toJsonEncodedString(jsonMarshallingService);
}
}
@Suspendable
private StateAndRef<GameBoardState> findUnconsumedGameBoardState (UUID gameProposalUuid) {
/*
* Get list of all unconsumed aka 'active' GameBoardState, then filter by UUID.
* Note, this is an inefficient way to perform this operation if there are a large
* number of 'active' GameProposals exists in storage.
*/
return this.ledgerService
.findUnconsumedStatesByType(GameBoardState.class)
.stream()
.filter(sar -> sar.getState().getContractState().getId().equals(gameProposalUuid))
.reduce((a, b) -> {throw new IllegalStateException("Multiple states: " +a +", " +b);})
.get();
}
@Suspendable
private UtxoSignedTransaction prepareSignedTransaction(
GameBoardCommand command,
StateAndRef<GameBoardState> utxoGameProposal,
GameBoardState newGameBoard
) {
UtxoTransactionBuilder trxBuilder = ledgerService.createTransactionBuilder()
.setNotary(utxoGameProposal.getState().getNotaryName())
.setTimeWindowBetween(Instant.now(), Instant.now().plusMillis(Duration.ofDays(1).toMillis()))
.addInputState(utxoGameProposal.getRef())
.addOutputState(newGameBoard)
.addCommand(command)
.addSignatories(utxoGameProposal.getState().getContractState().getParticipants());
return trxBuilder.toSignedTransaction();
}
}

View File

@ -44,8 +44,6 @@ public class CommandFlow implements ClientStartableFlow {
final CommandFlowArgs args = requestBody.getRequestBodyAs(jsonMarshallingService, CommandFlowArgs.class); final CommandFlowArgs args = requestBody.getRequestBodyAs(jsonMarshallingService, CommandFlowArgs.class);
final GameProposalCommand command = args.getCommand(); final GameProposalCommand command = args.getCommand();
System.out.println("Game Proposal command" + command);
final StateAndRef<GameProposalState> utxoGameProposal = findUnconsumedGameProposalState(args.getGameProposalUuid()); final StateAndRef<GameProposalState> utxoGameProposal = findUnconsumedGameProposalState(args.getGameProposalUuid());
final UtxoSignedTransaction trx = prepareSignedTransaction(command, utxoGameProposal); final UtxoSignedTransaction trx = prepareSignedTransaction(command, utxoGameProposal);