GameProposal ACCEPT

- create new GameBoard state
- test
This commit is contained in:
djmil 2023-09-11 13:50:05 +02:00
parent e235ecb942
commit 7f7722ecc0
23 changed files with 647 additions and 126 deletions

View File

@ -19,7 +19,8 @@ import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import djmil.cordacheckers.cordaclient.dao.HoldingIdentity;
import djmil.cordacheckers.cordaclient.dao.Color;
import djmil.cordacheckers.cordaclient.dao.Piece;
import djmil.cordacheckers.cordaclient.dao.GameBoard;
import djmil.cordacheckers.cordaclient.dao.GameProposal;
import djmil.cordacheckers.cordaclient.dao.VirtualNode;
import djmil.cordacheckers.cordaclient.dao.VirtualNodeList;
@ -28,7 +29,10 @@ import djmil.cordacheckers.cordaclient.dao.flow.ResponseBody;
import djmil.cordacheckers.cordaclient.dao.flow.arguments.GameProposalCreateReq;
import djmil.cordacheckers.cordaclient.dao.flow.arguments.GameProposalCreateRes;
import djmil.cordacheckers.cordaclient.dao.flow.arguments.GameProposalListRes;
import djmil.cordacheckers.cordaclient.dao.flow.arguments.GameProposalActionReq.Action;
import djmil.cordacheckers.cordaclient.dao.flow.arguments.Empty;
import djmil.cordacheckers.cordaclient.dao.flow.arguments.GameBoardListRes;
import djmil.cordacheckers.cordaclient.dao.flow.arguments.GameProposalActionAcceptRes;
import djmil.cordacheckers.cordaclient.dao.flow.arguments.GameProposalActionReq;
import djmil.cordacheckers.cordaclient.dao.flow.arguments.GameProposalActionRes;
@ -68,7 +72,7 @@ public class CordaClient {
public List<GameProposal> gameProposalList(HoldingIdentity holdingIdentity) {
final RequestBody requestBody = new RequestBody(
"list-" + UUID.randomUUID(),
"gp.list-" + UUID.randomUUID(),
"djmil.cordacheckers.gameproposal.ListFlow",
new Empty()
);
@ -88,67 +92,111 @@ public class CordaClient {
}
public UUID gameProposalCreate(
HoldingIdentity sender,
HoldingIdentity receiver,
Color receiverColor,
HoldingIdentity issuer,
HoldingIdentity acquier,
Piece.Color acquierColor,
String message
) throws JsonMappingException, JsonProcessingException {
final GameProposalCreateReq createGameProposal = new GameProposalCreateReq(
receiver.x500Name(),
receiverColor,
message
);
final RequestBody requestBody = new RequestBody(
"create-" + UUID.randomUUID(),
"gp.create-" + UUID.randomUUID(),
"djmil.cordacheckers.gameproposal.CreateFlow",
createGameProposal
new GameProposalCreateReq(
acquier.x500Name(),
acquierColor,
message
)
);
final GameProposalCreateRes createResult = cordaFlowExecute(
sender,
issuer,
requestBody,
GameProposalCreateRes.class
);
if (createResult.failureStatus() != null) {
System.out.println("GameProposalCreateFlow failed: " + createResult.failureStatus());
System.out.println("GameProposal.CreateFlow failed: " + createResult.failureStatus());
throw new RuntimeException("GameProsal: CreateFlow execution has failed");
}
return createResult.successStatus();
}
public String gameProposalAction(
HoldingIdentity self,
UUID gameProposalUuid,
GameProposalActionReq.Action action
public String gameProposalReject(
HoldingIdentity myHoldingIdentity,
UUID gameProposalUuid
) {
final GameProposalActionReq rejectGameProposal = new GameProposalActionReq(
gameProposalUuid.toString(),
action
);
final RequestBody requestBody = new RequestBody(
"reject-" + UUID.randomUUID(),
"gp.reject-" +UUID.randomUUID(),
"djmil.cordacheckers.gameproposal.ActionFlow",
rejectGameProposal
new GameProposalActionReq(
gameProposalUuid.toString(),
Action.REJECT
)
);
final GameProposalActionRes actionResult = cordaFlowExecute(
self,
myHoldingIdentity,
requestBody,
GameProposalActionRes.class
);
if (actionResult.failureStatus() != null) {
System.out.println("GameProposalActionFlow failed: " + actionResult.failureStatus());
System.out.println("GameProposal.ActionFlow failed: " + actionResult.failureStatus());
throw new RuntimeException("GameProsal: ActionFlow execution has failed");
}
return actionResult.successStatus();
}
public UUID gameProposalAccept(
HoldingIdentity myHoldingIdentity,
UUID gameProposalUuid
) {
final RequestBody requestBody = new RequestBody(
"gp.accept-" +UUID.randomUUID(),
"djmil.cordacheckers.gameproposal.ActionFlow",
new GameProposalActionReq(
gameProposalUuid.toString(),
Action.ACCEPT
)
);
final GameProposalActionAcceptRes actionResult = cordaFlowExecute(
myHoldingIdentity,
requestBody,
GameProposalActionAcceptRes.class
);
if (actionResult.failureStatus() != null) {
System.out.println("GameProposal.ActionFlow failed: " + actionResult.failureStatus());
throw new RuntimeException("GameProsal: ActionFlow execution has failed");
}
return actionResult.successStatus();
}
public List<GameBoard> gameBoardList(HoldingIdentity holdingIdentity) {
final RequestBody requestBody = new RequestBody(
"gb.list-" + UUID.randomUUID(),
"djmil.cordacheckers.gameboard.ListFlow",
new Empty()
);
final GameBoardListRes listFlowResult = cordaFlowExecute(
holdingIdentity,
requestBody,
GameBoardListRes.class
);
if (listFlowResult.failureStatus() != null) {
System.out.println("GameBoard.ListFlow failed: " + listFlowResult.failureStatus());
throw new RuntimeException("GameBoard: ListFlow execution has failed");
}
return listFlowResult.successStatus();
}
private <T> T cordaFlowExecute(HoldingIdentity holdingIdentity, RequestBody requestBody, Class<T> flowResultType) {
try {

View File

@ -1,6 +0,0 @@
package djmil.cordacheckers.cordaclient.dao;
public enum Color {
WHITE,
BLACK
}

View File

@ -0,0 +1,7 @@
package djmil.cordacheckers.cordaclient.dao;
import java.util.UUID;
public interface CordaState {
public UUID id();
}

View File

@ -0,0 +1,17 @@
package djmil.cordacheckers.cordaclient.dao;
import java.util.Map;
import java.util.UUID;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
@JsonDeserialize
public record GameBoard(
String opponentName,
Piece.Color opponentColor,
Boolean opponentMove,
Map<Integer, Piece> board,
String message,
UUID id) implements CordaState {
}

View File

@ -8,8 +8,8 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
public record GameProposal(
String issuer,
String acquier,
Color acquierColor,
Piece.Color acquierColor,
String message,
UUID id) {
UUID id) implements CordaState {
}

View File

@ -0,0 +1,41 @@
package djmil.cordacheckers.cordaclient.dao;
public class Piece {
public enum Type {
MAN,
KING,
}
public enum Color {
WHITE,
BLACK,
}
Color color;
Type type;
public Piece() {
this.color = null;
this.type = null;
}
public Piece(Color color, Type type) {
this.color = color;
this.type = type;
}
public Color getColor() {
return color;
}
public Type getType() {
return type;
}
@Override
public String toString() {
return color +"." +type;
}
}

View File

@ -0,0 +1,12 @@
package djmil.cordacheckers.cordaclient.dao.flow.arguments;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import djmil.cordacheckers.cordaclient.dao.GameBoard;
@JsonIgnoreProperties(ignoreUnknown = true)
public record GameBoardListRes(List<GameBoard> successStatus, String failureStatus) {
}

View File

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

View File

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

View File

@ -17,7 +17,7 @@ import com.fasterxml.jackson.databind.JsonMappingException;
import djmil.cordacheckers.cordaclient.CordaClient;
import djmil.cordacheckers.cordaclient.dao.HoldingIdentity;
import djmil.cordacheckers.cordaclient.dao.flow.arguments.GameProposalCreateReq;
import djmil.cordacheckers.cordaclient.dao.Color;
import djmil.cordacheckers.cordaclient.dao.Piece;
import djmil.cordacheckers.cordaclient.dao.GameProposal;
import djmil.cordacheckers.user.HoldingIdentityResolver;
import djmil.cordacheckers.user.User;
@ -61,7 +61,7 @@ public class GameProposalController {
final HoldingIdentity gpSender = sender.getHoldingIdentity();
// TODO: throw execption with custom type
final HoldingIdentity gpReceiver = holdingIdentityResolver.getByUsername(gpRequest.opponentName());
final Color gpReceiverColor = gpRequest.opponentColor();
final Piece.Color gpReceiverColor = gpRequest.opponentColor();
// TODO handle expectionns here
UUID newGameProposalUuid = cordaClient.gameProposalCreate(

View File

@ -1,19 +1,5 @@
package djmil.cordacheckers.cordaclient;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonMappingException;
import djmil.cordacheckers.cordaclient.dao.Color;
import djmil.cordacheckers.cordaclient.dao.GameProposal;
import djmil.cordacheckers.cordaclient.dao.VirtualNode;
import djmil.cordacheckers.cordaclient.dao.flow.arguments.GameProposalActionReq.Action;
import djmil.cordacheckers.user.HoldingIdentityResolver;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@ -22,6 +8,20 @@ import java.util.UUID;
import javax.naming.InvalidNameException;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonMappingException;
import djmil.cordacheckers.cordaclient.dao.CordaState;
import djmil.cordacheckers.cordaclient.dao.GameBoard;
import djmil.cordacheckers.cordaclient.dao.GameProposal;
import djmil.cordacheckers.cordaclient.dao.Piece;
import djmil.cordacheckers.cordaclient.dao.VirtualNode;
import djmil.cordacheckers.user.HoldingIdentityResolver;
@SpringBootTest
public class CordaClientTest {
@Autowired
@ -49,7 +49,7 @@ public class CordaClientTest {
void testGemeProposalCreate() throws JsonMappingException, JsonProcessingException {
final String gpIssuer = "alice";
final String gpAcquier = "bob";
final Color gpAcquierColor = Color.WHITE;
final Piece.Color gpAcquierColor = Piece.Color.WHITE;
final String gpMessage = "GameProposal create test";
final UUID createdGpUuid = cordaClient.gameProposalCreate(
@ -78,58 +78,118 @@ public class CordaClientTest {
@Test
void testGemeProposalReject() throws JsonMappingException, JsonProcessingException {
final String gpSender = "alice";
final String gpReceiver = "bob";
final Color gpReceiverColor = Color.WHITE;
final String gpIssuer = "alice";
final String gpAcquier = "bob";
final Piece.Color gpReceiverColor = Piece.Color.WHITE;
final String gpMessage = "GameProposal REJECT test";
final UUID gpUuid = cordaClient.gameProposalCreate(
holdingIdentityResolver.getByUsername(gpSender),
holdingIdentityResolver.getByUsername(gpReceiver),
holdingIdentityResolver.getByUsername(gpIssuer),
holdingIdentityResolver.getByUsername(gpAcquier),
gpReceiverColor,
gpMessage
);
System.out.println("Create GP UUID "+ gpUuid);
assertThatThrownBy(() -> {
cordaClient.gameProposalAction(
holdingIdentityResolver.getByUsername(gpSender),
gpUuid,
Action.REJECT);
assertThatThrownBy(() -> { // Issuer can not reject
cordaClient.gameProposalReject(
holdingIdentityResolver.getByUsername(gpIssuer),
gpUuid);
});
final String rejectRes = cordaClient.gameProposalAction(
holdingIdentityResolver.getByUsername(gpReceiver),
gpUuid,
Action.REJECT
final String rejectRes = cordaClient.gameProposalReject(
holdingIdentityResolver.getByUsername(gpAcquier),
gpUuid
);
assertThat(rejectRes).isEqualToIgnoringCase("REJECTED");
List<GameProposal> gpListSender = cordaClient.gameProposalList(
holdingIdentityResolver.getByUsername(gpSender));
holdingIdentityResolver.getByUsername(gpIssuer));
assertThat(findByUuid(gpListSender, gpUuid)).isNull();
List<GameProposal> gpListReceiver = cordaClient.gameProposalList(
holdingIdentityResolver.getByUsername(gpReceiver));
holdingIdentityResolver.getByUsername(gpAcquier));
assertThat(findByUuid(gpListReceiver, gpUuid)).isNull();
// GameProposal can not be rejected twice
assertThatThrownBy(() -> {
cordaClient.gameProposalAction(
holdingIdentityResolver.getByUsername(gpSender),
gpUuid,
Action.REJECT);
cordaClient.gameProposalReject(
holdingIdentityResolver.getByUsername(gpIssuer),
gpUuid);
});
}
private GameProposal findByUuid(List<GameProposal> gpList, UUID uuid) {
for (GameProposal gameProposal : gpList) {
if (gameProposal.id().compareTo(uuid) == 0)
return gameProposal;
@Test
void testGemeProposalAccept() throws JsonMappingException, JsonProcessingException {
final String gpIssuer = "alice";
final String gpAcquier = "bob";
final Piece.Color gpAcquierColor = Piece.Color.WHITE;
final String gpMessage = "GameProposal ACCEPT test";
final UUID gpUuid = cordaClient.gameProposalCreate(
holdingIdentityResolver.getByUsername(gpIssuer),
holdingIdentityResolver.getByUsername(gpAcquier),
gpAcquierColor,
gpMessage
);
System.out.println("New GameProposal UUID "+ gpUuid);
assertThatThrownBy(() -> { // Issuer can not accept
cordaClient.gameProposalAccept(
holdingIdentityResolver.getByUsername(gpIssuer),
gpUuid);
});
final UUID newGameBoardId = cordaClient.gameProposalAccept(
holdingIdentityResolver.getByUsername(gpAcquier),
gpUuid
);
System.out.println("New GameBoard UUID "+newGameBoardId);
List<GameBoard> gbListIssuer = cordaClient.gameBoardList(
holdingIdentityResolver.getByUsername(gpIssuer));
GameBoard gbAlice = findByUuid(gbListIssuer, newGameBoardId);
assertThat(gbAlice).isNotNull();
assertThat(gbAlice.opponentName()).isEqualToIgnoringCase(gpAcquier);
assertThat(gbAlice.opponentColor()).isEqualByComparingTo(gpAcquierColor);
assertThat(gbAlice.opponentMove()).isEqualTo(true);
List<GameBoard> gbListAcquier = cordaClient.gameBoardList(
holdingIdentityResolver.getByUsername(gpAcquier));
GameBoard bgBob = findByUuid(gbListAcquier, newGameBoardId);
assertThat(bgBob).isNotNull();
assertThat(bgBob.opponentName()).isEqualToIgnoringCase(gpIssuer);
assertThat(bgBob.opponentColor()).isEqualByComparingTo(Piece.Color.BLACK);
assertThat(bgBob.opponentMove()).isEqualTo(false);
assertThatThrownBy(() -> { // GameProposal can not be accepted twice
cordaClient.gameProposalAccept(
holdingIdentityResolver.getByUsername(gpAcquier),
gpUuid);
});
}
@Test
void testGemeProposalList() throws JsonMappingException, JsonProcessingException {
List <GameBoard> gbList = cordaClient.gameBoardList(
holdingIdentityResolver.getByUsername("bob"));
System.out.println("GB list: " +gbList);
}
private <T extends CordaState> T findByUuid(List<T> statesList, UUID uuid) {
for (T state : statesList) {
if (state.id().compareTo(uuid) == 0)
return state;
};
return null;
}

View File

@ -0,0 +1,69 @@
package djmil.cordacheckers.contracts;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.transaction.UtxoLedgerTransaction;
import static djmil.cordacheckers.contracts.GameProposalContract.Action;
public class GameBoardContract implements net.corda.v5.ledger.utxo.Contract {
private final static Logger log = LoggerFactory.getLogger(GameBoardContract.class);
public static class Move implements Command { }
public static class Surrender implements Command { }
public static class ClaimVictory implements Command { }
public static class ProposeDraw implements Command { }
@Override
public void verify(UtxoLedgerTransaction trx) {
log.info("GameBoardContract.verify()");
requireThat(trx.getCommands().size() == 1, REQUIRE_SINGLE_COMMAND);
Command command = trx.getCommands().get(0);
}
@Suspendable
public static GameProposalState getReferanceGameProposalState(UtxoLedgerTransaction trx) {
final List<Command> commandList = trx.getCommands();
requireThat(commandList.size() == 1, REQUIRE_SINGLE_COMMAND);
final Command command = commandList.get(0);
if (command instanceof Action && (Action)command == Action.ACCEPT) {
return trx.getInputStates(GameProposalState.class)
.stream()
.reduce( (a, b) -> {throw new IllegalStateException(SINGLE_STATE_EXPECTED);} )
.get();
} else
if (command instanceof GameBoardContract.Move)
{
List<GameProposalState> refStates = trx.getReferenceStates(GameProposalState.class);
if (refStates.size() == 1) {
return (GameProposalState) refStates.get(0);
}
}
throw new IllegalStateException(NO_REFERANCE_GAMEPROPOSAL_STATE_FOR_TRXID +trx.getId());
}
private static void requireThat(boolean asserted, String errorMessage) {
if(!asserted) {
throw new CordaRuntimeException("Failed requirement: " + errorMessage);
}
}
static final String REQUIRE_SINGLE_COMMAND = "Require a single command";
static final String 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 ";
}

View File

@ -3,7 +3,7 @@ package djmil.cordacheckers.contracts;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
//import djmil.cordacheckers.states.GameProposalResolutionState;
import djmil.cordacheckers.states.GameBoardState;
import djmil.cordacheckers.states.GameProposalState;
import net.corda.v5.base.exceptions.CordaRuntimeException;
import net.corda.v5.base.types.MemberX500Name;
@ -47,11 +47,11 @@ public class GameProposalContract implements net.corda.v5.ledger.utxo.Contract {
}
}
public MemberX500Name getSender(StateAndRef<GameProposalState> utxoGameProposal) {
public MemberX500Name getInitiator(StateAndRef<GameProposalState> utxoGameProposal) {
return getInitiator(utxoGameProposal.getState().getContractState());
}
public MemberX500Name getReceiver(StateAndRef<GameProposalState> utxoGameProposal) {
public MemberX500Name getRespondent(StateAndRef<GameProposalState> utxoGameProposal) {
return getRespondent(utxoGameProposal.getState().getContractState());
}
@ -64,7 +64,7 @@ public class GameProposalContract implements net.corda.v5.ledger.utxo.Contract {
requireThat(trx.getCommands().size() == 1, REQUIRE_SINGLE_COMMAND);
switch ((Action)(trx.getCommands().get(0))) {
switch ((trx.getCommands(Action.class).get(0))) {
case CREATE: {
requireThat(trx.getInputContractStates().isEmpty(), CREATE_INPUT_STATE);
requireThat(trx.getOutputContractStates().size() == 1, CREATE_OUTPUT_STATE);
@ -72,12 +72,21 @@ public class GameProposalContract implements net.corda.v5.ledger.utxo.Contract {
GameProposalState outputState = trx.getOutputStates(GameProposalState.class).get(0);
requireThat(outputState != null, CREATE_OUTPUT_STATE);
requireThat(outputState.getRecipientColor() != null, NON_NULL_RECIPIENT_COLOR);
requireThat(outputState.getAcquierColor() != null, NON_NULL_RECIPIENT_COLOR);
break; }
case ACCEPT:
throw new CordaRuntimeException("Unimplemented!");
//break;
requireThat(trx.getInputContractStates().size() == 1, ACCEPT_INPUT_STATE);
requireThat(trx.getOutputContractStates().size() == 1, ACCEPT_OUTPUT_STATE);
GameProposalState inGameProposal = trx.getInputStates(GameProposalState.class).get(0);
requireThat(inGameProposal != null, ACCEPT_INPUT_STATE);
GameBoardState outGameBoard = trx.getOutputStates(GameBoardState.class).get(0);
requireThat(outGameBoard != null, ACCEPT_INPUT_STATE);
requireThat(outGameBoard.getParticipants().containsAll(inGameProposal.getParticipants()), ACCEPT_PARTICIPANTS);
break;
case REJECT:
requireThat(trx.getInputContractStates().size() == 1, REJECT_INPUT_STATE);
@ -114,4 +123,8 @@ public class GameProposalContract implements net.corda.v5.ledger.utxo.Contract {
static final String CANCEL_INPUT_STATE = "Cancel command should have exactly one GameProposal input state";
static final String CANCEL_OUTPUT_STATE = "Cancel command should have no output states";
static final String ACCEPT_INPUT_STATE = "Accept command should have exactly one GameProposal input state";
static final String ACCEPT_OUTPUT_STATE = "Accept command should have exactly one GameBoard output state";
static final String ACCEPT_PARTICIPANTS = "Accept command: GameBoard participants should math GameProposal participants";
}

View File

@ -0,0 +1,93 @@
package djmil.cordacheckers.states;
import java.security.PublicKey;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
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.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 {
Map<Integer, Piece> board;
Piece.Color moveColor;
String message;
UUID id;
List<PublicKey> participants;
public GameBoardState(
StateAndRef<GameProposalState> utxoGameBoard
) {
this.board = new LinkedHashMap<Integer, Piece>(initialBoard);
this.moveColor = Piece.Color.WHITE;
this.message = null;
this.id = UUID.randomUUID();
this.participants = utxoGameBoard.getState().getContractState().getParticipants();
}
@ConstructorForDeserialization
public GameBoardState(Map<Integer, Piece> board, Color moveColor, String message, UUID id,
List<PublicKey> participants) {
this.board = board;
this.moveColor = moveColor;
this.message = message;
this.id = id;
this.participants = participants;
}
public Map<Integer, Piece> getBoard() {
return board;
}
public Piece.Color getMoveColor() {
return moveColor;
}
public String getMessage() {
return message;
}
public UUID getId() {
return id;
}
public List<PublicKey> getParticipants() {
return participants;
}
public final static Map<Integer, Piece> 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)),
Map.entry( 2, new Piece(Piece.Color.BLACK, Piece.Type.MAN)),
Map.entry( 3, new Piece(Piece.Color.BLACK, Piece.Type.MAN)),
Map.entry( 4, new Piece(Piece.Color.BLACK, Piece.Type.MAN)),
Map.entry( 5, new Piece(Piece.Color.BLACK, Piece.Type.MAN)),
Map.entry( 6, new Piece(Piece.Color.BLACK, Piece.Type.MAN)),
Map.entry( 7, new Piece(Piece.Color.BLACK, Piece.Type.MAN)),
Map.entry( 8, new Piece(Piece.Color.BLACK, Piece.Type.MAN)),
Map.entry( 9, new Piece(Piece.Color.BLACK, Piece.Type.MAN)),
Map.entry(10, new Piece(Piece.Color.BLACK, Piece.Type.MAN)),
Map.entry(11, new Piece(Piece.Color.BLACK, Piece.Type.MAN)),
Map.entry(12, new Piece(Piece.Color.BLACK, Piece.Type.MAN)),
Map.entry(21, new Piece(Piece.Color.WHITE, Piece.Type.MAN)),
Map.entry(22, new Piece(Piece.Color.WHITE, Piece.Type.MAN)),
Map.entry(23, new Piece(Piece.Color.WHITE, Piece.Type.MAN)),
Map.entry(24, new Piece(Piece.Color.WHITE, Piece.Type.MAN)),
Map.entry(25, new Piece(Piece.Color.WHITE, Piece.Type.MAN)),
Map.entry(26, new Piece(Piece.Color.WHITE, Piece.Type.MAN)),
Map.entry(27, new Piece(Piece.Color.WHITE, Piece.Type.MAN)),
Map.entry(28, new Piece(Piece.Color.WHITE, Piece.Type.MAN)),
Map.entry(29, new Piece(Piece.Color.WHITE, Piece.Type.MAN)),
Map.entry(30, new Piece(Piece.Color.WHITE, Piece.Type.MAN)),
Map.entry(31, new Piece(Piece.Color.WHITE, Piece.Type.MAN)),
Map.entry(32, new Piece(Piece.Color.WHITE, Piece.Type.MAN))
);
}

View File

@ -15,24 +15,23 @@ public class GameProposalState implements ContractState {
MemberX500Name issuer;
MemberX500Name acquier;
Piece.Color recipientColor;
Piece.Color acquierColor;
String message;
UUID id;
List<PublicKey> participants;
// Allows serialisation and to use a specified UUID
@ConstructorForDeserialization
public GameProposalState(
MemberX500Name issuer,
MemberX500Name acquier,
Piece.Color recipientColor,
Piece.Color acquierColor,
String message,
UUID id,
List<PublicKey> participants
) {
this.issuer = issuer;
this.acquier = acquier;
this.recipientColor = recipientColor;
this.acquierColor = acquierColor;
this.message = message;
this.id = id;
this.participants = participants;
@ -46,8 +45,8 @@ public class GameProposalState implements ContractState {
return acquier;
}
public Piece.Color getRecipientColor() {
return recipientColor;
public Piece.Color getAcquierColor() {
return acquierColor;
}
public String getMessage() {
@ -61,4 +60,29 @@ public class GameProposalState implements ContractState {
public List<PublicKey> getParticipants() {
return participants;
}
public MemberX500Name getWhitePlayerName() {
return acquierColor == Piece.Color.WHITE ? getAcquier() : getIssuer();
}
public MemberX500Name getBlackPlayerName() {
return acquierColor == Piece.Color.BLACK ? getAcquier() : getIssuer();
}
public MemberX500Name getOpponentName(MemberX500Name myName) {
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;
}
}

View File

@ -1,5 +1,6 @@
package djmil.cordacheckers.states;
import net.corda.v5.base.annotations.ConstructorForDeserialization;
import net.corda.v5.base.annotations.CordaSerializable;
@CordaSerializable
@ -20,5 +21,18 @@ public class Piece {
Color color;
Type type;
@ConstructorForDeserialization
public Piece(Color color, Type type) {
this.color = color;
this.type = type;
}
public Color getColor() {
return color;
}
public Type getType() {
return type;
}
}

View File

@ -0,0 +1,80 @@
package djmil.cordacheckers.gameboard;
import java.util.LinkedList;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import djmil.cordacheckers.FlowResult;
import djmil.cordacheckers.contracts.GameBoardContract;
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.marshalling.JsonMarshallingService;
import net.corda.v5.application.membership.MemberLookup;
import net.corda.v5.base.annotations.Suspendable;
import net.corda.v5.base.types.MemberX500Name;
import net.corda.v5.crypto.SecureHash;
import net.corda.v5.ledger.utxo.StateAndRef;
import net.corda.v5.ledger.utxo.UtxoLedgerService;
import net.corda.v5.ledger.utxo.transaction.UtxoLedgerTransaction;
public class ListFlow implements ClientStartableFlow {
private final static Logger log = LoggerFactory.getLogger(ListFlow.class);
@CordaInject
public MemberLookup memberLookup;
@CordaInject
public UtxoLedgerService utxoLedgerService;
@CordaInject
public JsonMarshallingService jsonMarshallingService;
@Suspendable
@Override
public String call(ClientRequestBody requestBody) {
try {
final var unconsumedGameBoardList = utxoLedgerService
.findUnconsumedStatesByType(GameBoardState.class);
List<ListItem> res = new LinkedList<ListItem>();
for (StateAndRef<GameBoardState> gbState :unconsumedGameBoardList) {
// NOTE: lambda operations (aka .map) can not be used with UtxoLedgerService instances
res.add(prepareListItem(gbState));
}
return new FlowResult(res)
.toJsonEncodedString(jsonMarshallingService);
}
catch (Exception e) {
log.warn("GameBoard.ListFlow failed to process utxo request body " + requestBody + " because: " + e.getMessage());
return new FlowResult(e).toJsonEncodedString(jsonMarshallingService);
}
}
@Suspendable
private ListItem prepareListItem(StateAndRef<GameBoardState> stateAndRef) {
final MemberX500Name myName = memberLookup.myInfo().getName();
final SecureHash trxId = stateAndRef.getRef().getTransactionId();
final UtxoLedgerTransaction utxoGameBoard = utxoLedgerService
.findLedgerTransaction(trxId);
final GameProposalState referanceGameProposal = GameBoardContract
.getReferanceGameProposalState(utxoGameBoard);
return new ListItem(
stateAndRef.getState().getContractState(),
referanceGameProposal.getOpponentName(myName),
referanceGameProposal.getOpponentColor(myName)
);
}
}

View File

@ -0,0 +1,41 @@
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,13 +1,12 @@
package djmil.cordacheckers.gameproposal;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import djmil.cordacheckers.FlowResult;
import djmil.cordacheckers.states.GameBoardState;
import djmil.cordacheckers.states.GameProposalState;
import net.corda.v5.application.flows.ClientRequestBody;
import net.corda.v5.application.flows.ClientStartableFlow;
@ -15,7 +14,6 @@ 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.base.exceptions.CordaRuntimeException;
import net.corda.v5.crypto.SecureHash;
import net.corda.v5.ledger.utxo.StateAndRef;
import net.corda.v5.ledger.utxo.UtxoLedgerService;
@ -47,12 +45,19 @@ public class ActionFlow implements ClientStartableFlow {
final ActionFlowArgs args = requestBody.getRequestBodyAs(jsonMarshallingService, ActionFlowArgs.class);
final Action action = args.getAction();
final StateAndRef<GameProposalState> inputState = findUnconsumedGameProposalState(args.getGameProposalUuid());
final StateAndRef<GameProposalState> utxoGameProposal = findUnconsumedGameProposalState(args.getGameProposalUuid());
final UtxoSignedTransaction trx = prepareSignedTransaction(action, inputState);
final UtxoSignedTransaction trx = prepareSignedTransaction(action, utxoGameProposal);
final SecureHash trxId = this.flowEngine
.subFlow( new CommitSubFlow(trx, action.getReceiver(inputState)) );
.subFlow( new CommitSubFlow(trx, action.getRespondent(utxoGameProposal)) );
if (action == Action.ACCEPT) {
GameBoardState newGb = (GameBoardState)trx.getOutputStateAndRefs().get(0).getState().getContractState();
return new FlowResult(newGb.getId(), trxId)
.toJsonEncodedString(jsonMarshallingService);
}
return new FlowResult(action+"ED", trxId) // REJECT+ED
.toJsonEncodedString(jsonMarshallingService);
@ -71,36 +76,31 @@ public class ActionFlow implements ClientStartableFlow {
* Note, this is an inefficient way to perform this operation if there are a large
* number of 'active' GameProposals exists in storage.
*/
List<StateAndRef<GameProposalState>> stateAndRefs = this.ledgerService
return this.ledgerService
.findUnconsumedStatesByType(GameProposalState.class)
.stream()
.filter(sar -> sar.getState().getContractState().getId().equals(gameProposalUuid))
.collect(Collectors.toList());
if (stateAndRefs.size() != 1) {
throw new CordaRuntimeException("Expected only one GameProposal state with id " + gameProposalUuid +
", but found " + stateAndRefs.size());
}
return stateAndRefs.get(0);
.reduce((a, b) -> {throw new IllegalStateException("Multiple states: " +a +", " +b);})
.get();
}
@Suspendable
private UtxoSignedTransaction prepareSignedTransaction(
Action action,
StateAndRef<GameProposalState> inputState
StateAndRef<GameProposalState> utxoGameProposal
) {
UtxoTransactionBuilder trxBuilder = ledgerService.createTransactionBuilder()
.setNotary(inputState.getState().getNotaryName())
.setNotary(utxoGameProposal.getState().getNotaryName())
.setTimeWindowBetween(Instant.now(), Instant.now().plusMillis(Duration.ofDays(1).toMillis()))
.addInputState(inputState.getRef())
.addInputState(utxoGameProposal.getRef())
.addCommand(action)
.addSignatories(inputState.getState().getContractState().getParticipants());
.addSignatories(utxoGameProposal.getState().getContractState().getParticipants());
if (action == Action.ACCEPT) {
// TODO
// .addOutputState(outputState)
throw new RuntimeException(action +" unimplemented");
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();

View File

@ -21,6 +21,7 @@ import static djmil.cordacheckers.contracts.GameProposalContract.Action;
@InitiatedBy(protocol = "game-proposal")
public class CommitResponderFlow implements ResponderFlow {
private final static Logger log = LoggerFactory.getLogger(CommitResponderFlow.class);
@CordaInject
@ -38,7 +39,7 @@ public class CommitResponderFlow implements ResponderFlow {
final GameProposalState gameProposal = getGameProposal(ledgerTransaction);
checkSessionParticipants(session, gameProposal, action);
checkParticipants(session, gameProposal, action);
/*
* Other checks / actions ?
@ -59,7 +60,7 @@ public class CommitResponderFlow implements ResponderFlow {
}
@Suspendable
void checkSessionParticipants(
void checkParticipants(
FlowSession session,
GameProposalState gameProposal,
Action action
@ -90,6 +91,6 @@ public class CommitResponderFlow implements ResponderFlow {
default:
throw new RuntimeException(Action.UNSUPPORTED_ACTION_VALUE_OF +action);
}
}
}

View File

@ -10,7 +10,6 @@ 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.base.annotations.Suspendable;

View File

@ -29,7 +29,7 @@ public class ListFlow implements ClientStartableFlow {
@Override
public String call(ClientRequestBody requestBody) {
try {
log.info("ListChatsFlow.call() called");
log.info("ListFlow.call() called");
// Queries the VNode's vault for unconsumed states and converts the resulting
// List<StateAndRef<GameProposalState>> to a _serializable_ List<ListItem> DTO
@ -41,7 +41,7 @@ public class ListFlow implements ClientStartableFlow {
return new FlowResult(unconsumedGameProposaList).toJsonEncodedString(jsonMarshallingService);
} catch (Exception e) {
log.warn("CreateGameProposal flow failed to process utxo request body " + requestBody + " because: " + e.getMessage());
log.warn("ListFlow failed to process utxo request body " + requestBody + " because: " + e.getMessage());
return new FlowResult(e).toJsonEncodedString(jsonMarshallingService);
}
}

View File

@ -3,16 +3,17 @@ package djmil.cordacheckers.gameproposal;
import java.util.UUID;
import djmil.cordacheckers.states.GameProposalState;
import djmil.cordacheckers.states.Piece;
// Class to hold results of the List flow.
// The GameProposal(s) cannot be returned directly as the 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 UUID. It is possible to create custom serializers for the JsonMarshallingService in the future.
// JsonMarshallingService can only serialize simple classes that the underlying Jackson serializer recognises,
// hence creating a DTO style object which consists only of Strings and a UUIDs. It is possible to create custom
// serializers for the JsonMarshallingService in the future.
public class ListItem {
public final String issuer;
public final String acquier;
public final String acquierColor;
public final Piece.Color acquierColor;
public final String message;
public final UUID id;
@ -28,7 +29,7 @@ public class ListItem {
public ListItem(GameProposalState state) {
this.issuer = state.getIssuer().getCommonName();
this.acquier = state.getAcquier().getCommonName();
this.acquierColor = state.getRecipientColor().name();
this.acquierColor = state.getAcquierColor();
this.message = state.getMessage();
this.id = state.getId();
}