Compare commits
5 Commits
7df57cb4d2
...
e235ecb942
Author | SHA1 | Date | |
---|---|---|---|
e235ecb942 | |||
159bcd706e | |||
5cc579230f | |||
c1dbb3d213 | |||
a9b70b963c |
@ -14,17 +14,23 @@ 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.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.GameProposalAction;
|
||||
import djmil.cordacheckers.cordaclient.dao.flow.arguments.GameProposalActionReq;
|
||||
import djmil.cordacheckers.cordaclient.dao.flow.arguments.GameProposalActionRes;
|
||||
|
||||
@Service
|
||||
public class CordaClient {
|
||||
@ -59,7 +65,7 @@ public class CordaClient {
|
||||
* @param holdingIdentity
|
||||
* @return GameProposals list in JSON form
|
||||
*/
|
||||
public String listGameProposals(HoldingIdentity holdingIdentity) {
|
||||
public List<GameProposal> gameProposalList(HoldingIdentity holdingIdentity) {
|
||||
|
||||
final RequestBody requestBody = new RequestBody(
|
||||
"list-" + UUID.randomUUID(),
|
||||
@ -67,21 +73,27 @@ public class CordaClient {
|
||||
new Empty()
|
||||
);
|
||||
|
||||
final String gameProposalsJsonString = cordaFlowExecute(
|
||||
final GameProposalListRes listFlowResult = cordaFlowExecute(
|
||||
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");
|
||||
}
|
||||
|
||||
public String createGameProposal(
|
||||
return listFlowResult.successStatus();
|
||||
}
|
||||
|
||||
public UUID gameProposalCreate(
|
||||
HoldingIdentity sender,
|
||||
HoldingIdentity receiver,
|
||||
Color receiverColor,
|
||||
String message
|
||||
) {
|
||||
final CreateGameProposal createGameProposal = new CreateGameProposal(
|
||||
) throws JsonMappingException, JsonProcessingException {
|
||||
final GameProposalCreateReq createGameProposal = new GameProposalCreateReq(
|
||||
receiver.x500Name(),
|
||||
receiverColor,
|
||||
message
|
||||
@ -93,21 +105,28 @@ public class CordaClient {
|
||||
createGameProposal
|
||||
);
|
||||
|
||||
final String createdGameProposalUuid = cordaFlowExecute(
|
||||
final GameProposalCreateRes createResult = cordaFlowExecute(
|
||||
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");
|
||||
}
|
||||
|
||||
public String rejectGameProposal(
|
||||
return createResult.successStatus();
|
||||
}
|
||||
|
||||
public String gameProposalAction(
|
||||
HoldingIdentity self,
|
||||
String gameProposalUuid
|
||||
UUID gameProposalUuid,
|
||||
GameProposalActionReq.Action action
|
||||
) {
|
||||
final GameProposalAction rejectGameProposal = new GameProposalAction(
|
||||
gameProposalUuid,
|
||||
GameProposalAction.Action.REJECT
|
||||
final GameProposalActionReq rejectGameProposal = new GameProposalActionReq(
|
||||
gameProposalUuid.toString(),
|
||||
action
|
||||
);
|
||||
|
||||
final RequestBody requestBody = new RequestBody(
|
||||
@ -116,15 +135,21 @@ public class CordaClient {
|
||||
rejectGameProposal
|
||||
);
|
||||
|
||||
final String createdGameProposalUuid = cordaFlowExecute(
|
||||
final GameProposalActionRes actionResult = cordaFlowExecute(
|
||||
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");
|
||||
}
|
||||
|
||||
private String cordaFlowExecute(HoldingIdentity holdingIdentity, RequestBody requestBody) {
|
||||
return actionResult.successStatus();
|
||||
}
|
||||
|
||||
private <T> T cordaFlowExecute(HoldingIdentity holdingIdentity, RequestBody requestBody, Class<T> flowResultType) {
|
||||
|
||||
try {
|
||||
final String requestBodyJson = this.jsonMapper.writeValueAsString(requestBody);
|
||||
@ -134,15 +159,9 @@ public class CordaClient {
|
||||
requestBodyJson
|
||||
);
|
||||
|
||||
final String flowExecutionResult = cordaFlowPoll(startedFlow);
|
||||
final String flowResult = cordaFlowPoll(startedFlow);
|
||||
|
||||
// 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;
|
||||
return this.jsonMapper.readValue(flowResult, flowResultType);
|
||||
}
|
||||
catch (Exception e) {
|
||||
throw new RuntimeException("Unable to perform "+requestBody.flowClassName()
|
||||
@ -183,7 +202,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 +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(
|
||||
"/flow/"
|
||||
@ -204,15 +223,16 @@ public class CordaClient {
|
||||
"CordaClient.cordaFlowPoll: empty getBody()"
|
||||
);
|
||||
|
||||
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: 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");
|
||||
}
|
||||
}
|
||||
|
@ -1,13 +1,15 @@
|
||||
package djmil.cordacheckers.cordaclient.dao;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
|
||||
@JsonDeserialize
|
||||
public record GameProposal(
|
||||
String sender,
|
||||
String recipient,
|
||||
Color recipientColor,
|
||||
String issuer,
|
||||
String acquier,
|
||||
Color acquierColor,
|
||||
String message,
|
||||
String id) {
|
||||
UUID id) {
|
||||
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
package djmil.cordacheckers.cordaclient.dao.flow.arguments;
|
||||
|
||||
public record GameProposalAction(String gameProposalUuid, Action action) {
|
||||
public record GameProposalActionReq(String gameProposalUuid, Action action) {
|
||||
public enum Action {
|
||||
ACCEPT,
|
||||
REJECT,
|
@ -0,0 +1,5 @@
|
||||
package djmil.cordacheckers.cordaclient.dao.flow.arguments;
|
||||
|
||||
public record GameProposalActionRes(String successStatus, String transactionId, String failureStatus) {
|
||||
|
||||
}
|
@ -2,6 +2,6 @@ package djmil.cordacheckers.cordaclient.dao.flow.arguments;
|
||||
|
||||
import djmil.cordacheckers.cordaclient.dao.Color;
|
||||
|
||||
public record CreateGameProposal(String opponentName, Color opponentColor, String message) {
|
||||
public record GameProposalCreateReq(String opponentName, Color opponentColor, String message) {
|
||||
|
||||
}
|
@ -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) {
|
||||
|
||||
}
|
@ -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) {
|
||||
|
||||
}
|
@ -1,6 +1,8 @@
|
||||
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;
|
||||
@ -9,9 +11,12 @@ 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.CreateGameProposal;
|
||||
import djmil.cordacheckers.cordaclient.dao.flow.arguments.GameProposalCreateReq;
|
||||
import djmil.cordacheckers.cordaclient.dao.Color;
|
||||
import djmil.cordacheckers.cordaclient.dao.GameProposal;
|
||||
import djmil.cordacheckers.user.HoldingIdentityResolver;
|
||||
@ -36,9 +41,9 @@ public class GameProposalController {
|
||||
public ResponseEntity<String> findAllUnconsumed(
|
||||
@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()
|
||||
@ -50,15 +55,16 @@ public class GameProposalController {
|
||||
@PostMapping()
|
||||
public ResponseEntity<Void> createGameProposal(
|
||||
@AuthenticationPrincipal User sender,
|
||||
@RequestBody CreateGameProposal gpRequest,
|
||||
@RequestBody GameProposalCreateReq 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();
|
||||
|
||||
String newGameProposalUuid = cordaClient.createGameProposal(
|
||||
// TODO handle expectionns here
|
||||
UUID newGameProposalUuid = cordaClient.gameProposalCreate(
|
||||
gpSender,
|
||||
gpReceiver,
|
||||
gpReceiverColor,
|
||||
|
@ -6,14 +6,19 @@ 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;
|
||||
|
||||
@ -34,44 +39,51 @@ public class CordaClientTest {
|
||||
|
||||
@Test
|
||||
void testGameProposalList() throws JsonProcessingException {
|
||||
String resp = cordaClient.listGameProposals(
|
||||
holdingIdentityResolver.getByUsername("alice"));
|
||||
List<GameProposal> gpList = cordaClient.gameProposalList(
|
||||
holdingIdentityResolver.getByUsername("Alice"));
|
||||
|
||||
System.out.println("testListGameProposals "+ resp);
|
||||
System.out.println("testListGameProposals\n"+ gpList);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGemeProposalCreate() {
|
||||
final String gpSender = "alice";
|
||||
final String gpReceiver = "bob";
|
||||
final Color gpReceiverColor = Color.WHITE;
|
||||
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 String gpUuid = cordaClient.createGameProposal(
|
||||
holdingIdentityResolver.getByUsername(gpSender),
|
||||
holdingIdentityResolver.getByUsername(gpReceiver),
|
||||
gpReceiverColor,
|
||||
final UUID createdGpUuid = cordaClient.gameProposalCreate(
|
||||
holdingIdentityResolver.getByUsername(gpIssuer),
|
||||
holdingIdentityResolver.getByUsername(gpAcquier),
|
||||
gpAcquierColor,
|
||||
gpMessage
|
||||
);
|
||||
|
||||
String listResSender = cordaClient.listGameProposals(
|
||||
holdingIdentityResolver.getByUsername(gpSender));
|
||||
List<GameProposal> gpListSender = cordaClient.gameProposalList(
|
||||
holdingIdentityResolver.getByUsername(gpIssuer));
|
||||
|
||||
String listResReceiver = cordaClient.listGameProposals(
|
||||
holdingIdentityResolver.getByUsername(gpReceiver));
|
||||
assertThat(findByUuid(gpListSender, createdGpUuid)).isNotNull();
|
||||
|
||||
assertThat(listResSender).contains(gpUuid);
|
||||
assertThat(listResReceiver).contains(gpUuid);
|
||||
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() {
|
||||
void testGemeProposalReject() throws JsonMappingException, JsonProcessingException {
|
||||
final String gpSender = "alice";
|
||||
final String gpReceiver = "bob";
|
||||
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(gpReceiver),
|
||||
gpReceiverColor,
|
||||
@ -80,20 +92,45 @@ public class CordaClientTest {
|
||||
|
||||
System.out.println("Create GP UUID "+ gpUuid);
|
||||
|
||||
String listResSender = cordaClient.listGameProposals(
|
||||
holdingIdentityResolver.getByUsername(gpSender));
|
||||
assertThatThrownBy(() -> {
|
||||
cordaClient.gameProposalAction(
|
||||
holdingIdentityResolver.getByUsername(gpSender),
|
||||
gpUuid,
|
||||
Action.REJECT);
|
||||
});
|
||||
|
||||
String listResReceiver = cordaClient.listGameProposals(
|
||||
holdingIdentityResolver.getByUsername(gpReceiver));
|
||||
|
||||
assertThat(listResSender).contains(gpUuid);
|
||||
assertThat(listResReceiver).contains(gpUuid);
|
||||
|
||||
final String rejectRes = cordaClient.rejectGameProposal(
|
||||
final String rejectRes = cordaClient.gameProposalAction(
|
||||
holdingIdentityResolver.getByUsername(gpReceiver),
|
||||
"1ed70601-c79a-486d-b907-8537f317083a"
|
||||
gpUuid,
|
||||
Action.REJECT
|
||||
);
|
||||
|
||||
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,29 +3,69 @@ 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 class Create implements Command { }
|
||||
public static class Accept implements Command { }
|
||||
public static class Reject implements Command { }
|
||||
public static class Cancel implements Command { }
|
||||
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: ";
|
||||
}
|
||||
|
||||
@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);
|
||||
|
||||
if (command instanceof Create) {
|
||||
switch ((Action)(trx.getCommands().get(0))) {
|
||||
case CREATE: {
|
||||
requireThat(trx.getInputContractStates().isEmpty(), CREATE_INPUT_STATE);
|
||||
requireThat(trx.getOutputContractStates().size() == 1, CREATE_OUTPUT_STATE);
|
||||
|
||||
@ -33,35 +73,25 @@ public class GameProposalContract implements net.corda.v5.ledger.utxo.Contract {
|
||||
requireThat(outputState != null, CREATE_OUTPUT_STATE);
|
||||
|
||||
requireThat(outputState.getRecipientColor() != null, NON_NULL_RECIPIENT_COLOR);
|
||||
} else
|
||||
if (command instanceof Accept) {
|
||||
// TODO outputState -> Game
|
||||
break; }
|
||||
|
||||
case ACCEPT:
|
||||
throw new CordaRuntimeException("Unimplemented!");
|
||||
} else
|
||||
if (command instanceof Reject) {
|
||||
//break;
|
||||
|
||||
case REJECT:
|
||||
requireThat(trx.getInputContractStates().size() == 1, REJECT_INPUT_STATE);
|
||||
requireThat(trx.getOutputContractStates().size() == 1, REJECT_OUTPUT_STATE);
|
||||
requireThat(trx.getOutputContractStates().isEmpty(), REJECT_OUTPUT_STATE);
|
||||
requireThat(trx.getInputStates(GameProposalState.class).get(0) != null, REJECT_INPUT_STATE);
|
||||
break;
|
||||
|
||||
GameProposalState inputState = trx.getInputStates(GameProposalState.class).get(0);
|
||||
requireThat(inputState != null, REJECT_INPUT_STATE);
|
||||
|
||||
GameProposalResolutionState outputState = trx.getOutputStates(GameProposalResolutionState.class).get(0);
|
||||
requireThat(outputState != null, REJECT_OUTPUT_STATE);
|
||||
|
||||
requireThat(outputState.outcome == GameProposalResolutionState.Resolution.REJECT, REJECT_OUTPUT_OUTCOME);
|
||||
} else
|
||||
if (command instanceof Cancel) {
|
||||
case CANCEL:
|
||||
requireThat(trx.getInputContractStates().size() == 1, CANCEL_INPUT_STATE);
|
||||
requireThat(trx.getOutputContractStates().size() == 1, CANCEL_OUTPUT_STATE);
|
||||
requireThat(trx.getOutputContractStates().isEmpty(), CANCEL_OUTPUT_STATE);
|
||||
requireThat(trx.getInputStates(GameProposalState.class).get(0) != null, CANCEL_INPUT_STATE);
|
||||
break;
|
||||
|
||||
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 {
|
||||
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 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_OUTCOME = "Reject output state should have Resolution value set to REJECT";
|
||||
static final String REJECT_OUTPUT_STATE = "Reject command should have no output states";
|
||||
|
||||
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_OUTCOME = "Cancel output state should have Resolution value set to CANCEL";
|
||||
static final String CANCEL_OUTPUT_STATE = "Cancel command should have no output states";
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -6,7 +6,6 @@ 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;
|
||||
@ -14,46 +13,40 @@ import net.corda.v5.ledger.utxo.ContractState;
|
||||
@BelongsToContract(GameProposalContract.class)
|
||||
public class GameProposalState implements ContractState {
|
||||
|
||||
@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;
|
||||
MemberX500Name issuer;
|
||||
MemberX500Name acquier;
|
||||
Piece.Color recipientColor;
|
||||
String message;
|
||||
UUID id;
|
||||
List<PublicKey> participants;
|
||||
|
||||
// Allows serialisation and to use a specified UUID
|
||||
@ConstructorForDeserialization
|
||||
public GameProposalState(
|
||||
MemberX500Name sender,
|
||||
MemberX500Name recipient,
|
||||
Color recipientColor,
|
||||
MemberX500Name issuer,
|
||||
MemberX500Name acquier,
|
||||
Piece.Color recipientColor,
|
||||
String message,
|
||||
UUID id,
|
||||
List<PublicKey> participants
|
||||
) {
|
||||
this.sender = sender;
|
||||
this.recipient = recipient;
|
||||
this.issuer = issuer;
|
||||
this.acquier = acquier;
|
||||
this.recipientColor = recipientColor;
|
||||
this.message = message;
|
||||
this.id = id;
|
||||
this.participants = participants;
|
||||
}
|
||||
|
||||
public MemberX500Name getSender() {
|
||||
return sender;
|
||||
public MemberX500Name getIssuer() {
|
||||
return issuer;
|
||||
}
|
||||
|
||||
public MemberX500Name getRecipient() {
|
||||
return recipient;
|
||||
public MemberX500Name getAcquier() {
|
||||
return acquier;
|
||||
}
|
||||
|
||||
public Color getRecipientColor() {
|
||||
public Piece.Color getRecipientColor() {
|
||||
return recipientColor;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
||||
}
|
@ -1,18 +1,28 @@
|
||||
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,8 +1,6 @@
|
||||
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;
|
||||
|
||||
@ -10,21 +8,15 @@ 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.InitiatingFlow;
|
||||
import net.corda.v5.application.flows.FlowEngine;
|
||||
import net.corda.v5.application.marshalling.JsonMarshallingService;
|
||||
import net.corda.v5.application.membership.MemberLookup;
|
||||
import net.corda.v5.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.base.types.MemberX500Name;
|
||||
import net.corda.v5.ledger.utxo.Command;
|
||||
import net.corda.v5.crypto.SecureHash;
|
||||
import net.corda.v5.ledger.utxo.StateAndRef;
|
||||
import net.corda.v5.ledger.utxo.UtxoLedgerService;
|
||||
import net.corda.v5.ledger.utxo.transaction.UtxoSignedTransaction;
|
||||
@ -33,7 +25,8 @@ import net.corda.v5.ledger.utxo.transaction.UtxoTransactionBuilder;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
|
||||
@InitiatingFlow(protocol = "game-proposal-action")
|
||||
import static djmil.cordacheckers.contracts.GameProposalContract.Action;
|
||||
|
||||
public class ActionFlow implements ClientStartableFlow {
|
||||
|
||||
private final static Logger log = LoggerFactory.getLogger(CreateFlow.class);
|
||||
@ -45,33 +38,24 @@ public class ActionFlow implements ClientStartableFlow {
|
||||
public UtxoLedgerService ledgerService;
|
||||
|
||||
@CordaInject
|
||||
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())
|
||||
);
|
||||
public FlowEngine flowEngine;
|
||||
|
||||
@Override
|
||||
@Suspendable
|
||||
public String call(ClientRequestBody requestBody) {
|
||||
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()
|
||||
);
|
||||
final UtxoSignedTransaction trx = prepareSignedTransaction(action, inputState);
|
||||
|
||||
String trxResult = doTrunsaction(inputState, outputState);
|
||||
final SecureHash trxId = this.flowEngine
|
||||
.subFlow( new CommitSubFlow(trx, action.getReceiver(inputState)) );
|
||||
|
||||
return new FlowResult(trxResult).toJsonEncodedString(jsonMarshallingService);
|
||||
return new FlowResult(action+"ED", trxId) // REJECT+ED
|
||||
.toJsonEncodedString(jsonMarshallingService);
|
||||
}
|
||||
catch (Exception e) {
|
||||
log.warn("GameProposalAction flow failed to process utxo request body " + requestBody +
|
||||
@ -83,7 +67,7 @@ public class ActionFlow implements ClientStartableFlow {
|
||||
@Suspendable
|
||||
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
|
||||
* number of 'active' GameProposals exists in storage.
|
||||
*/
|
||||
@ -102,26 +86,24 @@ public class ActionFlow implements ClientStartableFlow {
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
private String doTrunsaction(StateAndRef<GameProposalState> inputState, GameProposalResolutionState outputState) {
|
||||
UtxoTransactionBuilder txBuilder = ledgerService.createTransactionBuilder()
|
||||
private UtxoSignedTransaction prepareSignedTransaction(
|
||||
Action action,
|
||||
StateAndRef<GameProposalState> inputState
|
||||
) {
|
||||
UtxoTransactionBuilder trxBuilder = ledgerService.createTransactionBuilder()
|
||||
.setNotary(inputState.getState().getNotaryName())
|
||||
.setTimeWindowBetween(Instant.now(), Instant.now().plusMillis(Duration.ofDays(1).toMillis()))
|
||||
.addInputState(inputState.getRef())
|
||||
.addOutputState(outputState)
|
||||
.addCommand(resoultion2command.get(outputState.outcome))
|
||||
.addSignatories(outputState.getParticipants());
|
||||
.addCommand(action)
|
||||
.addSignatories(inputState.getState().getContractState().getParticipants());
|
||||
|
||||
UtxoSignedTransaction signedTransaction = txBuilder.toSignedTransaction();
|
||||
if (action == Action.ACCEPT) {
|
||||
// TODO
|
||||
// .addOutputState(outputState)
|
||||
throw new RuntimeException(action +" unimplemented");
|
||||
}
|
||||
|
||||
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
|
||||
return trxBuilder.toSignedTransaction();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -2,8 +2,7 @@ package djmil.cordacheckers.gameproposal;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import djmil.cordacheckers.states.GameProposalResolutionState;
|
||||
import djmil.cordacheckers.states.GameProposalResolutionState.Resolution;
|
||||
import djmil.cordacheckers.contracts.GameProposalContract;
|
||||
|
||||
public class ActionFlowArgs {
|
||||
private UUID gameProposalUuid;
|
||||
@ -15,8 +14,8 @@ public class ActionFlowArgs {
|
||||
this.action = null;
|
||||
}
|
||||
|
||||
public Resolution getAction() {
|
||||
return GameProposalResolutionState.Resolution.valueOf(this.action);
|
||||
public GameProposalContract.Action getAction() {
|
||||
return GameProposalContract.Action.valueOf(this.action);
|
||||
}
|
||||
|
||||
public UUID getGameProposalUuid() {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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,26 +13,23 @@ 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);
|
||||
@ -52,19 +49,22 @@ 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;
|
||||
|
||||
GameProposalState gameProposal = buildGameProposalStateFrom(requestBody);
|
||||
String trxResult = doTrunsaction(gameProposal);
|
||||
final GameProposalState newGameProposal = buildGameProposalStateFrom(requestBody);
|
||||
|
||||
return new FlowResult(trxResult).toJsonEncodedString(jsonMarshallingService);
|
||||
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);
|
||||
}
|
||||
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);
|
||||
|
||||
GameProposalState.Color opponentColor = GameProposalState.Color.valueOf(args.opponentColor);
|
||||
Piece.Color opponentColor = Piece.Color.valueOf(args.opponentColor);
|
||||
if (opponentColor == null) {
|
||||
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);
|
||||
}
|
||||
|
||||
@ -101,24 +101,15 @@ public class CreateFlow implements ClientStartableFlow{
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
private String doTrunsaction(GameProposalState gameProposal) {
|
||||
NotaryInfo notary = notaryLookup.getNotaryServices().iterator().next();
|
||||
private UtxoSignedTransaction prepareSignedTransaction(GameProposalState outputGameProposalState) {
|
||||
final NotaryInfo notary = notaryLookup.getNotaryServices().iterator().next();
|
||||
|
||||
UtxoTransactionBuilder txBuilder = ledgerService.createTransactionBuilder()
|
||||
return ledgerService.createTransactionBuilder()
|
||||
.setNotary(notary.getName())
|
||||
.setTimeWindowBetween(Instant.now(), Instant.now().plusMillis(Duration.ofDays(1).toMillis()))
|
||||
.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();
|
||||
.addOutputState(outputGameProposalState)
|
||||
.addCommand(Action.CREATE)
|
||||
.addSignatories(outputGameProposalState.getParticipants())
|
||||
.toSignedTransaction();
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -10,25 +10,25 @@ 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 sender;
|
||||
public final String recipient;
|
||||
public final String recipientColor;
|
||||
public final String issuer;
|
||||
public final String acquier;
|
||||
public final String acquierColor;
|
||||
public final String message;
|
||||
public final UUID id;
|
||||
|
||||
// Serialisation service requires a default constructor
|
||||
public ListItem() {
|
||||
this.sender = null;
|
||||
this.recipient = null;
|
||||
this.recipientColor = null;
|
||||
this.issuer = null;
|
||||
this.acquier = null;
|
||||
this.acquierColor = null;
|
||||
this.message = null;
|
||||
this.id = null;
|
||||
}
|
||||
|
||||
public ListItem(GameProposalState state) {
|
||||
this.sender = state.getSender().getCommonName();
|
||||
this.recipient = state.getRecipient().getCommonName();
|
||||
this.recipientColor = state.getRecipientColor().name();
|
||||
this.issuer = state.getIssuer().getCommonName();
|
||||
this.acquier = state.getAcquier().getCommonName();
|
||||
this.acquierColor = state.getRecipientColor().name();
|
||||
this.message = state.getMessage();
|
||||
this.id = state.getId();
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user