Compare commits
	
		
			No commits in common. "e235ecb942a7e53cc1f32648974c0c76a2a48287" and "7df57cb4d291ccc5ad2842ef20363d8096dc0bf0" have entirely different histories.
		
	
	
		
			e235ecb942
			...
			7df57cb4d2
		
	
		
| @ -14,23 +14,17 @@ import org.springframework.http.ResponseEntity; | ||||
| import org.springframework.stereotype.Service; | ||||
| import org.springframework.web.client.RestTemplate; | ||||
| 
 | ||||
| import com.fasterxml.jackson.core.JsonProcessingException; | ||||
| 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.GameProposal; | ||||
| import djmil.cordacheckers.cordaclient.dao.VirtualNode; | ||||
| import djmil.cordacheckers.cordaclient.dao.VirtualNodeList; | ||||
| import djmil.cordacheckers.cordaclient.dao.flow.RequestBody; | ||||
| 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.CreateGameProposal; | ||||
| import djmil.cordacheckers.cordaclient.dao.flow.arguments.Empty; | ||||
| import djmil.cordacheckers.cordaclient.dao.flow.arguments.GameProposalActionReq; | ||||
| import djmil.cordacheckers.cordaclient.dao.flow.arguments.GameProposalActionRes; | ||||
| import djmil.cordacheckers.cordaclient.dao.flow.arguments.GameProposalAction; | ||||
| 
 | ||||
