From 8971462c74e785a98780f9eb4d5ed8a25303cd0c Mon Sep 17 00:00:00 2001 From: djmil Date: Fri, 22 Sep 2023 21:39:42 +0200 Subject: [PATCH] GameResultBuilder subflow - looks for custodian and add it to the State participants - extra parameter to Commit subflow to initiate exchange session with Custodian as well --- .../cordaclient/RankingTests.java | 29 ++++++-- .../cordacheckers/states/GameResultState.java | 4 +- .../djmil/cordacheckers/states/GameState.java | 13 ++++ .../gameboard/SurrenderFlow.java | 37 ++++------ .../gameresult/GameResultBuilder.java | 74 +++++++++++++++++++ .../gamestate/CommitSubFlow.java | 22 +++++- .../gamestate/CommitSubFlowResponder.java | 14 ++-- 7 files changed, 154 insertions(+), 39 deletions(-) create mode 100644 corda/workflows/src/main/java/djmil/cordacheckers/gameresult/GameResultBuilder.java diff --git a/backend/src/test/java/djmil/cordacheckers/cordaclient/RankingTests.java b/backend/src/test/java/djmil/cordacheckers/cordaclient/RankingTests.java index ae62e18..43acced 100644 --- a/backend/src/test/java/djmil/cordacheckers/cordaclient/RankingTests.java +++ b/backend/src/test/java/djmil/cordacheckers/cordaclient/RankingTests.java @@ -8,6 +8,9 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import djmil.cordacheckers.cordaclient.dao.GameState; +import djmil.cordacheckers.cordaclient.dao.GameState.Status; +import djmil.cordacheckers.cordaclient.dao.Piece; import djmil.cordacheckers.cordaclient.dao.Rank; import djmil.cordacheckers.user.HoldingIdentityResolver; @@ -22,12 +25,28 @@ public class RankingTests { @Test void testGlobalRanking() { final var hiCustodian = holdingIdentityResolver.getCustodian(); - + final List liderboard1 = cordaClient.fetchRanking(hiCustodian); + + final var hiWinner = holdingIdentityResolver.getByUsername("Charlie"); + final var hiLooser = holdingIdentityResolver.getByUsername("Bob"); + + final GameState game = cordaClient.gameProposalCreate( + hiWinner, hiLooser, Piece.Color.WHITE, "GameBoard GLOBAL_RANKING test"); + + cordaClient.gameProposalAccept(hiLooser, game.uuid()); + cordaClient.gameBoardSurrender(hiLooser, game.uuid()); + + final List liderboard2 = cordaClient.fetchRanking(hiCustodian); + + System.out.println(liderboard1); + System.out.println(liderboard2); + } + + @Test + void testIndividualRanking() { + final var hiCustodian = holdingIdentityResolver.getByUsername("Bob"); final List liderboard = cordaClient.fetchRanking(hiCustodian); - - System.out.println("Liderboard " +liderboard); - - + System.out.println(liderboard); } } diff --git a/corda/contracts/src/main/java/djmil/cordacheckers/states/GameResultState.java b/corda/contracts/src/main/java/djmil/cordacheckers/states/GameResultState.java index 1a54c83..d8c02ce 100644 --- a/corda/contracts/src/main/java/djmil/cordacheckers/states/GameResultState.java +++ b/corda/contracts/src/main/java/djmil/cordacheckers/states/GameResultState.java @@ -21,8 +21,8 @@ public class GameResultState extends GameState { this.winnerName = winnerName; } - public GameResultState(GameBoardState gameBoardState, MemberX500Name winnerName) { - super(gameBoardState.whitePlayer, gameBoardState.blackPlayer, gameBoardState.gameUuid, null, gameBoardState.participants); + public GameResultState(GameBoardState gameBoardState, MemberX500Name winnerName, PublicKey custodianPubicKey) { + super(gameBoardState.whitePlayer, gameBoardState.blackPlayer, gameBoardState.gameUuid, null, gameBoardState.participants, custodianPubicKey); this.winnerName = winnerName; } diff --git a/corda/contracts/src/main/java/djmil/cordacheckers/states/GameState.java b/corda/contracts/src/main/java/djmil/cordacheckers/states/GameState.java index 662cec0..987e534 100644 --- a/corda/contracts/src/main/java/djmil/cordacheckers/states/GameState.java +++ b/corda/contracts/src/main/java/djmil/cordacheckers/states/GameState.java @@ -1,6 +1,7 @@ package djmil.cordacheckers.states; import java.security.PublicKey; +import java.util.LinkedList; import java.util.List; import java.util.UUID; @@ -25,6 +26,18 @@ public abstract class GameState implements ContractState { this.participants = participants; } + GameState(MemberX500Name whitePlayer, MemberX500Name blackPlayer, UUID gameUuid, + String message, List participants, PublicKey additionalParticipand) { + this.whitePlayer = whitePlayer; + this.blackPlayer = blackPlayer; + this.gameUuid = gameUuid; + this.message = message; + + var deepCopy = new LinkedList<>(participants); + deepCopy.add(additionalParticipand); + this.participants = deepCopy; + } + public MemberX500Name getWhitePlayer() { return whitePlayer; } diff --git a/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/SurrenderFlow.java b/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/SurrenderFlow.java index 0eba9cc..ac1bd04 100644 --- a/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/SurrenderFlow.java +++ b/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/SurrenderFlow.java @@ -8,13 +8,13 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import djmil.cordacheckers.contracts.GameCommand; +import djmil.cordacheckers.gameresult.GameResultBuilder; +import djmil.cordacheckers.gameresult.GameResultBuilder.Reason; import djmil.cordacheckers.gamestate.CommitSubFlow; import djmil.cordacheckers.gamestate.FlowResponce; import djmil.cordacheckers.gamestate.GetFlow; import djmil.cordacheckers.gamestate.View; import djmil.cordacheckers.gamestate.ViewBuilder; -import djmil.cordacheckers.states.GameBoardState; -import djmil.cordacheckers.states.GameResultState; import djmil.cordacheckers.states.GameState; import net.corda.v5.application.flows.ClientRequestBody; import net.corda.v5.application.flows.ClientStartableFlow; @@ -23,7 +23,6 @@ import net.corda.v5.application.flows.FlowEngine; import net.corda.v5.application.marshalling.JsonMarshallingService; import net.corda.v5.application.membership.MemberLookup; import net.corda.v5.base.annotations.Suspendable; -import net.corda.v5.base.types.MemberX500Name; import net.corda.v5.crypto.SecureHash; import net.corda.v5.ledger.common.NotaryLookup; import net.corda.v5.ledger.utxo.StateAndRef; @@ -57,24 +56,27 @@ public class SurrenderFlow implements ClientStartableFlow{ try { final GameCommand command = new GameCommand(GameCommand.Action.GAME_BOARD_SURRENDER); - final UUID gameStateUuid = UUID.fromString(requestBody.getRequestBody()); + final UUID gameUuid = UUID.fromString(requestBody.getRequestBody()); - final StateAndRef inputStateSar = this.flowEngine - .subFlow(new GetFlow(gameStateUuid)); + final StateAndRef gameBoardSar = this.flowEngine + .subFlow(new GetFlow(gameUuid)); - final GameResultState outputState = prepareGameResultState(inputStateSar); + final GameResultBuilder.Result out = this.flowEngine + .subFlow(new GameResultBuilder(gameBoardSar, Reason.SURRENDER)); final UtxoSignedTransaction gameBoardSurrenderTrx = utxoLedgerService.createTransactionBuilder() .addCommand(command) - .addInputState(inputStateSar.getRef()) - .addOutputState(outputState) - .addSignatories(outputState.getParticipants()) - .setNotary(inputStateSar.getState().getNotaryName()) + .addInputState(gameBoardSar.getRef()) + .addOutputState(out.gameResult) + .addSignatories(out.gameResult.getParticipants()) + .setNotary(gameBoardSar.getState().getNotaryName()) .setTimeWindowUntil(Instant.now().plusMillis(Duration.ofDays(1).toMillis())) .toSignedTransaction(); utxoTrxId = this.flowEngine - .subFlow(new CommitSubFlow(gameBoardSurrenderTrx, command.getCounterparty(inputStateSar))); + .subFlow(new CommitSubFlow(gameBoardSurrenderTrx, + command.getCounterparty(gameBoardSar), + out.custodyName)); final View gameStateView = this.flowEngine .subFlow(new ViewBuilder(utxoTrxId)); @@ -90,15 +92,4 @@ public class SurrenderFlow implements ClientStartableFlow{ } } - @Suspendable - GameResultState prepareGameResultState(StateAndRef gameStateSar) { - final GameState gameState = gameStateSar.getState().getContractState(); - final GameBoardState gameBoard = (GameBoardState) gameState; - - final MemberX500Name myName = memberLookup.myInfo().getName(); - final MemberX500Name winnerName = gameBoard.getOpponentName(myName); - - return new GameResultState(gameBoard, winnerName); - } - } diff --git a/corda/workflows/src/main/java/djmil/cordacheckers/gameresult/GameResultBuilder.java b/corda/workflows/src/main/java/djmil/cordacheckers/gameresult/GameResultBuilder.java new file mode 100644 index 0000000..94e726c --- /dev/null +++ b/corda/workflows/src/main/java/djmil/cordacheckers/gameresult/GameResultBuilder.java @@ -0,0 +1,74 @@ +package djmil.cordacheckers.gameresult; + +import djmil.cordacheckers.states.GameBoardState; +import djmil.cordacheckers.states.GameResultState; +import djmil.cordacheckers.states.GameState; +import net.corda.v5.application.flows.CordaInject; +import net.corda.v5.application.flows.SubFlow; +import net.corda.v5.application.membership.MemberLookup; +import net.corda.v5.base.annotations.Suspendable; +import net.corda.v5.base.types.MemberX500Name; +import net.corda.v5.ledger.utxo.StateAndRef; +import net.corda.v5.membership.MemberInfo; + +public class GameResultBuilder implements SubFlow { + public class Result { + final public GameResultState gameResult; + final public MemberX500Name custodyName; + + public Result(GameResultState gameResult, MemberX500Name custodyName){ + this.gameResult = gameResult; + this.custodyName = custodyName; + } + } + + public static enum Reason { + VICTORY, + SURRENDER + } + + private final GameBoardState gameBoard; + private final Reason reason; + + public GameResultBuilder(StateAndRef gameBoardSar, Reason reason) { + this.gameBoard = (GameBoardState)gameBoardSar.getState().getContractState(); + this.reason = reason; + } + + @CordaInject + public MemberLookup memberLookup; + + @Override + @Suspendable + public Result call() { + final MemberInfo custodiaInfo = findCustodian(); + + return new Result( + new GameResultState(gameBoard, winnerName(), custodiaInfo.getLedgerKeys().get(0)), + custodiaInfo.getName()); + } + + @Suspendable + MemberInfo findCustodian() { + return memberLookup.lookup() + .stream() + .filter(m -> m.getName().getOrganizationUnit().equals("Custodian") ) + .reduce((a,b) -> {throw new IllegalStateException("Multiple Custodian VNodes");}) + .orElseThrow( () -> new IllegalStateException("No Custodian VNode found")); + } + + @Suspendable + MemberX500Name winnerName() { + final MemberX500Name myName = memberLookup.myInfo().getName(); + + switch(reason) { + case VICTORY: + return myName; + case SURRENDER: + return gameBoard.getOpponentName(myName); + } + + throw new IllegalStateException("Bad reason"); + } + +} diff --git a/corda/workflows/src/main/java/djmil/cordacheckers/gamestate/CommitSubFlow.java b/corda/workflows/src/main/java/djmil/cordacheckers/gamestate/CommitSubFlow.java index d513dc3..72ffc11 100644 --- a/corda/workflows/src/main/java/djmil/cordacheckers/gamestate/CommitSubFlow.java +++ b/corda/workflows/src/main/java/djmil/cordacheckers/gamestate/CommitSubFlow.java @@ -1,6 +1,7 @@ package djmil.cordacheckers.gamestate; import java.util.Arrays; +import java.util.LinkedList; import java.util.List; import org.slf4j.Logger; @@ -9,6 +10,7 @@ import org.slf4j.LoggerFactory; import net.corda.v5.application.flows.CordaInject; import net.corda.v5.application.flows.InitiatingFlow; import net.corda.v5.application.flows.SubFlow; +import net.corda.v5.application.membership.MemberLookup; import net.corda.v5.application.messaging.FlowMessaging; import net.corda.v5.application.messaging.FlowSession; import net.corda.v5.base.annotations.Suspendable; @@ -23,10 +25,18 @@ public class CommitSubFlow implements SubFlow { private final static Logger log = LoggerFactory.getLogger(CommitSubFlow.class); private final UtxoSignedTransaction utxTrxCandidate; private final MemberX500Name counterpartyName; + private final MemberX500Name custodyName; public CommitSubFlow(UtxoSignedTransaction signedTransaction, MemberX500Name counterpartyName) { this.utxTrxCandidate = signedTransaction; this.counterpartyName = counterpartyName; + this.custodyName = null; + } + + public CommitSubFlow(UtxoSignedTransaction signedTransaction, MemberX500Name counterpartyName, MemberX500Name custodyName) { + this.utxTrxCandidate = signedTransaction; + this.counterpartyName = counterpartyName; + this.custodyName = custodyName; } @CordaInject @@ -35,19 +45,25 @@ public class CommitSubFlow implements SubFlow { @CordaInject public FlowMessaging flowMessaging; + @CordaInject + public MemberLookup memberLookup; + @Override @Suspendable public SecureHash call() { log.info("GameState commit started"); - final FlowSession session = flowMessaging.initiateFlow(this.counterpartyName); - /* * Calls the Corda provided finalise() function which gather signatures from the counterparty, * notarises the transaction and persists the transaction to each party's vault. */ - final List sessionsList = Arrays.asList(session); + final FlowSession session = flowMessaging.initiateFlow(this.counterpartyName); + List sessionsList = new LinkedList(Arrays.asList(session)); + + if (custodyName != null) { + sessionsList.add(flowMessaging.initiateFlow(custodyName)); + } final SecureHash trxId = ledgerService .finalize(this.utxTrxCandidate, sessionsList) diff --git a/corda/workflows/src/main/java/djmil/cordacheckers/gamestate/CommitSubFlowResponder.java b/corda/workflows/src/main/java/djmil/cordacheckers/gamestate/CommitSubFlowResponder.java index 0e81a57..392c02f 100644 --- a/corda/workflows/src/main/java/djmil/cordacheckers/gamestate/CommitSubFlowResponder.java +++ b/corda/workflows/src/main/java/djmil/cordacheckers/gamestate/CommitSubFlowResponder.java @@ -100,16 +100,18 @@ public class CommitSubFlowResponder implements ResponderFlow { @Suspendable void checkParticipants(FlowSession session, GameCommand gameCommand, GameState gameState) throws ParticipantException { - final var myName = memberLookup.myInfo().getName(); - final var opponentName = gameState.getOpponentName(myName); // throws NotInvolved final var conterpartyName = session.getCounterparty(); final var actorName = gameCommand.getInitiator(gameState); - - if (conterpartyName.compareTo(opponentName) != 0) - throw new ParticipantException("Counterparty", conterpartyName, opponentName); - if (actorName.compareTo(conterpartyName) != 0) throw new ParticipantException("Actor", conterpartyName, actorName); + + final var myName = memberLookup.myInfo().getName(); + if (myName.getOrganizationUnit().equals("Custodian")) + return; // Custodian shall not validate state's counterparty + + final var opponentName = gameState.getOpponentName(myName); // throws NotInvolved + if (conterpartyName.compareTo(opponentName) != 0) + throw new ParticipantException("Counterparty", conterpartyName, opponentName); } public static class ParticipantException extends Exception {