This commit is contained in:
djmil 2023-08-14 21:37:54 +02:00
parent 3a4cba0c63
commit d8c2382883
38 changed files with 2097 additions and 0 deletions

10
corda/.ci/Jenkinsfile vendored Normal file
View File

@ -0,0 +1,10 @@
@Library('corda-shared-build-pipeline-steps@5.1') _
cordaPipeline(
nexusAppId: 'com.corda.CSDE-Java.5.0',
publishRepoPrefix: '',
slimBuild: true,
runUnitTests: false,
dedicatedJobForSnykDelta: false,
slackChannel: '#corda-corda5-dev-ex-build-notifications'
)

View File

@ -0,0 +1,6 @@
@Library('corda-shared-build-pipeline-steps@5.1') _
cordaSnykScanPipeline (
snykTokenId: 'r3-snyk-corda5',
snykAdditionalCommands: "--all-sub-projects -d"
)

View File

@ -0,0 +1,14 @@
name: 'Check PR Title'
on:
pull_request:
types: [opened, edited, reopened]
jobs:
check-pr-title:
runs-on: ubuntu-latest
steps:
- uses: morrisoncole/pr-lint-action@v1.6.1
with:
title-regex: '^((CORDA|EG|ENT|INFRA|CORE|ES)-\d+)(.*)'
on-failed-regex-comment: "PR title failed to match regex -> `%regex%`"
repo-token: "${{ secrets.GITHUB_TOKEN }}"

91
corda/.gitignore vendored Normal file
View File

@ -0,0 +1,91 @@
# 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
# Ignore temporary data files
*.dat

View File

@ -0,0 +1,15 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="DebugCorDapp" type="Remote">
<option name="USE_SOCKET_TRANSPORT" value="true" />
<option name="SERVER_MODE" value="false" />
<option name="SHMEM_ADDRESS" />
<option name="HOST" value="localhost" />
<option name="PORT" value="5005" />
<option name="AUTO_RESTART" value="false" />
<RunnerSettings RunnerId="Debug">
<option name="DEBUG_PORT" value="5005" />
<option name="LOCAL" value="false" />
</RunnerSettings>
<method v="2" />
</configuration>
</component>

14
corda/.snyk Normal file
View File

@ -0,0 +1,14 @@
# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities.
version: v1.25.0
# ignores vulnerabilities until expiry date; change duration by modifying expiry date
ignore:
SNYK-JAVA-ORGJETBRAINSKOTLIN-2393744:
- '*':
reason: >-
This vulnerability relates to information exposure via creation of
temporary files (via Kotlin functions) with insecure permissions.
Corda does not use any of the vulnerable functions so it is not
susceptible to this vulnerability
expires: 2023-10-19T17:15:26.836Z
created: 2023-02-02T17:15:26.839Z
patch: {}

112
corda/README.md Normal file
View File

