Corda: GameProposal

This commit is contained in:
djmil 2023-08-30 20:43:31 +02:00
parent 43c4252df1
commit c4048c20b6
11 changed files with 450 additions and 5 deletions

View File

@ -10,7 +10,7 @@ plugins {
}
allprojects {
group 'com.r3.developers.csdetemplate'
group 'djmil.cordacheckers'
version '1.0-SNAPSHOT'
def javaVersion = VERSION_11

View File

@ -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"
}
}

View File

@ -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";
}

View File

@ -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();
}
}

View File

@ -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"
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -0,0 +1,5 @@
package djmil.cordacheckers.gameproposal;
public class RejectFlow {
}