diff --git a/corda/.ci/Jenkinsfile b/corda/.ci/Jenkinsfile new file mode 100644 index 0000000..202ac99 --- /dev/null +++ b/corda/.ci/Jenkinsfile @@ -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' + ) diff --git a/corda/.ci/nightly/JenkinsfileSnykScan b/corda/.ci/nightly/JenkinsfileSnykScan new file mode 100644 index 0000000..8093716 --- /dev/null +++ b/corda/.ci/nightly/JenkinsfileSnykScan @@ -0,0 +1,6 @@ +@Library('corda-shared-build-pipeline-steps@5.1') _ + +cordaSnykScanPipeline ( + snykTokenId: 'r3-snyk-corda5', + snykAdditionalCommands: "--all-sub-projects -d" +) diff --git a/corda/.github/workflows/check-pr-title.yaml b/corda/.github/workflows/check-pr-title.yaml new file mode 100644 index 0000000..7d427b0 --- /dev/null +++ b/corda/.github/workflows/check-pr-title.yaml @@ -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 }}" diff --git a/corda/.gitignore b/corda/.gitignore new file mode 100644 index 0000000..b9a99ba --- /dev/null +++ b/corda/.gitignore @@ -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 \ No newline at end of file diff --git a/corda/.run/runConfigurations/DebugCorDapp.run.xml b/corda/.run/runConfigurations/DebugCorDapp.run.xml new file mode 100644 index 0000000..1d8da82 --- /dev/null +++ b/corda/.run/runConfigurations/DebugCorDapp.run.xml @@ -0,0 +1,15 @@ + + + + \ No newline at end of file diff --git a/corda/.snyk b/corda/.snyk new file mode 100644 index 0000000..c04521f --- /dev/null +++ b/corda/.snyk @@ -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: {} diff --git a/corda/README.md b/corda/README.md new file mode 100644 index 0000000..c4c7951 --- /dev/null +++ b/corda/README.md @@ -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. diff --git a/corda/build.gradle b/corda/build.gradle new file mode 100644 index 0000000..e852ddb --- /dev/null +++ b/corda/build.gradle @@ -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 + } + + } +} + diff --git a/corda/config/gradle-plugin-default-key.pem b/corda/config/gradle-plugin-default-key.pem new file mode 100644 index 0000000..5294bbd --- /dev/null +++ b/corda/config/gradle-plugin-default-key.pem @@ -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----- \ No newline at end of file diff --git a/corda/config/log4j2.xml b/corda/config/log4j2.xml new file mode 100644 index 0000000..909222c --- /dev/null +++ b/corda/config/log4j2.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/corda/config/r3-ca-key.pem b/corda/config/r3-ca-key.pem new file mode 100644 index 0000000..4214227 --- /dev/null +++ b/corda/config/r3-ca-key.pem @@ -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----- diff --git a/corda/config/static-network-config.json b/corda/config/static-network-config.json new file mode 100644 index 0000000..b0f2519 --- /dev/null +++ b/corda/config/static-network-config.json @@ -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" + } +] diff --git a/corda/contracts/build.gradle b/corda/contracts/build.gradle new file mode 100644 index 0000000..da26473 --- /dev/null +++ b/corda/contracts/build.gradle @@ -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 component’s +// 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 + } + } +} diff --git a/corda/contracts/src/main/java/com/r3/developers/csdetemplate/utxoexample/contracts/ChatContract.java b/corda/contracts/src/main/java/com/r3/developers/csdetemplate/utxoexample/contracts/ChatContract.java new file mode 100644 index 0000000..846b286 --- /dev/null +++ b/corda/contracts/src/main/java/com/r3/developers/csdetemplate/utxoexample/contracts/ChatContract.java @@ -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); + } + } +} diff --git a/corda/contracts/src/main/java/com/r3/developers/csdetemplate/utxoexample/states/ChatState.java b/corda/contracts/src/main/java/com/r3/developers/csdetemplate/utxoexample/states/ChatState.java new file mode 100644 index 0000000..bc85e7f --- /dev/null +++ b/corda/contracts/src/main/java/com/r3/developers/csdetemplate/utxoexample/states/ChatState.java @@ -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 participants; + + // Allows serialisation and to use a specified UUID. + @ConstructorForDeserialization + public ChatState(UUID id, + String chatName, + MemberX500Name messageFrom, + String message, + List participants) { + this.id = id; + this.chatName = chatName; + this.messageFrom = messageFrom; + this.message = message; + this.participants = participants; + } + + public UUID getId() { + return id; + } + public String getChatName() { + return chatName; + } + public MemberX500Name getMessageFrom() { + return messageFrom; + } + public String getMessage() { + return message; + } + + public List getParticipants() { + return participants; + } + + public ChatState updateMessage(MemberX500Name name, String message) { + return new ChatState(id, chatName, name, message, participants); + } +} \ No newline at end of file diff --git a/corda/gradle.properties b/corda/gradle.properties new file mode 100644 index 0000000..21d41e7 --- /dev/null +++ b/corda/gradle.properties @@ -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 diff --git a/corda/gradle/wrapper/gradle-wrapper.jar b/corda/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..943f0cb Binary files /dev/null and b/corda/gradle/wrapper/gradle-wrapper.jar differ diff --git a/corda/gradle/wrapper/gradle-wrapper.properties b/corda/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..5083229 --- /dev/null +++ b/corda/gradle/wrapper/gradle-wrapper.properties @@ -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 diff --git a/corda/gradlew b/corda/gradlew new file mode 100755 index 0000000..65dcd68 --- /dev/null +++ b/corda/gradlew @@ -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" "$@" diff --git a/corda/gradlew.bat b/corda/gradlew.bat new file mode 100644 index 0000000..6689b85 --- /dev/null +++ b/corda/gradlew.bat @@ -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 diff --git a/corda/settings.gradle b/corda/settings.gradle new file mode 100644 index 0000000..22d345b --- /dev/null +++ b/corda/settings.gradle @@ -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' + diff --git a/corda/workflows/build.gradle b/corda/workflows/build.gradle new file mode 100644 index 0000000..ddc1790 --- /dev/null +++ b/corda/workflows/build.gradle @@ -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 component’s +// 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 + } + } +} diff --git a/corda/workflows/src/main/java/com/r3/developers/csdetemplate/flowexample/workflows/Message.java b/corda/workflows/src/main/java/com/r3/developers/csdetemplate/flowexample/workflows/Message.java new file mode 100644 index 0000000..b20791e --- /dev/null +++ b/corda/workflows/src/main/java/com/r3/developers/csdetemplate/flowexample/workflows/Message.java @@ -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; + } + + +} diff --git a/corda/workflows/src/main/java/com/r3/developers/csdetemplate/flowexample/workflows/MyFirstFlow.java b/corda/workflows/src/main/java/com/r3/developers/csdetemplate/flowexample/workflows/MyFirstFlow.java new file mode 100644 index 0000000..4e7b368 --- /dev/null +++ b/corda/workflows/src/main/java/com/r3/developers/csdetemplate/flowexample/workflows/MyFirstFlow.java @@ -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" + } +} + */ diff --git a/corda/workflows/src/main/java/com/r3/developers/csdetemplate/flowexample/workflows/MyFirstFlowResponder.java b/corda/workflows/src/main/java/com/r3/developers/csdetemplate/flowexample/workflows/MyFirstFlowResponder.java new file mode 100644 index 0000000..7e7eb74 --- /dev/null +++ b/corda/workflows/src/main/java/com/r3/developers/csdetemplate/flowexample/workflows/MyFirstFlowResponder.java @@ -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); + } +} diff --git a/corda/workflows/src/main/java/com/r3/developers/csdetemplate/flowexample/workflows/MyFirstFlowStartArgs.java b/corda/workflows/src/main/java/com/r3/developers/csdetemplate/flowexample/workflows/MyFirstFlowStartArgs.java new file mode 100644 index 0000000..bfcca2d --- /dev/null +++ b/corda/workflows/src/main/java/com/r3/developers/csdetemplate/flowexample/workflows/MyFirstFlowStartArgs.java @@ -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; + } +} diff --git a/corda/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/ChatStateResults.java b/corda/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/ChatStateResults.java new file mode 100644 index 0000000..5c3f37f --- /dev/null +++ b/corda/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/ChatStateResults.java @@ -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; + } +} diff --git a/corda/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/CreateNewChatFlow.java b/corda/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/CreateNewChatFlow.java new file mode 100644 index 0000000..f3c7286 --- /dev/null +++ b/corda/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/CreateNewChatFlow.java @@ -0,0 +1,118 @@ +package com.r3.developers.csdetemplate.utxoexample.workflows; + +import com.r3.developers.csdetemplate.utxoexample.contracts.ChatContract; +import com.r3.developers.csdetemplate.utxoexample.states.ChatState; +import net.corda.v5.application.flows.*; +import net.corda.v5.application.marshalling.JsonMarshallingService; +import net.corda.v5.application.membership.MemberLookup; +import net.corda.v5.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" + } +} + */ diff --git a/corda/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/CreateNewChatFlowArgs.java b/corda/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/CreateNewChatFlowArgs.java new file mode 100644 index 0000000..c2b815e --- /dev/null +++ b/corda/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/CreateNewChatFlowArgs.java @@ -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; + } +} \ No newline at end of file diff --git a/corda/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/FinalizeChatResponderFlow.java b/corda/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/FinalizeChatResponderFlow.java new file mode 100644 index 0000000..0cb2f3d --- /dev/null +++ b/corda/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/FinalizeChatResponderFlow.java @@ -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 bannedWords = Arrays.asList("banana", "apple", "pear"); + return bannedWords.stream().anyMatch(str::contains); + } + + @Suspendable + Boolean checkMessageFromMatchesCounterparty(ChatState state, MemberX500Name otherMember) { + return state.getMessageFrom().equals(otherMember); + } + +} diff --git a/corda/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/FinalizeChatSubFlow.java b/corda/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/FinalizeChatSubFlow.java new file mode 100644 index 0000000..b6b17f9 --- /dev/null +++ b/corda/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/FinalizeChatSubFlow.java @@ -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 { + + 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 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; + } +} diff --git a/corda/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/GetChatFlow.java b/corda/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/GetChatFlow.java new file mode 100644 index 0000000..6f192ea --- /dev/null +++ b/corda/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/GetChatFlow.java @@ -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> chatStateAndRefs = ledgerService.findUnconsumedStatesByType(ChatState.class); + List> chatStateAndRefsWithId = chatStateAndRefs.stream() + .filter(sar -> sar.getState().getContractState().getId().equals(flowArgs.getId())).collect(toList()); + if (chatStateAndRefsWithId.size() != 1) throw new CordaRuntimeException("Multiple or zero Chat states with id " + flowArgs.getId() + " found"); + StateAndRef chatStateAndRef = chatStateAndRefsWithId.get(0); + + // 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 resolveMessagesFromBackchain(StateAndRef stateAndRef, int numberOfRecords) { + + // Set up a Mutable List to collect the MessageAndSender(s) + List 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 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> 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" + } +} + */ + diff --git a/corda/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/GetChatFlowArgs.java b/corda/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/GetChatFlowArgs.java new file mode 100644 index 0000000..d1bac81 --- /dev/null +++ b/corda/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/GetChatFlowArgs.java @@ -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; + } +} diff --git a/corda/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/ListChatsFlow.java b/corda/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/ListChatsFlow.java new file mode 100644 index 0000000..1416b7c --- /dev/null +++ b/corda/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/ListChatsFlow.java @@ -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> states = utxoLedgerService.findUnconsumedStatesByType(ChatState.class); + List results = states.stream().map( stateAndRef -> + new ChatStateResults( + stateAndRef.getState().getContractState().getId(), + stateAndRef.getState().getContractState().getChatName(), + stateAndRef.getState().getContractState().getMessageFrom().toString(), + stateAndRef.getState().getContractState().getMessage() + ) + ).collect(Collectors.toList()); + + // 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": {} +} +*/ \ No newline at end of file diff --git a/corda/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/MessageAndSender.java b/corda/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/MessageAndSender.java new file mode 100644 index 0000000..7be54fd --- /dev/null +++ b/corda/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/MessageAndSender.java @@ -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; + } +} diff --git a/corda/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/UpdateChatFlow.java b/corda/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/UpdateChatFlow.java new file mode 100644 index 0000000..49e84a1 --- /dev/null +++ b/corda/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/UpdateChatFlow.java @@ -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> chatStateAndRefs = ledgerService.findUnconsumedStatesByType(ChatState.class); + List> chatStateAndRefsWithId = chatStateAndRefs.stream() + .filter(sar -> sar.getState().getContractState().getId().equals(flowArgs.getId())).collect(toList()); + if (chatStateAndRefsWithId.size() != 1) throw new CordaRuntimeException("Multiple or zero Chat states with id " + flowArgs.getId() + " found"); + StateAndRef chatStateAndRef = chatStateAndRefsWithId.get(0); + + // Get MemberInfos for the Vnode running the flow and the otherMember. + MemberInfo myInfo = memberLookup.myInfo(); + ChatState state = chatStateAndRef.getState().getContractState(); + + List members = state.getParticipants().stream().map( + it -> requireNonNull(memberLookup.lookup(it), "Member not found from public Key "+ it + ".") + ).collect(toList()); + members.remove(myInfo); + if(members.size() != 1) throw new RuntimeException("Should be only one participant other than the initiator"); + MemberInfo otherMember = members.get(0); + + // 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?" + } +} + */ \ No newline at end of file diff --git a/corda/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/UpdateChatFlowArgs.java b/corda/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/UpdateChatFlowArgs.java new file mode 100644 index 0000000..8ac7009 --- /dev/null +++ b/corda/workflows/src/main/java/com/r3/developers/csdetemplate/utxoexample/workflows/UpdateChatFlowArgs.java @@ -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; + } +} diff --git a/corda/workflows/src/test/java/com/r3/developers/csdetemplate/flowexample/workflows/MyFirstFlowTest.java b/corda/workflows/src/test/java/com/r3/developers/csdetemplate/flowexample/workflows/MyFirstFlowTest.java new file mode 100644 index 0000000..9dea76c --- /dev/null +++ b/corda/workflows/src/test/java/com/r3/developers/csdetemplate/flowexample/workflows/MyFirstFlowTest.java @@ -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")); +// } +//}