@ -0,0 +1,112 @@
# CSDE-cordapp-template-java
To help make the process of prototyping CorDapps on Corda 5 more straight forward we have developed the Cordapp Standard Development Environment (CSDE).
The CSDE is obtained by cloning this CSDE-Cordapp-Template-Java repository to your local machine. The CSDE provides:
- A pre-setup Cordapp Project which you can use as a starting point to develop your own prototypes.
- A base Gradle configuration which brings in the dependencies you need to write and test a Corda 5 Cordapp.
- A set of Gradle helper tasks which speed up and simplify the development and deployment process.
- Debug configuration for debugging a local Corda cluster.
- The MyFirstFlow code which forms the basis of this getting started documentation, this is located in package com.r3.developers.csdetemplate.flowexample
- A UTXO example in package com.r3.developers.csdetemplate.utxoexample packages
- Ability to configure the Members of the Local Corda Network.
To find out how to use the CSDE, please refer to the *Getting Started Using the CSDE* subsection within the *Developing Applications* section in the latest Corda 5 documentation at https://docs.r3.com/
## Chat app
We have built a simple one to one chat app to demo some functionalities of the next gen Corda platform.
In this app you can:
1. Create a new chat with a counterparty. `CreateNewChatFlow`
2. List out the chat entries you had. `ListChatsFlow`
3. Individually query out the history of one chat entry. `GetChatFlowArgs`
4. Continue chatting within the chat entry with the counterparty. `UpdateChatFlow`
### Setting up
1. We will begin our test deployment with clicking the `startCorda`. This task will load up the combined Corda workers in docker.
A successful deployment will allow you to open the REST APIs at: https://localhost:8888/api/v1/swagger#. You can test out some of the
functions to check connectivity. (GET /cpi function call should return an empty list as for now.)
2. We will now deploy the cordapp with a click of `5-vNodeSetup` task. Upon successful deployment of the CPI, the GET /cpi function call should now return the meta data of the cpi you just upload
### Running the chat app
In Corda 5, flows will be triggered via `POST /flow/{holdingidentityshorthash}` and flow result will need to be view at `GET /flow/{holdingidentityshorthash}/{clientrequestid}`
* holdingidentityshorthash: the id of the network participants, ie Bob, Alice, Charlie. You can view all the short hashes of the network member with another gradle task called `ListVNodes`
* clientrequestid: the id you specify in the flow requestBody when you trigger a flow.
#### Step 1: Create Chat Entry
Pick a VNode identity to initiate the chat, and get its short hash. (Let's pick Alice. Dont pick Bob because Bob is the person who we will have the chat with).
Go to `POST /flow/{holdingidentityshorthash}`, enter the identity short hash(Alice's hash) and request body:
```
{
"clientRequestId": "create-1",
"flowClassName": "com.r3.developers.csdetemplate.utxoexample.workflows.CreateNewChatFlow",
"requestBody": {
"chatName":"Chat with Bob",
"otherMember":"CN=Bob, OU=Test Dept, O=R3, L=London, C=GB",
"message": "Hello Bob"
}
}
```
After trigger the create-chat flow, hop to `GET /flow/{holdingidentityshorthash}/{clientrequestid}` and enter the short hash(Alice's hash) and clientrequestid to view the flow result
#### Step 2: List the chat
In order to continue the chat, we would need the chat ID. This step will bring out all the chat entries this entity (Alice) has.
Go to `POST /flow/{holdingidentityshorthash}`, enter the identity short hash(Alice's hash) and request body:
```
{
"clientRequestId": "list-1",
"flowClassName": "com.r3.developers.csdetemplate.utxoexample.workflows.ListChatsFlow",
"requestBody": {}
}
```
After trigger the list-chats flow, again, we need to hop to `GET /flow/{holdingidentityshorthash}/{clientrequestid}` and check the result. As the screenshot shows, in the response body,
we will see a list of chat entries, but it currently only has one entry. And we can see the id of the chat entry. Let's record that id.
#### Step 3: Continue the chat with `UpdateChatFlow`
In this step, we will continue the chat between Alice and Bob.
Goto `POST /flow/{holdingidentityshorthash}`, enter the identity short hash and request body. Note that here we can have either Alice or Bob's short hash. If you enter Alice's hash,
this message will be recorded as a message from Alice, vice versa. And the id field is the chat entry id we got from the previous step.
```
{
"clientRequestId": "update-1",
"flowClassName": "com.r3.developers.csdetemplate.utxoexample.workflows.UpdateChatFlow",
"requestBody": {
"id":" ** fill in id **",
"message": "How are you today?"
}
}
```
And as for the result of this flow, go to `GET /flow/{holdingidentityshorthash}/{clientrequestid}` and enter the required fields.
#### Step 4: See the whole chat history of one chat entry
After a few back and forth of the messaging, you can view entire chat history by calling GetChatFlow.
```
{
"clientRequestId": "get-1",
"flowClassName": "com.r3.developers.csdetemplate.utxoexample.workflows.GetChatFlow",
"requestBody": {
"id":" ** fill in id **",
"numberOfRecords":"4"
}
}
```
And as for the result, you need to go to the Get API again and enter the short hash and client request ID.
Thus, we have concluded a full run through of the chat app.

67
corda/build.gradle Normal file
View File

@ -0,0 +1,67 @@
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 'net.corda.plugins.csde'
}
allprojects {
group 'com.r3.developers.csdetemplate'
version '1.0-SNAPSHOT'
def javaVersion = VERSION_11
// Configure the CSDE
csde {
cordaClusterURL = "https://localhost:8888"
networkConfigFile = "$rootDir/config/static-network-config.json"
r3RootCertFile = "config/r3-ca-key.pem"
corDappCpiName = "MyCorDapp"
notaryCpiName = "NotaryServer"
cordaRpcUser = "admin"
cordaRpcPasswd ="admin"
workflowsModuleName = workflowsModule
csdeWorkspaceDir = "$rootDir/workspace"
notaryVersion = cordaNotaryPluginsVersion
combinedWorkerVersion = combinedWorkerJarVersion
postgresJdbcVersion = "42.4.3"
cordaDbContainerName = "CSDEpostgresql"
cordaBinDir = "${System.getProperty("user.home")}/.corda/corda5"
cordaCliBinDir = "${System.getProperty("user.home")}/.corda/cli"
}
// Declare the set of Java compiler options we need to build a CorDapp.
tasks.withType(JavaCompile) {
// -parameters - Needed for reflection and serialization to work correctly.
options.compilerArgs += [
"-parameters"
]
}
repositories {
// All dependencies are held in Maven Central
mavenLocal()
mavenCentral()
}
tasks.withType(Test).configureEach {
useJUnitPlatform()
}
}
publishing {
publications {
maven(MavenPublication) {
artifactId "corda-CSDE-java-sample"
groupId project.group
artifact jar
}
}
}

View File

@ -0,0 +1,13 @@
-----BEGIN CERTIFICATE-----
MIIB7zCCAZOgAwIBAgIEFyV7dzAMBggqhkjOPQQDAgUAMFsxCzAJBgNVBAYTAkdC
MQ8wDQYDVQQHDAZMb25kb24xDjAMBgNVBAoMBUNvcmRhMQswCQYDVQQLDAJSMzEe
MBwGA1UEAwwVQ29yZGEgRGV2IENvZGUgU2lnbmVyMB4XDTIwMDYyNTE4NTI1NFoX
DTMwMDYyMzE4NTI1NFowWzELMAkGA1UEBhMCR0IxDzANBgNVBAcTBkxvbmRvbjEO
MAwGA1UEChMFQ29yZGExCzAJBgNVBAsTAlIzMR4wHAYDVQQDExVDb3JkYSBEZXYg
Q29kZSBTaWduZXIwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQDjSJtzQ+ldDFt
pHiqdSJebOGPZcvZbmC/PIJRsZZUF1bl3PfMqyG3EmAe0CeFAfLzPQtf2qTAnmJj
lGTkkQhxo0MwQTATBgNVHSUEDDAKBggrBgEFBQcDAzALBgNVHQ8EBAMCB4AwHQYD
VR0OBBYEFLMkL2nlYRLvgZZq7GIIqbe4df4pMAwGCCqGSM49BAMCBQADSAAwRQIh
ALB0ipx6EplT1fbUKqgc7rjH+pV1RQ4oKF+TkfjPdxnAAiArBdAI15uI70wf+xlL
zU+Rc5yMtcOY4/moZUq36r0Ilg==
-----END CERTIFICATE-----

51
corda/config/log4j2.xml Normal file
View File

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="INFO">
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} %X - %msg%n"/>
</Console>
<RollingFile name="App"
fileName="logs/corda.log"
filePattern="logs/corda.%d{MM-dd-yyyy}.%i.log"
ignoreExceptions="false">
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36} %X - %msg%n"/>
<Policies>
<OnStartupTriggeringPolicy />
<TimeBasedTriggeringPolicy />
<SizeBasedTriggeringPolicy size="10 MB" />
</Policies>
<DefaultRolloverStrategy>
<Delete basePath="logs/">
<IfFileName glob="logs/corda.*.log">
<IfAny>
<IfAccumulatedFileSize exceeds="500 MB" />
<IfAccumulatedFileCount exceeds="10" />
</IfAny>
</IfFileName>
<IfLastModified age="7d" />
</Delete>
</DefaultRolloverStrategy>
</RollingFile>
</Appenders>
<Loggers>
<logger name="Console">
<AppenderRef ref="Console" level="info"/>
</logger>
<!-- log warn only for these 3rd party libs -->
<Logger name="com.zaxxer.hikari" level="warn" />
<Logger name="io.javalin.Javalin" level="warn" />
<Logger name="org.apache.aries.spifly" level="warn" />
<Logger name="org.apache.kafka" level="warn" />
<Logger name="org.eclipse.jetty" level="warn" />
<Logger name="org.hibernate" level="warn" />
<!-- default to warn only for OSGi logging -->
<Logger name="net.corda.osgi.framework.OSGiFrameworkWrap" level="warn" />
<root level="debug">
<AppenderRef ref="App" level="info"/>
</root>
</Loggers>
</Configuration>

View File

@ -0,0 +1,32 @@
-----BEGIN CERTIFICATE-----
MIIFkDCCA3igAwIBAgIQBZsbV56OITLiOQe9p3d1XDANBgkqhkiG9w0BAQwFADBi
MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
d3cuZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3Qg
RzQwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBiMQswCQYDVQQGEwJV
UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu
Y29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwggIiMA0GCSqG
SIb3DQEBAQUAA4ICDwAwggIKAoICAQC/5pBzaN675F1KPDAiMGkz7MKnJS7JIT3y
ithZwuEppz1Yq3aaza57G4QNxDAf8xukOBbrVsaXbR2rsnnyyhHS5F/WBTxSD1If
xp4VpX6+n6lXFllVcq9ok3DCsrp1mWpzMpTREEQQLt+C8weE5nQ7bXHiLQwb7iDV
ySAdYyktzuxeTsiT+CFhmzTrBcZe7FsavOvJz82sNEBfsXpm7nfISKhmV1efVFiO
DCu3T6cw2Vbuyntd463JT17lNecxy9qTXtyOj4DatpGYQJB5w3jHtrHEtWoYOAMQ
jdjUN6QuBX2I9YI+EJFwq1WCQTLX2wRzKm6RAXwhTNS8rhsDdV14Ztk6MUSaM0C/
CNdaSaTC5qmgZ92kJ7yhTzm1EVgX9yRcRo9k98FpiHaYdj1ZXUJ2h4mXaXpI8OCi
EhtmmnTK3kse5w5jrubU75KSOp493ADkRSWJtppEGSt+wJS00mFt6zPZxd9LBADM
fRyVw4/3IbKyEbe7f/LVjHAsQWCqsWMYRJUadmJ+9oCw++hkpjPRiQfhvbfmQ6QY
uKZ3AeEPlAwhHbJUKSWJbOUOUlFHdL4mrLZBdd56rF+NP8m800ERElvlEFDrMcXK
chYiCd98THU/Y+whX8QgUWtvsauGi0/C1kVfnSD8oR7FwI+isX4KJpn15GkvmB0t
9dmpsh3lGwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB
hjAdBgNVHQ4EFgQU7NfjgtJxXWRM3y5nP+e6mK4cD08wDQYJKoZIhvcNAQEMBQAD
ggIBALth2X2pbL4XxJEbw6GiAI3jZGgPVs93rnD5/ZpKmbnJeFwMDF/k5hQpVgs2
SV1EY+CtnJYYZhsjDT156W1r1lT40jzBQ0CuHVD1UvyQO7uYmWlrx8GnqGikJ9yd
+SeuMIW59mdNOj6PWTkiU0TryF0Dyu1Qen1iIQqAyHNm0aAFYF/opbSnr6j3bTWc
fFqK1qI4mfN4i/RN0iAL3gTujJtHgXINwBQy7zBZLq7gcfJW5GqXb5JQbZaNaHqa
sjYUegbyJLkJEVDXCLG4iXqEI2FCKeWjzaIgQdfRnGTZ6iahixTXTBmyUEFxPT9N
cCOGDErcgdLMMpSEDQgJlxxPwO5rIHQw0uA5NBCFIRUBCOhVMt5xSdkoF1BN5r5N
0XWs0Mr7QbhDparTwwVETyw2m+L64kW4I1NsBm9nVX9GtUw/bihaeSbSpKhil9Ie
4u1Ki7wb/UdKDd9nZn6yW0HQO+T0O/QEY+nvwlQAUaCKKsnOeMzV6ocEGLPOr0mI
r/OSmbaz5mEP0oUA51Aa5BuVnRmhuZyxm7EAHu/QD09CbMkKvO5D+jpxpchNJqU1
/YldvIViHTLSoCtU7ZpXwdv6EM8Zt4tKG48BtieVU+i2iW1bvGjUI+iLUaJW+fCm
gKDWHrO8Dw9TdSmq6hN35N6MgSGtBxBHEa2HPQfRdbzP82Z+
-----END CERTIFICATE-----

View File

@ -0,0 +1,23 @@
[
{
"x500Name" : "CN=Alice, OU=Test Dept, O=R3, L=London, C=GB",
"cpi" : "MyCorDapp"
},
{
"x500Name" : "CN=Bob, OU=Test Dept, O=R3, L=London, C=GB",
"cpi" : "MyCorDapp"
},
{
"x500Name" : "CN=Charlie, OU=Test Dept, O=R3, L=London, C=GB",
"cpi" : "MyCorDapp"
},
{
"x500Name" : "CN=Dave, OU=Test Dept, O=R3, L=London, C=GB",
"cpi" : "MyCorDapp"
},
{
"x500Name" : "CN=NotaryRep1, OU=Test Dept, O=R3, L=London, C=GB",
"cpi" : "NotaryServer",
"serviceX500Name": "CN=NotaryService, OU=Test Dept, O=R3, L=London, C=GB"
}
]

View File

@ -0,0 +1,82 @@
plugins {
// Include the cordapp-cpb plugin. This automatically includes the cordapp-cpk plugin as well.
// These extend existing build environment so that CPB and CPK files can be built.
// This includes a CorDapp DSL that allows the developer to supply metadata for the CorDapp
// required by Corda.
id 'net.corda.plugins.cordapp-cpb2'
id 'org.jetbrains.kotlin.jvm'
id 'maven-publish'
}
// Declare dependencies for the modules we will use.
// A cordaProvided declaration is required for anything that we use that the Corda API provides.
// This is required to allow us to build CorDapp modules as OSGi bundles that CPI and CPB files are built on.
dependencies {
cordaProvided 'org.jetbrains.kotlin:kotlin-osgi-bundle'
// Declare a "platform" so that we use the correct set of dependency versions for the version of the
// Corda API specified.
cordaProvided platform("net.corda:corda-api:$cordaApiVersion")
// If using transistive dependencies this will provide most of Corda-API:
// cordaProvided 'net.corda:corda-application'
// Alternatively we can explicitly specify all our Corda-API dependencies:
cordaProvided 'net.corda:corda-base'
cordaProvided 'net.corda:corda-application'
cordaProvided 'net.corda:corda-crypto'
cordaProvided 'net.corda:corda-membership'
// cordaProvided 'net.corda:corda-persistence'
cordaProvided 'net.corda:corda-serialization'
cordaProvided 'net.corda:corda-ledger-utxo'
cordaProvided 'net.corda:corda-ledger-consensual'
// CorDapps that use the UTXO ledger must include at least one notary client plugin
cordapp "com.r3.corda.notary.plugin.nonvalidating:notary-plugin-non-validating-client:$cordaNotaryPluginsVersion"
// The CorDapp uses the slf4j logging framework. Corda-API provides this so we need a 'cordaProvided' declaration.
cordaProvided 'org.slf4j:slf4j-api'
// 3rd party libraries
// Required
testImplementation "org.slf4j:slf4j-simple:2.0.0"
testImplementation "org.junit.jupiter:junit-jupiter:$junitVersion"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion"
// Optional but used by example tests.
testImplementation "org.mockito:mockito-core:$mockitoVersion"
testImplementation "org.mockito.kotlin:mockito-kotlin:$mockitoKotlinVersion"
testImplementation "org.hamcrest:hamcrest-library:$hamcrestVersion"
}
// The CordApp section.
// This is part of the DSL provided by the corda plugins to define metadata for our CorDapp.
// Each component of the CorDapp would get its own CorDapp section in the build.gradle file for the components
// subproject.
// This is required by the corda plugins to build the CorDapp.
cordapp {
// "targetPlatformVersion" and "minimumPlatformVersion" are intended to specify the preferred
// and earliest versions of the Corda platform that the CorDapp will run on respectively.
// Enforced versioning has not implemented yet so we need to pass in a dummy value for now.
// The platform version will correspond to and be roughly equivalent to the Corda API version.
targetPlatformVersion = platformVersion.toInteger()
minimumPlatformVersion = platformVersion.toInteger()
// The cordapp section contains either a workflow or contract subsection depending on the type of component.
// Declares the type and metadata of the CPK (this CPB has one CPK).
contract {
name "ContractsModuleNameHere"
versionId 1
vendor "VendorNameHere"
}
}
publishing {
publications {
maven(MavenPublication) {
from components.cordapp
}
}
}

View File

@ -0,0 +1,72 @@
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.transaction.UtxoLedgerTransaction;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ChatContract implements Contract {
private final static Logger log = LoggerFactory.getLogger(ChatContract.class);
// Use constants to hold the error messages
// This allows the tests to use them, meaning if they are updated you won't need to fix tests just because the wording was updated
static final String REQUIRE_SINGLE_COMMAND = "Require a single command.";
static final String UNKNOWN_COMMAND = "Unsupported command";
static final String OUTPUT_STATE_SHOULD_ONLY_HAVE_TWO_PARTICIPANTS = "The output state should have two and only two participants.";
static final String TRANSACTION_SHOULD_BE_SIGNED_BY_ALL_PARTICIPANTS = "The transaction should have been signed by both participants.";
static final String CREATE_COMMAND_SHOULD_HAVE_NO_INPUT_STATES = "When command is Create there should be no input states.";
static final String CREATE_COMMAND_SHOULD_HAVE_ONLY_ONE_OUTPUT_STATE = "When command is Create there should be one and only one output state.";
static final String UPDATE_COMMAND_SHOULD_HAVE_ONLY_ONE_INPUT_STATE = "When command is Update there should be one and only one input state.";
static final String UPDATE_COMMAND_SHOULD_HAVE_ONLY_ONE_OUTPUT_STATE = "When command is Update there should be one and only one output state.";
static final String UPDATE_COMMAND_ID_SHOULD_NOT_CHANGE = "When command is Update id must not change.";
static final String UPDATE_COMMAND_CHATNAME_SHOULD_NOT_CHANGE = "When command is Update chatName must not change.";
static final String UPDATE_COMMAND_PARTICIPANTS_SHOULD_NOT_CHANGE = "When command is Update participants must not change.";
public static class Create implements Command { }
public static class Update implements Command { }
@Override
public void verify(UtxoLedgerTransaction transaction) {
requireThat( transaction.getCommands().size() == 1, REQUIRE_SINGLE_COMMAND);
Command command = transaction.getCommands().get(0);
ChatState output = transaction.getOutputStates(ChatState.class).get(0);
requireThat(output.getParticipants().size() == 2, OUTPUT_STATE_SHOULD_ONLY_HAVE_TWO_PARTICIPANTS);
requireThat(transaction.getSignatories().containsAll(output.getParticipants()), TRANSACTION_SHOULD_BE_SIGNED_BY_ALL_PARTICIPANTS);
if(command.getClass() == Create.class) {
requireThat(transaction.getInputContractStates().isEmpty(), CREATE_COMMAND_SHOULD_HAVE_NO_INPUT_STATES);
requireThat(transaction.getOutputContractStates().size() == 1, CREATE_COMMAND_SHOULD_HAVE_ONLY_ONE_OUTPUT_STATE);
}
else if(command.getClass() == Update.class) {
requireThat(transaction.getInputContractStates().size() == 1, UPDATE_COMMAND_SHOULD_HAVE_ONLY_ONE_INPUT_STATE);
requireThat(transaction.getOutputContractStates().size() == 1, UPDATE_COMMAND_SHOULD_HAVE_ONLY_ONE_OUTPUT_STATE);
ChatState input = transaction.getInputStates(ChatState.class).get(0);
requireThat(input.getId().equals(output.getId()), UPDATE_COMMAND_ID_SHOULD_NOT_CHANGE);
requireThat(input.getChatName().equals(output.getChatName()), UPDATE_COMMAND_CHATNAME_SHOULD_NOT_CHANGE);
requireThat(
input.getParticipants().containsAll(output.getParticipants()) &&
output.getParticipants().containsAll(input.getParticipants()),
UPDATE_COMMAND_PARTICIPANTS_SHOULD_NOT_CHANGE);
}
else {
throw new CordaRuntimeException(UNKNOWN_COMMAND);
}
}
private void requireThat(boolean asserted, String errorMessage) {
if(!asserted) {
throw new CordaRuntimeException("Failed requirement: " + errorMessage);
}
}
}

View File

@ -0,0 +1,55 @@
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.types.MemberX500Name;
import net.corda.v5.ledger.utxo.BelongsToContract;
import net.corda.v5.ledger.utxo.ContractState;
import java.security.PublicKey;
import java.util.*;
@BelongsToContract(ChatContract.class)
public class ChatState implements ContractState {
private UUID id;
private String chatName;
private MemberX500Name messageFrom;
private String message;
public List<PublicKey> participants;
// 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;
}
public UUID getId() {
return id;
}
public String getChatName() {
return chatName;
}
public MemberX500Name getMessageFrom() {
return messageFrom;
}
public String getMessage() {
return message;
}
public List<PublicKey> getParticipants() {
return participants;
}
public ChatState updateMessage(MemberX500Name name, String message) {
return new ChatState(id, chatName, name, message, participants);
}
}

