diff --git a/kotlin-samples/oracle-foreign-exchange/.ci/Jenkinsfile b/kotlin-samples/oracle-foreign-exchange/.ci/Jenkinsfile
new file mode 100644
index 0000000..2108886
--- /dev/null
+++ b/kotlin-samples/oracle-foreign-exchange/.ci/Jenkinsfile
@@ -0,0 +1,11 @@
+@Library('corda-shared-build-pipeline-steps@5.0') _
+
+cordaPipeline(
+ nexusAppId: 'com.corda.CSDE-kotlin.5.0',
+ publishRepoPrefix: '',
+ slimBuild: true,
+ runUnitTests: false,
+ dedicatedJobForSnykDelta: false,
+ slackChannel: '#corda-corda5-dev-ex-build-notifications',
+ gitHubComments: false
+ )
diff --git a/kotlin-samples/oracle-foreign-exchange/.ci/nightly/JenkinsfileSnykScan b/kotlin-samples/oracle-foreign-exchange/.ci/nightly/JenkinsfileSnykScan
new file mode 100644
index 0000000..fc2b1ee
--- /dev/null
+++ b/kotlin-samples/oracle-foreign-exchange/.ci/nightly/JenkinsfileSnykScan
@@ -0,0 +1,6 @@
+@Library('corda-shared-build-pipeline-steps@5.0') _
+
+cordaSnykScanPipeline (
+ snykTokenId: 'r3-snyk-corda5',
+ snykAdditionalCommands: "--all-sub-projects -d"
+)
diff --git a/kotlin-samples/oracle-foreign-exchange/.gitignore b/kotlin-samples/oracle-foreign-exchange/.gitignore
new file mode 100644
index 0000000..d2879c4
--- /dev/null
+++ b/kotlin-samples/oracle-foreign-exchange/.gitignore
@@ -0,0 +1,86 @@
+
+# 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/**
+
+# ingore temporary data files
+*.dat
\ No newline at end of file
diff --git a/kotlin-samples/oracle-foreign-exchange/.run/runConfigurations/DebugCorDapp.run.xml b/kotlin-samples/oracle-foreign-exchange/.run/runConfigurations/DebugCorDapp.run.xml
new file mode 100644
index 0000000..1d8da82
--- /dev/null
+++ b/kotlin-samples/oracle-foreign-exchange/.run/runConfigurations/DebugCorDapp.run.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/kotlin-samples/oracle-foreign-exchange/.snyk b/kotlin-samples/oracle-foreign-exchange/.snyk
new file mode 100644
index 0000000..a4b3e0e
--- /dev/null
+++ b/kotlin-samples/oracle-foreign-exchange/.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:08:41.029Z
+ created: 2023-02-02T17:08:41.032Z
+patch: {}
\ No newline at end of file
diff --git a/kotlin-samples/oracle-foreign-exchange/README.md b/kotlin-samples/oracle-foreign-exchange/README.md
new file mode 100644
index 0000000..5de3330
--- /dev/null
+++ b/kotlin-samples/oracle-foreign-exchange/README.md
@@ -0,0 +1,101 @@
+# Foreign Exchange
+In this app, you can trigger the `CreateFxTransaction` client-sided workflow to request the creation of a Foreign Exchange
+transaction from one currency to another with any other member vNode within the network.
+
+This CorDapp demonstrates the implementation of Oracle Services in Corda 5.
+
+## About this sample
+
+The following diagram illustrates the logic of the CreateFxTransaction workflow which instructs the CorDapp to create foreign exchange
+transactions. In this example let us use `Alice` as the initiator and `Bob` as the recipient:
+
+![CreateFx Transaction Workflow](oracle-foreign-exchange-sample.png)
+
+- The flow starts with `Alice` interpreting the request that triggered the flow.
+- Then, `Alice` executes the `QuoteExchangeRateSubFlow` to request a conversion rate from the `Fx Oracle Service` given the inputs.
+- The oracle service responds by returning the correct conversion rate.
+- Next, `Alice` executes the `ConfirmQuoteSubFlow` to request `Bob` to confirm if the conversion rate is correct from its perspective.
+- Within the SubFlow, `Bob` executes the `QuoteAgainSubFlow` to communicate to the `Fx Oracle service` for a conversion rate
+independently from `Alice`.
+- The `Fx Oracle Service` responds to `Bob` the same conversion rate.
+- Once, `Bob` is satisfied with equality of the proposed conversion rate, it responds to `Alice` with a confirmation
+- Now that `Alice` and `Bob` both agree, `Alice` creates and signs a transaction detailing everything about the foreign exchange
+- `Alice` sends the transaction to all relevant parties (the `Notary` and then `Bob`)
+- The `Notary` notarizes the transaction and sends the fully signed transaction back to `Alice`, who sends it back to `Bob`
+so that every node updates their record to include the new foreign exchange transaction
+
+---
+## Usage
+### Setting Up
+1. Start the sandbox environment by clicking the Gradle task `Tasks > csde-cordapp > startCorda`.
+ A successful deployment will allow you to open the REST APIs: https://localhost:8888/api/v1/swagger
+2. Deploy the CorDapp by clicking `Tasks > csde-cordapp > 5-vNodesSetup`. When successful, you should be able to see the
+CPI metadata of the CorDapp you deployed by calling the `GET /cpi/` endpoint.
+3. Take note of the identity short hash of the `Alice` member node by either:
+ 1. looking at the output log of the run from the previous step OR
+ 2. using the Gradle task `Tasks > csde-qeuries > listVNodes` to lists all the member nodes labeled with their identity
+ short hashes
+
+### Running the app
+#### Getting ready to trigger the workflow to create a transaction
+Please note that the following steps for running the app will use `Alice` as the initiator and `Bob` as the recipient for
+the transaction but feel free to use any other member node as the recipient. Just be sure to correctly include the correct
+'identity short hash' for the initiator and correct 'X500 Name' of the member node.
+
+We trigger flows using the endpoint `POST /flow/{holdingidentityshorthash}` and obtain the flow result using the endpoint
+`GET /flow/{holdingidentityshorthash}/{clientrequestid}`, where:
+- `holdingidentityshorthash`: the id of the network participants that will initiate the initial client-startable workflow
+- `clientrequestid`: the id you specify in the flow requestBody when you trigger a flow.
+
+To trigger the correct flow, we need to identify the `flowClassName`, which can be obtained by calling the endpoint
+`GET /flowclass/{holdingidentityshorthash}`.
+
+#### Triggering and Fetching the flow to create a Foreign Exchange Transaction
+Now that we have all the necessary information let's initate our transaction flow!
+
+To create the transaction, call `POST /flow/{holdingidentityshorthash}` using `Alice`'s identity hash and the following
+request body:
+```json
+{
+ "clientRequestId": "create-fx-1",
+ "flowClassName": "com.r3.developers.samples.fx.workflows.CreateFxTransaction",
+ "requestBody": {
+ "convertingFrom": "GBP",
+ "convertingTo": "USD",
+ "amount": 100,
+ "recipientMemberName":"CN=Bob, OU=Test Dept, O=R3, L=London, C=GB"
+ }
+}
+```
+
+To view the results of the flow and obtain the transaction details specified by the implementation of the code, call
+`GET /flow/{holdingidentityshorthash}/create-fx-1`. The response JSON should look like the following:
+```json
+{
+ "holdingIdentityShortHash": "12C58A1CAF2A",
+ "clientRequestId": "create-fx-1",
+ "flowId": "1ec4c16c-618c-46bd-9f35-0d5c07fdbd09",
+ "flowStatus": "COMPLETED",
+ "flowResult": "The FX transaction of 100 GBP to USD (exchange rate = 1.27) amounting to 127.0 is SUCCESSFUL. | transaction:SHA-256D:D8B1AC29E23E175B2B79056756DDFA1C1DAF66FF958B7FC047BF8BAD0D588862",
+ "flowError": null,
+ "timestamp": "2023-08-01T06:55:00.000Z"
+}
+```
+
+#### Closing down the CorDapp
+
+Simply run the Gradle task `Tasks > csde-corda > stopCorda`
+
+Alternatively you can run the following command from the terminal:
+```
+./gradlew stopCorda
+```
+
+This is because resources that were created from the previous `startCorda` Gradle task needs to be properly closed down.
+
+----
+### Constraints:
+Please bear in mind the following restrictions for this CorDapp sample:
+1. The supported currencies are: `[ GBP, EUR, USD, CAD ]`.
+2. The requested transaction amount must be greater than 0.00 of any currency.
+3. The conversion rates are hardcoded. Create external HTTP requests inside the sandbox environment merits its own sample.
\ No newline at end of file
diff --git a/kotlin-samples/oracle-foreign-exchange/build.gradle b/kotlin-samples/oracle-foreign-exchange/build.gradle
new file mode 100644
index 0000000..af75bfa
--- /dev/null
+++ b/kotlin-samples/oracle-foreign-exchange/build.gradle
@@ -0,0 +1,84 @@
+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.samples'
+ version '1.0-SNAPSHOT'
+
+ def javaVersion = VERSION_11
+
+ // Configure the CSDE
+ csde {
+ cordaClusterURL = "https://localhost:8888"
+ networkConfigFile = "config/static-network-config.json"
+ r3RootCertFile = "config/r3-ca-key.pem"
+ corDappCpiName = "MyCorDapp"
+ notaryCpiName = "NotaryServer"
+ cordaRpcUser = "admin"
+ cordaRpcPasswd ="admin"
+ workflowsModuleName = workflowsModule
+ csdeWorkspaceDir = "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 Kotlin compiler options we need to build a CorDapp.
+ tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
+ kotlinOptions {
+ allWarningsAsErrors = false
+
+ // Specify the version of Kotlin that we are that we will be developing.
+ languageVersion = '1.7'
+ // Specify the Kotlin libraries that code is compatible with
+ apiVersion = '1.7'
+ // Note that we Need to use a version of Kotlin that will be compatible with the Corda API.
+ // Currently that is developed in Kotlin 1.7 so picking the same version ensures compatibility with that.
+
+ // Specify the version of Java to target.
+ jvmTarget = javaVersion
+
+ // Needed for reflection to work correctly.
+ javaParameters = true
+
+ // -Xjvm-default determines how Kotlin supports default methods.
+ // JetBrains currently recommends developers use -Xjvm-default=all
+ // https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.jvm/-jvm-default/
+ freeCompilerArgs += [
+ "-Xjvm-default=all"
+ ]
+ }
+ }
+
+ repositories {
+ // All dependencies are held in Maven Central
+ mavenLocal()
+ mavenCentral()
+ }
+
+ tasks.withType(Test).configureEach {
+ useJUnitPlatform()
+ }
+
+}
+
+publishing {
+ publications {
+ maven(MavenPublication) {
+ artifactId "corda-oracle-foreign-exchange-kotlin-sample"
+ groupId project.group
+ artifact jar
+ }
+ }
+}
\ No newline at end of file
diff --git a/kotlin-samples/oracle-foreign-exchange/config/gradle-plugin-default-key.pem b/kotlin-samples/oracle-foreign-exchange/config/gradle-plugin-default-key.pem
new file mode 100644
index 0000000..5294bbd
--- /dev/null
+++ b/kotlin-samples/oracle-foreign-exchange/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/kotlin-samples/oracle-foreign-exchange/config/log4j2.xml b/kotlin-samples/oracle-foreign-exchange/config/log4j2.xml
new file mode 100644
index 0000000..909222c
--- /dev/null
+++ b/kotlin-samples/oracle-foreign-exchange/config/log4j2.xml
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/kotlin-samples/oracle-foreign-exchange/config/r3-ca-key.pem b/kotlin-samples/oracle-foreign-exchange/config/r3-ca-key.pem
new file mode 100644
index 0000000..a803613
--- /dev/null
+++ b/kotlin-samples/oracle-foreign-exchange/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-----
\ No newline at end of file
diff --git a/kotlin-samples/oracle-foreign-exchange/config/static-network-config.json b/kotlin-samples/oracle-foreign-exchange/config/static-network-config.json
new file mode 100644
index 0000000..57dae70
--- /dev/null
+++ b/kotlin-samples/oracle-foreign-exchange/config/static-network-config.json
@@ -0,0 +1,27 @@
+[
+ {
+ "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"
+ },
+ {
+ "x500Name" : "CN=ForeignExchangeService, OU=Test Dept, O=R3, L=London, C=GB",
+ "cpi" : "MyCorDapp"
+ }
+]
diff --git a/kotlin-samples/oracle-foreign-exchange/contracts/build.gradle b/kotlin-samples/oracle-foreign-exchange/contracts/build.gradle
new file mode 100644
index 0000000..37cb3dd
--- /dev/null
+++ b/kotlin-samples/oracle-foreign-exchange/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 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).
+ contract {
+ name "ContractsModuleNameHere"
+ versionId 1
+ vendor "VendorNameHere"
+ }
+}
+
+publishing {
+ publications {
+ maven(MavenPublication) {
+ from components.cordapp
+ }
+ }
+}
diff --git a/kotlin-samples/oracle-foreign-exchange/contracts/src/main/kotlin/com/r3/developers/samples/fx/ForeignExchange.kt b/kotlin-samples/oracle-foreign-exchange/contracts/src/main/kotlin/com/r3/developers/samples/fx/ForeignExchange.kt
new file mode 100644
index 0000000..b003a38
--- /dev/null
+++ b/kotlin-samples/oracle-foreign-exchange/contracts/src/main/kotlin/com/r3/developers/samples/fx/ForeignExchange.kt
@@ -0,0 +1,24 @@
+package com.r3.developers.samples.fx
+
+import net.corda.v5.ledger.utxo.BelongsToContract
+import net.corda.v5.ledger.utxo.ContractState
+import java.math.BigDecimal
+import java.security.PublicKey
+
+//Links the contract state "ForeignExchange" with the ForeignExchangeContract contract class
+@BelongsToContract(ForeignExchangeContract::class)
+class ForeignExchange(
+ val initiatorIdentity: PublicKey,
+ val recipientIdentity: PublicKey,
+ val amount: BigDecimal,
+ val convertingFrom: SupportedCurrencyCodes,
+ val convertingTo: SupportedCurrencyCodes,
+ val exchangeRate: BigDecimal,
+ val convertedAmount: BigDecimal,
+ val status: TransactionStatuses,
+ private val participants: List
+): ContractState {
+ override fun getParticipants(): List = participants
+
+ override fun toString() = "The FX transaction of $amount $convertingFrom to $convertingTo (exchange rate = ${exchangeRate.toDouble()}) amounting to ${convertedAmount.toDouble()} is $status."
+}
diff --git a/kotlin-samples/oracle-foreign-exchange/contracts/src/main/kotlin/com/r3/developers/samples/fx/ForeignExchangeContract.kt b/kotlin-samples/oracle-foreign-exchange/contracts/src/main/kotlin/com/r3/developers/samples/fx/ForeignExchangeContract.kt
new file mode 100644
index 0000000..825049a
--- /dev/null
+++ b/kotlin-samples/oracle-foreign-exchange/contracts/src/main/kotlin/com/r3/developers/samples/fx/ForeignExchangeContract.kt
@@ -0,0 +1,72 @@
+package com.r3.developers.samples.fx
+
+import net.corda.v5.base.annotations.CordaSerializable
+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 java.math.BigDecimal
+import java.security.PublicKey
+
+@CordaSerializable
+enum class SupportedCurrencyCodes { GBP, EUR, USD, CAD }
+
+@CordaSerializable
+enum class TransactionStatuses { SUCCESSFUL, FAILED }
+
+//This is used by the CreateFxTransaction workflow class to add a command to a ForeignExchange state
+interface ForeignExchangeCommands: Command {
+ class Create(
+ val initiatorIdentity: PublicKey,
+ val recipientIdentity: PublicKey,
+ val amount: BigDecimal,
+ val convertingFrom: SupportedCurrencyCodes,
+ val convertingTo: SupportedCurrencyCodes,
+ val exchangeRate: BigDecimal,
+ val convertedAmount: BigDecimal,
+ val status: TransactionStatuses
+ ): ForeignExchangeCommands
+}
+
+class ForeignExchangeContract: Contract {
+
+ internal companion object {
+ const val CONTRACT_RULE_SINGLE_COMMAND = "Exactly one ForeignExchangeCommands command must be present in the transaction."
+ const val CONTRACT_RULE_UNRECOGNIZED_COMMAND = "An unrecognized Command is given:"
+ const val CONTRACT_RULE_SINGLE_OUTPUT = "A ForeignExchange transaction requires a single output state to be created."
+ const val NON_POSITIVE = "The requested transaction amount is not greater than 0."
+ const val NON_MATCHING = "The proposed output state variables does not match the inputs of the command."
+ }
+
+ override fun verify(transaction: UtxoLedgerTransaction) {
+ val command = transaction.getCommands(ForeignExchangeCommands::class.java).singleOrNull()
+ ?: throw CordaRuntimeException(CONTRACT_RULE_SINGLE_COMMAND)
+ val commandName = command::class.java.name
+ val output = transaction.getOutputStates(ForeignExchange::class.java).singleOrNull()
+ ?: throw CordaRuntimeException(CONTRACT_RULE_SINGLE_OUTPUT)
+
+ when(command) {
+ is ForeignExchangeCommands.Create -> {
+ NON_POSITIVE using { output.amount > BigDecimal(0) }
+ NON_MATCHING using {
+ command.initiatorIdentity == output.initiatorIdentity &&
+ command.recipientIdentity == output.recipientIdentity &&
+ command.amount == output.amount &&
+ command.convertingFrom == output.convertingFrom &&
+ command.convertingTo == output.convertingTo &&
+ command.exchangeRate == output.exchangeRate &&
+ command.convertedAmount == output.convertedAmount &&
+ command.status == output.status
+ }
+ }
+ else -> {
+ throw IllegalArgumentException("$CONTRACT_RULE_UNRECOGNIZED_COMMAND $commandName.")
+ }
+ }
+ }
+
+ private infix fun String.using(expr: () -> Boolean) {
+ if (!expr.invoke()) throw CordaRuntimeException("Failed contract requirement: $this")
+ }
+
+}
\ No newline at end of file
diff --git a/kotlin-samples/oracle-foreign-exchange/gradle.properties b/kotlin-samples/oracle-foreign-exchange/gradle.properties
new file mode 100644
index 0000000..282b05a
--- /dev/null
+++ b/kotlin-samples/oracle-foreign-exchange/gradle.properties
@@ -0,0 +1,37 @@
+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
\ No newline at end of file
diff --git a/kotlin-samples/oracle-foreign-exchange/gradle/wrapper/gradle-wrapper.jar b/kotlin-samples/oracle-foreign-exchange/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..943f0cb
Binary files /dev/null and b/kotlin-samples/oracle-foreign-exchange/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/kotlin-samples/oracle-foreign-exchange/gradle/wrapper/gradle-wrapper.properties b/kotlin-samples/oracle-foreign-exchange/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..5083229
--- /dev/null
+++ b/kotlin-samples/oracle-foreign-exchange/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/kotlin-samples/oracle-foreign-exchange/gradlew b/kotlin-samples/oracle-foreign-exchange/gradlew
new file mode 100644
index 0000000..65dcd68
--- /dev/null
+++ b/kotlin-samples/oracle-foreign-exchange/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/kotlin-samples/oracle-foreign-exchange/gradlew.bat b/kotlin-samples/oracle-foreign-exchange/gradlew.bat
new file mode 100644
index 0000000..93e3f59
--- /dev/null
+++ b/kotlin-samples/oracle-foreign-exchange/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/kotlin-samples/oracle-foreign-exchange/oracle-foreign-exchange-sample.png b/kotlin-samples/oracle-foreign-exchange/oracle-foreign-exchange-sample.png
new file mode 100644
index 0000000..dc35e21
Binary files /dev/null and b/kotlin-samples/oracle-foreign-exchange/oracle-foreign-exchange-sample.png differ
diff --git a/kotlin-samples/oracle-foreign-exchange/settings.gradle b/kotlin-samples/oracle-foreign-exchange/settings.gradle
new file mode 100644
index 0000000..ce7b7f6
--- /dev/null
+++ b/kotlin-samples/oracle-foreign-exchange/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 = 'corda5-oracle-foreign-exchange-kotlin'
+include ':workflows'
+include ':contracts'
+
diff --git a/kotlin-samples/oracle-foreign-exchange/workflows/build.gradle b/kotlin-samples/oracle-foreign-exchange/workflows/build.gradle
new file mode 100644
index 0000000..ddc1790
--- /dev/null
+++ b/kotlin-samples/oracle-foreign-exchange/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/kotlin-samples/oracle-foreign-exchange/workflows/src/main/kotlin/com/r3/developers/samples/fx/services/FxService.kt b/kotlin-samples/oracle-foreign-exchange/workflows/src/main/kotlin/com/r3/developers/samples/fx/services/FxService.kt
new file mode 100644
index 0000000..30a89be
--- /dev/null
+++ b/kotlin-samples/oracle-foreign-exchange/workflows/src/main/kotlin/com/r3/developers/samples/fx/services/FxService.kt
@@ -0,0 +1,59 @@
+package com.r3.developers.samples.fx.services
+
+import net.corda.v5.base.exceptions.CordaRuntimeException
+import org.slf4j.LoggerFactory
+import java.math.BigDecimal
+
+class FxService() {
+
+ private companion object{
+ val log = LoggerFactory.getLogger(this::class.java.enclosingClass)
+
+ const val INVALID_CURRENCY_PAIR = "Invalid currencyPair given"
+ }
+
+ fun quoteFxRate(currencyPair: String): BigDecimal {
+ "$INVALID_CURRENCY_PAIR: '$currencyPair'" using { enumContains(currencyPair) }
+ val conversionRate = ConversionRateTable.valueOf(currencyPair).conversionRate
+ log.info("currencyPair:$currencyPair | conversionRate:$conversionRate")
+ return conversionRate
+ }
+
+ private infix fun String.using(expr: () -> Boolean) {
+ if (!expr.invoke()) throw CordaRuntimeException("[FxService] Failed Expectation: $this")
+ }
+
+ private inline fun > enumContains(name: String): Boolean {
+ return enumValues().any { it.name == name}
+ }
+
+}
+
+/**
+Please note: currently, it is difficult to create HTTP requests inside the sandbox environment.
+So, rather than querying for conversion rates via an external source, we are simply hardcoding a small set of currencies
+It is not impossible, but it is a separate advanced topic that merits its own sample or be an extended sample of this
+Stay tuned with update notes to see when easy external messaging within the sandbox environment will be released!
+ **/
+//convention for currencyPairing: "[currency_from][currency_to]"
+enum class ConversionRateTable(val conversionRate: BigDecimal) {
+ // Source: https://www.xe.com/currencyconverter/convert/
+ // Timestamp of when data was obtained: 2023-08-24T16:00Z
+ GBPGBP(BigDecimal(1)),
+ GBPEUR(BigDecimal(1.16)),
+ GBPUSD(BigDecimal(1.27)),
+ GBPCAD(BigDecimal(1.72)),
+ EURGBP(BigDecimal(0.85)),
+ EUREUR(BigDecimal(1)),
+ EURUSD(BigDecimal(1.08)),
+ EURCAD(BigDecimal(1.47)),
+ USDGBP(BigDecimal(0.78)),
+ USDEUR(BigDecimal(0.91)),
+ USDUSD(BigDecimal(1)),
+ USDCAD(BigDecimal(1.35)),
+ CADGBP(BigDecimal(0.58)),
+ CADEUR(BigDecimal(0.67)),
+ CADUSD(BigDecimal(0.73)),
+ CADCAD(BigDecimal(1))
+
+}
\ No newline at end of file
diff --git a/kotlin-samples/oracle-foreign-exchange/workflows/src/main/kotlin/com/r3/developers/samples/fx/workflows/ConfirmQuoteSubFlow.kt b/kotlin-samples/oracle-foreign-exchange/workflows/src/main/kotlin/com/r3/developers/samples/fx/workflows/ConfirmQuoteSubFlow.kt
new file mode 100644
index 0000000..077d9a1
--- /dev/null
+++ b/kotlin-samples/oracle-foreign-exchange/workflows/src/main/kotlin/com/r3/developers/samples/fx/workflows/ConfirmQuoteSubFlow.kt
@@ -0,0 +1,73 @@
+package com.r3.developers.samples.fx.workflows
+
+import net.corda.v5.application.flows.CordaInject
+import net.corda.v5.application.flows.FlowEngine
+import net.corda.v5.application.flows.InitiatedBy
+import net.corda.v5.application.flows.InitiatingFlow
+import net.corda.v5.application.flows.ResponderFlow
+import net.corda.v5.application.flows.SubFlow
+import net.corda.v5.application.messaging.FlowSession
+import net.corda.v5.base.annotations.CordaSerializable
+import net.corda.v5.base.annotations.Suspendable
+import net.corda.v5.base.types.MemberX500Name
+import org.slf4j.LoggerFactory
+import java.math.BigDecimal
+
+@CordaSerializable
+data class RecipientConfirmQuoteRequest(val currencyPair: String, val conversionRate: BigDecimal, val recipientName: MemberX500Name, val fxServiceName: MemberX500Name)
+
+@CordaSerializable
+data class RecipientConfirmQuoteResponse(val confirmed: Boolean, val recipientConversionRate: BigDecimal)
+
+@InitiatingFlow(protocol = "confirm-quote")
+class ConfirmQuoteSubFlow(
+ private val confirmQuoteRequest: RecipientConfirmQuoteRequest,
+ private val sessionAliceToRecipient: FlowSession
+): SubFlow {
+
+ private companion object {
+ val log = LoggerFactory.getLogger(this::class.java.enclosingClass)
+ const val FLOW_CALL = "ConfirmQuoteSubFlow.call() called"
+ }
+
+ @Suspendable
+ override fun call(): RecipientConfirmQuoteResponse {
+ log.info(FLOW_CALL)
+ return sessionAliceToRecipient.sendAndReceive(RecipientConfirmQuoteResponse::class.java,confirmQuoteRequest)
+ }
+}
+
+
+@InitiatedBy(protocol = "confirm-quote")
+class ConfirmQuoteSubFlowResponder(): ResponderFlow {
+ private companion object {
+ val log = LoggerFactory.getLogger(this::class.java.enclosingClass)
+ const val FLOW_CALL = "ConfirmQuoteSubFlowResponder.call() called"
+ const val REQUEST_QUOTE = "Recipient to confirming quote from FxService"
+ const val SENDING = "Sending confirmation to requester"
+ }
+
+ @CordaInject
+ lateinit var flowEngine: FlowEngine
+
+ @Suspendable
+ override fun call(session: FlowSession) {
+ log.info(FLOW_CALL)
+ val receivedMessage = session.receive(RecipientConfirmQuoteRequest::class.java)
+ val currencyPair = receivedMessage.currencyPair
+ val fxServiceName = receivedMessage.fxServiceName
+ val initiatorConversionRate = receivedMessage.conversionRate
+
+ //query to oracle
+ log.info(REQUEST_QUOTE)
+ val requestBody = QuoteFxRateRequest(currencyPair, fxServiceName)
+ val quoteAgainResponse: QuoteFxRateResponse = flowEngine.subFlow(QuoteAgainSubFlow(requestBody))
+ val recipientConversionRate = quoteAgainResponse.conversionRate
+ val doTheyMatch = (initiatorConversionRate==recipientConversionRate)
+ log.info("initiatorConversionRate:$initiatorConversionRate | recipientConversionRate:$recipientConversionRate | doTheyMatch: $doTheyMatch")
+
+ log.info(SENDING)
+ val response = RecipientConfirmQuoteResponse(doTheyMatch,recipientConversionRate)
+ session.send(response)
+ }
+}
\ No newline at end of file
diff --git a/kotlin-samples/oracle-foreign-exchange/workflows/src/main/kotlin/com/r3/developers/samples/fx/workflows/CreateFxTransaction.kt b/kotlin-samples/oracle-foreign-exchange/workflows/src/main/kotlin/com/r3/developers/samples/fx/workflows/CreateFxTransaction.kt
new file mode 100644
index 0000000..d22823e
--- /dev/null
+++ b/kotlin-samples/oracle-foreign-exchange/workflows/src/main/kotlin/com/r3/developers/samples/fx/workflows/CreateFxTransaction.kt
@@ -0,0 +1,162 @@
+package com.r3.developers.samples.fx.workflows
+
+import com.r3.developers.samples.fx.ForeignExchange
+import com.r3.developers.samples.fx.ForeignExchangeCommands
+import com.r3.developers.samples.fx.SupportedCurrencyCodes
+import com.r3.developers.samples.fx.TransactionStatuses
+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.flows.InitiatingFlow
+import net.corda.v5.application.marshalling.JsonMarshallingService
+import net.corda.v5.application.membership.MemberLookup
+import net.corda.v5.application.messaging.FlowMessaging
+import net.corda.v5.base.annotations.Suspendable
+import net.corda.v5.base.types.MemberX500Name
+import net.corda.v5.ledger.utxo.UtxoLedgerService
+import net.corda.v5.membership.MemberInfo
+import org.slf4j.LoggerFactory
+import java.math.BigDecimal
+import java.security.PublicKey
+import java.time.Instant
+import java.time.temporal.ChronoUnit
+
+// This is the client-side worfklow that gets called first. It will:
+// - set up and identify the recipient member node, the FxService node and the notary node
+// - initiate a flowSession between Alice and the FxService and trigger the QuoteExchangeRateSubFlow sub-flow to get a quote given the requested inputs
+// - initiate another flowSession between Alice and the recipient (specified by MemberX500 name in the inputs) to confirm the quote
+// -- Note, how the ConfirmQuoteSubFlow sub-flow that gets triggered will trigger the QuoteAgainSubFlow sub-flow so that
+// -- the flowSession connects the recipient with the FxService
+// - if both parties agree with the conversion rate, build the ForeignExchange transaction including the output state, command, etc.
+// - finalize the transaction and return the id as well as the output state to communicate the entire flow's completion.
+@InitiatingFlow(protocol = "create-fx-tx")
+class CreateFxTransaction(): ClientStartableFlow {
+
+ private data class CreateFxTransaction(
+ val convertingFrom: SupportedCurrencyCodes,
+ val convertingTo: SupportedCurrencyCodes,
+ val amount: BigDecimal,
+ val recipientMemberName: String
+ )
+
+ private companion object {
+ val log = LoggerFactory.getLogger(this::class.java.enclosingClass)
+
+ const val CALLED = "CreateFxTransaction.call() called."
+ const val SET_UP = "Initializing flow variables."
+ const val RECIPIENT_NOT_FOUND = "The recipient member vNode in the request body is not found on the network."
+ const val GET_FX_QUOTE = "Getting FX Rate Quote from FxService"
+ const val GET_RECIPIENT_QUOTE_CONFIRMATION = "Asking recipient to confirm conversion rate with FxService."
+ const val QUOTE_UNCONFIRMED = "Recipient declined the fx quote proposed by initiator. "
+ const val PREPARE_TRANSACTION = "Prparing the transaction variables."
+ const val BUILD_TRANSACTION = "Building the transaction."
+ const val FINALIZE_TRANSACTION = "Finalizing the transaction."
+
+ val notaryName: MemberX500Name = MemberX500Name.parse("CN=NotaryService, OU=Test Dept, O=R3, L=London, C=GB")
+ val fxServiceName: MemberX500Name = MemberX500Name.parse("CN=ForeignExchangeService, OU=Test Dept, O=R3, L=London, C=GB")
+ }
+
+ @CordaInject
+ lateinit var jsonMarshallingService: JsonMarshallingService
+
+ @CordaInject
+ lateinit var memberLookup: MemberLookup
+
+ @CordaInject
+ lateinit var flowEngine: FlowEngine
+
+ @CordaInject
+ lateinit var flowMessaging: FlowMessaging
+
+ @CordaInject
+ lateinit var ledgerService: UtxoLedgerService
+
+ @Suspendable
+ override fun call(requestBody: ClientRequestBody): String {
+
+ // Deserializing the JSON string input into meaningful data for the workflow
+ log.info(CALLED)
+ val request = requestBody.getRequestBodyAs(jsonMarshallingService,CreateFxTransaction::class.java)
+ val convertingFrom = request.convertingFrom
+ val convertingTo = request.convertingTo
+ val amount = request.amount
+ val recipientName = MemberX500Name.parse(request.recipientMemberName)
+
+ // Obtaining the public identity of the member nodes involved in the transaction
+ log.info(SET_UP)
+ val recipientMemberInfo: MemberInfo = memberLookup.lookup(recipientName)
+ ?: throw IllegalArgumentException("$RECIPIENT_NOT_FOUND: '$recipientName'")
+ val ourPublicIdentity: PublicKey = memberLookup.myInfo().ledgerKeys.first()
+ val recipientMemberIdentity: PublicKey = recipientMemberInfo.ledgerKeys.first()
+
+ // Executing the sub-flow to obtain a quote from the oracle FxService
+ log.info(GET_FX_QUOTE)
+ val sessionAliceService = flowMessaging.initiateFlow(fxServiceName)
+ val currencyPair = "$convertingFrom$convertingTo"
+ val quoteRequest = QuoteFxRateRequest(currencyPair, fxServiceName)
+ val quoteResponse: QuoteFxRateResponse = flowEngine.subFlow(QuoteExchangeRateSubFlow(quoteRequest,sessionAliceService))
+ val initiatorConversionRate = quoteResponse.conversionRate
+ val convertedAmount = amount*initiatorConversionRate
+
+ // Communicating the FX rate with the recipient so that it may also execute sub-flows to obtain
+ // a quote from the oracle FxService themselves
+ log.info(GET_RECIPIENT_QUOTE_CONFIRMATION)
+ val sessionAliceRecipientQuote = flowMessaging.initiateFlow(recipientName)
+ val recipientConfirmationRequest = RecipientConfirmQuoteRequest(currencyPair,initiatorConversionRate,recipientName, fxServiceName)
+ val recipientResponse: RecipientConfirmQuoteResponse = flowEngine.subFlow(ConfirmQuoteSubFlow(recipientConfirmationRequest,sessionAliceRecipientQuote))
+
+ // If the quotes obtained by the initiator and the recipient, then the workflow ends and returns the discrepancy
+ if(!recipientResponse.confirmed){
+ return buildString {
+ append(QUOTE_UNCONFIRMED)
+ append("currencyPair:$currencyPair. ")
+ append("initiatorConversionRate:${initiatorConversionRate.toDouble()}. ")
+ append("recipientConversionRate:${recipientResponse.recipientConversionRate.toDouble()}.")
+ }
+ }
+
+ // Creating the output state and command involved when building the transaction
+ log.info(PREPARE_TRANSACTION)
+ val outputState = ForeignExchange(
+ ourPublicIdentity,
+ recipientMemberIdentity,
+ amount,
+ convertingFrom,
+ convertingTo,
+ initiatorConversionRate,
+ convertedAmount,
+ TransactionStatuses.SUCCESSFUL,
+ listOf(ourPublicIdentity,recipientMemberIdentity)
+ )
+ val command = ForeignExchangeCommands.Create(
+ ourPublicIdentity,
+ recipientMemberIdentity,
+ amount,
+ convertingFrom,
+ convertingTo,
+ initiatorConversionRate,
+ convertedAmount,
+ TransactionStatuses.SUCCESSFUL
+ )
+
+ // The initiator creates and signs the foreign exchange transaction
+ log.info(BUILD_TRANSACTION)
+ val fxTransaction = ledgerService.createTransactionBuilder()
+ .setNotary(notaryName)
+ .addOutputState(outputState) // contract state verification happens under the hood here
+ .addCommand(command)
+ .addSignatories(listOf(ourPublicIdentity,recipientMemberIdentity))
+ .setTimeWindowUntil(Instant.now().plus(1,ChronoUnit.DAYS))
+ .toSignedTransaction()
+
+ // The initiator requests for the finalization of the transaction from all the involved parties
+ log.info(FINALIZE_TRANSACTION)
+ val sessionAliceRecipientFinalize = flowMessaging.initiateFlow(recipientName)
+ val finalizedTransaction = flowEngine.subFlow(FinalizeFxTransactionSubFlow(fxTransaction,listOf(sessionAliceRecipientFinalize)))
+
+ // remember how we override the state ForeignExchange.toString() method to be a meaningful output message
+ return "$outputState | transaction:${finalizedTransaction.id}"
+ }
+
+}
\ No newline at end of file
diff --git a/kotlin-samples/oracle-foreign-exchange/workflows/src/main/kotlin/com/r3/developers/samples/fx/workflows/FinalizeFxTransactionSubFlow.kt b/kotlin-samples/oracle-foreign-exchange/workflows/src/main/kotlin/com/r3/developers/samples/fx/workflows/FinalizeFxTransactionSubFlow.kt
new file mode 100644
index 0000000..cd583ee
--- /dev/null
+++ b/kotlin-samples/oracle-foreign-exchange/workflows/src/main/kotlin/com/r3/developers/samples/fx/workflows/FinalizeFxTransactionSubFlow.kt
@@ -0,0 +1,53 @@
+package com.r3.developers.samples.fx.workflows
+
+import net.corda.v5.application.flows.CordaInject
+import net.corda.v5.application.flows.InitiatedBy
+import net.corda.v5.application.flows.InitiatingFlow
+import net.corda.v5.application.flows.ResponderFlow
+import net.corda.v5.application.flows.SubFlow
+import net.corda.v5.application.messaging.FlowSession
+import net.corda.v5.ledger.utxo.UtxoLedgerService
+import net.corda.v5.ledger.utxo.transaction.UtxoSignedTransaction
+import org.slf4j.LoggerFactory
+import java.time.Instant
+
+@InitiatingFlow(protocol = "finalize")
+internal class FinalizeFxTransactionSubFlow(
+private val fxTransaction: UtxoSignedTransaction,
+private val sessions: List
+): SubFlow {
+
+ private companion object {
+ private val log = LoggerFactory.getLogger(this::class.java.enclosingClass)
+ const val FLOW_CALL = "FinalizeFxTransactionSubFlow.call() called."
+ }
+
+ @CordaInject
+ lateinit var ledgerService: UtxoLedgerService
+
+ override fun call(): UtxoSignedTransaction {
+ log.info(FLOW_CALL)
+ return ledgerService.finalize(fxTransaction, sessions).transaction
+ }
+
+}
+
+@InitiatedBy(protocol = "finalize")
+internal class FinalizeFxTransactionSubFlowResponder(): ResponderFlow {
+
+ private companion object {
+ val log = LoggerFactory.getLogger(this::class.java.enclosingClass)
+ const val FLOW_CALL = "FinalizeFxTransactionSubFlowResponder.call() called."
+ }
+
+ @CordaInject
+ lateinit var utxoLedgerService: UtxoLedgerService
+
+ override fun call(session: FlowSession) {
+ log.info(FLOW_CALL)
+ utxoLedgerService.receiveFinality(session){
+ // Implement any pre-signing checks here...
+ transaction -> transaction.timeWindow.until.isAfter(Instant.now())
+ }
+ }
+}
\ No newline at end of file
diff --git a/kotlin-samples/oracle-foreign-exchange/workflows/src/main/kotlin/com/r3/developers/samples/fx/workflows/QuoteAgainSubFlow.kt b/kotlin-samples/oracle-foreign-exchange/workflows/src/main/kotlin/com/r3/developers/samples/fx/workflows/QuoteAgainSubFlow.kt
new file mode 100644
index 0000000..045a717
--- /dev/null
+++ b/kotlin-samples/oracle-foreign-exchange/workflows/src/main/kotlin/com/r3/developers/samples/fx/workflows/QuoteAgainSubFlow.kt
@@ -0,0 +1,39 @@
+package com.r3.developers.samples.fx.workflows
+
+import com.r3.developers.samples.fx.services.FxService
+import net.corda.v5.application.flows.CordaInject
+import net.corda.v5.application.flows.InitiatedBy
+import net.corda.v5.application.flows.InitiatingFlow
+import net.corda.v5.application.flows.ResponderFlow
+import net.corda.v5.application.flows.SubFlow
+import net.corda.v5.application.messaging.FlowMessaging
+import net.corda.v5.application.messaging.FlowSession
+import net.corda.v5.base.annotations.Suspendable
+
+@InitiatingFlow(protocol = "quote-again")
+class QuoteAgainSubFlow (
+ private val quoteFxRateRequest: QuoteFxRateRequest,
+): SubFlow {
+
+ @CordaInject
+ lateinit var flowMessaging: FlowMessaging
+
+ @Suspendable
+ override fun call(): QuoteFxRateResponse {
+ val sessionRecipientAndService = flowMessaging.initiateFlow(quoteFxRateRequest.fxServiceName)
+ return sessionRecipientAndService.sendAndReceive(QuoteFxRateResponse::class.java,quoteFxRateRequest)
+ }
+}
+
+@InitiatedBy(protocol = "quote-again")
+class QuoteAgainSubFlowResponder(): ResponderFlow {
+
+ @Suspendable
+ override fun call(session: FlowSession) {
+ val receivedMessage = session.receive(QuoteFxRateRequest::class.java)
+ val currencyPair = receivedMessage.currencyPair
+ val conversionRate = FxService().quoteFxRate(currencyPair)
+ val response = QuoteFxRateResponse(conversionRate)
+ session.send(response)
+ }
+}
\ No newline at end of file
diff --git a/kotlin-samples/oracle-foreign-exchange/workflows/src/main/kotlin/com/r3/developers/samples/fx/workflows/QuoteExchangeRateSubFlow.kt b/kotlin-samples/oracle-foreign-exchange/workflows/src/main/kotlin/com/r3/developers/samples/fx/workflows/QuoteExchangeRateSubFlow.kt
new file mode 100644
index 0000000..a56e9cc
--- /dev/null
+++ b/kotlin-samples/oracle-foreign-exchange/workflows/src/main/kotlin/com/r3/developers/samples/fx/workflows/QuoteExchangeRateSubFlow.kt
@@ -0,0 +1,65 @@
+package com.r3.developers.samples.fx.workflows
+
+import com.r3.developers.samples.fx.services.FxService
+import net.corda.v5.application.flows.InitiatedBy
+import net.corda.v5.application.flows.InitiatingFlow
+import net.corda.v5.application.flows.ResponderFlow
+import net.corda.v5.application.flows.SubFlow
+import net.corda.v5.application.messaging.FlowSession
+import net.corda.v5.base.annotations.CordaSerializable
+import net.corda.v5.base.annotations.Suspendable
+import net.corda.v5.base.types.MemberX500Name
+import org.slf4j.LoggerFactory
+import java.math.BigDecimal
+
+@CordaSerializable
+data class QuoteFxRateRequest(val currencyPair: String, val fxServiceName: MemberX500Name)
+
+@CordaSerializable
+data class QuoteFxRateResponse(val conversionRate: BigDecimal)
+
+@InitiatingFlow(protocol = "quote-fx")
+class QuoteExchangeRateSubFlow(
+ private val quoteFxRateRequest: QuoteFxRateRequest,
+ private val session: FlowSession
+): SubFlow {
+
+ private companion object {
+ val log = LoggerFactory.getLogger(this::class.java.enclosingClass)
+ const val FLOW_CALL = "QuoteExchangeRateSubFlow.call() called"
+ }
+
+ @Suspendable
+ override fun call(): QuoteFxRateResponse {
+ log.info(FLOW_CALL)
+ return session.sendAndReceive(QuoteFxRateResponse::class.java,quoteFxRateRequest)
+ }
+}
+
+@InitiatedBy(protocol = "quote-fx")
+class QuoteExchangeRateSubFlowResponder(): ResponderFlow {
+
+ private companion object {
+ val log = LoggerFactory.getLogger(this::class.java.enclosingClass)
+
+ const val FLOW_CALL = "QuoteExchangeRateSubFlowResponder.call() called"
+ const val QUOTING = "FxService to generate quote"
+ const val SENDING = "Sending QuoteFxRateResponse to requester"
+ }
+
+ @Suspendable
+ override fun call(session: FlowSession) {
+ log.info(FLOW_CALL)
+
+ val receivedMessage = session.receive(QuoteFxRateRequest::class.java)
+ log.info("receivedMessage:$receivedMessage")
+
+ log.info(QUOTING)
+ val currencyPair = receivedMessage.currencyPair
+ val conversionRate = FxService().quoteFxRate(currencyPair)
+ val response = QuoteFxRateResponse(conversionRate)
+
+ log.info(SENDING)
+ session.send(response)
+ }
+}
\ No newline at end of file