Compare commits

...

5 Commits

Author SHA1 Message Date
e235ecb942 Corda: CommitSubFlow 2023-09-07 21:33:37 +02:00
159bcd706e GameProposal updates
- remove GameProposalResolution state
use ladger trx history instead

- use issuer/acquier instead of sennder/receiver
2023-09-07 14:47:53 +02:00
5cc579230f SpringBoot: test GP can not be rejected twice 2023-09-07 14:00:27 +02:00
c1dbb3d213 Corda: Piece class 2023-09-07 09:57:54 +02:00
a9b70b963c CordaClient: use FlowResult 2023-09-06 12:39:52 +02:00
22 changed files with 509 additions and 453 deletions

View File

@ -14,17 +14,23 @@ import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate; 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 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.Color;
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;
import djmil.cordacheckers.cordaclient.dao.flow.RequestBody; import djmil.cordacheckers.cordaclient.dao.flow.RequestBody;
import djmil.cordacheckers.cordaclient.dao.flow.ResponseBody; import djmil.cordacheckers.cordaclient.dao.flow.ResponseBody;
import djmil.cordacheckers.cordaclient.dao.flow.arguments.CreateGameProposal; 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.Empty; import djmil.cordacheckers.cordaclient.dao.flow.arguments.Empty;
import djmil.cordacheckers.cordaclient.dao.flow.arguments.GameProposalAction; import djmil.cordacheckers.cordaclient.dao.flow.arguments.GameProposalActionReq;
import djmil.cordacheckers.cordaclient.dao.flow.arguments.GameProposalActionRes;
@Service @Service
public class CordaClient { public class CordaClient {
@ -59,7 +65,7 @@ public class CordaClient {
* @param holdingIdentity * @param holdingIdentity
* @return GameProposals list in JSON form * @return GameProposals list in JSON form
*/ */
public String listGameProposals(HoldingIdentity holdingIdentity) { public List<GameProposal> gameProposalList(HoldingIdentity holdingIdentity) {
final RequestBody requestBody = new RequestBody( final RequestBody requestBody = new RequestBody(
"list-" + UUID.randomUUID(), "list-" + UUID.randomUUID(),
@ -67,21 +73,27 @@ public class CordaClient {
new Empty() new Empty()
); );
final String gameProposalsJsonString = cordaFlowExecute( final GameProposalListRes listFlowResult = cordaFlowExecute(
holdingIdentity, holdingIdentity,
requestBody requestBody,
GameProposalListRes.class
); );
return gameProposalsJsonString; if (listFlowResult.failureStatus() != null) {
System.out.println("GameProposalCreateFlow failed: " + listFlowResult.failureStatus());
throw new RuntimeException("GameProsal: CreateFlow execution has failed");
}
return listFlowResult.successStatus();
} }
public String createGameProposal( public UUID gameProposalCreate(
HoldingIdentity sender, HoldingIdentity sender,
HoldingIdentity receiver, HoldingIdentity receiver,
Color receiverColor, Color receiverColor,
String message String message
) { ) throws JsonMappingException, JsonProcessingException {
final CreateGameProposal createGameProposal = new CreateGameProposal( final GameProposalCreateReq createGameProposal = new GameProposalCreateReq(
receiver.x500Name(), receiver.x500Name(),
receiverColor, receiverColor,
message message
@ -93,21 +105,28 @@ public class CordaClient {
createGameProposal createGameProposal
); );
final String createdGameProposalUuid = cordaFlowExecute( final GameProposalCreateRes createResult = cordaFlowExecute(
sender, sender,
requestBody requestBody,
GameProposalCreateRes.class
); );
return createdGameProposalUuid; if (createResult.failureStatus() != null) {
System.out.println("GameProposalCreateFlow failed: " + createResult.failureStatus());
throw new RuntimeException("GameProsal: CreateFlow execution has failed");
}
return createResult.successStatus();
} }
public String rejectGameProposal( public String gameProposalAction(
HoldingIdentity self, HoldingIdentity self,
String gameProposalUuid UUID gameProposalUuid,
GameProposalActionReq.Action action
) { ) {
final GameProposalAction rejectGameProposal = new GameProposalAction( final GameProposalActionReq rejectGameProposal = new GameProposalActionReq(
gameProposalUuid, gameProposalUuid.toString(),
GameProposalAction.Action.REJECT action
); );
final RequestBody requestBody = new RequestBody( final RequestBody requestBody = new RequestBody(
@ -116,15 +135,21 @@ public class CordaClient {
rejectGameProposal rejectGameProposal
); );
final String createdGameProposalUuid = cordaFlowExecute( final GameProposalActionRes actionResult = cordaFlowExecute(
self, self,
requestBody requestBody,
GameProposalActionRes.class
); );
return createdGameProposalUuid; if (actionResult.failureStatus() != null) {
System.out.println("GameProposalActionFlow failed: " + actionResult.failureStatus());
throw new RuntimeException("GameProsal: ActionFlow execution has failed");
}
return actionResult.successStatus();
} }
private String cordaFlowExecute(HoldingIdentity holdingIdentity, RequestBody requestBody) { private <T> T cordaFlowExecute(HoldingIdentity holdingIdentity, RequestBody requestBody, Class<T> flowResultType) {
try { try {
final String requestBodyJson = this.jsonMapper.writeValueAsString(requestBody); final String requestBodyJson = this.jsonMapper.writeValueAsString(requestBody);
@ -134,15 +159,9 @@ public class CordaClient {
requestBodyJson requestBodyJson
); );
final String flowExecutionResult = cordaFlowPoll(startedFlow); final String flowResult = cordaFlowPoll(startedFlow);
// NOTE: return this.jsonMapper.readValue(flowResult, flowResultType);
// 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) { catch (Exception e) {
throw new RuntimeException("Unable to perform "+requestBody.flowClassName() throw new RuntimeException("Unable to perform "+requestBody.flowClassName()
@ -183,7 +202,7 @@ public class CordaClient {
for (int retry = 0; retry < 6; retry++) { for (int retry = 0; retry < 6; retry++) {
// Give Corda cluster some time to process our request // Give Corda cluster some time to process our request
TimeUnit.SECONDS.sleep(retry*retry +1); // 1 2 5 8 17 33 sec TimeUnit.SECONDS.sleep(retry*retry +2); // 2 3 6 10 18 27 sec
final ResponseEntity<ResponseBody> responce = this.restTemplate.exchange( final ResponseEntity<ResponseBody> responce = this.restTemplate.exchange(
"/flow/" "/flow/"
@ -204,15 +223,16 @@ public class CordaClient {
"CordaClient.cordaFlowPoll: empty getBody()" "CordaClient.cordaFlowPoll: empty getBody()"
); );
if (responseBody.isFlowCompleted() && responseBody.flowResult() != null) {
System.out.println("Completed "+responseBody.flowResult());
return responseBody.flowResult();
} else
if (responseBody.flowError() != null) { if (responseBody.flowError() != null) {
return "Flow execution error: " +responseBody.flowError(); throw new RuntimeException("CordaClient.cordaFlowPoll: flow execution error: "
+responseBody.flowError());
}
if (responseBody.isFlowCompleted() && responseBody.flowResult() != null) {
return responseBody.flowResult();
} }
} }
return "CordaClient.cordaFlowPoll: retry limit"; throw new RuntimeException ("CordaClient.cordaFlowPoll: retry limit");
} }
} }

View File

@ -1,13 +1,15 @@
package djmil.cordacheckers.cordaclient.dao; package djmil.cordacheckers.cordaclient.dao;
import java.util.UUID;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
@JsonDeserialize @JsonDeserialize
public record GameProposal( public record GameProposal(
String sender, String issuer,
String recipient, String acquier,
Color recipientColor, Color acquierColor,
String message, String message,
String id) { UUID id) {
} }

View File

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

View File

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

View File

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

View File

@ -0,0 +1,10 @@
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) {
}

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.GameProposal;
@JsonIgnoreProperties(ignoreUnknown = true)
public record GameProposalListRes(List<GameProposal> successStatus, String failureStatus) {
}

View File

@ -1,6 +1,8 @@
package djmil.cordacheckers.gameproposal; package djmil.cordacheckers.gameproposal;
import java.net.URI; import java.net.URI;
import java.util.List;
import java.util.UUID;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
@ -9,9 +11,12 @@ import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.util.UriComponentsBuilder; 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.CordaClient;
import djmil.cordacheckers.cordaclient.dao.HoldingIdentity; import djmil.cordacheckers.cordaclient.dao.HoldingIdentity;
import djmil.cordacheckers.cordaclient.dao.flow.arguments.CreateGameProposal; import djmil.cordacheckers.cordaclient.dao.flow.arguments.GameProposalCreateReq;
import djmil.cordacheckers.cordaclient.dao.Color; import djmil.cordacheckers.cordaclient.dao.Color;
import djmil.cordacheckers.cordaclient.dao.GameProposal; import djmil.cordacheckers.cordaclient.dao.GameProposal;
import djmil.cordacheckers.user.HoldingIdentityResolver; import djmil.cordacheckers.user.HoldingIdentityResolver;
@ -36,9 +41,9 @@ public class GameProposalController {
public ResponseEntity<String> findAllUnconsumed( public ResponseEntity<String> findAllUnconsumed(
@AuthenticationPrincipal User player @AuthenticationPrincipal User player
) { ) {
String gpList = cordaClient.listGameProposals(player.getHoldingIdentity()); List<GameProposal> gpList = cordaClient.gameProposalList(player.getHoldingIdentity());
return ResponseEntity.ok(gpList); return ResponseEntity.ok(gpList.toString());
} }
// @PostMapping() // @PostMapping()
@ -50,15 +55,16 @@ public class GameProposalController {
@PostMapping() @PostMapping()
public ResponseEntity<Void> createGameProposal( public ResponseEntity<Void> createGameProposal(
@AuthenticationPrincipal User sender, @AuthenticationPrincipal User sender,
@RequestBody CreateGameProposal gpRequest, @RequestBody GameProposalCreateReq gpRequest,
UriComponentsBuilder ucb UriComponentsBuilder ucb
) { ) throws JsonMappingException, JsonProcessingException {
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 Color gpReceiverColor = gpRequest.opponentColor();
String newGameProposalUuid = cordaClient.createGameProposal( // TODO handle expectionns here
UUID newGameProposalUuid = cordaClient.gameProposalCreate(
gpSender, gpSender,
gpReceiver, gpReceiver,
gpReceiverColor, gpReceiverColor,

View File

@ -6,14 +6,19 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonMappingException;
import djmil.cordacheckers.cordaclient.dao.Color; import djmil.cordacheckers.cordaclient.dao.Color;
import djmil.cordacheckers.cordaclient.dao.GameProposal;
import djmil.cordacheckers.cordaclient.dao.VirtualNode; import djmil.cordacheckers.cordaclient.dao.VirtualNode;
import djmil.cordacheckers.cordaclient.dao.flow.arguments.GameProposalActionReq.Action;
import djmil.cordacheckers.user.HoldingIdentityResolver; 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 java.util.List; import java.util.List;
import java.util.UUID;
import javax.naming.InvalidNameException; import javax.naming.InvalidNameException;
@ -34,44 +39,51 @@ public class CordaClientTest {
@Test @Test
void testGameProposalList() throws JsonProcessingException { void testGameProposalList() throws JsonProcessingException {
String resp = cordaClient.listGameProposals( List<GameProposal> gpList = cordaClient.gameProposalList(
holdingIdentityResolver.getByUsername("alice")); holdingIdentityResolver.getByUsername("Alice"));
System.out.println("testListGameProposals "+ resp); System.out.println("testListGameProposals\n"+ gpList);
} }
@Test @Test
void testGemeProposalCreate() { void testGemeProposalCreate() 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 Color gpAcquierColor = Color.WHITE;
final String gpMessage = "GameProposal create test"; final String gpMessage = "GameProposal create test";
final String gpUuid = cordaClient.createGameProposal( final UUID createdGpUuid = cordaClient.gameProposalCreate(
holdingIdentityResolver.getByUsername(gpSender), holdingIdentityResolver.getByUsername(gpIssuer),
holdingIdentityResolver.getByUsername(gpReceiver), holdingIdentityResolver.getByUsername(gpAcquier),
gpReceiverColor, gpAcquierColor,
gpMessage gpMessage
); );
String listResSender = cordaClient.listGameProposals( List<GameProposal> gpListSender = cordaClient.gameProposalList(
holdingIdentityResolver.getByUsername(gpSender)); holdingIdentityResolver.getByUsername(gpIssuer));
String listResReceiver = cordaClient.listGameProposals( assertThat(findByUuid(gpListSender, createdGpUuid)).isNotNull();
holdingIdentityResolver.getByUsername(gpReceiver));
assertThat(listResSender).contains(gpUuid); List<GameProposal> gpListReceiver = cordaClient.gameProposalList(
assertThat(listResReceiver).contains(gpUuid); 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 @Test
void testGemeProposalReject() { void testGemeProposalReject() throws JsonMappingException, JsonProcessingException {
final String gpSender = "alice"; final String gpSender = "alice";
final String gpReceiver = "bob"; final String gpReceiver = "bob";
final Color gpReceiverColor = Color.WHITE; final Color gpReceiverColor = Color.WHITE;
final String gpMessage = "GameProposal create test"; final String gpMessage = "GameProposal REJECT test";
final String gpUuid = cordaClient.createGameProposal( final UUID gpUuid = cordaClient.gameProposalCreate(
holdingIdentityResolver.getByUsername(gpSender), holdingIdentityResolver.getByUsername(gpSender),
holdingIdentityResolver.getByUsername(gpReceiver), holdingIdentityResolver.getByUsername(gpReceiver),
gpReceiverColor, gpReceiverColor,
@ -80,20 +92,45 @@ public class CordaClientTest {
System.out.println("Create GP UUID "+ gpUuid); System.out.println("Create GP UUID "+ gpUuid);
String listResSender = cordaClient.listGameProposals( assertThatThrownBy(() -> {
holdingIdentityResolver.getByUsername(gpSender)); cordaClient.gameProposalAction(
holdingIdentityResolver.getByUsername(gpSender),
gpUuid,
Action.REJECT);
});
String listResReceiver = cordaClient.listGameProposals( final String rejectRes = cordaClient.gameProposalAction(
holdingIdentityResolver.getByUsername(gpReceiver));
assertThat(listResSender).contains(gpUuid);
assertThat(listResReceiver).contains(gpUuid);
final String rejectRes = cordaClient.rejectGameProposal(
holdingIdentityResolver.getByUsername(gpReceiver), holdingIdentityResolver.getByUsername(gpReceiver),
"1ed70601-c79a-486d-b907-8537f317083a" gpUuid,
Action.REJECT
); );
assertThat(rejectRes).isEqualToIgnoringCase("REJECTED"); 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;
} }
} }

View File

@ -3,66 +3,96 @@ 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.GameProposalResolutionState;
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.ledger.utxo.Command; import net.corda.v5.ledger.utxo.Command;
import net.corda.v5.ledger.utxo.StateAndRef;
import net.corda.v5.ledger.utxo.transaction.UtxoLedgerTransaction; import net.corda.v5.ledger.utxo.transaction.UtxoLedgerTransaction;
public class GameProposalContract implements net.corda.v5.ledger.utxo.Contract { public class GameProposalContract implements net.corda.v5.ledger.utxo.Contract {
private final static Logger log = LoggerFactory.getLogger(GameProposalContract.class); private final static Logger log = LoggerFactory.getLogger(GameProposalContract.class);
public static class Create implements Command { } public static enum Action implements Command {
public static class Accept implements Command { } CREATE,
public static class Reject implements Command { } ACCEPT,
public static class Cancel implements Command { } 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: ";
}
@Override @Override
public void verify(UtxoLedgerTransaction trx) { public void verify(UtxoLedgerTransaction trx) {
log.info("GameProposalContract.verify() called"); log.info("GameProposalContract.verify() called");
requireThat(trx.getCommands().size() == 1, REQUIRE_SINGLE_COMMAND); requireThat(trx.getCommands().size() == 1, REQUIRE_SINGLE_COMMAND);
Command command = trx.getCommands().get(0);
if (command instanceof Create) { switch ((Action)(trx.getCommands().get(0))) {
requireThat(trx.getInputContractStates().isEmpty(), CREATE_INPUT_STATE); case CREATE: {
requireThat(trx.getOutputContractStates().size() == 1, CREATE_OUTPUT_STATE); 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);
requireThat(outputState.getRecipientColor() != null, NON_NULL_RECIPIENT_COLOR); GameProposalState outputState = trx.getOutputStates(GameProposalState.class).get(0);
} else requireThat(outputState != null, CREATE_OUTPUT_STATE);
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);
GameProposalState inputState = trx.getInputStates(GameProposalState.class).get(0); requireThat(outputState.getRecipientColor() != null, NON_NULL_RECIPIENT_COLOR);
requireThat(inputState != null, REJECT_INPUT_STATE); break; }
GameProposalResolutionState outputState = trx.getOutputStates(GameProposalResolutionState.class).get(0);
requireThat(outputState != null, REJECT_OUTPUT_STATE);
requireThat(outputState.outcome == GameProposalResolutionState.Resolution.REJECT, REJECT_OUTPUT_OUTCOME); case ACCEPT:
} else throw new CordaRuntimeException("Unimplemented!");
if (command instanceof Cancel) { //break;
requireThat(trx.getInputContractStates().size() == 1, CANCEL_INPUT_STATE);
requireThat(trx.getOutputContractStates().size() == 1, CANCEL_OUTPUT_STATE);
GameProposalState inputState = trx.getInputStates(GameProposalState.class).get(0); case REJECT:
requireThat(inputState != null, CANCEL_INPUT_STATE); requireThat(trx.getInputContractStates().size() == 1, REJECT_INPUT_STATE);
requireThat(trx.getOutputContractStates().isEmpty(), REJECT_OUTPUT_STATE);
GameProposalResolutionState outputState = trx.getOutputStates(GameProposalResolutionState.class).get(0); requireThat(trx.getInputStates(GameProposalState.class).get(0) != null, REJECT_INPUT_STATE);
requireThat(outputState != null, CANCEL_OUTPUT_STATE); break;
requireThat(outputState.outcome == GameProposalResolutionState.Resolution.CANCEL, CANCEL_OUTPUT_OUTCOME); case CANCEL:
} else { requireThat(trx.getInputContractStates().size() == 1, CANCEL_INPUT_STATE);
throw new CordaRuntimeException(UNKNOWN_COMMAND); requireThat(trx.getOutputContractStates().isEmpty(), CANCEL_OUTPUT_STATE);
requireThat(trx.getInputStates(GameProposalState.class).get(0) != null, CANCEL_INPUT_STATE);
break;
default:
throw new CordaRuntimeException(UNKNOWN_COMMAND);
} }
} }
@ -80,10 +110,8 @@ 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 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_INPUT_STATE = "Reject command should have exactly one GameProposal input state";
static final String REJECT_OUTPUT_STATE = "Reject command should have exactly one GameProposalResolution output states"; static final String REJECT_OUTPUT_STATE = "Reject command should have no 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_INPUT_STATE = "Cancel command should have exactly one GameProposal input state";
static final String CANCEL_OUTPUT_STATE = "Cancel command should have exactly one GameProposalResolution output states"; static final String CANCEL_OUTPUT_STATE = "Cancel command should have no output states";
static final String CANCEL_OUTPUT_OUTCOME = "Cancel output state should have Resolution value set to CANCEL";
} }

View File

@ -1,63 +0,0 @@
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());
}
}
}

View File

@ -6,7 +6,6 @@ import java.util.UUID;
import djmil.cordacheckers.contracts.GameProposalContract; import djmil.cordacheckers.contracts.GameProposalContract;
import net.corda.v5.base.annotations.ConstructorForDeserialization; 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.base.types.MemberX500Name;
import net.corda.v5.ledger.utxo.BelongsToContract; import net.corda.v5.ledger.utxo.BelongsToContract;
import net.corda.v5.ledger.utxo.ContractState; import net.corda.v5.ledger.utxo.ContractState;
@ -14,46 +13,40 @@ import net.corda.v5.ledger.utxo.ContractState;
@BelongsToContract(GameProposalContract.class) @BelongsToContract(GameProposalContract.class)
public class GameProposalState implements ContractState { public class GameProposalState implements ContractState {
@CordaSerializable MemberX500Name issuer;
public enum Color { MemberX500Name acquier;
WHITE, Piece.Color recipientColor;
BLACK, String message;
} UUID id;
List<PublicKey> participants;
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 // Allows serialisation and to use a specified UUID
@ConstructorForDeserialization @ConstructorForDeserialization
public GameProposalState( public GameProposalState(
MemberX500Name sender, MemberX500Name issuer,
MemberX500Name recipient, MemberX500Name acquier,
Color recipientColor, Piece.Color recipientColor,
String message, String message,
UUID id, UUID id,
List<PublicKey> participants List<PublicKey> participants
) { ) {
this.sender = sender; this.issuer = issuer;
this.recipient = recipient; this.acquier = acquier;
this.recipientColor = recipientColor; this.recipientColor = recipientColor;
this.message = message; this.message = message;
this.id = id; this.id = id;
this.participants = participants; this.participants = participants;
} }
public MemberX500Name getSender() { public MemberX500Name getIssuer() {
return sender; return issuer;
} }
public MemberX500Name getRecipient() { public MemberX500Name getAcquier() {
return recipient; return acquier;
} }
public Color getRecipientColor() { public Piece.Color getRecipientColor() {
return recipientColor; return recipientColor;
} }

View File

@ -0,0 +1,24 @@
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;
}

View File

@ -1,18 +1,28 @@
package djmil.cordacheckers; package djmil.cordacheckers;
import net.corda.v5.application.marshalling.JsonMarshallingService; import net.corda.v5.application.marshalling.JsonMarshallingService;
import net.corda.v5.crypto.SecureHash;
public class FlowResult { public class FlowResult {
public final Object successStatus; public final Object successStatus;
public final String transactionId;
public final String failureStatus; public final String failureStatus;
public FlowResult(Object success) { public FlowResult(Object success) {
this.successStatus = 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; this.failureStatus = null;
} }
public FlowResult(Exception exception) { public FlowResult(Exception exception) {
this.successStatus = null; this.successStatus = null;
this.transactionId = null;
this.failureStatus = exception.getMessage(); this.failureStatus = exception.getMessage();
} }

View File

@ -1,8 +1,6 @@
package djmil.cordacheckers.gameproposal; package djmil.cordacheckers.gameproposal;
import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.UUID; import java.util.UUID;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -10,21 +8,15 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import djmil.cordacheckers.FlowResult; import djmil.cordacheckers.FlowResult;
import djmil.cordacheckers.contracts.GameProposalContract;
import djmil.cordacheckers.states.GameProposalResolutionState;
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;
import net.corda.v5.application.flows.CordaInject; import net.corda.v5.application.flows.CordaInject;
import net.corda.v5.application.flows.InitiatingFlow; 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.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.annotations.Suspendable;
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.crypto.SecureHash;
import net.corda.v5.ledger.utxo.Command;
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;
import net.corda.v5.ledger.utxo.transaction.UtxoSignedTransaction; import net.corda.v5.ledger.utxo.transaction.UtxoSignedTransaction;
@ -33,7 +25,8 @@ import net.corda.v5.ledger.utxo.transaction.UtxoTransactionBuilder;
import java.time.Duration; import java.time.Duration;
import java.time.Instant; import java.time.Instant;
@InitiatingFlow(protocol = "game-proposal-action") import static djmil.cordacheckers.contracts.GameProposalContract.Action;
public class ActionFlow implements ClientStartableFlow { public class ActionFlow implements ClientStartableFlow {
private final static Logger log = LoggerFactory.getLogger(CreateFlow.class); private final static Logger log = LoggerFactory.getLogger(CreateFlow.class);
@ -45,33 +38,24 @@ public class ActionFlow implements ClientStartableFlow {
public UtxoLedgerService ledgerService; public UtxoLedgerService ledgerService;
@CordaInject @CordaInject
public FlowMessaging flowMessaging; public FlowEngine flowEngine;
@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 @Override
@Suspendable @Suspendable
public String call(ClientRequestBody requestBody) { public String call(ClientRequestBody requestBody) {
try { try {
ActionFlowArgs args = requestBody.getRequestBodyAs(jsonMarshallingService, ActionFlowArgs.class); final ActionFlowArgs args = requestBody.getRequestBodyAs(jsonMarshallingService, ActionFlowArgs.class);
final Action action = args.getAction();
StateAndRef<GameProposalState> inputState = findUnconsumedGameProposalState(args.getGameProposalUuid()); final StateAndRef<GameProposalState> inputState = findUnconsumedGameProposalState(args.getGameProposalUuid());
GameProposalResolutionState outputState = new GameProposalResolutionState(
args.getAction(),
inputState.getState().getContractState()
);
String trxResult = doTrunsaction(inputState, outputState); final UtxoSignedTransaction trx = prepareSignedTransaction(action, inputState);
return new FlowResult(trxResult).toJsonEncodedString(jsonMarshallingService); final SecureHash trxId = this.flowEngine
.subFlow( new CommitSubFlow(trx, action.getReceiver(inputState)) );
return new FlowResult(action+"ED", trxId) // REJECT+ED
.toJsonEncodedString(jsonMarshallingService);
} }
catch (Exception e) { catch (Exception e) {
log.warn("GameProposalAction flow failed to process utxo request body " + requestBody + log.warn("GameProposalAction flow failed to process utxo request body " + requestBody +
@ -83,7 +67,7 @@ public class ActionFlow implements ClientStartableFlow {
@Suspendable @Suspendable
private StateAndRef<GameProposalState> findUnconsumedGameProposalState (UUID gameProposalUuid) { private StateAndRef<GameProposalState> findUnconsumedGameProposalState (UUID gameProposalUuid) {
/* /*
* Get list of all unconsumed aka 'actuve' GameProposalStates, then filter by UUID. * Get list of all unconsumed aka 'active' GameProposalStates, then filter by UUID.
* Note, this is an inefficient way to perform this operation if there are a large * 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.
*/ */
@ -102,26 +86,24 @@ public class ActionFlow implements ClientStartableFlow {
} }
@Suspendable @Suspendable
private String doTrunsaction(StateAndRef<GameProposalState> inputState, GameProposalResolutionState outputState) { private UtxoSignedTransaction prepareSignedTransaction(
UtxoTransactionBuilder txBuilder = ledgerService.createTransactionBuilder() Action action,
StateAndRef<GameProposalState> inputState
) {
UtxoTransactionBuilder trxBuilder = ledgerService.createTransactionBuilder()
.setNotary(inputState.getState().getNotaryName()) .setNotary(inputState.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(inputState.getRef())
.addOutputState(outputState) .addCommand(action)
.addCommand(resoultion2command.get(outputState.outcome)) .addSignatories(inputState.getState().getContractState().getParticipants());
.addSignatories(outputState.getParticipants());
UtxoSignedTransaction signedTransaction = txBuilder.toSignedTransaction(); if (action == Action.ACCEPT) {
// TODO
// .addOutputState(outputState)
throw new RuntimeException(action +" unimplemented");
}
FlowSession session = flowMessaging.initiateFlow( return trxBuilder.toSignedTransaction();
outputState.getRecipient(inputState.getState().getContractState())
);
List<FlowSession> sessionsList = Arrays.asList(session);
ledgerService.finalize(signedTransaction, sessionsList);
return outputState.getOutcome()+"ED"; // REJECT+ED
} }
} }

View File

@ -2,8 +2,7 @@ package djmil.cordacheckers.gameproposal;
import java.util.UUID; import java.util.UUID;
import djmil.cordacheckers.states.GameProposalResolutionState; import djmil.cordacheckers.contracts.GameProposalContract;
import djmil.cordacheckers.states.GameProposalResolutionState.Resolution;
public class ActionFlowArgs { public class ActionFlowArgs {
private UUID gameProposalUuid; private UUID gameProposalUuid;
@ -15,8 +14,8 @@ public class ActionFlowArgs {
this.action = null; this.action = null;
} }
public Resolution getAction() { public GameProposalContract.Action getAction() {
return GameProposalResolutionState.Resolution.valueOf(this.action); return GameProposalContract.Action.valueOf(this.action);
} }
public UUID getGameProposalUuid() { public UUID getGameProposalUuid() {

View File

@ -1,83 +0,0 @@
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;
}
}

View File

@ -0,0 +1,95 @@
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);
}
}
}

View File

@ -0,0 +1,60 @@
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;
}
}

View File

@ -4,8 +4,8 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import djmil.cordacheckers.FlowResult; import djmil.cordacheckers.FlowResult;
import djmil.cordacheckers.contracts.GameProposalContract;
import djmil.cordacheckers.states.GameProposalState; import djmil.cordacheckers.states.GameProposalState;
import djmil.cordacheckers.states.Piece;
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;
import net.corda.v5.application.flows.CordaInject; import net.corda.v5.application.flows.CordaInject;
@ -13,26 +13,23 @@ import net.corda.v5.application.flows.FlowEngine;
import net.corda.v5.application.flows.InitiatingFlow; 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.application.messaging.FlowMessaging;
import net.corda.v5.application.messaging.FlowSession;
import net.corda.v5.base.annotations.Suspendable; import net.corda.v5.base.annotations.Suspendable;
import net.corda.v5.base.types.MemberX500Name; 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.common.NotaryLookup;
import net.corda.v5.ledger.utxo.UtxoLedgerService; import net.corda.v5.ledger.utxo.UtxoLedgerService;
import net.corda.v5.ledger.utxo.transaction.UtxoSignedTransaction; 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.MemberInfo;
import net.corda.v5.membership.NotaryInfo; import net.corda.v5.membership.NotaryInfo;
import static java.util.Objects.requireNonNull; import static java.util.Objects.requireNonNull;
import static djmil.cordacheckers.contracts.GameProposalContract.Action;
import java.time.Duration; import java.time.Duration;
import java.time.Instant; import java.time.Instant;
import java.util.Arrays; import java.util.Arrays;
import java.util.List;
import java.util.UUID; import java.util.UUID;
@InitiatingFlow(protocol = "game-proposal-create")
public class CreateFlow implements ClientStartableFlow{ public class CreateFlow implements ClientStartableFlow{
private final static Logger log = LoggerFactory.getLogger(CreateFlow.class); private final static Logger log = LoggerFactory.getLogger(CreateFlow.class);
@ -52,19 +49,22 @@ public class CreateFlow implements ClientStartableFlow{
@CordaInject @CordaInject
public FlowEngine flowEngine; public FlowEngine flowEngine;
@CordaInject
public FlowMessaging flowMessaging;
@Suspendable @Suspendable
@Override @Override
public String call(ClientRequestBody requestBody) { public String call(ClientRequestBody requestBody) {
try { try {
log.info("flow: Create Game Proposal"); log.info("flow: Create Game Proposal");
final Action actino = Action.CREATE;
GameProposalState gameProposal = buildGameProposalStateFrom(requestBody); final GameProposalState newGameProposal = buildGameProposalStateFrom(requestBody);
String trxResult = doTrunsaction(gameProposal);
final UtxoSignedTransaction trx = prepareSignedTransaction(newGameProposal);
return new FlowResult(trxResult).toJsonEncodedString(jsonMarshallingService); final SecureHash trxId = this.flowEngine
.subFlow(new CommitSubFlow(trx, actino.getRespondent(newGameProposal)));
return new FlowResult(newGameProposal.getId(), trxId)
.toJsonEncodedString(jsonMarshallingService);
} }
catch (Exception e) { catch (Exception e) {
log.warn("CreateGameProposal flow failed to process utxo request body " + requestBody + 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) { private GameProposalState buildGameProposalStateFrom(ClientRequestBody requestBody) {
CreateFlowArgs args = requestBody.getRequestBodyAs(jsonMarshallingService, CreateFlowArgs.class); CreateFlowArgs args = requestBody.getRequestBodyAs(jsonMarshallingService, CreateFlowArgs.class);
GameProposalState.Color opponentColor = GameProposalState.Color.valueOf(args.opponentColor); Piece.Color opponentColor = Piece.Color.valueOf(args.opponentColor);
if (opponentColor == null) { if (opponentColor == null) {
throw new RuntimeException("Allowed values for opponentColor are: " throw new RuntimeException("Allowed values for opponentColor are: "
+ GameProposalState.Color.WHITE.name() +", " + GameProposalState.Color.BLACK.name() + Piece.Color.WHITE.name() +", " + Piece.Color.BLACK.name()
+ ". Actual value was: " + args.opponentColor); + ". Actual value was: " + args.opponentColor);
} }
@ -101,24 +101,15 @@ public class CreateFlow implements ClientStartableFlow{
} }
@Suspendable @Suspendable
private String doTrunsaction(GameProposalState gameProposal) { private UtxoSignedTransaction prepareSignedTransaction(GameProposalState outputGameProposalState) {
NotaryInfo notary = notaryLookup.getNotaryServices().iterator().next(); final NotaryInfo notary = notaryLookup.getNotaryServices().iterator().next();
UtxoTransactionBuilder txBuilder = ledgerService.createTransactionBuilder() return ledgerService.createTransactionBuilder()
.setNotary(notary.getName()) .setNotary(notary.getName())
.setTimeWindowBetween(Instant.now(), Instant.now().plusMillis(Duration.ofDays(1).toMillis())) .setTimeWindowBetween(Instant.now(), Instant.now().plusMillis(Duration.ofDays(1).toMillis()))
.addOutputState(gameProposal) .addOutputState(outputGameProposalState)
.addCommand(new GameProposalContract.Create()) .addCommand(Action.CREATE)
.addSignatories(gameProposal.getParticipants()); .addSignatories(outputGameProposalState.getParticipants())
.toSignedTransaction();
UtxoSignedTransaction signedTransaction = txBuilder.toSignedTransaction();
FlowSession session = flowMessaging.initiateFlow(gameProposal.getRecipient());
List<FlowSession> sessionsList = Arrays.asList(session);
ledgerService.finalize(signedTransaction, sessionsList);
return gameProposal.id.toString();
} }
} }

View File

@ -1,72 +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.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;
}
}

View File

@ -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. // and a UUID. It is possible to create custom serializers for the JsonMarshallingService in the future.
public class ListItem { public class ListItem {
public final String sender; public final String issuer;
public final String recipient; public final String acquier;
public final String recipientColor; public final String acquierColor;
public final String message; public final String message;
public final UUID id; public final UUID id;
// Serialisation service requires a default constructor // Serialisation service requires a default constructor
public ListItem() { public ListItem() {
this.sender = null; this.issuer = null;
this.recipient = null; this.acquier = null;
this.recipientColor = null; this.acquierColor = null;
this.message = null; this.message = null;
this.id = null; this.id = null;
} }
public ListItem(GameProposalState state) { public ListItem(GameProposalState state) {
this.sender = state.getSender().getCommonName(); this.issuer = state.getIssuer().getCommonName();
this.recipient = state.getRecipient().getCommonName(); this.acquier = state.getAcquier().getCommonName();
this.recipientColor = state.getRecipientColor().name(); this.acquierColor = state.getRecipientColor().name();
this.message = state.getMessage(); this.message = state.getMessage();
this.id = state.getId(); this.id = state.getId();
} }
} }