diff --git a/java-samples/sendAndRecieveTransaction/.ci/Jenkinsfile b/java-samples/sendAndRecieveTransaction/.ci/Jenkinsfile new file mode 100644 index 0000000..f926354 --- /dev/null +++ b/java-samples/sendAndRecieveTransaction/.ci/Jenkinsfile @@ -0,0 +1,10 @@ +@Library('corda-shared-build-pipeline-steps@5.0') _ + +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/java-samples/sendAndRecieveTransaction/.ci/nightly/JenkinsfileSnykScan b/java-samples/sendAndRecieveTransaction/.ci/nightly/JenkinsfileSnykScan new file mode 100644 index 0000000..fc2b1ee --- /dev/null +++ b/java-samples/sendAndRecieveTransaction/.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/java-samples/sendAndRecieveTransaction/.gitignore b/java-samples/sendAndRecieveTransaction/.gitignore new file mode 100644 index 0000000..d2879c4 --- /dev/null +++ b/java-samples/sendAndRecieveTransaction/.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/java-samples/sendAndRecieveTransaction/.run/runConfigurations/DebugCorDapp.run.xml b/java-samples/sendAndRecieveTransaction/.run/runConfigurations/DebugCorDapp.run.xml new file mode 100644 index 0000000..1d8da82 --- /dev/null +++ b/java-samples/sendAndRecieveTransaction/.run/runConfigurations/DebugCorDapp.run.xml @@ -0,0 +1,15 @@ + + + + \ No newline at end of file diff --git a/java-samples/sendAndRecieveTransaction/.snyk b/java-samples/sendAndRecieveTransaction/.snyk new file mode 100644 index 0000000..c04521f --- /dev/null +++ b/java-samples/sendAndRecieveTransaction/.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/java-samples/sendAndRecieveTransaction/FlowManagementUI/Dockerfile b/java-samples/sendAndRecieveTransaction/FlowManagementUI/Dockerfile new file mode 100644 index 0000000..016bc21 --- /dev/null +++ b/java-samples/sendAndRecieveTransaction/FlowManagementUI/Dockerfile @@ -0,0 +1,5 @@ +FROM python +WORKDIR /app +COPY . /app +RUN pip install -r requirements.txt +CMD ["python3", "app.py"] \ No newline at end of file diff --git a/java-samples/sendAndRecieveTransaction/FlowManagementUI/README.md b/java-samples/sendAndRecieveTransaction/FlowManagementUI/README.md new file mode 100644 index 0000000..fffff29 --- /dev/null +++ b/java-samples/sendAndRecieveTransaction/FlowManagementUI/README.md @@ -0,0 +1,84 @@ +# Corda 5 CorDapp Flow Management Tool + + +This user guide provides step-by-step instructions on using the Corda 5 flow management tool. This article will help you learn how to connect the running corDapp, make flow calls, configure flow queries, and retrieve results. + +## Prerequisites +* Install and run Python and Flask framework. link. + +* Prepare your local Corda 5 environment. (By default, the Flow Management Tool is looking to connect to https://localhost:8888/api/v5_2/swagger#/ with Login: Admin and Password: Admin.) + +* Clong the Flow Management Tool repository. FlowManagementUI: main + +## Set Up + +1. Assuming your local Corda 5 environment is populated and the swagger endpoint is at: https://localhost:8888/api/v5_2/swagger#/ + +2. Navigate to where you downloaded the Corda 5 Flow Management Tool + +3. To run the framework + * Navigate to the file name using cd command. + * use the python app.py command to run it. + ![image](https://github.com/parisyup/FlowManagementUI/assets/51169685/f0c3bf59-8180-48a0-91cc-80f2d260e530) + + * Later on, click on the IP Address which will open the Interface: + +![image(4)](https://github.com/parisyup/FlowManagementUI/assets/66366646/8d88e37c-edbb-4d6d-8bcd-d773e818a106) + + +The Flow Management Tool should be automatically connected with the CorDapp running locally from your CSDE. You can test the connection by click on the dropdown list at the Flow Initiator section. You should be able to see the vNodes of your started CorDapp from CSDE. + +![image](https://github.com/parisyup/FlowManagementUI/assets/51169685/5a2356f2-cd14-489c-abd0-4afe0bf0d251) + +## Set Up With Docker + +1- Open up Command Prompt + +2- Navigate to the application folder using the CD commands + +3- Ensure that Docker application is open and build the image using the following command: +`docker build -t your-image-name .` + +Make sure to include the dot at the end of the command + +the `your-image-name` at the end of the command can be whatever you like but make sure to use the same name in the next step + +4- Run the docker image using the following command: +`docker run --rm -it --expose 8888 -p 5000:5000 your-image-name` + +5- You can access the website by using https://localhost:5000 or https://127.0.0.1:5000 + +## Using the Flow Management Tool + +### Selecting the Flow Initiator + +As the first step of using the Flow Management Tool, you would need to select the Flow Initiator. The Flow Initiator indicates which vNode will be triggering the flow. If you wish to have Alice to run a transaction to Bob, select the X500Name of Alice. The selected vNode’s shortHash (Corda 5 Network participant identifier) will also be shown below the dropdown list to signify your selection. + +### Function 1: To Make a Flow Call + +1. Click on "Flow Call" tab in the application. +2. Paste the your JSON format request body into the request input box. +3. Click Post button to trigger the call. + +![image(5)](https://github.com/parisyup/FlowManagementUI/assets/66366646/c65195a6-0a70-4354-804e-37884f657746) + + +### Function 2: To Configure Flow Query + +1. Click on the “Flow Query” tab. +2. Choose whether to query a single flow or all flows at the selected Flow Initiator. +3. If you choose to query all of the flows, select “Query All Flows“ then click “Get“. + +![image](https://github.com/parisyup/FlowManagementUI/assets/51169685/0482cfa4-7ee1-42f2-8786-2d8ad80b2936) +4. If you choose to query a single flow, select “Query Single Flow“, please add the ClientID in specified filed. +5. Click on “Get” to retrieve the result. + +![image(6)](https://github.com/parisyup/FlowManagementUI/assets/66366646/13e979b0-f76e-4f2c-9d55-81be8880890b) + +If you have any suggestions or questions, feel free to give us your feedback through Github for a better experience in the future! + +## Conclusion +In summary, our project introduces a specialized flow management layer on top of Swagger for Corda developers. We understand the challenges developers face in testing Corda applications due to the complexity of commands, our solution focuses on simplifying the process. + +Our all-in-one flow management system provides developers with a unified platform, streamlining development and enhancing efficiency. A key feature allows developers to run flows directly from an externally developed website and monitor their status in real-time, offering a user-friendly and practical solution for Corda developers. Overall, our project aims to make Corda development more accessible and tailored to the specific needs of flow management. + diff --git a/java-samples/sendAndRecieveTransaction/FlowManagementUI/app.py b/java-samples/sendAndRecieveTransaction/FlowManagementUI/app.py new file mode 100644 index 0000000..5d3e3f6 --- /dev/null +++ b/java-samples/sendAndRecieveTransaction/FlowManagementUI/app.py @@ -0,0 +1,12 @@ +from flask import Flask +from flask import render_template +app = Flask(__name__) + + +@app.route('/') +def home(): # put application's code here + return render_template("index.html") + +if __name__ == '__main__': + app.run(debug=True, host='0.0.0.0') + diff --git a/java-samples/sendAndRecieveTransaction/FlowManagementUI/requirements.txt b/java-samples/sendAndRecieveTransaction/FlowManagementUI/requirements.txt new file mode 100644 index 0000000..8ab6294 --- /dev/null +++ b/java-samples/sendAndRecieveTransaction/FlowManagementUI/requirements.txt @@ -0,0 +1 @@ +flask \ No newline at end of file diff --git a/java-samples/sendAndRecieveTransaction/FlowManagementUI/static/Scripts/script.js b/java-samples/sendAndRecieveTransaction/FlowManagementUI/static/Scripts/script.js new file mode 100644 index 0000000..8fd4ec0 --- /dev/null +++ b/java-samples/sendAndRecieveTransaction/FlowManagementUI/static/Scripts/script.js @@ -0,0 +1,322 @@ +// This script contains functions for making API requests, handling data, and updating the UI. + +// Variable to store the selected X500Name from the dropdown +let selectedX500Name; + +// Variable to indicate whether data is currently being loaded from the pull request +let loading = false; + +// Function to make a GET request to an external API and return the data +function getData() { + // Replace the URL with the actual API endpoint + return fetch('https://jsonplaceholder.typicode.com/todos/1') + .then(response => response.json()) + .then(data => { + console.log('API Result:', data); + + // Return specific data fields + return { + id: data.id, + title: data.title + }; + }) + .catch(error => { + console.error('Error:', error); + }); +} + +// Function to get the data and display it on the page +function getDataAndDisplay() { + getData() + .then(result => { + // Update the result div with the data + document.getElementById('result').innerHTML = ` +

ID: ${result.id}

+

Title: ${result.title}

+ `; + }) + .catch(error => { + console.error('Error:', error); + }); +} + +// Function to make a GET request to retrieve CPI data +function getCPI() { + const url = 'https://localhost:8888/api/v1/cpi'; + // Perform the GET request with authorization headers + return fetch(url, { + method: 'GET', + headers: { + 'Accept': 'application/json', + 'Authorization': 'Basic YWRtaW46YWRtaW4=' + } + }) + .then(response => response.json()) + .then(data => { + console.log('API Result:', data); + // Further processing of the data can be done here + }) + .catch(error => { + console.error('Error:', error); + }); +} + +// Function to get all virtual nodes and populate a dropdown with the data +function getAllVirtualNodes() { + const url = 'https://localhost:8888/api/v1/virtualnode'; + + return fetch(url, { + method: 'GET', + headers: { + 'Accept': 'application/json', + 'Authorization': 'Basic YWRtaW46YWRtaW4=' + } + }) + .then(response => response.json()) + .then(data => { + // Extract virtualNodes from the API response + const virtualNodes = data.virtualNodes; + console.log('API Result:', virtualNodes); + + // Process the data and populate the dropdown + if (Array.isArray(virtualNodes)) { + const dropdown = document.getElementById('itemDropdown'); + dropdown.innerHTML = ''; + dropdown.innerHTML += ''; + + virtualNodes.forEach(item => { + // Display each item on the console + console.log('Item X500Name:', item.holdingIdentity.x500Name); + console.log('Item ShortHash:', item.holdingIdentity.shortHash); + + // Create an option element and add it to the dropdown + const option = document.createElement('option'); + option.value = item.holdingIdentity.shortHash; + option.text = item.holdingIdentity.x500Name; + dropdown.appendChild(option); + }); + + // Add event listener to the dropdown to detect changes + dropdown.addEventListener('change', function () { + selectedX500Name = this.value; + console.log('Selected ShortHash:', selectedX500Name); + + // Call a function or update a variable based on the selected item + handleDropdownChange(selectedX500Name); + }); + + } else { + console.warn('API Result is not an array.'); + } + }) + .catch(error => { + console.error('Error:', error); + }); +} + +// Initialize the virtual nodes dropdown on page load +getAllVirtualNodes(); + +// function to handle dropdown change +function handleDropdownChange(selectedX500Name) { + console.log('Handling dropdown change for Item ID:', selectedX500Name); + getSelectedVNode(); + // Additional actions based on the selected item can be performed here +} + +// Function to get the selected virtual node and display it +function getSelectedVNode() { + const displayElement = document.getElementById('selectedX500Display'); + displayElement.textContent = `Selected X500: ${selectedX500Name}`; +} + +// Function to get all flow results based on the selected virtual node +function getAllFlowResult() { + const url = `https://localhost:8888/api/v1/flow/${selectedX500Name}`; + + return fetch(url, { + method: 'GET', + headers: { + 'Accept': 'application/json', + 'Authorization': 'Basic YWRtaW46YWRtaW4=' + } + }) + .then(response => response.json()) + .then(data => { + const flowStatusResponses = data.flowStatusResponses; + console.log('API Result:', flowStatusResponses); + + // Convert the JSON object to a string for display + const jsonString = JSON.stringify(flowStatusResponses, null, 2); + + // Display the JSON string in the result div + document.getElementById('idResutl').innerHTML = `
${jsonString}
`; + }) + .catch(error => { + console.error('Error:', error); + }); +} + +// Function to make a POST request with a request body +async function postCallFlow() { + let postBtn = $('#postBtn'); + const url = `https://localhost:8888/api/v1/flow/${selectedX500Name}`; + + // Change the button text to indicate loading + postBtn.html('Loading...'); + + try { + // Perform the POST request with the provided request body + const response = await fetch(url, { + method: 'POST', + headers: { + 'Accept': 'application/json', + 'Authorization': 'Basic YWRtaW46YWRtaW4=', + 'Content-Type': 'application/json' + }, + body: `${document.getElementById('requestBody').value}` + }); + + // Parse the response as JSON + const data = await response.json(); + + if (!data.ok) { + console.log(data.status); + + // Check if the response status is not OK (2xx range) + if (data.status == "409" || data.status == "400") { + let flowStatusResponse = "title: " + data.title + "\nStatus: " + data.status; + + // Display additional details for 400 status + if (data.status == "400") { + flowStatusResponse += "\nDetails: \n Cause: " + data.details.cause + "\n Reason: " + data.details.reason; + } + + document.getElementById('queryResult').innerHTML = `
${flowStatusResponse}
`; + return; + } + } + + let msg = "null"; + let typ = "null"; + + // Construct a string with the flow status responses + let flowStatusResponses = "client request ID: " + data.clientRequestId + + "\nFlow Result " + data.flowResult + + "\nFlow Error Message: " + msg + + "\nflow error type: " + typ + + "\nFlow ID: " + data.flowId + + "\nFlow status: " + data.flowStatus + + "\nHolding identity short hash: " + data.holdingIdentityShortHash + + "\nTime stamp: " + data.timestamp; + + console.log('API Result:', flowStatusResponses); + + // Display the flow status responses in the result div + document.getElementById('queryResult').innerHTML = `
${flowStatusResponses}
`; + } catch (error) { + console.log('Error:', error); + } finally { + // Restore the button text after the operation is complete + postBtn.html('Post'); + } +} + +// Function to display an item on the page +function displayItemOnPage(item) { + const resultDiv = document.getElementById('queryResult'); + + // Create a new element to display the item + const itemElement = document.createElement('div'); + itemElement.innerHTML = ` +

QueryResult: ${item}

