Corda: GameProposal
This commit is contained in:
parent
43c4252df1
commit
c4048c20b6
@ -10,7 +10,7 @@ plugins {
|
||||
}
|
||||
|
||||
allprojects {
|
||||
group 'com.r3.developers.csdetemplate'
|
||||
group 'djmil.cordacheckers'
|
||||
version '1.0-SNAPSHOT'
|
||||
|
||||
def javaVersion = VERSION_11
|
||||
|
@ -67,9 +67,9 @@ cordapp {
|
||||
// The cordapp section contains either a workflow or contract subsection depending on the type of component.
|
||||
// Declares the type and metadata of the CPK (this CPB has one CPK).
|
||||
contract {
|
||||
name "ContractsModuleNameHere"
|
||||
name "CordaCkeckersContracts"
|
||||
versionId 1
|
||||
vendor "VendorNameHere"
|
||||
vendor "djmil"
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,71 @@
|
||||
package djmil.cordacheckers.contracts;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import djmil.cordacheckers.states.GameProposalState;
|
||||
import net.corda.v5.base.exceptions.CordaRuntimeException;
|
||||
import net.corda.v5.ledger.utxo.Command;
|
||||
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 Cancle implements Command { }
|
||||
|
||||
@Override
|
||||
public void verify(UtxoLedgerTransaction trx) {
|
||||
log.info("GameProposalContract.verify() called");
|
||||
|
||||
requireThat(trx.getCommands().size() == 1, REQUIRE_SINGLE_COMMAND);
|
||||
Command command = trx.getCommands().get(0);
|
||||
|
||||
if (command instanceof Create) {
|
||||
requireThat(trx.getInputContractStates().isEmpty(), CREATE_INPUT_STATE);
|
||||
requireThat(trx.getOutputContractStates().size() == 1, CREATE_OUTPUT_STATE);
|
||||
|
||||
GameProposalState outputState = trx.getOutputStates(GameProposalState.class).get(0);
|
||||
requireThat(outputState != null, CREATE_OUTPUT_STATE);
|
||||
|
||||
requireThat(outputState.getYouPlayAs() != null, CREATE_NOT_NULL_YOU_PLAY_AS);
|
||||
} else
|
||||
if (command instanceof Accept) {
|
||||
// TODO outputState -> Game
|
||||
throw new CordaRuntimeException("Unimplemented!");
|
||||
} else
|
||||
if (command instanceof Reject) {
|
||||
requireThat(trx.getInputContractStates().size() == 1, REJECT_INPUT_STATE);
|
||||
requireThat(trx.getOutputContractStates().isEmpty(), REJECT_OUTPUT_STATE);
|
||||
|
||||
GameProposalState inputState = trx.getInputStates(GameProposalState.class).get(0);
|
||||
requireThat(inputState != null, REJECT_INPUT_STATE);
|
||||
} else
|
||||
if (command instanceof Cancle) {
|
||||
// TODO cancle game state
|
||||
throw new CordaRuntimeException("Unimplemented!");
|
||||
} else {
|
||||
throw new CordaRuntimeException(UNKNOWN_COMMAND);
|
||||
}
|
||||
}
|
||||
|
||||
private void requireThat(boolean asserted, String errorMessage) {
|
||||
if(!asserted) {
|
||||
throw new CordaRuntimeException("Failed requirement: " + errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
static final String REQUIRE_SINGLE_COMMAND = "Require a single command";
|
||||
static final String UNKNOWN_COMMAND = "Unsupported command";
|
||||
|
||||
static final String CREATE_INPUT_STATE = "Create command should have no input states";
|
||||
static final String CREATE_OUTPUT_STATE = "Create command should output exactly one GameProposal state";
|
||||
static final String CREATE_NOT_NULL_YOU_PLAY_AS = "GameProposal.youPlayAs field can not be null";
|
||||
|
||||
static final String REJECT_INPUT_STATE = "Reject command should have exactly one GameProposal state";
|
||||
static final String REJECT_OUTPUT_STATE = "Reject command should have no output states";
|
||||
|
||||
}
|
@ -0,0 +1,78 @@
|
||||
package djmil.cordacheckers.states;
|
||||
|
||||
import java.security.PublicKey;
|
||||
import java.util.List;
|
||||
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;
|
||||
|
||||
@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 youPlayAs;
|
||||
public final String additionalMessage;
|
||||
public final UUID id;
|
||||
public final List<PublicKey> participants;
|
||||
|
||||
// Allows serialisation and to use a specified UUID
|
||||
@ConstructorForDeserialization
|
||||
public GameProposalState(
|
||||
MemberX500Name sender,
|
||||
MemberX500Name recipient,
|
||||
Color youPlayAs,
|
||||
String additionalMessage,
|
||||
UUID id,
|
||||
List<PublicKey> participants) {
|
||||
this.sender = sender;
|
||||
this.recipient = recipient;
|
||||
this.youPlayAs = youPlayAs;
|
||||
this.additionalMessage = additionalMessage;
|
||||
this.id = id;
|
||||
this.participants = participants;
|
||||
}
|
||||
|
||||
public MemberX500Name getSender() {
|
||||
return sender;
|
||||
}
|
||||
|
||||
public MemberX500Name getRecipient() {
|
||||
return recipient;
|
||||
}
|
||||
|
||||
public Color getYouPlayAs() {
|
||||
return youPlayAs;
|
||||
}
|
||||
|
||||
public String getAdditionalMessage() {
|
||||
return additionalMessage;
|
||||
}
|
||||
|
||||
public UUID getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public List<PublicKey> getParticipants() {
|
||||
return participants;
|
||||
}
|
||||
|
||||
public String getRecipientCommonName() {
|
||||
return recipient == null ? "" : recipient.getCommonName();
|
||||
}
|
||||
|
||||
public String getSenderCommonName() {
|
||||
return sender == null ? "" : sender.getCommonName();
|
||||
}
|
||||
}
|
@ -69,9 +69,9 @@ cordapp {
|
||||
// The cordapp section contains either a workflow or contract subsection depending on the type of component.
|
||||
// Declares the type and metadata of the CPK (this CPB has one CPK).
|
||||
workflow {
|
||||
name "WorkflowsModuleNameHere"
|
||||
name "CordaCkeckers"
|
||||
versionId 1
|
||||
vendor "VendorNameHere"
|
||||
vendor "djmil"
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,125 @@
|
||||
package djmil.cordacheckers.gameproposal;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import djmil.cordacheckers.contracts.GameProposalContract;
|
||||
import djmil.cordacheckers.states.GameProposalState;
|
||||
import net.corda.v5.application.flows.ClientRequestBody;
|
||||
import net.corda.v5.application.flows.ClientStartableFlow;
|
||||
import net.corda.v5.application.flows.CordaInject;
|
||||
import net.corda.v5.application.flows.FlowEngine;
|
||||
import net.corda.v5.application.flows.InitiatingFlow;
|
||||
import net.corda.v5.application.marshalling.JsonMarshallingService;
|
||||
import net.corda.v5.application.membership.MemberLookup;
|
||||
import net.corda.v5.application.messaging.FlowMessaging;
|
||||
import net.corda.v5.application.messaging.FlowSession;
|
||||
import net.corda.v5.base.annotations.Suspendable;
|
||||
import net.corda.v5.base.exceptions.CordaRuntimeException;
|
||||
import net.corda.v5.base.types.MemberX500Name;
|
||||
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 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);
|
||||
|
||||
@CordaInject
|
||||
public JsonMarshallingService jsonMarshallingService;
|
||||
|
||||
@CordaInject
|
||||
public MemberLookup memberLookup;
|
||||
|
||||
@CordaInject
|
||||
public NotaryLookup notaryLookup;
|
||||
|
||||
@CordaInject
|
||||
public UtxoLedgerService ledgerService;
|
||||
|
||||
@CordaInject
|
||||
public FlowEngine flowEngine;
|
||||
|
||||
@CordaInject
|
||||
public FlowMessaging flowMessaging;
|
||||
|
||||
@Suspendable
|
||||
@Override
|
||||
public String call(ClientRequestBody requestBody) {
|
||||
try {
|
||||
log.info("flow: Create Game Proposal");
|
||||
|
||||
GameProposalState gameProposal = buildGameProposalStateFrom(requestBody);
|
||||
String result = doTrunsaction(gameProposal);
|
||||
|
||||
return result;
|
||||
}
|
||||
catch (Exception e) {
|
||||
log.warn("CreateGameProposal flow failed to process utxo request body " + requestBody + " because: " + e.getMessage());
|
||||
throw new CordaRuntimeException(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
private GameProposalState buildGameProposalStateFrom(ClientRequestBody requestBody) {
|
||||
CreateFlowArgs args = requestBody.getRequestBodyAs(jsonMarshallingService, CreateFlowArgs.class);
|
||||
|
||||
GameProposalState.Color opponentColor = GameProposalState.Color.valueOf(args.opponentColor);
|
||||
if (opponentColor == null) {
|
||||
throw new RuntimeException("Allowed values for opponentColor are: "
|
||||
+ GameProposalState.Color.WHITE.name() +", " + GameProposalState.Color.BLACK.name()
|
||||
+ ". Actual value was: " + args.opponentColor);
|
||||
}
|
||||
|
||||
MemberInfo myInfo = memberLookup.myInfo();
|
||||
MemberInfo opponentInfo = requireNonNull(
|
||||
memberLookup.lookup(MemberX500Name.parse(args.opponentName)),
|
||||
"MemberLookup can't find opponentName specified in flow arguments: " + args.opponentName
|
||||
);
|
||||
|
||||
return new GameProposalState(
|
||||
myInfo.getName(),
|
||||
opponentInfo.getName(),
|
||||
GameProposalState.Color.valueOf(args.opponentColor),
|
||||
args.additionalMessage,
|
||||
UUID.randomUUID(),
|
||||
Arrays.asList(myInfo.getLedgerKeys().get(0), opponentInfo.getLedgerKeys().get(0))
|
||||
);
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
private String doTrunsaction(GameProposalState gameProposal) {
|
||||
NotaryInfo notary = notaryLookup.getNotaryServices().iterator().next();
|
||||
|
||||
UtxoTransactionBuilder txBuilder = 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);
|
||||
|
||||
UtxoSignedTransaction finalizedSignedTransaction = ledgerService
|
||||
.finalize(signedTransaction, sessionsList)
|
||||
.getTransaction();
|
||||
|
||||
return finalizedSignedTransaction.getId().toString();
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
package djmil.cordacheckers.gameproposal;
|
||||
|
||||
public class CreateFlowArgs {
|
||||
public final String opponentName;
|
||||
public final String opponentColor;
|
||||
public final String additionalMessage;
|
||||
|
||||
public CreateFlowArgs(String opponentName, String opponentColor, String additionalMessage) {
|
||||
this.opponentName = opponentName;
|
||||
this.opponentColor = opponentColor;
|
||||
this.additionalMessage = additionalMessage;
|
||||
}
|
||||
|
||||
// Serialisation service requires a default constructor
|
||||
public CreateFlowArgs() {
|
||||
this(null, null, null);
|
||||
}
|
||||
}
|
@ -0,0 +1,72 @@
|
||||
package djmil.cordacheckers.gameproposal;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import djmil.cordacheckers.states.GameProposalState;
|
||||
import net.corda.v5.application.flows.CordaInject;
|
||||
import net.corda.v5.application.flows.InitiatedBy;
|
||||
import net.corda.v5.application.flows.ResponderFlow;
|
||||
import net.corda.v5.application.membership.MemberLookup;
|
||||
import net.corda.v5.application.messaging.FlowSession;
|
||||
import net.corda.v5.base.annotations.Suspendable;
|
||||
import net.corda.v5.base.exceptions.CordaRuntimeException;
|
||||
import net.corda.v5.base.types.MemberX500Name;
|
||||
import net.corda.v5.ledger.utxo.UtxoLedgerService;
|
||||
import net.corda.v5.ledger.utxo.transaction.UtxoSignedTransaction;
|
||||
import net.corda.v5.ledger.utxo.transaction.UtxoTransactionValidator;
|
||||
|
||||
@InitiatedBy(protocol = "game-proposal-create")
|
||||
public class CreateResponder implements ResponderFlow {
|
||||
private final static Logger log = LoggerFactory.getLogger(CreateResponder.class);
|
||||
|
||||
@CordaInject
|
||||
public MemberLookup memberLookup;
|
||||
|
||||
@CordaInject
|
||||
public UtxoLedgerService utxoLedgerService;
|
||||
|
||||
@Suspendable
|
||||
@Override
|
||||
public void call(FlowSession session) {
|
||||
try {
|
||||
// Defines the lambda validator used in receiveFinality below.
|
||||
UtxoTransactionValidator txValidator = ledgerTransaction -> {
|
||||
GameProposalState gameProposal = (GameProposalState) ledgerTransaction.getOutputContractStates().get(0);
|
||||
// Uses checkForBannedWords() and checkMessageFromMatchesCounterparty() functions
|
||||
// to check whether to sign the transaction.
|
||||
if (!checkParticipants(gameProposal, session.getCounterparty())) {
|
||||
throw new CordaRuntimeException("Failed verification");
|
||||
}
|
||||
|
||||
log.info("Verified the transaction - " + ledgerTransaction.getId());
|
||||
};
|
||||
|
||||
// Calls receiveFinality() function which provides the responder to the finalise() function
|
||||
// in the Initiating Flow. Accepts a lambda validator containing the business logic to decide whether
|
||||
// responder should sign the Transaction.
|
||||
UtxoSignedTransaction finalizedSignedTransaction = utxoLedgerService
|
||||
.receiveFinality(session, txValidator)
|
||||
.getTransaction();
|
||||
|
||||
log.info("Finished responder flow - " + finalizedSignedTransaction.getId());
|
||||
}
|
||||
// Soft fails the flow and log the exception.
|
||||
catch(Exception e)
|
||||
{
|
||||
log.warn("Exceptionally finished responder flow", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
Boolean checkParticipants(GameProposalState gameProposal, MemberX500Name counterpartyName) {
|
||||
MemberX500Name myName = memberLookup.myInfo().getName();
|
||||
|
||||
if (gameProposal.getRecipient().compareTo(myName) == 0 &&
|
||||
gameProposal.getSender().compareTo(counterpartyName) == 0)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
package djmil.cordacheckers.gameproposal;
|
||||
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import djmil.cordacheckers.states.GameProposalState;
|
||||
import net.corda.v5.application.flows.ClientRequestBody;
|
||||
import net.corda.v5.application.flows.ClientStartableFlow;
|
||||
import net.corda.v5.application.flows.CordaInject;
|
||||
import net.corda.v5.application.marshalling.JsonMarshallingService;
|
||||
import net.corda.v5.ledger.utxo.StateAndRef;
|
||||
import net.corda.v5.ledger.utxo.UtxoLedgerService;
|
||||
|
||||
public class ListFlow implements ClientStartableFlow {
|
||||
|
||||
private final static Logger log = LoggerFactory.getLogger(ListFlow.class);
|
||||
|
||||
@CordaInject
|
||||
public UtxoLedgerService utxoLedgerService;
|
||||
|
||||
@CordaInject
|
||||
public JsonMarshallingService jsonMarshallingService;
|
||||
|
||||
@Override
|
||||
public String call(ClientRequestBody requestBody) {
|
||||
log.info("ListChatsFlow.call() called");
|
||||
|
||||
// Queries the VNode's vault for unconsumed states and converts the result to a serializable DTO.
|
||||
java.util.List<StateAndRef<GameProposalState>> states = utxoLedgerService.findUnconsumedStatesByType(GameProposalState.class);
|
||||
|
||||
java.util.List<ListItem> results = states.stream().map( stateAndRef ->
|
||||
new ListItem(stateAndRef.getState().getContractState())
|
||||
).collect(Collectors.toList());
|
||||
|
||||
// Uses the JsonMarshallingService's format() function to serialize the DTO to Json.
|
||||
return jsonMarshallingService.format(results);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
package djmil.cordacheckers.gameproposal;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import djmil.cordacheckers.states.GameProposalState;
|
||||
|
||||
// Class to hold results of the List flow.
|
||||
// The GameProposal(s) cannot be returned directly as the JsonMarshallingService can only serialize simple classes
|
||||
// that the underlying Jackson serializer recognises, hence creating a DTO style object which consists only of Strings
|
||||
// and a 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 youPlayAs;
|
||||
public final String additionalMessage;
|
||||
public final UUID id;
|
||||
|
||||
// Serialisation service requires a default constructor
|
||||
public ListItem() {
|
||||
this.sender = null;
|
||||
this.recipient = null;
|
||||
this.youPlayAs = null;
|
||||
this.additionalMessage = null;
|
||||
this.id = null;
|
||||
}
|
||||
|
||||
public ListItem(GameProposalState state) {
|
||||
this.sender = state.getSenderCommonName();
|
||||
this.recipient = state.getRecipientCommonName();
|
||||
this.youPlayAs = state.getYouPlayAs().name();
|
||||
this.additionalMessage = state.getAdditionalMessage();
|
||||
this.id = state.getId();
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
package djmil.cordacheckers.gameproposal;
|
||||
|
||||
public class RejectFlow {
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user