diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c81757e --- /dev/null +++ b/.gitignore @@ -0,0 +1,88 @@ + +# Eclipse, ctags, Mac metadata, log files +.classpath +.project +.settings +tags +.DS_Store +*.log +*.orig + +# Created by .ignore support plugin (hsz.mobi) + +.gradle +local.properties +.gradletasknamecache + +# General build files +**/build/* + +lib/quasar.jar + +**/logs/* + +### JetBrains template +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio + +*.iml + +## Directory-based project format: +.idea/*.xml +.idea/.name +.idea/copyright +.idea/inspectionProfiles +.idea/libraries +.idea/shelf +.idea/dataSources +.idea/markdown-navigator +.idea/runConfigurations +.idea/dictionaries + + +# Include the -parameters compiler option by default in IntelliJ required for serialization. +!.idea/codeStyleSettings.xml + + +## File-based project format: +*.ipr +*.iws + +## Plugin-specific files: + +# IntelliJ +**/out/ +/classes/ + + + +# vim +*.swp +*.swn +*.swo + + + +# Directory generated during Resolve and TestOSGi gradle tasks +bnd/ + +# Ignore Gradle build output directory +build +/.idea/codeStyles/codeStyleConfig.xml +/.idea/codeStyles/Project.xml + + + +# Ignore Visual studio directory +bin/ + + + +*.cpi +*.cpb +*.cpk +workspace/** +#CordaPID.dat +#*.pem +#*.pfx +#CPIFileStatus*.json +#GroupPolicy.json diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/build.gradle b/build.gradle index 9cb78fe..96dc489 100644 --- a/build.gradle +++ b/build.gradle @@ -2,19 +2,15 @@ import static org.gradle.api.JavaVersion.VERSION_11 plugins { id 'org.jetbrains.kotlin.jvm' - id 'net.corda.cordapp.cordapp-configuration' - id 'org.jetbrains.kotlin.plugin.jpa' - id 'java' id 'maven-publish' - id 'csde' } allprojects { - group 'com.r3.hellocorda' + group 'com.r3.developers.csdetemplate' version '1.0-SNAPSHOT' def javaVersion = VERSION_11 diff --git a/buildSrc/src/main/groovy/csde.gradle b/buildSrc/src/main/groovy/csde.gradle index bef129c..673ce8f 100644 --- a/buildSrc/src/main/groovy/csde.gradle +++ b/buildSrc/src/main/groovy/csde.gradle @@ -10,6 +10,7 @@ // todo: add test corda running/live task // todo: add a test to check docker is running and display error if not + halt start corda // todo: add a clean corda task. +// todo: fix logging level and make it configurable. import com.r3.csde.CordaLifeCycleHelper diff --git a/contracts/src/main/java/com/r3/developers/csdetemplate/utxoexample/contracts/ChatContract.java b/contracts/src/main/java/com/r3/developers/csdetemplate/utxoexample/contracts/ChatContract.java new file mode 100644 index 0000000..15f6b54 --- /dev/null +++ b/contracts/src/main/java/com/r3/developers/csdetemplate/utxoexample/contracts/ChatContract.java @@ -0,0 +1,71 @@ +package com.r3.developers.csdetemplate.utxoexample.contracts; + +import com.r3.developers.csdetemplate.utxoexample.states.ChatState; +import net.corda.v5.base.exceptions.CordaRuntimeException; +import net.corda.v5.ledger.utxo.Command; +import net.corda.v5.ledger.utxo.Contract; +import net.corda.v5.ledger.utxo.ContractState; +import net.corda.v5.ledger.utxo.StateAndRef; +import net.corda.v5.ledger.utxo.transaction.UtxoLedgerTransaction; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.security.PublicKey; +import java.util.Objects; +import java.util.Set; + +import static java.util.Objects.*; + +// todo: START here + +public class ChatContract implements Contract { + + private final static Logger log = LoggerFactory.getLogger(ChatContract.class); + + public static class Create implements Command { } + public static class Update implements Command { } + +// @Override +// public boolean isRelevant(@NotNull ContractState state, @NotNull Set myKeys) { +// return Contract.super.isRelevant(state, myKeys); +// } + + @Override + public void verify(UtxoLedgerTransaction transaction) { + +// Command command = requireNonNull( transaction.getCommands().get(0), "Require a single command"); // this doesn't ensure there is one command + requireThat( transaction.getCommands().size() == 1, "Require a single command."); + Command command = transaction.getCommands().get(0); + + ChatState output = transaction.getOutputStates(ChatState.class).get(0); + + requireThat(output.getParticipants().size() == 2, "The output state should have two and only two participants."); + + if(command.getClass() == Create.class) { + requireThat(transaction.getInputContractStates().isEmpty(), "When command is Create there should be no input states."); + requireThat(transaction.getOutputContractStates().size() == 1, "When command is Create there should be one and only one output state."); + } + else if(command.getClass() == Update.class) { + requireThat(transaction.getInputContractStates().size() == 1, "When command is Update there should be one and only one input state."); + requireThat(transaction.getOutputContractStates().size() == 1, "When command is Update there should be one and only one output state."); + + ChatState input = transaction.getInputStates(ChatState.class).get(0); + requireThat(input.getId().equals(output.getId()), "When command is Update id must not change."); + requireThat(input.getChatName().equals(output.getChatName()), "When command is Update chatName must not change."); + requireThat( + input.getParticipants().containsAll(output.getParticipants()) && + output.getParticipants().containsAll(input.getParticipants()), + "When command is Update participants must not change."); + } + else { + throw new CordaRuntimeException("Unsupported command"); + } + } + + private void requireThat(boolean asserted, String errorMessage) { + if(!asserted) { + throw new CordaRuntimeException("Failed requirement: " + errorMessage); + } + } +} diff --git a/contracts/src/main/java/com/r3/developers/csdetemplate/utxoexample/states/ChatState.java b/contracts/src/main/java/com/r3/developers/csdetemplate/utxoexample/states/ChatState.java new file mode 100644 index 0000000..20f80ef --- /dev/null +++ b/contracts/src/main/java/com/r3/developers/csdetemplate/utxoexample/states/ChatState.java @@ -0,0 +1,89 @@ +package com.r3.developers.csdetemplate.utxoexample.states; + +import com.r3.developers.csdetemplate.utxoexample.contracts.ChatContract; +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; +import org.jetbrains.annotations.NotNull; + +import java.security.PublicKey; +import java.util.*; + +// todo: Clear out commented code + +//@CordaSerializable +@BelongsToContract(ChatContract.class) +public class ChatState implements ContractState { + + private UUID id; + private String chatName; + private MemberX500Name messageFrom; + private String message; + public List participants; + +// public ChatState() { // todo why do we need this? +// } + + // Allows serialisation and to use a specified UUID. + @ConstructorForDeserialization + public ChatState(UUID id, + String chatName, + MemberX500Name messageFrom, + String message, + List participants) { + this.id = id; + this.chatName = chatName; + this.messageFrom = messageFrom; + this.message = message; + this.participants = participants; + } + +// // Convenience constructor for initial ChatState objects that need a new UUID generated. +// public ChatState(String chatName, +// MemberX500Name messageFrom, +// String message, +// List participants) { +// this(UUID.randomUUID(), +// chatName, +// messageFrom, +// message, +// participants); +// } + + public UUID getId() { + return id; + } + public String getChatName() { + return chatName; + } + public MemberX500Name getMessageFrom() { + return messageFrom; + } + public String getMessage() { + return message; + } +// @NotNull +// @Override + public List getParticipants() { + return participants; + } + + + public ChatState updateMessage(MemberX500Name name, String message) { + return new ChatState(id, chatName, name, message, participants); + } +// +// // todo: why is this overridden +// @Override +// public String toString() { +// return ChatState.class.getName() + +// "(id=" + id + +// ", chatName=" + chatName + +// ", messageFrom=" + messageFrom + +// ", message=" + message + +// ", participants=" + participants + +// ")"; +// } +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index fb1520b..5a08d07 100644 --- a/settings.gradle +++ b/settings.gradle @@ -22,6 +22,3 @@ rootProject.name = 'csde-cordapp-template-java' include ':workflows' include ':contracts' - - - diff --git a/workflows/src/main/java/com/r3/developers/csdetemplate/workflows/Message.java b/workflows/src/main/java/com/r3/developers/csdetemplate/flowexample/workflows/Message.java similarity index 81% rename from workflows/src/main/java/com/r3/developers/csdetemplate/workflows/Message.java rename to workflows/src/main/java/com/r3/developers/csdetemplate/flowexample/workflows/Message.java index c19960d..b20791e 100644 --- a/workflows/src/main/java/com/r3/developers/csdetemplate/workflows/Message.java +++ b/workflows/src/main/java/com/r3/developers/csdetemplate/flowexample/workflows/Message.java @@ -1,4 +1,4 @@ -package com.r3.developers.csdetemplate.workflows; +package com.r3.developers.csdetemplate.flowexample.workflows; import net.corda.v5.base.annotations.CordaSerializable; import net.corda.v5.base.types.MemberX500Name; @@ -7,6 +7,10 @@ import net.corda.v5.base.types.MemberX500Name; // send it from one virtual node to another. @CordaSerializable public class Message { + + private MemberX500Name sender; + private String message; + public Message(MemberX500Name sender, String message) { this.sender = sender; this.message = message; @@ -20,6 +24,5 @@ public class Message { return message; } - public MemberX500Name sender; - public String message; + } diff --git a/workflows/src/main/java/com/r3/developers/csdetemplate/workflows/MyFirstFlow.java b/workflows/src/main/java/com/r3/developers/csdetemplate/flowexample/workflows/MyFirstFlow.java similarity index 90% rename from workflows/src/main/java/com/r3/developers/csdetemplate/workflows/MyFirstFlow.java rename to workflows/src/main/java/com/r3/developers/csdetemplate/flowexample/workflows/MyFirstFlow.java index 0263bc1..a2ffb32 100644 --- a/workflows/src/main/java/com/r3/developers/csdetemplate/workflows/MyFirstFlow.java +++ b/workflows/src/main/java/com/r3/developers/csdetemplate/flowexample/workflows/MyFirstFlow.java @@ -1,4 +1,4 @@ -package com.r3.developers.csdetemplate.workflows; +package com.r3.developers.csdetemplate.flowexample.workflows; import net.corda.v5.application.flows.*; import net.corda.v5.application.marshalling.JsonMarshallingService; @@ -19,7 +19,7 @@ import org.slf4j.LoggerFactory; public class MyFirstFlow implements RPCStartableFlow { // Log messages from the flows for debugging. - private final Logger log = LoggerFactory.getLogger(MyFirstFlow.class); + private final static Logger log = LoggerFactory.getLogger(MyFirstFlow.class); // Corda has a set of injectable services which are injected into the flow at runtime. // Flows declare them with @CordaInjectable, then the flows have access to their services. @@ -58,7 +58,7 @@ public class MyFirstFlow implements RPCStartableFlow { MyFirstFlowStartArgs flowArgs = requestBody.getRequestBodyAs(jsonMarshallingService, MyFirstFlowStartArgs.class); // Obtain the MemberX500Name of the counterparty. - MemberX500Name otherMember = flowArgs.otherMember; + MemberX500Name otherMember = flowArgs.getOtherMember(); // Get our identity from the MemberLookup service. MemberX500Name ourIdentity = memberLookup.myInfo().getName(); @@ -67,7 +67,7 @@ public class MyFirstFlow implements RPCStartableFlow { Message message = new Message(otherMember, "Hello from " + ourIdentity + "."); // Log the message to be sent. - log.info("MFF: message.message: " + message.message); + log.info("MFF: message.message: " + message.getMessage()); // Start a flow session with the otherMember using the FlowMessaging service. // The otherMember's virtual node will run the corresponding MyFirstFlowResponder responder flow. @@ -82,16 +82,15 @@ public class MyFirstFlow implements RPCStartableFlow { // The return value of a RPCStartableFlow must always be a String. This will be passed // back as the REST RPC response when the status of the flow is queried on Corda, or as the return // value from the flow when testing using the simulator. - return response.message; + return response.getMessage(); } } - /* RequestBody for triggering the flow via http-rpc: { "clientRequestId": "r1", - "flowClassName": "com.r3.developers.csdetemplate.workflows.MyFirstFlow", + "flowClassName": "com.r3.developers.csdetemplate.flowexample.workflows.MyFirstFlow", "requestData": { "otherMember":"CN=Bob, OU=Test Dept, O=R3, L=London, C=GB" } diff --git a/workflows/src/main/java/com/r3/developers/csdetemplate/workflows/MyFirstFlowResponder.java b/workflows/src/main/java/com/r3/developers/csdetemplate/flowexample/workflows/MyFirstFlowResponder.java similarity index 84% rename from workflows/src/main/java/com/r3/developers/csdetemplate/workflows/MyFirstFlowResponder.java rename to workflows/src/main/java/com/r3/developers/csdetemplate/flowexample/workflows/MyFirstFlowResponder.java index e7a1ff2..a41fc13 100644 --- a/workflows/src/main/java/com/r3/developers/csdetemplate/workflows/MyFirstFlowResponder.java +++ b/workflows/src/main/java/com/r3/developers/csdetemplate/flowexample/workflows/MyFirstFlowResponder.java @@ -1,4 +1,4 @@ -package com.r3.developers.csdetemplate.workflows; +package com.r3.developers.csdetemplate.flowexample.workflows; import net.corda.v5.application.flows.CordaInject; import net.corda.v5.application.flows.InitiatedBy; @@ -18,7 +18,7 @@ import org.slf4j.LoggerFactory; public class MyFirstFlowResponder implements ResponderFlow { // Log messages from the flows for debugging. - private final Logger log = LoggerFactory.getLogger(MyFirstFlowResponder.class); + private final static Logger log = LoggerFactory.getLogger(MyFirstFlowResponder.class); // MemberLookup looks for information about members of the virtual network which // this CorDapp operates in. @@ -44,7 +44,7 @@ public class MyFirstFlowResponder implements ResponderFlow { Message receivedMessage = session.receive(Message.class); // Log the message as a proxy for performing some useful operation on it. - log.info("MFF: Message received from " + receivedMessage.sender + ":" + receivedMessage.message); + log.info("MFF: Message received from " + receivedMessage.getSender() + ":" + receivedMessage.getMessage()); // Get our identity from the MemberLookup service. MemberX500Name ourIdentity = memberLookup.myInfo().getName(); @@ -54,19 +54,9 @@ public class MyFirstFlowResponder implements ResponderFlow { "Hello " + session.getCounterparty().getCommonName() + ", best wishes from " + ourIdentity.getCommonName()); // Log the response to be sent. - log.info("MFF: response.message: " + response.message); + log.info("MFF: response.message: " + response.getMessage()); // Send the response via the send method on the flow session session.send(response); } } -/* -RequestBody for triggering the flow via http-rpc: -{ - "clientRequestId": "r1", - "flowClassName": "com.r3.developers.csdetemplate.MyFirstFlow", - "requestData": { - "otherMember":"CN=Bob, OU=Test Dept, O=R3, L=London, C=GB" - } -} - */ \ No newline at end of file diff --git a/workflows/src/main/java/com/r3/developers/csdetemplate/workflows/MyFirstFlowStartArgs.java b/workflows/src/main/java/com/r3/developers/csdetemplate/flowexample/workflows/MyFirstFlowStartArgs.java similarity index 67% rename from workflows/src/main/java/com/r3/developers/csdetemplate/workflows/MyFirstFlowStartArgs.java rename to workflows/src/main/java/com/r3/developers/csdetemplate/flowexample/workflows/MyFirstFlowStartArgs.java index 51a83c6..bfcca2d 100644 --- a/workflows/src/main/java/com/r3/developers/csdetemplate/workflows/MyFirstFlowStartArgs.java +++ b/workflows/src/main/java/com/r3/developers/csdetemplate/flowexample/workflows/MyFirstFlowStartArgs.java @@ -1,15 +1,19 @@ -package com.r3.developers.csdetemplate.workflows; +package com.r3.developers.csdetemplate.flowexample.workflows; import net.corda.v5.base.types.MemberX500Name; // A class to hold the arguments required to start the flow public class MyFirstFlowStartArgs { - public MemberX500Name otherMember; + private MemberX500Name otherMember; + + // The JSON Marshalling Service, which handles serialisation, needs this constructor. + public MyFirstFlowStartArgs() {} public MyFirstFlowStartArgs(MemberX500Name otherMember) { this.otherMember = otherMember; } - // The JSON Marshalling Service, which handles serialisation, needs this constructor. - public MyFirstFlowStartArgs() {} + public MemberX500Name getOtherMember() { + return otherMember; + } } diff --git a/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/ChatStateResults.java b/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/ChatStateResults.java new file mode 100644 index 0000000..9a6b500 --- /dev/null +++ b/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/ChatStateResults.java @@ -0,0 +1,36 @@ +package com.r3.developers.csdetemplate.utxoexample.workflows; + +import java.util.UUID; + +public class ChatStateResults { + + private UUID id; + private String chatName; + private String messageFromName; + private String message; + + public ChatStateResults() {} + + public ChatStateResults(UUID id, String chatName, String messageFromName, String message) { + this.id = id; + this.chatName = chatName; + this.messageFromName = messageFromName; + this.message = message; + } + + public UUID getId() { + return id; + } + + public String getChatName() { + return chatName; + } + + public String getMessageFromName() { + return messageFromName; + } + + public String getMessage() { + return message; + } +} diff --git a/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/CreateNewChatFlow.java b/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/CreateNewChatFlow.java new file mode 100644 index 0000000..2b1de5f --- /dev/null +++ b/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/CreateNewChatFlow.java @@ -0,0 +1,119 @@ +package com.r3.developers.csdetemplate.utxoexample.workflows; + +import com.r3.developers.csdetemplate.utxoexample.contracts.ChatContract; +import com.r3.developers.csdetemplate.utxoexample.states.ChatState; +import net.corda.v5.application.flows.*; +import net.corda.v5.application.marshalling.JsonMarshallingService; +import net.corda.v5.application.membership.MemberLookup; +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.common.Party; +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 org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.security.PublicKey; +import java.time.Duration; +import java.time.Instant; +import java.util.Arrays; +import java.util.Objects; +import java.util.UUID; + +import static java.util.Objects.*; + +public class CreateNewChatFlow implements RPCStartableFlow { + + private final static Logger log = LoggerFactory.getLogger(CreateNewChatFlow.class); + + @CordaInject + public JsonMarshallingService jsonMarshallingService; + + @CordaInject + public MemberLookup memberLookup; + + @CordaInject + public UtxoLedgerService ledgerService; + + @CordaInject + public NotaryLookup notaryLookup; + + @CordaInject + public FlowEngine flowEngine; + + + @Suspendable + @Override + public String call( RPCRequestData requestBody) { + + log.info("CreateNewChatFlow.call() called"); + + try { + CreateNewChatFlowArgs flowArgs = requestBody.getRequestBodyAs(jsonMarshallingService, CreateNewChatFlowArgs.class); + + MemberInfo myInfo = memberLookup.myInfo(); + MemberInfo otherMember = requireNonNull( + memberLookup.lookup(MemberX500Name.parse(flowArgs.getOtherMember())), + "MemberLookup can't find otherMember specified in flow arguments." + ); + + ChatState chatState = new ChatState( + UUID.randomUUID(), + flowArgs.getChatName(), + myInfo.getName(), + flowArgs.getMessage(), + Arrays.asList(myInfo.getLedgerKeys().get(0), otherMember.getLedgerKeys().get(0)) + ); + + NotaryInfo notary = notaryLookup.getNotaryServices().iterator().next(); + + PublicKey notaryKey = null; + for(MemberInfo memberInfo: memberLookup.lookup()){ + if(Objects.equals( + memberInfo.getMemberProvidedContext().get("corda.notary.service.name"), + notary.getName().toString())) { + notaryKey = memberInfo.getLedgerKeys().get(0); + break; + } + } + + if(notaryKey == null) { + throw new CordaRuntimeException("No notary PublicKey found"); + } + + UtxoTransactionBuilder txBuilder = ledgerService.getTransactionBuilder() + .setNotary(new Party(notary.getName(), notaryKey)) + .setTimeWindowBetween(Instant.now(), Instant.now().plusMillis(Duration.ofDays(1).toMillis())) + .addOutputState(chatState) + .addCommand(new ChatContract.Create()) + .addSignatories(chatState.getParticipants()); + + @SuppressWarnings("DEPRECATION") + UtxoSignedTransaction signedTransaction = txBuilder.toSignedTransaction(myInfo.getLedgerKeys().get(0)); + + return flowEngine.subFlow(new FinalizeChatSubFlow(signedTransaction, otherMember.getName())); + } + catch (Exception e) { + log.warn("Failed to process utxo flow for request body " + requestBody + " because: " + e.getMessage()); + throw new CordaRuntimeException(e.getMessage()); + } + } +} + +/* +RequestBody for triggering the flow via http-rpc: +{ + "clientRequestId": "create-1", + "flowClassName": "com.r3.developers.csdetemplate.utxoexample.workflows.CreateNewChatFlow", + "requestData": { + "chatName":"Chat with Bob", + "otherMember":"CN=Bob, OU=Test Dept, O=R3, L=London, C=GB", + "message": "Hello Bob" + } +} + */ diff --git a/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/CreateNewChatFlowArgs.java b/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/CreateNewChatFlowArgs.java new file mode 100644 index 0000000..56c5b67 --- /dev/null +++ b/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/CreateNewChatFlowArgs.java @@ -0,0 +1,29 @@ +package com.r3.developers.csdetemplate.utxoexample.workflows; + +public class CreateNewChatFlowArgs{ + + // Serialisation service requires a default constructor + public CreateNewChatFlowArgs() {} + + private String chatName; + private String message; + private String otherMember; + + public CreateNewChatFlowArgs(String chatName, String message, String otherMember) { + this.chatName = chatName; + this.message = message; + this.otherMember = otherMember; + } + + public String getChatName() { + return chatName; + } + + public String getMessage() { + return message; + } + + public String getOtherMember() { + return otherMember; + } +} \ No newline at end of file diff --git a/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/FinalizeChatResponderFlow.java b/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/FinalizeChatResponderFlow.java new file mode 100644 index 0000000..51af173 --- /dev/null +++ b/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/FinalizeChatResponderFlow.java @@ -0,0 +1,62 @@ +package com.r3.developers.csdetemplate.utxoexample.workflows; + +import com.r3.developers.csdetemplate.utxoexample.states.ChatState; +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.messaging.FlowSession; +import net.corda.v5.base.annotations.Suspendable; +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; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Arrays; +import java.util.List; + +@InitiatedBy(protocol = "finalize-chat-protocol") +public class FinalizeChatResponderFlow implements ResponderFlow { + private final static Logger log = LoggerFactory.getLogger(FinalizeChatResponderFlow.class); + + @CordaInject + public UtxoLedgerService utxoLedgerService; + + @Suspendable + @Override + public void call(FlowSession session) { + + log.info("FinalizeChatResponderFlow.call() called"); + + try { + UtxoTransactionValidator txValidator = ledgerTransaction -> { + ChatState state = (ChatState) ledgerTransaction.getOutputContractStates().get(0); + if (checkForBannedWords(state.getMessage()) || !checkMessageFromMatchesCounterparty(state, session.getCounterparty())) { + throw new IllegalStateException("Failed verification"); + } + log.info("Verified the transaction - " + ledgerTransaction.getId()); + }; + + UtxoSignedTransaction finalizedSignedTransaction = utxoLedgerService.receiveFinality(session, txValidator); + log.info("Finished responder flow - " + finalizedSignedTransaction.getId()); + } + catch(Exception e) + { + log.warn("Exceptionally finished responder flow", e); + } + } + + + @Suspendable + Boolean checkForBannedWords(String str) { + List bannedWords = Arrays.asList("banana", "apple", "pear"); + return bannedWords.stream().anyMatch(str::contains); + } + + @Suspendable + Boolean checkMessageFromMatchesCounterparty(ChatState state, MemberX500Name otherMember) { + return state.getMessageFrom().equals(otherMember); + } + +} diff --git a/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/FinalizeChatSubFlow.java b/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/FinalizeChatSubFlow.java new file mode 100644 index 0000000..0148e0f --- /dev/null +++ b/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/FinalizeChatSubFlow.java @@ -0,0 +1,61 @@ +package com.r3.developers.csdetemplate.utxoexample.workflows; + +import net.corda.v5.application.flows.*; +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.ledger.utxo.UtxoLedgerService; +import net.corda.v5.ledger.utxo.transaction.UtxoSignedTransaction; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Arrays; +import java.util.List; + +@InitiatingFlow(protocol = "finalize-chat-protocol") +public class FinalizeChatSubFlow implements SubFlow { + + private final static Logger log = LoggerFactory.getLogger(FinalizeChatSubFlow.class); + private final UtxoSignedTransaction signedTransaction; + private final MemberX500Name otherMember; + + public FinalizeChatSubFlow(UtxoSignedTransaction signedTransaction, MemberX500Name otherMember) { + this.signedTransaction = signedTransaction; + this.otherMember = otherMember; + } + + @CordaInject + public UtxoLedgerService ledgerService; + + @CordaInject + public FlowMessaging flowMessaging; + + @Override + @Suspendable + public String call() { + + log.info("FinalizeChatFlow.call() called"); + + FlowSession session = flowMessaging.initiateFlow(otherMember); + + String result; + try { + List sessionsList = Arrays.asList(session); + + UtxoSignedTransaction finalizedSignedTransaction = ledgerService.finalize( + signedTransaction, + sessionsList + ); + + result = finalizedSignedTransaction.getId().toString(); + log.info("Success! Response: " + result); + + } catch (Exception e) { + log.warn("Finality failed", e); + result = "Finality failed, " + e.getMessage(); + } + + return result; + } +} diff --git a/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/GetChatFlow.java b/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/GetChatFlow.java new file mode 100644 index 0000000..66d1064 --- /dev/null +++ b/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/GetChatFlow.java @@ -0,0 +1,100 @@ +package com.r3.developers.csdetemplate.utxoexample.workflows; + +import com.r3.developers.csdetemplate.utxoexample.states.ChatState; +import net.corda.v5.application.flows.CordaInject; +import net.corda.v5.application.flows.RPCRequestData; +import net.corda.v5.application.flows.RPCStartableFlow; +import net.corda.v5.application.marshalling.JsonMarshallingService; +import net.corda.v5.base.annotations.Suspendable; +import net.corda.v5.base.exceptions.CordaRuntimeException; +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.UtxoLedgerTransaction; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.*; +import static java.util.Objects.*; +import static java.util.stream.Collectors.toList; + +public class GetChatFlow implements RPCStartableFlow { + + private final static Logger log = LoggerFactory.getLogger(GetChatFlow.class); + + @CordaInject + public JsonMarshallingService jsonMarshallingService; + + @CordaInject + public UtxoLedgerService ledgerService; + +// @NotNull + @Override + @Suspendable + public String call(RPCRequestData requestBody) { + + GetChatFlowArgs flowArgs = requestBody.getRequestBodyAs(jsonMarshallingService, GetChatFlowArgs.class); + + List> chatStateAndRefs = ledgerService.findUnconsumedStatesByType(ChatState.class); + List> chatStateAndRefsWithId = chatStateAndRefs.stream() + .filter(sar -> sar.getState().getContractState().getId().equals(flowArgs.getId())).collect(toList()); + if (chatStateAndRefsWithId.size() != 1) throw new CordaRuntimeException("Multiple or zero Chat states with id " + flowArgs.getId() + " found"); + StateAndRef chatStateAndRef = chatStateAndRefsWithId.get(0); + + return jsonMarshallingService.format(resolveMessagesFromBackchain(chatStateAndRef, flowArgs.getNumberOfRecords() )); + } + + @NotNull + @Suspendable + private List resolveMessagesFromBackchain(StateAndRef stateAndRef, int numberOfRecords) { + + List messages = new LinkedList<>(); + + StateAndRef currentStateAndRef = stateAndRef; + int recordsToFetch = numberOfRecords; + boolean moreBackchain = true; + + while (moreBackchain) { + + SecureHash transactionId = currentStateAndRef.getRef().getTransactionHash(); + + UtxoLedgerTransaction transaction = requireNonNull( + ledgerService.findLedgerTransaction(transactionId), + "Transaction " + transactionId + " not found." + ); + + List chatStates = transaction.getOutputStates(ChatState.class); + if (chatStates.size() != 1) throw new CordaRuntimeException( + "Expecting one and only one ChatState output for transaction " + transactionId + "."); + ChatState output = chatStates.get(0); + + messages.add(new GetChatResponse(output.getMessageFrom().toString(), output.getMessage())); + recordsToFetch--; + + List> inputStateAndRefs = transaction.getInputStateAndRefs(); + + if (inputStateAndRefs.isEmpty() || recordsToFetch == 0) { + moreBackchain = false; + } else if (inputStateAndRefs.size() > 1) { + throw new CordaRuntimeException("More than one input state found for transaction " + transactionId + "."); + } else { + currentStateAndRef = inputStateAndRefs.get(0); + } + } + return messages; + } +} + +/* +RequestBody for triggering the flow via http-rpc: +{ + "clientRequestId": "get-1", + "flowClassName": "com.r3.developers.csdetemplate.utxoexample.workflows.GetChatFlow", + "requestData": { + "id":"** fill in id **", + "numberOfRecords":"4" + } +} + */ + diff --git a/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/GetChatFlowArgs.java b/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/GetChatFlowArgs.java new file mode 100644 index 0000000..05d1934 --- /dev/null +++ b/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/GetChatFlowArgs.java @@ -0,0 +1,23 @@ +package com.r3.developers.csdetemplate.utxoexample.workflows; + +import java.util.UUID; + +public class GetChatFlowArgs { + + private UUID id; + private int numberOfRecords; + public GetChatFlowArgs() {} + + public GetChatFlowArgs(UUID id, int numberOfRecords ) { + this.id = id; + this.numberOfRecords = numberOfRecords; + } + + public UUID getId() { + return id; + } + + public int getNumberOfRecords() { + return numberOfRecords; + } +} diff --git a/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/GetChatResponse.java b/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/GetChatResponse.java new file mode 100644 index 0000000..4fbedc9 --- /dev/null +++ b/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/GetChatResponse.java @@ -0,0 +1,21 @@ +package com.r3.developers.csdetemplate.utxoexample.workflows; + +public class GetChatResponse { + + private String messageFrom; + private String message; + public GetChatResponse() {} + + public GetChatResponse(String messageFrom, String message) { + this.messageFrom = messageFrom; + this.message = message; + } + + public String getMessageFrom() { + return messageFrom; + } + + public String getMessage() { + return message; + } +} diff --git a/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/ListChatsFlow.java b/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/ListChatsFlow.java new file mode 100644 index 0000000..0e526cc --- /dev/null +++ b/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/ListChatsFlow.java @@ -0,0 +1,54 @@ +package com.r3.developers.csdetemplate.utxoexample.workflows; + +import com.r3.developers.csdetemplate.utxoexample.states.ChatState; +import net.corda.v5.application.flows.CordaInject; +import net.corda.v5.application.flows.RPCRequestData; +import net.corda.v5.application.flows.RPCStartableFlow; +import net.corda.v5.application.marshalling.JsonMarshallingService; +import net.corda.v5.base.annotations.Suspendable; +import net.corda.v5.ledger.utxo.StateAndRef; +import net.corda.v5.ledger.utxo.UtxoLedgerService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.*; +import java.util.stream.Collectors; + +public class ListChatsFlow implements RPCStartableFlow{ + + private final static Logger log = LoggerFactory.getLogger(ListChatsFlow.class); + + @CordaInject + public JsonMarshallingService jsonMarshallingService; + + @CordaInject + public UtxoLedgerService utxoLedgerService; + + @Suspendable + @Override + public String call(RPCRequestData requestBody) { + + log.info("ListChatsFlow.call() called"); + + List> states = utxoLedgerService.findUnconsumedStatesByType(ChatState.class); + List results = states.stream().map( stateAndRef -> + new ChatStateResults( + stateAndRef.getState().getContractState().getId(), + stateAndRef.getState().getContractState().getChatName(), + stateAndRef.getState().getContractState().getMessageFrom().toString(), + stateAndRef.getState().getContractState().getMessage() + ) + ).collect(Collectors.toList()); + + return jsonMarshallingService.format(results); + } +} + +/* +RequestBody for triggering the flow via http-rpc: +{ + "clientRequestId": "list-1", + "flowClassName": "com.r3.developers.csdetemplate.utxoexample.workflows.ListChatsFlow", + "requestData": {} +} +*/ \ No newline at end of file diff --git a/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/UpdateChatFlow.java b/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/UpdateChatFlow.java new file mode 100644 index 0000000..07227b2 --- /dev/null +++ b/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/UpdateChatFlow.java @@ -0,0 +1,103 @@ +package com.r3.developers.csdetemplate.utxoexample.workflows; + +import com.r3.developers.csdetemplate.utxoexample.contracts.ChatContract; +import com.r3.developers.csdetemplate.utxoexample.states.ChatState; +import net.corda.v5.application.flows.CordaInject; +import net.corda.v5.application.flows.FlowEngine; +import net.corda.v5.application.flows.RPCRequestData; +import net.corda.v5.application.flows.RPCStartableFlow; +import net.corda.v5.application.marshalling.JsonMarshallingService; +import net.corda.v5.application.membership.MemberLookup; +import net.corda.v5.base.annotations.Suspendable; +import net.corda.v5.base.exceptions.CordaRuntimeException; +import net.corda.v5.ledger.utxo.StateAndRef; +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 org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.time.Duration; +import java.time.Instant; +import java.util.List; + +import static java.util.Objects.*; +import static java.util.stream.Collectors.toList; + +public class UpdateChatFlow implements RPCStartableFlow { + + private final static Logger log = LoggerFactory.getLogger(UpdateChatFlow.class); + + @CordaInject + public JsonMarshallingService jsonMarshallingService; + + @CordaInject + public MemberLookup memberLookup; + + @CordaInject + public UtxoLedgerService ledgerService; + + @CordaInject + public FlowEngine flowEngine; + + @Suspendable + @Override + public String call(RPCRequestData requestBody) { + + log.info("UpdateNewChatFlow.call() called"); + + try { + UpdateChatFlowArgs flowArgs = requestBody.getRequestBodyAs(jsonMarshallingService, UpdateChatFlowArgs.class); + + List> chatStateAndRefs = ledgerService.findUnconsumedStatesByType(ChatState.class); + List> chatStateAndRefsWithId = chatStateAndRefs.stream() + .filter(sar -> sar.getState().getContractState().getId().equals(flowArgs.getId())).collect(toList()); + if (chatStateAndRefsWithId.size() != 1) throw new CordaRuntimeException("Multiple or zero Chat states with id " + flowArgs.getId() + " found"); + StateAndRef chatStateAndRef = chatStateAndRefsWithId.get(0); + + + MemberInfo myInfo = memberLookup.myInfo(); + ChatState state = chatStateAndRef.getState().getContractState(); + + List members = state.getParticipants().stream().map( + it -> requireNonNull(memberLookup.lookup(it), "Member not found from public Key "+ it + ".") + ).collect(toList()); + + members.remove(myInfo); + if(members.size() != 1) throw new RuntimeException("Should be only one participant other than the initiator"); + + MemberInfo otherMember = members.get(0); + + ChatState newChatState = state.updateMessage(myInfo.getName(), flowArgs.getMessage()); + + UtxoTransactionBuilder txBuilder = ledgerService.getTransactionBuilder() + .setNotary(chatStateAndRef.getState().getNotary()) + .setTimeWindowBetween(Instant.now(), Instant.now().plusMillis(Duration.ofDays(1).toMillis())) + .addOutputState(newChatState) + .addInputState(chatStateAndRef.getRef()) + .addCommand(new ChatContract.Update()) + .addSignatories(newChatState.getParticipants()); + + @SuppressWarnings("DEPRECATION") + UtxoSignedTransaction signedTransaction = txBuilder.toSignedTransaction(myInfo.getLedgerKeys().get(0)); + + return flowEngine.subFlow(new FinalizeChatSubFlow(signedTransaction, otherMember.getName())); + } catch (Exception e) { + log.warn("Failed to process utxo flow for request body " + requestBody + " because: " + e.getMessage()); + throw e; + } + } +} + +/* +RequestBody for triggering the flow via http-rpc: +{ + "clientRequestId": "update-1", + "flowClassName": "com.r3.developers.csdetemplate.utxoexample.workflows.UpdateChatFlow", + "requestData": { + "id":" ** fill in id **", + "message": "How are you today?" + } +} + */ \ No newline at end of file diff --git a/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/UpdateChatFlowArgs.java b/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/UpdateChatFlowArgs.java new file mode 100644 index 0000000..44d982d --- /dev/null +++ b/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/UpdateChatFlowArgs.java @@ -0,0 +1,23 @@ +package com.r3.developers.csdetemplate.utxoexample.workflows; + +import java.util.UUID; + +public class UpdateChatFlowArgs { + public UpdateChatFlowArgs() {} + + private UUID id; + private String message; + + public UpdateChatFlowArgs(UUID id, String message) { + this.id = id; + this.message = message; + } + + public UUID getId() { + return id; + } + + public String getMessage() { + return message; + } +} diff --git a/workflows/src/test/java/com/r3/developers/csdetemplate/workflows/MyFirstFlowTest.java b/workflows/src/test/java/com/r3/developers/csdetemplate/flowexample/workflows/MyFirstFlowTest.java similarity index 97% rename from workflows/src/test/java/com/r3/developers/csdetemplate/workflows/MyFirstFlowTest.java rename to workflows/src/test/java/com/r3/developers/csdetemplate/flowexample/workflows/MyFirstFlowTest.java index af2a32d..523ee95 100644 --- a/workflows/src/test/java/com/r3/developers/csdetemplate/workflows/MyFirstFlowTest.java +++ b/workflows/src/test/java/com/r3/developers/csdetemplate/flowexample/workflows/MyFirstFlowTest.java @@ -1,4 +1,4 @@ -package com.r3.developers.csdetemplate.workflows; +package com.r3.developers.csdetemplate.flowexample.workflows; import net.corda.simulator.RequestData; import net.corda.simulator.SimulatedVirtualNode;