40
corda/gradle.properties Normal file
View File

@ -0,0 +1,40 @@
kotlin.code.style=official
# Specify the version of the Corda-API to use.
# This needs to match the version supported by the Corda Cluster the CorDapp will run on.
cordaApiVersion=5.0.0.765
# Specify the version of the notary plugins to use.
# Currently packaged as part of corda-runtime-os, so should be set to a corda-runtime-os version.
cordaNotaryPluginsVersion=5.0.0.0
# Specify the version of the Combined Worker to use
combinedWorkerJarVersion=5.0.0.0
# Specify the version of the cordapp-cpb and cordapp-cpk plugins
cordaPluginsVersion=7.0.3
# Specify the version of the CSDE gradle plugin to use
csdePluginVersion=1.1.0
# Specify the name of the workflows module
workflowsModule=workflows
# For the time being this just needs to be set to a dummy value.
platformVersion = 999
# Version of Kotlin to use.
# We recommend using a version close to that used by Corda-API.
kotlinVersion = 1.7.21
# Do not use default dependencies.
kotlin.stdlib.default.dependency=false
# Test Tooling Dependency Versions
junitVersion = 5.8.2
mockitoKotlinVersion=4.0.0
mockitoVersion=4.6.1
hamcrestVersion=2.2
# Use JDK Zulu 11
org.gradle.java.home=/Library/Java/JavaVirtualMachines/zulu-11.jdk/Contents/Home

