├── .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 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/.idea/runConfigurations/Run_Template_Cordapp.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/.idea/runConfigurations/Run_Template_RPC_Client.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/.idea/runConfigurations/Unit_tests.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
11 |
17 |
18 |
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 | 
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 | 
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 | 
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 | 
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 | 
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 |
--------------------------------------------------------------------------------