Merge pull request #14 from corda/mattb/CORE-8063-review-and-complete-kotlin-to-java-conversion

Mattb/core 8063 review and complete kotlin to java conversion
This commit is contained in:
Matt Bradbury 2023-01-29 21:44:14 +00:00 committed by GitHub
commit 3fcf9a1c49
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 914 additions and 37 deletions

88
.gitignore vendored Normal file
View File

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

8
.idea/.gitignore vendored Normal file
View File

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

View File

@ -2,19 +2,15 @@ import static org.gradle.api.JavaVersion.VERSION_11
plugins { plugins {
id 'org.jetbrains.kotlin.jvm' id 'org.jetbrains.kotlin.jvm'
id 'net.corda.cordapp.cordapp-configuration' id 'net.corda.cordapp.cordapp-configuration'
id 'org.jetbrains.kotlin.plugin.jpa' id 'org.jetbrains.kotlin.plugin.jpa'
id 'java' id 'java'
id 'maven-publish' id 'maven-publish'
id 'csde' id 'csde'
} }
allprojects { allprojects {
group 'com.r3.hellocorda' group 'com.r3.developers.csdetemplate'
version '1.0-SNAPSHOT' version '1.0-SNAPSHOT'
def javaVersion = VERSION_11 def javaVersion = VERSION_11

View File

@ -10,6 +10,7 @@
// todo: add test corda running/live task // 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 test to check docker is running and display error if not + halt start corda
// todo: add a clean corda task. // todo: add a clean corda task.
// todo: fix logging level and make it configurable.
import com.r3.csde.CordaLifeCycleHelper import com.r3.csde.CordaLifeCycleHelper

View File

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

View File

@ -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<PublicKey> 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<PublicKey> 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<PublicKey> 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<PublicKey> 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 +
// ")";
// }
}

View File

@ -22,6 +22,3 @@ rootProject.name = 'csde-cordapp-template-java'
include ':workflows' include ':workflows'
include ':contracts' include ':contracts'

View File

@ -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.annotations.CordaSerializable;
import net.corda.v5.base.types.MemberX500Name; 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. // send it from one virtual node to another.
@CordaSerializable @CordaSerializable
public class Message { public class Message {
private MemberX500Name sender;
private String message;
public Message(MemberX500Name sender, String message) { public Message(MemberX500Name sender, String message) {
this.sender = sender; this.sender = sender;
this.message = message; this.message = message;
@ -20,6 +24,5 @@ public class Message {
return message; return message;
} }
public MemberX500Name sender;
public String message;
} }

View File

@ -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.flows.*;
import net.corda.v5.application.marshalling.JsonMarshallingService; import net.corda.v5.application.marshalling.JsonMarshallingService;
@ -19,7 +19,7 @@ import org.slf4j.LoggerFactory;
public class MyFirstFlow implements RPCStartableFlow { public class MyFirstFlow implements RPCStartableFlow {
// Log messages from the flows for debugging. // 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. // 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. // 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); MyFirstFlowStartArgs flowArgs = requestBody.getRequestBodyAs(jsonMarshallingService, MyFirstFlowStartArgs.class);
// Obtain the MemberX500Name of the counterparty. // Obtain the MemberX500Name of the counterparty.
MemberX500Name otherMember = flowArgs.otherMember; MemberX500Name otherMember = flowArgs.getOtherMember();
// Get our identity from the MemberLookup service. // Get our identity from the MemberLookup service.
MemberX500Name ourIdentity = memberLookup.myInfo().getName(); MemberX500Name ourIdentity = memberLookup.myInfo().getName();
@ -67,7 +67,7 @@ public class MyFirstFlow implements RPCStartableFlow {
Message message = new Message(otherMember, "Hello from " + ourIdentity + "."); Message message = new Message(otherMember, "Hello from " + ourIdentity + ".");
// Log the message to be sent. // 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. // Start a flow session with the otherMember using the FlowMessaging service.
// The otherMember's virtual node will run the corresponding MyFirstFlowResponder responder flow. // 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 // 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 // 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. // value from the flow when testing using the simulator.
return response.message; return response.getMessage();
} }
} }
/* /*
RequestBody for triggering the flow via http-rpc: RequestBody for triggering the flow via http-rpc:
{ {
"clientRequestId": "r1", "clientRequestId": "r1",
"flowClassName": "com.r3.developers.csdetemplate.workflows.MyFirstFlow", "flowClassName": "com.r3.developers.csdetemplate.flowexample.workflows.MyFirstFlow",
"requestData": { "requestData": {
"otherMember":"CN=Bob, OU=Test Dept, O=R3, L=London, C=GB" "otherMember":"CN=Bob, OU=Test Dept, O=R3, L=London, C=GB"
} }

View File

@ -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.CordaInject;
import net.corda.v5.application.flows.InitiatedBy; import net.corda.v5.application.flows.InitiatedBy;
@ -18,7 +18,7 @@ import org.slf4j.LoggerFactory;
public class MyFirstFlowResponder implements ResponderFlow { public class MyFirstFlowResponder implements ResponderFlow {
// Log messages from the flows for debugging. // 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 // MemberLookup looks for information about members of the virtual network which
// this CorDapp operates in. // this CorDapp operates in.
@ -44,7 +44,7 @@ public class MyFirstFlowResponder implements ResponderFlow {
Message receivedMessage = session.receive(Message.class); Message receivedMessage = session.receive(Message.class);
// Log the message as a proxy for performing some useful operation on it. // 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. // Get our identity from the MemberLookup service.
MemberX500Name ourIdentity = memberLookup.myInfo().getName(); MemberX500Name ourIdentity = memberLookup.myInfo().getName();
@ -54,19 +54,9 @@ public class MyFirstFlowResponder implements ResponderFlow {
"Hello " + session.getCounterparty().getCommonName() + ", best wishes from " + ourIdentity.getCommonName()); "Hello " + session.getCounterparty().getCommonName() + ", best wishes from " + ourIdentity.getCommonName());
// Log the response to be sent. // 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 // Send the response via the send method on the flow session
session.send(response); 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"
}
}
*/

View File

@ -1,15 +1,19 @@
package com.r3.developers.csdetemplate.workflows; package com.r3.developers.csdetemplate.flowexample.workflows;
import net.corda.v5.base.types.MemberX500Name; import net.corda.v5.base.types.MemberX500Name;
// A class to hold the arguments required to start the flow // A class to hold the arguments required to start the flow
public class MyFirstFlowStartArgs { 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) { public MyFirstFlowStartArgs(MemberX500Name otherMember) {
this.otherMember = otherMember; this.otherMember = otherMember;
} }
// The JSON Marshalling Service, which handles serialisation, needs this constructor. public MemberX500Name getOtherMember() {
public MyFirstFlowStartArgs() {} return otherMember;
}
} }

