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 com.fasterxml.jackson.databind.ObjectMapper;
import djmil.cordacheckers.cordaclient.dao.HoldingIdentity; 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.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;
@ -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.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.Action;
import djmil.cordacheckers.cordaclient.dao.flow.arguments.Empty; 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.GameProposalActionReq;
import djmil.cordacheckers.cordaclient.dao.flow.arguments.GameProposalActionRes; import djmil.cordacheckers.cordaclient.dao.flow.arguments.GameProposalActionRes;
@ -68,7 +72,7 @@ public class CordaClient {
public List<GameProposal> gameProposalList(HoldingIdentity holdingIdentity) { public List<GameProposal> gameProposalList(HoldingIdentity holdingIdentity) {
final RequestBody requestBody = new RequestBody( final RequestBody requestBody = new RequestBody(
"list-" + UUID.randomUUID(), "gp.list-" + UUID.randomUUID(),
"djmil.cordacheckers.gameproposal.ListFlow", "djmil.cordacheckers.gameproposal.ListFlow",
new Empty() new Empty()
); );
@ -88,67 +92,111 @@ public class CordaClient {
} }
public UUID gameProposalCreate( public UUID gameProposalCreate(
HoldingIdentity sender, HoldingIdentity issuer,
HoldingIdentity receiver, HoldingIdentity acquier,
Color receiverColor, Piece.Color acquierColor,
String message String message
) throws JsonMappingException, JsonProcessingException { ) throws JsonMappingException, JsonProcessingException {
final GameProposalCreateReq createGameProposal = new GameProposalCreateReq(
receiver.x500Name(),
receiverColor,
message
);
final RequestBody requestBody = new RequestBody( final RequestBody requestBody = new RequestBody(
"create-" + UUID.randomUUID(), "gp.create-" + UUID.randomUUID(),
"djmil.cordacheckers.gameproposal.CreateFlow", "djmil.cordacheckers.gameproposal.CreateFlow",
createGameProposal new GameProposalCreateReq(
acquier.x500Name(),
acquierColor,
message
)
); );
final GameProposalCreateRes createResult = cordaFlowExecute( final GameProposalCreateRes createResult = cordaFlowExecute(
sender, issuer,
requestBody, requestBody,
GameProposalCreateRes.class GameProposalCreateRes.class
); );
if (createResult.failureStatus() != null) { 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"); throw new RuntimeException("GameProsal: CreateFlow execution has failed");
} }
return createResult.successStatus(); return createResult.successStatus();
} }
public String gameProposalAction( public String gameProposalReject(
HoldingIdentity self, HoldingIdentity myHoldingIdentity,
UUID gameProposalUuid, UUID gameProposalUuid
GameProposalActionReq.Action action
) { ) {
final GameProposalActionReq rejectGameProposal = new GameProposalActionReq(
gameProposalUuid.toString(),
action
);
final RequestBody requestBody = new RequestBody( final RequestBody requestBody = new RequestBody(
"reject-" + UUID.randomUUID(), "gp.reject-" +UUID.randomUUID(),
"djmil.cordacheckers.gameproposal.ActionFlow", "djmil.cordacheckers.gameproposal.ActionFlow",
rejectGameProposal new GameProposalActionReq(
gameProposalUuid.toString(),
Action.REJECT
)
); );
final GameProposalActionRes actionResult = cordaFlowExecute( final GameProposalActionRes actionResult = cordaFlowExecute(
self, myHoldingIdentity,
requestBody, requestBody,
GameProposalActionRes.class GameProposalActionRes.class
); );
if (actionResult.failureStatus() != null) { 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"); throw new RuntimeException("GameProsal: ActionFlow execution has failed");
} }
return actionResult.successStatus(); 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) { private <T> T cordaFlowExecute(HoldingIdentity holdingIdentity, RequestBody requestBody, Class<T> flowResultType) {
try { 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( public record GameProposal(
String issuer, String issuer,
String acquier, String acquier,
Color acquierColor, Piece.Color acquierColor,
String message, 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; 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.CordaClient;
import djmil.cordacheckers.cordaclient.dao.HoldingIdentity; import djmil.cordacheckers.cordaclient.dao.HoldingIdentity;
import djmil.cordacheckers.cordaclient.dao.flow.arguments.GameProposalCreateReq; 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.cordaclient.dao.GameProposal;
import djmil.cordacheckers.user.HoldingIdentityResolver; import djmil.cordacheckers.user.HoldingIdentityResolver;
import djmil.cordacheckers.user.User; import djmil.cordacheckers.user.User;
@ -61,7 +61,7 @@ public class GameProposalController {
final HoldingIdentity gpSender = sender.getHoldingIdentity(); final HoldingIdentity gpSender = sender.getHoldingIdentity();
// TODO: throw execption with custom type // TODO: throw execption with custom type
final HoldingIdentity gpReceiver = holdingIdentityResolver.getByUsername(gpRequest.opponentName()); final HoldingIdentity gpReceiver = holdingIdentityResolver.getByUsername(gpRequest.opponentName());
final Color gpReceiverColor = gpRequest.opponentColor(); final Piece.Color gpReceiverColor = gpRequest.opponentColor();
// TODO handle expectionns here // TODO handle expectionns here
UUID newGameProposalUuid = cordaClient.gameProposalCreate( UUID newGameProposalUuid = cordaClient.gameProposalCreate(

View File

@ -1,19 +1,5 @@
package djmil.cordacheckers.cordaclient; 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.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.assertThatThrownBy;
@ -22,6 +8,20 @@ import java.util.UUID;
import javax.naming.InvalidNameException; 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 @SpringBootTest
public class CordaClientTest { public class CordaClientTest {
@Autowired @Autowired
@ -49,7 +49,7 @@ public class CordaClientTest {
void testGemeProposalCreate() throws JsonMappingException, JsonProcessingException { void testGemeProposalCreate() throws JsonMappingException, JsonProcessingException {
final String gpIssuer = "alice"; final String gpIssuer = "alice";
final String gpAcquier = "bob"; final String gpAcquier = "bob";
final Color gpAcquierColor = Color.WHITE; final Piece.Color gpAcquierColor = Piece.Color.WHITE;
final String gpMessage = "GameProposal create test"; final String gpMessage = "GameProposal create test";
final UUID createdGpUuid = cordaClient.gameProposalCreate( final UUID createdGpUuid = cordaClient.gameProposalCreate(
@ -78,58 +78,118 @@ public class CordaClientTest {
@Test @Test
void testGemeProposalReject() throws JsonMappingException, JsonProcessingException { void testGemeProposalReject() throws JsonMappingException, JsonProcessingException {
final String gpSender = "alice"; final String gpIssuer = "alice";
final String gpReceiver = "bob"; final String gpAcquier = "bob";
final Color gpReceiverColor = Color.WHITE; final Piece.Color gpReceiverColor = Piece.Color.WHITE;
final String gpMessage = "GameProposal REJECT test"; final String gpMessage = "GameProposal REJECT test";
final UUID gpUuid = cordaClient.gameProposalCreate( final UUID gpUuid = cordaClient.gameProposalCreate(
holdingIdentityResolver.getByUsername(gpSender), holdingIdentityResolver.getByUsername(gpIssuer),
holdingIdentityResolver.getByUsername(gpReceiver), holdingIdentityResolver.getByUsername(gpAcquier),
gpReceiverColor, gpReceiverColor,
gpMessage gpMessage
); );
System.out.println("Create GP UUID "+ gpUuid); System.out.println("Create GP UUID "+ gpUuid);
assertThatThrownBy(() -> { assertThatThrownBy(() -> { // Issuer can not reject
cordaClient.gameProposalAction( cordaClient.gameProposalReject(
holdingIdentityResolver.getByUsername(gpSender), holdingIdentityResolver.getByUsername(gpIssuer),
gpUuid, gpUuid);
Action.REJECT);
}); });
final String rejectRes = cordaClient.gameProposalAction( final String rejectRes = cordaClient.gameProposalReject(
holdingIdentityResolver.getByUsername(gpReceiver), holdingIdentityResolver.getByUsername(gpAcquier),
gpUuid, gpUuid
Action.REJECT
); );
assertThat(rejectRes).isEqualToIgnoringCase("REJECTED"); assertThat(rejectRes).isEqualToIgnoringCase("REJECTED");
List<GameProposal> gpListSender = cordaClient.gameProposalList( List<GameProposal> gpListSender = cordaClient.gameProposalList(
holdingIdentityResolver.getByUsername(gpSender)); holdingIdentityResolver.getByUsername(gpIssuer));
assertThat(findByUuid(gpListSender, gpUuid)).isNull(); assertThat(findByUuid(gpListSender, gpUuid)).isNull();
List<GameProposal> gpListReceiver = cordaClient.gameProposalList( List<GameProposal> gpListReceiver = cordaClient.gameProposalList(
holdingIdentityResolver.getByUsername(gpReceiver)); holdingIdentityResolver.getByUsername(gpAcquier));
assertThat(findByUuid(gpListReceiver, gpUuid)).isNull(); assertThat(findByUuid(gpListReceiver, gpUuid)).isNull();
// GameProposal can not be rejected twice // GameProposal can not be rejected twice
assertThatThrownBy(() -> { assertThatThrownBy(() -> {
cordaClient.gameProposalAction( cordaClient.gameProposalReject(
holdingIdentityResolver.getByUsername(gpSender), holdingIdentityResolver.getByUsername(gpIssuer),
gpUuid, gpUuid);
Action.REJECT);
}); });
} }
private GameProposal findByUuid(List<GameProposal> gpList, UUID uuid) { @Test
for (GameProposal gameProposal : gpList) { void testGemeProposalAccept() throws JsonMappingException, JsonProcessingException {
if (gameProposal.id().compareTo(uuid) == 0) final String gpIssuer = "alice";
return gameProposal; 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; 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.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
//import djmil.cordacheckers.states.GameProposalResolutionState; 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;
import net.corda.v5.base.types.MemberX500Name; 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()); return getInitiator(utxoGameProposal.getState().getContractState());
} }
public MemberX500Name getReceiver(StateAndRef<GameProposalState> utxoGameProposal) { public MemberX500Name getRespondent(StateAndRef<GameProposalState> utxoGameProposal) {
return getRespondent(utxoGameProposal.getState().getContractState()); 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); requireThat(trx.getCommands().size() == 1, REQUIRE_SINGLE_COMMAND);
switch ((Action)(trx.getCommands().get(0))) { switch ((trx.getCommands(Action.class).get(0))) {
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);
@ -72,12 +72,21 @@ public class GameProposalContract implements net.corda.v5.ledger.utxo.Contract {
GameProposalState outputState = trx.getOutputStates(GameProposalState.class).get(0); GameProposalState outputState = trx.getOutputStates(GameProposalState.class).get(0);
requireThat(outputState != null, CREATE_OUTPUT_STATE); requireThat(outputState != null, CREATE_OUTPUT_STATE);
requireThat(outputState.getRecipientColor() != null, NON_NULL_RECIPIENT_COLOR); requireThat(outputState.getAcquierColor() != null, NON_NULL_RECIPIENT_COLOR);
break; } break; }
case ACCEPT: case ACCEPT:
throw new CordaRuntimeException("Unimplemented!"); requireThat(trx.getInputContractStates().size() == 1, ACCEPT_INPUT_STATE);
//break; 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: case REJECT:
requireThat(trx.getInputContractStates().size() == 1, REJECT_INPUT_STATE); 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_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 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 issuer;
MemberX500Name acquier; MemberX500Name acquier;
Piece.Color recipientColor; Piece.Color acquierColor;
String message; String message;
UUID id; UUID id;
List<PublicKey> participants; List<PublicKey> participants;
// Allows serialisation and to use a specified UUID
@ConstructorForDeserialization @ConstructorForDeserialization
public GameProposalState( public GameProposalState(
MemberX500Name issuer, MemberX500Name issuer,
MemberX500Name acquier, MemberX500Name acquier,
Piece.Color recipientColor, Piece.Color acquierColor,
String message, String message,
UUID id, UUID id,
List<PublicKey> participants List<PublicKey> participants
) { ) {
this.issuer = issuer; this.issuer = issuer;
this.acquier = acquier; this.acquier = acquier;
this.recipientColor = recipientColor; this.acquierColor = acquierColor;
this.message = message; this.message = message;
this.id = id; this.id = id;
this.participants = participants; this.participants = participants;
@ -46,8 +45,8 @@ public class GameProposalState implements ContractState {
return acquier; return acquier;
} }
public Piece.Color getRecipientColor() { public Piece.Color getAcquierColor() {
return recipientColor; return acquierColor;
} }
public String getMessage() { public String getMessage() {
@ -61,4 +60,29 @@ public class GameProposalState implements ContractState {
public List<PublicKey> getParticipants() { public List<PublicKey> getParticipants() {
return participants; 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; package djmil.cordacheckers.states;
import net.corda.v5.base.annotations.ConstructorForDeserialization;
import net.corda.v5.base.annotations.CordaSerializable; import net.corda.v5.base.annotations.CordaSerializable;
@CordaSerializable @CordaSerializable
@ -20,5 +21,18 @@ public class Piece {
Color color; Color color;
Type type; 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; package djmil.cordacheckers.gameproposal;
import java.util.List;
import java.util.UUID; import java.util.UUID;
import java.util.stream.Collectors;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import djmil.cordacheckers.FlowResult; import djmil.cordacheckers.FlowResult;
import djmil.cordacheckers.states.GameBoardState;
import djmil.cordacheckers.states.GameProposalState; import djmil.cordacheckers.states.GameProposalState;
import net.corda.v5.application.flows.ClientRequestBody; import net.corda.v5.application.flows.ClientRequestBody;
import net.corda.v5.application.flows.ClientStartableFlow; 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.flows.FlowEngine;
import net.corda.v5.application.marshalling.JsonMarshallingService; import net.corda.v5.application.marshalling.JsonMarshallingService;
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.crypto.SecureHash; import net.corda.v5.crypto.SecureHash;
import net.corda.v5.ledger.utxo.StateAndRef; import net.corda.v5.ledger.utxo.StateAndRef;
import net.corda.v5.ledger.utxo.UtxoLedgerService; 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 ActionFlowArgs args = requestBody.getRequestBodyAs(jsonMarshallingService, ActionFlowArgs.class);
final Action action = args.getAction(); 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 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 return new FlowResult(action+"ED", trxId) // REJECT+ED
.toJsonEncodedString(jsonMarshallingService); .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 * Note, this is an inefficient way to perform this operation if there are a large
* number of 'active' GameProposals exists in storage. * number of 'active' GameProposals exists in storage.
*/ */
List<StateAndRef<GameProposalState>> stateAndRefs = this.ledgerService return this.ledgerService
.findUnconsumedStatesByType(GameProposalState.class) .findUnconsumedStatesByType(GameProposalState.class)
.stream() .stream()
.filter(sar -> sar.getState().getContractState().getId().equals(gameProposalUuid)) .filter(sar -> sar.getState().getContractState().getId().equals(gameProposalUuid))
.collect(Collectors.toList()); .reduce((a, b) -> {throw new IllegalStateException("Multiple states: " +a +", " +b);})
.get();
if (stateAndRefs.size() != 1) {
throw new CordaRuntimeException("Expected only one GameProposal state with id " + gameProposalUuid +
", but found " + stateAndRefs.size());
}
return stateAndRefs.get(0);
} }
@Suspendable @Suspendable
private UtxoSignedTransaction prepareSignedTransaction( private UtxoSignedTransaction prepareSignedTransaction(
Action action, Action action,
StateAndRef<GameProposalState> inputState StateAndRef<GameProposalState> utxoGameProposal
) { ) {
UtxoTransactionBuilder trxBuilder = ledgerService.createTransactionBuilder() UtxoTransactionBuilder trxBuilder = ledgerService.createTransactionBuilder()
.setNotary(inputState.getState().getNotaryName()) .setNotary(utxoGameProposal.getState().getNotaryName())
.setTimeWindowBetween(Instant.now(), Instant.now().plusMillis(Duration.ofDays(1).toMillis())) .setTimeWindowBetween(Instant.now(), Instant.now().plusMillis(Duration.ofDays(1).toMillis()))
.addInputState(inputState.getRef()) .addInputState(utxoGameProposal.getRef())
.addCommand(action) .addCommand(action)
.addSignatories(inputState.getState().getContractState().getParticipants()); .addSignatories(utxoGameProposal.getState().getContractState().getParticipants());
if (action == Action.ACCEPT) { if (action == Action.ACCEPT) {
// TODO trxBuilder = trxBuilder
// .addOutputState(outputState) .addOutputState(new GameBoardState(utxoGameProposal));
throw new RuntimeException(action +" unimplemented"); //A state cannot be both an input and a reference input in the same transaction
//.addReferenceState(utxoGameProposal.getRef());
} }
return trxBuilder.toSignedTransaction(); return trxBuilder.toSignedTransaction();

View File

@ -21,6 +21,7 @@ import static djmil.cordacheckers.contracts.GameProposalContract.Action;
@InitiatedBy(protocol = "game-proposal") @InitiatedBy(protocol = "game-proposal")
public class CommitResponderFlow implements ResponderFlow { public class CommitResponderFlow implements ResponderFlow {
private final static Logger log = LoggerFactory.getLogger(CommitResponderFlow.class); private final static Logger log = LoggerFactory.getLogger(CommitResponderFlow.class);
@CordaInject @CordaInject
@ -38,7 +39,7 @@ public class CommitResponderFlow implements ResponderFlow {
final GameProposalState gameProposal = getGameProposal(ledgerTransaction); final GameProposalState gameProposal = getGameProposal(ledgerTransaction);
checkSessionParticipants(session, gameProposal, action); checkParticipants(session, gameProposal, action);
/* /*
* Other checks / actions ? * Other checks / actions ?
@ -59,7 +60,7 @@ public class CommitResponderFlow implements ResponderFlow {
} }
@Suspendable @Suspendable
void checkSessionParticipants( void checkParticipants(
FlowSession session, FlowSession session,
GameProposalState gameProposal, GameProposalState gameProposal,
Action action Action action
@ -90,6 +91,6 @@ public class CommitResponderFlow implements ResponderFlow {
default: default:
throw new RuntimeException(Action.UNSUPPORTED_ACTION_VALUE_OF +action); 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.ClientStartableFlow;
import net.corda.v5.application.flows.CordaInject; import net.corda.v5.application.flows.CordaInject;
import net.corda.v5.application.flows.FlowEngine; 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.marshalling.JsonMarshallingService;
import net.corda.v5.application.membership.MemberLookup; import net.corda.v5.application.membership.MemberLookup;
import net.corda.v5.base.annotations.Suspendable; import net.corda.v5.base.annotations.Suspendable;

View File

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

View File

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