BIN
corda/gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View File

@ -0,0 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-bin.zip
networkTimeout=10000
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

244
corda/gradlew vendored Executable file
View File

@ -0,0 +1,244 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

92
corda/gradlew.bat vendored Normal file
View File

@ -0,0 +1,92 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

26
corda/settings.gradle Normal file
View File

@ -0,0 +1,26 @@
pluginManagement {
// Declare the repositories where plugins are stored.
repositories {
gradlePluginPortal()
mavenCentral()
mavenLocal()
}
// The plugin dependencies with versions of the plugins congruent with the specified CorDapp plugin version,
// Corda API version, and Kotlin version.
plugins {
id 'net.corda.plugins.cordapp-cpk2' version cordaPluginsVersion
id 'net.corda.plugins.cordapp-cpb2' version cordaPluginsVersion
id 'net.corda.cordapp.cordapp-configuration' version cordaApiVersion
id 'net.corda.plugins.csde' version csdePluginVersion
id 'org.jetbrains.kotlin.jvm' version kotlinVersion
id 'org.jetbrains.kotlin.plugin.jpa' version kotlinVersion
id 'org.jetbrains.kotlin.plugin.allopen' version kotlinVersion
}
}
// Root project name, used in naming the project as a whole and used in naming objects built by the project.
rootProject.name = 'csde-cordapp-template-java'
include ':workflows'
include ':contracts'

