From 3195235b366f71e891746796a2630ef82d9a4d00 Mon Sep 17 00:00:00 2001 From: Chris Barratt Date: Fri, 13 Jan 2023 17:35:53 +0000 Subject: [PATCH] WIP everything compiles --- .../utxoexample/contracts/ChatContract.java | 63 ++++++++++ .../utxoexample/states/ChatState.java | 103 +++++++++++++++ settings.gradle | 3 - .../{ => flowexample}/workflows/Message.java | 2 +- .../workflows/MyFirstFlow.java | 2 +- .../workflows/MyFirstFlowResponder.java | 2 +- .../workflows/MyFirstFlowStartArgs.java | 2 +- .../workflows/AppendChatResponderFlow.java | 45 +++++++ .../workflows/AppendChatSubFlow.java | 59 +++++++++ .../workflows/ChatStateResults.java | 51 ++++++++ .../workflows/CreateNewChatFlow.java | 118 ++++++++++++++++++ .../workflows/CreateNewChatFlowArgs.java | 41 ++++++ .../utxoexample/workflows/GetChatFlow.java | 98 +++++++++++++++ .../workflows/GetChatFlowArgs.java | 32 +++++ .../workflows/GetChatResponse.java | 28 +++++ .../utxoexample/workflows/ListChatsFlow.java | 60 +++++++++ .../workflows/ResponderValidationHelpers.java | 25 ++++ .../utxoexample/workflows/UpdateChatFlow.java | 110 ++++++++++++++++ .../workflows/UpdateChatFlowArgs.java | 32 +++++ .../workflows/utilities/CorDappHelpers.java | 24 ++++ .../workflows/MyFirstFlowTest.java | 2 +- 21 files changed, 894 insertions(+), 8 deletions(-) create mode 100644 contracts/src/main/java/com/r3/developers/csdetemplate/utxoexample/contracts/ChatContract.java create mode 100644 contracts/src/main/java/com/r3/developers/csdetemplate/utxoexample/states/ChatState.java rename workflows/src/main/java/com/r3/developers/csdetemplate/{ => flowexample}/workflows/Message.java (90%) rename workflows/src/main/java/com/r3/developers/csdetemplate/{ => flowexample}/workflows/MyFirstFlow.java (98%) rename workflows/src/main/java/com/r3/developers/csdetemplate/{ => flowexample}/workflows/MyFirstFlowResponder.java (98%) rename workflows/src/main/java/com/r3/developers/csdetemplate/{ => flowexample}/workflows/MyFirstFlowStartArgs.java (87%) create mode 100644 workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/AppendChatResponderFlow.java create mode 100644 workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/AppendChatSubFlow.java create mode 100644 workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/ChatStateResults.java create mode 100644 workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/CreateNewChatFlow.java create mode 100644 workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/CreateNewChatFlowArgs.java create mode 100644 workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/GetChatFlow.java create mode 100644 workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/GetChatFlowArgs.java create mode 100644 workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/GetChatResponse.java create mode 100644 workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/ListChatsFlow.java create mode 100644 workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/ResponderValidationHelpers.java create mode 100644 workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/UpdateChatFlow.java create mode 100644 workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/UpdateChatFlowArgs.java create mode 100644 workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/utilities/CorDappHelpers.java rename workflows/src/test/java/com/r3/developers/csdetemplate/{ => flowexample}/workflows/MyFirstFlowTest.java (97%) 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..dfd5f6f --- /dev/null +++ b/contracts/src/main/java/com/r3/developers/csdetemplate/utxoexample/contracts/ChatContract.java @@ -0,0 +1,63 @@ +package com.r3.developers.csdetemplate.utxoexample.contracts; + +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.transaction.UtxoLedgerTransaction; +import org.jetbrains.annotations.NotNull; + +import java.security.PublicKey; +import java.util.Set; + +import static java.util.Objects.*; + +public class ChatContract implements Contract { + + 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(@NotNull UtxoLedgerTransaction transaction) throws IllegalArgumentException { + Command command = requireNonNull( transaction.getCommands().get(0), "Require a single command"); + + if(command.getClass() == Create.class) { + requireThat(transaction.getInputContractStates().isEmpty(), "When command is Create there should be no input state"); + 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"); + } + else { + throw new IllegalArgumentException("Unsupported command"); + } + + /* + // This seems to be make Intellij unhappy. + switch(command.getClass()) { + case Create.class: + requireThat(transaction.getInputContractStates().isEmpty(), "When command is Create there should be no input state"); + requireThat(transaction.getOutputContractStates().size() == 1, "When command is Create there should be one and only one output state"); + break; + case 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"); + break; + default: + throw new IllegalArgumentException("Unsupported command"); + } + + */ + } + + private void requireThat(boolean asserted, String errorMessage) throws IllegalArgumentException { + if(!asserted) { + throw new IllegalArgumentException("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..289d2a0 --- /dev/null +++ b/contracts/src/main/java/com/r3/developers/csdetemplate/utxoexample/states/ChatState.java @@ -0,0 +1,103 @@ +package com.r3.developers.csdetemplate.utxoexample.states; + +import com.r3.developers.csdetemplate.utxoexample.contracts.ChatContract; +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.*; + +/* +@BelongsToContract(ChatContract::class) +data class ChatState( + val id : UUID = UUID.randomUUID(), + val chatName: String, + val messageFrom: MemberX500Name, + val message: String, + override val participants: List) : ContractState { + + fun updateMessage(messageFrom: MemberX500Name, message: String) = copy(messageFrom = messageFrom, message = message) + } + +*/ + +@BelongsToContract(ChatContract.class) +public class ChatState implements ContractState { + public ChatState() { + } + + 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; + } + + public ChatState(String chatName, + MemberX500Name messageFrom, + String message, + List participants + ) { + this(UUID.randomUUID(), + chatName, + messageFrom, + message, + participants); + } + + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + public String getChatName() { + return chatName; + } + + public void setChatName(String chatName) { + this.chatName = chatName; + } + + public MemberX500Name getMessageFrom() { + return messageFrom; + } + + public void setMessageFrom(MemberX500Name messageFrom) { + this.messageFrom = messageFrom; + } + + public String getMessage() { + return message; + } + + @NotNull + @Override + public List getParticipants() { + return participants; + } + + public void setParticipants(List participants) { + this.participants = participants; + } + + private UUID id; + private String chatName; + private MemberX500Name messageFrom; + private String message; + List participants; + + public ChatState updateMessage(MemberX500Name name, String message) { + return new ChatState(chatName, name, message, 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 90% 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..7e2a44e 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; 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 98% 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..fc51cb2 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; 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 98% 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..efdbdfe 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; 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 87% 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..c67cad3 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,4 +1,4 @@ -package com.r3.developers.csdetemplate.workflows; +package com.r3.developers.csdetemplate.flowexample.workflows; import net.corda.v5.base.types.MemberX500Name; diff --git a/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/AppendChatResponderFlow.java b/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/AppendChatResponderFlow.java new file mode 100644 index 0000000..4abc2fd --- /dev/null +++ b/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/AppendChatResponderFlow.java @@ -0,0 +1,45 @@ +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.ledger.utxo.UtxoLedgerService; +import net.corda.v5.ledger.utxo.transaction.UtxoSignedTransaction; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static com.r3.developers.csdetemplate.utxoexample.workflows.ResponderValidationHelpers.checkForBannedWords; +import static com.r3.developers.csdetemplate.utxoexample.workflows.ResponderValidationHelpers.checkMessageFromMatchesCounterparty; + +@InitiatedBy(protocol = "append-chat-protocol") +class AppendChatResponderFlow implements ResponderFlow { + +private final Logger log = LoggerFactory.getLogger(AppendChatResponderFlow.class); + +@CordaInject +public UtxoLedgerService utxoLedgerService; + + @Suspendable + @Override + public void call(@NotNull FlowSession session) { + try { + UtxoSignedTransaction finalizedSignedTransaction = utxoLedgerService.receiveFinality(session, ledgerTransaction -> + { + ChatState state = (ChatState) ledgerTransaction.getInputContractStates().get(0); + if (checkForBannedWords(state.getMessage()) || !checkMessageFromMatchesCounterparty(state, session.getCounterparty())) { + throw new IllegalStateException("Failed verification"); + } + log.info("Verified the transaction - " + ledgerTransaction.getId()); + }); + log.info("Finished responder flow - " + finalizedSignedTransaction.getId()); + } + catch(Exception e) + { + log.warn("Exceptionally finished responder flow", e); + } + } +} diff --git a/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/AppendChatSubFlow.java b/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/AppendChatSubFlow.java new file mode 100644 index 0000000..9eb83a2 --- /dev/null +++ b/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/AppendChatSubFlow.java @@ -0,0 +1,59 @@ +package com.r3.developers.csdetemplate.utxoexample.workflows; + +import com.r3.developers.csdetemplate.utxoexample.states.ChatState; +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 = "append-chat-protocol") +public class AppendChatSubFlow implements SubFlow { + + public AppendChatSubFlow() {} + public AppendChatSubFlow(UtxoSignedTransaction signedTransaction, MemberX500Name otherMember) { + this.signedTransaction = signedTransaction; + this.otherMember = otherMember; + } + + private final Logger log = LoggerFactory.getLogger(AppendChatSubFlow.class); + + @CordaInject + public UtxoLedgerService ledgerService; + + @CordaInject + public FlowMessaging flowMessaging; + + @Override + @Suspendable + public String call() { + + log.info("AppendChatFlow.call() called"); + + FlowSession session = flowMessaging.initiateFlow(otherMember); + + String retVal; + try { + UtxoSignedTransaction finalizedSignedTransaction = ledgerService.finalize( + signedTransaction, + List.of(session) + ); + retVal = finalizedSignedTransaction.getId().toString(); + log.info("Success! Response: " + retVal); + } catch (Exception e) { + log.warn("Finality failed", e); + retVal = "Finality failed, " + e.getMessage(); + } + return retVal; + } + + private UtxoSignedTransaction signedTransaction; + private MemberX500Name 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..383c3de --- /dev/null +++ b/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/ChatStateResults.java @@ -0,0 +1,51 @@ +package com.r3.developers.csdetemplate.utxoexample.workflows; + +import java.util.UUID; + +public class ChatStateResults { + 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 void setId(UUID id) { + this.id = id; + } + + public String getChatName() { + return chatName; + } + + public void setChatName(String chatName) { + this.chatName = chatName; + } + + public String getMessageFromName() { + return messageFromName; + } + + public void setMessageFromName(String messageFromName) { + this.messageFromName = messageFromName; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + private UUID id; + private String chatName; + private String messageFromName; + private String 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..6af7cea --- /dev/null +++ b/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/CreateNewChatFlow.java @@ -0,0 +1,118 @@ +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.application.messaging.FlowMessaging; +import net.corda.v5.base.annotations.Suspendable; +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.jetbrains.annotations.NotNull; +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.List; +import java.util.Objects; +import java.util.function.Predicate; + +import static java.util.Objects.*; + +@InitiatingFlow(protocol = "create-chat-protocol") +public class CreateNewChatFlow implements RPCStartableFlow { + + private final Logger log = LoggerFactory.getLogger(CreateNewChatFlow.class); + + @CordaInject + public JsonMarshallingService jsonMarshallingService; + + @CordaInject + public MemberLookup memberLookup; + + @CordaInject + public UtxoLedgerService ledgerService; + + @CordaInject + public NotaryLookup notaryLookup; + + @CordaInject + public FlowMessaging flowMessaging; + + @CordaInject + public FlowEngine flowEngine; + + @NotNull + @Suspendable + @Override + public String call(@NotNull RPCRequestData requestBody) throws IllegalArgumentException { + 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())), + "can't find other member" + ); + + ChatState chatState = new ChatState( + flowArgs.getChatName(), + myInfo.getName(), + flowArgs.getMessage(), + Arrays.asList(myInfo.getLedgerKeys().get(0), otherMember.getLedgerKeys().get(0)) + ); + + NotaryInfo notary = notaryLookup.getNotaryServices().iterator().next(); + Predicate myPred = memberInfo -> Objects.equals( + memberInfo.getMemberProvidedContext().get("corda.notary.service.name"), + notary.getName().toString() + ); + + List lmi = memberLookup.lookup(); + MemberInfo thing = lmi.stream().filter(myPred).iterator().next(); + PublicKey notaryKey = thing.getLedgerKeys().get(0); + + 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 AppendChatSubFlow(signedTransaction, otherMember.getName())); + } + catch (Exception e) { + log.warn("Failed to process utxo flow for request body '$requestBody' because:'${e.message}'"); + throw e; + } + } +} + + +/* +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..4fb711b --- /dev/null +++ b/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/CreateNewChatFlowArgs.java @@ -0,0 +1,41 @@ +package com.r3.developers.csdetemplate.utxoexample.workflows; + +public class CreateNewChatFlowArgs{ + + public CreateNewChatFlowArgs() {} + public CreateNewChatFlowArgs(String chatName, String message, String otherMember) { + this.chatName = chatName; + this.message = message; + this.otherMember = otherMember; + } + + public String getChatName() { + return chatName; + } + + public void setChatName(String chatName) { + this.chatName = chatName; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public String getOtherMember() { + return otherMember; + } + + public void setOtherMember(String otherMember) { + this.otherMember = otherMember; + } + + private String chatName; + private String message; + + + private String otherMember; +} \ No newline at end of file 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..264468e --- /dev/null +++ b/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/GetChatFlow.java @@ -0,0 +1,98 @@ +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.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 com.r3.developers.csdetemplate.utxoexample.workflows.utilities.CorDappHelpers.findAndExpectExactlyOne; +import static java.util.Objects.*; + +public class GetChatFlow implements RPCStartableFlow { + + private final Logger log = LoggerFactory.getLogger(GetChatFlow.class); + + @CordaInject + public JsonMarshallingService jsonMarshallingService; + + @CordaInject + public UtxoLedgerService ledgerService; + + @NotNull + @Override + @Suspendable + public String call(RPCRequestData requestBody) throws IllegalArgumentException { + GetChatFlowArgs flowArgs = requestBody.getRequestBodyAs(jsonMarshallingService, GetChatFlowArgs.class); + List> stateAndRefs = ledgerService.findUnconsumedStatesByType(ChatState.class); + + StateAndRef state = findAndExpectExactlyOne(stateAndRefs, + stateAndRef -> stateAndRef.getState().getContractState().getId() == flowArgs.getId(), + "did not find an unique ChatState" + ); + + return jsonMarshallingService.format(resolveMessagesFromBackchain(state, flowArgs.getNumberOfRecords() )); + } + + @NotNull + @Suspendable + private List resolveMessagesFromBackchain(StateAndRef stateAndRef, int numberOfRecords) throws IllegalArgumentException { + + 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" + ); + + ChatState output = findAndExpectExactlyOne( + transaction.getOutputStates(ChatState.class), + "Expecting one and only one ChatState output for transaction " + transactionId + ); + + 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 IllegalArgumentException("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..4250549 --- /dev/null +++ b/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/GetChatFlowArgs.java @@ -0,0 +1,32 @@ +package com.r3.developers.csdetemplate.utxoexample.workflows; + +import java.util.UUID; + +public class GetChatFlowArgs { + public GetChatFlowArgs() {} + + public GetChatFlowArgs(UUID id, int numberOfRecords ) { + this.id = id; + this.numberOfRecords = numberOfRecords; + } + + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + + public int getNumberOfRecords() { + return numberOfRecords; + } + + public void setNumberOfRecords(int numberOfRecords) { + this.numberOfRecords = numberOfRecords; + } + + private UUID id; + private int 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..2425faf --- /dev/null +++ b/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/GetChatResponse.java @@ -0,0 +1,28 @@ +package com.r3.developers.csdetemplate.utxoexample.workflows; + +public class GetChatResponse { + public GetChatResponse() {} + public GetChatResponse(String messageFrom, String message) { + this.messageFrom = messageFrom; + this.message = message; + } + + public String getMessageFrom() { + return messageFrom; + } + + public void setMessageFrom(String messageFrom) { + this.messageFrom = messageFrom; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + private String messageFrom; + private String 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..3ec4854 --- /dev/null +++ b/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/ListChatsFlow.java @@ -0,0 +1,60 @@ +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.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.application.messaging.FlowMessaging; +import net.corda.v5.base.annotations.Suspendable; +import net.corda.v5.ledger.common.NotaryLookup; +import net.corda.v5.ledger.utxo.StateAndRef; +import net.corda.v5.ledger.utxo.UtxoLedgerService; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.*; +import java.util.stream.Collectors; + +public class ListChatsFlow implements RPCStartableFlow{ + + private final Logger log = LoggerFactory.getLogger(ListChatsFlow.class); + + @CordaInject + public JsonMarshallingService jsonMarshallingService; + + @CordaInject + public UtxoLedgerService utxoLedgerService; + + @NotNull + @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/ResponderValidationHelpers.java b/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/ResponderValidationHelpers.java new file mode 100644 index 0000000..245d9bb --- /dev/null +++ b/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/ResponderValidationHelpers.java @@ -0,0 +1,25 @@ +package com.r3.developers.csdetemplate.utxoexample.workflows; + +import com.r3.developers.csdetemplate.utxoexample.states.ChatState; +import net.corda.v5.base.annotations.Suspendable; +import net.corda.v5.base.types.MemberX500Name; + +import java.util.Arrays; +import java.util.List; + +public final class ResponderValidationHelpers { + public final static List bannedWords = Arrays.asList("banana", "apple", "pear"); + + @Suspendable + public static boolean checkForBannedWords(String str) { + return bannedWords.stream().anyMatch(str::contains); + } + + @Suspendable + public static boolean checkMessageFromMatchesCounterparty(ChatState state, MemberX500Name otherMember) { + return state.getMessageFrom() == otherMember; + } + + // This class just introduces a scope for some helper functions and should not be instantiated. + private ResponderValidationHelpers() {} +} 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..e218af4 --- /dev/null +++ b/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/UpdateChatFlow.java @@ -0,0 +1,110 @@ +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.application.messaging.FlowMessaging; +import net.corda.v5.base.annotations.Suspendable; +import net.corda.v5.ledger.common.NotaryLookup; +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.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.time.Duration; +import java.time.Instant; +import java.util.List; +import java.util.stream.Collectors; + +import static com.r3.developers.csdetemplate.utxoexample.workflows.utilities.CorDappHelpers.findAndExpectExactlyOne; +import static java.util.Objects.*; + +public class UpdateChatFlow implements RPCStartableFlow { + + private final Logger log = LoggerFactory.getLogger(UpdateChatFlow.class); + + @CordaInject + public JsonMarshallingService jsonMarshallingService; + + @CordaInject + public MemberLookup memberLookup; + + @CordaInject + public UtxoLedgerService ledgerService; + + @CordaInject + public NotaryLookup notaryLookup; + + @CordaInject + public FlowMessaging flowMessaging; + + @CordaInject + public FlowEngine flowEngine; + + @NotNull + @Suspendable + @Override + public String call(RPCRequestData requestBody) throws IllegalArgumentException{ + + log.info("UpdateNewChatFlow.call() called"); + + try { + UpdateChatFlowArgs flowArgs = requestBody.getRequestBodyAs(jsonMarshallingService, UpdateChatFlowArgs.class); + + // look up state (this is very inefficient) + StateAndRef stateAndRef = findAndExpectExactlyOne( + ledgerService.findUnconsumedStatesByType(ChatState.class), + sAndR -> sAndR.getState().getContractState().getId() == flowArgs.getId(), + "Multiple or zero Chat states with id " + flowArgs.getId() + " found" + ); + + MemberInfo myInfo = memberLookup.myInfo(); + ChatState state = stateAndRef.getState().getContractState(); + + List members = state.getParticipants().stream().map( + it -> requireNonNull(memberLookup.lookup(it), "Member not found from Key") + ).collect(Collectors.toList()); + + + // Now we want to check that there is only + /* + val otherMember = (members - myInfo).singleOrNull() + ?: throw Exception("Should be only one participant other than the initiator") + + */ + // NEED TO ADD CHECKS + members.remove(myInfo); + MemberInfo otherMember = members.get(0); + + // This needs to be a deep copy? + ChatState newChatState = state.updateMessage(myInfo.getName(), flowArgs.getMessage()); + + UtxoTransactionBuilder txBuilder = ledgerService.getTransactionBuilder() + .setNotary(stateAndRef.getState().getNotary()) + .setTimeWindowBetween(Instant.now(), Instant.now().plusMillis(Duration.ofDays(1).toMillis())) + .addOutputState(newChatState) + .addInputState(stateAndRef.getRef()) + .addCommand(new ChatContract.Update()) + .addSignatories(newChatState.getParticipants()); + + @SuppressWarnings("DEPRECATION") + UtxoSignedTransaction signedTransaction = txBuilder.toSignedTransaction(myInfo.getLedgerKeys().get(0)); + + return flowEngine.subFlow(new AppendChatSubFlow(signedTransaction, otherMember.getName())); + + } catch (Exception e) { + log.warn("Failed to process utxo flow for request body '$requestBody' because:'${e.message}'"); + throw e; + } + } +} + 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..c171bca --- /dev/null +++ b/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/UpdateChatFlowArgs.java @@ -0,0 +1,32 @@ +package com.r3.developers.csdetemplate.utxoexample.workflows; + +import java.util.UUID; + +public class UpdateChatFlowArgs { + public UpdateChatFlowArgs() {} + + public UpdateChatFlowArgs(UUID id, String message) { + this.id = id; + this.message = message; + } + + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + private UUID id; + private String message; + +} diff --git a/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/utilities/CorDappHelpers.java b/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/utilities/CorDappHelpers.java new file mode 100644 index 0000000..7a22c44 --- /dev/null +++ b/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/utilities/CorDappHelpers.java @@ -0,0 +1,24 @@ +package com.r3.developers.csdetemplate.utxoexample.workflows.utilities; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +public final class CorDappHelpers { + public static T findAndExpectExactlyOne(Collection collection, Predicate filterFn, String exceptionMsg) throws IllegalArgumentException + { + Collection results = collection.stream().filter(filterFn).collect(Collectors.toList()); + if(results.size() != 1){ + throw new IllegalArgumentException(exceptionMsg); + //throw new Exception(exceptionMsg); + } + return results.iterator().next(); + } + + public static T findAndExpectExactlyOne(Collection collection, String exceptionMsg) throws IllegalArgumentException { + return findAndExpectExactlyOne(collection, e -> true, exceptionMsg); + } +} 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;