+ `; + + // Append the new element to the result div + resultDiv.appendChild(itemElement); +} + +// Function to open a specific tab by hiding/showing content +function openTab(tabName) { + // Hide all tab content + var tabContents = document.getElementsByClassName("tab-content"); + for (var i = 0; i < tabContents.length; i++) { + tabContents[i].style.display = "none"; + document.getElementById(tabContents[i].id + "-tab").style.backgroundColor = "rgb(52,152,219)"; + } + + // Show the selected tab content + var selectedTab = document.getElementById(tabName); + if (selectedTab) { + selectedTab.style.display = "block"; + document.getElementById(tabName + "-tab").style.backgroundColor = "rgb(173,216,230)"; + } +} + +// Function to display a flow for a specific virtual node +function oneFlow() { + const url = `https://localhost:8888/api/v1/flow/${selectedX500Name}/${document.getElementById('clientID').value}`; + + // Validate clientID input + if (document.getElementById('clientID').value == "") { + alert("Please input a clientId"); + return; + } + + // Perform a GET request to display a flow + return fetch(url, { + method: 'GET', + headers: { + 'Accept': 'application/json', + 'Authorization': 'Basic YWRtaW46YWRtaW4=' + } + }) + .then(response => response.json()) + .then(data => { + let msg = "null"; + let typ = "null"; + + // Check if the flow status is "FAILED" and extract error details + if (data.flowStatus == "FAILED") { + msg = data.flowError.message; + typ = data.flowError.type; + } + + // Construct a string with the flow status responses + const flowStatusResponses = "client request ID: " + data.clientRequestId + + "\nFlow Result " + data.flowResult + + "\nFlow Error Message: " + msg + + "\nflow error type: " + typ + + "\nFlow ID: " + data.flowId + + "\nFlow status: " + data.flowStatus + + "\nHolding identity short hash: " + data.holdingIdentityShortHash + + "\nTime stamp: " + data.timestamp; + + console.log('API Result:', flowStatusResponses); + + // Display the flow status responses in the result div + document.getElementById('idResutl').innerHTML = `
${flowStatusResponses}
`; + }) + .catch(error => { + console.error('Error:', error); + }); +} + +// Function to determine which flow-related action to execute based on user input +function executeButtonFlow() { + // Check the value of the dropdown to determine which action to perform + if (document.getElementById("dropdown").value == "option1") { + getAllFlowResult(); + } else { + oneFlow(); + } +} + +function queryDropDownChange(){ + if (document.getElementById("dropdown").value == "option1") { + document.getElementById("clientID").style.display = 'none'; + }else{ + document.getElementById("clientID").style.display = 'block'; + + } +} \ No newline at end of file diff --git a/java-samples/sendAndRecieveTransaction/FlowManagementUI/static/css/main.css b/java-samples/sendAndRecieveTransaction/FlowManagementUI/static/css/main.css new file mode 100644 index 0000000..5e4d849 --- /dev/null +++ b/java-samples/sendAndRecieveTransaction/FlowManagementUI/static/css/main.css @@ -0,0 +1,202 @@ +body { + font-family: 'Roboto', sans-serif; + background-color: #f4f4f4; + color: #333; + margin: 50px; /* Add margin to the entire body */ + padding: 0; +} + +h1 { + text-align: center; + color: #0e0c0c; +} + +/* Style for the label */ +label { + display: block; + margin-bottom: 10px; + font-weight: bold; + color: #333; +} +/* Style for the dropdown button */ +select { + width: 20%; + padding: 8px; + margin-right: 10px; + border: 1px solid #ccc; + border-radius: 4px; + box-sizing: border-box; + font-size: 14px; + color: #555; +} + +#itemDropdown { + width: 40%; + padding: 10px; + box-sizing: border-box; +} + +#clientID{ + padding: 8px; + margin-bottom: 15px; + margin-right: 10px; + border: 1px solid #ccc; + border-radius: 4px; + box-sizing: border-box; + font-size: 14px; + color: #555; +} + +#OneFlowButon{ + padding: 8px; + margin-bottom: 15px; + margin-right: 10px; + border: 1px solid #ccc; + border-radius: 4px; + box-sizing: border-box; + font-size: 14px; +} + +#result { + margin-bottom: 10px; +} + +/* Button styling */ +button { + background-color: #3498db; + color: #fff; + padding: 10px 25px; + font-size: 16px; + border: 2px; + border-radius: 15px; + cursor: pointer; + transition: background-color 0.3s ease; +} + +button:hover { + background-color: #2980b9; +} + +/* Tab styling */ +.tab { + list-style-type: none; /* Remove default list styles */ + display: inline-block; /* Display tabs inline */ + padding: 2px 00px; /* Add padding to the tabs */ + margin: 0 1px; /* Add margin between tabs */ + cursor: pointer; /* Change cursor to pointer on hover */ +} + + +.tab li { + flex: 1; + text-align: center; + padding: 10px; + background-color: #3498db; + color: #fff; + border-radius: 8px 15px 0 0; + cursor: pointer; + transition: background-color 0.3s ease; +} + +.tab li:hover { + background-color: #2980b9; +} + +/* Tab content styling */ +.tab-content { + display: none; + padding: 20px; + border: 1px solid #3498db; + border-radius: 0 0 5px 5px; + background-color: #fff; +} + +.styled-input { + width: 100%; + padding: 10px; + margin-bottom: 10px; + box-sizing: border-box; +} + +.output { + border: 1px solid #3498db; + padding: 10px; + border-radius: 5px; + background-color: #fff; + box-sizing: border-box; +} + +#idResutl { + border: 1px solid #3498db; + padding: 10px; + border-radius: 5px; + background-color: #fff; + margin-top: 10px; + height: 290px; + max-height: 290px; /* Set a maximum height for the scroll box */ + overflow-y: auto; /* Enable vertical scrolling if content exceeds the box height */ +} + +/* Responsive design */ +@media screen and (max-width: 600px) { + .tab li { + border-radius: 5px; + margin-bottom: 5px; + } + .tab-content { + border-radius: 5px; + } +} +#call { + display: -ms-inline-flexbox; + flex-wrap: wrap; + +} + +/* Style for side-by-side input boxes */ +.flowcall-container { + display: flex; +} + +.input-box, .text-box { + width: 150px; /* Set the desired width */ + margin-right: 10px; /* Optional: Add margin for spacing between input boxes */ + border-radius: 5px; + border: 1px solid #3498db; +} + +.queryoption-container{ + display: flex; +} + + + +#requestBody { + flex: 1; + box-sizing: border-box; + width: 100px; /* Set the desired width */ + height: 300px; /* Set the desired height */ + padding: 10px; /* Optional: Add padding for better aesthetics */ + margin-right: 10px; /* Add some margin between the input and button */ +} + +#postBtn { + flex: 0 0 auto; /* Don't allow the button to grow or shrink */ + margin-top: 10px; /* Add some margin between the button and result box */ + margin-right: 10px; /* Add some margin between the button and result box */ + width: 500px; /* Set the desired width */ + height: 50px; /* Set the desired height */ +} + +#queryResult { + flex: 1; + box-sizing: border-box; + width: 100px; /* Set the desired width */ + height: 300px; /* Set the desired height */ + padding: 10px; /* Optional: Add padding for better aesthetics */ +} +.content-box{ + padding: 10px; /* Add padding to the content boxes */ +} + + diff --git a/java-samples/sendAndRecieveTransaction/FlowManagementUI/templates/index.html b/java-samples/sendAndRecieveTransaction/FlowManagementUI/templates/index.html new file mode 100644 index 0000000..44df7ac --- /dev/null +++ b/java-samples/sendAndRecieveTransaction/FlowManagementUI/templates/index.html @@ -0,0 +1,87 @@ + + + + + + + + + + Flask Frontend Example + + + + + + + + + + + + +

Flow Management APIs

+
+ + + + + + +

Please select a flow initiator

+ + + + + +
+ +
+ + + + +
+
+ + +
Result will be displayed here
+
+ +
+
+ + +
+ + + +
+ + + + + + + + +
+ + + + + +
Result will be displayed here
+
+ + diff --git a/java-samples/sendAndRecieveTransaction/README.md b/java-samples/sendAndRecieveTransaction/README.md new file mode 100644 index 0000000..46f983b --- /dev/null +++ b/java-samples/sendAndRecieveTransaction/README.md @@ -0,0 +1,120 @@ +# sendAndReceiveTransaction + +When working with the Corda platform, every transaction is stored in the participants' +vaults. The vault is a where all the transactions involving the owner are securely saved. +Each vault is unique and accessible only by its owner, +serving as a ledger to track all the owner's transactions. +However, there are scenarios where you may want a third party to receive a copy of the +transaction. This is where the SendAndRecieveTransaction function becomes essential. +For instance, if Alice conducts a transaction with Bob and wants Charlie to receive a copy, +Alice can simply run a flow using the transaction ID to send a copy to Charlie's vault. +Another application of this function can be an automated reporting tool, +which can be utilized at the end of each transaction finalizing flow to automatically +report to a specific vnode. This functionality can act like a bookkeeper, +meticulously tracking each transaction and ensuring accurate record-keeping. +` + +### 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/v5_2/swagger#/. You can test out some + 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 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 IOUState between two parties +Pick a VNode identity to initiate the IOU creation, and get its short hash. (Let's pick Alice. Don't pick Bob because Bob is the person who alice will borrow from). + +Go to `POST /flow/{holdingidentityshorthash}`, enter the identity short hash(Alice's hash) and request body: +``` +{ + "clientRequestId": "createiou-1", + "flowClassName": "com.r3.developers.samples.obligation.workflows.IOUIssueFlow", + "requestBody": { + "amount":"20", + "lender":"CN=Bob, OU=Test Dept, O=R3, L=London, C=GB" + } +} +``` + +After trigger the create-IOU flow, hop to `GET /flow/{holdingidentityshorthash}/{clientrequestid}` and enter the short hash(Alice's hash) and client request id to view the flow result +The stateRef of the transaction will be returned as a result of the flow query. Which is the "flowResult". + +#### Step 2: Sending a copy of the transaction to a third party. +If a member needs to share a copy of their transaction with another member, +they can do so using the process outlined below. For instance, +if Alice wishes to send a copy of the transaction to Dave, +we can execute the following request body with her short hash: + +``` +{ + "clientRequestId": "sendAndRecieve-1", + "flowClassName": "com.r3.developers.samples.obligation.workflows.sendAndRecieveTransactionFlow", + "requestBody": { + "stateRef": "STATEREF ID HERE", + "members": ["CN=Dave, OU=Test Dept, O=R3, L=London, C=GB"], + "forceBackchain": "false" + } +} +``` + +Ensure to replace the stateRef variable with the stateRef of the transaction. The stateRef will be found when you run the GET call, +QueryAll in the swaggerAPI. The stateRef is labeled as `flowResult` in response body, begins with SHA-256D:XXXXX.. +``` +[ + { + "holdingIdentityShortHash": "A93A019B324E", + "clientRequestId": "createiou-1", + "flowId": "26ea3f95-141b-4aaa-9b58-e3a685dc54d3", + "flowStatus": "COMPLETED", + "flowResult": "SHA-256D:B3D87C8B446C277B5658BBB2A18DC7491539D898B70F074418878091AE315B4A", + "flowError": null, + "timestamp": "2024-07-08T04:37:16.175Z" + } +] +``` +After running this flow Dave will have the transaction in his vault. + + +Results + +Currently, Alice has two transactions stored in her vault. +The transaction we aim to send to Charlie is identified by the ID SHA-256D:8DFDD672…. +You can view Alice's transactions in the image provided below: + +

+ Encumbrance Flow +

+ +On the other hand, Charlie’s vault currently holds no transactions, +as illustrated in the image below: + +

+ Encumbrance Flow +

+ +Once Alice executes the flow, Charlie’s vault is updated to include the transaction, +which is displayed as follows: + +

+ Encumbrance Flow +