View File

@ -0,0 +1,84 @@
plugins {
// Include the cordapp-cpb plugin. This automatically includes the cordapp-cpk plugin as well.
// These extend existing build environment so that CPB and CPK files can be built.
// This includes a CorDapp DSL that allows the developer to supply metadata for the CorDapp
// required by Corda.
id 'net.corda.plugins.cordapp-cpb2'
id 'org.jetbrains.kotlin.jvm'
id 'maven-publish'
}
// Declare dependencies for the modules we will use.
// A cordaProvided declaration is required for anything that we use that the Corda API provides.
// This is required to allow us to build CorDapp modules as OSGi bundles that CPI and CPB files are built on.
dependencies {
// From other subprojects:
cordapp project(':contracts')
cordaProvided 'org.jetbrains.kotlin:kotlin-osgi-bundle'
// Declare a "platform" so that we use the correct set of dependency versions for the version of the
// Corda API specified.
cordaProvided platform("net.corda:corda-api:$cordaApiVersion")
// If using transistive dependencies this will provide most of Corda-API:
// cordaProvided 'net.corda:corda-application'
// Alternatively we can explicitly specify all our Corda-API dependencies:
cordaProvided 'net.corda:corda-base'
cordaProvided 'net.corda:corda-application'
cordaProvided 'net.corda:corda-crypto'
cordaProvided 'net.corda:corda-membership'
// cordaProvided 'net.corda:corda-persistence'
cordaProvided 'net.corda:corda-serialization'
cordaProvided 'net.corda:corda-ledger-utxo'
cordaProvided 'net.corda:corda-ledger-consensual'
// CorDapps that use the UTXO ledger must include at least one notary client plugin
cordapp "com.r3.corda.notary.plugin.nonvalidating:notary-plugin-non-validating-client:$cordaNotaryPluginsVersion"
// The CorDapp uses the slf4j logging framework. Corda-API provides this so we need a 'cordaProvided' declaration.
cordaProvided 'org.slf4j:slf4j-api'
// 3rd party libraries
// Required
testImplementation "org.slf4j:slf4j-simple:2.0.0"
testImplementation "org.junit.jupiter:junit-jupiter:$junitVersion"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion"
// Optional but used by exmaple tests.
testImplementation "org.mockito:mockito-core:$mockitoVersion"
testImplementation "org.mockito.kotlin:mockito-kotlin:$mockitoKotlinVersion"
testImplementation "org.hamcrest:hamcrest-library:$hamcrestVersion"
}
// The CordApp section.
// This is part of the DSL provided by the corda plugins to define metadata for our CorDapp.
// Each component of the CorDapp would get its own CorDapp section in the build.gradle file for the components
// subproject.
// This is required by the corda plugins to build the CorDapp.
cordapp {
// "targetPlatformVersion" and "minimumPlatformVersion" are intended to specify the preferred
// and earliest versions of the Corda platform that the CorDapp will run on respectively.
// Enforced versioning has not implemented yet so we need to pass in a dummy value for now.
// The platform version will correspond to and be roughly equivalent to the Corda API version.
targetPlatformVersion = platformVersion.toInteger()
minimumPlatformVersion = platformVersion.toInteger()
// The cordapp section contains either a workflow or contract subsection depending on the type of component.
// Declares the type and metadata of the CPK (this CPB has one CPK).
workflow {
name "WorkflowsModuleNameHere"
versionId 1
vendor "VendorNameHere"
}
}
publishing {
publications {
maven(MavenPublication) {
from components.cordapp
}
}
}

View File

@ -0,0 +1,28 @@
package com.r3.developers.csdetemplate.flowexample.workflows;
import net.corda.v5.base.annotations.CordaSerializable;
import net.corda.v5.base.types.MemberX500Name;
// Where a class contains a message, mark it with @CordaSerializable to enable Corda to
// 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;
}
public MemberX500Name getSender() {
return sender;
}
public String getMessage() {
return message;
}
}

View File

@ -0,0 +1,96 @@
package com.r3.developers.csdetemplate.flowexample.workflows;
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.application.messaging.FlowSession;
import net.corda.v5.base.annotations.Suspendable;
import net.corda.v5.base.types.MemberX500Name;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
// MyFirstFlow is an initiating flow, its corresponding responder flow is called MyFirstFlowResponder (defined below)
// to link the two sides of the flow together they need to have the same protocol.
@InitiatingFlow(protocol = "my-first-flow")
// MyFirstFlow should inherit from ClientStartableFlow, which tells Corda it can be started via an REST call
public class MyFirstFlow implements ClientStartableFlow {
// Log messages from the flows for debugging.
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.
// JsonMarshallingService provides a service for manipulating JSON.
@CordaInject
public JsonMarshallingService jsonMarshallingService;
// FlowMessaging provides a service that establishes flow sessions between virtual nodes
// that send and receive payloads between them.
@CordaInject
public FlowMessaging flowMessaging;
// MemberLookup provides a service for looking up information about members of the virtual network which
// this CorDapp operates in.
@CordaInject
public MemberLookup memberLookup;
public MyFirstFlow() {}
// When a flow is invoked its call() method is called.
// Call() methods must be marked as @Suspendable, this allows Corda to pause mid-execution to wait
// for a response from the other flows and services.
@Suspendable
@Override
public String call(ClientRequestBody requestBody) {
// Follow what happens in the console or logs.
log.info("MFF: MyFirstFlow.call() called");
// Show the requestBody in the logs - this can be used to help establish the format for starting a flow on Corda.
log.info("MFF: requestBody: " + requestBody.getRequestBody());
// Deserialize the Json requestBody into the MyfirstFlowStartArgs class using the JsonSerialisation service.
MyFirstFlowStartArgs flowArgs = requestBody.getRequestBodyAs(jsonMarshallingService, MyFirstFlowStartArgs.class);
// Obtain the MemberX500Name of the counterparty.
MemberX500Name otherMember = flowArgs.getOtherMember();
// Get our identity from the MemberLookup service.
MemberX500Name ourIdentity = memberLookup.myInfo().getName();
// Create the message payload using the MessageClass we defined.
Message message = new Message(otherMember, "Hello from " + ourIdentity + ".");
// Log the message to be sent.
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.
FlowSession session = flowMessaging.initiateFlow(otherMember);
// Send the Payload using the send method on the session to the MyFirstFlowResponder responder flow.
session.send(message);
// Receive a response from the responder flow.
Message response = session.receive(Message.class);
// The return value of a ClientStartableFlow must always be a String. This will be passed
// back as the REST response when the status of the flow is queried on Corda.
return response.getMessage();
}
}
/*
RequestBody for triggering the flow via REST:
{
"clientRequestId": "r1",
"flowClassName": "com.r3.developers.csdetemplate.flowexample.workflows.MyFirstFlow",
"requestBody": {
"otherMember":"CN=Bob, OU=Test Dept, O=R3, L=London, C=GB"
}
}
*/

