├── .gitignore ├── .idea └── runConfigurations │ ├── Debug_CorDapp.xml │ ├── Run_Template_Cordapp.xml │ ├── Run_Template_RPC_Client.xml │ └── Unit_tests.xml ├── LICENCE ├── README.md ├── TRADEMARK ├── build.gradle ├── config ├── dev │ └── log4j2.xml └── test │ └── log4j2.xml ├── cordapp ├── build.gradle └── src │ ├── integrationTest │ └── kotlin │ │ └── net │ │ └── corda │ │ └── demos │ │ └── crowdFunding │ │ └── DriverBasedTest.kt │ ├── main │ ├── kotlin │ │ └── net │ │ │ └── corda │ │ │ └── demos │ │ │ └── crowdFunding │ │ │ ├── Plugin.kt │ │ │ ├── Utils.kt │ │ │ ├── contracts │ │ │ ├── CampaignContract.kt │ │ │ └── PledgeContract.kt │ │ │ ├── flows │ │ │ ├── BroadcastTransaction.kt │ │ │ ├── EndCampaign.kt │ │ │ ├── MakePledge.kt │ │ │ ├── RecordTransactionAsObserver.kt │ │ │ └── StartCampaign.kt │ │ │ └── structures │ │ │ ├── Campaign.kt │ │ │ ├── CampaignResult.kt │ │ │ ├── CashStatesPayload.kt │ │ │ └── Pledge.kt │ └── resources │ │ ├── META-INF │ │ └── services │ │ │ └── net.corda.core.serialization.SerializationWhitelist │ │ └── certificates │ │ ├── readme.txt │ │ ├── sslkeystore.jks │ │ └── truststore.jks │ └── test │ └── kotlin │ └── net │ └── corda │ └── demos │ └── crowdFunding │ ├── NodeDriver.kt │ ├── contracts │ ├── CampaignTests.kt │ └── PledgeTests.kt │ └── flows │ ├── CrowdFundingTest.kt │ ├── EndCampaignTests.kt │ ├── MakePledgeTests.kt │ └── StartCampaignTests.kt ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── images ├── campaign-end-failure.png ├── campaign-end-success.png ├── create-campaign.png └── create-pledge.png ├── lib ├── README.txt └── quasar.jar └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | # Eclipse, ctags, Mac metadata, log files 2 | .classpath 3 | .project 4 | .settings 5 | tags 6 | .DS_Store 7 | *.log 8 | *.log.gz 9 | *.orig 10 | 11 | .gradle 12 | 13 | # General build files 14 | **/build/* 15 | !docs/build/* 16 | 17 | lib/dokka.jar 18 | 19 | ### JetBrains template 20 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio 21 | 22 | *.iml 23 | 24 | ## Directory-based project format: 25 | #.idea 26 | 27 | # if you remove the above rule, at least ignore the following: 28 | 29 | # Specific files to avoid churn 30 | .idea/*.xml 31 | .idea/copyright 32 | .idea/jsLibraryMappings.xml 33 | 34 | # User-specific stuff: 35 | .idea/tasks.xml 36 | .idea/dictionaries 37 | 38 | # Sensitive or high-churn files: 39 | .idea/dataSources.ids 40 | .idea/dataSources.xml 41 | .idea/sqlDataSources.xml 42 | .idea/dynamic.xml 43 | .idea/uiDesigner.xml 44 | 45 | # Gradle: 46 | .idea/libraries 47 | 48 | # Mongo Explorer plugin: 49 | .idea/mongoSettings.xml 50 | 51 | ## File-based project format: 52 | *.ipr 53 | *.iws 54 | 55 | ## Plugin-specific files: 56 | 57 | # IntelliJ 58 | /out/ 59 | cordapp/out/ 60 | cordapp-contracts-states/out/ 61 | 62 | # mpeltonen/sbt-idea plugin 63 | .idea_modules/ 64 | 65 | # JIRA plugin 66 | atlassian-ide-plugin.xml 67 | 68 | # Crashlytics plugin (for Android Studio and IntelliJ) 69 | com_crashlytics_export_strings.xml 70 | crashlytics.properties 71 | crashlytics-build.properties 72 | 73 | # docs related 74 | docs/virtualenv/ 75 | -------------------------------------------------------------------------------- /.idea/runConfigurations/Debug_CorDapp.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 14 | -------------------------------------------------------------------------------- /.idea/runConfigurations/Run_Template_Cordapp.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 15 | -------------------------------------------------------------------------------- /.idea/runConfigurations/Run_Template_RPC_Client.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 15 | -------------------------------------------------------------------------------- /.idea/runConfigurations/Unit_tests.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 17 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | Copyright 2016, R3 Limited. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Corda](https://www.corda.net/wp-content/uploads/2016/11/fg005_corda_b.png) 2 | 3 | # Crowd Funding Demo (Observable States) 4 | 5 | This is a small demo that aims to demonstrate the new observable states feature in Version 2 of Corda. As well as 6 | observable states, it also uses the following features: 7 | 8 | * Confidential identities 9 | * Queryable states and custom vault queries 10 | * Schedulable states 11 | 12 | ## TODO 13 | 14 | 1. Finish the contract and flow unit tests 15 | 2. Add a Spring Reactive web server 16 | 3. Finish the campaign contract code 17 | 4. Finish the README.md 18 | 6. Fill in the checkTransaction menthod of collect sigs flow 19 | 7. The manager should check that the cash transactions send the cash to him. For this to work, we need to use the same key for each cash state 20 | 21 | ## How it works 22 | 23 | There are two types of parties: 24 | 25 | * Campaign Managers 26 | * Pledgers 27 | 28 | As all nodes have the same CorDapp they all have the capability of setting up campaigns and pledging to other campaigns. 29 | 30 | 1. The demo begins with a node starting a new campaign. The node starting a new campaign becomes the manager for that 31 | campaign. The manager also needs to specify what the target amount to raise is, along with the campaign deadline and 32 | a name for the campaign. The manager is the only participant in the `Campaign` state, so it should *only* be stored 33 | in the manager node's vault. However, as we use the observable states feature via the `BroadcastTransaction` and 34 | `RecordTransactionAsObserver` flows, all the other nodes on the network will store this campaign state in their 35 | vaults too. The create new campaign transaction looks like this: 36 | 37 | ![Transaction for creating a new campaign.](images/create-campaign.png) 38 | 39 | 2. To make a pledge to a campaign, a node user must know the `linearId` of the campaign they wish to pledge to. As all 40 | the nodes on the network will receive new `Campaign` states, they can query their vault to enumerate all the 41 | campaigns, then pick the one they wish to pledge to. Making a pledge requires a node user to specify the `linearId` 42 | of the campaign they wish to pledge to as well as the amount they wish to pledge. The pledging node then constructs a 43 | transaction that contains the new `Pledge` state as an output as well as the `Campaign` state to be updated. As such 44 | there is both a `Campaign` input and output. The transaction looks like this: 45 | 46 | ![Transaction for creating a new campaign.](images/create-pledge.png) 47 | 48 | The `CampaignContract` code ensures that `Campaign.raisedSoFar` property is updated in-line with the amount pledged. 49 | Note in the above diagram, as this is the only pledge so far in this scenario, the `Campaign` output state reflects 50 | the amount in the `Pledge` state. 51 | 52 | This as with the create campaign transaction covered above, this transaction is broadcast to all nodes on the 53 | network. It is worth noting, that currently, one can only observe a *whole* transaction as opposed to parts of a 54 | transaction. This is not necessarily an issue for privacy as the pledgers can create a confidential identity to use 55 | when pledging, such that it is only known by the pledger and the campaign manager. The main complication with only 56 | being able to store full transactions manifests itself when querying the vault - all the pledges that you have been 57 | broadcast, can be returned 58 | 59 | 3. The `Campaign` is actually a `SchedulableState`. When we create a new campaign, we are required to enter a deadline 60 | to which the campaign will run until. Once the deadline is reached, a flow is run to determine whether the campaign 61 | was a success or failure. 62 | 63 | **Success:** The campaign ends successfully if the target amount is reached. In this case, an atomic transaction is 64 | produced to exit the `Campaign` state and all the `Pledge` states from the ledger as well as transfer the required 65 | pledged amounts in cash from the pledgers to the campaign manger. 66 | 67 | ![Transaction for creating a new campaign.](images/campaign-end-success.png) 68 | 69 | **Failure:** The campaign ends in failure if the target is not reached. In this case, an atomic transaction is 70 | produced to exit the `Campaign` state and all the `Pledge` states from the ledger. The transaction looks like this: 71 | 72 | ![Transaction for creating a new campaign.](images/campaign-end-failure.png) 73 | 74 | As with all the other transactions in this demo, these transactions will be broadcast to all other nodes on the 75 | network. 76 | 77 | ## Assumptions 78 | 79 | 1. If a node makes a pledge, they will have enough cash to fulfill the pledge when the campaign ends. 80 | 2. Confidential pledger identities are adequate from a privacy perspective. We don't mind that the cash and pledge 81 | states are shared to all nodes on the business network. If this an issue then we need to facilitate observable 82 | states via the use of `FilteredTransaction`s. 83 | 3. Each pledger can only make one pledger per campaign. They may update their pledge if they wish, though. 84 | 4. The `Campaign` state only needs to be signed by the campaign manager. 85 | 5. The `Pledge` states are bilateral agreements and are signed by the pledger and the campaign manager. 86 | 6. The `Campaign` state is included in all create pledge transactions to make sure the amount raised is updated with the 87 | correct amount pledged. As such, if two pledgers try to pledge at the same time, one pledge will end up being a 88 | double spend. We assume this is OK. 89 | 7. Any node can start a `Campaign`. 90 | 8. Nodes can pledge to multiple `Campaign`s. 91 | 9. After the campaign ends, it is OK to exit the `Campaign` state and all the `Pledge` states. 92 | 10. Only campaign managers can cancel pledges and a pledge cancellation must be accompanied with a campaign state and 93 | campaign End command. 94 | 95 | ## Pre-Requisites 96 | 97 | You will need the following installed on your machine before you can start: 98 | 99 | * [JDK 8](http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html) 100 | installed and available on your path (Minimum version: 1.8_131). 101 | * [IntelliJ IDEA](https://www.jetbrains.com/idea/download/) (Minimum version 2017.1) 102 | * git 103 | * Optional: [h2 web console](http://www.h2database.com/html/download.html) 104 | (download the "platform-independent zip") 105 | 106 | For more detailed information, see the 107 | [getting set up](https://docs.corda.net/getting-set-up.html) page on the 108 | Corda docsite. 109 | 110 | ## Running the unit tests 111 | 112 | Via gradle: 113 | 114 | **Unix:** 115 | 116 | ./gradlew clean test 117 | 118 | **Windows:** 119 | 120 | gradlew.bat clean test 121 | 122 | If you want to run the tests via IntelliJ then navigate to `cordapp/strc/test/kotlin/net.corda.demos.crowdFunding` in 123 | the project explorer to see the unit test files. To run a test click the Green arrow in the left handle margin of the 124 | editor. 125 | 126 | # Running the CorDapp 127 | 128 | ## Clone the repo 129 | 130 | To get started, clone this repository with: 131 | 132 | git clone https://github.com/roger3cev/observable-states.git 133 | 134 | And change directories to the newly cloned repo: 135 | 136 | cd observable-states 137 | 138 | ## Building the CorDapp: 139 | 140 | **Unix:** 141 | 142 | ./gradlew deployNodes 143 | 144 | **Windows:** 145 | 146 | gradlew.bat deployNodes 147 | 148 | Note: You'll need to re-run this build step after making any changes to 149 | the template for these to take effect on the node. 150 | 151 | ## Running the Nodes 152 | 153 | Once the build finishes, change directories to the folder where the newly 154 | built nodes are located: 155 | 156 | cd build/nodes 157 | 158 | The Gradle build script will have created a folder for each node. You'll 159 | see three folders, one for each node and a `runnodes` script. You can 160 | run the nodes with: 161 | 162 | **Unix:** 163 | 164 | ./runnodes --log-to-console --logging-level=INFO 165 | 166 | **Windows:** 167 | 168 | runnodes.bat --log-to-console --logging-level=INFO 169 | 170 | You should now have three Corda nodes running on your machine serving 171 | the template. 172 | 173 | When the nodes have booted up, you should see a message like the following 174 | in the console: 175 | 176 | Node started up and registered in 5.007 sec 177 | 178 | ## Interacting with the CorDapp via HTTP 179 | 180 | To be added. 181 | 182 | ## Further reading 183 | 184 | Tutorials and developer docs for CorDapps and Corda are 185 | [here](https://docs.corda.net/). -------------------------------------------------------------------------------- /TRADEMARK: -------------------------------------------------------------------------------- 1 | Corda and the Corda logo are trademarks of R3CEV LLC and its affiliates. All rights reserved. 2 | 3 | For R3CEV LLC's trademark and logo usage information, please consult our Trademark Usage Policy at 4 | https://www.r3.com/trademark-policy/. 5 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.corda_release_version = '2.0.0' 3 | ext.corda_gradle_plugins_version = '1.0.0' 4 | ext.kotlin_version = '1.1.4' 5 | ext.junit_version = '4.12' 6 | ext.quasar_version = '0.7.6' 7 | 8 | repositories { 9 | mavenLocal() 10 | mavenCentral() 11 | jcenter() 12 | } 13 | 14 | dependencies { 15 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 16 | classpath "net.corda.plugins:cordformation:$corda_gradle_plugins_version" 17 | classpath "net.corda.plugins:quasar-utils:$corda_gradle_plugins_version" 18 | classpath "org.jetbrains.kotlin:kotlin-noarg:$kotlin_version" 19 | } 20 | } 21 | 22 | repositories { 23 | mavenLocal() 24 | jcenter() 25 | mavenCentral() 26 | maven { url 'https://jitpack.io' } 27 | maven { url 'https://ci-artifactory.corda.r3cev.com/artifactory/corda-releases' } 28 | } 29 | 30 | apply plugin: 'kotlin' 31 | apply plugin: 'net.corda.plugins.cordformation' 32 | apply plugin: 'net.corda.plugins.quasar-utils' 33 | 34 | sourceSets { 35 | main { 36 | resources { 37 | srcDir "config/dev" 38 | } 39 | } 40 | test { 41 | resources { 42 | srcDir "config/test" 43 | } 44 | } 45 | integrationTest { 46 | kotlin { 47 | compileClasspath += main.output + test.output 48 | runtimeClasspath += main.output + test.output 49 | srcDir file('src/integration-test/kotlin') 50 | } 51 | } 52 | } 53 | 54 | configurations { 55 | integrationTestCompile.extendsFrom testCompile 56 | integrationTestRuntime.extendsFrom testRuntime 57 | } 58 | 59 | dependencies { 60 | compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version" 61 | testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version" 62 | testCompile "junit:junit:$junit_version" 63 | 64 | // Corda integration dependencies 65 | cordaCompile "net.corda:corda-core:$corda_release_version" 66 | cordaCompile "net.corda:corda-finance:$corda_release_version" 67 | cordaCompile "net.corda:corda-jackson:$corda_release_version" 68 | cordaCompile "net.corda:corda-rpc:$corda_release_version" 69 | cordaCompile "net.corda:corda-node-api:$corda_release_version" 70 | cordaCompile "net.corda:corda-webserver-impl:$corda_release_version" 71 | cordaRuntime "net.corda:corda:$corda_release_version" 72 | cordaRuntime "net.corda:corda-webserver:$corda_release_version" 73 | 74 | testCompile "net.corda:corda-node-driver:$corda_release_version" 75 | 76 | // CorDapp dependencies 77 | // Specify your CorDapp's dependencies below, including dependent CorDapps. 78 | // We've defined Cash as a dependent CorDapp as an example. 79 | cordapp project(":cordapp") 80 | cordapp "net.corda:corda-finance:$corda_release_version" 81 | } 82 | 83 | task integrationTest(type: Test, dependsOn: []) { 84 | testClassesDirs = sourceSets.integrationTest.output.classesDirs 85 | classpath = sourceSets.integrationTest.runtimeClasspath 86 | } 87 | 88 | tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { 89 | kotlinOptions { 90 | languageVersion = "1.1" 91 | apiVersion = "1.1" 92 | jvmTarget = "1.8" 93 | javaParameters = true // Useful for reflection. 94 | } 95 | } 96 | 97 | task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { 98 | directory "./build/nodes" 99 | networkMap "O=Controller,L=London,C=GB" 100 | node { 101 | name "O=Controller,L=London,C=GB" 102 | advertisedServices = ["corda.notary.validating"] 103 | p2pPort 10002 104 | rpcPort 10003 105 | cordapps = [ 106 | "$project.group:shared:$project.version", 107 | "net.corda:corda-finance:$corda_release_version" 108 | ] 109 | } 110 | node { 111 | name "O=PartyA,L=London,C=GB" 112 | advertisedServices = [] 113 | p2pPort 10005 114 | rpcPort 10006 115 | webPort 10007 116 | cordapps = [ 117 | "$project.group:shared:$project.version", 118 | "$project.group:cordapp:$project.version", 119 | "net.corda:corda-finance:$corda_release_version" 120 | ] 121 | rpcUsers = [[ user: "user1", "password": "test", "permissions": []]] 122 | } 123 | node { 124 | name "O=PartyB,L=New York,C=US" 125 | advertisedServices = [] 126 | p2pPort 10008 127 | rpcPort 10009 128 | webPort 10010 129 | cordapps = [ 130 | "$project.group:cordapp:$project.version", 131 | "net.corda:corda-finance:$corda_release_version" 132 | ] 133 | rpcUsers = [[ user: "user1", "password": "test", "permissions": []]] 134 | } 135 | } 136 | 137 | task runTemplateClient(type: JavaExec) { 138 | classpath = sourceSets.main.runtimeClasspath 139 | main = 'com.template.ClientKt' 140 | args 'localhost:10006' 141 | } -------------------------------------------------------------------------------- /config/dev/log4j2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | logs 6 | node-${hostName} 7 | ${log-path}/archive 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | %highlight{%level{length=1} %d{HH:mm:ss} %T %c{1}.%M - %msg%n}{INFO=white,WARN=red,FATAL=bright red blink} 17 | > 18 | 19 | 20 | 21 | 23 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /config/test/log4j2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | [%-5level] %d{HH:mm:ss.SSS} [%t] %c{1}.%M - %msg%n 8 | > 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /cordapp/build.gradle: -------------------------------------------------------------------------------- 1 | repositories { 2 | mavenLocal() 3 | jcenter() 4 | mavenCentral() 5 | maven { url 'https://jitpack.io' } 6 | maven { url 'https://ci-artifactory.corda.r3cev.com/artifactory/corda-releases' } 7 | } 8 | 9 | 10 | apply plugin: 'kotlin' 11 | apply plugin: 'kotlin-jpa' 12 | apply plugin: 'net.corda.plugins.cordformation' 13 | apply plugin: 'net.corda.plugins.quasar-utils' 14 | 15 | sourceSets { 16 | main { 17 | resources { 18 | srcDir "config/dev" 19 | } 20 | } 21 | test { 22 | resources { 23 | srcDir "config/test" 24 | } 25 | } 26 | integrationTest { 27 | kotlin { 28 | compileClasspath += main.output + test.output 29 | runtimeClasspath += main.output + test.output 30 | srcDir file('src/integration-test/kotlin') 31 | } 32 | } 33 | } 34 | 35 | configurations { 36 | integrationTestCompile.extendsFrom testCompile 37 | integrationTestRuntime.extendsFrom testRuntime 38 | } 39 | 40 | dependencies { 41 | compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version" 42 | testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version" 43 | testCompile "junit:junit:$junit_version" 44 | 45 | // Corda integration dependencies 46 | cordaCompile "net.corda:corda-core:$corda_release_version" 47 | cordaCompile "net.corda:corda-finance:$corda_release_version" 48 | cordaCompile "net.corda:corda-jackson:$corda_release_version" 49 | cordaCompile "net.corda:corda-rpc:$corda_release_version" 50 | cordaCompile "net.corda:corda-node-api:$corda_release_version" 51 | cordaCompile "net.corda:corda-webserver-impl:$corda_release_version" 52 | cordaRuntime "net.corda:corda:$corda_release_version" 53 | cordaRuntime "net.corda:corda-webserver:$corda_release_version" 54 | 55 | testCompile "net.corda:corda-node-driver:$corda_release_version" 56 | 57 | // CorDapp dependencies 58 | // Specify your CorDapp's dependencies below, including dependent CorDapps. 59 | // We've defined Cash as a dependent CorDapp as an example. 60 | cordapp "net.corda:corda-finance:$corda_release_version" 61 | } 62 | 63 | task integrationTest(type: Test, dependsOn: []) { 64 | testClassesDirs = sourceSets.integrationTest.output.classesDirs 65 | classpath = sourceSets.integrationTest.runtimeClasspath 66 | } 67 | 68 | tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { 69 | kotlinOptions { 70 | languageVersion = "1.1" 71 | apiVersion = "1.1" 72 | jvmTarget = "1.8" 73 | javaParameters = true // Useful for reflection. 74 | } 75 | } -------------------------------------------------------------------------------- /cordapp/src/integrationTest/kotlin/net/corda/demos/crowdFunding/DriverBasedTest.kt: -------------------------------------------------------------------------------- 1 | package net.corda.demos.crowdFunding 2 | 3 | import net.corda.core.utilities.getOrThrow 4 | import net.corda.node.services.transactions.SimpleNotaryService 5 | import net.corda.nodeapi.internal.ServiceInfo 6 | import net.corda.testing.DUMMY_BANK_A 7 | import net.corda.testing.DUMMY_BANK_B 8 | import net.corda.testing.DUMMY_NOTARY 9 | import net.corda.testing.driver.driver 10 | import org.junit.Assert 11 | import org.junit.Test 12 | 13 | class DriverBasedTest { 14 | @Test 15 | fun `run driver test`() { 16 | driver(isDebug = true, startNodesInProcess = true) { 17 | // This starts three nodes simultaneously with startNode, which returns a future that completes when the node 18 | // has completed startup. Then these are all resolved with getOrThrow which returns the NodeHandle list. 19 | val (notaryHandle, nodeAHandle, nodeBHandle) = listOf( 20 | startNode(providedName = DUMMY_NOTARY.name, advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type))), 21 | startNode(providedName = DUMMY_BANK_A.name), 22 | startNode(providedName = DUMMY_BANK_B.name) 23 | ).map { it.getOrThrow() } 24 | 25 | // This test will call via the RPC proxy to find a party of another node to verify that the nodes have 26 | // started and can communicate. This is a very basic test, in practice tests would be starting flows, 27 | // and verifying the states in the vault and other important metrics to ensure that your CorDapp is working 28 | // as intended. 29 | Assert.assertEquals(notaryHandle.rpc.wellKnownPartyFromX500Name(DUMMY_BANK_A.name)!!.name, DUMMY_BANK_A.name) 30 | Assert.assertEquals(nodeAHandle.rpc.wellKnownPartyFromX500Name(DUMMY_BANK_B.name)!!.name, DUMMY_BANK_B.name) 31 | Assert.assertEquals(nodeBHandle.rpc.wellKnownPartyFromX500Name(DUMMY_NOTARY.name)!!.name, DUMMY_NOTARY.name) 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /cordapp/src/main/kotlin/net/corda/demos/crowdFunding/Plugin.kt: -------------------------------------------------------------------------------- 1 | package net.corda.demos.crowdFunding 2 | 3 | import net.corda.core.serialization.SerializationWhitelist 4 | import net.corda.core.transactions.TransactionBuilder 5 | 6 | class Plugin : SerializationWhitelist { 7 | override val whitelist: List> 8 | get() = listOf( 9 | TransactionBuilder::class.java 10 | ) 11 | } -------------------------------------------------------------------------------- /cordapp/src/main/kotlin/net/corda/demos/crowdFunding/Utils.kt: -------------------------------------------------------------------------------- 1 | package net.corda.demos.crowdFunding 2 | 3 | import net.corda.core.contracts.ContractState 4 | import net.corda.core.contracts.StateAndRef 5 | import net.corda.core.node.ServiceHub 6 | import net.corda.core.node.services.Vault 7 | import net.corda.core.node.services.queryBy 8 | import net.corda.core.node.services.vault.QueryCriteria 9 | import net.corda.core.node.services.vault.builder 10 | import net.corda.demos.crowdFunding.structures.Campaign 11 | import net.corda.demos.crowdFunding.structures.Pledge 12 | import java.security.PublicKey 13 | 14 | /** Pick out all the pledges for the specified campaign. */ 15 | fun pledgersForCampaign(services: ServiceHub, campaign: Campaign): List> { 16 | val generalCriteria = QueryCriteria.VaultQueryCriteria(Vault.StateStatus.UNCONSUMED) 17 | return builder { 18 | val campaignReference = Pledge.PledgeSchemaV1.PledgeEntity::campaign_reference.equal(campaign.linearId.id.toString()) 19 | val customCriteria = QueryCriteria.VaultCustomQueryCriteria(campaignReference) 20 | val criteria = generalCriteria `and` customCriteria 21 | services.vaultService.queryBy(criteria) 22 | }.states 23 | } 24 | 25 | /** Return a set of PublicKeys from the list of participants of a state. */ 26 | fun keysFromParticipants(obligation: ContractState): Set { 27 | return obligation.participants.map { 28 | it.owningKey 29 | }.toSet() 30 | } -------------------------------------------------------------------------------- /cordapp/src/main/kotlin/net/corda/demos/crowdFunding/contracts/CampaignContract.kt: -------------------------------------------------------------------------------- 1 | package net.corda.demos.crowdFunding.contracts 2 | 3 | import net.corda.core.contracts.* 4 | import net.corda.core.contracts.Requirements.using 5 | import net.corda.core.transactions.LedgerTransaction 6 | import net.corda.demos.crowdFunding.keysFromParticipants 7 | import net.corda.demos.crowdFunding.structures.Campaign 8 | import net.corda.demos.crowdFunding.structures.Pledge 9 | import net.corda.finance.contracts.asset.Cash 10 | import java.security.PublicKey 11 | import java.time.Instant 12 | 13 | // TODO We need to improve this contract code so it works with confidential identities. 14 | class CampaignContract : Contract { 15 | 16 | companion object { 17 | @JvmStatic 18 | val CONTRACT_REF = "net.corda.demos.crowdFunding.contracts.CampaignContract" 19 | } 20 | 21 | interface Commands : CommandData 22 | class Start : TypeOnlyCommandData(), Commands 23 | class End : TypeOnlyCommandData(), Commands 24 | class AcceptPledge : TypeOnlyCommandData(), Commands 25 | 26 | override fun verify(tx: LedgerTransaction) { 27 | val campaignCommand = tx.commands.requireSingleCommand() 28 | val setOfSigners = campaignCommand.signers.toSet() 29 | 30 | when (campaignCommand.value) { 31 | is Start -> verifyStart(tx, setOfSigners) 32 | is End -> verifyEnd(tx, setOfSigners) 33 | is AcceptPledge -> verifyPledge(tx, setOfSigners) 34 | else -> throw IllegalArgumentException("Unrecognised command.") 35 | } 36 | } 37 | 38 | private fun verifyStart(tx: LedgerTransaction, signers: Set) = requireThat { 39 | // Assert we have the right amount and type of states. 40 | "No inputs should be consumed when starting a campaign." using (tx.inputStates.isEmpty()) 41 | "Only one campaign state should be created when starting a campaign." using (tx.outputStates.size == 1) 42 | // There can only be one output state and it must be a Campaign state. 43 | val campaign = tx.outputStates.single() as Campaign 44 | 45 | // Assert stuff over the state. 46 | "A newly issued campaign must have a positive target." using 47 | (campaign.target > Amount(0, campaign.target.token)) 48 | "A newly issued campaign must start with no pledges." using 49 | (campaign.raisedSoFar == Amount(0, campaign.target.token)) 50 | "The deadline must be in the future." using (campaign.deadline > Instant.now()) 51 | "There must be a campaign name." using (campaign.name != "") 52 | 53 | // Assert correct signers. 54 | "The campaign must be signed by the manager only." using (signers == keysFromParticipants(campaign)) 55 | } 56 | 57 | private fun verifyPledge(tx: LedgerTransaction, signers: Set) = requireThat { 58 | // Assert we have the right amount and type of states. 59 | "The can only one input state in an accept pledge transaction." using (tx.inputStates.size == 1) 60 | "There must be two output states in an accept pledge transaction." using (tx.outputStates.size == 2) 61 | val campaignInput = tx.inputsOfType().single() 62 | val campaignOutput = tx.outputsOfType().single() 63 | val pledgeOutput = tx.outputsOfType().single() 64 | 65 | // Assert stuff about the pledge in relation to the campaign state. 66 | val changeInAmountRaised = campaignOutput.raisedSoFar - campaignInput.raisedSoFar 67 | "The pledge must be for this campaign." using (pledgeOutput.campaignReference == campaignOutput.linearId) 68 | "The campaign must be updated by the amount pledged." using (pledgeOutput.amount == changeInAmountRaised) 69 | 70 | // Assert stuff cannot change in the campaign state. 71 | "The campaign name may not change when accepting a pledge." using (campaignInput.name == campaignOutput.name) 72 | "The campaign deadline may not change when accepting a pledge." using 73 | (campaignInput.deadline == campaignOutput.deadline) 74 | "The campaign manager may not change when accepting a pledge." using 75 | (campaignInput.manager == campaignOutput.manager) 76 | "The campaign reference (linearId) may not change when accepting a pledge." using 77 | (campaignInput.linearId == campaignOutput.linearId) 78 | "The campaign target may not change when accepting a pledge." using 79 | (campaignInput.target == campaignOutput.target) 80 | 81 | // Assert that we can't make any pledges after the deadline. 82 | tx.timeWindow?.midpoint?.let { 83 | "No pledges can be accepted after the deadline." using (it < campaignOutput.deadline) 84 | } ?: throw IllegalArgumentException("A timestamp is required when pledging to a campaign.") 85 | 86 | // Assert correct signer. 87 | "The campaign must be signed by the manager only." using (signers.single() == campaignOutput.manager.owningKey) 88 | } 89 | 90 | private fun verifyEnd(tx: LedgerTransaction, signers: Set) = requireThat { 91 | // Assert we have the right amount and type of states. 92 | "Only one campaign can end per transaction." using (tx.inputsOfType().size == 1) 93 | "There must be no campaign output states when ending a campaign." using (tx.outputsOfType().isEmpty()) 94 | "There must be no pledge output states when ending a campaign." using (tx.outputsOfType().isEmpty()) 95 | // Get references to all the pledge and campaign states. Might have multiple or zero pledges, who knows? 96 | val campaignInput = tx.inputsOfType().single() 97 | val pledgeInputs = tx.inputsOfType() 98 | val cashInputs = tx.inputsOfType() 99 | // Check there are states of no other types in this transaction. 100 | val totalInputStates = 1 + pledgeInputs.size + cashInputs.size 101 | "Un-required states have been added to this transaction." using (tx.inputs.size == totalInputStates) 102 | 103 | // Check we are not ending this campaign early. 104 | "The deadline must have passed before the campaign can be ended." using (campaignInput.deadline < Instant.now()) 105 | 106 | // Check to see how many pledges we received. 107 | val zero = Amount.zero(campaignInput.target.token) 108 | val sumOfAllPledges = pledgeInputs.map { (amount) -> amount }.fold(zero) { acc, curr -> acc + curr } 109 | "There is a mismatch between input pledge states and Campaign.raiseSoFar" using 110 | (campaignInput.raisedSoFar == sumOfAllPledges) 111 | 112 | // do different stuff depending on how many pledges we get. 113 | when { 114 | sumOfAllPledges == zero -> verifyNoPledges(tx) 115 | sumOfAllPledges < campaignInput.target -> verifyMissedTarget(tx) 116 | sumOfAllPledges >= campaignInput.target -> verifyHitTarget(tx, campaignInput, pledgeInputs) 117 | } 118 | 119 | // Check the campaign state is signed by the campaign manager. 120 | "Ending campaign transactions must be signed by the campaign manager." using 121 | (campaignInput.manager.owningKey in signers) 122 | } 123 | 124 | private fun verifyNoPledges(tx: LedgerTransaction) { 125 | "No pledges were raised so there should be no pledge inputs." using (tx.inputsOfType().isEmpty()) 126 | "No pledges were raised so there should be no cash inputs." using (tx.inputsOfType().isEmpty()) 127 | "No pledges were raised so there should be no cash outputs." using (tx.outputsOfType().isEmpty()) 128 | "There are disallowed input state types in this transaction." using (tx.inputs.size == 1) 129 | "There are disallowed output state types in this transaction." using (tx.outputs.isEmpty()) 130 | } 131 | 132 | private fun verifyMissedTarget(tx: LedgerTransaction) { 133 | val pledges = tx.inputsOfType() 134 | "Pledges were raised so there should be pledge inputs." using (pledges.isNotEmpty()) 135 | "Pledges were raised but we didn't hit the target so there should be no cash inputs." using 136 | (tx.inputsOfType().isEmpty()) 137 | "Pledges were raised but we didn't hit the target so there should be no cash outputs." using 138 | (tx.outputsOfType().isEmpty()) 139 | "There are disallowed input state types in this transaction." using (pledges.size + 1 == tx.inputs.size) 140 | "There are disallowed output state types in this transaction." using (tx.outputs.isEmpty()) 141 | } 142 | 143 | private fun verifyHitTarget(tx: LedgerTransaction, campaign: Campaign, pledges: List) { 144 | "Pledges were raised so there should be pledge inputs." using (pledges.isNotEmpty()) 145 | val cashOutputs = tx.outputsOfType().sortedBy { it.amount.withoutIssuer() } 146 | val cashPaidToManager = cashOutputs.filter { it.owner == campaign.manager }.sortedBy { it.amount.withoutIssuer() } 147 | "There must be a payment for each pledge." using (cashOutputs.size == cashPaidToManager.size) 148 | 149 | // All the cash payments should match up with the pledges. No more, no less. 150 | // Zip kills multiple birds with one stone. 151 | // It ensures that number of output cash states paid to campaign manager == number of cancelled pledges. 152 | val matchedPayments = pledges.zip(cashPaidToManager) { pledge, cash -> pledge.amount == cash.amount.withoutIssuer() } 153 | "At least one of the cash payments is of an incorrect value. " using (matchedPayments.all { true }) 154 | // The cash contract will assure amount of input cash == output cash and verify the correct signers. 155 | } 156 | } -------------------------------------------------------------------------------- /cordapp/src/main/kotlin/net/corda/demos/crowdFunding/contracts/PledgeContract.kt: -------------------------------------------------------------------------------- 1 | package net.corda.demos.crowdFunding.contracts 2 | 3 | import net.corda.core.contracts.* 4 | import net.corda.core.transactions.LedgerTransaction 5 | import net.corda.demos.crowdFunding.keysFromParticipants 6 | import net.corda.demos.crowdFunding.structures.Campaign 7 | import net.corda.demos.crowdFunding.structures.Pledge 8 | import java.security.PublicKey 9 | 10 | class PledgeContract : Contract { 11 | 12 | companion object { 13 | @JvmStatic 14 | val CONTRACT_REF = "net.corda.demos.crowdFunding.contracts.PledgeContract" 15 | } 16 | 17 | interface Commands : CommandData 18 | class Create : TypeOnlyCommandData(), Commands 19 | class Cancel : TypeOnlyCommandData(), Commands 20 | class Update : TypeOnlyCommandData(), Commands // TODO: Update pledge. 21 | 22 | override fun verify(tx: LedgerTransaction) { 23 | // We only need the pledge commands at this point to determine which part of the contract code to run. 24 | val pledgeCommand = tx.commands.requireSingleCommand() 25 | val setOfSigners = pledgeCommand.signers.toSet() 26 | 27 | when (pledgeCommand.value) { 28 | is Create -> verifyCreate(tx, setOfSigners) 29 | is Cancel -> verifyCancel(tx, setOfSigners) 30 | else -> throw IllegalArgumentException("Unrecognised command.") 31 | } 32 | } 33 | 34 | private fun verifyCreate(tx: LedgerTransaction, signers: Set) = requireThat { 35 | // Group pledges by campaign id. 36 | val pledgeStates = tx.groupStates(Pledge::class.java, { it.linearId }) 37 | "You can only create one pledge at a time." using (pledgeStates.size == 1) 38 | val campaignStates = tx.groupStates(Campaign::class.java, { it.linearId }) 39 | "A Pledge can only be created if there are campaign states present." using (campaignStates.isNotEmpty()) 40 | 41 | // Assert we have the right amount and type of states. 42 | val pledgeStatesGroup = pledgeStates.single() 43 | "No inputs should be consumed when creating a pledge." using (pledgeStatesGroup.inputs.isEmpty()) 44 | "Only one campaign state should be created when starting a campaign." using (pledgeStatesGroup.outputs.size == 1) 45 | val pledge = pledgeStatesGroup.outputs.single() 46 | 47 | // Assert stuff over the state. 48 | "You cannot pledge a zero amount." using (pledge.amount > Amount(0, pledge.amount.token)) 49 | 50 | // Assert correct signers. 51 | "The campaign must be signed by the manager and the pledger." using (signers == keysFromParticipants(pledge)) 52 | } 53 | 54 | private fun verifyCancel(tx: LedgerTransaction, signers: Set) = requireThat { 55 | // Group pledges by linear id. 56 | val pledgeGroups = tx.groupStates(Pledge::class.java, { it.linearId }) 57 | 58 | // Check that there is a campaign state present. If there is then the campaign contract code will be run as well. 59 | "A Pledge can only be cancelled if there is a campaign input state present." using 60 | (tx.inputsOfType().size == 1) 61 | val campaign = tx.inputsOfType().single() 62 | 63 | // Verify each pledge separately. 64 | pledgeGroups.forEach { (inputs, outputs) -> 65 | // Check there's only one output per group. 66 | "No outputs should be created when cancelling a pledge." using (outputs.isEmpty()) 67 | "There should be no duplicate pledge states." using (inputs.size == 1) 68 | val pledge = inputs.single() 69 | "You are cancelling a pledge for a different campaign!" using (pledge.campaignReference == campaign.linearId) 70 | 71 | // Assert correct signers (Only the campaign manager can cancel a pledge). 72 | "The cancel pledge transaction must be signed by the campaign manager of the campaign the pledge is for." using 73 | (campaign.manager.owningKey == signers.single()) 74 | } 75 | } 76 | } -------------------------------------------------------------------------------- /cordapp/src/main/kotlin/net/corda/demos/crowdFunding/flows/BroadcastTransaction.kt: -------------------------------------------------------------------------------- 1 | package net.corda.demos.crowdFunding.flows 2 | 3 | import co.paralleluniverse.fibers.Suspendable 4 | import net.corda.core.flows.FlowLogic 5 | import net.corda.core.flows.InitiatingFlow 6 | import net.corda.core.flows.SendTransactionFlow 7 | import net.corda.core.transactions.SignedTransaction 8 | 9 | /** 10 | * Filters out any notary identities and removes our identity, then broadcasts the [SignedTransaction] to all the 11 | * remaining identities. 12 | */ 13 | @InitiatingFlow 14 | class BroadcastTransaction(val stx: SignedTransaction) : FlowLogic() { 15 | 16 | @Suspendable 17 | override fun call() { 18 | // Get a list of all identities from the network map cache. 19 | val everyone = serviceHub.networkMapCache.allNodes.flatMap { it.legalIdentities } 20 | 21 | // Filter out the notary identities and remove our identity. 22 | val everyoneButMeAndNotary = everyone.filter { serviceHub.networkMapCache.isNotary(it).not() } - ourIdentity 23 | 24 | // Create a session for each remaining party. 25 | val sessions = everyoneButMeAndNotary.map { initiateFlow(it) } 26 | 27 | // Send the transaction to all the remaining parties. 28 | sessions.forEach { subFlow(SendTransactionFlow(it, stx)) } 29 | } 30 | 31 | } -------------------------------------------------------------------------------- /cordapp/src/main/kotlin/net/corda/demos/crowdFunding/flows/EndCampaign.kt: -------------------------------------------------------------------------------- 1 | package net.corda.demos.crowdFunding.flows 2 | 3 | import co.paralleluniverse.fibers.Suspendable 4 | import net.corda.core.contracts.Command 5 | import net.corda.core.contracts.ContractState 6 | import net.corda.core.contracts.StateRef 7 | import net.corda.core.flows.* 8 | import net.corda.core.identity.Party 9 | import net.corda.core.transactions.SignedTransaction 10 | import net.corda.core.transactions.TransactionBuilder 11 | import net.corda.core.utilities.unwrap 12 | import net.corda.demos.crowdFunding.contracts.CampaignContract 13 | import net.corda.demos.crowdFunding.contracts.PledgeContract 14 | import net.corda.demos.crowdFunding.pledgersForCampaign 15 | import net.corda.demos.crowdFunding.structures.Campaign 16 | import net.corda.demos.crowdFunding.structures.CampaignResult 17 | import net.corda.demos.crowdFunding.structures.CashStatesPayload 18 | import net.corda.finance.contracts.asset.CASH_PROGRAM_ID 19 | import net.corda.finance.contracts.asset.Cash 20 | 21 | /** 22 | * This pair of flows deals with ending the campaign, whether it successfully reaches its target or not. If the 23 | * campaign reaches the target then the manager sends a [CampaignResult.Success] object to all the pledgers, asking them 24 | * to provide cash states equal to the pledge they previously made. They send back the cash states to the manager who 25 | * assembles the transaction which exits the campaign and pledge states, and transfers the cash to the campaign manager. 26 | */ 27 | object EndCampaign { 28 | 29 | @SchedulableFlow 30 | @InitiatingFlow 31 | class Initiator(private val stateRef: StateRef) : FlowLogic() { 32 | 33 | /** 34 | * Sends a Success message to each one of the pledgers. In response, they send back cash states equal to the 35 | * amount which they previously pledged. We also need to receive the stateRefs from them, so we can verify the 36 | * transaction that we end up building. 37 | * */ 38 | @Suspendable 39 | fun requestPledgedCash(sessions: List): CashStatesPayload { 40 | // Send a request to each pledger and get the dependency transactions as well. 41 | val cashStates = sessions.map { session -> 42 | // Generate a new anonymous key for each payer. 43 | // Send "Success" message. 44 | session.send(CampaignResult.Success(stateRef)) 45 | // Resolve transactions for the given StateRefs. 46 | subFlow(ReceiveStateAndRefFlow(session)) 47 | // Receive the cash inputs, outputs and public keys. 48 | session.receive().unwrap { it } 49 | } 50 | 51 | // Return all of the collected states and keys. 52 | return CashStatesPayload( 53 | cashStates.flatMap { it.inputs }, 54 | cashStates.flatMap { it.outputs }, 55 | cashStates.flatMap { it.signingKeys } 56 | ) 57 | } 58 | 59 | /** Common stuff that happens whether we meet the target of not. */ 60 | private fun cancelPledges(campaign: Campaign): TransactionBuilder { 61 | // Pick a notary. Don't care which one. 62 | val notary: Party = serviceHub.networkMapCache.notaryIdentities.first() 63 | val utx = TransactionBuilder(notary = notary) 64 | 65 | // Create inputs. 66 | val pledgerStateAndRefs = pledgersForCampaign(serviceHub, campaign) 67 | val campaignInputStateAndRef = serviceHub.toStateAndRef(stateRef) 68 | 69 | // Create commands. 70 | val endCampaignCommand = Command(CampaignContract.End(), campaign.manager.owningKey) 71 | val cancelPledgeCommand = Command(PledgeContract.Cancel(), campaign.manager.owningKey) 72 | 73 | // Add the above 74 | pledgerStateAndRefs.forEach { utx.addInputState(it) } 75 | utx.addInputState(campaignInputStateAndRef) 76 | utx.addCommand(endCampaignCommand) 77 | utx.addCommand(cancelPledgeCommand) 78 | 79 | return utx 80 | } 81 | 82 | /** Do the common stuff then request the pledged cash. Once we have all the states, put them in the builder. */ 83 | @Suspendable 84 | fun handleSuccess(campaign: Campaign, sessions: List): TransactionBuilder { 85 | // Do the stuff we must do anyway. 86 | val utx = cancelPledges(campaign) 87 | 88 | // Gather the cash states from the pledgers. 89 | val cashStates = requestPledgedCash(sessions) 90 | 91 | // Add the cash inputs, outputs and command. 92 | cashStates.inputs.forEach { utx.addInputState(it) } 93 | cashStates.outputs.forEach { utx.addOutputState(it, CASH_PROGRAM_ID) } 94 | utx.addCommand(Cash.Commands.Move(), cashStates.signingKeys) 95 | 96 | return utx 97 | } 98 | 99 | @Suspendable 100 | override fun call(): SignedTransaction { 101 | // Get the actual state from the ref. 102 | val campaign = serviceHub.loadState(stateRef).data as Campaign 103 | 104 | // As all nodes have the campaign state, all will try to start this flow. Abort for all but the manger. 105 | if (campaign.manager != ourIdentity) { 106 | throw FlowException("Only the campaign manager can run this flow.") 107 | } 108 | 109 | // Get the pledges for this campaign. Remember, everyone has a copy of them. 110 | val pledgersForCampaign = pledgersForCampaign(serviceHub, campaign) 111 | 112 | // Create flow sessions for all pledgers. 113 | val sessions = pledgersForCampaign.map { (state) -> 114 | val pledger = serviceHub.identityService.requireWellKnownPartyFromAnonymous(state.data.pledger) 115 | initiateFlow(pledger) 116 | } 117 | 118 | // Do different things depending on whether we've raised enough, or not. 119 | val utx = when { 120 | campaign.raisedSoFar < campaign.target -> { 121 | sessions.forEach { session -> session.send(CampaignResult.Failure()) } 122 | cancelPledges(campaign) 123 | } 124 | else -> handleSuccess(campaign, sessions) 125 | } 126 | 127 | // Sign, finalise and distribute the transaction. 128 | val ptx = serviceHub.signInitialTransaction(utx) 129 | val stx = subFlow(CollectSignaturesFlow(ptx, sessions.map { it })) 130 | val ftx = subFlow(FinalityFlow(stx)) 131 | 132 | // Broadcast this transaction to all the other nodes on the business network. 133 | subFlow(BroadcastTransaction(ftx)) 134 | 135 | return ftx 136 | } 137 | 138 | } 139 | 140 | @InitiatedBy(Initiator::class) 141 | class Responder(val otherSession: FlowSession) : FlowLogic() { 142 | 143 | @Suspendable 144 | fun handleSuccess(campaignRef: StateRef) { 145 | // Get our Pledge state for this campaign. 146 | val campaign = serviceHub.loadState(campaignRef).data as Campaign 147 | val results = pledgersForCampaign(serviceHub, campaign) 148 | 149 | // Find our pledge. We have to do this as we have ALL the pledges for this campaign in our vault. 150 | // This is because ReceiveTransactionFlow only allows us to record the WHOLE SignedTransaction and not a 151 | // filtered transaction. In an ideal World, we would be able to send a filtered transaction that only shows 152 | // the Campaign state and not the pledge states, so we would ONLY ever have our Pledge states in the vault. 153 | // From a privacy perspective, this doesn't matter, though. As all the pledgers generate random keys. 154 | val amount = results.single { (state) -> 155 | serviceHub.identityService.wellKnownPartyFromAnonymous(state.data.pledger) == ourIdentity 156 | }.state.data.amount 157 | 158 | // Using generate spend is the best way to get cash states to spend. 159 | val (utx, _) = Cash.generateSpend(serviceHub, TransactionBuilder(), amount, campaign.manager) 160 | 161 | // The Cash contract design won't allow more than one move command per transaction. As, we are collecting 162 | // cash from potentially multiple parties, we have pull out the items from the transaction builder so we 163 | // can add them back to the OTHER transaction builder but with only ONE move command. 164 | val inputStateAndRefs = utx.inputStates().map { serviceHub.toStateAndRef(it) } 165 | val outputStates = utx.outputStates().map { it.data as Cash.State } 166 | val signingKeys = utx.commands().flatMap { it.signers } 167 | 168 | // We need to send the cash state dependency transactions so the manager can verify the tx proposal. 169 | subFlow(SendStateAndRefFlow(otherSession, inputStateAndRefs)) 170 | 171 | // Send the payload back to the campaign manager. 172 | val pledgedCashStates = CashStatesPayload(inputStateAndRefs, outputStates, signingKeys) 173 | otherSession.send(pledgedCashStates) 174 | } 175 | 176 | @Suspendable 177 | override fun call() { 178 | val campaignResult = otherSession.receive().unwrap { it } 179 | 180 | when (campaignResult) { 181 | is CampaignResult.Success -> handleSuccess(campaignResult.campaignRef) 182 | is CampaignResult.Failure -> return 183 | } 184 | 185 | val flow = object : SignTransactionFlow(otherSession) { 186 | override fun checkTransaction(stx: SignedTransaction) = Unit // TODO 187 | } 188 | 189 | subFlow(flow) 190 | } 191 | 192 | } 193 | 194 | } -------------------------------------------------------------------------------- /cordapp/src/main/kotlin/net/corda/demos/crowdFunding/flows/MakePledge.kt: -------------------------------------------------------------------------------- 1 | package net.corda.demos.crowdFunding.flows 2 | 3 | import co.paralleluniverse.fibers.Suspendable 4 | import net.corda.confidential.IdentitySyncFlow 5 | import net.corda.core.contracts.Amount 6 | import net.corda.core.contracts.Command 7 | import net.corda.core.contracts.StateAndContract 8 | import net.corda.core.contracts.UniqueIdentifier 9 | import net.corda.core.flows.* 10 | import net.corda.core.identity.Party 11 | import net.corda.core.node.services.queryBy 12 | import net.corda.core.node.services.vault.QueryCriteria 13 | import net.corda.core.transactions.SignedTransaction 14 | import net.corda.core.transactions.TransactionBuilder 15 | import net.corda.core.utilities.seconds 16 | import net.corda.core.utilities.unwrap 17 | import net.corda.demos.crowdFunding.contracts.CampaignContract 18 | import net.corda.demos.crowdFunding.contracts.PledgeContract 19 | import net.corda.demos.crowdFunding.structures.Campaign 20 | import net.corda.demos.crowdFunding.structures.Pledge 21 | import java.time.Instant 22 | import java.util.* 23 | 24 | /** 25 | * This pair of flows handles the pledging of money to a specific crowd funding campaign. A pledge is always initiated 26 | * by the pledger. We can do it this way because the campaign manager broadcasts the campaign state to all parties 27 | * on the crowd funding business network. 28 | */ 29 | object MakePledge { 30 | 31 | /** 32 | * Takes an amount of currency and a campaign reference then creates a new pledge state and updates the existing 33 | * campaign state to reflect the new pledge. 34 | */ 35 | @StartableByRPC 36 | @InitiatingFlow 37 | class Initiator( 38 | private val amount: Amount, 39 | private val campaignReference: UniqueIdentifier, 40 | private val broadcastToObservers: Boolean 41 | ) : FlowLogic() { 42 | 43 | @Suspendable 44 | override fun call(): SignedTransaction { 45 | // Pick a notary. Don't care which one. 46 | val notary: Party = serviceHub.networkMapCache.notaryIdentities.first() 47 | 48 | // Get the Campaign state corresponding to the provided ID from our vault. 49 | val queryCriteria = QueryCriteria.LinearStateQueryCriteria(linearId = listOf(campaignReference)) 50 | val campaignInputStateAndRef = serviceHub.vaultService.queryBy(queryCriteria).states.single() 51 | val campaignState = campaignInputStateAndRef.state.data 52 | 53 | // Generate a new key and cert, so the other pledgers don't know who we are. 54 | val me = serviceHub.keyManagementService.freshKeyAndCert( 55 | ourIdentityAndCert, 56 | revocationEnabled = false 57 | ).party.anonymise() 58 | 59 | // Assemble the other transaction components. 60 | // Commands: 61 | // We need a Create Pledge command and a Campaign Pledge command, as well as the Campaign input + output and 62 | // the new Pledge output state. The new pledge needs to be signed by the campaign manager and the pledger as 63 | // it is a bi-lateral agreement. The pledge to campaign command only needs to be signed by the campaign 64 | // manager. Either way, both the manager and the pledger need to sign this transaction. 65 | val acceptPledgeCommand = Command(CampaignContract.AcceptPledge(), campaignState.manager.owningKey) 66 | val createPledgeCommand = Command(PledgeContract.Create(), listOf(me.owningKey, campaignState.manager.owningKey)) 67 | 68 | // Output states: 69 | // We create a new pledge state that reflects the requested amount, referencing the campaign Id we want to 70 | // pledge money to. We then need to update the amount of money this campaign has raised and add the linear 71 | // id of the new pledge to the campaign. 72 | val pledgeOutputState = Pledge(amount, me, campaignState.manager, campaignReference) 73 | val pledgeOutputStateAndContract = StateAndContract(pledgeOutputState, PledgeContract.CONTRACT_REF) 74 | val newRaisedSoFar = campaignState.raisedSoFar + amount 75 | val campaignOutputState = campaignState.copy(raisedSoFar = newRaisedSoFar) 76 | val campaignOutputStateAndContract = StateAndContract(campaignOutputState, CampaignContract.CONTRACT_REF) 77 | 78 | // Build the transaction. 79 | val utx = TransactionBuilder(notary = notary).withItems( 80 | pledgeOutputStateAndContract, // Output 81 | campaignOutputStateAndContract, // Output 82 | campaignInputStateAndRef, // Input 83 | acceptPledgeCommand, // Command 84 | createPledgeCommand // Command 85 | ) 86 | 87 | // Set the time for when this transaction happened. 88 | utx.setTimeWindow(Instant.now(), 30.seconds) 89 | 90 | // Sign, sync identities, finalise and record the transaction. 91 | val ptx = serviceHub.signInitialTransaction(builder = utx, signingPubKeys = listOf(me.owningKey)) 92 | val session = initiateFlow(campaignState.manager) 93 | subFlow(IdentitySyncFlow.Send(otherSide = session, tx = ptx.tx)) 94 | val stx = subFlow(CollectSignaturesFlow(ptx, setOf(session), listOf(me.owningKey))) 95 | val ftx = subFlow(FinalityFlow(stx)) 96 | 97 | // Let the campaign manager know whether we want to broadcast this update to observers, or not. 98 | session.sendAndReceive(broadcastToObservers) 99 | 100 | return ftx 101 | } 102 | 103 | } 104 | 105 | /** 106 | * This side is only run by the campaign manager who checks the proposed pledge then waits for the pledge 107 | * transaction to be committed and broadcasts it to all the parties on the business network. 108 | */ 109 | @InitiatedBy(Initiator::class) 110 | class Responder(val otherSession: FlowSession) : FlowLogic() { 111 | 112 | @Suspendable 113 | override fun call() { 114 | subFlow(IdentitySyncFlow.Receive(otherSideSession = otherSession)) 115 | 116 | // As the manager, we might want to do some checking of the pledge before we sign it. 117 | val flow = object : SignTransactionFlow(otherSession) { 118 | override fun checkTransaction(stx: SignedTransaction) = Unit // TODO: Add some checks here. 119 | } 120 | 121 | val stx = subFlow(flow) 122 | 123 | // Once the transaction has been committed then we then broadcast from the manager so we don't compromise 124 | // the confidentiality of the pledging identities, if they choose to be anonymous. 125 | // Only do this if the pledger asks it to be done, though. 126 | val broadcastToObservers = otherSession.receive().unwrap { it } 127 | if (broadcastToObservers) { 128 | val ftx = waitForLedgerCommit(stx.id) 129 | subFlow(BroadcastTransaction(ftx)) 130 | } 131 | 132 | // We want the other side to block or at least wait a while for the transaction to be broadcast. 133 | otherSession.send(Unit) 134 | } 135 | 136 | } 137 | 138 | } 139 | 140 | -------------------------------------------------------------------------------- /cordapp/src/main/kotlin/net/corda/demos/crowdFunding/flows/RecordTransactionAsObserver.kt: -------------------------------------------------------------------------------- 1 | package net.corda.demos.crowdFunding.flows 2 | 3 | import co.paralleluniverse.fibers.Suspendable 4 | import net.corda.core.flows.FlowLogic 5 | import net.corda.core.flows.FlowSession 6 | import net.corda.core.flows.InitiatedBy 7 | import net.corda.core.flows.ReceiveTransactionFlow 8 | import net.corda.core.node.StatesToRecord 9 | 10 | /** 11 | * Other side of the [BroadcastTransaction] flow. It uses the observable states feature. When [ReceiveTransactionFlow] 12 | * is called, the [StatesToRecord.ALL_VISIBLE] parameter is used so that all the states are recorded despite the 13 | * receiving node not being a participant in these states. 14 | * 15 | * WARNING: This feature still needs work. Storing fungible states, like cash when you are not the owner will cause 16 | * problems when using [generateSpend] as the vault currently assumes that all states in the vault are spendable. States 17 | * you are only an observer of are NOT spendable! 18 | */ 19 | @InitiatedBy(BroadcastTransaction::class) 20 | class RecordTransactionAsObserver(val otherSession: FlowSession) : FlowLogic() { 21 | 22 | @Suspendable 23 | override fun call() { 24 | // Receive and record the new campaign state in our vault EVEN THOUGH we are not a participant as we are 25 | // using 'ALL_VISIBLE'. 26 | val flow = ReceiveTransactionFlow( 27 | otherSideSession = otherSession, 28 | checkSufficientSignatures = true, 29 | statesToRecord = StatesToRecord.ALL_VISIBLE 30 | ) 31 | 32 | subFlow(flow) 33 | } 34 | 35 | } -------------------------------------------------------------------------------- /cordapp/src/main/kotlin/net/corda/demos/crowdFunding/flows/StartCampaign.kt: -------------------------------------------------------------------------------- 1 | package net.corda.demos.crowdFunding.flows 2 | 3 | import co.paralleluniverse.fibers.Suspendable 4 | import net.corda.core.contracts.Command 5 | import net.corda.core.contracts.StateAndContract 6 | import net.corda.core.flows.FinalityFlow 7 | import net.corda.core.flows.FlowLogic 8 | import net.corda.core.flows.StartableByRPC 9 | import net.corda.core.identity.Party 10 | import net.corda.core.transactions.SignedTransaction 11 | import net.corda.core.transactions.TransactionBuilder 12 | import net.corda.demos.crowdFunding.contracts.CampaignContract 13 | import net.corda.demos.crowdFunding.structures.Campaign 14 | 15 | /** 16 | * A flow that handles the starting of a new campaigns. It creates a new [Campaign] and stores it in in the vault of the 17 | * node that runs this flow (the manager of the campaign) and then broadcasts it to all the other nodes on the 18 | * crowdFunding business network. 19 | * 20 | * The nodes receiving the broadcast use the observable states feature by recording all visible output states despite 21 | * the fact the only participant for the [Campaign] start is the [manager] of the [Campaign]. 22 | */ 23 | @StartableByRPC 24 | class StartCampaign(private val newCampaign: Campaign) : FlowLogic() { 25 | 26 | @Suspendable 27 | override fun call(): SignedTransaction { 28 | // Pick a notary. Don't care which one. 29 | val notary: Party = serviceHub.networkMapCache.notaryIdentities.first() 30 | 31 | // Assemble the transaction components. 32 | val startCommand = Command(CampaignContract.Start(), listOf(ourIdentity.owningKey)) 33 | val outputState = StateAndContract(newCampaign, CampaignContract.CONTRACT_REF) 34 | 35 | // Build, sign and record the transaction. 36 | val utx = TransactionBuilder(notary = notary).withItems(outputState, startCommand) 37 | val stx = serviceHub.signInitialTransaction(utx) 38 | val ftx = subFlow(FinalityFlow(stx)) 39 | 40 | // Broadcast this transaction to all parties on this business network. 41 | subFlow(BroadcastTransaction(ftx)) 42 | 43 | return ftx 44 | } 45 | 46 | } -------------------------------------------------------------------------------- /cordapp/src/main/kotlin/net/corda/demos/crowdFunding/structures/Campaign.kt: -------------------------------------------------------------------------------- 1 | package net.corda.demos.crowdFunding.structures 2 | 3 | import net.corda.core.contracts.* 4 | import net.corda.core.flows.FlowLogicRefFactory 5 | import net.corda.core.identity.AbstractParty 6 | import net.corda.core.identity.Party 7 | import net.corda.demos.crowdFunding.flows.EndCampaign 8 | import java.time.Instant 9 | import java.util.* 10 | 11 | data class Campaign( 12 | val name: String, 13 | val manager: Party, 14 | val target: Amount, 15 | val deadline: Instant, 16 | val raisedSoFar: Amount = Amount(0, target.token), 17 | override val participants: List = listOf(manager), 18 | override val linearId: UniqueIdentifier = UniqueIdentifier() 19 | ) : LinearState, SchedulableState { 20 | override fun nextScheduledActivity(thisStateRef: StateRef, flowLogicRefFactory: FlowLogicRefFactory): ScheduledActivity? { 21 | return ScheduledActivity(flowLogicRefFactory.create(EndCampaign.Initiator::class.java, thisStateRef), deadline) 22 | } 23 | } -------------------------------------------------------------------------------- /cordapp/src/main/kotlin/net/corda/demos/crowdFunding/structures/CampaignResult.kt: -------------------------------------------------------------------------------- 1 | package net.corda.demos.crowdFunding.structures 2 | 3 | import net.corda.core.contracts.StateRef 4 | import net.corda.core.serialization.CordaSerializable 5 | 6 | @CordaSerializable 7 | sealed class CampaignResult { 8 | class Success(val campaignRef: StateRef) : CampaignResult() 9 | class Failure : CampaignResult() 10 | } -------------------------------------------------------------------------------- /cordapp/src/main/kotlin/net/corda/demos/crowdFunding/structures/CashStatesPayload.kt: -------------------------------------------------------------------------------- 1 | package net.corda.demos.crowdFunding.structures 2 | 3 | import net.corda.core.contracts.StateAndRef 4 | import net.corda.core.serialization.CordaSerializable 5 | import net.corda.finance.contracts.asset.Cash 6 | import java.security.PublicKey 7 | 8 | /** 9 | * A payload to transport the cash states from the pledger to the manager. We have to do this due to the way the [Cash] 10 | * contract is currently implemented. It can only have one move command and it is impossible to remove commands from 11 | */ 12 | 13 | @CordaSerializable 14 | class CashStatesPayload( 15 | val inputs: List>, 16 | val outputs: List, 17 | val signingKeys: List 18 | ) -------------------------------------------------------------------------------- /cordapp/src/main/kotlin/net/corda/demos/crowdFunding/structures/Pledge.kt: -------------------------------------------------------------------------------- 1 | package net.corda.demos.crowdFunding.structures 2 | 3 | import net.corda.core.contracts.Amount 4 | import net.corda.core.contracts.LinearState 5 | import net.corda.core.contracts.UniqueIdentifier 6 | import net.corda.core.identity.AbstractParty 7 | import net.corda.core.identity.Party 8 | import net.corda.core.schemas.MappedSchema 9 | import net.corda.core.schemas.PersistentState 10 | import net.corda.core.schemas.QueryableState 11 | import java.util.* 12 | import javax.persistence.Column 13 | import javax.persistence.Entity 14 | import javax.persistence.Lob 15 | import javax.persistence.Table 16 | 17 | data class Pledge( 18 | val amount: Amount, 19 | val pledger: AbstractParty, 20 | val manager: Party, 21 | val campaignReference: UniqueIdentifier, 22 | override val participants: List = listOf(pledger, manager), 23 | override val linearId: UniqueIdentifier = UniqueIdentifier() 24 | ) : LinearState, QueryableState { 25 | override fun supportedSchemas() = listOf(PledgeSchemaV1) 26 | override fun generateMappedObject(schema: MappedSchema) = PledgeSchemaV1.PledgeEntity(this) 27 | 28 | object PledgeSchemaV1 : MappedSchema(Pledge::class.java, 1, listOf(PledgeEntity::class.java)) { 29 | @Entity 30 | @Table(name = "pledges") 31 | class PledgeEntity(pledge: Pledge) : PersistentState() { 32 | @Column 33 | var currency: String = pledge.amount.token.toString() 34 | @Column 35 | var amount: Long = pledge.amount.quantity 36 | @Column 37 | @Lob 38 | var pledger: ByteArray = pledge.pledger.owningKey.encoded 39 | @Column 40 | @Lob 41 | var manager: ByteArray = pledge.manager.owningKey.encoded 42 | @Column 43 | var campaign_reference: String = pledge.campaignReference.id.toString() 44 | @Column 45 | var linear_id: String = pledge.linearId.id.toString() 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /cordapp/src/main/resources/META-INF/services/net.corda.core.serialization.SerializationWhitelist: -------------------------------------------------------------------------------- 1 | # Register here any serialization whitelists for 3rd party classes extending from net.corda.core.serialization.SerializationWhitelist 2 | net.corda.demos.crowdFunding.Plugin 3 | -------------------------------------------------------------------------------- /cordapp/src/main/resources/certificates/readme.txt: -------------------------------------------------------------------------------- 1 | These certificates are used for development mode only. -------------------------------------------------------------------------------- /cordapp/src/main/resources/certificates/sslkeystore.jks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roger-that-dev/observable-states/b047e746d4d04649606c8f0dfa3a229acbc08567/cordapp/src/main/resources/certificates/sslkeystore.jks -------------------------------------------------------------------------------- /cordapp/src/main/resources/certificates/truststore.jks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roger-that-dev/observable-states/b047e746d4d04649606c8f0dfa3a229acbc08567/cordapp/src/main/resources/certificates/truststore.jks -------------------------------------------------------------------------------- /cordapp/src/test/kotlin/net/corda/demos/crowdFunding/NodeDriver.kt: -------------------------------------------------------------------------------- 1 | package net.corda.demos.crowdFunding 2 | 3 | import net.corda.core.identity.CordaX500Name 4 | import net.corda.core.utilities.getOrThrow 5 | import net.corda.node.services.transactions.ValidatingNotaryService 6 | import net.corda.nodeapi.User 7 | import net.corda.nodeapi.internal.ServiceInfo 8 | import net.corda.testing.driver.driver 9 | 10 | /** 11 | * This file is exclusively for being able to run your nodes through an IDE (as opposed to using deployNodes) 12 | * Do not use in a production environment. 13 | * 14 | * To debug your CorDapp: 15 | * 16 | * 1. Run the "Run Template CorDapp" run configuration. 17 | * 2. Wait for all the nodes to start. 18 | * 3. Note the debug ports for each node, which should be output to the console. The "Debug CorDapp" configuration runs 19 | * with port 5007, which should be "PartyA". In any case, double-check the console output to be sure. 20 | * 4. Set your breakpoints in your CorDapp code. 21 | * 5. Run the "Debug CorDapp" remote debug run configuration. 22 | */ 23 | fun main(args: Array) { 24 | // No permissions required as we are not invoking flows. 25 | val user = User("user1", "test", permissions = setOf()) 26 | driver(isDebug = true) { 27 | val (_, nodeA, nodeB) = listOf( 28 | startNode(providedName = CordaX500Name("Controller", "London", "GB"), advertisedServices = setOf(ServiceInfo(ValidatingNotaryService.type))), 29 | startNode(providedName = CordaX500Name("PartyA", "London", "GB"), rpcUsers = listOf(user)), 30 | startNode(providedName = CordaX500Name("PartyB", "New York", "US"), rpcUsers = listOf(user))).map { it.getOrThrow() } 31 | 32 | startWebserver(nodeA) 33 | startWebserver(nodeB) 34 | 35 | waitForAllNodesToFinish() 36 | } 37 | } -------------------------------------------------------------------------------- /cordapp/src/test/kotlin/net/corda/demos/crowdFunding/contracts/CampaignTests.kt: -------------------------------------------------------------------------------- 1 | package net.corda.demos.crowdFunding.contracts 2 | 3 | import net.corda.core.contracts.UniqueIdentifier 4 | import net.corda.core.crypto.entropyToKeyPair 5 | import net.corda.core.identity.CordaX500Name 6 | import net.corda.core.identity.Party 7 | import net.corda.core.utilities.OpaqueBytes 8 | import net.corda.core.utilities.hours 9 | import net.corda.core.utilities.seconds 10 | import net.corda.demos.crowdFunding.structures.Campaign 11 | import net.corda.demos.crowdFunding.structures.Pledge 12 | import net.corda.finance.DOLLARS 13 | import net.corda.finance.POUNDS 14 | import net.corda.finance.contracts.asset.CASH_PROGRAM_ID 15 | import net.corda.finance.contracts.asset.Cash 16 | import net.corda.testing.contracts.DUMMY_PROGRAM_ID 17 | import net.corda.testing.contracts.DummyState 18 | import net.corda.testing.ledger 19 | import net.corda.testing.setCordappPackages 20 | import net.corda.testing.unsetCordappPackages 21 | import org.junit.After 22 | import org.junit.Before 23 | import org.junit.Test 24 | import java.math.BigInteger 25 | import java.security.KeyPair 26 | import java.time.Instant 27 | 28 | class CampaignTests { 29 | 30 | private val issuer: Party = createParty("Issuer", 0) 31 | private val A: Party = createParty("A", 1) 32 | private val B: Party = createParty("B", 2) 33 | private val C: Party = createParty("C", 3) 34 | private val D: Party = createParty("D", 4) 35 | private val E: Party = createParty("E", 5) 36 | 37 | val defaultRef = OpaqueBytes(ByteArray(1, { 1 })) 38 | val defaultIssuer = issuer.ref(defaultRef) 39 | 40 | private fun createParty(name: String, random: Long): Party { 41 | val key: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(random)) } 42 | val identity = CordaX500Name(organisation = "Party$name", locality = "TestLand", country = "GB") 43 | return Party(identity, key.public) 44 | } 45 | 46 | private fun partyKeys(vararg parties: Party) = parties.map { it.owningKey } 47 | 48 | @Before 49 | fun before() { 50 | setCordappPackages( 51 | "net.corda.testing.contracts", 52 | "net.corda.demos.crowdFunding", 53 | "net.corda.finance" 54 | ) 55 | } 56 | 57 | @After 58 | fun after() { 59 | unsetCordappPackages() 60 | } 61 | 62 | private val oneHourFromNow = Instant.now() + 1.hours 63 | 64 | /** A failed campaign with no pledges that ends now. */ 65 | private val failedCampaignWithNoPledges = Campaign( 66 | name = "Roger's Campaign", 67 | manager = A, 68 | target = 1000.POUNDS, 69 | deadline = oneHourFromNow 70 | ) 71 | 72 | private val newValidCampaign = Campaign( 73 | name = "Roger's Campaign", 74 | manager = A, 75 | target = 1000.POUNDS, 76 | deadline = oneHourFromNow 77 | ) 78 | 79 | @Test 80 | fun `Start new campaign tests`() { 81 | ledger { 82 | // Valid start new campaign transaction. 83 | transaction { 84 | output(CampaignContract.CONTRACT_REF) { newValidCampaign } 85 | command(*partyKeys(A).toTypedArray()) { CampaignContract.Start() } 86 | this.verifies() 87 | } 88 | 89 | // Signers incorrect. 90 | transaction { 91 | output(CampaignContract.CONTRACT_REF) { newValidCampaign } 92 | command(*partyKeys(B, A).toTypedArray()) { CampaignContract.Start() } 93 | this.fails() 94 | } 95 | 96 | // Signers incorrect. 97 | transaction { 98 | output(CampaignContract.CONTRACT_REF) { newValidCampaign } 99 | command(*partyKeys(B, A).toTypedArray()) { CampaignContract.Start() } 100 | this.fails() 101 | } 102 | 103 | // Incorrect inputs / outputs. 104 | transaction { 105 | input(DUMMY_PROGRAM_ID) { DummyState() } 106 | output(CampaignContract.CONTRACT_REF) { newValidCampaign } 107 | command(*partyKeys(A).toTypedArray()) { CampaignContract.Start() } 108 | this.fails() 109 | } 110 | 111 | // Incorrect inputs / outputs. 112 | transaction { 113 | output(DUMMY_PROGRAM_ID) { DummyState() } 114 | output(CampaignContract.CONTRACT_REF) { newValidCampaign } 115 | command(*partyKeys(A).toTypedArray()) { CampaignContract.Start() } 116 | this.fails() 117 | } 118 | 119 | // Zero amount. 120 | transaction { 121 | output(CampaignContract.CONTRACT_REF) { Campaign("Test", A, 0.POUNDS, oneHourFromNow) } 122 | command(*partyKeys(A).toTypedArray()) { CampaignContract.Start() } 123 | this.fails() 124 | } 125 | 126 | // Deadline not in future. 127 | transaction { 128 | output(CampaignContract.CONTRACT_REF) { Campaign("Test", A, 0.POUNDS, Instant.now()) } 129 | command(*partyKeys(A).toTypedArray()) { CampaignContract.Start() } 130 | this.fails() 131 | } 132 | 133 | // No name. 134 | transaction { 135 | output(CampaignContract.CONTRACT_REF) { Campaign("", A, 0.POUNDS, Instant.now()) } 136 | command(*partyKeys(A).toTypedArray()) { CampaignContract.Start() } 137 | this.fails() 138 | } 139 | 140 | // Raised so far, not zero. 141 | transaction { 142 | output(CampaignContract.CONTRACT_REF) { Campaign("Test", A, 0.POUNDS, Instant.now(), 10.POUNDS) } 143 | command(*partyKeys(A).toTypedArray()) { CampaignContract.Start() } 144 | this.fails() 145 | } 146 | } 147 | } 148 | 149 | @Test 150 | fun `Make a pledge tests`() { 151 | val defaultPledge = Pledge(100.POUNDS, B, A, newValidCampaign.linearId) 152 | 153 | ledger { 154 | // Valid make pledge transaction. 155 | transaction { 156 | input(CampaignContract.CONTRACT_REF) { newValidCampaign } 157 | output(CampaignContract.CONTRACT_REF) { newValidCampaign.copy(raisedSoFar = newValidCampaign.raisedSoFar + defaultPledge.amount) } 158 | output(PledgeContract.CONTRACT_REF) { defaultPledge } 159 | command(*partyKeys(A, B).toTypedArray()) { PledgeContract.Create() } 160 | command(*partyKeys(A).toTypedArray()) { CampaignContract.AcceptPledge() } 161 | timeWindow(Instant.now(), 5.seconds) 162 | this.verifies() 163 | } 164 | 165 | // Amounts don't match up. 166 | transaction { 167 | input(CampaignContract.CONTRACT_REF) { newValidCampaign } 168 | output(CampaignContract.CONTRACT_REF) { newValidCampaign.copy(raisedSoFar = 200.POUNDS) } // Wrong amount. 169 | output(PledgeContract.CONTRACT_REF) { defaultPledge } 170 | command(*partyKeys(A, B).toTypedArray()) { PledgeContract.Create() } 171 | command(*partyKeys(A).toTypedArray()) { CampaignContract.AcceptPledge() } 172 | timeWindow(Instant.now(), 5.seconds) 173 | this.fails() 174 | } 175 | 176 | // Wrong currency. 177 | transaction { 178 | input(CampaignContract.CONTRACT_REF) { newValidCampaign.copy(target = 1000.DOLLARS) } 179 | output(CampaignContract.CONTRACT_REF) { newValidCampaign.copy(raisedSoFar = 200.DOLLARS) } 180 | output(PledgeContract.CONTRACT_REF) { defaultPledge } 181 | command(*partyKeys(A, B).toTypedArray()) { PledgeContract.Create() } 182 | command(*partyKeys(A).toTypedArray()) { CampaignContract.AcceptPledge() } 183 | timeWindow(Instant.now(), 5.seconds) 184 | this.fails() 185 | } 186 | 187 | // Missing states. 188 | transaction { 189 | input(CampaignContract.CONTRACT_REF) { newValidCampaign } 190 | output(CampaignContract.CONTRACT_REF) { newValidCampaign.copy(raisedSoFar = 100.POUNDS) } 191 | command(*partyKeys(A, B).toTypedArray()) { PledgeContract.Create() } 192 | command(*partyKeys(A).toTypedArray()) { CampaignContract.AcceptPledge() } 193 | timeWindow(Instant.now(), 5.seconds) 194 | this.fails() 195 | } 196 | 197 | transaction { 198 | output(CampaignContract.CONTRACT_REF) { newValidCampaign.copy(raisedSoFar = 100.POUNDS) } 199 | output(PledgeContract.CONTRACT_REF) { defaultPledge } 200 | command(*partyKeys(A, B).toTypedArray()) { PledgeContract.Create() } 201 | command(*partyKeys(A).toTypedArray()) { CampaignContract.AcceptPledge() } 202 | timeWindow(Instant.now(), 5.seconds) 203 | this.fails() 204 | } 205 | 206 | // Additional irrelevant states. 207 | transaction { 208 | input(DUMMY_PROGRAM_ID) { DummyState() } 209 | input(CampaignContract.CONTRACT_REF) { newValidCampaign } 210 | output(CampaignContract.CONTRACT_REF) { newValidCampaign.copy(raisedSoFar = 100.POUNDS) } 211 | output(PledgeContract.CONTRACT_REF) { defaultPledge } 212 | command(*partyKeys(A, B).toTypedArray()) { PledgeContract.Create() } 213 | command(*partyKeys(A).toTypedArray()) { CampaignContract.AcceptPledge() } 214 | timeWindow(Instant.now(), 5.seconds) 215 | this.fails() 216 | } 217 | 218 | transaction { 219 | output(DUMMY_PROGRAM_ID) { DummyState() } 220 | input(CampaignContract.CONTRACT_REF) { newValidCampaign } 221 | output(CampaignContract.CONTRACT_REF) { newValidCampaign.copy(raisedSoFar = 100.POUNDS) } 222 | output(PledgeContract.CONTRACT_REF) { defaultPledge } 223 | command(*partyKeys(A, B).toTypedArray()) { PledgeContract.Create() } 224 | command(*partyKeys(A).toTypedArray()) { CampaignContract.AcceptPledge() } 225 | timeWindow(Instant.now(), 5.seconds) 226 | this.fails() 227 | } 228 | 229 | // Changing campaign stuff that shouldn't change. 230 | transaction { 231 | input(CampaignContract.CONTRACT_REF) { newValidCampaign } 232 | output(CampaignContract.CONTRACT_REF) { newValidCampaign.copy(raisedSoFar = 100.POUNDS, name = "Changed") } 233 | output(PledgeContract.CONTRACT_REF) { defaultPledge } 234 | command(*partyKeys(A, B).toTypedArray()) { PledgeContract.Create() } 235 | command(*partyKeys(A).toTypedArray()) { CampaignContract.AcceptPledge() } 236 | timeWindow(Instant.now(), 5.seconds) 237 | this.fails() 238 | } 239 | 240 | transaction { 241 | input(CampaignContract.CONTRACT_REF) { newValidCampaign } 242 | output(CampaignContract.CONTRACT_REF) { newValidCampaign.copy(raisedSoFar = 100.POUNDS, target = 200.POUNDS) } 243 | output(PledgeContract.CONTRACT_REF) { defaultPledge } 244 | command(*partyKeys(A, B).toTypedArray()) { PledgeContract.Create() } 245 | command(*partyKeys(A).toTypedArray()) { CampaignContract.AcceptPledge() } 246 | timeWindow(Instant.now(), 5.seconds) 247 | this.fails() 248 | } 249 | 250 | transaction { 251 | input(CampaignContract.CONTRACT_REF) { newValidCampaign } 252 | output(CampaignContract.CONTRACT_REF) { newValidCampaign.copy(raisedSoFar = 100.POUNDS, linearId = UniqueIdentifier()) } 253 | output(PledgeContract.CONTRACT_REF) { defaultPledge } 254 | command(*partyKeys(A, B).toTypedArray()) { PledgeContract.Create() } 255 | command(*partyKeys(A).toTypedArray()) { CampaignContract.AcceptPledge() } 256 | timeWindow(Instant.now(), 5.seconds) 257 | this.fails() 258 | } 259 | 260 | transaction { 261 | input(CampaignContract.CONTRACT_REF) { newValidCampaign } 262 | output(CampaignContract.CONTRACT_REF) { newValidCampaign.copy(raisedSoFar = 100.POUNDS, deadline = Instant.now()) } 263 | output(PledgeContract.CONTRACT_REF) { defaultPledge } 264 | command(*partyKeys(A, B).toTypedArray()) { PledgeContract.Create() } 265 | command(*partyKeys(A).toTypedArray()) { CampaignContract.AcceptPledge() } 266 | timeWindow(Instant.now(), 5.seconds) 267 | this.fails() 268 | } 269 | 270 | // Pledge for wrong campaign. 271 | transaction { 272 | input(CampaignContract.CONTRACT_REF) { newValidCampaign } 273 | output(CampaignContract.CONTRACT_REF) { newValidCampaign.copy(raisedSoFar = 100.POUNDS) } 274 | output(PledgeContract.CONTRACT_REF) { defaultPledge.copy(campaignReference = UniqueIdentifier()) } // Wrong campaign reference. 275 | command(*partyKeys(A, B).toTypedArray()) { PledgeContract.Create() } 276 | command(*partyKeys(A).toTypedArray()) { CampaignContract.AcceptPledge() } 277 | timeWindow(Instant.now(), 5.seconds) 278 | this.fails() 279 | } 280 | 281 | // Pledge after deadline. 282 | transaction { 283 | input(CampaignContract.CONTRACT_REF) { newValidCampaign } 284 | output(CampaignContract.CONTRACT_REF) { newValidCampaign.copy(raisedSoFar = 100.POUNDS) } 285 | output(PledgeContract.CONTRACT_REF) { defaultPledge } 286 | command(*partyKeys(A, B).toTypedArray()) { PledgeContract.Create() } 287 | command(*partyKeys(A).toTypedArray()) { CampaignContract.AcceptPledge() } 288 | timeWindow(newValidCampaign.deadline + 1.seconds, 5.seconds) 289 | this.fails() 290 | } 291 | 292 | // Wrong keys in accept pledge command. 293 | transaction { 294 | input(CampaignContract.CONTRACT_REF) { newValidCampaign } 295 | output(CampaignContract.CONTRACT_REF) { newValidCampaign.copy(raisedSoFar = 100.POUNDS) } 296 | output(PledgeContract.CONTRACT_REF) { defaultPledge } 297 | command(*partyKeys(A, B).toTypedArray()) { PledgeContract.Create() } 298 | command(*partyKeys(B).toTypedArray()) { CampaignContract.AcceptPledge() } 299 | timeWindow(Instant.now(), 5.seconds) 300 | this.fails() 301 | } 302 | 303 | transaction { 304 | input(CampaignContract.CONTRACT_REF) { newValidCampaign } 305 | output(CampaignContract.CONTRACT_REF) { newValidCampaign.copy(raisedSoFar = 100.POUNDS) } 306 | output(PledgeContract.CONTRACT_REF) { defaultPledge } 307 | command(*partyKeys(A, B).toTypedArray()) { PledgeContract.Create() } 308 | command(*partyKeys(B, A).toTypedArray()) { CampaignContract.AcceptPledge() } 309 | timeWindow(Instant.now(), 5.seconds) 310 | this.fails() 311 | } 312 | 313 | // Pledge after deadline. 314 | transaction { 315 | input(CampaignContract.CONTRACT_REF) { newValidCampaign } 316 | output(CampaignContract.CONTRACT_REF) { newValidCampaign.copy(raisedSoFar = 100.POUNDS) } 317 | output(PledgeContract.CONTRACT_REF) { defaultPledge } 318 | command(*partyKeys(A, B).toTypedArray()) { PledgeContract.Create() } 319 | command(*partyKeys(A).toTypedArray()) { CampaignContract.AcceptPledge() } 320 | timeWindow(newValidCampaign.deadline + 1.seconds, 5.seconds) 321 | this.fails() 322 | } 323 | } 324 | } 325 | 326 | @Test 327 | fun `End campaign after failure with no pledges`() { 328 | val pledge = Pledge(100.POUNDS, B, A, newValidCampaign.linearId) 329 | val endedCampaign = newValidCampaign.copy(deadline = Instant.now().minusSeconds(1)) 330 | 331 | ledger { 332 | transaction { 333 | input(CampaignContract.CONTRACT_REF) { endedCampaign } 334 | command(*partyKeys(A).toTypedArray()) { CampaignContract.End() } 335 | this.verifies() 336 | } 337 | 338 | transaction { 339 | input(CampaignContract.CONTRACT_REF) { endedCampaign } 340 | input(PledgeContract.CONTRACT_REF) { pledge } 341 | command(*partyKeys(A).toTypedArray()) { CampaignContract.End() } 342 | this.fails() 343 | } 344 | } 345 | } 346 | 347 | @Test 348 | fun `End campaign after failure with some pledges`() { 349 | val pledge = Pledge(100.POUNDS, B, A, newValidCampaign.linearId) 350 | val endedCampaign = newValidCampaign.copy(deadline = Instant.now().minusSeconds(1)) 351 | 352 | ledger { 353 | transaction { 354 | input(CampaignContract.CONTRACT_REF) { endedCampaign.copy(raisedSoFar = 100.POUNDS) } 355 | input(PledgeContract.CONTRACT_REF) { pledge } 356 | command(*partyKeys(A).toTypedArray()) { CampaignContract.End() } 357 | command(*partyKeys(A).toTypedArray()) { PledgeContract.Cancel() } 358 | this.verifies() 359 | } 360 | 361 | transaction { 362 | input(CampaignContract.CONTRACT_REF) { endedCampaign.copy(raisedSoFar = 500.POUNDS) } 363 | input(PledgeContract.CONTRACT_REF) { pledge } 364 | input(PledgeContract.CONTRACT_REF) { pledge.copy(linearId = UniqueIdentifier()) } 365 | input(PledgeContract.CONTRACT_REF) { pledge.copy(linearId = UniqueIdentifier()) } 366 | input(PledgeContract.CONTRACT_REF) { pledge.copy(linearId = UniqueIdentifier()) } 367 | input(PledgeContract.CONTRACT_REF) { pledge.copy(linearId = UniqueIdentifier()) } 368 | command(*partyKeys(A).toTypedArray()) { CampaignContract.End() } 369 | command(*partyKeys(A).toTypedArray()) { PledgeContract.Cancel() } 370 | this.verifies() 371 | } 372 | 373 | // Wrong campaign. 374 | transaction { 375 | input(CampaignContract.CONTRACT_REF) { endedCampaign.copy(raisedSoFar = 100.POUNDS) } 376 | input(PledgeContract.CONTRACT_REF) { pledge.copy(campaignReference = UniqueIdentifier()) } 377 | command(*partyKeys(A).toTypedArray()) { CampaignContract.End() } 378 | command(*partyKeys(A).toTypedArray()) { PledgeContract.Cancel() } 379 | this.fails() 380 | } 381 | 382 | // Raised vs pledge mismatch. 383 | transaction { 384 | input(CampaignContract.CONTRACT_REF) { endedCampaign.copy(raisedSoFar = 100.POUNDS) } 385 | command(*partyKeys(A).toTypedArray()) { CampaignContract.End() } 386 | command(*partyKeys(A).toTypedArray()) { PledgeContract.Cancel() } 387 | this.fails() 388 | } 389 | } 390 | } 391 | 392 | @Test 393 | fun `End campaign in success`() { 394 | val endedCampaign = newValidCampaign.copy(deadline = Instant.now().minusSeconds(1)) 395 | 396 | ledger { 397 | transaction { 398 | input(CampaignContract.CONTRACT_REF) { endedCampaign } 399 | input(PledgeContract.CONTRACT_REF) { Pledge(500.POUNDS, B, A, endedCampaign.linearId) } 400 | input(PledgeContract.CONTRACT_REF) { Pledge(200.POUNDS, D, A, endedCampaign.linearId) } 401 | input(PledgeContract.CONTRACT_REF) { Pledge(300.POUNDS, C, A, endedCampaign.linearId) } 402 | input(CASH_PROGRAM_ID) { Cash.State(defaultIssuer, 500.POUNDS, B) } 403 | input(CASH_PROGRAM_ID) { Cash.State(defaultIssuer, 300.POUNDS, C) } 404 | input(CASH_PROGRAM_ID) { Cash.State(defaultIssuer, 200.POUNDS, D) } 405 | output(CASH_PROGRAM_ID) { Cash.State(defaultIssuer, 500.POUNDS, A) } 406 | output(CASH_PROGRAM_ID) { Cash.State(defaultIssuer, 300.POUNDS, A) } 407 | output(CASH_PROGRAM_ID) { Cash.State(defaultIssuer, 200.POUNDS, A) } 408 | command(*partyKeys(A).toTypedArray()) { CampaignContract.End() } 409 | command(*partyKeys(A).toTypedArray()) { PledgeContract.Cancel() } 410 | command(*partyKeys(B, C, D).toTypedArray()) { Cash.Commands.Move() } 411 | this.verifies() 412 | } 413 | } 414 | } 415 | 416 | } -------------------------------------------------------------------------------- /cordapp/src/test/kotlin/net/corda/demos/crowdFunding/contracts/PledgeTests.kt: -------------------------------------------------------------------------------- 1 | package net.corda.demos.crowdFunding.contracts 2 | 3 | import net.corda.core.contracts.UniqueIdentifier 4 | import net.corda.core.crypto.entropyToKeyPair 5 | import net.corda.core.identity.CordaX500Name 6 | import net.corda.core.identity.Party 7 | import net.corda.core.utilities.OpaqueBytes 8 | import net.corda.core.utilities.hours 9 | import net.corda.core.utilities.seconds 10 | import net.corda.demos.crowdFunding.structures.Campaign 11 | import net.corda.demos.crowdFunding.structures.Pledge 12 | import net.corda.finance.POUNDS 13 | import net.corda.testing.ledger 14 | import net.corda.testing.setCordappPackages 15 | import net.corda.testing.unsetCordappPackages 16 | import org.junit.After 17 | import org.junit.Before 18 | import org.junit.Test 19 | import java.math.BigInteger 20 | import java.security.KeyPair 21 | import java.time.Instant 22 | 23 | // TODO: Write more tests. 24 | class PledgeTests { 25 | 26 | private val issuer: Party = createParty("Issuer", 0) 27 | private val A: Party = createParty("A", 1) 28 | private val B: Party = createParty("B", 2) 29 | private val C: Party = createParty("C", 3) 30 | private val D: Party = createParty("D", 4) 31 | private val E: Party = createParty("E", 5) 32 | 33 | val defaultRef = OpaqueBytes(ByteArray(1, { 1 })) 34 | val defaultIssuer = issuer.ref(defaultRef) 35 | 36 | private fun createParty(name: String, random: Long): Party { 37 | val key: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(random)) } 38 | val identity = CordaX500Name(organisation = "Party$name", locality = "TestLand", country = "GB") 39 | return Party(identity, key.public) 40 | } 41 | 42 | private fun partyKeys(vararg parties: Party) = parties.map { it.owningKey } 43 | 44 | @Before 45 | fun before() { 46 | setCordappPackages( 47 | "net.corda.testing.contracts", 48 | "net.corda.demos.crowdFunding", 49 | "net.corda.finance" 50 | ) 51 | } 52 | 53 | @After 54 | fun after() { 55 | unsetCordappPackages() 56 | } 57 | 58 | private val oneHourFromNow = Instant.now() + 1.hours 59 | 60 | /** A failed campaign with no pledges that ends now. */ 61 | private val failedCampaignWithNoPledges = Campaign( 62 | name = "Roger's Campaign", 63 | manager = A, 64 | target = 1000.POUNDS, 65 | deadline = oneHourFromNow 66 | ) 67 | 68 | private val newValidCampaign = Campaign( 69 | name = "Roger's Campaign", 70 | manager = A, 71 | target = 1000.POUNDS, 72 | deadline = oneHourFromNow 73 | ) 74 | 75 | @Test 76 | fun `Make pledge tests`() { 77 | val defaultPledge = Pledge(100.POUNDS, B, A, newValidCampaign.linearId) 78 | 79 | ledger { 80 | // Valid make pledge transaction. 81 | transaction { 82 | input(CampaignContract.CONTRACT_REF) { newValidCampaign } 83 | output(CampaignContract.CONTRACT_REF) { newValidCampaign.copy(raisedSoFar = newValidCampaign.raisedSoFar + defaultPledge.amount) } 84 | output(PledgeContract.CONTRACT_REF) { defaultPledge } 85 | command(*partyKeys(A, B).toTypedArray()) { PledgeContract.Create() } 86 | command(*partyKeys(A).toTypedArray()) { CampaignContract.AcceptPledge() } 87 | timeWindow(Instant.now(), 5.seconds) 88 | this.verifies() 89 | } 90 | 91 | // Pledging a zero amount. 92 | transaction { 93 | input(CampaignContract.CONTRACT_REF) { newValidCampaign } 94 | output(CampaignContract.CONTRACT_REF) { newValidCampaign.copy() } 95 | output(PledgeContract.CONTRACT_REF) { defaultPledge.copy(amount = 0.POUNDS) } 96 | command(*partyKeys(A, B).toTypedArray()) { PledgeContract.Create() } 97 | command(*partyKeys(A).toTypedArray()) { CampaignContract.AcceptPledge() } 98 | timeWindow(Instant.now(), 5.seconds) 99 | this.fails() 100 | } 101 | 102 | // Creating more than one pledge at a time. 103 | transaction { 104 | input(CampaignContract.CONTRACT_REF) { newValidCampaign } 105 | output(CampaignContract.CONTRACT_REF) { newValidCampaign.copy(raisedSoFar = newValidCampaign.raisedSoFar + defaultPledge.amount) } 106 | output(PledgeContract.CONTRACT_REF) { defaultPledge } 107 | output(PledgeContract.CONTRACT_REF) { defaultPledge } 108 | command(*partyKeys(A, B).toTypedArray()) { PledgeContract.Create() } 109 | command(*partyKeys(A).toTypedArray()) { CampaignContract.AcceptPledge() } 110 | timeWindow(Instant.now(), 5.seconds) 111 | this.fails() 112 | } 113 | 114 | // States in the wrong place. 115 | transaction { 116 | input(CampaignContract.CONTRACT_REF) { newValidCampaign } 117 | output(CampaignContract.CONTRACT_REF) { newValidCampaign.copy(raisedSoFar = 200.POUNDS) } 118 | output(PledgeContract.CONTRACT_REF) { defaultPledge } 119 | input(PledgeContract.CONTRACT_REF) { defaultPledge } 120 | command(*partyKeys(A, B).toTypedArray()) { PledgeContract.Create() } 121 | command(*partyKeys(A).toTypedArray()) { CampaignContract.AcceptPledge() } 122 | timeWindow(Instant.now(), 5.seconds) 123 | this.fails() 124 | } 125 | 126 | // Missing campaign states. 127 | transaction { 128 | output(PledgeContract.CONTRACT_REF) { defaultPledge } 129 | command(*partyKeys(A, B).toTypedArray()) { PledgeContract.Create() } 130 | command(*partyKeys(A).toTypedArray()) { CampaignContract.AcceptPledge() } 131 | timeWindow(Instant.now(), 5.seconds) 132 | this.fails() 133 | } 134 | 135 | // Incorrect signers. 136 | transaction { 137 | input(CampaignContract.CONTRACT_REF) { newValidCampaign } 138 | output(CampaignContract.CONTRACT_REF) { newValidCampaign.copy(raisedSoFar = newValidCampaign.raisedSoFar + defaultPledge.amount) } 139 | output(PledgeContract.CONTRACT_REF) { defaultPledge } 140 | command(*partyKeys(A).toTypedArray()) { PledgeContract.Create() } 141 | command(*partyKeys(A).toTypedArray()) { CampaignContract.AcceptPledge() } 142 | timeWindow(Instant.now(), 5.seconds) 143 | this.fails() 144 | } 145 | 146 | transaction { 147 | input(CampaignContract.CONTRACT_REF) { newValidCampaign } 148 | output(CampaignContract.CONTRACT_REF) { newValidCampaign.copy(raisedSoFar = newValidCampaign.raisedSoFar + defaultPledge.amount) } 149 | output(PledgeContract.CONTRACT_REF) { defaultPledge } 150 | command(*partyKeys(B).toTypedArray()) { PledgeContract.Create() } 151 | command(*partyKeys(A).toTypedArray()) { CampaignContract.AcceptPledge() } 152 | timeWindow(Instant.now(), 5.seconds) 153 | this.fails() 154 | } 155 | } 156 | 157 | } 158 | 159 | @Test 160 | fun `Cancel pledge tests`() { 161 | val defaultPledge = Pledge(100.POUNDS, B, A, newValidCampaign.linearId) 162 | 163 | val endedCampaign = newValidCampaign.copy(deadline = Instant.now().minusSeconds(1)) 164 | 165 | ledger { 166 | // Valid make pledge transaction. 167 | transaction { 168 | input(CampaignContract.CONTRACT_REF) { endedCampaign } 169 | input(PledgeContract.CONTRACT_REF) { defaultPledge } 170 | command(*partyKeys(A).toTypedArray()) { PledgeContract.Cancel() } 171 | command(*partyKeys(A).toTypedArray()) { CampaignContract.End() } 172 | this.verifies() 173 | } 174 | 175 | // Has pledge outputs. 176 | transaction { 177 | input(CampaignContract.CONTRACT_REF) { endedCampaign } 178 | input(PledgeContract.CONTRACT_REF) { defaultPledge } 179 | output(PledgeContract.CONTRACT_REF) { defaultPledge } 180 | command(*partyKeys(A, B).toTypedArray()) { PledgeContract.Cancel() } 181 | command(*partyKeys(A).toTypedArray()) { CampaignContract.End() } 182 | this.fails() 183 | } 184 | 185 | // Wrong public key. 186 | transaction { 187 | input(CampaignContract.CONTRACT_REF) { endedCampaign } 188 | input(PledgeContract.CONTRACT_REF) { defaultPledge } 189 | command(*partyKeys(B).toTypedArray()) { PledgeContract.Cancel() } 190 | command(*partyKeys(A).toTypedArray()) { CampaignContract.End() } 191 | this.fails() 192 | } 193 | 194 | // No campaign state present. 195 | transaction { 196 | input(PledgeContract.CONTRACT_REF) { defaultPledge } 197 | command(*partyKeys(B).toTypedArray()) { PledgeContract.Cancel() } 198 | command(*partyKeys(A).toTypedArray()) { CampaignContract.End() } 199 | this.fails() 200 | } 201 | 202 | // Cancelling a pledge for a different campaign. 203 | transaction { 204 | input(CampaignContract.CONTRACT_REF) { endedCampaign } 205 | input(PledgeContract.CONTRACT_REF) { defaultPledge.copy(campaignReference = UniqueIdentifier()) } 206 | command(*partyKeys(A).toTypedArray()) { PledgeContract.Cancel() } 207 | command(*partyKeys(A).toTypedArray()) { CampaignContract.End() } 208 | this.fails() 209 | } 210 | } 211 | } 212 | 213 | } -------------------------------------------------------------------------------- /cordapp/src/test/kotlin/net/corda/demos/crowdFunding/flows/CrowdFundingTest.kt: -------------------------------------------------------------------------------- 1 | package net.corda.demos.crowdFunding.flows 2 | 3 | import net.corda.core.concurrent.CordaFuture 4 | import net.corda.core.contracts.Amount 5 | import net.corda.core.flows.FlowLogic 6 | import net.corda.core.identity.CordaX500Name 7 | import net.corda.core.identity.Party 8 | import net.corda.core.transactions.SignedTransaction 9 | import net.corda.core.utilities.OpaqueBytes 10 | import net.corda.core.utilities.getOrThrow 11 | import net.corda.core.utilities.loggerFor 12 | import net.corda.finance.flows.CashIssueFlow 13 | import net.corda.node.internal.StartedNode 14 | import net.corda.testing.node.MockNetwork 15 | import net.corda.testing.setCordappPackages 16 | import net.corda.testing.unsetCordappPackages 17 | import org.junit.After 18 | import org.junit.Before 19 | import org.slf4j.Logger 20 | import java.time.Instant 21 | import java.util.* 22 | 23 | abstract class CrowdFundingTest(val numberOfNodes: Int) { 24 | 25 | lateinit protected var net: MockNetwork 26 | lateinit protected var nodes: List> 27 | 28 | @Before 29 | abstract fun initialiseNodes() 30 | 31 | @Before 32 | fun setupNetwork() { 33 | setCordappPackages( 34 | "net.corda.demos.crowdFunding", 35 | "net.corda.finance" 36 | ) 37 | net = MockNetwork(threadPerNode = true) 38 | nodes = createSomeNodes(numberOfNodes) 39 | nodes.forEach { node -> registerFlowsAndServices(node) } 40 | } 41 | 42 | @After 43 | fun tearDownNetwork() { 44 | net.stopNodes() 45 | unsetCordappPackages() 46 | } 47 | 48 | companion object { 49 | val logger: Logger = loggerFor() 50 | } 51 | 52 | private fun calculateDeadlineInSeconds(interval: Long) = Instant.now().plusSeconds(interval) 53 | protected val oneSecondFromNow: Instant get() = calculateDeadlineInSeconds(1L) 54 | protected val fiveSecondsFromNow: Instant get() = calculateDeadlineInSeconds(5L) 55 | protected val tenSecondsFromNow: Instant get() = calculateDeadlineInSeconds(10L) 56 | protected val oneMinuteFromNow: Instant get() = calculateDeadlineInSeconds(60L) 57 | 58 | protected fun registerFlowsAndServices(node: StartedNode) { 59 | val mockNode = node.internals 60 | mockNode.registerInitiatedFlow(RecordTransactionAsObserver::class.java) 61 | mockNode.registerInitiatedFlow(MakePledge.Responder::class.java) 62 | mockNode.registerInitiatedFlow(EndCampaign.Responder::class.java) 63 | } 64 | 65 | protected fun createSomeNodes(numberOfNodes: Int = 2): List> { 66 | val notary = net.createNotaryNode(legalName = CordaX500Name("Notary", "London", "GB")) 67 | return (1..numberOfNodes).map { current -> 68 | val char = current.toChar() + 64 69 | val name = CordaX500Name("Party$char", "London", "GB") 70 | net.createPartyNode(notary.network.myAddress, name) 71 | } 72 | } 73 | 74 | fun StartedNode.start(logic: FlowLogic): CordaFuture { 75 | return this.services.startFlow(logic).resultFuture 76 | } 77 | 78 | fun StartedNode.legalIdentity(): Party { 79 | return this.services.myInfo.legalIdentities.first() 80 | } 81 | 82 | protected fun selfIssueCash(party: StartedNode, 83 | amount: Amount): SignedTransaction { 84 | val notary = party.services.networkMapCache.notaryIdentities.firstOrNull() 85 | ?: throw IllegalStateException("Could not find a notary.") 86 | val issueRef = OpaqueBytes.of(0) 87 | val issueRequest = CashIssueFlow.IssueRequest(amount, issueRef, notary) 88 | val flow = CashIssueFlow(issueRequest) 89 | return party.services.startFlow(flow).resultFuture.getOrThrow().stx 90 | } 91 | 92 | } 93 | -------------------------------------------------------------------------------- /cordapp/src/test/kotlin/net/corda/demos/crowdFunding/flows/EndCampaignTests.kt: -------------------------------------------------------------------------------- 1 | package net.corda.demos.crowdFunding.flows 2 | 3 | import net.corda.core.contracts.UniqueIdentifier 4 | import net.corda.core.node.services.queryBy 5 | import net.corda.core.node.services.vault.QueryCriteria 6 | import net.corda.core.utilities.getOrThrow 7 | import net.corda.demos.crowdFunding.structures.Campaign 8 | import net.corda.demos.crowdFunding.structures.Pledge 9 | import net.corda.finance.GBP 10 | import net.corda.finance.POUNDS 11 | import net.corda.finance.contracts.getCashBalance 12 | import net.corda.node.internal.StartedNode 13 | import net.corda.testing.node.MockNetwork 14 | import org.junit.Before 15 | import org.junit.Test 16 | import kotlin.test.assertEquals 17 | 18 | // TODO: Refactor repeated tests. 19 | class EndCampaignTests : CrowdFundingTest(numberOfNodes = 5) { 20 | 21 | lateinit var A: StartedNode 22 | lateinit var B: StartedNode 23 | lateinit var C: StartedNode 24 | lateinit var D: StartedNode 25 | lateinit var E: StartedNode 26 | 27 | @Before 28 | override fun initialiseNodes() { 29 | A = nodes[0] 30 | B = nodes[1] 31 | C = nodes[2] 32 | D = nodes[3] 33 | E = nodes[4] 34 | } 35 | 36 | private val rogersCampaign 37 | get() = Campaign( 38 | name = "Roger's Campaign", 39 | target = 1000.POUNDS, 40 | manager = A.legalIdentity(), 41 | deadline = fiveSecondsFromNow 42 | ) 43 | 44 | private fun checkUpdatesAreCommitted( 45 | party: StartedNode, 46 | campaignId: UniqueIdentifier, 47 | campaignState: Campaign 48 | ) { 49 | // Check that the EndCampaign transaction is committed by B and the Pledge/Campaign states are consumed. 50 | party.database.transaction { 51 | val (_, observable) = party.services.validatedTransactions.track() 52 | observable.first { it.tx.outputStates.isEmpty() }.subscribe { logger.info(it.tx.toString()) } 53 | 54 | val campaignQuery = QueryCriteria.LinearStateQueryCriteria(linearId = listOf(campaignId)) 55 | assertEquals(emptyList(), party.services.vaultService.queryBy(campaignQuery).states) 56 | } 57 | } 58 | 59 | // TODO: Finish this unit test. 60 | @Test 61 | fun `start campaign, make a pledge, don't raise enough, then end the campaign with a failure`() { 62 | // Start a campaign on PartyA. 63 | val startCampaignFlow = StartCampaign(rogersCampaign) 64 | val newCampaign = A.start(startCampaignFlow).getOrThrow() 65 | val newCampaignState = newCampaign.tx.outputs.single().data as Campaign 66 | val newCampaignId = newCampaignState.linearId 67 | 68 | // B makes a pledge to A's campaign. 69 | val makePledgeFlow = MakePledge.Initiator(100.POUNDS, newCampaignId, broadcastToObservers = true) 70 | val campaignAfterFirstPledge = B.start(makePledgeFlow).getOrThrow() 71 | val campaignStateAfterFirstPledge = campaignAfterFirstPledge.tx.outputsOfType().single() 72 | 73 | // Wait for the campaign to end... 74 | net.waitQuiescent() 75 | 76 | checkUpdatesAreCommitted(A, newCampaignId, campaignStateAfterFirstPledge) 77 | checkUpdatesAreCommitted(B, newCampaignId, campaignStateAfterFirstPledge) 78 | checkUpdatesAreCommitted(C, newCampaignId, campaignStateAfterFirstPledge) 79 | checkUpdatesAreCommitted(D, newCampaignId, campaignStateAfterFirstPledge) 80 | checkUpdatesAreCommitted(E, newCampaignId, campaignStateAfterFirstPledge) 81 | } 82 | 83 | @Test 84 | fun `start campaign, make a pledge, raise enough, then end the campaign with a success`() { 85 | // Issue cash to begin with. 86 | val bCash = selfIssueCash(B, 500.POUNDS) 87 | val cCash = selfIssueCash(C, 500.POUNDS) 88 | // Start a campaign on PartyA. 89 | val startCampaignFlow = StartCampaign(rogersCampaign) 90 | val newCampaign = A.start(startCampaignFlow).getOrThrow() 91 | val newCampaignState = newCampaign.tx.outputs.single().data as Campaign 92 | val newCampaignStateRef = newCampaign.tx.outRef(0).ref 93 | val newCampaignId = newCampaignState.linearId 94 | 95 | logger.info("New campaign started") 96 | logger.info(newCampaign.toString()) 97 | logger.info(newCampaign.tx.toString()) 98 | 99 | // B makes a pledge to A's campaign. 100 | val bMakePledgeFlow = MakePledge.Initiator(500.POUNDS, newCampaignId, broadcastToObservers = true) 101 | val campaignAfterFirstPledge = B.start(bMakePledgeFlow).getOrThrow() 102 | val campaignStateAfterFirstPledge = campaignAfterFirstPledge.tx.outputsOfType().single() 103 | val campaignStateRefAfterFirstPledge = campaignAfterFirstPledge.tx.outRefsOfType().single().ref 104 | val firstPledge = campaignAfterFirstPledge.tx.outputsOfType().single() 105 | 106 | logger.info("PartyB pledges £500 to PartyA") 107 | logger.info(campaignAfterFirstPledge.toString()) 108 | logger.info(campaignAfterFirstPledge.tx.toString()) 109 | 110 | // We need this to avoid double spend exceptions. 111 | Thread.sleep(1000) 112 | 113 | // C makes a pledge to A's campaign. 114 | val cMakePledgeFlow = MakePledge.Initiator(500.POUNDS, newCampaignId, broadcastToObservers = true) 115 | val campaignAfterSecondPledge = C.start(cMakePledgeFlow).getOrThrow() 116 | val campaignStateAfterSecondPledge = campaignAfterSecondPledge.tx.outputsOfType().single() 117 | val campaignStateRefAfterSecondPledge = campaignAfterSecondPledge.tx.outRefsOfType().single().ref 118 | val secondPledge = campaignAfterSecondPledge.tx.outputsOfType().single() 119 | 120 | logger.info("PartyC pledges £500 to PartyA") 121 | logger.info(campaignAfterSecondPledge.toString()) 122 | logger.info(campaignAfterSecondPledge.tx.toString()) 123 | 124 | logger.info("PartyA runs the EndCampaign flow and requests cash from the pledgers (PartyB and PartyC).") 125 | A.database.transaction { 126 | val (_, observable) = A.services.validatedTransactions.track() 127 | observable.subscribe { tx -> 128 | // Don't log dependency transactions. 129 | val myKeys = A.services.keyManagementService.filterMyKeys(tx.tx.requiredSigningKeys).toList() 130 | if (myKeys.isNotEmpty()) { 131 | logger.info(tx.tx.toString()) 132 | } 133 | } 134 | } 135 | 136 | net.waitQuiescent() 137 | 138 | // Now perform the tests to check everyone has the correct data. 139 | 140 | // See that everyone gets the new campaign. 141 | val aNewCampaign = A.database.transaction { A.services.loadState(newCampaignStateRef).data } 142 | val bNewCampaign = B.database.transaction { B.services.loadState(newCampaignStateRef).data } 143 | val cNewCampaign = C.database.transaction { C.services.loadState(newCampaignStateRef).data } 144 | val dNewCampaign = D.database.transaction { D.services.loadState(newCampaignStateRef).data } 145 | val eNewCampaign = E.database.transaction { E.services.loadState(newCampaignStateRef).data } 146 | 147 | assertEquals(1, setOf(newCampaignState, aNewCampaign, bNewCampaign, cNewCampaign, dNewCampaign, eNewCampaign).size) 148 | 149 | // See that everyone gets the updated campaign after the first pledge. 150 | val aCampaignAfterPledge = A.database.transaction { A.services.loadState(campaignStateRefAfterFirstPledge).data } 151 | val bCampaignAfterPledge = B.database.transaction { B.services.loadState(campaignStateRefAfterFirstPledge).data } 152 | val cCampaignAfterPledge = C.database.transaction { C.services.loadState(campaignStateRefAfterFirstPledge).data } 153 | val dCampaignAfterPledge = D.database.transaction { D.services.loadState(campaignStateRefAfterFirstPledge).data } 154 | val eCampaignAfterPledge = E.database.transaction { E.services.loadState(campaignStateRefAfterFirstPledge).data } 155 | 156 | // All parties should have the same updated Campaign state. 157 | assertEquals(1, setOf(campaignStateAfterFirstPledge, aCampaignAfterPledge, bCampaignAfterPledge, cCampaignAfterPledge, dCampaignAfterPledge, eCampaignAfterPledge).size) 158 | 159 | // See that confidentiality is maintained. 160 | assertEquals(B.legalIdentity(), A.services.identityService.wellKnownPartyFromAnonymous(firstPledge.pledger)) 161 | assertEquals(B.legalIdentity(), B.services.identityService.wellKnownPartyFromAnonymous(firstPledge.pledger)) 162 | assertEquals(null, C.database.transaction { C.services.identityService.wellKnownPartyFromAnonymous(firstPledge.pledger) }) 163 | assertEquals(null, D.database.transaction { D.services.identityService.wellKnownPartyFromAnonymous(firstPledge.pledger) }) 164 | assertEquals(null, E.database.transaction { E.services.identityService.wellKnownPartyFromAnonymous(firstPledge.pledger) }) 165 | 166 | // See that everyone gets the updated campaign after the second pledge. 167 | val aCampaignAfterSecondPledge = A.database.transaction { A.services.loadState(campaignStateRefAfterSecondPledge).data } 168 | val bCampaignAfterSecondPledge = B.database.transaction { B.services.loadState(campaignStateRefAfterSecondPledge).data } 169 | val cCampaignAfterSecondPledge = C.database.transaction { C.services.loadState(campaignStateRefAfterSecondPledge).data } 170 | val dCampaignAfterSecondPledge = D.database.transaction { D.services.loadState(campaignStateRefAfterSecondPledge).data } 171 | val eCampaignAfterSecondPledge = E.database.transaction { E.services.loadState(campaignStateRefAfterSecondPledge).data } 172 | 173 | // All parties should have the same updated Campaign state. 174 | assertEquals(1, setOf(campaignStateAfterSecondPledge, aCampaignAfterSecondPledge, bCampaignAfterSecondPledge, cCampaignAfterSecondPledge, dCampaignAfterSecondPledge, eCampaignAfterSecondPledge).size) 175 | 176 | // See that confidentiality is maintained. 177 | assertEquals(C.legalIdentity(), A.services.identityService.wellKnownPartyFromAnonymous(secondPledge.pledger)) 178 | assertEquals(null, B.database.transaction { B.services.identityService.wellKnownPartyFromAnonymous(secondPledge.pledger) }) 179 | assertEquals(C.legalIdentity(), C.services.identityService.wellKnownPartyFromAnonymous(secondPledge.pledger)) 180 | assertEquals(null, D.database.transaction { D.services.identityService.wellKnownPartyFromAnonymous(secondPledge.pledger) }) 181 | assertEquals(null, E.database.transaction { E.services.identityService.wellKnownPartyFromAnonymous(secondPledge.pledger) }) 182 | 183 | // WARNING: The nodes which were not involved in the pledging or the campaign get to see the transferred cash in their vaults!!!!!!!! 184 | // This is not a bug but a consequence of storing ALL output states in a transaction. 185 | // We need to change this such that a filtered transaction can be recorded instead of a full SignedTransaction. 186 | // The other option is not to broadcast the pledge transactions. 187 | A.database.transaction { logger.info(A.services.getCashBalance(GBP).toString()) } 188 | B.database.transaction { logger.info(B.services.getCashBalance(GBP).toString()) } 189 | C.database.transaction { logger.info(C.services.getCashBalance(GBP).toString()) } 190 | D.database.transaction { logger.info(D.services.getCashBalance(GBP).toString()) } 191 | E.database.transaction { logger.info(E.services.getCashBalance(GBP).toString()) } 192 | } 193 | 194 | } -------------------------------------------------------------------------------- /cordapp/src/test/kotlin/net/corda/demos/crowdFunding/flows/MakePledgeTests.kt: -------------------------------------------------------------------------------- 1 | package net.corda.demos.crowdFunding.flows 2 | 3 | import net.corda.core.contracts.TransactionResolutionException 4 | import net.corda.core.utilities.getOrThrow 5 | import net.corda.demos.crowdFunding.structures.Campaign 6 | import net.corda.demos.crowdFunding.structures.Pledge 7 | import net.corda.finance.POUNDS 8 | import net.corda.node.internal.StartedNode 9 | import net.corda.testing.node.MockNetwork 10 | import org.junit.Before 11 | import org.junit.Test 12 | import kotlin.test.assertEquals 13 | import kotlin.test.assertFailsWith 14 | 15 | class MakePledgeTests : CrowdFundingTest(numberOfNodes = 5) { 16 | 17 | lateinit var A: StartedNode 18 | lateinit var B: StartedNode 19 | lateinit var C: StartedNode 20 | lateinit var D: StartedNode 21 | lateinit var E: StartedNode 22 | 23 | @Before 24 | override fun initialiseNodes() { 25 | A = nodes[0] 26 | B = nodes[1] 27 | C = nodes[2] 28 | D = nodes[3] 29 | E = nodes[4] 30 | } 31 | 32 | @Test 33 | fun `successfully make a pledge and broadcast the updated campaign state to all parties`() { 34 | // Campaign. 35 | val rogersCampaign = Campaign( 36 | name = "Roger's Campaign", 37 | target = 1000.POUNDS, 38 | manager = A.legalIdentity(), 39 | deadline = fiveSecondsFromNow 40 | ) 41 | 42 | // Start a new campaign. 43 | val startCampaignFlow = StartCampaign(rogersCampaign) 44 | val createCampaignTransaction = A.start(startCampaignFlow).getOrThrow() 45 | 46 | // Extract the state from the transaction. 47 | val campaignState = createCampaignTransaction.tx.outputs.single().data as Campaign 48 | val campaignId = campaignState.linearId 49 | 50 | // Make a pledge from PartyB to PartyA for £100. 51 | val makePledgeFlow = MakePledge.Initiator(100.POUNDS, campaignId, broadcastToObservers = true) 52 | val acceptPledgeTransaction = B.start(makePledgeFlow).getOrThrow() 53 | 54 | logger.info("New campaign started") 55 | logger.info(createCampaignTransaction.toString()) 56 | logger.info(createCampaignTransaction.tx.toString()) 57 | 58 | logger.info("PartyB pledges £100 to PartyA") 59 | logger.info(acceptPledgeTransaction.toString()) 60 | logger.info(acceptPledgeTransaction.tx.toString()) 61 | 62 | //Extract the states from the transaction. 63 | val campaignStateRefAfterPledge = acceptPledgeTransaction.tx.outRefsOfType().single().ref 64 | val campaignAfterPledge = acceptPledgeTransaction.tx.outputsOfType().single() 65 | val newPledgeStateRef = acceptPledgeTransaction.tx.outRefsOfType().single().ref 66 | val newPledge = acceptPledgeTransaction.tx.outputsOfType().single() 67 | 68 | val aCampaignAfterPledge = A.database.transaction { A.services.loadState(campaignStateRefAfterPledge).data } 69 | val bCampaignAfterPledge = B.database.transaction { B.services.loadState(campaignStateRefAfterPledge).data } 70 | val cCampaignAfterPledge = C.database.transaction { C.services.loadState(campaignStateRefAfterPledge).data } 71 | val dCampaignAfterPledge = D.database.transaction { D.services.loadState(campaignStateRefAfterPledge).data } 72 | val eCampaignAfterPledge = E.database.transaction { E.services.loadState(campaignStateRefAfterPledge).data } 73 | 74 | // All parties should have the same updated Campaign state. 75 | assertEquals(1, 76 | setOf( 77 | campaignAfterPledge, 78 | aCampaignAfterPledge, 79 | bCampaignAfterPledge, 80 | cCampaignAfterPledge, 81 | dCampaignAfterPledge, 82 | eCampaignAfterPledge 83 | ).size 84 | ) 85 | 86 | val aNewPledge = A.database.transaction { A.services.loadState(newPledgeStateRef).data } as Pledge 87 | val bNewPledge = B.database.transaction { B.services.loadState(newPledgeStateRef).data } as Pledge 88 | val cNewPledge = C.database.transaction { C.services.loadState(newPledgeStateRef).data } as Pledge 89 | val dNewPledge = D.database.transaction { D.services.loadState(newPledgeStateRef).data } as Pledge 90 | val eNewPledge = E.database.transaction { E.services.loadState(newPledgeStateRef).data } as Pledge 91 | 92 | // All parties should have the same Pledge state. 93 | assertEquals(1, 94 | setOf( 95 | newPledge, 96 | aNewPledge, 97 | bNewPledge, 98 | cNewPledge, 99 | dNewPledge, 100 | eNewPledge 101 | ).size 102 | ) 103 | 104 | // Only A and B should know the identity of the pledger (who is B in this case). 105 | assertEquals(B.legalIdentity(), A.services.identityService.wellKnownPartyFromAnonymous(newPledge.pledger)) 106 | assertEquals(B.legalIdentity(), B.services.identityService.wellKnownPartyFromAnonymous(newPledge.pledger)) 107 | assertEquals(null, C.services.identityService.wellKnownPartyFromAnonymous(newPledge.pledger)) 108 | assertEquals(null, D.services.identityService.wellKnownPartyFromAnonymous(newPledge.pledger)) 109 | assertEquals(null, E.services.identityService.wellKnownPartyFromAnonymous(newPledge.pledger)) 110 | 111 | net.waitQuiescent() 112 | } 113 | 114 | @Test 115 | fun `successfully make a pledge without broadcasting the updated campaign state to all parties`() { 116 | // Campaign. 117 | val rogersCampaign = Campaign( 118 | name = "Roger's Campaign", 119 | target = 1000.POUNDS, 120 | manager = A.legalIdentity(), 121 | deadline = fiveSecondsFromNow // We shut the nodes down before the EndCampaignFlow is run though. 122 | ) 123 | 124 | // Start a new campaign. 125 | val startCampaignFlow = StartCampaign(rogersCampaign) 126 | val createCampaignTransaction = A.start(startCampaignFlow).getOrThrow() 127 | 128 | // Extract the state from the transaction. 129 | val campaignState = createCampaignTransaction.tx.outputs.single().data as Campaign 130 | val campaignId = campaignState.linearId 131 | 132 | // Make a pledge from PartyB to PartyA for £100 but don't broadcast it to everyone else. 133 | val makePledgeFlow = MakePledge.Initiator(100.POUNDS, campaignId, broadcastToObservers = false) 134 | val acceptPledgeTransaction = B.start(makePledgeFlow).getOrThrow() 135 | 136 | logger.info("New campaign started") 137 | logger.info(createCampaignTransaction.toString()) 138 | logger.info(createCampaignTransaction.tx.toString()) 139 | 140 | logger.info("PartyB pledges £100 to PartyA") 141 | logger.info(acceptPledgeTransaction.toString()) 142 | logger.info(acceptPledgeTransaction.tx.toString()) 143 | 144 | //Extract the states from the transaction. 145 | val campaignStateRefAfterPledge = acceptPledgeTransaction.tx.outRefsOfType().single().ref 146 | val campaignAfterPledge = acceptPledgeTransaction.tx.outputsOfType().single() 147 | val newPledgeStateRef = acceptPledgeTransaction.tx.outRefsOfType().single().ref 148 | val newPledge = acceptPledgeTransaction.tx.outputsOfType().single() 149 | 150 | val aCampaignAfterPledge = A.database.transaction { A.services.loadState(campaignStateRefAfterPledge).data } 151 | val bCampaignAfterPledge = B.database.transaction { B.services.loadState(campaignStateRefAfterPledge).data } 152 | assertFailsWith(TransactionResolutionException::class) { C.database.transaction { C.services.loadState(campaignStateRefAfterPledge) } } 153 | assertFailsWith(TransactionResolutionException::class) { D.database.transaction { D.services.loadState(campaignStateRefAfterPledge) } } 154 | assertFailsWith(TransactionResolutionException::class) { E.database.transaction { E.services.loadState(campaignStateRefAfterPledge) } } 155 | 156 | // Only PartyA and PartyB should have the updated campaign state. 157 | assertEquals(1, setOf(campaignAfterPledge, aCampaignAfterPledge, bCampaignAfterPledge).size) 158 | 159 | val aNewPledge = A.database.transaction { A.services.loadState(newPledgeStateRef).data } as Pledge 160 | val bNewPledge = B.database.transaction { B.services.loadState(newPledgeStateRef).data } as Pledge 161 | assertFailsWith(TransactionResolutionException::class) { C.database.transaction { C.services.loadState(newPledgeStateRef) } } 162 | assertFailsWith(TransactionResolutionException::class) { D.database.transaction { D.services.loadState(newPledgeStateRef) } } 163 | assertFailsWith(TransactionResolutionException::class) { E.database.transaction { E.services.loadState(newPledgeStateRef) } } 164 | 165 | // Only PartyA and PartyB should have the updated campaign state. 166 | assertEquals(1, setOf(newPledge, aNewPledge, bNewPledge).size) 167 | 168 | // Only A and B should know the identity of the pledger (who is B in this case). Of course, the others won't know. 169 | assertEquals(B.legalIdentity(), A.services.identityService.wellKnownPartyFromAnonymous(newPledge.pledger)) 170 | assertEquals(B.legalIdentity(), B.services.identityService.wellKnownPartyFromAnonymous(newPledge.pledger)) 171 | assertEquals(null, C.database.transaction { C.services.identityService.wellKnownPartyFromAnonymous(newPledge.pledger) }) 172 | assertEquals(null, D.database.transaction { D.services.identityService.wellKnownPartyFromAnonymous(newPledge.pledger) }) 173 | assertEquals(null, E.database.transaction { E.services.identityService.wellKnownPartyFromAnonymous(newPledge.pledger) }) 174 | 175 | net.waitQuiescent() 176 | } 177 | 178 | } -------------------------------------------------------------------------------- /cordapp/src/test/kotlin/net/corda/demos/crowdFunding/flows/StartCampaignTests.kt: -------------------------------------------------------------------------------- 1 | package net.corda.demos.crowdFunding.flows 2 | 3 | import net.corda.core.utilities.getOrThrow 4 | import net.corda.demos.crowdFunding.structures.Campaign 5 | import net.corda.finance.POUNDS 6 | import net.corda.node.internal.StartedNode 7 | import net.corda.testing.node.MockNetwork 8 | import org.junit.Before 9 | import org.junit.Test 10 | import kotlin.test.assertEquals 11 | 12 | class StartCampaignTests : CrowdFundingTest(numberOfNodes = 5) { 13 | 14 | lateinit var A: StartedNode 15 | lateinit var B: StartedNode 16 | lateinit var C: StartedNode 17 | lateinit var D: StartedNode 18 | lateinit var E: StartedNode 19 | 20 | @Before 21 | override fun initialiseNodes() { 22 | A = nodes[0] 23 | B = nodes[1] 24 | C = nodes[2] 25 | D = nodes[3] 26 | E = nodes[4] 27 | } 28 | 29 | private val rogersCampaign 30 | get() = Campaign( 31 | name = "Roger's Campaign", 32 | target = 1000.POUNDS, 33 | manager = A.legalIdentity(), 34 | deadline = fiveSecondsFromNow 35 | ) 36 | 37 | @Test 38 | fun `successfully start and broadcast campaign to all nodes`() { 39 | // Start a new Campaign. 40 | val flow = StartCampaign(rogersCampaign) 41 | val campaign = A.start(flow).getOrThrow() 42 | 43 | // Extract the state from the transaction. 44 | val campaignStateRef = campaign.tx.outRef(0).ref 45 | val campaignState = campaign.tx.outputs.single() 46 | 47 | // Get the Campaign state from the observer node vaults. 48 | val aCampaign = A.database.transaction { A.services.loadState(campaignStateRef) } 49 | val bCampaign = B.database.transaction { B.services.loadState(campaignStateRef) } 50 | val cCampaign = C.database.transaction { C.services.loadState(campaignStateRef) } 51 | val dCampaign = D.database.transaction { D.services.loadState(campaignStateRef) } 52 | val eCampaign = E.database.transaction { E.services.loadState(campaignStateRef) } 53 | 54 | // All the states should be equal. 55 | assertEquals(1, setOf(campaignState, aCampaign, bCampaign, cCampaign, dCampaign, eCampaign).size) 56 | 57 | logger.info("Even though PartyA is the only participant in the Campaign, all other parties should have a copy of it.") 58 | logger.info("The Campaign state does not include any information about the observers.") 59 | logger.info("PartyA: $campaignState") 60 | logger.info("PartyB: $bCampaign") 61 | logger.info("PartyC: $cCampaign") 62 | logger.info("PartyD: $dCampaign") 63 | logger.info("PartyE: $eCampaign") 64 | 65 | // We just shut down the nodes now - no need to wait for the nextScheduledActivity. 66 | } 67 | 68 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | name=Test 2 | group=net.corda.demos 3 | version=0.1 4 | kotlin.incremental=false -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roger-that-dev/observable-states/b047e746d4d04649606c8f0dfa3a229acbc08567/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Aug 25 12:50:39 BST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /images/campaign-end-failure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roger-that-dev/observable-states/b047e746d4d04649606c8f0dfa3a229acbc08567/images/campaign-end-failure.png -------------------------------------------------------------------------------- /images/campaign-end-success.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roger-that-dev/observable-states/b047e746d4d04649606c8f0dfa3a229acbc08567/images/campaign-end-success.png -------------------------------------------------------------------------------- /images/create-campaign.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roger-that-dev/observable-states/b047e746d4d04649606c8f0dfa3a229acbc08567/images/create-campaign.png -------------------------------------------------------------------------------- /images/create-pledge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roger-that-dev/observable-states/b047e746d4d04649606c8f0dfa3a229acbc08567/images/create-pledge.png -------------------------------------------------------------------------------- /lib/README.txt: -------------------------------------------------------------------------------- 1 | The Quasar.jar in this directory is for runtime instrumentation of classes by Quasar. 2 | 3 | When running corda outside of the given gradle building you must add the following flag with the 4 | correct path to your call to Java: 5 | 6 | java -javaagent:path-to-quasar-jar.jar ... 7 | 8 | See the Quasar docs for more information: http://docs.paralleluniverse.co/quasar/ -------------------------------------------------------------------------------- /lib/quasar.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roger-that-dev/observable-states/b047e746d4d04649606c8f0dfa3a229acbc08567/lib/quasar.jar -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include 'cordapp' 2 | 3 | --------------------------------------------------------------------------------