+ +All images of the vault were sourced through DBeaver by establishing a connection +to the Cordapp using PostgreSQL. The credentials utilized for this connection are as follows: + +POSTGRES_DB = cordacluster
+POSTGRES_USER = postgres
+POSTGRES_PASSWORD = password + +To access the vault, navigate through the hierarchy in +PostgreSQL: Databases > cordacluster > Schemas > vnode_vault_(HASH_ID_OF_VNODE) > +Tables > utxo_transaction. To view the transactions, +simply double-click on utxo_transaction. \ No newline at end of file diff --git a/java-samples/sendAndRecieveTransaction/aliceVault.png b/java-samples/sendAndRecieveTransaction/aliceVault.png new file mode 100644 index 0000000..d377488 Binary files /dev/null and b/java-samples/sendAndRecieveTransaction/aliceVault.png differ diff --git a/java-samples/sendAndRecieveTransaction/build.gradle b/java-samples/sendAndRecieveTransaction/build.gradle new file mode 100644 index 0000000..eb0a9bd --- /dev/null +++ b/java-samples/sendAndRecieveTransaction/build.gradle @@ -0,0 +1,73 @@ +import static org.gradle.api.JavaVersion.VERSION_17 +import static org.gradle.jvm.toolchain.JavaLanguageVersion.of + +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.gradle.plugin' +} + +allprojects { + group 'com.r3.developers.cordapptemplate' + version '1.0-SNAPSHOT' + + // Configure Corda runtime gradle plugin + cordaRuntimeGradlePlugin { + notaryVersion = cordaNotaryPluginsVersion + notaryCpiName = "NotaryServer" + corDappCpiName = "MyCorDapp" + cpiUploadTimeout = "30000" + vnodeRegistrationTimeout = "60000" + cordaProcessorTimeout = "300000" + workflowsModuleName = "workflows" + cordaClusterURL = "https://localhost:8888" + cordaRestUser = "admin" + cordaRestPasswd ="admin" + composeFilePath = "config/combined-worker-compose.yaml" + networkConfigFile = "config/static-network-config.json" + r3RootCertFile = "config/r3-ca-key.pem" + skipTestsDuringBuildCpis = "false" + cordaRuntimePluginWorkspaceDir = "workspace" + cordaBinDir = "${System.getProperty("user.home")}/.corda/corda5" + cordaCliBinDir = "${System.getProperty("user.home")}/.corda/cli" + } + + java { + toolchain { + languageVersion = of(VERSION_17.majorVersion.toInteger()) + } + withSourcesJar() + } + + // 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-dev-template-java-sample" + groupId project.group + artifact jar + } + } +} diff --git a/java-samples/sendAndRecieveTransaction/charlieAfter.png b/java-samples/sendAndRecieveTransaction/charlieAfter.png new file mode 100644 index 0000000..51d47a7 Binary files /dev/null and b/java-samples/sendAndRecieveTransaction/charlieAfter.png differ diff --git a/java-samples/sendAndRecieveTransaction/charlieBefore.png b/java-samples/sendAndRecieveTransaction/charlieBefore.png new file mode 100644 index 0000000..e9cb661 Binary files /dev/null and b/java-samples/sendAndRecieveTransaction/charlieBefore.png differ diff --git a/java-samples/sendAndRecieveTransaction/config/combined-worker-compose.yaml b/java-samples/sendAndRecieveTransaction/config/combined-worker-compose.yaml new file mode 100644 index 0000000..da2495d --- /dev/null +++ b/java-samples/sendAndRecieveTransaction/config/combined-worker-compose.yaml @@ -0,0 +1,87 @@ +version: '2' +services: + postgresql: + image: postgres:14.10 + restart: unless-stopped + tty: true + environment: + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=password + - POSTGRES_DB=cordacluster + ports: + - 5432:5432 + + kafka: + image: confluentinc/cp-kafka:7.6.0 + ports: + - 9092:9092 + environment: + KAFKA_NODE_ID: 1 + CLUSTER_ID: ZDFiZmU3ODUyMzRiNGI3NG + KAFKA_PROCESS_ROLES: broker,controller + KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka:9093 + KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:9092,DOCKER_INTERNAL://0.0.0.0:29092,CONTROLLER://0.0.0.0:9093 + KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092,DOCKER_INTERNAL://kafka:29092 + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,DOCKER_INTERNAL:PLAINTEXT,CONTROLLER:PLAINTEXT + KAFKA_INTER_BROKER_LISTENER_NAME: DOCKER_INTERNAL + KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER + KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 + KAFKA_DEFAULT_REPLICATION_FACTOR: 1 + KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 + KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1 + KAFKA_AUTO_CREATE_TOPICS_ENABLE: "true" + + kafka-create-topics: + image: openjdk:17-jdk + depends_on: + - kafka + volumes: + - ${CORDA_CLI:-~/.corda/cli}:/opt/corda-cli + working_dir: /opt/corda-cli + command: [ + "java", + "-jar", + "corda-cli.jar", + "topic", + "-b=kafka:29092", + "create", + "connect" + ] + + corda: + image: corda/corda-os-combined-worker-kafka:5.2.0.0 + depends_on: + - postgresql + - kafka + - kafka-create-topics + volumes: + - ../config:/config + - ../logs:/logs + environment: + JAVA_TOOL_OPTIONS: -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 + LOG4J_CONFIG_FILE: config/log4j2.xml + CONSOLE_LOG_LEVEL: info + ENABLE_LOG4J2_DEBUG: false + command: [ + "-mbus.busType=KAFKA", + "-mbootstrap.servers=kafka:29092", + "-spassphrase=password", + "-ssalt=salt", + "-ddatabase.user=user", + "-ddatabase.pass=password", + "-ddatabase.jdbc.url=jdbc:postgresql://postgresql:5432/cordacluster", + "-ddatabase.jdbc.directory=/opt/jdbc-driver/" + ] + ports: + - 8888:8888 + - 7004:7004 + - 5005:5005 + + flow-management-tool: + depends_on: + - corda + build: + context: ../FlowManagementUI + dockerfile: Dockerfile + ports: + - 5000:5000 \ No newline at end of file diff --git a/java-samples/sendAndRecieveTransaction/config/gradle-plugin-default-key.pem b/java-samples/sendAndRecieveTransaction/config/gradle-plugin-default-key.pem new file mode 100644 index 0000000..5294bbd --- /dev/null +++ b/java-samples/sendAndRecieveTransaction/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/java-samples/sendAndRecieveTransaction/config/log4j2.xml b/java-samples/sendAndRecieveTransaction/config/log4j2.xml new file mode 100644 index 0000000..909222c --- /dev/null +++ b/java-samples/sendAndRecieveTransaction/config/log4j2.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/java-samples/sendAndRecieveTransaction/config/r3-ca-key.pem b/java-samples/sendAndRecieveTransaction/config/r3-ca-key.pem new file mode 100644 index 0000000..a803613 --- /dev/null +++ b/java-samples/sendAndRecieveTransaction/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/java-samples/sendAndRecieveTransaction/config/static-network-config.json b/java-samples/sendAndRecieveTransaction/config/static-network-config.json new file mode 100644 index 0000000..b0f2519 --- /dev/null +++ b/java-samples/sendAndRecieveTransaction/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/java-samples/sendAndRecieveTransaction/contracts/build.gradle b/java-samples/sendAndRecieveTransaction/contracts/build.gradle new file mode 100644 index 0000000..513a070 --- /dev/null +++ b/java-samples/sendAndRecieveTransaction/contracts/build.gradle @@ -0,0 +1,86 @@ +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:$slf4jVersion" + 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" +// testImplementation "com.r3.corda.ledger.utxo:contract-testing:$contractTestingVersion" +} + +// 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 contractsModule + versionId 1 + licence cordappLicense + vendor cordappVendorName + } +} + +// Use the name of the contract module as the name of the generated CPK and CPB. +archivesBaseName = cordapp.contract.name.isPresent() ? cordapp.contract.name.get() : contractsModule + +publishing { + publications { + maven(MavenPublication) { + from components.cordapp + } + } +} \ No newline at end of file diff --git a/java-samples/sendAndRecieveTransaction/contracts/src/main/java/com/r3/developers/samples/obligation/contracts/IOUContract.java b/java-samples/sendAndRecieveTransaction/contracts/src/main/java/com/r3/developers/samples/obligation/contracts/IOUContract.java new file mode 100644 index 0000000..c162215 --- /dev/null +++ b/java-samples/sendAndRecieveTransaction/contracts/src/main/java/com/r3/developers/samples/obligation/contracts/IOUContract.java @@ -0,0 +1,50 @@ +package com.r3.developers.samples.obligation.contracts; + +import com.r3.developers.samples.obligation.states.IOUState; +import net.corda.v5.base.exceptions.CordaRuntimeException; +import net.corda.v5.ledger.utxo.Command; +import net.corda.v5.ledger.utxo.Contract; +import net.corda.v5.ledger.utxo.ContractState; +import net.corda.v5.ledger.utxo.transaction.UtxoLedgerTransaction; + +import java.security.PublicKey; +import java.util.Set; + + +public class IOUContract implements Contract { + + //IOU Commands + public static class Issue implements Command { } + public static class Settle implements Command { } + public static class Transfer implements Command { } + + @Override + public void verify(UtxoLedgerTransaction transaction) { + + // Ensures that there is only one command in the transaction + requireThat( transaction.getCommands().size() == 1, "Require a single command."); + Command command = transaction.getCommands().get(0); + IOUState output = transaction.getOutputStates(IOUState.class).get(0); + requireThat(output.getParticipants().size() == 2, "The output state should have two and only two participants."); + + // Switches case based on the command + if(command.getClass() == IOUContract.Issue.class) {// Rules applied only to transactions with the Issue Command. + requireThat(transaction.getOutputContractStates().size() == 1, "Only one output states should be created when issuing an IOU."); + }else if(command.getClass() == IOUContract.Transfer.class) {// Rules applied only to transactions with the Transfer Command. + requireThat( transaction.getInputContractStates().size() > 0, "There must be one input IOU."); + } + else if(command.getClass() == IOUContract.Settle.class) {// Rules applied only to transactions with the Settle Command. + requireThat( transaction.getInputContractStates().size() > 0, "There must be one input IOU."); + } + else { + throw new CordaRuntimeException("Unsupported command"); + } + } + + // Helper function to allow writing constraints in the Corda 4 '"text" using (boolean)' style + private void requireThat(boolean asserted, String errorMessage) { + if(!asserted) { + throw new CordaRuntimeException("Failed requirement: " + errorMessage); + } + } +} diff --git a/java-samples/sendAndRecieveTransaction/contracts/src/main/java/com/r3/developers/samples/obligation/states/IOUState.java b/java-samples/sendAndRecieveTransaction/contracts/src/main/java/com/r3/developers/samples/obligation/states/IOUState.java new file mode 100644 index 0000000..120558a --- /dev/null +++ b/java-samples/sendAndRecieveTransaction/contracts/src/main/java/com/r3/developers/samples/obligation/states/IOUState.java @@ -0,0 +1,81 @@ +package com.r3.developers.samples.obligation.states; + +import com.r3.developers.samples.obligation.contracts.IOUContract; +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.List; +import java.util.UUID; + +//Link with the Contract class +@BelongsToContract(IOUContract.class) +public class IOUState implements ContractState { + + //private variables + public final int amount; + public final MemberX500Name lender; + public final MemberX500Name borrower; + public final int paid; + private final UUID linearId; + public List participants; + + + @ConstructorForDeserialization + public IOUState(int amount, MemberX500Name lender, MemberX500Name borrower, int paid, UUID linearId, List participants) { + this.amount = amount; + this.lender = lender; + this.borrower = borrower; + this.paid = paid; + this.linearId = linearId; + this.participants = participants; + } + + public IOUState(int amount, MemberX500Name lender, MemberX500Name borrower, List participants) { + this.amount = amount; + this.lender = lender; + this.borrower = borrower; + this.paid = 0; + this.linearId = UUID.randomUUID(); + this.participants = participants; + } + + public int getAmount() { + return amount; + } + + public MemberX500Name getLender() { + return lender; + } + + public MemberX500Name getBorrower() { + return borrower; + } + + public int getPaid() { + return paid; + } + + public UUID getLinearId() { + return linearId; + } + + @Override + public List getParticipants() { + return participants; + } + + //Helper method for settle flow + public IOUState pay(int amountToPay) { + int newAmountPaid = this.paid + (amountToPay); + return new IOUState(amount, lender, borrower, newAmountPaid,this.linearId,this.participants); + } + + //Helper method for transfer flow + public IOUState withNewLender(MemberX500Name newLender, List newParticipants) { + return new IOUState(amount, newLender, borrower, paid,linearId,newParticipants); + } + +} diff --git a/java-samples/sendAndRecieveTransaction/gradle.properties b/java-samples/sendAndRecieveTransaction/gradle.properties new file mode 100644 index 0000000..2549e5f --- /dev/null +++ b/java-samples/sendAndRecieveTransaction/gradle.properties @@ -0,0 +1,73 @@ +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.2.0.52 + +# 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.2.0.0 + +# Specify the version of the cordapp-cpb and cordapp-cpk plugins +cordaPluginsVersion=7.0.4 + +# Specify the version of the Corda runtime Gradle plugin to use +cordaGradlePluginVersion=5.2.0.0 + +# Specify the name of the workflows module +# This will be the name of the generated cpk and cpb files +workflowsModule=workflows + +# Specify the name of the contracts module +# This will be the name of the generated cpk and cpb files +contractsModule=contracts + +# Specify the location of where Corda 5 binaries can be downloaded +# Relative path from user.home +cordaBinariesDirectory = .corda/corda5 + +# Specify the location of where Corda 5 CLI binaries can be downloaded +# Relative path from user.home +cordaCliBinariesDirectory = .corda/cli + +# Metadata for the CorDapp. +cordappLicense="Apache License, Version 2.0" +cordappVendorName="R3" + +# 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.10.0 +mockitoKotlinVersion=4.0.0 +mockitoVersion=4.6.1 +hamcrestVersion=2.2 +assertjVersion = 3.24.1 +contractTestingVersion=1.0.0-beta-+ +jacksonVersion=2.15.2 +slf4jVersion=1.7.36 + +# Specify the maximum amount of time allowed for the CPI upload +# As your CorDapp grows you might need to increase this +# Value is in milliseconds +cpiUploadDefault=10000 + +# Specify the length of time, in milliseconds, that Corda waits for an individual event to process. +# Keep at -1 to use the default. Refer to the Corda Api Docs for the exact value. +processorTimeout=-1 + +# Specify the maximum amount of time allowed to check all vNodes are registered +# Value is in milliseconds +vnodeRegistrationTimeoutDefault=30000 + +# Specify if you want to run the contracts and workflows tests as part of the corda-runtime-plugin-cordapp > buildCpis task +# False by default, will execute the tests every time you stand the template up - gives extra protection +# Set to true to skip the tests, making the launching process quicker. You will be responsible for running workflow tests yourself +skipTestsDuringBuildCpis=false \ No newline at end of file diff --git a/java-samples/sendAndRecieveTransaction/gradle/wrapper/gradle-wrapper.jar b/java-samples/sendAndRecieveTransaction/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..943f0cb Binary files /dev/null and b/java-samples/sendAndRecieveTransaction/gradle/wrapper/gradle-wrapper.jar differ diff --git a/java-samples/sendAndRecieveTransaction/gradle/wrapper/gradle-wrapper.properties b/java-samples/sendAndRecieveTransaction/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..5083229 --- /dev/null +++ b/java-samples/sendAndRecieveTransaction/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/java-samples/sendAndRecieveTransaction/gradlew b/java-samples/sendAndRecieveTransaction/gradlew new file mode 100644 index 0000000..65dcd68 --- /dev/null +++ b/java-samples/sendAndRecieveTransaction/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/java-samples/sendAndRecieveTransaction/gradlew.bat b/java-samples/sendAndRecieveTransaction/gradlew.bat new file mode 100644 index 0000000..93e3f59 --- /dev/null +++ b/java-samples/sendAndRecieveTransaction/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/java-samples/sendAndRecieveTransaction/settings.gradle b/java-samples/sendAndRecieveTransaction/settings.gradle new file mode 100644 index 0000000..bdec735 --- /dev/null +++ b/java-samples/sendAndRecieveTransaction/settings.gradle @@ -0,0 +1,25 @@ +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 'org.jetbrains.kotlin.jvm' version kotlinVersion + id 'org.jetbrains.kotlin.plugin.jpa' version kotlinVersion + id 'org.jetbrains.kotlin.plugin.allopen' version kotlinVersion + id 'net.corda.gradle.plugin' version cordaGradlePluginVersion + } +} + +// Root project name, used in naming the project as a whole and used in naming objects built by the project. +rootProject.name = 'sendAndRecieveTransaction' +include ':workflows' +include ':contracts' \ No newline at end of file diff --git a/java-samples/sendAndRecieveTransaction/workflows/build.gradle b/java-samples/sendAndRecieveTransaction/workflows/build.gradle new file mode 100644 index 0000000..748dd3d --- /dev/null +++ b/java-samples/sendAndRecieveTransaction/workflows/build.gradle @@ -0,0 +1,96 @@ +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 { + constraints { + testImplementation('org.slf4j:slf4j-api') { + version { + // Corda cannot use SLF4J 2.x yet. + strictly '1.7.36' + } + } + } + // 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:$slf4jVersion" + 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" + testImplementation "org.assertj:assertj-core:$assertjVersion" +} + +// 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 workflowsModule + versionId 1 + licence cordappLicense + vendor cordappVendorName + } +} + +// Use the name of the workflow module as the name of the generated CPK and CPB. +archivesBaseName = cordapp.workflow.name.isPresent() ? cordapp.workflow.name.get() : workflowsModule + +publishing { + publications { + maven(MavenPublication) { + from components.cordapp + } + } +} \ No newline at end of file diff --git a/java-samples/sendAndRecieveTransaction/workflows/src/main/java/com/r3/developers/samples/obligation/workflows/FinalizeIOUFlow.java b/java-samples/sendAndRecieveTransaction/workflows/src/main/java/com/r3/developers/samples/obligation/workflows/FinalizeIOUFlow.java new file mode 100644 index 0000000..10c14be --- /dev/null +++ b/java-samples/sendAndRecieveTransaction/workflows/src/main/java/com/r3/developers/samples/obligation/workflows/FinalizeIOUFlow.java @@ -0,0 +1,125 @@ +package com.r3.developers.samples.obligation.workflows; + +import com.r3.developers.samples.obligation.states.IOUState; +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.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.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +public class FinalizeIOUFlow { + private final static Logger log = LoggerFactory.getLogger(FinalizeIOUFlow.class); + + // @InitiatingFlow declares the protocol which will be used to link the initiator to the responder. + @InitiatingFlow(protocol = "finalize-iou-protocol") + public static class FinalizeIOU implements SubFlow { + + private final UtxoSignedTransaction signedTransaction; + private final List otherMembers; + + public FinalizeIOU(UtxoSignedTransaction signedTransaction, List otherMembers) { + this.signedTransaction = signedTransaction; + this.otherMembers = otherMembers; + } + + // 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("FinalizeIOU.call() called"); + + // Initiates a session with the other Member. + //FlowSession session = flowMessaging.initiateFlow(otherMember); + + List sessionsList = new ArrayList<>(); + + for (MemberX500Name member: otherMembers) { + sessionsList.add(flowMessaging.initiateFlow(member)); + } + + // 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. + String result; + try { + + 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; + } + } + + @InitiatedBy(protocol = "finalize-iou-protocol") + public static class FinalizeIOUResponderFlow implements ResponderFlow { + + // 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("FinalizeIOUResponderFlow.call() called"); + + try { + // Defines the lambda validator used in receiveFinality below. + UtxoTransactionValidator txValidator = ledgerTransaction -> { + + // Note, this exception will only be shown in the logs if Corda Logging is set to debug. + if(!(ledgerTransaction.getOutputContractStates().get(0).getClass().equals(IOUState.class))) + throw new CordaRuntimeException("Failed verification - transaction did not have exactly one output IOUState."); + + 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()); + log.info("HEREEE PPP- " + finalizedSignedTransaction.getOutputStateAndRefs().stream().map(it -> it.getRef().toString()).toString()); + log.warn(finalizedSignedTransaction.getOutputStateAndRefs().stream() + .map(stateAndRef -> stateAndRef.getRef().toString()) + .toList() + .toString()); + + } + // Soft fails the flow and log the exception. + catch(Exception e) + { + log.warn("Exceptionally finished responder flow", e); + } + } + } +} diff --git a/java-samples/sendAndRecieveTransaction/workflows/src/main/java/com/r3/developers/samples/obligation/workflows/IOUIssueFlow.java b/java-samples/sendAndRecieveTransaction/workflows/src/main/java/com/r3/developers/samples/obligation/workflows/IOUIssueFlow.java new file mode 100644 index 0000000..fad623e --- /dev/null +++ b/java-samples/sendAndRecieveTransaction/workflows/src/main/java/com/r3/developers/samples/obligation/workflows/IOUIssueFlow.java @@ -0,0 +1,139 @@ +package com.r3.developers.samples.obligation.workflows; + +import com.r3.developers.samples.obligation.contracts.IOUContract; +import com.r3.developers.samples.obligation.states.IOUState; +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.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 static java.util.Objects.requireNonNull; + +public class IOUIssueFlow implements ClientStartableFlow { + private final static Logger log = LoggerFactory.getLogger(IOUIssueFlow.class); + + // Injects the JsonMarshallingService to read and populate JSON parameters. + @CordaInject + public JsonMarshallingService jsonMarshallingService; + + // Injects the MemberLookup to look up the VNode identities. + @CordaInject + public MemberLookup memberLookup; + + // Injects the UtxoLedgerService to enable the flow to make use of the Ledger API. + @CordaInject + public UtxoLedgerService ledgerService; + + // Injects the NotaryLookup to look up the notary identity. + @CordaInject + public NotaryLookup notaryLookup; + + // FlowEngine service is required to run SubFlows. + @CordaInject + public FlowEngine flowEngine; + + @Override + @Suspendable + public String call(ClientRequestBody requestBody) { + log.info("IOUIssueFlow.call() called"); + + try { + // Obtain the deserialized input arguments to the flow from the requestBody. + IOUIssueFlowArgs flowArgs = requestBody.getRequestBodyAs(jsonMarshallingService, IOUIssueFlowArgs.class); + + // Get MemberInfos for the Vnode running the flow and the otherMember. + MemberInfo myInfo = memberLookup.myInfo(); + MemberInfo lenderInfo = requireNonNull( + memberLookup.lookup(MemberX500Name.parse(flowArgs.getLender())), + "MemberLookup can't find otherMember specified in flow arguments." + ); + log.info("PASS 0"); + // Create the IOUState from the input arguments and member information. + IOUState iou = new IOUState( + Integer.parseInt(flowArgs.getAmount()), + lenderInfo.getName(), + myInfo.getName(), + Arrays.asList(myInfo.getLedgerKeys().get(0), lenderInfo.getLedgerKeys().get(0)) + ); + log.info("PASS 1"); + // Obtain the Notary name and public key. + NotaryInfo notary = requireNonNull( + notaryLookup.lookup(MemberX500Name.parse("CN=NotaryService, OU=Test Dept, O=R3, L=London, C=GB")), + "NotaryLookup can't find notary specified in flow arguments." + ); + + log.info("PASS 2"); + PublicKey notaryKey = notary.getPublicKey();; + for(MemberInfo memberInfo: memberLookup.lookup()){ + if(!memberInfo.getLedgerKeys().isEmpty()) { + if (Objects.equals( + memberInfo.getMemberProvidedContext().get("corda.notary.service.name"), + notary.getName().toString())) { + notaryKey = memberInfo.getLedgerKeys().get(0); + break; + } + } + } + log.info("PASS 3"); + // Note, in Java CorDapps only unchecked RuntimeExceptions can be thrown not + // declared checked exceptions as this changes the method signature and breaks override. + if(notaryKey == null) { + throw new CordaRuntimeException("No notary PublicKey found"); + + } + log.info("PASS 4"); + // 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(iou) + .addCommand(new IOUContract.Issue()) + .addSignatories(iou.getParticipants()); + log.info("PASS 5"); + // Convert the transaction builder to a UTXOSignedTransaction and sign with this Vnode's first Ledger key. + // Note, toSignedTransaction() is currently a placeholder method, hence being marked as deprecated. + @SuppressWarnings("DEPRECATION") + UtxoSignedTransaction signedTransaction = txBuilder.toSignedTransaction(); + + // Call FinalizeIOUSubFlow 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 FinalizeIOUFlow.FinalizeIOU(signedTransaction, Arrays.asList(lenderInfo.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 http-rpc: +{ + "clientRequestId": "createiou-1", + "flowClassName": "com.r3.developers.samples.obligation.workflows.IOUIssueFlow", + "requestBody": { + "amount":"20", + "lender":"CN=Bob, OU=Test Dept, O=R3, L=London, C=GB" + } +} + */ diff --git a/java-samples/sendAndRecieveTransaction/workflows/src/main/java/com/r3/developers/samples/obligation/workflows/IOUIssueFlowArgs.java b/java-samples/sendAndRecieveTransaction/workflows/src/main/java/com/r3/developers/samples/obligation/workflows/IOUIssueFlowArgs.java new file mode 100644 index 0000000..4b49bdb --- /dev/null +++ b/java-samples/sendAndRecieveTransaction/workflows/src/main/java/com/r3/developers/samples/obligation/workflows/IOUIssueFlowArgs.java @@ -0,0 +1,23 @@ +package com.r3.developers.samples.obligation.workflows; + +// A class to hold the deserialized arguments required to start the flow. +public class IOUIssueFlowArgs { + private String amount; + private String lender; + + public IOUIssueFlowArgs() { + } + + public IOUIssueFlowArgs(String amount, String lender) { + this.amount = amount; + this.lender = lender; + } + + public String getAmount() { + return amount; + } + + public String getLender() { + return lender; + } +} diff --git a/java-samples/sendAndRecieveTransaction/workflows/src/main/java/com/r3/developers/samples/obligation/workflows/IOUSettleFlow.java b/java-samples/sendAndRecieveTransaction/workflows/src/main/java/com/r3/developers/samples/obligation/workflows/IOUSettleFlow.java new file mode 100644 index 0000000..0e18e85 --- /dev/null +++ b/java-samples/sendAndRecieveTransaction/workflows/src/main/java/com/r3/developers/samples/obligation/workflows/IOUSettleFlow.java @@ -0,0 +1,127 @@ +package com.r3.developers.samples.obligation.workflows; + + +import com.r3.developers.samples.obligation.contracts.IOUContract; +import com.r3.developers.samples.obligation.states.IOUState; +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.base.types.MemberX500Name; +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.Arrays; +import java.util.List; +import java.util.UUID; + +import static java.util.Objects.requireNonNull; +import static java.util.stream.Collectors.toList; + +public class IOUSettleFlow implements ClientStartableFlow { + + private final static Logger log = LoggerFactory.getLogger(IOUSettleFlow.class); + + // Injects the JsonMarshallingService to read and populate JSON parameters. + @CordaInject + public JsonMarshallingService jsonMarshallingService; + + // Injects the MemberLookup to look up the VNode identities. + @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; + + @Override + @Suspendable + public String call(ClientRequestBody requestBody) { + log.info("IOUSettleFlow.call() called"); + + try { + // Obtain the deserialized input arguments to the flow from the requestBody. + IOUSettleFlowArgs flowArgs = requestBody.getRequestBodyAs(jsonMarshallingService, IOUSettleFlowArgs.class); + + // Get flow args from the input JSON + UUID iouID = flowArgs.getIouID(); + int amountSettle = Integer.parseInt(flowArgs.getAmountSettle()); + + //query the IOU input + List> iouStateAndRefs = ledgerService.findUnconsumedStatesByExactType(IOUState.class,100, Instant.now()).getResults(); + List> iouStateAndRefsWithId = iouStateAndRefs.stream() + .filter(sar -> sar.getState().getContractState().getLinearId().equals(iouID)).collect(toList()); + + if (iouStateAndRefsWithId.size() != 1) throw new CordaRuntimeException("Multiple or zero IOU states with id " + iouID + " found"); + StateAndRef iouStateAndRef = iouStateAndRefsWithId.get(0); + IOUState iouInput = iouStateAndRef.getState().getContractState(); + + //flow logic checks + MemberInfo myInfo = memberLookup.myInfo(); + if (!(myInfo.getName().equals(iouInput.getBorrower()))) throw new CordaRuntimeException("Only IOU borrower can settle the IOU."); + MemberInfo lenderInfo = requireNonNull( + memberLookup.lookup(iouInput.getLender()), + "MemberLookup can't find otherMember specified in flow arguments." + ); + + // Create the IOUState from the input arguments and member information. + IOUState iouOutput = iouInput.pay(amountSettle); + + //get notary from input + MemberX500Name notary = iouStateAndRef.getState().getNotaryName(); + + // Use UTXOTransactionBuilder to build up the draft transaction. + UtxoTransactionBuilder txBuilder = ledgerService.createTransactionBuilder() + .setNotary(notary) + .setTimeWindowBetween(Instant.now(), Instant.now().plusMillis(Duration.ofDays(1).toMillis())) + .addInputState(iouStateAndRef.getRef()) + .addOutputState(iouOutput) + .addCommand(new IOUContract.Settle()) + .addSignatories(iouOutput.getParticipants()); + + // Convert the transaction builder to a UTXOSignedTransaction and sign with this Vnode's first Ledger key. + // Note, toSignedTransaction() is currently a placeholder method, hence being marked as deprecated. + UtxoSignedTransaction signedTransaction = txBuilder.toSignedTransaction(); + + // Call FinalizeIOUSubFlow 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 FinalizeIOUFlow.FinalizeIOU(signedTransaction, Arrays.asList(lenderInfo.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 http-rpc: +{ + "clientRequestId": "settleiou-1", + "flowClassName": "com.r3.developers.samples.obligation.workflows.IOUSettleFlow", + "requestBody": { + "amountSettle":"10", + "iouID":" ** fill in id **" + } +} +1ac69d82-804b-487b-9178-ea527d0e4b80 +*/ + + diff --git a/java-samples/sendAndRecieveTransaction/workflows/src/main/java/com/r3/developers/samples/obligation/workflows/IOUSettleFlowArgs.java b/java-samples/sendAndRecieveTransaction/workflows/src/main/java/com/r3/developers/samples/obligation/workflows/IOUSettleFlowArgs.java new file mode 100644 index 0000000..d2b605e --- /dev/null +++ b/java-samples/sendAndRecieveTransaction/workflows/src/main/java/com/r3/developers/samples/obligation/workflows/IOUSettleFlowArgs.java @@ -0,0 +1,25 @@ +package com.r3.developers.samples.obligation.workflows; + +import java.util.UUID; + +// A class to hold the deserialized arguments required to start the flow. +public class IOUSettleFlowArgs { + private String amountSettle; + private UUID iouID; + + public IOUSettleFlowArgs() { + } + + public IOUSettleFlowArgs(String amountSettle, UUID iouID) { + this.amountSettle = amountSettle; + this.iouID = iouID; + } + + public String getAmountSettle() { + return amountSettle; + } + + public UUID getIouID() { + return iouID; + } +} \ No newline at end of file diff --git a/java-samples/sendAndRecieveTransaction/workflows/src/main/java/com/r3/developers/samples/obligation/workflows/IOUTransferFlow.java b/java-samples/sendAndRecieveTransaction/workflows/src/main/java/com/r3/developers/samples/obligation/workflows/IOUTransferFlow.java new file mode 100644 index 0000000..939bba9 --- /dev/null +++ b/java-samples/sendAndRecieveTransaction/workflows/src/main/java/com/r3/developers/samples/obligation/workflows/IOUTransferFlow.java @@ -0,0 +1,127 @@ +package com.r3.developers.samples.obligation.workflows; + + +import com.r3.developers.samples.obligation.contracts.IOUContract; +import com.r3.developers.samples.obligation.states.IOUState; +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.base.types.MemberX500Name; +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.Arrays; +import java.util.List; +import java.util.UUID; + +import static java.util.Objects.requireNonNull; +import static java.util.stream.Collectors.toList; + +public class IOUTransferFlow implements ClientStartableFlow { + + private final static Logger log = LoggerFactory.getLogger(IOUTransferFlow.class); + + // Injects the JsonMarshallingService to read and populate JSON parameters. + @CordaInject + public JsonMarshallingService jsonMarshallingService; + + // Injects the MemberLookup to look up the VNode identities. + @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; + + @Override + @Suspendable + public String call(ClientRequestBody requestBody) { + log.info("IOUTransferFlow.call() called"); + + try { + // Obtain the deserialized input arguments to the flow from the requestBody. + IOUTransferFlowArgs flowArgs = requestBody.getRequestBodyAs(jsonMarshallingService, IOUTransferFlowArgs.class); + + // Get flow args from the input JSON + UUID iouID = flowArgs.getIouID(); + + //query the IOU input + List> iouStateAndRefs = ledgerService.findUnconsumedStatesByExactType(IOUState.class,100, Instant.now()).getResults(); + List> iouStateAndRefsWithId = iouStateAndRefs.stream() + .filter(sar -> sar.getState().getContractState().getLinearId().equals(iouID)).collect(toList()); + if (iouStateAndRefsWithId.size() != 1) throw new CordaRuntimeException("Multiple or zero IOU states with id " + iouID + " found"); + StateAndRef iouStateAndRef = iouStateAndRefsWithId.get(0); + IOUState iouInput = iouStateAndRef.getState().getContractState(); + + //flow logic checks + MemberInfo myInfo = memberLookup.myInfo(); + if (!(myInfo.getName().equals(iouInput.getLender()))) throw new CordaRuntimeException("Only IOU lender can transfer the IOU."); + + // Get MemberInfos for the Vnode running the flow and the otherMember. + MemberInfo newLenderInfo = requireNonNull( + memberLookup.lookup(MemberX500Name.parse(flowArgs.getNewLender())), + "MemberLookup can't find otherMember specified in flow arguments." + ); + MemberInfo borrower = requireNonNull( + memberLookup.lookup(iouInput.getBorrower()), + "MemberLookup can't find otherMember specified in flow arguments." + ); + + // Create the IOUState from the input arguments and member information. + IOUState iouOutput = iouInput.withNewLender(newLenderInfo.getName(), Arrays.asList(borrower.getLedgerKeys().get(0), newLenderInfo.getLedgerKeys().get(0))); + + //get notary from input + MemberX500Name notary = iouStateAndRef.getState().getNotaryName(); + + // Use UTXOTransactionBuilder to build up the draft transaction. + UtxoTransactionBuilder txBuilder = ledgerService.createTransactionBuilder() + .setNotary(notary) + .setTimeWindowBetween(Instant.now(), Instant.now().plusMillis(Duration.ofDays(1).toMillis())) + .addInputState(iouStateAndRef.getRef()) + .addOutputState(iouOutput) + .addCommand(new IOUContract.Transfer()) + .addSignatories(Arrays.asList(borrower.getLedgerKeys().get(0), newLenderInfo.getLedgerKeys().get(0),myInfo.getLedgerKeys().get(0))); + + // Convert the transaction builder to a UTXOSignedTransaction and sign with this Vnode's first Ledger key. + // Note, toSignedTransaction() is currently a placeholder method, hence being marked as deprecated. + UtxoSignedTransaction signedTransaction = txBuilder.toSignedTransaction(); + + // Call FinalizeIOUSubFlow 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 FinalizeIOUFlow.FinalizeIOU(signedTransaction, Arrays.asList(borrower.getName(),newLenderInfo.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 http-rpc: +{ + "clientRequestId": "transferiou-1", + "flowClassName": "com.r3.developers.samples.obligation.workflows.IOUTransferFlow", + "requestBody": { + "newLender":"CN=Charlie, OU=Test Dept, O=R3, L=London, C=GB", + "iouID":"1ac69d82-804b-487b-9178-ea527d0e4b80" + } +} + */ \ No newline at end of file diff --git a/java-samples/sendAndRecieveTransaction/workflows/src/main/java/com/r3/developers/samples/obligation/workflows/IOUTransferFlowArgs.java b/java-samples/sendAndRecieveTransaction/workflows/src/main/java/com/r3/developers/samples/obligation/workflows/IOUTransferFlowArgs.java new file mode 100644 index 0000000..d0e5b03 --- /dev/null +++ b/java-samples/sendAndRecieveTransaction/workflows/src/main/java/com/r3/developers/samples/obligation/workflows/IOUTransferFlowArgs.java @@ -0,0 +1,26 @@ +package com.r3.developers.samples.obligation.workflows; + +import java.util.UUID; + +// A class to hold the deserialized arguments required to start the flow. +public class IOUTransferFlowArgs { + + private String newLender; + private UUID iouID; + + public IOUTransferFlowArgs() { + } + + public IOUTransferFlowArgs(String newLender, UUID iouID) { + this.newLender = newLender; + this.iouID = iouID; + } + + public String getNewLender() { + return newLender; + } + + public UUID getIouID() { + return iouID; + } +} diff --git a/java-samples/sendAndRecieveTransaction/workflows/src/main/java/com/r3/developers/samples/obligation/workflows/ListIOUFlow.java b/java-samples/sendAndRecieveTransaction/workflows/src/main/java/com/r3/developers/samples/obligation/workflows/ListIOUFlow.java new file mode 100644 index 0000000..f7498bd --- /dev/null +++ b/java-samples/sendAndRecieveTransaction/workflows/src/main/java/com/r3/developers/samples/obligation/workflows/ListIOUFlow.java @@ -0,0 +1,59 @@ +package com.r3.developers.samples.obligation.workflows; + +import com.r3.developers.samples.obligation.states.IOUState; +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.time.Instant; +import java.util.List; +import java.util.stream.Collectors; + +public class ListIOUFlow implements ClientStartableFlow { + + private final static Logger log = LoggerFactory.getLogger(ListIOUFlow.class); + + // Injects the JsonMarshallingService to read and populate JSON parameters. + @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("ListIOUFlow.call() called"); + + // Queries the VNode's vault for unconsumed states and converts the result to a serializable DTO. + List> states = utxoLedgerService.findUnconsumedStatesByExactType(IOUState.class,100, Instant.now()).getResults(); + List results = states.stream().map(stateAndRef -> + new ListIOUFlowResults( + stateAndRef.getState().getContractState().getLinearId(), + stateAndRef.getState().getContractState().getAmount(), + stateAndRef.getState().getContractState().getBorrower().toString(), + stateAndRef.getState().getContractState().getLender().toString(), + stateAndRef.getState().getContractState().getPaid() + ) + ).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 http-rpc: +{ + "clientRequestId": "list-1", + "flowClassName": "com.r3.developers.samples.obligation.workflows.ListIOUFlow", + "requestBody": {} +} +*/ \ No newline at end of file diff --git a/java-samples/sendAndRecieveTransaction/workflows/src/main/java/com/r3/developers/samples/obligation/workflows/ListIOUFlowResults.java b/java-samples/sendAndRecieveTransaction/workflows/src/main/java/com/r3/developers/samples/obligation/workflows/ListIOUFlowResults.java new file mode 100644 index 0000000..170f44b --- /dev/null +++ b/java-samples/sendAndRecieveTransaction/workflows/src/main/java/com/r3/developers/samples/obligation/workflows/ListIOUFlowResults.java @@ -0,0 +1,44 @@ +package com.r3.developers.samples.obligation.workflows; + +import java.util.UUID; + +// A class to hold the deserialized arguments required to start the flow. +public class ListIOUFlowResults { + + private UUID id; + private int amount; + private String borrower; + private String lender; + private int paid; + + public ListIOUFlowResults() { + } + + public ListIOUFlowResults(UUID id, int amount, String borrower, String lender, int paid) { + this.id = id; + this.amount = amount; + this.borrower = borrower; + this.lender = lender; + this.paid = paid; + } + + public UUID getId() { + return id; + } + + public int getAmount() { + return amount; + } + + public String getBorrower() { + return borrower; + } + + public String getLender() { + return lender; + } + + public int getPaid() { + return paid; + } +} diff --git a/java-samples/sendAndRecieveTransaction/workflows/src/main/java/com/r3/developers/samples/obligation/workflows/ReceiveTransactionFlow.java b/java-samples/sendAndRecieveTransaction/workflows/src/main/java/com/r3/developers/samples/obligation/workflows/ReceiveTransactionFlow.java new file mode 100644 index 0000000..fbf7303 --- /dev/null +++ b/java-samples/sendAndRecieveTransaction/workflows/src/main/java/com/r3/developers/samples/obligation/workflows/ReceiveTransactionFlow.java @@ -0,0 +1,26 @@ +package com.r3.developers.samples.obligation.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.messaging.FlowSession; +import net.corda.v5.base.annotations.Suspendable; +import net.corda.v5.ledger.utxo.UtxoLedgerService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@InitiatedBy(protocol = "utxo-transaction-transmission-protocol") +public class ReceiveTransactionFlow implements ResponderFlow { + private static final Logger log = LoggerFactory.getLogger(ReceiveTransactionFlow.class); + + @CordaInject + private UtxoLedgerService utxoLedgerService; + + @Suspendable + @Override + public void call(FlowSession session) { + // Receive the transaction and log its details. + var transaction = utxoLedgerService.receiveTransaction(session); + log.info("Received transaction - " + transaction.getId()); + } +} diff --git a/java-samples/sendAndRecieveTransaction/workflows/src/main/java/com/r3/developers/samples/obligation/workflows/sendAndReceiveTransaction.java b/java-samples/sendAndRecieveTransaction/workflows/src/main/java/com/r3/developers/samples/obligation/workflows/sendAndReceiveTransaction.java new file mode 100644 index 0000000..fc76ecb --- /dev/null +++ b/java-samples/sendAndRecieveTransaction/workflows/src/main/java/com/r3/developers/samples/obligation/workflows/sendAndReceiveTransaction.java @@ -0,0 +1,99 @@ +package com.r3.developers.samples.obligation.workflows; + +import net.corda.v5.application.crypto.DigestService; +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 net.corda.v5.crypto.SecureHash; +import net.corda.v5.ledger.utxo.StateRef; +import net.corda.v5.ledger.utxo.UtxoLedgerService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; +import java.util.stream.Collectors; + +@InitiatingFlow(protocol = "utxo-transaction-transmission-protocol") +public class sendAndReceiveTransaction implements ClientStartableFlow { + + @CordaInject + private FlowMessaging flowMessaging; + + @CordaInject + private UtxoLedgerService utxoLedgerService; + + @CordaInject + private JsonMarshallingService jsonMarshallingService; + + @CordaInject + private MemberLookup memberLookup; + + @CordaInject + private DigestService digestService; + + private static final Logger log = LoggerFactory.getLogger(sendAndReceiveTransaction.class); + + @Suspendable + @Override + public String call(ClientRequestBody requestBody) { + sendAndRecieveTransactionArgs request = requestBody.getRequestBodyAs(jsonMarshallingService, sendAndRecieveTransactionArgs.class); + + // Parse the state reference to obtain the transaction ID. + SecureHash transactionId = StateRef.parse(request.getStateRef() + ":0", digestService).getTransactionId(); + + // Retrieve the signed transaction from the ledger. + var transaction = requireNotNull(utxoLedgerService.findSignedTransaction(transactionId), + "Transaction is not found or verified."); + + // Map the X500 names in the request to Member objects, ensuring each member exists. + var members = request.getMembers().stream() + .map(x500 -> requireNotNull(memberLookup.lookup(MemberX500Name.parse(x500)), + "Member " + x500 + " does not exist in the membership group")) + .collect(Collectors.toList()); + + // Initialize the sessions with the memebers that will be used to send the transaction. + var sessions = members.stream() + .map(member -> flowMessaging.initiateFlow(member.getName())) + .collect(Collectors.toList()); + + // Send the transaction with or without backchain depending on the request. + try { + if (request.isForceBackchain()) { + utxoLedgerService.sendTransactionWithBackchain(transaction, sessions); + } else { + utxoLedgerService.sendTransaction(transaction, sessions); + } + } catch (Exception e) { + // Log and rethrow any exceptions encountered during transaction sending. + log.warn("Sending transaction for " + transactionId + " failed.", e); + throw e; + } + + // Format and log the successful transaction response. + String response = jsonMarshallingService.format(transactionId.toString()); + log.info("SendTransaction is successful. Response: " + response); + return response; + } + + private T requireNotNull(T obj, String message) { + if (obj == null) throw new IllegalArgumentException(message); + return obj; + } +} + +/* +RequestBody for triggering the flow via http-rpc: +{ + "clientRequestId": "sendAndRecieve-1", + "flowClassName": "com.r3.developers.samples.obligation.workflows.sendAndReceiveTransaction", + "requestBody": { + "stateRef": "STATE REF ID HERE", + "members": ["CN=Charlie, OU=Test Dept, O=R3, L=London, C=GB"], + "forceBackchain": "false" + } +} +*/ diff --git a/java-samples/sendAndRecieveTransaction/workflows/src/main/java/com/r3/developers/samples/obligation/workflows/sendAndRecieveTransactionArgs.java b/java-samples/sendAndRecieveTransaction/workflows/src/main/java/com/r3/developers/samples/obligation/workflows/sendAndRecieveTransactionArgs.java new file mode 100644 index 0000000..d1cfd98 --- /dev/null +++ b/java-samples/sendAndRecieveTransaction/workflows/src/main/java/com/r3/developers/samples/obligation/workflows/sendAndRecieveTransactionArgs.java @@ -0,0 +1,33 @@ +package com.r3.developers.samples.obligation.workflows; + +import java.util.List; + +public class sendAndRecieveTransactionArgs { + + + private String stateRef; + private List members; + private boolean forceBackchain; + + public sendAndRecieveTransactionArgs(){ + + } + public sendAndRecieveTransactionArgs(String stateRef, List members, boolean forceBackchain) { + this.stateRef = stateRef; + this.members = members; + this.forceBackchain = forceBackchain; + } + + public String getStateRef() { + return stateRef; + } + + public List getMembers() { + return members; + } + + public boolean isForceBackchain() { + return forceBackchain; + } + +} diff --git a/java-samples/sendAndRecieveTransaction/workflows/src/test/java/com/r3/developers/samples/obligation/MyFirstFlowTest.java b/java-samples/sendAndRecieveTransaction/workflows/src/test/java/com/r3/developers/samples/obligation/MyFirstFlowTest.java new file mode 100644 index 0000000..f1efee1 --- /dev/null +++ b/java-samples/sendAndRecieveTransaction/workflows/src/test/java/com/r3/developers/samples/obligation/MyFirstFlowTest.java @@ -0,0 +1,41 @@ +package com.r3.developers.samples.obligation; +// +//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")); +// } +//} diff --git a/kotlin-samples/sendAndRecieveTransaction/.ci/Jenkinsfile b/kotlin-samples/sendAndRecieveTransaction/.ci/Jenkinsfile new file mode 100644 index 0000000..2108886 --- /dev/null +++ b/kotlin-samples/sendAndRecieveTransaction/.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/sendAndRecieveTransaction/.ci/nightly/JenkinsfileSnykScan b/kotlin-samples/sendAndRecieveTransaction/.ci/nightly/JenkinsfileSnykScan new file mode 100644 index 0000000..fc2b1ee --- /dev/null +++ b/kotlin-samples/sendAndRecieveTransaction/.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/sendAndRecieveTransaction/.gitignore b/kotlin-samples/sendAndRecieveTransaction/.gitignore new file mode 100644 index 0000000..d2879c4 --- /dev/null +++ b/kotlin-samples/sendAndRecieveTransaction/.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/sendAndRecieveTransaction/.run/runConfigurations/DebugCorDapp.run.xml b/kotlin-samples/sendAndRecieveTransaction/.run/runConfigurations/DebugCorDapp.run.xml new file mode 100644 index 0000000..1d8da82 --- /dev/null +++ b/kotlin-samples/sendAndRecieveTransaction/.run/runConfigurations/DebugCorDapp.run.xml @@ -0,0 +1,15 @@ + + + + \ No newline at end of file diff --git a/kotlin-samples/sendAndRecieveTransaction/.snyk b/kotlin-samples/sendAndRecieveTransaction/.snyk new file mode 100644 index 0000000..a4b3e0e --- /dev/null +++ b/kotlin-samples/sendAndRecieveTransaction/.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/sendAndRecieveTransaction/FlowManagementUI/Dockerfile b/kotlin-samples/sendAndRecieveTransaction/FlowManagementUI/Dockerfile new file mode 100644 index 0000000..016bc21 --- /dev/null +++ b/kotlin-samples/sendAndRecieveTransaction/FlowManagementUI/Dockerfile @@ -0,0 +1,5 @@ +FROM python +WORKDIR /app +COPY . /app +RUN pip install -r requirements.txt +CMD ["python3", "app.py"] \ No newline at end of file diff --git a/kotlin-samples/sendAndRecieveTransaction/FlowManagementUI/README.md b/kotlin-samples/sendAndRecieveTransaction/FlowManagementUI/README.md new file mode 100644 index 0000000..fffff29 --- /dev/null +++ b/kotlin-samples/sendAndRecieveTransaction/FlowManagementUI/README.md @@ -0,0 +1,84 @@ +# Corda 5 CorDapp Flow Management Tool + + +This user guide provides step-by-step instructions on using the Corda 5 flow management tool. This article will help you learn how to connect the running corDapp, make flow calls, configure flow queries, and retrieve results. + +## Prerequisites +* Install and run Python and Flask framework. link. + +* Prepare your local Corda 5 environment. (By default, the Flow Management Tool is looking to connect to https://localhost:8888/api/v5_2/swagger#/ with Login: Admin and Password: Admin.) + +* Clong the Flow Management Tool repository. FlowManagementUI: main + +## Set Up + +1. Assuming your local Corda 5 environment is populated and the swagger endpoint is at: https://localhost:8888/api/v5_2/swagger#/ + +2. Navigate to where you downloaded the Corda 5 Flow Management Tool + +3. To run the framework + * Navigate to the file name using cd command. + * use the python app.py command to run it. + ![image](https://github.com/parisyup/FlowManagementUI/assets/51169685/f0c3bf59-8180-48a0-91cc-80f2d260e530) + + * Later on, click on the IP Address which will open the Interface: + +![image(4)](https://github.com/parisyup/FlowManagementUI/assets/66366646/8d88e37c-edbb-4d6d-8bcd-d773e818a106) + + +The Flow Management Tool should be automatically connected with the CorDapp running locally from your CSDE. You can test the connection by click on the dropdown list at the Flow Initiator section. You should be able to see the vNodes of your started CorDapp from CSDE. + +![image](https://github.com/parisyup/FlowManagementUI/assets/51169685/5a2356f2-cd14-489c-abd0-4afe0bf0d251) + +## Set Up With Docker + +1- Open up Command Prompt + +2- Navigate to the application folder using the CD commands + +3- Ensure that Docker application is open and build the image using the following command: +`docker build -t your-image-name .` + +Make sure to include the dot at the end of the command + +the `your-image-name` at the end of the command can be whatever you like but make sure to use the same name in the next step + +4- Run the docker image using the following command: +`docker run --rm -it --expose 8888 -p 5000:5000 your-image-name` + +5- You can access the website by using https://localhost:5000 or https://127.0.0.1:5000 + +## Using the Flow Management Tool + +### Selecting the Flow Initiator + +As the first step of using the Flow Management Tool, you would need to select the Flow Initiator. The Flow Initiator indicates which vNode will be triggering the flow. If you wish to have Alice to run a transaction to Bob, select the X500Name of Alice. The selected vNode’s shortHash (Corda 5 Network participant identifier) will also be shown below the dropdown list to signify your selection. + +### Function 1: To Make a Flow Call + +1. Click on "Flow Call" tab in the application. +2. Paste the your JSON format request body into the request input box. +3. Click Post button to trigger the call. + +![image(5)](https://github.com/parisyup/FlowManagementUI/assets/66366646/c65195a6-0a70-4354-804e-37884f657746) + + +### Function 2: To Configure Flow Query + +1. Click on the “Flow Query” tab. +2. Choose whether to query a single flow or all flows at the selected Flow Initiator. +3. If you choose to query all of the flows, select “Query All Flows“ then click “Get“. + +![image](https://github.com/parisyup/FlowManagementUI/assets/51169685/0482cfa4-7ee1-42f2-8786-2d8ad80b2936) +4. If you choose to query a single flow, select “Query Single Flow“, please add the ClientID in specified filed. +5. Click on “Get” to retrieve the result. + +![image(6)](https://github.com/parisyup/FlowManagementUI/assets/66366646/13e979b0-f76e-4f2c-9d55-81be8880890b) + +If you have any suggestions or questions, feel free to give us your feedback through Github for a better experience in the future! + +## Conclusion +In summary, our project introduces a specialized flow management layer on top of Swagger for Corda developers. We understand the challenges developers face in testing Corda applications due to the complexity of commands, our solution focuses on simplifying the process. + +Our all-in-one flow management system provides developers with a unified platform, streamlining development and enhancing efficiency. A key feature allows developers to run flows directly from an externally developed website and monitor their status in real-time, offering a user-friendly and practical solution for Corda developers. Overall, our project aims to make Corda development more accessible and tailored to the specific needs of flow management. + diff --git a/kotlin-samples/sendAndRecieveTransaction/FlowManagementUI/app.py b/kotlin-samples/sendAndRecieveTransaction/FlowManagementUI/app.py new file mode 100644 index 0000000..5d3e3f6 --- /dev/null +++ b/kotlin-samples/sendAndRecieveTransaction/FlowManagementUI/app.py @@ -0,0 +1,12 @@ +from flask import Flask +from flask import render_template +app = Flask(__name__) + + +@app.route('/') +def home(): # put application's code here + return render_template("index.html") + +if __name__ == '__main__': + app.run(debug=True, host='0.0.0.0') + diff --git a/kotlin-samples/sendAndRecieveTransaction/FlowManagementUI/requirements.txt b/kotlin-samples/sendAndRecieveTransaction/FlowManagementUI/requirements.txt new file mode 100644 index 0000000..8ab6294 --- /dev/null +++ b/kotlin-samples/sendAndRecieveTransaction/FlowManagementUI/requirements.txt @@ -0,0 +1 @@ +flask \ No newline at end of file diff --git a/kotlin-samples/sendAndRecieveTransaction/FlowManagementUI/static/Scripts/script.js b/kotlin-samples/sendAndRecieveTransaction/FlowManagementUI/static/Scripts/script.js new file mode 100644 index 0000000..8fd4ec0 --- /dev/null +++ b/kotlin-samples/sendAndRecieveTransaction/FlowManagementUI/static/Scripts/script.js @@ -0,0 +1,322 @@ +// This script contains functions for making API requests, handling data, and updating the UI. + +// Variable to store the selected X500Name from the dropdown +let selectedX500Name; + +// Variable to indicate whether data is currently being loaded from the pull request +let loading = false; + +// Function to make a GET request to an external API and return the data +function getData() { + // Replace the URL with the actual API endpoint + return fetch('https://jsonplaceholder.typicode.com/todos/1') + .then(response => response.json()) + .then(data => { + console.log('API Result:', data); + + // Return specific data fields + return { + id: data.id, + title: data.title + }; + }) + .catch(error => { + console.error('Error:', error); + }); +} + +// Function to get the data and display it on the page +function getDataAndDisplay() { + getData() + .then(result => { + // Update the result div with the data + document.getElementById('result').innerHTML = ` +