View File

@ -0,0 +1,62 @@
package com.r3.developers.csdetemplate.flowexample.workflows;
import net.corda.v5.application.flows.CordaInject;
import net.corda.v5.application.flows.InitiatedBy;
import net.corda.v5.application.flows.ResponderFlow;
import net.corda.v5.application.membership.MemberLookup;
import net.corda.v5.application.messaging.FlowSession;
import net.corda.v5.base.annotations.Suspendable;
import net.corda.v5.base.types.MemberX500Name;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
// MyFirstFlowResponder is a responder flow, its corresponding initiating flow is called MyFirstFlow (defined in MyFirstFlow.java)
// to link the two sides of the flow together they need to have the same protocol.
@InitiatedBy(protocol = "my-first-flow")
// Responder flows must inherit from ResponderFlow
public class MyFirstFlowResponder implements ResponderFlow {
// Log messages from the flows for debugging.
private final static Logger log = LoggerFactory.getLogger(MyFirstFlowResponder.class);
// MemberLookup looks for information about members of the virtual network which
// this CorDapp operates in.
@CordaInject
public MemberLookup memberLookup;
public MyFirstFlowResponder() {}
// Responder flows are invoked when an initiating flow makes a call via a session set up with the virtual
// node hosting the responder flow. When a responder flow is invoked its call() method is called.
// call() methods must be marked as @Suspendable, this allows Corda to pause mid-execution to wait
// for a response from the other flows and services.
// The call() method has the flow session passed in as a parameter by Corda so the session is available to
// responder flow code, you don't need to inject the FlowMessaging service.
@Suspendable
@Override
public void call(FlowSession session) {
// Follow what happens in the console or logs.
log.info("MFF: MyFirstResponderFlow.call() called");
// Receive the payload and deserialize it into a message class.
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.getSender() + ":" + receivedMessage.getMessage());
// Get our identity from the MemberLookup service.
MemberX500Name ourIdentity = memberLookup.myInfo().getName();
// Create a message to greet the sender.
Message response = new Message(ourIdentity,
"Hello " + session.getCounterparty().getCommonName() + ", best wishes from " + ourIdentity.getCommonName());
// Log the response to be sent.
log.info("MFF: response.message: " + response.getMessage());
// Send the response via the send method on the flow session
session.send(response);
}
}

View File

@ -0,0 +1,19 @@
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 {
private MemberX500Name otherMember;
// The JSON Marshalling Service, which handles serialisation, needs this constructor.
public MyFirstFlowStartArgs() {}
public MyFirstFlowStartArgs(MemberX500Name otherMember) {
this.otherMember = otherMember;
}
public MemberX500Name getOtherMember() {
return otherMember;
}
}

View File