View File

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

View File

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

View File

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

View File

@ -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<String> bannedWords = Arrays.asList("banana", "apple", "pear");
return bannedWords.stream().anyMatch(str::contains);
}
@Suspendable
Boolean checkMessageFromMatchesCounterparty(ChatState state, MemberX500Name otherMember) {
return state.getMessageFrom().equals(otherMember);
}
}

View File

@ -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<String> {
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<FlowSession> 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;
}
}

View File

@ -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<StateAndRef<ChatState>> chatStateAndRefs = ledgerService.findUnconsumedStatesByType(ChatState.class);
List<StateAndRef<ChatState>> 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<ChatState> chatStateAndRef = chatStateAndRefsWithId.get(0);
return jsonMarshallingService.format(resolveMessagesFromBackchain(chatStateAndRef, flowArgs.getNumberOfRecords() ));
}
@NotNull
@Suspendable
private List<GetChatResponse> resolveMessagesFromBackchain(StateAndRef<?> stateAndRef, int numberOfRecords) {
List<GetChatResponse> 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<ChatState> 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<StateAndRef<?>> 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"
}
}
*/

View File

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

View File

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

View File

@ -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<StateAndRef<ChatState>> states = utxoLedgerService.findUnconsumedStatesByType(ChatState.class);
List<ChatStateResults> 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": {}
}
*/

View File

@ -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<StateAndRef<ChatState>> chatStateAndRefs = ledgerService.findUnconsumedStatesByType(ChatState.class);
List<StateAndRef<ChatState>> 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<ChatState> chatStateAndRef = chatStateAndRefsWithId.get(0);
MemberInfo myInfo = memberLookup.myInfo();
ChatState state = chatStateAndRef.getState().getContractState();
List<MemberInfo> 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?"
}
}
*/

View File

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

View File

@ -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.RequestData;
import net.corda.simulator.SimulatedVirtualNode; import net.corda.simulator.SimulatedVirtualNode;