ID: ${result.id}

+

Title: ${result.title}

+ `; + }) + .catch(error => { + console.error('Error:', error); + }); +} + +// Function to make a GET request to retrieve CPI data +function getCPI() { + const url = 'https://localhost:8888/api/v1/cpi'; + // Perform the GET request with authorization headers + return fetch(url, { + method: 'GET', + headers: { + 'Accept': 'application/json', + 'Authorization': 'Basic YWRtaW46YWRtaW4=' + } + }) + .then(response => response.json()) + .then(data => { + console.log('API Result:', data); + // Further processing of the data can be done here + }) + .catch(error => { + console.error('Error:', error); + }); +} + +// Function to get all virtual nodes and populate a dropdown with the data +function getAllVirtualNodes() { + const url = 'https://localhost:8888/api/v1/virtualnode'; + + return fetch(url, { + method: 'GET', + headers: { + 'Accept': 'application/json', + 'Authorization': 'Basic YWRtaW46YWRtaW4=' + } + }) + .then(response => response.json()) + .then(data => { + // Extract virtualNodes from the API response + const virtualNodes = data.virtualNodes; + console.log('API Result:', virtualNodes); + + // Process the data and populate the dropdown + if (Array.isArray(virtualNodes)) { + const dropdown = document.getElementById('itemDropdown'); + dropdown.innerHTML = ''; + dropdown.innerHTML += ''; + + virtualNodes.forEach(item => { + // Display each item on the console + console.log('Item X500Name:', item.holdingIdentity.x500Name); + console.log('Item ShortHash:', item.holdingIdentity.shortHash); + + // Create an option element and add it to the dropdown + const option = document.createElement('option'); + option.value = item.holdingIdentity.shortHash; + option.text = item.holdingIdentity.x500Name; + dropdown.appendChild(option); + }); + + // Add event listener to the dropdown to detect changes + dropdown.addEventListener('change', function () { + selectedX500Name = this.value; + console.log('Selected ShortHash:', selectedX500Name); + + // Call a function or update a variable based on the selected item + handleDropdownChange(selectedX500Name); + }); + + } else { + console.warn('API Result is not an array.'); + } + }) + .catch(error => { + console.error('Error:', error); + }); +} + +// Initialize the virtual nodes dropdown on page load +getAllVirtualNodes(); + +// function to handle dropdown change +function handleDropdownChange(selectedX500Name) { + console.log('Handling dropdown change for Item ID:', selectedX500Name); + getSelectedVNode(); + // Additional actions based on the selected item can be performed here +} + +// Function to get the selected virtual node and display it +function getSelectedVNode() { + const displayElement = document.getElementById('selectedX500Display'); + displayElement.textContent = `Selected X500: ${selectedX500Name}`; +} + +// Function to get all flow results based on the selected virtual node +function getAllFlowResult() { + const url = `https://localhost:8888/api/v1/flow/${selectedX500Name}`; + + return fetch(url, { + method: 'GET', + headers: { + 'Accept': 'application/json', + 'Authorization': 'Basic YWRtaW46YWRtaW4=' + } + }) + .then(response => response.json()) + .then(data => { + const flowStatusResponses = data.flowStatusResponses; + console.log('API Result:', flowStatusResponses); + + // Convert the JSON object to a string for display + const jsonString = JSON.stringify(flowStatusResponses, null, 2); + + // Display the JSON string in the result div + document.getElementById('idResutl').innerHTML = `
${jsonString}
`; + }) + .catch(error => { + console.error('Error:', error); + }); +} + +// Function to make a POST request with a request body +async function postCallFlow() { + let postBtn = $('#postBtn'); + const url = `https://localhost:8888/api/v1/flow/${selectedX500Name}`; + + // Change the button text to indicate loading + postBtn.html('Loading...'); + + try { + // Perform the POST request with the provided request body + const response = await fetch(url, { + method: 'POST', + headers: { + 'Accept': 'application/json', + 'Authorization': 'Basic YWRtaW46YWRtaW4=', + 'Content-Type': 'application/json' + }, + body: `${document.getElementById('requestBody').value}` + }); + + // Parse the response as JSON + const data = await response.json(); + + if (!data.ok) { + console.log(data.status); + + // Check if the response status is not OK (2xx range) + if (data.status == "409" || data.status == "400") { + let flowStatusResponse = "title: " + data.title + "\nStatus: " + data.status; + + // Display additional details for 400 status + if (data.status == "400") { + flowStatusResponse += "\nDetails: \n Cause: " + data.details.cause + "\n Reason: " + data.details.reason; + } + + document.getElementById('queryResult').innerHTML = `
${flowStatusResponse}
`; + return; + } + } + + let msg = "null"; + let typ = "null"; + + // Construct a string with the flow status responses + let flowStatusResponses = "client request ID: " + data.clientRequestId + + "\nFlow Result " + data.flowResult + + "\nFlow Error Message: " + msg + + "\nflow error type: " + typ + + "\nFlow ID: " + data.flowId + + "\nFlow status: " + data.flowStatus + + "\nHolding identity short hash: " + data.holdingIdentityShortHash + + "\nTime stamp: " + data.timestamp; + + console.log('API Result:', flowStatusResponses); + + // Display the flow status responses in the result div + document.getElementById('queryResult').innerHTML = `
${flowStatusResponses}
`; + } catch (error) { + console.log('Error:', error); + } finally { + // Restore the button text after the operation is complete + postBtn.html('Post'); + } +} + +// Function to display an item on the page +function displayItemOnPage(item) { + const resultDiv = document.getElementById('queryResult'); + + // Create a new element to display the item + const itemElement = document.createElement('div'); + itemElement.innerHTML = ` +