| @Service | ||||
| public class CordaClient { | ||||
| @ -65,7 +59,7 @@ public class CordaClient { | ||||
|      * @param holdingIdentity | ||||
|      * @return GameProposals list in JSON form | ||||
|      */ | ||||
|     public List<GameProposal> gameProposalList(HoldingIdentity holdingIdentity) { | ||||
|     public String listGameProposals(HoldingIdentity holdingIdentity) { | ||||
| 
 | ||||
|         final RequestBody requestBody = new RequestBody( | ||||
|             "list-" + UUID.randomUUID(), | ||||
| @ -73,27 +67,21 @@ public class CordaClient { | ||||
|             new Empty() | ||||
|             ); | ||||
| 
 | ||||
|         final GameProposalListRes listFlowResult = cordaFlowExecute( | ||||
|         final String gameProposalsJsonString = cordaFlowExecute( | ||||
|             holdingIdentity, | ||||
|             requestBody, | ||||
|             GameProposalListRes.class | ||||
|             requestBody | ||||
|             ); | ||||
| 
 | ||||
|         if (listFlowResult.failureStatus() != null) { | ||||
|             System.out.println("GameProposalCreateFlow failed: " + listFlowResult.failureStatus()); | ||||
|             throw new RuntimeException("GameProsal: CreateFlow execution has failed"); | ||||
|         } | ||||
| 
 | ||||
|         return listFlowResult.successStatus(); | ||||
|         return gameProposalsJsonString; | ||||
|     } | ||||
| 
 | ||||
|     public UUID gameProposalCreate( | ||||
|     public String createGameProposal( | ||||
|         HoldingIdentity sender, | ||||
|         HoldingIdentity receiver, | ||||
|         Color receiverColor, | ||||
|         String message | ||||
|     ) throws JsonMappingException, JsonProcessingException { | ||||
|         final GameProposalCreateReq createGameProposal = new GameProposalCreateReq( | ||||
|     ) { | ||||
|         final CreateGameProposal createGameProposal = new CreateGameProposal( | ||||
|             receiver.x500Name(), | ||||
|             receiverColor, | ||||
|             message | ||||
| @ -105,28 +93,21 @@ public class CordaClient { | ||||
|             createGameProposal | ||||
|             ); | ||||
| 
 | ||||
|         final GameProposalCreateRes createResult = cordaFlowExecute( | ||||
|         final String createdGameProposalUuid = cordaFlowExecute( | ||||
|             sender, | ||||
|             requestBody, | ||||
|             GameProposalCreateRes.class | ||||
|             requestBody | ||||
|             ); | ||||
| 
 | ||||
|         if (createResult.failureStatus() != null) { | ||||
|             System.out.println("GameProposalCreateFlow failed: " + createResult.failureStatus()); | ||||
|             throw new RuntimeException("GameProsal: CreateFlow execution has failed"); | ||||
|         } | ||||
| 
 | ||||
|         return createResult.successStatus(); | ||||
|         return createdGameProposalUuid; | ||||
|     } | ||||
| 
 | ||||
|     public String gameProposalAction( | ||||
|     public String rejectGameProposal( | ||||
|         HoldingIdentity self, | ||||
|         UUID gameProposalUuid, | ||||
|         GameProposalActionReq.Action action | ||||
|         String gameProposalUuid | ||||
|     ) { | ||||
|         final GameProposalActionReq rejectGameProposal = new GameProposalActionReq( | ||||
|             gameProposalUuid.toString(), | ||||
|             action | ||||
|         final GameProposalAction rejectGameProposal = new GameProposalAction( | ||||
|             gameProposalUuid, | ||||
|             GameProposalAction.Action.REJECT | ||||
|             ); | ||||
| 
 | ||||
|         final RequestBody requestBody = new RequestBody( | ||||
| @ -135,21 +116,15 @@ public class CordaClient { | ||||
|             rejectGameProposal | ||||
|             ); | ||||
| 
 | ||||
|         final GameProposalActionRes actionResult = cordaFlowExecute( | ||||
|         final String createdGameProposalUuid = cordaFlowExecute( | ||||
|             self, | ||||
|             requestBody, | ||||
|             GameProposalActionRes.class | ||||
|             requestBody | ||||
|             ); | ||||
| 
 | ||||
|         if (actionResult.failureStatus() != null) { | ||||
|             System.out.println("GameProposalActionFlow failed: " + actionResult.failureStatus()); | ||||
|             throw new RuntimeException("GameProsal: ActionFlow execution has failed"); | ||||
|         } | ||||
| 
 | ||||
|         return actionResult.successStatus(); | ||||
|         return createdGameProposalUuid; | ||||
|     } | ||||
| 
 | ||||
|     private <T> T cordaFlowExecute(HoldingIdentity holdingIdentity, RequestBody requestBody, Class<T> flowResultType) { | ||||
|     private String cordaFlowExecute(HoldingIdentity holdingIdentity, RequestBody requestBody) { | ||||
| 
 | ||||
|         try { | ||||
|             final String requestBodyJson = this.jsonMapper.writeValueAsString(requestBody); | ||||
| @ -159,9 +134,15 @@ public class CordaClient { | ||||
|                 requestBodyJson | ||||
|             ); | ||||
| 
 | ||||
|             final String flowResult = cordaFlowPoll(startedFlow); | ||||
|             final String flowExecutionResult = cordaFlowPoll(startedFlow); | ||||
| 
 | ||||
|             return this.jsonMapper.readValue(flowResult, flowResultType); | ||||
|             // NOTE: | ||||
|             // At this point, real production code, probably should convert data between CordaFlow | ||||
|             // abstarction into ReactApp abstraction. Instead, to limit boring json shuffling, all | ||||
|             // family of Corda.List flows were deliberatly designed to return frontend frendly JSONs. | ||||
|             // At the same time, all other Corda flows, simply return plain text string with | ||||
|             // operation result. | ||||
|             return flowExecutionResult; | ||||
|         }  | ||||
|         catch (Exception e) { | ||||
|             throw new RuntimeException("Unable to perform "+requestBody.flowClassName() | ||||
| @ -202,7 +183,7 @@ public class CordaClient { | ||||
| 
 | ||||
|         for (int retry = 0; retry < 6; retry++) { | ||||
|             // Give Corda cluster some time to process our request | ||||
|             TimeUnit.SECONDS.sleep(retry*retry +2); // 2 3 6 10 18 27 sec | ||||
|             TimeUnit.SECONDS.sleep(retry*retry +1); // 1 2 5 8 17 33 sec | ||||
| 
 | ||||
|             final ResponseEntity<ResponseBody> responce = this.restTemplate.exchange( | ||||
|                 "/flow/"  | ||||
| @ -223,16 +204,15 @@ public class CordaClient { | ||||
|                 "CordaClient.cordaFlowPoll: empty getBody()" | ||||
|             ); | ||||
| 
 | ||||
|             if (responseBody.flowError() != null) { | ||||
|                 throw new RuntimeException("CordaClient.cordaFlowPoll: flow execution error: "  | ||||
|                                             +responseBody.flowError()); | ||||
|             } | ||||
| 
 | ||||
|             if (responseBody.isFlowCompleted() && responseBody.flowResult() != null) { | ||||
|                 System.out.println("Completed "+responseBody.flowResult()); | ||||
|                 return responseBody.flowResult(); | ||||
|             } else | ||||
|             if (responseBody.flowError() != null) { | ||||
|                 return "Flow execution error: " +responseBody.flowError(); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         throw new RuntimeException ("CordaClient.cordaFlowPoll: retry limit"); | ||||
|         return "CordaClient.cordaFlowPoll: retry limit"; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -1,15 +1,13 @@ | ||||
| package djmil.cordacheckers.cordaclient.dao; | ||||
| 
 | ||||
| import java.util.UUID; | ||||
| 
 | ||||
| import com.fasterxml.jackson.databind.annotation.JsonDeserialize; | ||||
| 
 | ||||
| @JsonDeserialize | ||||
| public record GameProposal( | ||||
|     String issuer, | ||||
|     String acquier, | ||||
|     Color acquierColor, | ||||
|     String sender, | ||||
|     String recipient, | ||||
|     Color recipientColor, | ||||
|     String message, | ||||
|     UUID id) { | ||||
|     String id) { | ||||
|      | ||||
| } | ||||
|  | ||||
| @ -2,6 +2,6 @@ package djmil.cordacheckers.cordaclient.dao.flow.arguments; | ||||
| 
 | ||||
| import djmil.cordacheckers.cordaclient.dao.Color; | ||||
| 
 | ||||
| public record GameProposalCreateReq(String opponentName, Color opponentColor, String message) { | ||||
| public record CreateGameProposal(String opponentName, Color opponentColor, String message) { | ||||
|      | ||||
| } | ||||
| @ -1,6 +1,6 @@ | ||||
| package djmil.cordacheckers.cordaclient.dao.flow.arguments; | ||||
| 
 | ||||
| public record GameProposalActionReq(String gameProposalUuid, Action action) { | ||||
| public record GameProposalAction(String gameProposalUuid, Action action) { | ||||
|     public enum Action { | ||||
|         ACCEPT, | ||||
|         REJECT, | ||||
| @ -1,5 +0,0 @@ | ||||
| package djmil.cordacheckers.cordaclient.dao.flow.arguments; | ||||
| 
 | ||||
| public record GameProposalActionRes(String successStatus, String transactionId, String failureStatus) { | ||||
|      | ||||
| } | ||||
| @ -1,10 +0,0 @@ | ||||
| package djmil.cordacheckers.cordaclient.dao.flow.arguments; | ||||
| 
 | ||||
| import java.util.UUID; | ||||
| 
 | ||||
| import com.fasterxml.jackson.annotation.JsonIgnoreProperties; | ||||
| 
 | ||||
| @JsonIgnoreProperties(ignoreUnknown = true) | ||||
| public record GameProposalCreateRes(UUID successStatus, String failureStatus) { | ||||
|      | ||||
| } | ||||
| @ -1,12 +0,0 @@ | ||||
| package djmil.cordacheckers.cordaclient.dao.flow.arguments; | ||||
| 
 | ||||
| import java.util.List; | ||||
| 
 | ||||
| import com.fasterxml.jackson.annotation.JsonIgnoreProperties; | ||||
| 
 | ||||
| import djmil.cordacheckers.cordaclient.dao.GameProposal; | ||||
| 
 | ||||
| @JsonIgnoreProperties(ignoreUnknown = true) | ||||
| public record GameProposalListRes(List<GameProposal> successStatus, String failureStatus) { | ||||
|      | ||||
| } | ||||
| @ -1,8 +1,6 @@ | ||||
| package djmil.cordacheckers.gameproposal; | ||||
| 
 | ||||
| import java.net.URI; | ||||
| import java.util.List; | ||||
| import java.util.UUID; | ||||
| 
 | ||||
| import org.springframework.beans.factory.annotation.Autowired; | ||||
| import org.springframework.http.ResponseEntity; | ||||
| @ -11,12 +9,9 @@ import org.springframework.web.bind.annotation.GetMapping; | ||||
| import org.springframework.web.bind.annotation.RestController; | ||||
| import org.springframework.web.util.UriComponentsBuilder; | ||||
| 
 | ||||
| import com.fasterxml.jackson.core.JsonProcessingException; | ||||
| 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.flow.arguments.CreateGameProposal; | ||||
| import djmil.cordacheckers.cordaclient.dao.Color; | ||||
| import djmil.cordacheckers.cordaclient.dao.GameProposal; | ||||
| import djmil.cordacheckers.user.HoldingIdentityResolver; | ||||
| @ -41,9 +36,9 @@ public class GameProposalController { | ||||
|     public ResponseEntity<String> findAllUnconsumed( | ||||
|         @AuthenticationPrincipal User player | ||||
|     ) { | ||||
|         List<GameProposal> gpList = cordaClient.gameProposalList(player.getHoldingIdentity()); | ||||
|         String gpList = cordaClient.listGameProposals(player.getHoldingIdentity()); | ||||
| 
 | ||||
|         return ResponseEntity.ok(gpList.toString()); | ||||
|         return ResponseEntity.ok(gpList); | ||||
|     } | ||||
| 
 | ||||
|     // @PostMapping() | ||||
| @ -55,16 +50,15 @@ public class GameProposalController { | ||||
|     @PostMapping() | ||||
|     public ResponseEntity<Void> createGameProposal( | ||||
|         @AuthenticationPrincipal User sender, | ||||
|         @RequestBody GameProposalCreateReq gpRequest, | ||||
|         @RequestBody CreateGameProposal gpRequest, | ||||
|         UriComponentsBuilder ucb | ||||
|     ) throws JsonMappingException, JsonProcessingException { | ||||
|     ) { | ||||
|         final HoldingIdentity gpSender = sender.getHoldingIdentity(); | ||||
|         // TODO: throw execption with custom type | ||||
|         final HoldingIdentity gpReceiver = holdingIdentityResolver.getByUsername(gpRequest.opponentName()); | ||||
|         final Color gpReceiverColor = gpRequest.opponentColor(); | ||||
| 
 | ||||
|         // TODO handle expectionns here | ||||
|         UUID newGameProposalUuid = cordaClient.gameProposalCreate( | ||||
|         String newGameProposalUuid = cordaClient.createGameProposal( | ||||
|             gpSender, | ||||
|             gpReceiver, | ||||
|             gpReceiverColor, | ||||
|  | ||||
| @ -6,19 +6,14 @@ 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; | ||||
| 
 | ||||
| import java.util.List; | ||||
| import java.util.UUID; | ||||
| 
 | ||||
| import javax.naming.InvalidNameException; | ||||
| 
 | ||||
| @ -39,51 +34,44 @@ public class CordaClientTest { | ||||
| 
 | ||||
|     @Test | ||||
|     void testGameProposalList() throws JsonProcessingException { | ||||
|         List<GameProposal> gpList = cordaClient.gameProposalList( | ||||
|             holdingIdentityResolver.getByUsername("Alice")); | ||||
|         String resp = cordaClient.listGameProposals( | ||||
|             holdingIdentityResolver.getByUsername("alice")); | ||||
| 
 | ||||
|         System.out.println("testListGameProposals\n"+ gpList); | ||||
|         System.out.println("testListGameProposals "+ resp); | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     void testGemeProposalCreate() throws JsonMappingException, JsonProcessingException { | ||||
|         final String gpIssuer = "alice"; | ||||
|         final String gpAcquier = "bob"; | ||||
|         final Color gpAcquierColor = Color.WHITE; | ||||
|         final String gpMessage = "GameProposal create test"; | ||||
| 
 | ||||
|         final UUID createdGpUuid = cordaClient.gameProposalCreate( | ||||
|             holdingIdentityResolver.getByUsername(gpIssuer), | ||||
|             holdingIdentityResolver.getByUsername(gpAcquier), | ||||
|             gpAcquierColor, | ||||
|             gpMessage | ||||
|         ); | ||||
| 
 | ||||
|         List<GameProposal> gpListSender = cordaClient.gameProposalList( | ||||
|             holdingIdentityResolver.getByUsername(gpIssuer)); | ||||
| 
 | ||||
|         assertThat(findByUuid(gpListSender, createdGpUuid)).isNotNull(); | ||||
| 
 | ||||
|         List<GameProposal> gpListReceiver = cordaClient.gameProposalList( | ||||
|             holdingIdentityResolver.getByUsername(gpAcquier)); | ||||
| 
 | ||||
|         GameProposal gp; | ||||
|         assertThat(gp = findByUuid(gpListReceiver, createdGpUuid)).isNotNull(); | ||||
| 
 | ||||
|         assertThat(gp.acquier()).isEqualToIgnoringCase(gpAcquier); | ||||
|         assertThat(gp.issuer()).isEqualToIgnoringCase(gpIssuer); | ||||
|         assertThat(gp.acquierColor()).isEqualByComparingTo(gpAcquierColor); | ||||
|         assertThat(gp.message()).isEqualTo(gpMessage); | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     void testGemeProposalReject() throws JsonMappingException, JsonProcessingException { | ||||
|     void testGemeProposalCreate() { | ||||
|         final String gpSender = "alice"; | ||||
|         final String gpReceiver = "bob"; | ||||
|         final Color gpReceiverColor = Color.WHITE; | ||||
|         final String gpMessage = "GameProposal REJECT test"; | ||||
|         final String gpMessage = "GameProposal create test"; | ||||
| 
 | ||||
|         final UUID gpUuid = cordaClient.gameProposalCreate( | ||||
|         final String gpUuid = cordaClient.createGameProposal( | ||||
|             holdingIdentityResolver.getByUsername(gpSender), | ||||
|             holdingIdentityResolver.getByUsername(gpReceiver), | ||||
|             gpReceiverColor, | ||||
|             gpMessage | ||||
|         ); | ||||
| 
 | ||||
|         String listResSender = cordaClient.listGameProposals( | ||||
|             holdingIdentityResolver.getByUsername(gpSender)); | ||||
| 
 | ||||
|         String listResReceiver = cordaClient.listGameProposals( | ||||
|             holdingIdentityResolver.getByUsername(gpReceiver)); | ||||
| 
 | ||||
|         assertThat(listResSender).contains(gpUuid); | ||||
|         assertThat(listResReceiver).contains(gpUuid); | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     void testGemeProposalReject() { | ||||
|         final String gpSender = "alice"; | ||||
|         final String gpReceiver = "bob"; | ||||
|         final Color gpReceiverColor = Color.WHITE; | ||||
|         final String gpMessage = "GameProposal create test"; | ||||
| 
 | ||||
|         final String gpUuid = cordaClient.createGameProposal( | ||||
|             holdingIdentityResolver.getByUsername(gpSender), | ||||
|             holdingIdentityResolver.getByUsername(gpReceiver), | ||||
|             gpReceiverColor, | ||||
| @ -92,45 +80,20 @@ public class CordaClientTest { | ||||
| 
 | ||||
|         System.out.println("Create GP UUID "+ gpUuid); | ||||
| 
 | ||||
|         assertThatThrownBy(() -> { | ||||
|             cordaClient.gameProposalAction( | ||||
|                 holdingIdentityResolver.getByUsername(gpSender), | ||||
|                 gpUuid, | ||||
|                 Action.REJECT); | ||||
|         }); | ||||
|         String listResSender = cordaClient.listGameProposals( | ||||
|             holdingIdentityResolver.getByUsername(gpSender)); | ||||
| 
 | ||||
|         final String rejectRes = cordaClient.gameProposalAction( | ||||
|         String listResReceiver = cordaClient.listGameProposals( | ||||
|             holdingIdentityResolver.getByUsername(gpReceiver)); | ||||
| 
 | ||||
|         assertThat(listResSender).contains(gpUuid); | ||||
|         assertThat(listResReceiver).contains(gpUuid); | ||||
| 
 | ||||
|         final String rejectRes = cordaClient.rejectGameProposal( | ||||
|             holdingIdentityResolver.getByUsername(gpReceiver), | ||||
|             gpUuid, | ||||
|             Action.REJECT | ||||
|             "1ed70601-c79a-486d-b907-8537f317083a" | ||||
|         ); | ||||
| 
 | ||||
|         assertThat(rejectRes).isEqualToIgnoringCase("REJECTED"); | ||||
| 
 | ||||
|         List<GameProposal> gpListSender = cordaClient.gameProposalList( | ||||
|             holdingIdentityResolver.getByUsername(gpSender)); | ||||
| 
 | ||||
|         assertThat(findByUuid(gpListSender, gpUuid)).isNull(); | ||||
| 
 | ||||
|         List<GameProposal> gpListReceiver = cordaClient.gameProposalList( | ||||
|             holdingIdentityResolver.getByUsername(gpReceiver)); | ||||
| 
 | ||||
|         assertThat(findByUuid(gpListReceiver, gpUuid)).isNull(); | ||||
| 
 | ||||
|         // GameProposal can not be rejected twice  | ||||
|         assertThatThrownBy(() -> { | ||||
|             cordaClient.gameProposalAction( | ||||
|                 holdingIdentityResolver.getByUsername(gpSender), | ||||
|                 gpUuid, | ||||
|                 Action.REJECT); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     private GameProposal findByUuid(List<GameProposal> gpList, UUID uuid) { | ||||
|         for (GameProposal gameProposal : gpList) { | ||||
|             if (gameProposal.id().compareTo(uuid) == 0) | ||||
|                 return gameProposal; | ||||
|         }; | ||||
|         return null; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -3,96 +3,66 @@ package djmil.cordacheckers.contracts; | ||||
| import org.slf4j.Logger; | ||||
| import org.slf4j.LoggerFactory; | ||||
| 
 | ||||
| //import djmil.cordacheckers.states.GameProposalResolutionState; | ||||
| import djmil.cordacheckers.states.GameProposalResolutionState; | ||||
| import djmil.cordacheckers.states.GameProposalState; | ||||
| import net.corda.v5.base.exceptions.CordaRuntimeException; | ||||
| import net.corda.v5.base.types.MemberX500Name; | ||||
| import net.corda.v5.ledger.utxo.Command; | ||||
| import net.corda.v5.ledger.utxo.StateAndRef; | ||||
| import net.corda.v5.ledger.utxo.transaction.UtxoLedgerTransaction; | ||||
| 
 | ||||
| public class GameProposalContract implements net.corda.v5.ledger.utxo.Contract { | ||||
| 
 | ||||
|     private final static Logger log = LoggerFactory.getLogger(GameProposalContract.class); | ||||
| 
 | ||||
|     public static enum Action implements Command {  | ||||
|         CREATE, | ||||
|         ACCEPT, | ||||
|         REJECT, | ||||
|         CANCEL; | ||||
| 
 | ||||
|         public MemberX500Name getInitiator(GameProposalState gameProposalState) { | ||||
|             switch (this) { | ||||
|                 case CREATE: | ||||
|                 case CANCEL: | ||||
|                     return gameProposalState.getIssuer(); | ||||
|                 case ACCEPT: | ||||
|                 case REJECT: | ||||
|                     return gameProposalState.getAcquier(); | ||||
|                 default: | ||||
|                     throw new RuntimeException(UNSUPPORTED_ACTION_VALUE_OF + this.name()); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         public MemberX500Name getRespondent(GameProposalState gameProposalState) { | ||||
|             switch (this) { | ||||
|                 case CREATE: | ||||
|                 case CANCEL: | ||||
|                     return gameProposalState.getAcquier(); | ||||
|                 case ACCEPT: | ||||
|                 case REJECT: | ||||
|                     return gameProposalState.getIssuer(); | ||||
|                 default: | ||||
|                     throw new RuntimeException(UNSUPPORTED_ACTION_VALUE_OF + this.name()); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         public MemberX500Name getSender(StateAndRef<GameProposalState> utxoGameProposal) { | ||||
|             return getInitiator(utxoGameProposal.getState().getContractState()); | ||||
|         } | ||||
| 
 | ||||
|         public MemberX500Name getReceiver(StateAndRef<GameProposalState> utxoGameProposal) { | ||||
|             return getRespondent(utxoGameProposal.getState().getContractState()); | ||||
|         } | ||||
| 
 | ||||
|         public static final String UNSUPPORTED_ACTION_VALUE_OF = "Unsupported Action value: "; | ||||
|     } | ||||
|     public static class Create implements Command { } | ||||
|     public static class Accept implements Command { } | ||||
|     public static class Reject implements Command { } | ||||
|     public static class Cancel implements Command { } | ||||
| 
 | ||||
|     @Override | ||||
|     public void verify(UtxoLedgerTransaction trx) { | ||||
|         log.info("GameProposalContract.verify() called"); | ||||
| 
 | ||||
|         requireThat(trx.getCommands().size() == 1, REQUIRE_SINGLE_COMMAND); | ||||
|         Command command = trx.getCommands().get(0); | ||||
| 
 | ||||
|         switch ((Action)(trx.getCommands().get(0))) { | ||||
|             case CREATE: { | ||||
|                 requireThat(trx.getInputContractStates().isEmpty(), CREATE_INPUT_STATE); | ||||
|                 requireThat(trx.getOutputContractStates().size() == 1, CREATE_OUTPUT_STATE); | ||||
|         if (command instanceof Create) { | ||||
|             requireThat(trx.getInputContractStates().isEmpty(), CREATE_INPUT_STATE); | ||||
|             requireThat(trx.getOutputContractStates().size() == 1, CREATE_OUTPUT_STATE); | ||||
|              | ||||
|                 GameProposalState outputState = trx.getOutputStates(GameProposalState.class).get(0); | ||||
|                 requireThat(outputState != null, CREATE_OUTPUT_STATE); | ||||
|             GameProposalState outputState = trx.getOutputStates(GameProposalState.class).get(0); | ||||
|             requireThat(outputState != null, CREATE_OUTPUT_STATE); | ||||
| 
 | ||||
|                 requireThat(outputState.getRecipientColor() != null, NON_NULL_RECIPIENT_COLOR); | ||||
|                 break; } | ||||
|             requireThat(outputState.getRecipientColor() != null, NON_NULL_RECIPIENT_COLOR); | ||||
|         } else  | ||||
|         if (command instanceof Accept) { | ||||
|             // TODO outputState -> Game | ||||
|             throw new CordaRuntimeException("Unimplemented!"); | ||||
|         } else  | ||||
|         if (command instanceof Reject) { | ||||
|             requireThat(trx.getInputContractStates().size() == 1, REJECT_INPUT_STATE); | ||||
|             requireThat(trx.getOutputContractStates().size() == 1, REJECT_OUTPUT_STATE); | ||||
| 
 | ||||
|             case ACCEPT: | ||||
|                 throw new CordaRuntimeException("Unimplemented!"); | ||||
|                 //break;  | ||||
|             GameProposalState inputState = trx.getInputStates(GameProposalState.class).get(0); | ||||
|             requireThat(inputState != null, REJECT_INPUT_STATE); | ||||
|              | ||||
|             case REJECT: | ||||
|                 requireThat(trx.getInputContractStates().size() == 1, REJECT_INPUT_STATE); | ||||
|                 requireThat(trx.getOutputContractStates().isEmpty(), REJECT_OUTPUT_STATE); | ||||
|                 requireThat(trx.getInputStates(GameProposalState.class).get(0) != null, REJECT_INPUT_STATE); | ||||
|                 break; | ||||
|             GameProposalResolutionState outputState = trx.getOutputStates(GameProposalResolutionState.class).get(0); | ||||
|             requireThat(outputState != null, REJECT_OUTPUT_STATE); | ||||
| 
 | ||||
|             case CANCEL: | ||||
|                 requireThat(trx.getInputContractStates().size() == 1, CANCEL_INPUT_STATE); | ||||
|                 requireThat(trx.getOutputContractStates().isEmpty(), CANCEL_OUTPUT_STATE); | ||||
|                 requireThat(trx.getInputStates(GameProposalState.class).get(0) != null, CANCEL_INPUT_STATE); | ||||
|                 break; | ||||
|             requireThat(outputState.outcome == GameProposalResolutionState.Resolution.REJECT, REJECT_OUTPUT_OUTCOME); | ||||
|         } else  | ||||
|         if (command instanceof Cancel) { | ||||
|             requireThat(trx.getInputContractStates().size() == 1, CANCEL_INPUT_STATE); | ||||
|             requireThat(trx.getOutputContractStates().size() == 1, CANCEL_OUTPUT_STATE); | ||||
| 
 | ||||
|             default: | ||||
|                 throw new CordaRuntimeException(UNKNOWN_COMMAND); | ||||
|             GameProposalState inputState = trx.getInputStates(GameProposalState.class).get(0); | ||||
|             requireThat(inputState != null, CANCEL_INPUT_STATE); | ||||
|              | ||||
|             GameProposalResolutionState outputState = trx.getOutputStates(GameProposalResolutionState.class).get(0); | ||||
|             requireThat(outputState != null, CANCEL_OUTPUT_STATE); | ||||
| 
 | ||||
|             requireThat(outputState.outcome == GameProposalResolutionState.Resolution.CANCEL, CANCEL_OUTPUT_OUTCOME); | ||||
|         } else { | ||||
|             throw new CordaRuntimeException(UNKNOWN_COMMAND); | ||||
|         } | ||||
|     } | ||||
|      | ||||
| @ -110,8 +80,10 @@ public class GameProposalContract implements net.corda.v5.ledger.utxo.Contract { | ||||
|     static final String NON_NULL_RECIPIENT_COLOR = "GameProposal.recipientColor field can not be null"; | ||||
| 
 | ||||
|     static final String REJECT_INPUT_STATE  = "Reject command should have exactly one GameProposal input state"; | ||||
|     static final String REJECT_OUTPUT_STATE = "Reject command should have no output states"; | ||||
|     static final String REJECT_OUTPUT_STATE = "Reject command should have exactly one GameProposalResolution output states"; | ||||
|     static final String REJECT_OUTPUT_OUTCOME = "Reject output state should have Resolution value set to REJECT"; | ||||
| 
 | ||||
|     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 exactly one GameProposalResolution output states"; | ||||
|     static final String CANCEL_OUTPUT_OUTCOME = "Cancel output state should have Resolution value set to CANCEL"; | ||||
| } | ||||
|  | ||||
| @ -0,0 +1,63 @@ | ||||
| package djmil.cordacheckers.states; | ||||
| 
 | ||||
| import java.security.PublicKey; | ||||
| import java.util.List; | ||||
| 
 | ||||
| import djmil.cordacheckers.contracts.GameProposalContract; | ||||
| import net.corda.v5.base.annotations.ConstructorForDeserialization; | ||||
| import net.corda.v5.base.annotations.CordaSerializable; | ||||
| import net.corda.v5.base.types.MemberX500Name; | ||||
| import net.corda.v5.ledger.utxo.BelongsToContract; | ||||
| import net.corda.v5.ledger.utxo.ContractState; | ||||
| 
 | ||||
| @BelongsToContract(GameProposalContract.class) | ||||
| public class GameProposalResolutionState implements ContractState { | ||||
| 
 | ||||
|     @CordaSerializable | ||||
|     public enum Resolution { | ||||
|         ACCEPT, | ||||
|         REJECT, | ||||
|         CANCEL | ||||
|     } | ||||
| 
 | ||||
|     public final Resolution outcome; | ||||
|     public final List<PublicKey> participants; | ||||
| 
 | ||||
|     public GameProposalResolutionState( | ||||
|         Resolution outcome, | ||||
|         GameProposalState gameProposal | ||||
|     ) { | ||||
|         this.outcome = outcome; | ||||
|         this.participants = gameProposal.getParticipants(); | ||||
|     } | ||||
| 
 | ||||
|     @ConstructorForDeserialization | ||||
|     public GameProposalResolutionState( | ||||
|         Resolution outcome, | ||||
|         List<PublicKey> participants | ||||
|     ) { | ||||
|         this.outcome = outcome; | ||||
|         this.participants = participants; | ||||
|     } | ||||
| 
 | ||||
|     public Resolution getOutcome() { | ||||
|         return outcome; | ||||
|     } | ||||
| 
 | ||||
|     public List<PublicKey> getParticipants() { | ||||
|         return this.participants; | ||||
|     } | ||||
| 
 | ||||
|     public MemberX500Name getRecipient(GameProposalState gameProposal) { | ||||
|         switch (outcome) { | ||||
|             case ACCEPT: | ||||
|             case REJECT: | ||||
|                 return gameProposal.getSender(); | ||||
|             case CANCEL: | ||||
|                 return gameProposal.getRecipient(); | ||||
|             default: | ||||
|                 throw new RuntimeException("Unknown Resolution value: "+outcome.toString()); | ||||
|         } | ||||
|     } | ||||
|      | ||||
| } | ||||
| @ -6,6 +6,7 @@ import java.util.UUID; | ||||
| 
 | ||||
| import djmil.cordacheckers.contracts.GameProposalContract; | ||||
| import net.corda.v5.base.annotations.ConstructorForDeserialization; | ||||
| import net.corda.v5.base.annotations.CordaSerializable; | ||||
| import net.corda.v5.base.types.MemberX500Name; | ||||
| import net.corda.v5.ledger.utxo.BelongsToContract; | ||||
| import net.corda.v5.ledger.utxo.ContractState; | ||||
| @ -13,40 +14,46 @@ import net.corda.v5.ledger.utxo.ContractState; | ||||
| @BelongsToContract(GameProposalContract.class) | ||||
| public class GameProposalState implements ContractState { | ||||
| 
 | ||||
|     MemberX500Name issuer; | ||||
|     MemberX500Name acquier; | ||||
|     Piece.Color recipientColor; | ||||
|     String message; | ||||
|     UUID id; | ||||
|     List<PublicKey> participants; | ||||
|     @CordaSerializable | ||||
|     public enum Color { | ||||
|         WHITE, | ||||
|         BLACK, | ||||
|     } | ||||
|        | ||||
|     public final MemberX500Name sender; | ||||
|     public final MemberX500Name recipient; | ||||
|     public final Color recipientColor; | ||||
|     public final String message; | ||||
|     public final UUID id; | ||||
|     public final List<PublicKey> participants; | ||||
| 
 | ||||
|     // Allows serialisation and to use a specified UUID | ||||
|     @ConstructorForDeserialization | ||||
|     public GameProposalState( | ||||
|         MemberX500Name issuer, | ||||
|         MemberX500Name acquier, | ||||
|         Piece.Color recipientColor, | ||||
|         MemberX500Name sender, | ||||
|         MemberX500Name recipient, | ||||
|         Color recipientColor, | ||||
|         String message, | ||||
|         UUID id, | ||||
|         List<PublicKey> participants | ||||
|     ) { | ||||
|         this.issuer = issuer; | ||||
|         this.acquier = acquier; | ||||
|         this.sender = sender; | ||||
|         this.recipient = recipient; | ||||
|         this.recipientColor = recipientColor; | ||||
|         this.message = message; | ||||
|         this.id = id; | ||||
|         this.participants = participants; | ||||
|     } | ||||
| 
 | ||||
|     public MemberX500Name getIssuer() { | ||||
|         return issuer; | ||||
|     public MemberX500Name getSender() { | ||||
|         return sender; | ||||
|     } | ||||
| 
 | ||||
|     public MemberX500Name getAcquier() { | ||||
|         return acquier; | ||||
|     public MemberX500Name getRecipient() { | ||||
|         return recipient; | ||||
|     } | ||||
| 
 | ||||
|     public Piece.Color getRecipientColor() { | ||||
|     public Color getRecipientColor() { | ||||
|         return recipientColor; | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -1,24 +0,0 @@ | ||||
| package djmil.cordacheckers.states; | ||||
| 
 | ||||
| import net.corda.v5.base.annotations.CordaSerializable; | ||||
| 
 | ||||
| @CordaSerializable | ||||
| public class Piece { | ||||
|      | ||||
|     @CordaSerializable | ||||
|     public enum Type { | ||||
|         MAN, | ||||
|         KING, | ||||
|     } | ||||
| 
 | ||||
|     @CordaSerializable | ||||
|     public enum Color { | ||||
|         WHITE, | ||||
|         BLACK, | ||||
|     } | ||||
| 
 | ||||
|     Color color; | ||||
|     Type type; | ||||
| 
 | ||||
|      | ||||
| } | ||||
| @ -1,28 +1,18 @@ | ||||
| package djmil.cordacheckers; | ||||
| 
 | ||||
| import net.corda.v5.application.marshalling.JsonMarshallingService; | ||||
| import net.corda.v5.crypto.SecureHash; | ||||
| 
 | ||||
| public class FlowResult { | ||||
|     public final Object successStatus; | ||||
|     public final String transactionId; | ||||
|     public final String failureStatus; | ||||
| 
 | ||||
|     public FlowResult(Object success) { | ||||
|         this.successStatus = success; | ||||
|         this.transactionId = null; | ||||
|         this.failureStatus = null; | ||||
|     } | ||||
| 
 | ||||
|     public FlowResult(Object success, SecureHash transactionId) { | ||||
|         this.successStatus = success; | ||||
|         this.transactionId = transactionId.toString(); | ||||
|         this.failureStatus = null; | ||||
|     } | ||||
| 
 | ||||
|     public FlowResult(Exception exception) { | ||||
|         this.successStatus = null; | ||||
|         this.transactionId = null; | ||||
|         this.failureStatus = exception.getMessage(); | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -1,6 +1,8 @@ | ||||
| package djmil.cordacheckers.gameproposal; | ||||
| 
 | ||||
| import java.util.Arrays; | ||||
| import java.util.List; | ||||
| import java.util.Map; | ||||
| import java.util.UUID; | ||||
| import java.util.stream.Collectors; | ||||
| 
 | ||||
| @ -8,15 +10,21 @@ import org.slf4j.Logger; | ||||
| import org.slf4j.LoggerFactory; | ||||
| 
 | ||||
| import djmil.cordacheckers.FlowResult; | ||||
| import djmil.cordacheckers.contracts.GameProposalContract; | ||||
| import djmil.cordacheckers.states.GameProposalResolutionState; | ||||
| import djmil.cordacheckers.states.GameProposalState; | ||||
| import net.corda.v5.application.flows.ClientRequestBody; | ||||
| import net.corda.v5.application.flows.ClientStartableFlow; | ||||
| import net.corda.v5.application.flows.CordaInject; | ||||
| import net.corda.v5.application.flows.FlowEngine; | ||||
| import net.corda.v5.application.flows.InitiatingFlow; | ||||
| import net.corda.v5.application.marshalling.JsonMarshallingService; | ||||
| import net.corda.v5.application.membership.MemberLookup; | ||||
| import net.corda.v5.application.messaging.FlowMessaging; | ||||
| import net.corda.v5.application.messaging.FlowSession; | ||||
| import net.corda.v5.base.annotations.Suspendable; | ||||
| import net.corda.v5.base.exceptions.CordaRuntimeException; | ||||
| import net.corda.v5.crypto.SecureHash; | ||||
| import net.corda.v5.base.types.MemberX500Name; | ||||
| import net.corda.v5.ledger.utxo.Command; | ||||
| import net.corda.v5.ledger.utxo.StateAndRef; | ||||
| import net.corda.v5.ledger.utxo.UtxoLedgerService; | ||||
| import net.corda.v5.ledger.utxo.transaction.UtxoSignedTransaction; | ||||
| @ -25,8 +33,7 @@ import net.corda.v5.ledger.utxo.transaction.UtxoTransactionBuilder; | ||||
| import java.time.Duration; | ||||
| import java.time.Instant; | ||||
| 
 | ||||
| import static djmil.cordacheckers.contracts.GameProposalContract.Action; | ||||
| 
 | ||||
| @InitiatingFlow(protocol = "game-proposal-action") | ||||
| public class ActionFlow implements ClientStartableFlow { | ||||
| 
 | ||||
|     private final static Logger log = LoggerFactory.getLogger(CreateFlow.class); | ||||
| @ -38,24 +45,33 @@ public class ActionFlow implements ClientStartableFlow { | ||||
|     public UtxoLedgerService ledgerService; | ||||
| 
 | ||||
|     @CordaInject | ||||
|     public FlowEngine flowEngine; | ||||
|     public FlowMessaging flowMessaging; | ||||
| 
 | ||||
|     @CordaInject | ||||
|     public MemberLookup memberLookup; | ||||
| 
 | ||||
|     private final static Map<GameProposalResolutionState.Resolution, Command> resoultion2command = Map.ofEntries( | ||||
|         Map.entry(GameProposalResolutionState.Resolution.CANCEL, new GameProposalContract.Cancel()), | ||||
|         Map.entry(GameProposalResolutionState.Resolution.REJECT, new GameProposalContract.Reject()), | ||||
|         Map.entry(GameProposalResolutionState.Resolution.ACCEPT, new GameProposalContract.Accept()) | ||||
|     ); | ||||
| 
 | ||||
|     @Override | ||||
|     @Suspendable | ||||
|     public String call(ClientRequestBody requestBody) { | ||||
|         try { | ||||
|             final ActionFlowArgs args = requestBody.getRequestBodyAs(jsonMarshallingService, ActionFlowArgs.class); | ||||
|             final Action action = args.getAction(); | ||||
|             ActionFlowArgs args = requestBody.getRequestBodyAs(jsonMarshallingService, ActionFlowArgs.class); | ||||
| 
 | ||||
|             final StateAndRef<GameProposalState> inputState = findUnconsumedGameProposalState(args.getGameProposalUuid()); | ||||
|             StateAndRef<GameProposalState> inputState = findUnconsumedGameProposalState(args.getGameProposalUuid()); | ||||
|          | ||||
|             final UtxoSignedTransaction trx = prepareSignedTransaction(action, inputState); | ||||
|             GameProposalResolutionState outputState = new GameProposalResolutionState( | ||||
|                 args.getAction(), | ||||
|                 inputState.getState().getContractState() | ||||
|                 ); | ||||
| 
 | ||||
|             final SecureHash trxId = this.flowEngine | ||||
|                 .subFlow( new CommitSubFlow(trx, action.getReceiver(inputState)) ); | ||||
|             String trxResult = doTrunsaction(inputState, outputState); | ||||
| 
 | ||||
|             return new FlowResult(action+"ED", trxId)  // REJECT+ED | ||||
|                 .toJsonEncodedString(jsonMarshallingService); | ||||
|             return new FlowResult(trxResult).toJsonEncodedString(jsonMarshallingService); | ||||
|         }  | ||||
|         catch (Exception e) { | ||||
|             log.warn("GameProposalAction flow failed to process utxo request body " + requestBody +  | ||||
| @ -67,7 +83,7 @@ public class ActionFlow implements ClientStartableFlow { | ||||
|     @Suspendable | ||||
|     private StateAndRef<GameProposalState> findUnconsumedGameProposalState (UUID gameProposalUuid) { | ||||
|         /* | ||||
|          * Get list of all unconsumed aka 'active' GameProposalStates, then filter by UUID. | ||||
|          * Get list of all unconsumed aka 'actuve' GameProposalStates, then filter by UUID. | ||||
|          * Note, this is an inefficient way to perform this operation if there are a large | ||||
|          * number of 'active' GameProposals exists in storage. | ||||
|          */ | ||||
| @ -86,24 +102,26 @@ public class ActionFlow implements ClientStartableFlow { | ||||
|     } | ||||
| 
 | ||||
|     @Suspendable | ||||
|     private UtxoSignedTransaction prepareSignedTransaction( | ||||
|         Action action, | ||||
|         StateAndRef<GameProposalState> inputState | ||||
|     ) { | ||||
|         UtxoTransactionBuilder trxBuilder = ledgerService.createTransactionBuilder() | ||||
|     private String doTrunsaction(StateAndRef<GameProposalState> inputState, GameProposalResolutionState outputState) { | ||||
|         UtxoTransactionBuilder txBuilder = ledgerService.createTransactionBuilder() | ||||
|             .setNotary(inputState.getState().getNotaryName()) | ||||
|             .setTimeWindowBetween(Instant.now(), Instant.now().plusMillis(Duration.ofDays(1).toMillis())) | ||||
|             .addInputState(inputState.getRef()) | ||||
|             .addCommand(action) | ||||
|             .addSignatories(inputState.getState().getContractState().getParticipants()); | ||||
|             .addOutputState(outputState) | ||||
|             .addCommand(resoultion2command.get(outputState.outcome)) | ||||
|             .addSignatories(outputState.getParticipants()); | ||||
| 
 | ||||
|         if (action == Action.ACCEPT) { | ||||
|             // TODO | ||||
|             // .addOutputState(outputState) | ||||
|             throw new RuntimeException(action +" unimplemented"); | ||||
|         } | ||||
|         UtxoSignedTransaction signedTransaction = txBuilder.toSignedTransaction(); | ||||
| 
 | ||||
|         return trxBuilder.toSignedTransaction(); | ||||
|         FlowSession session = flowMessaging.initiateFlow( | ||||
|             outputState.getRecipient(inputState.getState().getContractState()) | ||||
|             ); | ||||
| 
 | ||||
|         List<FlowSession> sessionsList = Arrays.asList(session); | ||||
| 
 | ||||
|         ledgerService.finalize(signedTransaction, sessionsList); | ||||
| 
 | ||||
|         return outputState.getOutcome()+"ED"; // REJECT+ED | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | ||||
| @ -2,7 +2,8 @@ package djmil.cordacheckers.gameproposal; | ||||
| 
 | ||||
| import java.util.UUID; | ||||
| 
 | ||||
| import djmil.cordacheckers.contracts.GameProposalContract; | ||||
| import djmil.cordacheckers.states.GameProposalResolutionState; | ||||
| import djmil.cordacheckers.states.GameProposalResolutionState.Resolution; | ||||
| 
 | ||||
| public class ActionFlowArgs { | ||||
|     private UUID gameProposalUuid; | ||||
| @ -14,8 +15,8 @@ public class ActionFlowArgs { | ||||
|         this.action = null; | ||||
|     } | ||||
|      | ||||
|     public GameProposalContract.Action getAction() { | ||||
|         return GameProposalContract.Action.valueOf(this.action); | ||||
|     public Resolution getAction() { | ||||
|         return GameProposalResolutionState.Resolution.valueOf(this.action); | ||||
|     } | ||||
| 
 | ||||
|     public UUID getGameProposalUuid() { | ||||
|  | ||||
| @ -0,0 +1,83 @@ | ||||
| package djmil.cordacheckers.gameproposal; | ||||
| 
 | ||||
| import org.slf4j.Logger; | ||||
| import org.slf4j.LoggerFactory; | ||||
| 
 | ||||
| import djmil.cordacheckers.contracts.GameProposalContract.Accept; | ||||
| import djmil.cordacheckers.contracts.GameProposalContract.Cancel; | ||||
| import djmil.cordacheckers.contracts.GameProposalContract.Reject; | ||||
| import djmil.cordacheckers.states.GameProposalState; | ||||
| import net.corda.v5.application.flows.CordaInject; | ||||
| import net.corda.v5.application.flows.InitiatedBy; | ||||
| import net.corda.v5.application.flows.ResponderFlow; | ||||
| import net.corda.v5.application.membership.MemberLookup; | ||||
| import net.corda.v5.application.messaging.FlowSession; | ||||
| import net.corda.v5.base.annotations.Suspendable; | ||||
| import net.corda.v5.base.exceptions.CordaRuntimeException; | ||||
| import net.corda.v5.base.types.MemberX500Name; | ||||
| import net.corda.v5.ledger.utxo.Command; | ||||
| import net.corda.v5.ledger.utxo.UtxoLedgerService; | ||||
| import net.corda.v5.ledger.utxo.transaction.UtxoSignedTransaction; | ||||
| import net.corda.v5.ledger.utxo.transaction.UtxoTransactionValidator; | ||||
| 
 | ||||
| @InitiatedBy(protocol = "game-proposal-action") | ||||
| public class ActionResponder implements ResponderFlow { | ||||
|     private final static Logger log = LoggerFactory.getLogger(CreateResponder.class); | ||||
| 
 | ||||
|     @CordaInject | ||||
|     public MemberLookup memberLookup; | ||||
| 
 | ||||
|     @CordaInject | ||||
|     public UtxoLedgerService utxoLedgerService; | ||||
|      | ||||
|     @Suspendable | ||||
|     @Override | ||||
|     public void call(FlowSession session) { | ||||
|         try { | ||||
|             UtxoTransactionValidator txValidator = ledgerTransaction -> { | ||||
|                 GameProposalState gameProposal = (GameProposalState) ledgerTransaction.getInputContractStates().get(0); | ||||
| 
 | ||||
|                 Command command = ledgerTransaction.getCommands(Command.class).get(0); | ||||
| 
 | ||||
|                 if (!checkParticipants(gameProposal, session.getCounterparty(), command)) { | ||||
|                     throw new CordaRuntimeException("Failed verification"); | ||||
|                 } | ||||
| 
 | ||||
|                 log.info("Verified the transaction - " + ledgerTransaction.getId()); | ||||
|             }; | ||||
| 
 | ||||
|             // Calls receiveFinality() function which provides the responder to the finalise() function | ||||
|             // in the Initiating Flow. Accepts a lambda validator containing the business logic to decide whether | ||||
|             // responder should sign the Transaction. | ||||
|             UtxoSignedTransaction finalizedSignedTransaction = utxoLedgerService | ||||
|                     .receiveFinality(session, txValidator) | ||||
|                     .getTransaction(); | ||||
| 
 | ||||
|             log.info("Finished responder flow - " + finalizedSignedTransaction.getId()); | ||||
|         } | ||||
|         // Soft fails the flow and log the exception. | ||||
|         catch(Exception e) | ||||
|         { | ||||
|             log.warn("Exceptionally finished responder flow", e); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Suspendable | ||||
|     Boolean checkParticipants(GameProposalState gameProposal, MemberX500Name counterpartyName, Command command) { | ||||
|         MemberX500Name myName = memberLookup.myInfo().getName(); | ||||
| 
 | ||||
|         if (command instanceof Reject || command instanceof Accept) { | ||||
|             if (gameProposal.getRecipient().compareTo(counterpartyName) == 0 &&  | ||||
|                 gameProposal.getSender().compareTo(myName) == 0) | ||||
|                 return true; | ||||
|         } | ||||
| 
 | ||||
|         if (command instanceof Cancel) { | ||||
|             if (gameProposal.getRecipient().compareTo(myName) == 0 &&  | ||||
|                 gameProposal.getSender().compareTo(counterpartyName) == 0) | ||||
|                 return true; | ||||
|         } | ||||
| 
 | ||||
|         return false; | ||||
|     } | ||||
| } | ||||
| @ -1,95 +0,0 @@ | ||||
| package djmil.cordacheckers.gameproposal; | ||||
| 
 | ||||
| import org.slf4j.Logger; | ||||
| import org.slf4j.LoggerFactory; | ||||
| 
 | ||||
| import djmil.cordacheckers.states.GameProposalState; | ||||
| import net.corda.v5.application.flows.CordaInject; | ||||
| import net.corda.v5.application.flows.InitiatedBy; | ||||
| import net.corda.v5.application.flows.ResponderFlow; | ||||
| import net.corda.v5.application.membership.MemberLookup; | ||||
| import net.corda.v5.application.messaging.FlowSession; | ||||
| import net.corda.v5.base.annotations.Suspendable; | ||||
| import net.corda.v5.base.exceptions.CordaRuntimeException; | ||||
| import net.corda.v5.base.types.MemberX500Name; | ||||
| import net.corda.v5.ledger.utxo.UtxoLedgerService; | ||||
| import net.corda.v5.ledger.utxo.transaction.UtxoLedgerTransaction; | ||||
| import net.corda.v5.ledger.utxo.transaction.UtxoSignedTransaction; | ||||
| import net.corda.v5.ledger.utxo.transaction.UtxoTransactionValidator; | ||||
| 
 | ||||
| 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 | ||||
|     public MemberLookup memberLookup; | ||||
| 
 | ||||
|     @CordaInject | ||||
|     public UtxoLedgerService utxoLedgerService; | ||||
|      | ||||
|     @Suspendable | ||||
|     @Override | ||||
|     public void call(FlowSession session) { | ||||
|         try { | ||||
|             UtxoTransactionValidator txValidator = ledgerTransaction -> { | ||||
|                 final Action action = ledgerTransaction.getCommands(Action.class).get(0); | ||||
| 
 | ||||
|                 final GameProposalState gameProposal = getGameProposal(ledgerTransaction);  | ||||
| 
 | ||||
|                 checkSessionParticipants(session, gameProposal, action); | ||||
| 
 | ||||
|                 /* | ||||
|                  * Other checks / actions ? | ||||
|                  */ | ||||
| 
 | ||||
|                 log.info("Verified the transaction - " + ledgerTransaction.getId()); | ||||
|             }; | ||||
| 
 | ||||
|             UtxoSignedTransaction finalizedSignedTransaction = utxoLedgerService | ||||
|                     .receiveFinality(session, txValidator) | ||||
|                     .getTransaction(); | ||||
| 
 | ||||
|             log.info("Finished responder flow - " + finalizedSignedTransaction.getId()); | ||||
|         } | ||||
|         catch(Exception e) { | ||||
|             log.warn("Responder flow failed: ", e); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Suspendable | ||||
|     void checkSessionParticipants( | ||||
|         FlowSession session, | ||||
|         GameProposalState gameProposal, | ||||
|         Action action | ||||
|     ) { | ||||
|         final MemberX500Name myName = memberLookup.myInfo().getName(); | ||||
|         final MemberX500Name otherName = session.getCounterparty(); | ||||
| 
 | ||||
|         if (action.getRespondent(gameProposal).compareTo(myName) != 0) | ||||
|             throw new CordaRuntimeException("Bad GameProposal acquirer: expected '"+myName+"', actual '" +gameProposal.getAcquier() +"'"); | ||||
|          | ||||
|         if (action.getInitiator(gameProposal).compareTo(otherName) != 0) | ||||
|             throw new CordaRuntimeException("Bad GameProposal issuer: expected '"+otherName+"', actual '"+gameProposal.getIssuer()+"'"); | ||||
|     } | ||||
| 
 | ||||
|     @Suspendable | ||||
|     GameProposalState getGameProposal(UtxoLedgerTransaction trx) { | ||||
|         final Action action = trx.getCommands(Action.class).get(0); | ||||
|          | ||||
|         switch (action) { | ||||
|             case CREATE: | ||||
|                 return (GameProposalState)trx.getOutputContractStates().get(0); | ||||
| 
 | ||||
|             case ACCEPT: | ||||
|             case REJECT: | ||||
|             case CANCEL: | ||||
|                 return (GameProposalState)trx.getInputContractStates().get(0); | ||||
| 
 | ||||
|             default: | ||||
|                 throw new RuntimeException(Action.UNSUPPORTED_ACTION_VALUE_OF +action); | ||||
|         } | ||||
| 
 | ||||
|     } | ||||
| } | ||||
| @ -1,60 +0,0 @@ | ||||
| package djmil.cordacheckers.gameproposal; | ||||
| 
 | ||||
| import java.util.Arrays; | ||||
| import java.util.List; | ||||
| 
 | ||||
| import org.slf4j.Logger; | ||||
| import org.slf4j.LoggerFactory; | ||||
| 
 | ||||
| import net.corda.v5.application.flows.CordaInject; | ||||
| import net.corda.v5.application.flows.InitiatingFlow; | ||||
| import net.corda.v5.application.flows.SubFlow; | ||||
| import net.corda.v5.application.messaging.FlowMessaging; | ||||
| import net.corda.v5.application.messaging.FlowSession; | ||||
| import net.corda.v5.base.annotations.Suspendable; | ||||
| import net.corda.v5.base.types.MemberX500Name; | ||||
| import net.corda.v5.crypto.SecureHash; | ||||
| import net.corda.v5.ledger.utxo.UtxoLedgerService; | ||||
| import net.corda.v5.ledger.utxo.transaction.UtxoSignedTransaction; | ||||
| 
 | ||||
| @InitiatingFlow(protocol = "game-proposal") | ||||
| public class CommitSubFlow implements SubFlow<SecureHash> { | ||||
| 
 | ||||
|     private final static Logger log = LoggerFactory.getLogger(CommitSubFlow.class); | ||||
|     private final UtxoSignedTransaction signedTransaction; | ||||
|     private final MemberX500Name respondenName; | ||||
| 
 | ||||
|     public CommitSubFlow(UtxoSignedTransaction signedTransaction, MemberX500Name respondenName) { | ||||
|         this.signedTransaction = signedTransaction; | ||||
|         this.respondenName = respondenName; | ||||
|     } | ||||
| 
 | ||||
|     @CordaInject | ||||
|     public UtxoLedgerService ledgerService; | ||||
| 
 | ||||
|     @CordaInject | ||||
|     public FlowMessaging flowMessaging; | ||||
| 
 | ||||
|     @Override | ||||
|     @Suspendable | ||||
|     public SecureHash call() { | ||||
|         log.info("GamePropsal commit started"); | ||||
| 
 | ||||
|         final FlowSession session = flowMessaging.initiateFlow(this.respondenName); | ||||
| 
 | ||||
|         /* | ||||
|          * Calls the Corda provided finalise() function which gather signatures from the counterparty, | ||||
|          * notarises the transaction and persists the transaction to each party's vault. | ||||
|          */ | ||||
| 
 | ||||
|         final List<FlowSession> sessionsList = Arrays.asList(session); | ||||
| 
 | ||||
|         final SecureHash trxId = ledgerService | ||||
|             .finalize(this.signedTransaction, sessionsList) | ||||
|             .getTransaction() | ||||
|             .getId(); | ||||
| 
 | ||||
|         log.info("GamePropsal commit " +trxId); | ||||
|         return trxId; | ||||
|     } | ||||
| } | ||||
| @ -4,8 +4,8 @@ import org.slf4j.Logger; | ||||
| import org.slf4j.LoggerFactory; | ||||
| 
 | ||||
| import djmil.cordacheckers.FlowResult; | ||||
| import djmil.cordacheckers.contracts.GameProposalContract; | ||||
| import djmil.cordacheckers.states.GameProposalState; | ||||
| import djmil.cordacheckers.states.Piece; | ||||
| import net.corda.v5.application.flows.ClientRequestBody; | ||||
| import net.corda.v5.application.flows.ClientStartableFlow; | ||||
| import net.corda.v5.application.flows.CordaInject; | ||||
| @ -13,23 +13,26 @@ import net.corda.v5.application.flows.FlowEngine; | ||||
| import net.corda.v5.application.flows.InitiatingFlow; | ||||
| import net.corda.v5.application.marshalling.JsonMarshallingService; | ||||
| import net.corda.v5.application.membership.MemberLookup; | ||||
| import net.corda.v5.application.messaging.FlowMessaging; | ||||
| import net.corda.v5.application.messaging.FlowSession; | ||||
| import net.corda.v5.base.annotations.Suspendable; | ||||
| import net.corda.v5.base.types.MemberX500Name; | ||||
| import net.corda.v5.crypto.SecureHash; | ||||
| import net.corda.v5.ledger.common.NotaryLookup; | ||||
| import net.corda.v5.ledger.utxo.UtxoLedgerService; | ||||
| import net.corda.v5.ledger.utxo.transaction.UtxoSignedTransaction; | ||||
| import net.corda.v5.ledger.utxo.transaction.UtxoTransactionBuilder; | ||||
| import net.corda.v5.membership.MemberInfo; | ||||
| import net.corda.v5.membership.NotaryInfo; | ||||
| 
 | ||||
| import static java.util.Objects.requireNonNull; | ||||
| import static djmil.cordacheckers.contracts.GameProposalContract.Action; | ||||
| 
 | ||||
| import java.time.Duration; | ||||
| import java.time.Instant; | ||||
| import java.util.Arrays; | ||||
| import java.util.List; | ||||
| import java.util.UUID; | ||||
| 
 | ||||
| @InitiatingFlow(protocol = "game-proposal-create") | ||||
| public class CreateFlow implements ClientStartableFlow{ | ||||
| 
 | ||||
|      private final static Logger log = LoggerFactory.getLogger(CreateFlow.class); | ||||
| @ -49,22 +52,19 @@ public class CreateFlow implements ClientStartableFlow{ | ||||
|      @CordaInject | ||||
|      public FlowEngine flowEngine; | ||||
| 
 | ||||
|      @CordaInject | ||||
|      public FlowMessaging flowMessaging; | ||||
| 
 | ||||
|      @Suspendable | ||||
|      @Override | ||||
|      public String call(ClientRequestBody requestBody) { | ||||
|           try { | ||||
|                log.info("flow: Create Game Proposal"); | ||||
|                final Action actino = Action.CREATE; | ||||
| 
 | ||||
|                final GameProposalState newGameProposal = buildGameProposalStateFrom(requestBody); | ||||
|                GameProposalState gameProposal = buildGameProposalStateFrom(requestBody); | ||||
|                String trxResult = doTrunsaction(gameProposal); | ||||
| 
 | ||||
|                final UtxoSignedTransaction trx = prepareSignedTransaction(newGameProposal); | ||||
| 
 | ||||
|                final SecureHash trxId = this.flowEngine | ||||
|                     .subFlow(new CommitSubFlow(trx, actino.getRespondent(newGameProposal))); | ||||
| 
 | ||||
|                return new FlowResult(newGameProposal.getId(), trxId) | ||||
|                     .toJsonEncodedString(jsonMarshallingService); | ||||
|                return new FlowResult(trxResult).toJsonEncodedString(jsonMarshallingService); | ||||
|           }  | ||||
|           catch (Exception e) { | ||||
|                log.warn("CreateGameProposal flow failed to process utxo request body " + requestBody +  | ||||
| @ -77,10 +77,10 @@ public class CreateFlow implements ClientStartableFlow{ | ||||
|      private GameProposalState buildGameProposalStateFrom(ClientRequestBody requestBody) { | ||||
|           CreateFlowArgs args = requestBody.getRequestBodyAs(jsonMarshallingService, CreateFlowArgs.class); | ||||
| 
 | ||||
|           Piece.Color opponentColor = Piece.Color.valueOf(args.opponentColor); | ||||
|           GameProposalState.Color opponentColor = GameProposalState.Color.valueOf(args.opponentColor); | ||||
|           if (opponentColor == null) { | ||||
|                throw new RuntimeException("Allowed values for opponentColor are: " | ||||
|                     + Piece.Color.WHITE.name() +", "  + Piece.Color.BLACK.name()  | ||||
|                     + GameProposalState.Color.WHITE.name() +", "  + GameProposalState.Color.BLACK.name()  | ||||
|                     + ". Actual value was: " + args.opponentColor); | ||||
|           } | ||||
| 
 | ||||
| @ -101,15 +101,24 @@ public class CreateFlow implements ClientStartableFlow{ | ||||
|      } | ||||
| 
 | ||||
|      @Suspendable | ||||
|      private UtxoSignedTransaction prepareSignedTransaction(GameProposalState outputGameProposalState) {      | ||||
|           final NotaryInfo notary = notaryLookup.getNotaryServices().iterator().next(); | ||||
|      private String doTrunsaction(GameProposalState gameProposal) {      | ||||
|           NotaryInfo notary = notaryLookup.getNotaryServices().iterator().next(); | ||||
| 
 | ||||
|           return ledgerService.createTransactionBuilder() | ||||
|           UtxoTransactionBuilder txBuilder = ledgerService.createTransactionBuilder() | ||||
|                .setNotary(notary.getName()) | ||||
|                .setTimeWindowBetween(Instant.now(), Instant.now().plusMillis(Duration.ofDays(1).toMillis())) | ||||
|                .addOutputState(outputGameProposalState) | ||||
|                .addCommand(Action.CREATE) | ||||
|                .addSignatories(outputGameProposalState.getParticipants()) | ||||
|                .toSignedTransaction(); | ||||
|                .addOutputState(gameProposal) | ||||
|                .addCommand(new GameProposalContract.Create()) | ||||
|                .addSignatories(gameProposal.getParticipants()); | ||||
| 
 | ||||
|           UtxoSignedTransaction signedTransaction = txBuilder.toSignedTransaction(); | ||||
| 
 | ||||
|           FlowSession session = flowMessaging.initiateFlow(gameProposal.getRecipient()); | ||||
| 
 | ||||
|           List<FlowSession> sessionsList = Arrays.asList(session); | ||||
| 
 | ||||
|           ledgerService.finalize(signedTransaction, sessionsList); | ||||
| 
 | ||||
|           return gameProposal.id.toString(); | ||||
|      } | ||||
| } | ||||
|  | ||||
| @ -0,0 +1,72 @@ | ||||
| package djmil.cordacheckers.gameproposal; | ||||
| 
 | ||||
| import org.slf4j.Logger; | ||||
| import org.slf4j.LoggerFactory; | ||||
| 
 | ||||
| import djmil.cordacheckers.states.GameProposalState; | ||||
| import net.corda.v5.application.flows.CordaInject; | ||||
| import net.corda.v5.application.flows.InitiatedBy; | ||||
| import net.corda.v5.application.flows.ResponderFlow; | ||||
| import net.corda.v5.application.membership.MemberLookup; | ||||
| import net.corda.v5.application.messaging.FlowSession; | ||||
| import net.corda.v5.base.annotations.Suspendable; | ||||
| import net.corda.v5.base.exceptions.CordaRuntimeException; | ||||
| import net.corda.v5.base.types.MemberX500Name; | ||||
| import net.corda.v5.ledger.utxo.UtxoLedgerService; | ||||
| import net.corda.v5.ledger.utxo.transaction.UtxoSignedTransaction; | ||||
| import net.corda.v5.ledger.utxo.transaction.UtxoTransactionValidator; | ||||
| 
 | ||||
| @InitiatedBy(protocol = "game-proposal-create") | ||||
| public class CreateResponder implements ResponderFlow { | ||||
|     private final static Logger log = LoggerFactory.getLogger(CreateResponder.class); | ||||
| 
 | ||||
|     @CordaInject | ||||
|     public MemberLookup memberLookup; | ||||
| 
 | ||||
|     @CordaInject | ||||
|     public UtxoLedgerService utxoLedgerService; | ||||
|      | ||||
|     @Suspendable | ||||
|     @Override | ||||
|     public void call(FlowSession session) { | ||||
|         try { | ||||
|             // Defines the lambda validator used in receiveFinality below. | ||||
|             UtxoTransactionValidator txValidator = ledgerTransaction -> { | ||||
|                 GameProposalState gameProposal = (GameProposalState) ledgerTransaction.getOutputContractStates().get(0); | ||||
|                 // Uses checkForBannedWords() and checkMessageFromMatchesCounterparty() functions | ||||
|                 // to check whether to sign the transaction. | ||||
|                 if (!checkParticipants(gameProposal, session.getCounterparty())) { | ||||
|                     throw new CordaRuntimeException("Failed verification"); | ||||
|                 } | ||||
| 
 | ||||
|                 log.info("Verified the transaction - " + ledgerTransaction.getId()); | ||||
|             }; | ||||
| 
 | ||||
|             // Calls receiveFinality() function which provides the responder to the finalise() function | ||||
|             // in the Initiating Flow. Accepts a lambda validator containing the business logic to decide whether | ||||
|             // responder should sign the Transaction. | ||||
|             UtxoSignedTransaction finalizedSignedTransaction = utxoLedgerService | ||||
|                     .receiveFinality(session, txValidator) | ||||
|                     .getTransaction(); | ||||
| 
 | ||||
|             log.info("Finished responder flow - " + finalizedSignedTransaction.getId()); | ||||
|         } | ||||
|         // Soft fails the flow and log the exception. | ||||
|         catch(Exception e) | ||||
|         { | ||||
|             log.warn("Exceptionally finished responder flow", e); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Suspendable | ||||
|     Boolean checkParticipants(GameProposalState gameProposal, MemberX500Name counterpartyName) { | ||||
|         MemberX500Name myName = memberLookup.myInfo().getName(); | ||||
| 
 | ||||
|         if (gameProposal.getRecipient().compareTo(myName) == 0 &&  | ||||
|             gameProposal.getSender().compareTo(counterpartyName) == 0) | ||||
|             return true; | ||||
|      | ||||
|         return false; | ||||
|     } | ||||
|      | ||||
| } | ||||
| @ -10,26 +10,26 @@ import djmil.cordacheckers.states.GameProposalState; | ||||
| // and a UUID. 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 String sender; | ||||
|     public final String recipient; | ||||
|     public final String recipientColor; | ||||
|     public final String message; | ||||
|     public final UUID id; | ||||
| 
 | ||||
|     // Serialisation service requires a default constructor | ||||
|     public ListItem() {  | ||||
|         this.issuer = null; | ||||
|         this.acquier = null; | ||||
|         this.acquierColor = null; | ||||
|         this.sender = null; | ||||
|         this.recipient = null; | ||||
|         this.recipientColor = null; | ||||
|         this.message = null; | ||||
|         this.id = null; | ||||
|     } | ||||
| 
 | ||||
|     public ListItem(GameProposalState state) { | ||||
|         this.issuer  = state.getIssuer().getCommonName(); | ||||
|         this.acquier = state.getAcquier().getCommonName(); | ||||
|         this.acquierColor = state.getRecipientColor().name(); | ||||
|         this.message = state.getMessage(); | ||||
|         this.id      = state.getId(); | ||||
|         this.sender    = state.getSender().getCommonName(); | ||||
|         this.recipient = state.getRecipient().getCommonName(); | ||||
|         this.recipientColor = state.getRecipientColor().name(); | ||||
|         this.message   = state.getMessage(); | ||||
|         this.id        = state.getId(); | ||||
|     } | ||||
| } | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user