@ -0,0 +1,41 @@
package com.r3.developers.csdetemplate.utxoexample.workflows;
import java.util.UUID;
// Class to hold the ListChatFlow results.
// The ChatState(s) cannot be returned directly as the JsonMarshallingService can only serialize simple classes
// that the underlying Jackson serializer recognises, hence creating a DTO style object which consists only of Strings
// and a UUID. It is possible to create custom serializers for the JsonMarshallingService, but this beyond the scope
// of this simple example.
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,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.base.annotations.Suspendable;
import net.corda.v5.base.exceptions.CordaRuntimeException;
import net.corda.v5.base.types.MemberX500Name;
import net.corda.v5.ledger.common.NotaryLookup;
import net.corda.v5.ledger.utxo.UtxoLedgerService;
import net.corda.v5.ledger.utxo.transaction.UtxoSignedTransaction;
import net.corda.v5.ledger.utxo.transaction.UtxoTransactionBuilder;
import net.corda.v5.membership.MemberInfo;
import net.corda.v5.membership.NotaryInfo;
import 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.*;
// See Chat CorDapp Design section of the getting started docs for a description of this flow.
public class CreateNewChatFlow implements ClientStartableFlow {
private final static Logger log = LoggerFactory.getLogger(CreateNewChatFlow.class);
@CordaInject
public JsonMarshallingService jsonMarshallingService;
@CordaInject
public MemberLookup memberLookup;
// Injects the UtxoLedgerService to enable the flow to make use of the Ledger API.
@CordaInject
public UtxoLedgerService ledgerService;
@CordaInject
public NotaryLookup notaryLookup;
// FlowEngine service is required to run SubFlows.
@CordaInject
public FlowEngine flowEngine;
@Suspendable
@Override
public String call( ClientRequestBody requestBody) {
log.info("CreateNewChatFlow.call() called");
try {
// Obtain the deserialized input arguments to the flow from the requestBody.
CreateNewChatFlowArgs flowArgs = requestBody.getRequestBodyAs(jsonMarshallingService, CreateNewChatFlowArgs.class);
// Get MemberInfos for the Vnode running the flow and the otherMember.
MemberInfo myInfo = memberLookup.myInfo();
MemberInfo otherMember = requireNonNull(
memberLookup.lookup(MemberX500Name.parse(flowArgs.getOtherMember())),
"MemberLookup can't find otherMember specified in flow arguments."
);
// Create the ChatState from the input arguments and member information.
ChatState chatState = new ChatState(
UUID.randomUUID(),
flowArgs.getChatName(),
myInfo.getName(),
flowArgs.getMessage(),
Arrays.asList(myInfo.getLedgerKeys().get(0), otherMember.getLedgerKeys().get(0))
);
// Obtain the Notary name and public key.
NotaryInfo notary = notaryLookup.getNotaryServices().iterator().next();
// Use UTXOTransactionBuilder to build up the draft transaction.
UtxoTransactionBuilder txBuilder = ledgerService.createTransactionBuilder()
.setNotary(notary.getName())
.setTimeWindowBetween(Instant.now(), Instant.now().plusMillis(Duration.ofDays(1).toMillis()))
.addOutputState(chatState)
.addCommand(new ChatContract.Create())
.addSignatories(chatState.getParticipants());
// Convert the transaction builder to a UTXOSignedTransaction. Verifies the content of the
// UtxoTransactionBuilder and signs the transaction with any required signatories that belong to
// the current node.
UtxoSignedTransaction signedTransaction = txBuilder.toSignedTransaction();
// Call FinalizeChatSubFlow which will finalise the transaction.
// If successful the flow will return a String of the created transaction id,
// if not successful it will return an error message.
return flowEngine.subFlow(new FinalizeChatSubFlow(signedTransaction, otherMember.getName()));
}
// Catch any exceptions, log them and rethrow the exception.
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 REST:
{
"clientRequestId": "create-1",
"flowClassName": "com.r3.developers.csdetemplate.utxoexample.workflows.CreateNewChatFlow",
"requestBody": {
"chatName":"Chat with Bob",
"otherMember":"CN=Bob, OU=Test Dept, O=R3, L=London, C=GB",
"message": "Hello Bob"
}
}
*/

View File

@ -0,0 +1,30 @@
package com.r3.developers.csdetemplate.utxoexample.workflows;
// A class to hold the deserialized arguments required to start the flow.
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,75 @@
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.exceptions.CordaRuntimeException;
import net.corda.v5.base.types.MemberX500Name;
import net.corda.v5.ledger.utxo.UtxoLedgerService;
import net.corda.v5.ledger.utxo.transaction.UtxoSignedTransaction;
import net.corda.v5.ledger.utxo.transaction.UtxoTransactionValidator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Arrays;
import java.util.List;
// See Chat CorDapp Design section of the getting started docs for a description of this flow.
//@InitiatingBy declares the protocol which will be used to link the initiator to the responder.
@InitiatedBy(protocol = "finalize-chat-protocol")
public class FinalizeChatResponderFlow implements ResponderFlow {
private final static Logger log = LoggerFactory.getLogger(FinalizeChatResponderFlow.class);
// Injects the UtxoLedgerService to enable the flow to make use of the Ledger API.
@CordaInject
public UtxoLedgerService utxoLedgerService;
@Suspendable
@Override
public void call(FlowSession session) {
log.info("FinalizeChatResponderFlow.call() called");
try {
// Defines the lambda validator used in receiveFinality below.
UtxoTransactionValidator txValidator = ledgerTransaction -> {
ChatState state = (ChatState) ledgerTransaction.getOutputContractStates().get(0);
// Uses checkForBannedWords() and checkMessageFromMatchesCounterparty() functions
// to check whether to sign the transaction.
if (checkForBannedWords(state.getMessage()) || !checkMessageFromMatchesCounterparty(state, session.getCounterparty())) {
throw new CordaRuntimeException("Failed verification");
}
log.info("Verified the transaction - " + ledgerTransaction.getId());
};
// Calls receiveFinality() function which provides the responder to the finalise() function
// in the Initiating Flow. Accepts a lambda validator containing the business logic to decide whether
// responder should sign the Transaction.
UtxoSignedTransaction finalizedSignedTransaction = utxoLedgerService.receiveFinality(session, txValidator).getTransaction();
log.info("Finished responder flow - " + finalizedSignedTransaction.getId());
}
// Soft fails the flow and log the exception.
catch(Exception e)
{
log.warn("Exceptionally finished responder flow", e);
}
}
@Suspendable
Boolean 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,71 @@
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;
// See Chat CorDapp Design section of the getting started docs for a description of this flow.
// @InitiatingFlow declares the protocol which will be used to link the initiator to the responder.
@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;
}
// Injects the UtxoLedgerService to enable the flow to make use of the Ledger API.
@CordaInject
public UtxoLedgerService ledgerService;
@CordaInject
public FlowMessaging flowMessaging;
@Override
@Suspendable
public String call() {
log.info("FinalizeChatFlow.call() called");
// Initiates a session with the other Member.
FlowSession session = flowMessaging.initiateFlow(otherMember);
// Calls the Corda provided finalise() function which gather signatures from the counterparty,
// notarises the transaction and persists the transaction to each party's vault.
// On success returns the id of the transaction created. (This is different to the ChatState id)
String result;
try {
List<FlowSession> sessionsList = Arrays.asList(session);
UtxoSignedTransaction finalizedSignedTransaction = ledgerService.finalize(
signedTransaction,
sessionsList
).getTransaction();
result = finalizedSignedTransaction.getId().toString();
log.info("Success! Response: " + result);
}
// Soft fails the flow and returns the error message without throwing a flow exception.
catch (Exception e) {
log.warn("Finality failed", e);
result = "Finality failed, " + e.getMessage();
}
// Returns the transaction id converted as a string
return result;
}
}

View File

@ -0,0 +1,119 @@
package com.r3.developers.csdetemplate.utxoexample.workflows;
import com.r3.developers.csdetemplate.utxoexample.states.ChatState;
import net.corda.v5.application.flows.ClientRequestBody;
import net.corda.v5.application.flows.ClientStartableFlow;
import net.corda.v5.application.flows.CordaInject;
import net.corda.v5.application.marshalling.JsonMarshallingService;
import net.corda.v5.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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.*;
import static java.util.Objects.*;
import static java.util.stream.Collectors.toList;
// See Chat CorDapp Design section of the getting started docs for a description of this flow.
public class GetChatFlow implements ClientStartableFlow {
private final static Logger log = LoggerFactory.getLogger(GetChatFlow.class);
@CordaInject
public JsonMarshallingService jsonMarshallingService;
// Injects the UtxoLedgerService to enable the flow to make use of the Ledger API.
@CordaInject
public UtxoLedgerService ledgerService;
@Override
@Suspendable
public String call(ClientRequestBody requestBody) {
// Obtain the deserialized input arguments to the flow from the requestBody.
GetChatFlowArgs flowArgs = requestBody.getRequestBodyAs(jsonMarshallingService, GetChatFlowArgs.class);
// Look up the latest unconsumed ChatState with the given id.
// Note, this code brings all unconsumed states back, then filters them.
// This is an inefficient way to perform this operation when there are a large number of chats.
// Note, you will get this error if you input an id which has no corresponding ChatState (common error).
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);
// Calls resolveMessagesFromBackchain() which retrieves the chat history from the backchain.
return jsonMarshallingService.format(resolveMessagesFromBackchain(chatStateAndRef, flowArgs.getNumberOfRecords() ));
}
// resoveMessageFromBackchain() starts at the stateAndRef provided, which represents the unconsumed head of the
// backchain for this particular chat, then walks the chain backwards for the number of transaction specified in
// the numberOfRecords argument. For each transaction it adds the MessageAndSender representing the
// message and who sent it to a list which is then returned.
@Suspendable
private List<MessageAndSender> resolveMessagesFromBackchain(StateAndRef<?> stateAndRef, int numberOfRecords) {
// Set up a Mutable List to collect the MessageAndSender(s)
List<MessageAndSender> messages = new LinkedList<>();
// Set up initial conditions for walking the backchain.
StateAndRef<?> currentStateAndRef = stateAndRef;
int recordsToFetch = numberOfRecords;
boolean moreBackchain = true;
// Continue to loop until the start of the backchain or enough records have been retrieved.
while (moreBackchain) {
// Obtain the transaction id from the current StateAndRef and fetch the transaction from the vault.
SecureHash transactionId = currentStateAndRef.getRef().getTransactionId();
UtxoLedgerTransaction transaction = requireNonNull(
ledgerService.findLedgerTransaction(transactionId),
"Transaction " + transactionId + " not found."
);
// Get the output state from the transaction and use it to create a MessageAndSender Object which
// is appended to the mutable list.
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 MessageAndSender(output.getMessageFrom().toString(), output.getMessage()));
// Decrement the number of records to fetch.
recordsToFetch--;
// Get the reference to the input states.
List<StateAndRef<?>> inputStateAndRefs = transaction.getInputStateAndRefs();
// Check if there are no more input states (start of chain) or we have retrieved enough records.
// Check the transaction is not malformed by having too many input states.
// Set the currentStateAndRef to the input StateAndRef, then repeat the loop.
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 REST:
{
"clientRequestId": "get-1",
"flowClassName": "com.r3.developers.csdetemplate.utxoexample.workflows.GetChatFlow",
"requestBody": {
"id":"** fill in id **",
"numberOfRecords":"4"
}
}
*/

View File

@ -0,0 +1,24 @@
package com.r3.developers.csdetemplate.utxoexample.workflows;
import java.util.UUID;
// A class to hold the deserialized arguments required to start the flow.
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,58 @@
package com.r3.developers.csdetemplate.utxoexample.workflows;
import com.r3.developers.csdetemplate.utxoexample.states.ChatState;
import net.corda.v5.application.flows.ClientRequestBody;
import net.corda.v5.application.flows.ClientStartableFlow;
import net.corda.v5.application.flows.CordaInject;
import net.corda.v5.application.marshalling.JsonMarshallingService;
import net.corda.v5.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;
// See Chat CorDapp Design section of the getting started docs for a description of this flow.
public class ListChatsFlow implements ClientStartableFlow {
private final static Logger log = LoggerFactory.getLogger(ListChatsFlow.class);
@CordaInject
public JsonMarshallingService jsonMarshallingService;
// Injects the UtxoLedgerService to enable the flow to make use of the Ledger API.
@CordaInject
public UtxoLedgerService utxoLedgerService;
@Suspendable
@Override
public String call(ClientRequestBody requestBody) {
log.info("ListChatsFlow.call() called");
// Queries the VNode's vault for unconsumed states and converts the result to a serializable DTO.
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());
// Uses the JsonMarshallingService's format() function to serialize the DTO to Json.
return jsonMarshallingService.format(results);
}
}
/*
RequestBody for triggering the flow via REST:
{
"clientRequestId": "list-1",
"flowClassName": "com.r3.developers.csdetemplate.utxoexample.workflows.ListChatsFlow",
"requestBody": {}
}
*/

View File

@ -0,0 +1,22 @@
package com.r3.developers.csdetemplate.utxoexample.workflows;
// A class to pair the messageFrom and message together.
public class MessageAndSender {
private String messageFrom;
private String message;
public MessageAndSender() {}
public MessageAndSender(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,120 @@
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.ClientRequestBody;
import net.corda.v5.application.flows.ClientStartableFlow;
import net.corda.v5.application.flows.CordaInject;
import net.corda.v5.application.flows.FlowEngine;
import net.corda.v5.application.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;
// See Chat CorDapp Design section of the getting started docs for a description of this flow.
public class UpdateChatFlow implements ClientStartableFlow {
private final static Logger log = LoggerFactory.getLogger(UpdateChatFlow.class);
@CordaInject
public JsonMarshallingService jsonMarshallingService;
@CordaInject
public MemberLookup memberLookup;
// Injects the UtxoLedgerService to enable the flow to make use of the Ledger API.
@CordaInject
public UtxoLedgerService ledgerService;
// FlowEngine service is required to run SubFlows.
@CordaInject
public FlowEngine flowEngine;
@Suspendable
@Override
public String call(ClientRequestBody requestBody) {
log.info("UpdateNewChatFlow.call() called");
try {
// Obtain the deserialized input arguments to the flow from the requestBody.
UpdateChatFlowArgs flowArgs = requestBody.getRequestBodyAs(jsonMarshallingService, UpdateChatFlowArgs.class);
// Look up the latest unconsumed ChatState with the given id.
// Note, this code brings all unconsumed states back, then filters them.
// This is an inefficient way to perform this operation when there are a large number of chats.
// Note, you will get this error if you input an id which has no corresponding ChatState (common error).
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);
// Get MemberInfos for the Vnode running the flow and the otherMember.
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);
// Create a new ChatState using the updateMessage helper function.
ChatState newChatState = state.updateMessage(myInfo.getName(), flowArgs.getMessage());
// Use UTXOTransactionBuilder to build up the draft transaction.
UtxoTransactionBuilder txBuilder = ledgerService.createTransactionBuilder()
.setNotary(chatStateAndRef.getState().getNotaryName())
.setTimeWindowBetween(Instant.now(), Instant.now().plusMillis(Duration.ofDays(1).toMillis()))
.addOutputState(newChatState)
.addInputState(chatStateAndRef.getRef())
.addCommand(new ChatContract.Update())
.addSignatories(newChatState.getParticipants());
// Convert the transaction builder to a UTXOSignedTransaction. Verifies the content of the
// UtxoTransactionBuilder and signs the transaction with any required signatories that belong to
// the current node.
UtxoSignedTransaction signedTransaction = txBuilder.toSignedTransaction();
// Call FinalizeChatSubFlow which will finalise the transaction.
// If successful the flow will return a String of the created transaction id,
// if not successful it will return an error message.
return flowEngine.subFlow(new FinalizeChatSubFlow(signedTransaction, otherMember.getName()));
}
// Catch any exceptions, log them and rethrow the exception.
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 REST:
{
"clientRequestId": "update-1",
"flowClassName": "com.r3.developers.csdetemplate.utxoexample.workflows.UpdateChatFlow",
"requestBody": {
"id":" ** fill in id **",
"message": "How are you today?"
}
}
*/

View File

@ -0,0 +1,24 @@
package com.r3.developers.csdetemplate.utxoexample.workflows;
import java.util.UUID;
// A class to hold the deserialized arguments required to start the flow.
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

@ -0,0 +1,41 @@
//package com.r3.developers.csdetemplate.flowexample.workflows;
//
//import net.corda.simulator.RequestData;
//import net.corda.simulator.SimulatedVirtualNode;
//import net.corda.simulator.Simulator;
//import net.corda.v5.base.types.MemberX500Name;
//import org.junit.jupiter.api.Test;
//
//class MyFirstFlowTest {
// // Names picked to match the corda network in config/dev-net.json
// private MemberX500Name aliceX500 = MemberX500Name.parse("CN=Alice, OU=Test Dept, O=R3, L=London, C=GB");
// private MemberX500Name bobX500 = MemberX500Name.parse("CN=Bob, OU=Test Dept, O=R3, L=London, C=GB");
//
// @Test
// @SuppressWarnings("unchecked")
// public void test_that_MyFirstFLow_returns_correct_message() {
// // Instantiate an instance of the simulator.
// Simulator simulator = new Simulator();
//
// // Create Alice's and Bob's virtual nodes, including the classes of the flows which will be registered on each node.
// // Don't assign Bob's virtual node to a value. You don't need it for this particular test.
// SimulatedVirtualNode aliceVN = simulator.createVirtualNode(aliceX500, MyFirstFlow.class);
// simulator.createVirtualNode(bobX500, MyFirstFlowResponder.class);
//
// // Create an instance of the MyFirstFlowStartArgs which contains the request arguments for starting the flow.
// MyFirstFlowStartArgs myFirstFlowStartArgs = new MyFirstFlowStartArgs(bobX500);
//
// // Create a requestData object.
// RequestData requestData = RequestData.Companion.create(
// "request no 1", // A unique reference for the instance of the flow request.
// MyFirstFlow.class, // The name of the flow class which is to be started.
// myFirstFlowStartArgs // The object which contains the start arguments of the flow.
// );
//
// // Call the flow on Alice's virtual node and capture the response.
// String flowResponse = aliceVN.callFlow(requestData);
//
// // Check that the flow has returned the expected string.
// assert(flowResponse.equals("Hello Alice, best wishes from Bob"));
// }
//}