QueryResult: ${item}

+ `; + + // Append the new element to the result div + resultDiv.appendChild(itemElement); +} + +// Function to open a specific tab by hiding/showing content +function openTab(tabName) { + // Hide all tab content + var tabContents = document.getElementsByClassName("tab-content"); + for (var i = 0; i < tabContents.length; i++) { + tabContents[i].style.display = "none"; + document.getElementById(tabContents[i].id + "-tab").style.backgroundColor = "rgb(52,152,219)"; + } + + // Show the selected tab content + var selectedTab = document.getElementById(tabName); + if (selectedTab) { + selectedTab.style.display = "block"; + document.getElementById(tabName + "-tab").style.backgroundColor = "rgb(173,216,230)"; + } +} + +// Function to display a flow for a specific virtual node +function oneFlow() { + const url = `https://localhost:8888/api/v1/flow/${selectedX500Name}/${document.getElementById('clientID').value}`; + + // Validate clientID input + if (document.getElementById('clientID').value == "") { + alert("Please input a clientId"); + return; + } + + // Perform a GET request to display a flow + return fetch(url, { + method: 'GET', + headers: { + 'Accept': 'application/json', + 'Authorization': 'Basic YWRtaW46YWRtaW4=' + } + }) + .then(response => response.json()) + .then(data => { + let msg = "null"; + let typ = "null"; + + // Check if the flow status is "FAILED" and extract error details + if (data.flowStatus == "FAILED") { + msg = data.flowError.message; + typ = data.flowError.type; + } + + // Construct a string with the flow status responses + const flowStatusResponses = "client request ID: " + data.clientRequestId + + "\nFlow Result " + data.flowResult + + "\nFlow Error Message: " + msg + + "\nflow error type: " + typ + + "\nFlow ID: " + data.flowId + + "\nFlow status: " + data.flowStatus + + "\nHolding identity short hash: " + data.holdingIdentityShortHash + + "\nTime stamp: " + data.timestamp; + + console.log('API Result:', flowStatusResponses); + + // Display the flow status responses in the result div + document.getElementById('idResutl').innerHTML = `
${flowStatusResponses}
`; + }) + .catch(error => { + console.error('Error:', error); + }); +} + +// Function to determine which flow-related action to execute based on user input +function executeButtonFlow() { + // Check the value of the dropdown to determine which action to perform + if (document.getElementById("dropdown").value == "option1") { + getAllFlowResult(); + } else { + oneFlow(); + } +} + +function queryDropDownChange(){ + if (document.getElementById("dropdown").value == "option1") { + document.getElementById("clientID").style.display = 'none'; + }else{ + document.getElementById("clientID").style.display = 'block'; + + } +} \ No newline at end of file diff --git a/kotlin-samples/sendAndRecieveTransaction/FlowManagementUI/static/css/main.css b/kotlin-samples/sendAndRecieveTransaction/FlowManagementUI/static/css/main.css new file mode 100644 index 0000000..5e4d849 --- /dev/null +++ b/kotlin-samples/sendAndRecieveTransaction/FlowManagementUI/static/css/main.css @@ -0,0 +1,202 @@ +body { + font-family: 'Roboto', sans-serif; + background-color: #f4f4f4; + color: #333; + margin: 50px; /* Add margin to the entire body */ + padding: 0; +} + +h1 { + text-align: center; + color: #0e0c0c; +} + +/* Style for the label */ +label { + display: block; + margin-bottom: 10px; + font-weight: bold; + color: #333; +} +/* Style for the dropdown button */ +select { + width: 20%; + padding: 8px; + margin-right: 10px; + border: 1px solid #ccc; + border-radius: 4px; + box-sizing: border-box; + font-size: 14px; + color: #555; +} + +#itemDropdown { + width: 40%; + padding: 10px; + box-sizing: border-box; +} + +#clientID{ + padding: 8px; + margin-bottom: 15px; + margin-right: 10px; + border: 1px solid #ccc; + border-radius: 4px; + box-sizing: border-box; + font-size: 14px; + color: #555; +} + +#OneFlowButon{ + padding: 8px; + margin-bottom: 15px; + margin-right: 10px; + border: 1px solid #ccc; + border-radius: 4px; + box-sizing: border-box; + font-size: 14px; +} + +#result { + margin-bottom: 10px; +} + +/* Button styling */ +button { + background-color: #3498db; + color: #fff; + padding: 10px 25px; + font-size: 16px; + border: 2px; + border-radius: 15px; + cursor: pointer; + transition: background-color 0.3s ease; +} + +button:hover { + background-color: #2980b9; +} + +/* Tab styling */ +.tab { + list-style-type: none; /* Remove default list styles */ + display: inline-block; /* Display tabs inline */ + padding: 2px 00px; /* Add padding to the tabs */ + margin: 0 1px; /* Add margin between tabs */ + cursor: pointer; /* Change cursor to pointer on hover */ +} + + +.tab li { + flex: 1; + text-align: center; + padding: 10px; + background-color: #3498db; + color: #fff; + border-radius: 8px 15px 0 0; + cursor: pointer; + transition: background-color 0.3s ease; +} + +.tab li:hover { + background-color: #2980b9; +} + +/* Tab content styling */ +.tab-content { + display: none; + padding: 20px; + border: 1px solid #3498db; + border-radius: 0 0 5px 5px; + background-color: #fff; +} + +.styled-input { + width: 100%; + padding: 10px; + margin-bottom: 10px; + box-sizing: border-box; +} + +.output { + border: 1px solid #3498db; + padding: 10px; + border-radius: 5px; + background-color: #fff; + box-sizing: border-box; +} + +#idResutl { + border: 1px solid #3498db; + padding: 10px; + border-radius: 5px; + background-color: #fff; + margin-top: 10px; + height: 290px; + max-height: 290px; /* Set a maximum height for the scroll box */ + overflow-y: auto; /* Enable vertical scrolling if content exceeds the box height */ +} + +/* Responsive design */ +@media screen and (max-width: 600px) { + .tab li { + border-radius: 5px; + margin-bottom: 5px; + } + .tab-content { + border-radius: 5px; + } +} +#call { + display: -ms-inline-flexbox; + flex-wrap: wrap; + +} + +/* Style for side-by-side input boxes */ +.flowcall-container { + display: flex; +} + +.input-box, .text-box { + width: 150px; /* Set the desired width */ + margin-right: 10px; /* Optional: Add margin for spacing between input boxes */ + border-radius: 5px; + border: 1px solid #3498db; +} + +.queryoption-container{ + display: flex; +} + + + +#requestBody { + flex: 1; + box-sizing: border-box; + width: 100px; /* Set the desired width */ + height: 300px; /* Set the desired height */ + padding: 10px; /* Optional: Add padding for better aesthetics */ + margin-right: 10px; /* Add some margin between the input and button */ +} + +#postBtn { + flex: 0 0 auto; /* Don't allow the button to grow or shrink */ + margin-top: 10px; /* Add some margin between the button and result box */ + margin-right: 10px; /* Add some margin between the button and result box */ + width: 500px; /* Set the desired width */ + height: 50px; /* Set the desired height */ +} + +#queryResult { + flex: 1; + box-sizing: border-box; + width: 100px; /* Set the desired width */ + height: 300px; /* Set the desired height */ + padding: 10px; /* Optional: Add padding for better aesthetics */ +} +.content-box{ + padding: 10px; /* Add padding to the content boxes */ +} + + diff --git a/kotlin-samples/sendAndRecieveTransaction/FlowManagementUI/templates/index.html b/kotlin-samples/sendAndRecieveTransaction/FlowManagementUI/templates/index.html new file mode 100644 index 0000000..44df7ac --- /dev/null +++ b/kotlin-samples/sendAndRecieveTransaction/FlowManagementUI/templates/index.html @@ -0,0 +1,87 @@ + + + + + + + + + + Flask Frontend Example + + + + + + + + + + + + +

Flow Management APIs

+
+ + + + + + +

Please select a flow initiator

+ + +
    + +
  • Flow Call
  • +
  • Flow Query
  • + + +
    +
+ + +
+ +
+ + + + +
+
+ + +
Result will be displayed here
+
+ +
+
+ + +
+ + + +
+ + + + + + + + +
+ + + + + +
Result will be displayed here
+
+ + diff --git a/kotlin-samples/sendAndRecieveTransaction/README.md b/kotlin-samples/sendAndRecieveTransaction/README.md new file mode 100644 index 0000000..12d358c --- /dev/null +++ b/kotlin-samples/sendAndRecieveTransaction/README.md @@ -0,0 +1,120 @@ +# sendAndReceiveTransaction + +When working with the Corda platform, every transaction is stored in the participants' +vaults. The vault is a where all the transactions involving the owner are securely saved. +Each vault is unique and accessible only by its owner, +serving as a ledger to track all the owner's transactions. +However, there are scenarios where you may want a third party to receive a copy of the +transaction. This is where the SendAndRecieveTransaction function becomes essential. +For instance, if Alice conducts a transaction with Bob and wants Charlie to receive a copy, +Alice can simply run a flow using the transaction ID to send a copy to Charlie's vault. +Another application of this function can be an automated reporting tool, +which can be utilized at the end of each transaction finalizing flow to automatically +report to a specific vnode. This functionality can act like a bookkeeper, +meticulously tracking each transaction and ensuring accurate record-keeping. +` + +### 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/v5_2/swagger#/. You can test out some + 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 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 IOUState between two parties +Pick a VNode identity to initiate the IOU creation, and get its short hash. (Let's pick Alice. Don't pick Bob because Bob is the person who alice will borrow from). + +Go to `POST /flow/{holdingidentityshorthash}`, enter the identity short hash(Alice's hash) and request body: +``` +{ + "clientRequestId": "createiou-1", + "flowClassName": "com.r3.developers.samples.obligation.workflows.IOUIssueFlow", + "requestBody": { + "amount":"20", + "lender":"CN=Bob, OU=Test Dept, O=R3, L=London, C=GB" + } +} +``` + +After trigger the create-IOU flow, hop to `GET /flow/{holdingidentityshorthash}/{clientrequestid}` and enter the short hash(Alice's hash) and client request id to view the flow result +The stateRef of the transaction will be returned as a result of the flow query. Which is the "flowResult". + +#### Step 2: Sending a copy of the transaction to a third party. +If a member needs to share a copy of their transaction with another member, +they can do so using the process outlined below. For instance, +if Alice wishes to send a copy of the transaction to Dave, +we can execute the following request body with her short hash: + +``` +{ + "clientRequestId": "sendAndRecieve-1", + "flowClassName": "com.r3.developers.samples.obligation.workflows.sendAndRecieveTransactionFlow", + "requestBody": { + "stateRef": "STATEREF ID HERE", + "members": ["CN=Dave, OU=Test Dept, O=R3, L=London, C=GB"], + "forceBackchain": "false" + } +} +``` + +Ensure to replace the stateRef variable with the stateRef of the transaction. The stateRef will be found when you run the GET call, +QueryAll in the swaggerAPI. The stateRef is labeled as `flowResult` in response body, begins with SHA-256D:XXXXX.. +``` +[ + { + "holdingIdentityShortHash": "A93A019B324E", + "clientRequestId": "createiou-1", + "flowId": "26ea3f95-141b-4aaa-9b58-e3a685dc54d3", + "flowStatus": "COMPLETED", + "flowResult": "SHA-256D:B3D87C8B446C277B5658BBB2A18DC7491539D898B70F074418878091AE315B4A", + "flowError": null, + "timestamp": "2024-07-08T04:37:16.175Z" + } +] +``` +After running this flow Dave will have the transaction in his vault. + + +Results + +Currently, Alice has two transactions stored in her vault. +The transaction we aim to send to Charlie is identified by the ID SHA-256D:8DFDD672…. +You can view Alice's transactions in the image provided below: + +

+ Encumbrance Flow +

+ +On the other hand, Charlie’s vault currently holds no transactions, +as illustrated in the image below: + +

+ Encumbrance Flow +

+ +Once Alice executes the flow, Charlie’s vault is updated to include the transaction, +which is displayed as follows: + +

+ Encumbrance Flow +

+ +All images of the vault were sourced through DBeaver by establishing a connection +to the Cordapp using PostgreSQL. The credentials utilized for this connection are as follows: + +POSTGRES_DB = cordacluster
+POSTGRES_USER = postgres
+POSTGRES_PASSWORD = password + +To access the vault, navigate through the hierarchy in +PostgreSQL: Databases > cordacluster > Schemas > vnode_vault_(HASH_ID_OF_VNODE) > +Tables > utxo_transaction. To view the transactions, +simply double-click on utxo_transaction. \ No newline at end of file diff --git a/kotlin-samples/sendAndRecieveTransaction/aliceVault.png b/kotlin-samples/sendAndRecieveTransaction/aliceVault.png new file mode 100644 index 0000000..d377488 Binary files /dev/null and b/kotlin-samples/sendAndRecieveTransaction/aliceVault.png differ diff --git a/kotlin-samples/sendAndRecieveTransaction/build.gradle b/kotlin-samples/sendAndRecieveTransaction/build.gradle new file mode 100644 index 0000000..210776a --- /dev/null +++ b/kotlin-samples/sendAndRecieveTransaction/build.gradle @@ -0,0 +1,88 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +import static org.gradle.api.JavaVersion.VERSION_17 + +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.gradle.plugin' +} + +allprojects { + group 'com.r3.developers.cordapptemplate' + version '1.0-SNAPSHOT' + + def javaVersion = VERSION_17 + + // Configure Corda runtime gradle plugin + cordaRuntimeGradlePlugin { + notaryVersion = cordaNotaryPluginsVersion + notaryCpiName = "NotaryServer" + corDappCpiName = "MyCorDapp" + cpiUploadTimeout = "30000" + vnodeRegistrationTimeout = "60000" + cordaProcessorTimeout = "300000" + workflowsModuleName = "workflows" + cordaClusterURL = "https://localhost:8888" + cordaRestUser = "admin" + cordaRestPasswd ="admin" + composeFilePath = "config/combined-worker-compose.yaml" + networkConfigFile = "config/static-network-config.json" + r3RootCertFile = "config/r3-ca-key.pem" + skipTestsDuringBuildCpis = "false" + cordaRuntimePluginWorkspaceDir = "workspace" + 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(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-dev-template-kotlin-sample" + groupId project.group + artifact jar + } + } +} \ No newline at end of file diff --git a/kotlin-samples/sendAndRecieveTransaction/charlieAfter.png b/kotlin-samples/sendAndRecieveTransaction/charlieAfter.png new file mode 100644 index 0000000..51d47a7 Binary files /dev/null and b/kotlin-samples/sendAndRecieveTransaction/charlieAfter.png differ diff --git a/kotlin-samples/sendAndRecieveTransaction/charlieBefore.png b/kotlin-samples/sendAndRecieveTransaction/charlieBefore.png new file mode 100644 index 0000000..e9cb661 Binary files /dev/null and b/kotlin-samples/sendAndRecieveTransaction/charlieBefore.png differ diff --git a/kotlin-samples/sendAndRecieveTransaction/config/combined-worker-compose.yaml b/kotlin-samples/sendAndRecieveTransaction/config/combined-worker-compose.yaml new file mode 100644 index 0000000..da2495d --- /dev/null +++ b/kotlin-samples/sendAndRecieveTransaction/config/combined-worker-compose.yaml @@ -0,0 +1,87 @@ +version: '2' +services: + postgresql: + image: postgres:14.10 + restart: unless-stopped + tty: true + environment: + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=password + - POSTGRES_DB=cordacluster + ports: + - 5432:5432 + + kafka: + image: confluentinc/cp-kafka:7.6.0 + ports: + - 9092:9092 + environment: + KAFKA_NODE_ID: 1 + CLUSTER_ID: ZDFiZmU3ODUyMzRiNGI3NG + KAFKA_PROCESS_ROLES: broker,controller + KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka:9093 + KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:9092,DOCKER_INTERNAL://0.0.0.0:29092,CONTROLLER://0.0.0.0:9093 + KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092,DOCKER_INTERNAL://kafka:29092 + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,DOCKER_INTERNAL:PLAINTEXT,CONTROLLER:PLAINTEXT + KAFKA_INTER_BROKER_LISTENER_NAME: DOCKER_INTERNAL + KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER + KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 + KAFKA_DEFAULT_REPLICATION_FACTOR: 1 + KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 + KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1 + KAFKA_AUTO_CREATE_TOPICS_ENABLE: "true" + + kafka-create-topics: + image: openjdk:17-jdk + depends_on: + - kafka + volumes: + - ${CORDA_CLI:-~/.corda/cli}:/opt/corda-cli + working_dir: /opt/corda-cli + command: [ + "java", + "-jar", + "corda-cli.jar", + "topic", + "-b=kafka:29092", + "create", + "connect" + ] + + corda: + image: corda/corda-os-combined-worker-kafka:5.2.0.0 + depends_on: + - postgresql + - kafka + - kafka-create-topics + volumes: + - ../config:/config + - ../logs:/logs + environment: + JAVA_TOOL_OPTIONS: -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 + LOG4J_CONFIG_FILE: config/log4j2.xml + CONSOLE_LOG_LEVEL: info + ENABLE_LOG4J2_DEBUG: false + command: [ + "-mbus.busType=KAFKA", + "-mbootstrap.servers=kafka:29092", + "-spassphrase=password", + "-ssalt=salt", + "-ddatabase.user=user", + "-ddatabase.pass=password", + "-ddatabase.jdbc.url=jdbc:postgresql://postgresql:5432/cordacluster", + "-ddatabase.jdbc.directory=/opt/jdbc-driver/" + ] + ports: + - 8888:8888 + - 7004:7004 + - 5005:5005 + + flow-management-tool: + depends_on: + - corda + build: + context: ../FlowManagementUI + dockerfile: Dockerfile + ports: + - 5000:5000 \ No newline at end of file diff --git a/kotlin-samples/sendAndRecieveTransaction/config/gradle-plugin-default-key.pem b/kotlin-samples/sendAndRecieveTransaction/config/gradle-plugin-default-key.pem new file mode 100644 index 0000000..5294bbd --- /dev/null +++ b/kotlin-samples/sendAndRecieveTransaction/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/sendAndRecieveTransaction/config/log4j2.xml b/kotlin-samples/sendAndRecieveTransaction/config/log4j2.xml new file mode 100644 index 0000000..909222c --- /dev/null +++ b/kotlin-samples/sendAndRecieveTransaction/config/log4j2.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/kotlin-samples/sendAndRecieveTransaction/config/r3-ca-key.pem b/kotlin-samples/sendAndRecieveTransaction/config/r3-ca-key.pem new file mode 100644 index 0000000..a803613 --- /dev/null +++ b/kotlin-samples/sendAndRecieveTransaction/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/sendAndRecieveTransaction/config/static-network-config.json b/kotlin-samples/sendAndRecieveTransaction/config/static-network-config.json new file mode 100644 index 0000000..b0f2519 --- /dev/null +++ b/kotlin-samples/sendAndRecieveTransaction/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/kotlin-samples/sendAndRecieveTransaction/contracts/build.gradle b/kotlin-samples/sendAndRecieveTransaction/contracts/build.gradle new file mode 100644 index 0000000..c1f6ed6 --- /dev/null +++ b/kotlin-samples/sendAndRecieveTransaction/contracts/build.gradle @@ -0,0 +1,88 @@ + +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:$slf4jVersion" + 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" +// testImplementation "com.r3.corda.ledger.utxo:contract-testing:$contractTestingVersion" +// testImplementation "com.r3.corda.ledger.utxo:contract-testing-kotlin:$contractTestingVersion" +} + +// 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 contractsModule + versionId 1 + licence cordappLicense + vendor cordappVendorName + } +} + +// Use the name of the contract module as the name of the generated CPK and CPB. +archivesBaseName = cordapp.contract.name.isPresent() ? cordapp.contract.name.get() : contractsModule + +publishing { + publications { + maven(MavenPublication) { + from components.cordapp + } + } +} \ No newline at end of file diff --git a/kotlin-samples/sendAndRecieveTransaction/contracts/src/main/kotlin/com/r3/developers/samples/obligation/contracts/IOUContract.kt b/kotlin-samples/sendAndRecieveTransaction/contracts/src/main/kotlin/com/r3/developers/samples/obligation/contracts/IOUContract.kt new file mode 100644 index 0000000..25338ee --- /dev/null +++ b/kotlin-samples/sendAndRecieveTransaction/contracts/src/main/kotlin/com/r3/developers/samples/obligation/contracts/IOUContract.kt @@ -0,0 +1,55 @@ +package com.r3.developers.samples.obligation.contracts + +import com.r3.developers.samples.obligation.states.IOUState +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 + +class IOUContract : Contract { + + //IOU Commands + class Issue: Command + class Settle: Command + class Transfer: Command + + override fun verify(transaction: UtxoLedgerTransaction) { + + // Ensures that there is only one command in the transaction + val command = transaction.commands.singleOrNull() ?: throw CordaRuntimeException("Requires a single command.") + + "The output state should have two and only two participants." using { + val output = transaction.outputContractStates.first() as IOUState + output.participants.size== 2 + } + // Switches case based on the command + when(command) { + // Rules applied only to transactions with the Issue Command. + is Issue -> { + "When command is Create there should be one and only one output state." using (transaction.outputContractStates.size == 1) + } + // Rules applied only to transactions with the Settle Command. + is Settle -> { + "When command is Update there should be one and only one output state." using (transaction.outputContractStates.size == 1) + } + // Rules applied only to transactions with the Transfer Command. + is Transfer -> { + "When command is Update there should be one and only one output state." using (transaction.outputContractStates.size == 1) + } + else -> { + throw CordaRuntimeException("Command not allowed.") + } + } + } + + // Helper function to allow writing constraints in the Corda 4 '"text" using (boolean)' style + private infix fun String.using(expr: Boolean) { + if (!expr) throw CordaRuntimeException("Failed requirement: $this") + } + + // Helper function to allow writing constraints in '"text" using {lambda}' style where the last expression + // in the lambda is a boolean. + private infix fun String.using(expr: () -> Boolean) { + if (!expr.invoke()) throw CordaRuntimeException("Failed requirement: $this") + } +} \ No newline at end of file diff --git a/kotlin-samples/sendAndRecieveTransaction/contracts/src/main/kotlin/com/r3/developers/samples/obligation/states/IOUState.kt b/kotlin-samples/sendAndRecieveTransaction/contracts/src/main/kotlin/com/r3/developers/samples/obligation/states/IOUState.kt new file mode 100644 index 0000000..7dbb518 --- /dev/null +++ b/kotlin-samples/sendAndRecieveTransaction/contracts/src/main/kotlin/com/r3/developers/samples/obligation/states/IOUState.kt @@ -0,0 +1,36 @@ +package com.r3.developers.samples.obligation.states + +import com.r3.developers.samples.obligation.contracts.IOUContract +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.* + +//Link with the Contract class +@BelongsToContract(IOUContract::class) +data class IOUState ( + + //private variables + val amount: Int, + val lender: MemberX500Name, + val borrower: MemberX500Name, + val paid: Int, + val linearId: UUID, + private val participants: List +) : ContractState { + + //Helper method for settle flow + fun pay(amountToPay: Int) : IOUState { + return IOUState(amount,lender,borrower,paid+amountToPay,linearId,participants) + } + + //Helper method for transfer flow + fun withNewLender(newLender:MemberX500Name, newParticipants:List ): IOUState { + return IOUState(amount,newLender,borrower,paid,linearId,newParticipants) + } + + override fun getParticipants(): List { + return participants + } +} \ No newline at end of file diff --git a/kotlin-samples/sendAndRecieveTransaction/gradle.properties b/kotlin-samples/sendAndRecieveTransaction/gradle.properties new file mode 100644 index 0000000..2549e5f --- /dev/null +++ b/kotlin-samples/sendAndRecieveTransaction/gradle.properties @@ -0,0 +1,73 @@ +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.2.0.52 + +# 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.2.0.0 + +# Specify the version of the cordapp-cpb and cordapp-cpk plugins +cordaPluginsVersion=7.0.4 + +# Specify the version of the Corda runtime Gradle plugin to use +cordaGradlePluginVersion=5.2.0.0 + +# Specify the name of the workflows module +# This will be the name of the generated cpk and cpb files +workflowsModule=workflows + +# Specify the name of the contracts module +# This will be the name of the generated cpk and cpb files +contractsModule=contracts + +# Specify the location of where Corda 5 binaries can be downloaded +# Relative path from user.home +cordaBinariesDirectory = .corda/corda5 + +# Specify the location of where Corda 5 CLI binaries can be downloaded +# Relative path from user.home +cordaCliBinariesDirectory = .corda/cli + +# Metadata for the CorDapp. +cordappLicense="Apache License, Version 2.0" +cordappVendorName="R3" + +# 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.10.0 +mockitoKotlinVersion=4.0.0 +mockitoVersion=4.6.1 +hamcrestVersion=2.2 +assertjVersion = 3.24.1 +contractTestingVersion=1.0.0-beta-+ +jacksonVersion=2.15.2 +slf4jVersion=1.7.36 + +# Specify the maximum amount of time allowed for the CPI upload +# As your CorDapp grows you might need to increase this +# Value is in milliseconds +cpiUploadDefault=10000 + +# Specify the length of time, in milliseconds, that Corda waits for an individual event to process. +# Keep at -1 to use the default. Refer to the Corda Api Docs for the exact value. +processorTimeout=-1 + +# Specify the maximum amount of time allowed to check all vNodes are registered +# Value is in milliseconds +vnodeRegistrationTimeoutDefault=30000 + +# Specify if you want to run the contracts and workflows tests as part of the corda-runtime-plugin-cordapp > buildCpis task +# False by default, will execute the tests every time you stand the template up - gives extra protection +# Set to true to skip the tests, making the launching process quicker. You will be responsible for running workflow tests yourself +skipTestsDuringBuildCpis=false \ No newline at end of file diff --git a/kotlin-samples/sendAndRecieveTransaction/gradle/wrapper/gradle-wrapper.jar b/kotlin-samples/sendAndRecieveTransaction/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..943f0cb Binary files /dev/null and b/kotlin-samples/sendAndRecieveTransaction/gradle/wrapper/gradle-wrapper.jar differ diff --git a/kotlin-samples/sendAndRecieveTransaction/gradle/wrapper/gradle-wrapper.properties b/kotlin-samples/sendAndRecieveTransaction/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..5083229 --- /dev/null +++ b/kotlin-samples/sendAndRecieveTransaction/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/sendAndRecieveTransaction/gradlew b/kotlin-samples/sendAndRecieveTransaction/gradlew new file mode 100644 index 0000000..65dcd68 --- /dev/null +++ b/kotlin-samples/sendAndRecieveTransaction/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/sendAndRecieveTransaction/gradlew.bat b/kotlin-samples/sendAndRecieveTransaction/gradlew.bat new file mode 100644 index 0000000..93e3f59 --- /dev/null +++ b/kotlin-samples/sendAndRecieveTransaction/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/sendAndRecieveTransaction/settings.gradle b/kotlin-samples/sendAndRecieveTransaction/settings.gradle new file mode 100644 index 0000000..ee7121e --- /dev/null +++ b/kotlin-samples/sendAndRecieveTransaction/settings.gradle @@ -0,0 +1,25 @@ +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 'org.jetbrains.kotlin.jvm' version kotlinVersion + id 'org.jetbrains.kotlin.plugin.jpa' version kotlinVersion + id 'org.jetbrains.kotlin.plugin.allopen' version kotlinVersion + id 'net.corda.gradle.plugin' version cordaGradlePluginVersion + } +} + +// Root project name, used in naming the project as a whole and used in naming objects built by the project. +rootProject.name = 'sendAndReceiveTransaction' +include ':workflows' +include ':contracts' \ No newline at end of file diff --git a/kotlin-samples/sendAndRecieveTransaction/workflows/build.gradle b/kotlin-samples/sendAndRecieveTransaction/workflows/build.gradle new file mode 100644 index 0000000..c8bde66 --- /dev/null +++ b/kotlin-samples/sendAndRecieveTransaction/workflows/build.gradle @@ -0,0 +1,98 @@ +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 { + constraints { + testImplementation('org.slf4j:slf4j-api') { + version { + // Corda cannot use SLF4J 2.x yet. + strictly '1.7.36' + } + } + } + + // 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:$slf4jVersion" + 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" + testImplementation "org.assertj:assertj-core:$assertjVersion" + testImplementation "com.fasterxml.jackson.module:jackson-module-kotlin:$jacksonVersion" +} + +// 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 workflowsModule + versionId 1 + licence cordappLicense + vendor cordappVendorName + } +} + +// Use the name of the workflow module as the name of the generated CPK and CPB. +archivesBaseName = cordapp.workflow.name.isPresent() ? cordapp.workflow.name.get() : workflowsModule + +publishing { + publications { + maven(MavenPublication) { + from components.cordapp + } + } +} \ No newline at end of file diff --git a/kotlin-samples/sendAndRecieveTransaction/workflows/src/main/kotlin/com/r3/developers/samples/obligation/workflows/FinalizeIOUFlow.kt b/kotlin-samples/sendAndRecieveTransaction/workflows/src/main/kotlin/com/r3/developers/samples/obligation/workflows/FinalizeIOUFlow.kt new file mode 100644 index 0000000..9c09db4 --- /dev/null +++ b/kotlin-samples/sendAndRecieveTransaction/workflows/src/main/kotlin/com/r3/developers/samples/obligation/workflows/FinalizeIOUFlow.kt @@ -0,0 +1,98 @@ +package com.r3.developers.samples.obligation.workflows + +import com.r3.developers.samples.obligation.states.IOUState +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.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 org.slf4j.LoggerFactory + +// @InitiatingFlow declares the protocol which will be used to link the initiator to the responder. +@InitiatingFlow(protocol = "finalize-iou-protocol") +class FinalizeIOUSubFlow(private val signedTransaction: UtxoSignedTransaction, private val otherMember: List): SubFlow { + + private companion object { + val log = LoggerFactory.getLogger(this::class.java.enclosingClass) + } + + // Injects the UtxoLedgerService to enable the flow to make use of the Ledger API. + @CordaInject + lateinit var ledgerService: UtxoLedgerService + + @CordaInject + lateinit var flowMessaging: FlowMessaging + + @CordaInject + lateinit var utxoLedgerService: UtxoLedgerService + + + @Suspendable + override fun call(): String { + + log.info("FinalizeIOUSubFlow.call() called") + + // Initiates a session with the other Member. + val sessions = otherMember.map { flowMessaging.initiateFlow(it) } + + + return try { + // 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. + val finalizedSignedTransaction = ledgerService.finalize( + signedTransaction, + sessions + ) + // Returns the transaction id converted to a string. + finalizedSignedTransaction.transaction.id.toString().also { + log.info("Success! Response: $it") + } + } + // Soft fails the flow and returns the error message without throwing a flow exception. + catch (e: Exception) { + log.warn("Finality failed", e) + "Finality failed, ${e.message}" + } + } +} + +//@InitiatingBy declares the protocol which will be used to link the initiator to the responder. +@InitiatedBy(protocol = "finalize-iou-protocol") +class FinalizeIOUResponderFlow: ResponderFlow { + + private companion object { + val log = LoggerFactory.getLogger(this::class.java.enclosingClass) + } + + // Injects the UtxoLedgerService to enable the flow to make use of the Ledger API. + @CordaInject + lateinit var ledgerService: UtxoLedgerService + + @Suspendable + override fun call(session: FlowSession) { + + log.info("FinalizeIOUResponderFlow.call() called") + + try { + // 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. + val finalizedSignedTransaction = ledgerService.receiveFinality(session) { ledgerTransaction -> + + // Note, this exception will only be shown in the logs if Corda Logging is set to debug. + val state = ledgerTransaction.getOutputStates(IOUState::class.java).singleOrNull() ?: throw CordaRuntimeException("Failed verification - transaction did not have exactly one output IOUState.") + + log.info("Verified the transaction- ${ledgerTransaction.id}") + } + log.info("Finished responder flow - ${finalizedSignedTransaction.transaction.id}") + } + // Soft fails the flow and log the exception. + catch (e: Exception) { + log.warn("Exceptionally finished responder flow", e) + } + } +} \ No newline at end of file diff --git a/kotlin-samples/sendAndRecieveTransaction/workflows/src/main/kotlin/com/r3/developers/samples/obligation/workflows/IOUIssueFlow.kt b/kotlin-samples/sendAndRecieveTransaction/workflows/src/main/kotlin/com/r3/developers/samples/obligation/workflows/IOUIssueFlow.kt new file mode 100644 index 0000000..53b6a41 --- /dev/null +++ b/kotlin-samples/sendAndRecieveTransaction/workflows/src/main/kotlin/com/r3/developers/samples/obligation/workflows/IOUIssueFlow.kt @@ -0,0 +1,119 @@ +package com.r3.developers.samples.obligation.workflows + +import com.r3.developers.samples.obligation.contracts.IOUContract +import com.r3.developers.samples.obligation.states.IOUState +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.base.types.MemberX500Name +import net.corda.v5.ledger.common.NotaryLookup +import net.corda.v5.ledger.utxo.UtxoLedgerService +import org.slf4j.LoggerFactory +import java.time.Duration +import java.time.Instant +import java.util.* + + +// A class to hold the deserialized arguments required to start the flow. +data class IOUIssueFlowArgs(val amount: String, val lender: String) + +class IOUIssueFlow: ClientStartableFlow { + + private companion object { + val log = LoggerFactory.getLogger(this::class.java.enclosingClass) + } + + // Injects the JsonMarshallingService to read and populate JSON parameters. + @CordaInject + lateinit var jsonMarshallingService: JsonMarshallingService + + // Injects the MemberLookup to look up the VNode identities. + @CordaInject + lateinit var memberLookup: MemberLookup + + // Injects the UtxoLedgerService to enable the flow to make use of the Ledger API. + @CordaInject + lateinit var ledgerService: UtxoLedgerService + + // Injects the NotaryLookup to look up the notary identity. + @CordaInject + lateinit var notaryLookup: NotaryLookup + + // FlowEngine service is required to run SubFlows. + @CordaInject + lateinit var flowEngine: FlowEngine + + @Suspendable + override fun call(requestBody: ClientRequestBody): String { + log.info("IOUIssueFlow.call() called") + + try { + // Obtain the deserialized input arguments to the flow from the requestBody. + val flowArgs = requestBody.getRequestBodyAs(jsonMarshallingService, IOUIssueFlowArgs::class.java) + + // Get MemberInfos for the Vnode running the flow and the otherMember. + // Good practice in Kotlin CorDapps is to only throw RuntimeException. + // Note, in Java CorDapps only unchecked RuntimeExceptions can be thrown not + // declared checked exceptions as this changes the method signature and breaks override. + val myInfo = memberLookup.myInfo() + val lenderInfo = memberLookup.lookup(MemberX500Name.parse(flowArgs.lender)) + ?: throw CordaRuntimeException("MemberLookup can't find otherMember specified in flow arguments.") + + // Create the IOUState from the input arguments and member information. + val iou = IOUState( + amount = flowArgs.amount.toInt(), + lender = lenderInfo.name, + borrower = myInfo.name, + paid = 0, + linearId = UUID.randomUUID(), + listOf(myInfo.ledgerKeys[0],lenderInfo.ledgerKeys[0]) + ) + + // Obtain the notary. + val notary = notaryLookup.lookup(MemberX500Name.parse("CN=NotaryService, OU=Test Dept, O=R3, L=London, C=GB")) + ?: throw CordaRuntimeException("NotaryLookup can't find notary specified in flow arguments.") + + // Use UTXOTransactionBuilder to build up the draft transaction. + val txBuilder= ledgerService.createTransactionBuilder() + .setNotary(notary.name) + .setTimeWindowBetween(Instant.now(), Instant.now().plusMillis(Duration.ofDays(1).toMillis())) + .addOutputState(iou) + .addCommand(IOUContract.Issue()) + .addSignatories(iou.participants) + + // 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. + val signedTransaction = txBuilder.toSignedTransaction() + + // Call FinalizeIOUSubFlow 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(FinalizeIOUSubFlow(signedTransaction, listOf(lenderInfo.name))) + + + } + // Catch any exceptions, log them and rethrow the exception. + catch (e: Exception) { + log.warn("Failed to process utxo flow for request body '$requestBody' because:'${e.message}'") + throw e + } } + +} + +/* +RequestBody for triggering the flow via http-rpc: +{ + "clientRequestId": "createiou-1", + "flowClassName": "com.r3.developers.samples.obligation.workflows.IOUIssueFlow", + "requestBody": { + "amount":"20", + "lender":"CN=Bob, OU=Test Dept, O=R3, L=London, C=GB" + } +} + */ diff --git a/kotlin-samples/sendAndRecieveTransaction/workflows/src/main/kotlin/com/r3/developers/samples/obligation/workflows/IOUSettleFlow.kt b/kotlin-samples/sendAndRecieveTransaction/workflows/src/main/kotlin/com/r3/developers/samples/obligation/workflows/IOUSettleFlow.kt new file mode 100644 index 0000000..ff61817 --- /dev/null +++ b/kotlin-samples/sendAndRecieveTransaction/workflows/src/main/kotlin/com/r3/developers/samples/obligation/workflows/IOUSettleFlow.kt @@ -0,0 +1,119 @@ +package com.r3.developers.samples.obligation.workflows + +import com.r3.developers.samples.obligation.contracts.IOUContract +import com.r3.developers.samples.obligation.states.IOUState +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.UtxoLedgerService +import org.slf4j.LoggerFactory +import java.time.Duration +import java.time.Instant +import java.util.* + + +// A class to hold the deserialized arguments required to start the flow. +data class IOUSettleFlowArgs(val amountSettle: String, val iouID: UUID) + +class IOUSettleFlow: ClientStartableFlow { + + private companion object { + val log = LoggerFactory.getLogger(this::class.java.enclosingClass) + } + + // Injects the JsonMarshallingService to read and populate JSON parameters. + @CordaInject + lateinit var jsonMarshallingService: JsonMarshallingService + + // Injects the MemberLookup to look up the VNode identities. + @CordaInject + lateinit var memberLookup: MemberLookup + + // Injects the UtxoLedgerService to enable the flow to make use of the Ledger API. + @CordaInject + lateinit var ledgerService: UtxoLedgerService + + // FlowEngine service is required to run SubFlows. + @CordaInject + lateinit var flowEngine: FlowEngine + + @CordaInject + lateinit var utxoLedgerService: UtxoLedgerService + + @Suspendable + override fun call(requestBody: ClientRequestBody): String { + log.info("IOUSettleFlow.call() called") + + try { + // Obtain the deserialized input arguments to the flow from the requestBody. + val flowArgs = requestBody.getRequestBodyAs(jsonMarshallingService, IOUSettleFlowArgs::class.java) + + // Get flow args from the input JSON + val iouID = flowArgs.iouID + val amountSettle = flowArgs.amountSettle.toInt() + + //query the IOU input + val iouStateAndRefs = ledgerService.findUnconsumedStatesByExactType(IOUState::class.java,100, Instant.now()).results + val iouStateAndRefsWithId = iouStateAndRefs.filter { it.state.contractState.linearId.equals(iouID)} + + if (iouStateAndRefsWithId.size != 1) throw CordaRuntimeException("Multiple or zero IOU states with id \" + iouID + \" found") + val iouStateAndRef = iouStateAndRefsWithId[0] + val iouInput = iouStateAndRef.state.contractState + + //flow logic checks + val myInfo = memberLookup.myInfo() + if (!(myInfo.name.equals(iouInput.borrower))) throw CordaRuntimeException("Only IOU borrower can settle the IOU.") + val lenderInfo = memberLookup.lookup(iouInput.lender) ?: throw CordaRuntimeException("MemberLookup can't find otherMember specified in flow arguments.") + + // Create the IOUState from the input arguments and member information. + val iouOutput = iouInput.pay(amountSettle) + + //get notary from input + val notary = iouStateAndRef.state.notaryName + + // Use UTXOTransactionBuilder to build up the draft transaction. + val txBuilder= ledgerService.createTransactionBuilder() + .setNotary(notary) + .setTimeWindowBetween(Instant.now(), Instant.now().plusMillis(Duration.ofDays(1).toMillis())) + .addInputState(iouStateAndRef.ref) + .addOutputState(iouOutput) + .addCommand(IOUContract.Settle()) + .addSignatories(iouOutput.participants) + + // 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. + val signedTransaction = txBuilder.toSignedTransaction() + + + + + // Call FinalizeIOUSubFlow 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(FinalizeIOUSubFlow(signedTransaction, listOf(lenderInfo.name))) + } + // Catch any exceptions, log them and rethrow the exception. + catch (e: Exception) { + log.warn("Failed to process utxo flow for request body '$requestBody' because:'${e.message}'") + throw e + } } + +} +/* +RequestBody for triggering the flow via http-rpc: +{ + "clientRequestId": "settleiou-1", + "flowClassName": "com.r3.developers.samples.obligation.workflows.IOUSettleFlow", + "requestBody": { + "amountSettle":"10", + "iouID":"4ea35048-879e-43f0-9593-343388715627" + } +} +4ea35048-879e-43f0-9593-343388715627 +*/ diff --git a/kotlin-samples/sendAndRecieveTransaction/workflows/src/main/kotlin/com/r3/developers/samples/obligation/workflows/IOUTransferFlow.kt b/kotlin-samples/sendAndRecieveTransaction/workflows/src/main/kotlin/com/r3/developers/samples/obligation/workflows/IOUTransferFlow.kt new file mode 100644 index 0000000..62fc6e5 --- /dev/null +++ b/kotlin-samples/sendAndRecieveTransaction/workflows/src/main/kotlin/com/r3/developers/samples/obligation/workflows/IOUTransferFlow.kt @@ -0,0 +1,116 @@ +package com.r3.developers.samples.obligation.workflows + +import com.r3.developers.samples.obligation.contracts.IOUContract +import com.r3.developers.samples.obligation.states.IOUState +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.base.types.MemberX500Name +import net.corda.v5.ledger.utxo.UtxoLedgerService +import org.slf4j.LoggerFactory +import java.time.Duration +import java.time.Instant +import java.util.* + +// A class to hold the deserialized arguments required to start the flow. +data class IOUTransferFlowArgs(val newLender: String, val iouID: UUID) + +class IOUTransferFlow: ClientStartableFlow { + + private companion object { + val log = LoggerFactory.getLogger(this::class.java.enclosingClass) + } + + // Injects the JsonMarshallingService to read and populate JSON parameters. + @CordaInject + lateinit var jsonMarshallingService: JsonMarshallingService + + // Injects the MemberLookup to look up the VNode identities. + @CordaInject + lateinit var memberLookup: MemberLookup + + // Injects the UtxoLedgerService to enable the flow to make use of the Ledger API. + @CordaInject + lateinit var ledgerService: UtxoLedgerService + + // FlowEngine service is required to run SubFlows. + @CordaInject + lateinit var flowEngine: FlowEngine + + @Suspendable + override fun call(requestBody: ClientRequestBody): String { + log.info("IOUTransferFlow.call() called") + + try { + // Obtain the deserialized input arguments to the flow from the requestBody. + val flowArgs = requestBody.getRequestBodyAs(jsonMarshallingService, IOUTransferFlowArgs::class.java) + + // Get flow args from the input JSON + val iouID = flowArgs.iouID + + //query the IOU input + val iouStateAndRefs = ledgerService.findUnconsumedStatesByExactType(IOUState::class.java,100, Instant.now()).results + val iouStateAndRefsWithId = iouStateAndRefs.filter { it.state.contractState.linearId.equals(iouID)} + if (iouStateAndRefsWithId.size != 1) throw CordaRuntimeException("Multiple or zero IOU states with id \" + iouID + \" found") + val iouStateAndRef = iouStateAndRefsWithId[0] + val iouInput = iouStateAndRef.state.contractState + + //flow logic checks + val myInfo = memberLookup.myInfo() + if (!(myInfo.name.equals(iouInput.lender))) throw CordaRuntimeException("Only IOU borrower can settle the IOU.") + + // Get MemberInfos for the Vnode running the flow and the otherMember. + val borrower = memberLookup.lookup(iouInput.borrower) ?: throw CordaRuntimeException("MemberLookup can't find otherMember specified in flow arguments.") + val newLenderInfo = memberLookup.lookup(MemberX500Name.parse(flowArgs.newLender)) ?: throw CordaRuntimeException("MemberLookup can't find otherMember specified in flow arguments.") + + // Create the IOUState from the input arguments and member information. + val iouOutput = iouInput.withNewLender(newLenderInfo.name, listOf(borrower.ledgerKeys[0],newLenderInfo.ledgerKeys[0])) + + //get notary from input + val notary = iouStateAndRef.state.notaryName + + // Use UTXOTransactionBuilder to build up the draft transaction. + val txBuilder= ledgerService.createTransactionBuilder() + .setNotary(notary) + .setTimeWindowBetween(Instant.now(), Instant.now().plusMillis(Duration.ofDays(1).toMillis())) + .addInputState(iouStateAndRef.ref) + .addOutputState(iouOutput) + .addCommand(IOUContract.Settle()) + .addSignatories(iouOutput.participants + listOf(myInfo.ledgerKeys[0])) + + // 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. + val signedTransaction = txBuilder.toSignedTransaction() + + // Call FinalizeIOUSubFlow 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(FinalizeIOUSubFlow(signedTransaction, listOf(borrower.name,newLenderInfo.name))) + + + } + // Catch any exceptions, log them and rethrow the exception. + catch (e: Exception) { + log.warn("Failed to process utxo flow for request body '$requestBody' because:'${e.message}'") + throw e + } } + +} +/* +RequestBody for triggering the flow via http-rpc: +{ + "clientRequestId": "transferiou-1", + "flowClassName": "com.r3.developers.samples.obligation.workflows.IOUTransferFlow", + "requestBody": { + "newLender":"CN=Charlie, OU=Test Dept, O=R3, L=London, C=GB", + "iouID":"4ea35048-879e-43f0-9593-343388715627" + } +} +4ea35048-879e-43f0-9593-343388715627 +*/ diff --git a/kotlin-samples/sendAndRecieveTransaction/workflows/src/main/kotlin/com/r3/developers/samples/obligation/workflows/ListIOUFlow.kt b/kotlin-samples/sendAndRecieveTransaction/workflows/src/main/kotlin/com/r3/developers/samples/obligation/workflows/ListIOUFlow.kt new file mode 100644 index 0000000..1f49677 --- /dev/null +++ b/kotlin-samples/sendAndRecieveTransaction/workflows/src/main/kotlin/com/r3/developers/samples/obligation/workflows/ListIOUFlow.kt @@ -0,0 +1,58 @@ +package com.r3.developers.samples.obligation.workflows + +import com.r3.developers.samples.obligation.states.IOUState +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.UtxoLedgerService +import org.slf4j.LoggerFactory +import java.time.Instant +import java.util.* + +// A class to hold the deserialized arguments required to start the flow. +data class ListIOUFlowResults(val id: UUID,val amount: Int,val borrower: String,val lender: String,val paid: Int) + + +class ListIOUFlow: ClientStartableFlow { + + private companion object { + val log = LoggerFactory.getLogger(this::class.java.enclosingClass) + } + // Injects the JsonMarshallingService to read and populate JSON parameters. + @CordaInject + lateinit var jsonMarshallingService: JsonMarshallingService + + // Injects the UtxoLedgerService to enable the flow to make use of the Ledger API. + @CordaInject + lateinit var ledgerService: UtxoLedgerService + + @Suspendable + override fun call(requestBody: ClientRequestBody): String { + log.info("ListIOUFlow.call() called") + + + // Queries the VNode's vault for unconsumed states and converts the result to a serializable DTO. + val states = ledgerService.findUnconsumedStatesByExactType(IOUState::class.java,100, Instant.now()).results + val results = states.map { stateAndRef -> + ListIOUFlowResults( + stateAndRef.state.contractState.linearId, + stateAndRef.state.contractState.amount, + stateAndRef.state.contractState.borrower.toString(), + stateAndRef.state.contractState.lender.toString(), + stateAndRef.state.contractState.paid, + ) + } + // Uses the JsonMarshallingService's format() function to serialize the DTO to Json. + return jsonMarshallingService.format(results) + } +} +/* +RequestBody for triggering the flow via http-rpc: +{ + "clientRequestId": "list-1", + "flowClassName": "com.r3.developers.samples.obligation.workflows.ListIOUFlow", + "requestBody": {} +} +*/ \ No newline at end of file diff --git a/kotlin-samples/sendAndRecieveTransaction/workflows/src/main/kotlin/com/r3/developers/samples/obligation/workflows/sendAndRecieveTransactionFlow.kt b/kotlin-samples/sendAndRecieveTransaction/workflows/src/main/kotlin/com/r3/developers/samples/obligation/workflows/sendAndRecieveTransactionFlow.kt new file mode 100644 index 0000000..3c6e4d2 --- /dev/null +++ b/kotlin-samples/sendAndRecieveTransaction/workflows/src/main/kotlin/com/r3/developers/samples/obligation/workflows/sendAndRecieveTransactionFlow.kt @@ -0,0 +1,110 @@ +package com.r3.developers.samples.obligation.workflows + +import net.corda.v5.application.crypto.DigestService +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 net.corda.v5.ledger.utxo.StateRef +import net.corda.v5.ledger.utxo.UtxoLedgerService +import org.slf4j.LoggerFactory + +@InitiatingFlow(protocol = "utxo-transaction-transmission-protocol") +class sendAndRecieveTransactionFlow : ClientStartableFlow { + data class Request( + val stateRef: String, + val members: List, + val forceBackchain: Boolean = false + ) + + @CordaInject + lateinit var flowMessaging: FlowMessaging + + @CordaInject + lateinit var utxoLedgerService: UtxoLedgerService + + @CordaInject + lateinit var jsonMarshallingService: JsonMarshallingService + + @CordaInject + lateinit var memberLookup: MemberLookup + + @CordaInject + lateinit var digestService: DigestService + + private val log = LoggerFactory.getLogger(sendAndRecieveTransactionFlow::class.java) + + @Suspendable + override fun call(requestBody: ClientRequestBody): String { + val request = requestBody.getRequestBodyAs(jsonMarshallingService, Request::class.java) + + // Parse the state reference to obtain the transaction ID. + val transactionId = StateRef.parse(request.stateRef + ":0", digestService).transactionId + + // Retrieve the signed transaction from the ledger. + val transaction = requireNotNull(utxoLedgerService.findSignedTransaction(transactionId)) { + "Transaction is not found or verified." + } + + // Map the X500 names in the request to Member objects, ensuring each member exists. + val members = request.members.map { x500 -> + requireNotNull(memberLookup.lookup(MemberX500Name.parse(x500))) { + "Member $x500 does not exist in the membership group" + } + } + + // Initialize the sessions with the memebers that will be used to send the transaction. + val sessions = members.map { flowMessaging.initiateFlow(it.name) } + + // Send the transaction with or without backchain depending on the request. + try { + if (request.forceBackchain) { + utxoLedgerService.sendTransactionWithBackchain(transaction, sessions) + } else { + utxoLedgerService.sendTransaction(transaction, sessions) + } + } catch (e: Exception) { + // Log and rethrow any exceptions encountered during transaction sending. + log.warn("Sending transaction for $transactionId failed.", e) + throw e + } + + // Format and log the successful transaction response. + return jsonMarshallingService.format(transactionId.toString()).also { + log.info("SendTransaction is successful. Response: $it") + } + } + + + @InitiatedBy(protocol = "utxo-transaction-transmission-protocol") + class ReceiveTransactionFlow : ResponderFlow { + private val log = LoggerFactory.getLogger(ReceiveTransactionFlow::class.java) + + @CordaInject + lateinit var utxoLedgerService: UtxoLedgerService + + @Suspendable + override fun call(session: FlowSession) { + // Receive the transaction and log its details. + val transaction = utxoLedgerService.receiveTransaction(session) + log.info("Received transaction - ${transaction.id}") + } + } +} + +/* +RequestBody for triggering the flow via http-rpc: +{ + "clientRequestId": "sendAndRecieve-1", + "flowClassName": "com.r3.developers.samples.obligation.workflows.sendAndRecieveTransactionFlow", + "requestBody": { + "stateRef": "STATE REF ID HERE", + "members": ["CN=Charlie, OU=Test Dept, O=R3, L=London, C=GB"], + "forceBackchain": "false" + + } +} +*/ \ No newline at end of file diff --git a/kotlin-samples/sendAndRecieveTransaction/workflows/src/test/kotlin/com/r3/developers/samples/obligation/MyFirstFlowTest.kt b/kotlin-samples/sendAndRecieveTransaction/workflows/src/test/kotlin/com/r3/developers/samples/obligation/MyFirstFlowTest.kt new file mode 100644 index 0000000..dd513fb --- /dev/null +++ b/kotlin-samples/sendAndRecieveTransaction/workflows/src/test/kotlin/com/r3/developers/samples/obligation/MyFirstFlowTest.kt @@ -0,0 +1,45 @@ +package com.r3.developers.samples.obligation + +//import net.corda.simulator.RequestData +//import net.corda.simulator.Simulator +//import net.corda.v5.base.types.MemberX500Name +//import org.junit.jupiter.api.Test + +// Note: this simulator test has been commented out pending the merging of the UTXO code into the Gecko Branch. + + + +//class MyFirstFlowTest { +// +// // Names picked to match the corda network in config/static-network-config.json +// private val aliceX500 = MemberX500Name.parse("CN=Alice, OU=Test Dept, O=R3, L=London, C=GB") +// private val bobX500 = MemberX500Name.parse("CN=Bob, OU=Test Dept, O=R3, L=London, C=GB") +// +// @Test +// fun `test that MyFirstFLow returns correct message`() { +// +// // Instantiate an instance of the Simulator +// val simulator = Simulator() +// +// // Create Alice and Bob's virtual nodes, including the Class's of the flows which will be registered on each node. +// // We don't assign Bob's virtual node to a val because we don't need it for this particular test. +// val aliceVN = simulator.createVirtualNode(aliceX500, MyFirstFlow::class.java) +// simulator.createVirtualNode(bobX500, MyFirstFlowResponder::class.java) +// +// // Create an instance of the MyFirstFlowStartArgs which contains the request arguments for starting the flow +// val myFirstFlowStartArgs = MyFirstFlowStartArgs(bobX500) +// +// // Create a requestData object +// val requestData = RequestData.create( +// "request no 1", // A unique reference for the instance of the flow request +// MyFirstFlow::class.java, // 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 from the flow +// val flowResponse = aliceVN.callFlow(requestData) +// +// // Check that the flow has returned the expected string +// assert(flowResponse == "Hello Alice, best wishes from Bob") +// } +//}