diff --git a/LICENCE b/LICENCE new file mode 100644 index 0000000..3ff572d --- /dev/null +++ b/LICENCE @@ -0,0 +1,13 @@ + Copyright 2016, R3 Limited. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..f7f7eed --- /dev/null +++ b/README.md @@ -0,0 +1,63 @@ +# Three-way flows - DvP atomic transaction - CorDapp + +This CorDapp creates Delivery-vs-Payment atomic transaction for on ledger `Asset` transfer for `Cash` with three participants. + +**CorDapp Nodes:** + +* Security Seller: This party is owner of `Asset` state of type `OwnableState` on ledger. He sells these assets for cash by creating Corda transaction. +* Security Buyer: This party has some `Cash` tokens on his ledger. He purchases `Asset` securities for Cash. +* Clearing House: coordinates `Seller` and `Buyer` parties to process the settlement transaction. It initiate settlement of `Asset` transfer request and collect the required states (i.e. Asset and Cash) from counterparties (i.e. Seller and Buyer) in-order to complete the transaction. +* Notary: notary node to check double-spend of input states then verify and sign final transaction. + +**This CorDapp example business-logic flows as below:** + +* 1. The `Seller` party creates the `Asset` state of type `OwnableState` on ledger. +* 2. Create `AssetTransfer` state and share it with `Buyer` party who willing to buy the `Asset`. +* 3. The `Buyer` party review and accept the `AssetTransfer` transaction and further share this state with `ClearingHouse` party. +* 4. The `ClearingHouse` party - offline review and validate the `Asset` data received along with `AssetTransfer` state. If everything is good, he initiate the `AssetSettlementInitiatorFlow` flow to create the settlement transaction with three participants. On completion of settlement transaction `Buyer` party became owner of `Asset` state and issues `Cash` tokens equals to the amount of `Asset.purchaseCost` to `Seller` party on ledger. + +## Interacting with the CorDapp via Corda Shell + +Use relevant node's Shell console to initiate the flows. + +**Let's start flows from each Corda node's shell console step-by-step:** + +**1.** To create `Asset` - run below command on `SecuritySeller` node's console: +```console +flow start com.synechron.cordapp.seller.flows.CreateAssetStateFlow$Initiator cusip: "CUSIP222", assetName: "US Bond", purchaseCost: $10000 +``` +To see state created run below command: +```console +run vaultQuery contractStateType: com.synechron.cordapp.state.Asset +``` +Now we have on ledger asset of type OwnableState and ready to sell it to other party on network. + +**2.** To create `AssetTransfer` - run below command again on `SecuritySeller` node's console: +```console +flow start com.synechron.cordapp.seller.flows.CreateAssetTransferRequestInitiatorFlow cusip: "CUSIP222", securityBuyer: "O=SecurityBuyer,L=New York,C=US" +``` +To see `AssetTransfer` state created run below command again: +```console +run vaultQuery contractStateType: com.synechron.cordapp.state.AssetTransfer +``` +You can see the AssetTransfer state data and **copy** the `linearId.id` *field value* of it and save it with you as we required it in next step. + +**3.** The `Buyer` party confirm `AssetTransfer` request received by running below command on counterparty `SecurityBuyer` node's console: +```console +flow start com.synechron.cordapp.buyer.flows.ConfirmAssetTransferRequestInitiatorFlow linearId: "", clearingHouse: "O=ClearingHouse,L=New York,C=US" +``` +This flow update the `AssetTransfer.status` value to `PENDING` from it's initial value `PENDING_CONFIRMATION`. + +**4.** The `Buyer` party self-issue the `Cash` tokens on ledger using: +```console +flow start net.corda.finance.flows.CashIssueFlow amount: $20000, issuerBankPartyRef: 1234, notary: "O=Notary,L=New York,C=US" +``` +To see the issued cash balance run: +```console +run vaultQuery contractStateType: net.corda.finance.contracts.asset.Cash$State +``` +**5.** Run below command on the `ClearingHouse` party node's console to create settlement transaction and distribute to three participants: +```console +flow start com.synechron.cordapp.clearinghouse.flows.AssetSettlementInitiatorFlow linearId: "" +``` +On successful completion of flows the `AssetTransfer.status` field value became `TRANSFERRED`. Now you can cross-check the available `Asset` and `Cash` states on each counterparty corda node. diff --git a/TRADEMARK b/TRADEMARK new file mode 100644 index 0000000..aa7e20f --- /dev/null +++ b/TRADEMARK @@ -0,0 +1,4 @@ +Corda and the Corda logo are trademarks of R3CEV LLC and its affiliates. All rights reserved. + +For R3CEV LLC's trademark and logo usage information, please consult our Trademark Usage Policy at +https://www.r3.com/trademark-policy/. diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..26b17d4 --- /dev/null +++ b/build.gradle @@ -0,0 +1,165 @@ +buildscript { + ext.corda_release_group = 'net.corda' + ext.corda_release_version = '3.1-corda' + ext.corda_gradle_plugins_version = '3.1.0' + ext.kotlin_version = '1.1.60' + ext.junit_version = '4.12' + ext.quasar_version = '0.7.9' + + repositories { + mavenLocal() + mavenCentral() + jcenter() + } + + dependencies { + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + classpath "net.corda.plugins:cordapp:$corda_gradle_plugins_version" + classpath "net.corda.plugins:cordformation:$corda_gradle_plugins_version" + classpath "net.corda.plugins:quasar-utils:$corda_gradle_plugins_version" + } +} + +repositories { + mavenLocal() + jcenter() + mavenCentral() + maven { url 'https://jitpack.io' } + maven { url 'https://ci-artifactory.corda.r3cev.com/artifactory/corda-releases' } +} + +apply plugin: 'kotlin' +apply plugin: 'net.corda.plugins.cordapp' +apply plugin: 'net.corda.plugins.cordformation' +apply plugin: 'net.corda.plugins.quasar-utils' + +sourceSets { + main { + resources { + srcDir "config/dev" + } + } + test { + resources { + srcDir "config/test" + } + } + integrationTest { + kotlin { + compileClasspath += main.output + test.output + runtimeClasspath += main.output + test.output + srcDir file('src/integration-test/kotlin') + } + } +} + +configurations { + integrationTestCompile.extendsFrom testCompile + integrationTestRuntime.extendsFrom testRuntime +} + +dependencies { + compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version" + testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version" + testCompile "junit:junit:$junit_version" + + // Corda integration dependencies + cordaCompile "$corda_release_group:corda-core:$corda_release_version" + cordaCompile "$corda_release_group:corda-finance:$corda_release_version" + cordaCompile "$corda_release_group:corda-jackson:$corda_release_version" + cordaCompile "$corda_release_group:corda-rpc:$corda_release_version" + cordaCompile "$corda_release_group:corda-node-api:$corda_release_version" + cordaCompile "$corda_release_group:corda-webserver-impl:$corda_release_version" + cordaRuntime "$corda_release_group:corda:$corda_release_version" + cordaRuntime "$corda_release_group:corda-webserver:$corda_release_version" + + testCompile "$corda_release_group:corda-node-driver:$corda_release_version" + + // CorDapp dependencies + // Specify your CorDapp's dependencies below, including dependent CorDapps. + // We've defined Cash as a dependent CorDapp as an example. + cordapp project(":cordapp-contracts-states") + cordapp project(":cordapp-common") + cordapp project(":cordapp-security-buyer") + cordapp project(":cordapp-security-seller") + cordapp project(":cordapp-clearing-house") + cordapp "$corda_release_group:corda-finance:$corda_release_version" +} + +task integrationTest(type: Test, dependsOn: []) { + testClassesDirs = sourceSets.integrationTest.output.classesDirs + classpath = sourceSets.integrationTest.runtimeClasspath +} + +tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { + kotlinOptions { + languageVersion = "1.1" + apiVersion = "1.1" + jvmTarget = "1.8" + javaParameters = true // Useful for reflection. + } +} + +task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { + directory "./build/nodes" + node { + name "O=Notary,L=New York,C=US" + notary = [validating: true] + p2pPort 10002 + cordapps = [ + "$project.group:cordapp-contracts-states:$project.version", + "$corda_release_group:corda-finance:$corda_release_version" + ] + } + node { + name "O=SecuritySeller,L=New York,C=US" + p2pPort 10005 + rpcSettings { + address("localhost:10006") + adminAddress("localhost:10046") + } + cordapps = [ + "$corda_release_group:corda-finance:$corda_release_version", + "$project.group:cordapp-contracts-states:$project.version", + "$project.group:cordapp-common:$project.version", + "$project.group:cordapp-security-seller:$project.version" + ] + rpcUsers = [[user: "user1", "password": "test", "permissions": ["ALL"]]] + } + node { + name "O=SecurityBuyer,L=New York,C=US" + p2pPort 10008 + rpcSettings { + address("localhost:10009") + adminAddress("localhost:10049") + } + cordapps = [ + "$corda_release_group:corda-finance:$corda_release_version", + "$project.group:cordapp-contracts-states:$project.version", + "$project.group:cordapp-common:$project.version", + "$project.group:cordapp-security-buyer:$project.version" + ] + rpcUsers = [[user: "user1", "password": "test", "permissions": ["ALL"]]] + } + node { + name "O=ClearingHouse,L=New York,C=US" + p2pPort 10011 + rpcSettings { + address("localhost:10012") + adminAddress("localhost:10412") + } + cordapps = [ + "$corda_release_group:corda-finance:$corda_release_version", + "$project.group:cordapp-contracts-states:$project.version", + "$project.group:cordapp-common:$project.version", + "$project.group:cordapp-clearing-house:$project.version" + ] + rpcUsers = [[user: "user1", "password": "test", "permissions": ["ALL"]]] + } +} + +task runTemplateClient(type: JavaExec) { + classpath = sourceSets.main.runtimeClasspath + main = 'com.synechron.ClientKt' + args 'localhost:10006' +} \ No newline at end of file diff --git a/config/dev/log4j2.xml b/config/dev/log4j2.xml new file mode 100644 index 0000000..8572a97 --- /dev/null +++ b/config/dev/log4j2.xml @@ -0,0 +1,59 @@ + + + + + logs + node-${hostName} + ${log-path}/archive + + + + + + + + + %highlight{%level{length=1} %d{HH:mm:ss} %T %c{1}.%M - %msg%n}{INFO=white,WARN=red,FATAL=bright red blink} + > + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/config/test/log4j2.xml b/config/test/log4j2.xml new file mode 100644 index 0000000..abd94f6 --- /dev/null +++ b/config/test/log4j2.xml @@ -0,0 +1,20 @@ + + + + + + + [%-5level] %d{HH:mm:ss.SSS} [%t] %c{1}.%M - %msg%n + > + + + + + + + + + + + + diff --git a/cordapp-clearing-house/build.gradle b/cordapp-clearing-house/build.gradle new file mode 100644 index 0000000..ccce5d4 --- /dev/null +++ b/cordapp-clearing-house/build.gradle @@ -0,0 +1,75 @@ +repositories { + mavenLocal() + jcenter() + mavenCentral() + maven { url 'https://jitpack.io' } + maven { url 'https://ci-artifactory.corda.r3cev.com/artifactory/corda-releases' } +} + +apply plugin: 'kotlin' +apply plugin: 'net.corda.plugins.cordapp' +apply plugin: 'net.corda.plugins.cordformation' +apply plugin: 'net.corda.plugins.quasar-utils' + +sourceSets { + main { + resources { + srcDir "config/dev" + } + } + test { + resources { + srcDir "config/test" + } + } + integrationTest { + kotlin { + compileClasspath += main.output + test.output + runtimeClasspath += main.output + test.output + srcDir file('src/integration-test/kotlin') + } + } +} + +configurations { + integrationTestCompile.extendsFrom testCompile + integrationTestRuntime.extendsFrom testRuntime +} + +dependencies { + compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version" + testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version" + testCompile "junit:junit:$junit_version" + + // Corda integration dependencies + cordaCompile "$corda_release_group:corda-core:$corda_release_version" + cordaCompile "$corda_release_group:corda-finance:$corda_release_version" + cordaCompile "$corda_release_group:corda-jackson:$corda_release_version" + cordaCompile "$corda_release_group:corda-rpc:$corda_release_version" + cordaCompile "$corda_release_group:corda-node-api:$corda_release_version" + cordaCompile "$corda_release_group:corda-webserver-impl:$corda_release_version" + cordaRuntime "$corda_release_group:corda:$corda_release_version" + cordaRuntime "$corda_release_group:corda-webserver:$corda_release_version" + + testCompile "$corda_release_group:corda-node-driver:$corda_release_version" + + // CorDapp dependencies + // Specify your CorDapp's dependencies below, including dependent CorDapps. + // We've defined Cash as a dependent CorDapp as an example. + cordapp project(":cordapp-contracts-states") + cordapp project(":cordapp-common") +} + +task integrationTest(type: Test, dependsOn: []) { + testClassesDirs = sourceSets.integrationTest.output.classesDirs + classpath = sourceSets.integrationTest.runtimeClasspath +} + +tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { + kotlinOptions { + languageVersion = "1.1" + apiVersion = "1.1" + jvmTarget = "1.8" + javaParameters = true // Useful for reflection. + } +} \ No newline at end of file diff --git a/cordapp-clearing-house/src/main/kotlin/com/synechron/cordapp/clearinghouse/flows/AssetSettlementInitiatorFlow.kt b/cordapp-clearing-house/src/main/kotlin/com/synechron/cordapp/clearinghouse/flows/AssetSettlementInitiatorFlow.kt new file mode 100644 index 0000000..c39c146 --- /dev/null +++ b/cordapp-clearing-house/src/main/kotlin/com/synechron/cordapp/clearinghouse/flows/AssetSettlementInitiatorFlow.kt @@ -0,0 +1,138 @@ +package com.synechron.cordapp.clearinghouse.flows + +import co.paralleluniverse.fibers.Suspendable +import com.synechron.cordapp.common.exception.InvalidPartyException +import com.synechron.cordapp.common.flows.IdentitySyncFlow +import com.synechron.cordapp.common.flows.ReceiveTransactionUnVerifiedFlow +import com.synechron.cordapp.contract.AssetTransferContract +import com.synechron.cordapp.flows.AbstractAssetSettlementFlow +import com.synechron.cordapp.state.AssetTransfer +import com.synechron.cordapp.state.RequestStatus +import net.corda.core.contracts.Command +import net.corda.core.contracts.UniqueIdentifier +import net.corda.core.flows.CollectSignaturesFlow +import net.corda.core.flows.FinalityFlow +import net.corda.core.flows.SendTransactionFlow +import net.corda.core.flows.StartableByRPC +import net.corda.core.transactions.SignedTransaction +import net.corda.core.transactions.TransactionBuilder +import net.corda.core.utilities.ProgressTracker +import net.corda.core.utilities.seconds + +/** + * Create new transaction to process the received transaction to settle [AssetTransfer] request. + * It accepts the [AssetTransfer] state's [linearId] as input to start this flow and collects the [Cash] and [Asset] input and output states from counter-party. + * For demo: + * 1. Clearing House set requestStatus to [RequestStatus.TRANSFERRED] if everything is okay + * (i.e. by offline verifying the data of [AssetTransfer.asset] is valid). + * + * On successful completion of a flow, [Asset] state ownership is transferred to `Buyer` party + * and [Cash] tokens equals to [Asset.purchaseCost] is transferred to `Seller` party. + */ +@StartableByRPC +class AssetSettlementInitiatorFlow(val linearId: UniqueIdentifier) : AbstractAssetSettlementFlow() { + companion object { + object INITIALISING : ProgressTracker.Step("Performing initial steps.") + object BUILDING : ProgressTracker.Step("Building and verifying transaction.") + object COLLECT_STATES : ProgressTracker.Step("Collect Asset and Cash states from counterparty.") + object IDENTITY_SYNC : ProgressTracker.Step("Sync identities with counter parties.") { + override fun childProgressTracker() = IdentitySyncFlow.Send.tracker() + } + + object SIGNING : ProgressTracker.Step("Signing transaction.") + object COLLECTING : ProgressTracker.Step("Collecting counterparty signature.") { + override fun childProgressTracker() = CollectSignaturesFlow.tracker() + } + + object FINALISING : ProgressTracker.Step("Finalising transaction.") { + override fun childProgressTracker() = FinalityFlow.tracker() + } + + fun tracker() = ProgressTracker(INITIALISING, BUILDING, COLLECT_STATES, IDENTITY_SYNC, SIGNING, COLLECTING, FINALISING) + } + + override val progressTracker: ProgressTracker = tracker() + + @Suspendable + override fun call(): SignedTransaction { + progressTracker.currentStep = INITIALISING + val inAssetTransfer = serviceHub.loadState(linearId, AssetTransfer::class.java) + val participants = inAssetTransfer.state.data.participants + val outAssetTransfer = inAssetTransfer.state.data.copy(status = RequestStatus.TRANSFERRED) + + if (ourIdentity.name != serviceHub.resolveIdentity(outAssetTransfer.clearingHouse!!).name) { + throw InvalidPartyException("Flow must be initiated by Custodian.") + } + + progressTracker.currentStep = BUILDING + val txb = TransactionBuilder(inAssetTransfer.state.notary) + .addInputState(inAssetTransfer) + .addOutputState(outAssetTransfer, AssetTransferContract.ASSET_TRANSFER_CONTRACT_ID) + .addCommand(AssetTransferContract.Commands.SettleRequest(), participants.map { it.owningKey }) + + progressTracker.currentStep = COLLECT_STATES + //Create temporary partial transaction. + val tempPtx = serviceHub.signInitialTransaction(txb) + val securitySellerSession = initiateFlow(serviceHub.resolveIdentity(outAssetTransfer.securitySeller)) + //Send partial transaction to Security Owner i.e. `Seller`. + subFlow(SendTransactionFlow(securitySellerSession, tempPtx)) + //Receive transaction with input and output of Asset state. + val assetPtx = subFlow(ReceiveTransactionUnVerifiedFlow(securitySellerSession)) + + //Send partial transaction to `Buyer`. + val securityBuyerSession = initiateFlow(serviceHub.resolveIdentity(outAssetTransfer.securityBuyer)) + subFlow(SendTransactionFlow(securityBuyerSession, tempPtx)) + //Send flows ID for soft lock of the cash state. + securityBuyerSession.send(txb.lockId) + //Receive and register the anonymous identity were created for Cash transfer. + subFlow(net.corda.confidential.IdentitySyncFlow.Receive(securityBuyerSession)) + //Receive transaction with input and output state of Cash state. + val cashPtx = subFlow(ReceiveTransactionUnVerifiedFlow(securityBuyerSession)) + + //Add Asset states and commands to origin Transaction Builder `txb`. + val assetLtx = assetPtx.toLedgerTransaction(serviceHub, false) + assetLtx.inputs.forEach { + txb.addInputState(it) + } + assetLtx.outputs.forEach { + txb.addOutputState(it) + } + assetLtx.commands.forEach { + txb.addCommand(Command(it.value, it.signers)) + } + + //Add Cash states and commands to origin Transaction Builder `txb`. + val cashLtx = cashPtx.toLedgerTransaction(serviceHub, false) + cashLtx.inputs.forEach { + txb.addInputState(it) + } + cashLtx.outputs.forEach { + txb.addOutputState(it) + } + cashLtx.commands.forEach { + txb.addCommand(Command(it.value, it.signers)) + } + + progressTracker.currentStep = IDENTITY_SYNC + val counterPartySessions = setOf(securityBuyerSession, securitySellerSession) + subFlow(IdentitySyncFlow.Send(counterPartySessions, + txb.toWireTransaction(serviceHub), + IDENTITY_SYNC.childProgressTracker())) + + progressTracker.currentStep = SIGNING + txb.setTimeWindow(serviceHub.clock.instant(), 60.seconds) + val ptx = serviceHub.signInitialTransaction(txb, outAssetTransfer.clearingHouse!!.owningKey) + + progressTracker.currentStep = COLLECTING + val stx = subFlow(CollectSignaturesFlow( + ptx, + counterPartySessions, + listOf(outAssetTransfer.clearingHouse!!.owningKey), + COLLECTING.childProgressTracker()) + ) + + progressTracker.currentStep = FINALISING + val ftx = subFlow(FinalityFlow(stx, FINALISING.childProgressTracker())) + return ftx + } +} diff --git a/cordapp-clearing-house/src/main/kotlin/com/synechron/cordapp/clearinghouse/flows/ConfirmAssetTransferRequestResponderFlow.kt b/cordapp-clearing-house/src/main/kotlin/com/synechron/cordapp/clearinghouse/flows/ConfirmAssetTransferRequestResponderFlow.kt new file mode 100644 index 0000000..525f3fe --- /dev/null +++ b/cordapp-clearing-house/src/main/kotlin/com/synechron/cordapp/clearinghouse/flows/ConfirmAssetTransferRequestResponderFlow.kt @@ -0,0 +1,22 @@ +package com.synechron.cordapp.clearinghouse.flows + +import co.paralleluniverse.fibers.Suspendable +import com.synechron.cordapp.common.flows.IdentitySyncFlow +import com.synechron.cordapp.common.flows.SignTxFlow +import com.synechron.cordapp.flows.AbstractConfirmAssetTransferRequestFlow +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.FlowSession +import net.corda.core.flows.InitiatedBy +import net.corda.core.transactions.SignedTransaction + +@InitiatedBy(AbstractConfirmAssetTransferRequestFlow::class) +class ConfirmAssetTransferRequestResponderFlow(private val otherSideSession: FlowSession) : FlowLogic() { + @Suspendable + override fun call(): SignedTransaction { + //Identity sync flow. + subFlow(IdentitySyncFlow.Receive(otherSideSession)) + //Transaction verification and signing. + val stx = subFlow(SignTxFlow(otherSideSession)) + return waitForLedgerCommit(stx.id) + } +} diff --git a/cordapp-clearing-house/src/main/resources/META-INF/services/net.corda.core.serialization.SerializationWhitelist b/cordapp-clearing-house/src/main/resources/META-INF/services/net.corda.core.serialization.SerializationWhitelist new file mode 100644 index 0000000..18b3213 --- /dev/null +++ b/cordapp-clearing-house/src/main/resources/META-INF/services/net.corda.core.serialization.SerializationWhitelist @@ -0,0 +1 @@ +# Register here any serialization whitelists for 3rd party classes extending from net.corda.core.serialization.SerializationWhitelist \ No newline at end of file diff --git a/cordapp-clearing-house/src/main/resources/certificates/readme.txt b/cordapp-clearing-house/src/main/resources/certificates/readme.txt new file mode 100644 index 0000000..a34e719 --- /dev/null +++ b/cordapp-clearing-house/src/main/resources/certificates/readme.txt @@ -0,0 +1 @@ +These certificates are used for development mode only. \ No newline at end of file diff --git a/cordapp-clearing-house/src/main/resources/certificates/sslkeystore.jks b/cordapp-clearing-house/src/main/resources/certificates/sslkeystore.jks new file mode 100644 index 0000000..8d24454 Binary files /dev/null and b/cordapp-clearing-house/src/main/resources/certificates/sslkeystore.jks differ diff --git a/cordapp-clearing-house/src/main/resources/certificates/truststore.jks b/cordapp-clearing-house/src/main/resources/certificates/truststore.jks new file mode 100644 index 0000000..f21f91e Binary files /dev/null and b/cordapp-clearing-house/src/main/resources/certificates/truststore.jks differ diff --git a/cordapp-common/build.gradle b/cordapp-common/build.gradle new file mode 100644 index 0000000..4dbd971 --- /dev/null +++ b/cordapp-common/build.gradle @@ -0,0 +1,48 @@ +repositories { + mavenLocal() + jcenter() + mavenCentral() + maven { url 'https://jitpack.io' } + maven { url 'https://ci-artifactory.corda.r3cev.com/artifactory/corda-releases' } +} + +apply plugin: 'kotlin' +apply plugin: 'net.corda.plugins.cordapp' +apply plugin: 'net.corda.plugins.cordformation' + +sourceSets { + main { + resources { + srcDir "../config/dev" + } + } + test { + resources { + srcDir "config/test" + } + } +} + +dependencies { + compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version" + + // Corda integration dependencies + cordaCompile "$corda_release_group:corda-core:$corda_release_version" + cordaCompile "$corda_release_group:corda-finance:$corda_release_version" + cordaCompile "$corda_release_group:corda-jackson:$corda_release_version" + cordaCompile "$corda_release_group:corda-rpc:$corda_release_version" + cordaCompile "$corda_release_group:corda-node-api:$corda_release_version" + cordaCompile "$corda_release_group:corda-webserver-impl:$corda_release_version" + cordaRuntime "$corda_release_group:corda:$corda_release_version" + cordaRuntime "$corda_release_group:corda-webserver:$corda_release_version" + cordapp project(":cordapp-contracts-states") +} + +tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { + kotlinOptions { + languageVersion = "1.1" + apiVersion = "1.1" + jvmTarget = "1.8" + javaParameters = true // Useful for reflection. + } +} \ No newline at end of file diff --git a/cordapp-common/src/main/kotlin/com/synechron/cordapp/common/exception/InvalidPartyException.kt b/cordapp-common/src/main/kotlin/com/synechron/cordapp/common/exception/InvalidPartyException.kt new file mode 100644 index 0000000..513e701 --- /dev/null +++ b/cordapp-common/src/main/kotlin/com/synechron/cordapp/common/exception/InvalidPartyException.kt @@ -0,0 +1,5 @@ +package com.synechron.cordapp.common.exception + +import net.corda.core.CordaRuntimeException + +class InvalidPartyException(override val message: String) : CordaRuntimeException(message) \ No newline at end of file diff --git a/cordapp-common/src/main/kotlin/com/synechron/cordapp/common/exception/StateNotFoundException.kt b/cordapp-common/src/main/kotlin/com/synechron/cordapp/common/exception/StateNotFoundException.kt new file mode 100644 index 0000000..37b4a20 --- /dev/null +++ b/cordapp-common/src/main/kotlin/com/synechron/cordapp/common/exception/StateNotFoundException.kt @@ -0,0 +1,5 @@ +package com.synechron.cordapp.common.exception + +import net.corda.core.CordaRuntimeException + +class StateNotFoundException(override val message: String) : CordaRuntimeException(message) \ No newline at end of file diff --git a/cordapp-common/src/main/kotlin/com/synechron/cordapp/common/exception/TooManyStatesFoundException.kt b/cordapp-common/src/main/kotlin/com/synechron/cordapp/common/exception/TooManyStatesFoundException.kt new file mode 100644 index 0000000..1c27f31 --- /dev/null +++ b/cordapp-common/src/main/kotlin/com/synechron/cordapp/common/exception/TooManyStatesFoundException.kt @@ -0,0 +1,5 @@ +package com.synechron.cordapp.common.exception + +import net.corda.core.CordaRuntimeException + +class TooManyStatesFoundException(override val message: String) : CordaRuntimeException(message) \ No newline at end of file diff --git a/cordapp-common/src/main/kotlin/com/synechron/cordapp/common/flows/IdentitySyncFlow.kt b/cordapp-common/src/main/kotlin/com/synechron/cordapp/common/flows/IdentitySyncFlow.kt new file mode 100644 index 0000000..cfef3ea --- /dev/null +++ b/cordapp-common/src/main/kotlin/com/synechron/cordapp/common/flows/IdentitySyncFlow.kt @@ -0,0 +1,100 @@ +package com.synechron.cordapp.common.flows + +import co.paralleluniverse.fibers.Suspendable +import net.corda.core.contracts.ContractState +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.FlowSession +import net.corda.core.identity.AbstractParty +import net.corda.core.identity.PartyAndCertificate +import net.corda.core.transactions.WireTransaction +import net.corda.core.utilities.ProgressTracker +import net.corda.core.utilities.unwrap + +object IdentitySyncFlow { + /** + * Flow for ensuring that one or more counterparties to a transaction have the full certificate paths of confidential + * identities used in the transaction. This is intended for use as a subflow of another flow, typically between + * transaction assembly and signing. An example of where this is useful is where a recipient of a [Cash] state wants + * to know that it is being paid by the correct party, and the owner of the state is a confidential identity of that + * party. This flow would send a copy of the confidential identity path to the recipient, enabling them to verify that + * identity. + * + * @return a mapping of well known identities to the confidential identities used in the transaction. + */ + class Send(val otherSideSessions: Set, + val tx: WireTransaction, + override val progressTracker: ProgressTracker) : FlowLogic() { + constructor(otherSide: FlowSession, tx: WireTransaction) : this(setOf(otherSide), tx, tracker()) + + companion object { + object SYNCING_IDENTITIES : ProgressTracker.Step("Syncing identities") + + fun tracker() = ProgressTracker(SYNCING_IDENTITIES) + } + + @Suspendable + override fun call() { + progressTracker.currentStep = SYNCING_IDENTITIES + val states: List = (tx.inputs.map { serviceHub.loadState(it) }.requireNoNulls().map { it.data } + tx.outputs.map { it.data }) + + val identities: Set = states.flatMap { it.participants }.toSet() + // Filter participants down to the set of those not in the network map (are not well known) + val confidentialIdentities = identities + .filter { serviceHub.networkMapCache.getNodesByLegalIdentityKey(it.owningKey).isEmpty() } + .toList() + + val identityCertificates: Map = identities + .map { Pair(it, serviceHub.identityService.certificateFromKey(it.owningKey)) }.toMap() + + otherSideSessions.forEach { otherSideSession -> + val requestedIdentities: List = otherSideSession.sendAndReceive>(confidentialIdentities).unwrap { req -> + require(req.all { it in identityCertificates.keys }) { "${otherSideSession.counterparty} requested a confidential identity not part of transaction: ${tx.id}" } + req + } + + val sendIdentities: List = requestedIdentities.map { + val identityCertificate = identityCertificates[it] + if (identityCertificate != null) + identityCertificate + else + throw IllegalStateException("Counterparty requested a confidential identity for which we do not have the certificate path: ${tx.id}") + } + otherSideSession.send(sendIdentities) + } + } + + } + + /** + * Handle an offer to provide proof of identity (in the form of certificate paths) for confidential identities which + * we do not yet know about. + */ + class Receive(val otherSideSession: FlowSession) : FlowLogic() { + companion object { + object RECEIVING_IDENTITIES : ProgressTracker.Step("Receiving confidential identities") + object RECEIVING_CERTIFICATES : ProgressTracker.Step("Receiving certificates for unknown identities") + } + + override val progressTracker: ProgressTracker = ProgressTracker(RECEIVING_IDENTITIES, RECEIVING_CERTIFICATES) + + @Suspendable + override fun call() { + progressTracker.currentStep = RECEIVING_IDENTITIES + val allIdentities = otherSideSession.receive>().unwrap { it } + val unknownIdentities = allIdentities.filter { serviceHub.identityService.wellKnownPartyFromAnonymous(it) == null } + + progressTracker.currentStep = RECEIVING_CERTIFICATES + val missingIdentities = otherSideSession.sendAndReceive>(unknownIdentities) + + // Batch verify the identities we've received, so we know they're all correct before we start storing them in + // the identity service + missingIdentities.unwrap { identities -> + identities.forEach { it.verify(serviceHub.identityService.trustAnchor) } + identities + }.forEach { identity -> + // Store the received confidential identities in the identity service so we have a record of which well known identity they map to. + serviceHub.identityService.verifyAndRegisterIdentity(identity) + } + } + } +} diff --git a/cordapp-common/src/main/kotlin/com/synechron/cordapp/common/flows/UtilityFlows.kt b/cordapp-common/src/main/kotlin/com/synechron/cordapp/common/flows/UtilityFlows.kt new file mode 100644 index 0000000..7cdc109 --- /dev/null +++ b/cordapp-common/src/main/kotlin/com/synechron/cordapp/common/flows/UtilityFlows.kt @@ -0,0 +1,38 @@ +package com.synechron.cordapp.common.flows + +import co.paralleluniverse.fibers.Suspendable +import net.corda.core.contracts.AttachmentResolutionException +import net.corda.core.contracts.TransactionResolutionException +import net.corda.core.contracts.requireThat +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.FlowSession +import net.corda.core.flows.SignTransactionFlow +import net.corda.core.internal.ResolveTransactionsFlow +import net.corda.core.transactions.SignedTransaction +import net.corda.core.utilities.unwrap + +class SignTxWihoutVerifyFlow(otherFlow: FlowSession) : SignTransactionFlow(otherFlow) { + override fun checkTransaction(stx: SignedTransaction) { + // TODO: Add checking here. + } +} + +class SignTxFlow(otherFlow: FlowSession) : SignTransactionFlow(otherFlow) { + override fun checkTransaction(stx: SignedTransaction) = requireThat { + "Must be signed by the initiator." using (stx.sigs.any()) + stx.verify(serviceHub, false) + } +} + +class ReceiveTransactionUnVerifiedFlow(private val otherSideSession: FlowSession) : FlowLogic() { + @Suspendable + @Throws(AttachmentResolutionException::class, + TransactionResolutionException::class) + override fun call(): SignedTransaction { + val stx = otherSideSession.receive().unwrap { + subFlow(ResolveTransactionsFlow(it, otherSideSession)) + it + } + return stx + } +} \ No newline at end of file diff --git a/cordapp-common/src/main/kotlin/com/synechron/cordapp/plugins/SerializationWhitelistImpl.kt b/cordapp-common/src/main/kotlin/com/synechron/cordapp/plugins/SerializationWhitelistImpl.kt new file mode 100644 index 0000000..bc20e90 --- /dev/null +++ b/cordapp-common/src/main/kotlin/com/synechron/cordapp/plugins/SerializationWhitelistImpl.kt @@ -0,0 +1,9 @@ +package com.synechron.cordapp.plugins + +import net.corda.core.serialization.SerializationWhitelist +import net.corda.core.transactions.TransactionBuilder + +class SerializationWhitelistImpl : SerializationWhitelist { + override val whitelist: List> + get() = listOf(TransactionBuilder::class.java) +} \ No newline at end of file diff --git a/cordapp-common/src/main/kotlin/com/synechron/cordapp/utils/Utils.kt b/cordapp-common/src/main/kotlin/com/synechron/cordapp/utils/Utils.kt new file mode 100644 index 0000000..b5e8408 --- /dev/null +++ b/cordapp-common/src/main/kotlin/com/synechron/cordapp/utils/Utils.kt @@ -0,0 +1,18 @@ +package com.synechron.cordapp.utils + +import com.synechron.cordapp.schema.AssetSchemaV1 +import com.synechron.cordapp.state.Asset +import net.corda.core.contracts.StateAndRef +import net.corda.core.flows.FlowException +import net.corda.core.node.ServiceHub +import net.corda.core.node.services.queryBy +import net.corda.core.node.services.vault.Builder.equal +import net.corda.core.node.services.vault.QueryCriteria + +fun ServiceHub.getAssetByCusip(cusip: String): StateAndRef { + val cusipExpr = AssetSchemaV1.PersistentAsset::cusip.equal(cusip) + val cusipCriteria = QueryCriteria.VaultCustomQueryCriteria(cusipExpr) + + return this.vaultService.queryBy(cusipCriteria).states.singleOrNull() + ?: throw FlowException("Asset with id $cusip not found.") +} \ No newline at end of file diff --git a/cordapp-common/src/main/resources/META-INF/services/net.corda.core.serialization.SerializationWhitelist b/cordapp-common/src/main/resources/META-INF/services/net.corda.core.serialization.SerializationWhitelist new file mode 100644 index 0000000..a476d51 --- /dev/null +++ b/cordapp-common/src/main/resources/META-INF/services/net.corda.core.serialization.SerializationWhitelist @@ -0,0 +1,2 @@ +# Register here any serialization whitelists for 3rd party classes extending from net.corda.core.serialization.SerializationWhitelist +com.synechron.cordapp.plugins.SerializationWhitelistImpl \ No newline at end of file diff --git a/cordapp-common/src/main/resources/certificates/readme.txt b/cordapp-common/src/main/resources/certificates/readme.txt new file mode 100644 index 0000000..a34e719 --- /dev/null +++ b/cordapp-common/src/main/resources/certificates/readme.txt @@ -0,0 +1 @@ +These certificates are used for development mode only. \ No newline at end of file diff --git a/cordapp-common/src/main/resources/certificates/sslkeystore.jks b/cordapp-common/src/main/resources/certificates/sslkeystore.jks new file mode 100644 index 0000000..8d24454 Binary files /dev/null and b/cordapp-common/src/main/resources/certificates/sslkeystore.jks differ diff --git a/cordapp-common/src/main/resources/certificates/truststore.jks b/cordapp-common/src/main/resources/certificates/truststore.jks new file mode 100644 index 0000000..f21f91e Binary files /dev/null and b/cordapp-common/src/main/resources/certificates/truststore.jks differ diff --git a/cordapp-contracts-states/build.gradle b/cordapp-contracts-states/build.gradle new file mode 100644 index 0000000..030bc69 --- /dev/null +++ b/cordapp-contracts-states/build.gradle @@ -0,0 +1,47 @@ +repositories { + mavenLocal() + jcenter() + mavenCentral() + maven { url 'https://jitpack.io' } + maven { url 'https://ci-artifactory.corda.r3cev.com/artifactory/corda-releases' } +} + +apply plugin: 'kotlin' +apply plugin: 'net.corda.plugins.cordapp' +apply plugin: 'net.corda.plugins.cordformation' + +sourceSets { + main { + resources { + srcDir "../config/dev" + } + } + test { + resources { + srcDir "config/test" + } + } +} + +dependencies { + compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version" + + // Corda integration dependencies + cordaCompile "$corda_release_group:corda-core:$corda_release_version" + cordaCompile "$corda_release_group:corda-finance:$corda_release_version" + cordaCompile "$corda_release_group:corda-jackson:$corda_release_version" + cordaCompile "$corda_release_group:corda-rpc:$corda_release_version" + cordaCompile "$corda_release_group:corda-node-api:$corda_release_version" + cordaCompile "$corda_release_group:corda-webserver-impl:$corda_release_version" + cordaRuntime "$corda_release_group:corda:$corda_release_version" + cordaRuntime "$corda_release_group:corda-webserver:$corda_release_version" +} + +tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { + kotlinOptions { + languageVersion = "1.1" + apiVersion = "1.1" + jvmTarget = "1.8" + javaParameters = true // Useful for reflection. + } +} \ No newline at end of file diff --git a/cordapp-contracts-states/src/main/kotlin/com/synechron/cordapp/contract/AssetContract.kt b/cordapp-contracts-states/src/main/kotlin/com/synechron/cordapp/contract/AssetContract.kt new file mode 100644 index 0000000..bee2af5 --- /dev/null +++ b/cordapp-contracts-states/src/main/kotlin/com/synechron/cordapp/contract/AssetContract.kt @@ -0,0 +1,68 @@ +package com.synechron.cordapp.contract + +import com.synechron.cordapp.state.Asset +import com.synechron.cordapp.state.AssetTransfer +import net.corda.core.contracts.* +import net.corda.core.transactions.LedgerTransaction +import net.corda.finance.contracts.asset.Cash +import net.corda.finance.utils.sumCash +import java.security.PublicKey + +class AssetContract : Contract { + companion object { + @JvmStatic + val ASSET_CONTRACT_ID = AssetContract::class.java.name!! + } + + interface Commands : CommandData { + class Create : TypeOnlyCommandData(), Commands + class Transfer : TypeOnlyCommandData(), Commands + } + + override fun verify(tx: LedgerTransaction) { + val command = tx.commands.requireSingleCommand() + val setOfSigners = command.signers.toSet() + when (command.value) { + is Commands.Create -> verifyCreate(tx, setOfSigners) + is Commands.Transfer -> verifyTransfer(tx, setOfSigners) + else -> throw IllegalArgumentException("Unrecognised command.") + } + } + + private fun verifyCreate(tx: LedgerTransaction, signers: Set) = requireThat { + "No inputs must be consumed." using (tx.inputStates.isEmpty()) + "Only one out state should be created." using (tx.outputStates.size == 1) + val output = tx.outputsOfType().single() + "Must have a positive amount." using (output.purchaseCost > output.purchaseCost.copy(quantity = 0)) + "Owner only may sign the Asset issue transaction." using (output.owner.owningKey in signers) + } + + private fun verifyTransfer(tx: LedgerTransaction, signers: Set) = requireThat { + val inputAssets = tx.inputsOfType() + val inputAssetTransfers = tx.inputsOfType() + "There must be one input obligation." using (inputAssets.size == 1) + // Check there are output cash states. + // We don't care about cash inputs, the Cash contract handles those. + val cash = tx.outputsOfType() + "There must be output cash." using (cash.isNotEmpty()) + + // Check that the cash is being assigned to us. + val inputAsset = inputAssets.single() + val inputAssetTransfer = inputAssetTransfers.single() + val acceptableCash = cash.filter { it.owner in listOf(inputAsset.owner, inputAssetTransfer.securitySeller) } + "There must be output cash paid to the recipient." using (acceptableCash.isNotEmpty()) + + // Sum the cash being sent to us (we don't care about the issuer). + val sumAcceptableCash = acceptableCash.sumCash().withoutIssuer() + "The amount settled must be equal to the asset's purchase cost amount." using (inputAsset.purchaseCost == sumAcceptableCash) + + val outputs = tx.outputsOfType() + // If the obligation has been partially settled then it should still exist. + "There must be one output Asset." using (outputs.size == 1) + + // Check only the paid property changes. + val output = outputs.single() + "Must not not change Asset data except owner field value." using (inputAsset == output.copy(owner = inputAsset.owner)) + "Owner only may sign the Asset issue transaction." using (output.owner.owningKey in signers) + } +} \ No newline at end of file diff --git a/cordapp-contracts-states/src/main/kotlin/com/synechron/cordapp/contract/AssetTransferContract.kt b/cordapp-contracts-states/src/main/kotlin/com/synechron/cordapp/contract/AssetTransferContract.kt new file mode 100644 index 0000000..9de251c --- /dev/null +++ b/cordapp-contracts-states/src/main/kotlin/com/synechron/cordapp/contract/AssetTransferContract.kt @@ -0,0 +1,43 @@ +package com.synechron.cordapp.contract + +import net.corda.core.contracts.* +import net.corda.core.transactions.LedgerTransaction +import java.security.PublicKey + +class AssetTransferContract : Contract { + companion object { + @JvmStatic + val ASSET_TRANSFER_CONTRACT_ID = AssetTransferContract::class.java.name!! + } + + interface Commands : CommandData { + class CreateRequest : TypeOnlyCommandData(), Commands + class ConfirmRequest : TypeOnlyCommandData(), Commands + class SettleRequest : TypeOnlyCommandData(), Commands + } + + override fun verify(tx: LedgerTransaction) { + val command = tx.commands.requireSingleCommand() + val setOfSigners = command.signers.toSet() + when (command.value) { + is Commands.CreateRequest -> verifyCreateRequest(tx, setOfSigners) + is Commands.ConfirmRequest -> verifyConfirmRequest(tx, setOfSigners) + is Commands.SettleRequest -> verifySettleRequest(tx, setOfSigners) + else -> throw IllegalArgumentException("Unrecognised command.") + } + } + + private fun verifyCreateRequest(tx: LedgerTransaction, signers: Set) = requireThat { + "No inputs must be consumed." using (tx.inputStates.isEmpty()) + "Only one out state should be created." using (tx.outputStates.size == 1) + //TODO Add more rules to verify create asset transfer request. + } + + private fun verifyConfirmRequest(tx: LedgerTransaction, signers: Set) = requireThat { + //TODO Add rules to confirm asset transfer request. + } + + private fun verifySettleRequest(tx: LedgerTransaction, signers: Set) = requireThat { + //TODO Add rules to settle asset transfer request. + } +} diff --git a/cordapp-contracts-states/src/main/kotlin/com/synechron/cordapp/contract/ContractUtils.kt b/cordapp-contracts-states/src/main/kotlin/com/synechron/cordapp/contract/ContractUtils.kt new file mode 100644 index 0000000..ac6f73f --- /dev/null +++ b/cordapp-contracts-states/src/main/kotlin/com/synechron/cordapp/contract/ContractUtils.kt @@ -0,0 +1,10 @@ +package com.synechron.cordapp.contract + +import net.corda.core.contracts.ContractState +import java.security.PublicKey + +fun keysFromParticipants(state: ContractState): Set { + return state.participants.map { + it.owningKey + }.toSet() +} \ No newline at end of file diff --git a/cordapp-contracts-states/src/main/kotlin/com/synechron/cordapp/exception/NotaryNotFoundException.kt b/cordapp-contracts-states/src/main/kotlin/com/synechron/cordapp/exception/NotaryNotFoundException.kt new file mode 100644 index 0000000..29f4164 --- /dev/null +++ b/cordapp-contracts-states/src/main/kotlin/com/synechron/cordapp/exception/NotaryNotFoundException.kt @@ -0,0 +1,5 @@ +package com.synechron.cordapp.obligation.exception + +import net.corda.core.CordaRuntimeException + +class NotaryNotFoundException(override val message: String) : CordaRuntimeException(message) \ No newline at end of file diff --git a/cordapp-contracts-states/src/main/kotlin/com/synechron/cordapp/exception/StateNotFoundOnVaultException.kt b/cordapp-contracts-states/src/main/kotlin/com/synechron/cordapp/exception/StateNotFoundOnVaultException.kt new file mode 100644 index 0000000..6928193 --- /dev/null +++ b/cordapp-contracts-states/src/main/kotlin/com/synechron/cordapp/exception/StateNotFoundOnVaultException.kt @@ -0,0 +1,5 @@ +package com.synechron.cordapp.obligation.exception + +import net.corda.core.CordaRuntimeException + +class StateNotFoundOnVaultException(override val message: String) : CordaRuntimeException(message) \ No newline at end of file diff --git a/cordapp-contracts-states/src/main/kotlin/com/synechron/cordapp/flows/AbstractAssetSettlementFlow.kt b/cordapp-contracts-states/src/main/kotlin/com/synechron/cordapp/flows/AbstractAssetSettlementFlow.kt new file mode 100644 index 0000000..6cfb62b --- /dev/null +++ b/cordapp-contracts-states/src/main/kotlin/com/synechron/cordapp/flows/AbstractAssetSettlementFlow.kt @@ -0,0 +1,7 @@ +package com.synechron.cordapp.flows + +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.InitiatingFlow + +@InitiatingFlow +abstract class AbstractAssetSettlementFlow : FlowLogic(), FlowLogicCommonMethods \ No newline at end of file diff --git a/cordapp-contracts-states/src/main/kotlin/com/synechron/cordapp/flows/AbstractConfirmAssetTransferRequestFlow.kt b/cordapp-contracts-states/src/main/kotlin/com/synechron/cordapp/flows/AbstractConfirmAssetTransferRequestFlow.kt new file mode 100644 index 0000000..f5fc53a --- /dev/null +++ b/cordapp-contracts-states/src/main/kotlin/com/synechron/cordapp/flows/AbstractConfirmAssetTransferRequestFlow.kt @@ -0,0 +1,7 @@ +package com.synechron.cordapp.flows + +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.InitiatingFlow + +@InitiatingFlow +abstract class AbstractConfirmAssetTransferRequestFlow : FlowLogic(), FlowLogicCommonMethods \ No newline at end of file diff --git a/cordapp-contracts-states/src/main/kotlin/com/synechron/cordapp/flows/AbstractCreateAssetTransferRequestFlow.kt b/cordapp-contracts-states/src/main/kotlin/com/synechron/cordapp/flows/AbstractCreateAssetTransferRequestFlow.kt new file mode 100644 index 0000000..1bdcd17 --- /dev/null +++ b/cordapp-contracts-states/src/main/kotlin/com/synechron/cordapp/flows/AbstractCreateAssetTransferRequestFlow.kt @@ -0,0 +1,7 @@ +package com.synechron.cordapp.flows + +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.InitiatingFlow + +@InitiatingFlow +abstract class AbstractCreateAssetTransferRequestFlow : FlowLogic(), FlowLogicCommonMethods \ No newline at end of file diff --git a/cordapp-contracts-states/src/main/kotlin/com/synechron/cordapp/flows/FlowLogicCommonMethods.kt b/cordapp-contracts-states/src/main/kotlin/com/synechron/cordapp/flows/FlowLogicCommonMethods.kt new file mode 100644 index 0000000..13aa403 --- /dev/null +++ b/cordapp-contracts-states/src/main/kotlin/com/synechron/cordapp/flows/FlowLogicCommonMethods.kt @@ -0,0 +1,34 @@ +package com.synechron.cordapp.flows + +import com.synechron.cordapp.obligation.exception.NotaryNotFoundException +import com.synechron.cordapp.obligation.exception.StateNotFoundOnVaultException +import net.corda.core.contracts.ContractState +import net.corda.core.contracts.StateAndRef +import net.corda.core.contracts.UniqueIdentifier +import net.corda.core.identity.AbstractParty +import net.corda.core.identity.Party +import net.corda.core.node.ServiceHub +import net.corda.core.node.services.Vault +import net.corda.core.node.services.vault.QueryCriteria + +/*** + * Interface offers default functions implementation for get first notary from NetworkMap, + * find State on vault by linearId, and resolve anonymous or abstract party to well known identity. + */ +interface FlowLogicCommonMethods { + fun ServiceHub.firstNotary(): Party { + return this.networkMapCache.notaryIdentities.firstOrNull() + ?: throw NotaryNotFoundException("No available notary.") + } + + fun ServiceHub.loadState(linearId: UniqueIdentifier, clazz: Class): StateAndRef { + val queryCriteria = QueryCriteria.LinearStateQueryCriteria(null, + listOf(linearId), Vault.StateStatus.UNCONSUMED, null) + return this.vaultService.queryBy(clazz, queryCriteria).states.singleOrNull() + ?: throw StateNotFoundOnVaultException("State with id $linearId not found.") + } + + fun ServiceHub.resolveIdentity(abstractParty: AbstractParty): Party { + return this.identityService.requireWellKnownPartyFromAnonymous(abstractParty) + } +} \ No newline at end of file diff --git a/cordapp-contracts-states/src/main/kotlin/com/synechron/cordapp/schema/AssetSchema.kt b/cordapp-contracts-states/src/main/kotlin/com/synechron/cordapp/schema/AssetSchema.kt new file mode 100644 index 0000000..331dbe2 --- /dev/null +++ b/cordapp-contracts-states/src/main/kotlin/com/synechron/cordapp/schema/AssetSchema.kt @@ -0,0 +1,45 @@ +package com.synechron.cordapp.schema + +import net.corda.core.crypto.NullKeys +import net.corda.core.identity.AbstractParty +import net.corda.core.schemas.MappedSchema +import net.corda.core.schemas.PersistentState +import javax.persistence.* + +/** + * The family of schemas for [AssetSchema]. + */ +object AssetSchema + +/** + * First version of an [AssetSchema] schema. + */ +object AssetSchemaV1 : MappedSchema(schemaFamily = AssetSchema.javaClass, + version = 1, mappedTypes = listOf(PersistentAsset::class.java)) { + @Entity + @Table(name = "asset", indexes = arrayOf(Index(name = "idx_asset_owner", columnList = "owner"), + Index(name = "idx_asset_cusip", columnList = "cusip"))) + class PersistentAsset( + @Column(name = "cusip") + val cusip: String, + + @Column(name = "asset_name") + val assetName: String, + + @Column(name = "purchase_cost") + val purchaseCost: String, + + @Column(name = "owner") + val owner: AbstractParty, + + @ElementCollection + @Column(name = "participants") + @CollectionTable(name = "asset_participants", joinColumns = arrayOf( + JoinColumn(name = "output_index", referencedColumnName = "output_index"), + JoinColumn(name = "transaction_id", referencedColumnName = "transaction_id"))) + var participants: MutableSet? = null + ) : PersistentState() { + constructor() : this("default-constructor-required-for-hibernate", "", "", NullKeys.NULL_PARTY, mutableSetOf()) + } +} + diff --git a/cordapp-contracts-states/src/main/kotlin/com/synechron/cordapp/schema/AssetTransferSchema.kt b/cordapp-contracts-states/src/main/kotlin/com/synechron/cordapp/schema/AssetTransferSchema.kt new file mode 100644 index 0000000..fa28907 --- /dev/null +++ b/cordapp-contracts-states/src/main/kotlin/com/synechron/cordapp/schema/AssetTransferSchema.kt @@ -0,0 +1,52 @@ +package com.synechron.cordapp.schema + +import net.corda.core.crypto.NullKeys +import net.corda.core.identity.AbstractParty +import net.corda.core.schemas.MappedSchema +import net.corda.core.schemas.PersistentState +import javax.persistence.* + +/** + * The family of schemas for [AssetTransferSchema]. + */ +object AssetTransferSchema + +/** + * First version of an [AssetTransferSchema] schema. + */ +object AssetTransferSchemaV1 : MappedSchema(schemaFamily = AssetSchema.javaClass, + version = 1, mappedTypes = listOf(PersistentAssetTransfer::class.java)) { + @Entity + @Table(name = "asset_transfer", indexes = arrayOf(Index(name = "idx_asset_transfer_linearId", columnList = "linear_id"), + Index(name = "idx_asset_transfer_cusip", columnList = "cusip"))) + class PersistentAssetTransfer( + @Column(name = "cusip") + val cusip: String, + + @Column(name = "lender_of_security") + val securitySeller: AbstractParty, + + @Column(name = "lender_of_cash") + val securityBuyer: AbstractParty, + + @Column(name = "clearing_house") + val clearingHouse: AbstractParty?, + + @Column(name = "status") + val status: String, + + @ElementCollection + @Column(name = "participants") + @CollectionTable(name = "asset_transfer_participants", joinColumns = arrayOf( + JoinColumn(name = "output_index", referencedColumnName = "output_index"), + JoinColumn(name = "transaction_id", referencedColumnName = "transaction_id"))) + var participants: MutableSet? = null, + + @Column(name = "linear_id") + val linearId: String + ) : PersistentState() { + constructor() : this("default-constructor-required-for-hibernate", NullKeys.NULL_PARTY, NullKeys.NULL_PARTY, + NullKeys.NULL_PARTY, "", mutableSetOf(), "") + } +} + diff --git a/cordapp-contracts-states/src/main/kotlin/com/synechron/cordapp/state/Asset.kt b/cordapp-contracts-states/src/main/kotlin/com/synechron/cordapp/state/Asset.kt new file mode 100644 index 0000000..b07b5ba --- /dev/null +++ b/cordapp-contracts-states/src/main/kotlin/com/synechron/cordapp/state/Asset.kt @@ -0,0 +1,46 @@ +package com.synechron.cordapp.state + +import com.synechron.cordapp.contract.AssetContract +import com.synechron.cordapp.schema.AssetSchemaV1 +import net.corda.core.contracts.Amount +import net.corda.core.contracts.CommandAndState +import net.corda.core.contracts.OwnableState +import net.corda.core.crypto.NullKeys +import net.corda.core.identity.AbstractParty +import net.corda.core.schemas.MappedSchema +import net.corda.core.schemas.PersistentState +import net.corda.core.schemas.QueryableState +import java.util.* + +/** + * This states plays role of digital asset (i.e. bond, securities, stock, etc.) on ledger. + */ +//TODO Think of using [FungibleAsset] interface to implement [Asset] state. +data class Asset(val cusip: String, + val assetName: String, + val purchaseCost: Amount, + override val owner: AbstractParty +) : OwnableState, QueryableState { + override val participants: List = listOf(owner) + + fun withoutOwner() = copy(owner = NullKeys.NULL_PARTY) + + override fun withNewOwner(newOwner: AbstractParty): CommandAndState { + return CommandAndState(AssetContract.Commands.Transfer(), this.copy(owner = newOwner)) + } + + override fun generateMappedObject(schema: MappedSchema): PersistentState { + return when (schema) { + is AssetSchemaV1 -> AssetSchemaV1.PersistentAsset( + cusip = this.cusip, + assetName = this.assetName, + purchaseCost = this.purchaseCost.toString(), + owner = this.owner, + participants = this.participants.toMutableSet() + ) + else -> throw IllegalArgumentException("Unrecognised schema $schema") + } + } + + override fun supportedSchemas(): Iterable = setOf(AssetSchemaV1) +} \ No newline at end of file diff --git a/cordapp-contracts-states/src/main/kotlin/com/synechron/cordapp/state/AssetTransfer.kt b/cordapp-contracts-states/src/main/kotlin/com/synechron/cordapp/state/AssetTransfer.kt new file mode 100644 index 0000000..bdaba21 --- /dev/null +++ b/cordapp-contracts-states/src/main/kotlin/com/synechron/cordapp/state/AssetTransfer.kt @@ -0,0 +1,48 @@ +package com.synechron.cordapp.state + +import com.synechron.cordapp.schema.AssetTransferSchemaV1 +import com.fasterxml.jackson.annotation.JsonValue +import net.corda.core.contracts.LinearState +import net.corda.core.contracts.UniqueIdentifier +import net.corda.core.identity.AbstractParty +import net.corda.core.schemas.MappedSchema +import net.corda.core.schemas.PersistentState +import net.corda.core.schemas.QueryableState +import net.corda.core.serialization.CordaSerializable + +/** + * This state acting as deal data before the actual [Asset] being transfer to target buyer party on settlement. + */ +data class AssetTransfer(val asset: Asset, + val securitySeller: AbstractParty, + val securityBuyer: AbstractParty, + val clearingHouse: AbstractParty?, + val status: RequestStatus, + override val participants: List = listOf(securityBuyer, securitySeller), + override val linearId: UniqueIdentifier = UniqueIdentifier()) : LinearState, QueryableState { + override fun generateMappedObject(schema: MappedSchema): PersistentState { + return when (schema) { + is AssetTransferSchemaV1 -> AssetTransferSchemaV1.PersistentAssetTransfer( + cusip = this.asset.cusip, + securitySeller = this.securitySeller, + securityBuyer = this.securityBuyer, + clearingHouse = this.clearingHouse, + status = this.status.value, + participants = this.participants.toMutableSet(), + linearId = this.linearId.toString() + ) + else -> throw IllegalArgumentException("Unrecognised schema $schema") + } + } + + override fun supportedSchemas(): Iterable = setOf(AssetTransferSchemaV1) +} + +@CordaSerializable +enum class RequestStatus(@JsonValue val value: String) { + PENDING_CONFIRMATION("Pending Confirmation"), //Initial status + PENDING("Pending"), // updated by buyer + TRANSFERRED("Transferred"), // on valid asset data clearing house update this status + REJECTED("Rejected"), // on invalid asset data clearing house reject transaction with this status. + FAILED("Failed") // on fail of settlement e.g. with insufficient cash from Buyer party. +} \ No newline at end of file diff --git a/cordapp-security-buyer/build.gradle b/cordapp-security-buyer/build.gradle new file mode 100644 index 0000000..ccce5d4 --- /dev/null +++ b/cordapp-security-buyer/build.gradle @@ -0,0 +1,75 @@ +repositories { + mavenLocal() + jcenter() + mavenCentral() + maven { url 'https://jitpack.io' } + maven { url 'https://ci-artifactory.corda.r3cev.com/artifactory/corda-releases' } +} + +apply plugin: 'kotlin' +apply plugin: 'net.corda.plugins.cordapp' +apply plugin: 'net.corda.plugins.cordformation' +apply plugin: 'net.corda.plugins.quasar-utils' + +sourceSets { + main { + resources { + srcDir "config/dev" + } + } + test { + resources { + srcDir "config/test" + } + } + integrationTest { + kotlin { + compileClasspath += main.output + test.output + runtimeClasspath += main.output + test.output + srcDir file('src/integration-test/kotlin') + } + } +} + +configurations { + integrationTestCompile.extendsFrom testCompile + integrationTestRuntime.extendsFrom testRuntime +} + +dependencies { + compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version" + testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version" + testCompile "junit:junit:$junit_version" + + // Corda integration dependencies + cordaCompile "$corda_release_group:corda-core:$corda_release_version" + cordaCompile "$corda_release_group:corda-finance:$corda_release_version" + cordaCompile "$corda_release_group:corda-jackson:$corda_release_version" + cordaCompile "$corda_release_group:corda-rpc:$corda_release_version" + cordaCompile "$corda_release_group:corda-node-api:$corda_release_version" + cordaCompile "$corda_release_group:corda-webserver-impl:$corda_release_version" + cordaRuntime "$corda_release_group:corda:$corda_release_version" + cordaRuntime "$corda_release_group:corda-webserver:$corda_release_version" + + testCompile "$corda_release_group:corda-node-driver:$corda_release_version" + + // CorDapp dependencies + // Specify your CorDapp's dependencies below, including dependent CorDapps. + // We've defined Cash as a dependent CorDapp as an example. + cordapp project(":cordapp-contracts-states") + cordapp project(":cordapp-common") +} + +task integrationTest(type: Test, dependsOn: []) { + testClassesDirs = sourceSets.integrationTest.output.classesDirs + classpath = sourceSets.integrationTest.runtimeClasspath +} + +tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { + kotlinOptions { + languageVersion = "1.1" + apiVersion = "1.1" + jvmTarget = "1.8" + javaParameters = true // Useful for reflection. + } +} \ No newline at end of file diff --git a/cordapp-security-buyer/src/main/kotlin/com/synechron/cordapp/buyer/flows/AssetSettlementResponderFlow.kt b/cordapp-security-buyer/src/main/kotlin/com/synechron/cordapp/buyer/flows/AssetSettlementResponderFlow.kt new file mode 100644 index 0000000..bb810ac --- /dev/null +++ b/cordapp-security-buyer/src/main/kotlin/com/synechron/cordapp/buyer/flows/AssetSettlementResponderFlow.kt @@ -0,0 +1,61 @@ +package com.synechron.cordapp.buyer.flows + +import co.paralleluniverse.fibers.Suspendable +import com.synechron.cordapp.common.exception.TooManyStatesFoundException +import com.synechron.cordapp.common.flows.IdentitySyncFlow +import com.synechron.cordapp.common.flows.SignTxFlow +import com.synechron.cordapp.flows.AbstractAssetSettlementFlow +import com.synechron.cordapp.state.AssetTransfer +import net.corda.core.flows.* +import net.corda.core.node.StatesToRecord +import net.corda.core.transactions.SignedTransaction +import net.corda.core.transactions.TransactionBuilder +import net.corda.core.utilities.ProgressTracker +import net.corda.core.utilities.unwrap +import net.corda.finance.contracts.asset.Cash +import java.util.* + +/** + * Buyer review the received settlement transaction then issue the cash to `Seller` party. + */ +@InitiatedBy(AbstractAssetSettlementFlow::class) +class AssetSettlementResponderFlow(val otherSideSession: FlowSession) : FlowLogic() { + companion object { + object ADD_CASH : ProgressTracker.Step("Add cash states.") + object SYNC_IDENTITY : ProgressTracker.Step("Sync identities.") + } + + override val progressTracker: ProgressTracker = ProgressTracker(ADD_CASH, SYNC_IDENTITY) + + @Suspendable + override fun call(): SignedTransaction { + progressTracker.currentStep = ADD_CASH + val ptx1 = subFlow(ReceiveTransactionFlow(otherSideSession, false, StatesToRecord.NONE)) + val ltx1 = ptx1.toLedgerTransaction(serviceHub, false) + val assetTransfer = ltx1.inputStates.filterIsInstance().singleOrNull() + ?: throw TooManyStatesFoundException("Transaction with more than one `AssetTransfer` " + + "input states received from `${otherSideSession.counterparty}` party") + //Using initiating flow id to soft lock reserve the Cash state. + val initiatingFlowId = otherSideSession.receive().unwrap { it } + + //TODO Does it required to send cashSignKeys to initiating party? + //Issue cash to security owner i.e. `Seller` party. + val (txbWithCash, cashSignKeys) = Cash.generateSpend(serviceHub, + TransactionBuilder(notary = ltx1.notary, lockId = initiatingFlowId), //soft reserve the cash state. + assetTransfer.asset.purchaseCost, + ourIdentityAndCert, + assetTransfer.securitySeller) + + val ptx2 = serviceHub.signInitialTransaction(txbWithCash) + //Send the anonymous identity created for cash transfer request. + subFlow(net.corda.confidential.IdentitySyncFlow.Send(otherSideSession, ptx2.tx)) + //Send the transaction that contain the Cash input and output states. + subFlow(SendTransactionFlow(otherSideSession, ptx2)) + + progressTracker.currentStep = SYNC_IDENTITY + subFlow(IdentitySyncFlow.Receive(otherSideSession)) + + val stx = subFlow(SignTxFlow(otherSideSession)) + return waitForLedgerCommit(stx.id) + } +} diff --git a/cordapp-security-buyer/src/main/kotlin/com/synechron/cordapp/buyer/flows/ConfirmAssetTransferRequestInitiatorFlow.kt b/cordapp-security-buyer/src/main/kotlin/com/synechron/cordapp/buyer/flows/ConfirmAssetTransferRequestInitiatorFlow.kt new file mode 100644 index 0000000..4f21875 --- /dev/null +++ b/cordapp-security-buyer/src/main/kotlin/com/synechron/cordapp/buyer/flows/ConfirmAssetTransferRequestInitiatorFlow.kt @@ -0,0 +1,108 @@ +package com.synechron.cordapp.buyer.flows + +import co.paralleluniverse.fibers.Suspendable +import com.synechron.cordapp.common.exception.InvalidPartyException +import com.synechron.cordapp.common.flows.IdentitySyncFlow +import com.synechron.cordapp.contract.AssetTransferContract +import com.synechron.cordapp.contract.AssetTransferContract.Companion.ASSET_TRANSFER_CONTRACT_ID +import com.synechron.cordapp.flows.AbstractConfirmAssetTransferRequestFlow +import com.synechron.cordapp.state.AssetTransfer +import com.synechron.cordapp.state.RequestStatus.PENDING +import net.corda.confidential.SwapIdentitiesFlow +import net.corda.core.contracts.UniqueIdentifier +import net.corda.core.flows.CollectSignaturesFlow +import net.corda.core.flows.FinalityFlow +import net.corda.core.flows.FlowException +import net.corda.core.flows.StartableByRPC +import net.corda.core.identity.Party +import net.corda.core.transactions.SignedTransaction +import net.corda.core.transactions.TransactionBuilder +import net.corda.core.utilities.ProgressTracker +import net.corda.core.utilities.ProgressTracker.Step +import net.corda.core.utilities.seconds + +/** + * The security buyer uses this flow to review and confirm received transaction from seller of security. + * If everything is okay then `Buyer` party initiate this flow to send received transaction to `Clearing House` for further + * verification and settlement. + */ +@StartableByRPC +class ConfirmAssetTransferRequestInitiatorFlow(val linearId: UniqueIdentifier, val clearingHouse: Party) + : AbstractConfirmAssetTransferRequestFlow() { + companion object { + object SWAP_IDENTITY : ProgressTracker.Step("Swap Identity.") + object INITIALISING : ProgressTracker.Step("Performing initial steps.") + object BUILDING : ProgressTracker.Step("Building and verifying transaction.") + object SIGNING : Step("Signing transaction.") + + object IDENTITY_SYNC : Step("Sync identities with counter parties.") { + override fun childProgressTracker() = IdentitySyncFlow.Send.tracker() + } + + object COLLECTING : Step("Collecting counterparty signature.") { + override fun childProgressTracker() = CollectSignaturesFlow.tracker() + } + + object FINALISING : Step("Finalising transaction.") { + override fun childProgressTracker() = FinalityFlow.tracker() + } + + fun tracker() = ProgressTracker(SWAP_IDENTITY, INITIALISING, BUILDING, SIGNING, IDENTITY_SYNC, COLLECTING, FINALISING) + } + + override val progressTracker: ProgressTracker = tracker() + + @Suspendable + override fun call(): SignedTransaction { + progressTracker.currentStep = SWAP_IDENTITY + val txKeys = subFlow(SwapIdentitiesFlow(clearingHouse)) + check(txKeys.size == 2) { "Something went wrong when generating confidential identities." } + val anonymousCustodian = txKeys[clearingHouse] + ?: throw FlowException("Couldn't create anonymous identity for `$clearingHouse` party.") + + progressTracker.currentStep = INITIALISING + val input = serviceHub.loadState(linearId, AssetTransfer::class.java) + val participants = input.state.data.participants + anonymousCustodian + val output = input.state.data.copy(clearingHouse = anonymousCustodian, participants = participants, status = PENDING) + + if (ourIdentity.name != serviceHub.resolveIdentity(output.securityBuyer).name) { + throw InvalidPartyException("Flow must be initiated by Lender Of Cash.") + } + //TODO verify clearingHouse party should not be one of [output.securityBuyer, output.securitySeller]. + + progressTracker.currentStep = BUILDING + val txb = TransactionBuilder(input.state.notary) + .addInputState(input) + .addOutputState(output, ASSET_TRANSFER_CONTRACT_ID) + .addCommand(AssetTransferContract.Commands.ConfirmRequest(), participants.map { it.owningKey }) + .setTimeWindow(serviceHub.clock.instant(), 60.seconds) + + progressTracker.currentStep = SIGNING + val ptx = serviceHub.signInitialTransaction(txb, output.securityBuyer.owningKey) + + //Get counterparty flow session. + val counterPartySessions = participants.map { serviceHub.resolveIdentity(it) }.filter { it.name != ourIdentity.name } + .map { initiateFlow(it) }.toSet() + + progressTracker.currentStep = IDENTITY_SYNC + subFlow(IdentitySyncFlow.Send( + counterPartySessions, + txb.toWireTransaction(serviceHub), + IDENTITY_SYNC.childProgressTracker()) + ) + + // Step 5. Get the counter-party signatures. + progressTracker.currentStep = COLLECTING + val stx = subFlow(CollectSignaturesFlow( + ptx, + counterPartySessions, + listOf(output.securityBuyer.owningKey), + COLLECTING.childProgressTracker()) + ) + + // Step 6. Finalise the transaction. + progressTracker.currentStep = FINALISING + val ftx = subFlow(FinalityFlow(stx, FINALISING.childProgressTracker())) + return ftx + } +} \ No newline at end of file diff --git a/cordapp-security-buyer/src/main/kotlin/com/synechron/cordapp/buyer/flows/CreateAssetTransferRequestResponderFlow.kt b/cordapp-security-buyer/src/main/kotlin/com/synechron/cordapp/buyer/flows/CreateAssetTransferRequestResponderFlow.kt new file mode 100644 index 0000000..866f58c --- /dev/null +++ b/cordapp-security-buyer/src/main/kotlin/com/synechron/cordapp/buyer/flows/CreateAssetTransferRequestResponderFlow.kt @@ -0,0 +1,19 @@ +package com.synechron.cordapp.buyer.flows + +import co.paralleluniverse.fibers.Suspendable +import com.synechron.cordapp.common.flows.SignTxFlow +import com.synechron.cordapp.flows.AbstractCreateAssetTransferRequestFlow +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.FlowSession +import net.corda.core.flows.InitiatedBy +import net.corda.core.transactions.SignedTransaction + +@InitiatedBy(AbstractCreateAssetTransferRequestFlow::class) +class CreateAssetTransferRequestResponderFlow(private val otherSideSession: FlowSession) : FlowLogic() { + @Suspendable + override fun call(): SignedTransaction { + //Transaction verification and signing. + val stx = subFlow(SignTxFlow(otherSideSession)) + return waitForLedgerCommit(stx.id) + } +} \ No newline at end of file diff --git a/cordapp-security-buyer/src/main/resources/META-INF/services/net.corda.core.serialization.SerializationWhitelist b/cordapp-security-buyer/src/main/resources/META-INF/services/net.corda.core.serialization.SerializationWhitelist new file mode 100644 index 0000000..3e75dce --- /dev/null +++ b/cordapp-security-buyer/src/main/resources/META-INF/services/net.corda.core.serialization.SerializationWhitelist @@ -0,0 +1 @@ +# Register here any serialization whitelists for 3rd party classes extending from net.corda.core.serialization.SerializationWhitelist diff --git a/cordapp-security-buyer/src/main/resources/certificates/readme.txt b/cordapp-security-buyer/src/main/resources/certificates/readme.txt new file mode 100644 index 0000000..a34e719 --- /dev/null +++ b/cordapp-security-buyer/src/main/resources/certificates/readme.txt @@ -0,0 +1 @@ +These certificates are used for development mode only. \ No newline at end of file diff --git a/cordapp-security-buyer/src/main/resources/certificates/sslkeystore.jks b/cordapp-security-buyer/src/main/resources/certificates/sslkeystore.jks new file mode 100644 index 0000000..8d24454 Binary files /dev/null and b/cordapp-security-buyer/src/main/resources/certificates/sslkeystore.jks differ diff --git a/cordapp-security-buyer/src/main/resources/certificates/truststore.jks b/cordapp-security-buyer/src/main/resources/certificates/truststore.jks new file mode 100644 index 0000000..f21f91e Binary files /dev/null and b/cordapp-security-buyer/src/main/resources/certificates/truststore.jks differ diff --git a/cordapp-security-seller/build.gradle b/cordapp-security-seller/build.gradle new file mode 100644 index 0000000..ccce5d4 --- /dev/null +++ b/cordapp-security-seller/build.gradle @@ -0,0 +1,75 @@ +repositories { + mavenLocal() + jcenter() + mavenCentral() + maven { url 'https://jitpack.io' } + maven { url 'https://ci-artifactory.corda.r3cev.com/artifactory/corda-releases' } +} + +apply plugin: 'kotlin' +apply plugin: 'net.corda.plugins.cordapp' +apply plugin: 'net.corda.plugins.cordformation' +apply plugin: 'net.corda.plugins.quasar-utils' + +sourceSets { + main { + resources { + srcDir "config/dev" + } + } + test { + resources { + srcDir "config/test" + } + } + integrationTest { + kotlin { + compileClasspath += main.output + test.output + runtimeClasspath += main.output + test.output + srcDir file('src/integration-test/kotlin') + } + } +} + +configurations { + integrationTestCompile.extendsFrom testCompile + integrationTestRuntime.extendsFrom testRuntime +} + +dependencies { + compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version" + testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version" + testCompile "junit:junit:$junit_version" + + // Corda integration dependencies + cordaCompile "$corda_release_group:corda-core:$corda_release_version" + cordaCompile "$corda_release_group:corda-finance:$corda_release_version" + cordaCompile "$corda_release_group:corda-jackson:$corda_release_version" + cordaCompile "$corda_release_group:corda-rpc:$corda_release_version" + cordaCompile "$corda_release_group:corda-node-api:$corda_release_version" + cordaCompile "$corda_release_group:corda-webserver-impl:$corda_release_version" + cordaRuntime "$corda_release_group:corda:$corda_release_version" + cordaRuntime "$corda_release_group:corda-webserver:$corda_release_version" + + testCompile "$corda_release_group:corda-node-driver:$corda_release_version" + + // CorDapp dependencies + // Specify your CorDapp's dependencies below, including dependent CorDapps. + // We've defined Cash as a dependent CorDapp as an example. + cordapp project(":cordapp-contracts-states") + cordapp project(":cordapp-common") +} + +task integrationTest(type: Test, dependsOn: []) { + testClassesDirs = sourceSets.integrationTest.output.classesDirs + classpath = sourceSets.integrationTest.runtimeClasspath +} + +tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { + kotlinOptions { + languageVersion = "1.1" + apiVersion = "1.1" + jvmTarget = "1.8" + javaParameters = true // Useful for reflection. + } +} \ No newline at end of file diff --git a/cordapp-security-seller/src/main/kotlin/com/synechron/cordapp/seller/flows/AssetSettlementResponderFlow.kt b/cordapp-security-seller/src/main/kotlin/com/synechron/cordapp/seller/flows/AssetSettlementResponderFlow.kt new file mode 100644 index 0000000..2ca5f4c --- /dev/null +++ b/cordapp-security-seller/src/main/kotlin/com/synechron/cordapp/seller/flows/AssetSettlementResponderFlow.kt @@ -0,0 +1,58 @@ +package com.synechron.cordapp.seller.flows + +import co.paralleluniverse.fibers.Suspendable +import com.synechron.cordapp.common.exception.TooManyStatesFoundException +import com.synechron.cordapp.common.flows.IdentitySyncFlow +import com.synechron.cordapp.common.flows.SignTxFlow +import com.synechron.cordapp.contract.AssetContract +import com.synechron.cordapp.flows.AbstractAssetSettlementFlow +import com.synechron.cordapp.flows.FlowLogicCommonMethods +import com.synechron.cordapp.state.AssetTransfer +import com.synechron.cordapp.utils.getAssetByCusip +import net.corda.core.contracts.Command +import net.corda.core.flows.* +import net.corda.core.node.StatesToRecord +import net.corda.core.transactions.SignedTransaction +import net.corda.core.transactions.TransactionBuilder +import net.corda.core.utilities.ProgressTracker + +/** + * Seller review the received settlement transaction then create and send new temporary transaction + * to send input, output [Asset] states and command to change ownership to `Buyer` party. + */ +@InitiatedBy(AbstractAssetSettlementFlow::class) +class AssetSettlementResponderFlow(val otherSideSession: FlowSession) : FlowLogic(), FlowLogicCommonMethods { + companion object { + object ADD_ASSET : ProgressTracker.Step("Add Asset states to transaction builder.") + object SYNC_IDENTITY : ProgressTracker.Step("Sync identities.") + } + + override val progressTracker: ProgressTracker = ProgressTracker(ADD_ASSET, SYNC_IDENTITY) + + @Suspendable + override fun call(): SignedTransaction { + progressTracker.currentStep = ADD_ASSET + val ptx1 = subFlow(ReceiveTransactionFlow(otherSideSession, false, StatesToRecord.NONE)) + val ltx1 = ptx1.toLedgerTransaction(serviceHub, false) + + val assetTransfer = ltx1.inputStates.filterIsInstance().singleOrNull() + ?: throw TooManyStatesFoundException("Transaction with more than one `AssetTransfer` " + + "input states received from `${otherSideSession.counterparty}` party") + val assetStateAndRef = serviceHub.getAssetByCusip(assetTransfer.asset.cusip) + val (cmd, assetOutState) = assetStateAndRef.state.data.withNewOwner(assetTransfer.securityBuyer) + + val txb = TransactionBuilder(ltx1.notary) + txb.addInputState(assetStateAndRef) + txb.addOutputState(assetOutState, AssetContract.ASSET_CONTRACT_ID) + txb.addCommand(Command(cmd, assetOutState.owner.owningKey)) + val ptx2 = serviceHub.signInitialTransaction(txb) + //Send transaction with required in/out Asset state and command. + subFlow(SendTransactionFlow(otherSideSession, ptx2)) + + progressTracker.currentStep = SYNC_IDENTITY + subFlow(IdentitySyncFlow.Receive(otherSideSession)) + + val stx = subFlow(SignTxFlow(otherSideSession)) + return waitForLedgerCommit(stx.id) + } +} \ No newline at end of file diff --git a/cordapp-security-seller/src/main/kotlin/com/synechron/cordapp/seller/flows/ConfirmAssetTransferRequestHandlerFlow.kt b/cordapp-security-seller/src/main/kotlin/com/synechron/cordapp/seller/flows/ConfirmAssetTransferRequestHandlerFlow.kt new file mode 100644 index 0000000..e773b5c --- /dev/null +++ b/cordapp-security-seller/src/main/kotlin/com/synechron/cordapp/seller/flows/ConfirmAssetTransferRequestHandlerFlow.kt @@ -0,0 +1,22 @@ +package com.synechron.cordapp.seller.flows + +import co.paralleluniverse.fibers.Suspendable +import com.synechron.cordapp.common.flows.IdentitySyncFlow +import com.synechron.cordapp.common.flows.SignTxFlow +import com.synechron.cordapp.flows.AbstractConfirmAssetTransferRequestFlow +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.FlowSession +import net.corda.core.flows.InitiatedBy +import net.corda.core.transactions.SignedTransaction + +@InitiatedBy(AbstractConfirmAssetTransferRequestFlow::class) +class ConfirmAssetTransferRequestHandlerFlow(private val otherSideSession: FlowSession) : FlowLogic() { + @Suspendable + override fun call(): SignedTransaction { + //Identity sync flow. + subFlow(IdentitySyncFlow.Receive(otherSideSession)) + //Transaction verification and signing. + val stx = subFlow(SignTxFlow(otherSideSession)) + return waitForLedgerCommit(stx.id) + } +} diff --git a/cordapp-security-seller/src/main/kotlin/com/synechron/cordapp/seller/flows/CreateAssetStateFlow.kt b/cordapp-security-seller/src/main/kotlin/com/synechron/cordapp/seller/flows/CreateAssetStateFlow.kt new file mode 100644 index 0000000..ef414b8 --- /dev/null +++ b/cordapp-security-seller/src/main/kotlin/com/synechron/cordapp/seller/flows/CreateAssetStateFlow.kt @@ -0,0 +1,67 @@ +package com.synechron.cordapp.seller.flows + +import co.paralleluniverse.fibers.Suspendable +import com.synechron.cordapp.contract.AssetContract +import com.synechron.cordapp.contract.AssetContract.Companion.ASSET_CONTRACT_ID +import com.synechron.cordapp.flows.FlowLogicCommonMethods +import com.synechron.cordapp.state.Asset +import net.corda.core.contracts.Amount +import net.corda.core.flows.FinalityFlow +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.InitiatingFlow +import net.corda.core.flows.StartableByRPC +import net.corda.core.transactions.SignedTransaction +import net.corda.core.transactions.TransactionBuilder +import net.corda.core.utilities.ProgressTracker +import net.corda.core.utilities.ProgressTracker.Step +import net.corda.core.utilities.seconds +import java.util.* + +/** + * Create the [Asset] state on ledger. This state acting as security/bond on ledger which going to be sold for cash. + */ +object CreateAssetStateFlow { + @InitiatingFlow + @StartableByRPC + class Initiator(val cusip: String, + val assetName: String, + val purchaseCost: Amount) : FlowLogic(), FlowLogicCommonMethods { + + companion object { + object INITIALISING : ProgressTracker.Step("Performing initial steps.") + object BUILDING : ProgressTracker.Step("Building and verifying transaction.") + object SIGNING : Step("Signing transaction.") + + object FINALISING : Step("Finalising transaction.") { + override fun childProgressTracker() = FinalityFlow.tracker() + } + + fun tracker() = ProgressTracker(INITIALISING, BUILDING, SIGNING, FINALISING) + } + + override val progressTracker: ProgressTracker = tracker() + + @Suspendable + override fun call(): SignedTransaction { + // Step 1. Initialisation. + progressTracker.currentStep = INITIALISING + val asset = Asset(cusip, assetName, purchaseCost, ourIdentity) + + // Step 2. Building. + progressTracker.currentStep = BUILDING + val txb = TransactionBuilder(serviceHub.firstNotary()) + .addOutputState(asset, ASSET_CONTRACT_ID) + .addCommand(AssetContract.Commands.Create(), ourIdentity.owningKey) + .setTimeWindow(serviceHub.clock.instant(), 30.seconds) + + // Step 3. Sign the transaction. + progressTracker.currentStep = SIGNING + val stx = serviceHub.signInitialTransaction(txb) + + // Step 4. Finalise the transaction. + progressTracker.currentStep = FINALISING + val ftx = subFlow(FinalityFlow(stx, FINALISING.childProgressTracker())) + return ftx + } + } +} diff --git a/cordapp-security-seller/src/main/kotlin/com/synechron/cordapp/seller/flows/CreateAssetTransferRequestInitiatorFlow.kt b/cordapp-security-seller/src/main/kotlin/com/synechron/cordapp/seller/flows/CreateAssetTransferRequestInitiatorFlow.kt new file mode 100644 index 0000000..d6b1b1e --- /dev/null +++ b/cordapp-security-seller/src/main/kotlin/com/synechron/cordapp/seller/flows/CreateAssetTransferRequestInitiatorFlow.kt @@ -0,0 +1,89 @@ +package com.synechron.cordapp.seller.flows + +import co.paralleluniverse.fibers.Suspendable +import com.synechron.cordapp.common.exception.InvalidPartyException +import com.synechron.cordapp.contract.AssetTransferContract +import com.synechron.cordapp.contract.AssetTransferContract.Companion.ASSET_TRANSFER_CONTRACT_ID +import com.synechron.cordapp.flows.AbstractCreateAssetTransferRequestFlow +import com.synechron.cordapp.state.AssetTransfer +import com.synechron.cordapp.state.RequestStatus.PENDING_CONFIRMATION +import com.synechron.cordapp.utils.getAssetByCusip +import net.corda.confidential.SwapIdentitiesFlow +import net.corda.core.flows.CollectSignaturesFlow +import net.corda.core.flows.FinalityFlow +import net.corda.core.flows.FlowException +import net.corda.core.flows.StartableByRPC +import net.corda.core.identity.Party +import net.corda.core.transactions.SignedTransaction +import net.corda.core.transactions.TransactionBuilder +import net.corda.core.utilities.ProgressTracker +import net.corda.core.utilities.ProgressTracker.Step +import net.corda.core.utilities.seconds + +/** + * Owner of security (i.e. seller) creates [AssetTransfer] request state in-order to start deal with buyer. + */ +@StartableByRPC +class CreateAssetTransferRequestInitiatorFlow(val cusip: String, + val securityBuyer: Party) : AbstractCreateAssetTransferRequestFlow() { + + companion object { + object INITIALISING : ProgressTracker.Step("Performing initial steps.") + object BUILDING : ProgressTracker.Step("Building and verifying transaction.") + object SIGNING : Step("Signing transaction.") + object COLLECTING : Step("Collecting counterparty signature.") { + override fun childProgressTracker() = CollectSignaturesFlow.tracker() + } + + object FINALISING : Step("Finalising transaction.") { + override fun childProgressTracker() = FinalityFlow.tracker() + } + + fun tracker() = ProgressTracker(INITIALISING, BUILDING, SIGNING, COLLECTING, FINALISING) + } + + override val progressTracker: ProgressTracker = tracker() + + @Suspendable + override fun call(): SignedTransaction { + if (ourIdentity.name == securityBuyer.name) throw InvalidPartyException("Flow initiating party should not equals to Lender of Cash party.") + // Step 1. Initialisation. + progressTracker.currentStep = INITIALISING + val txKeys = subFlow(SwapIdentitiesFlow(securityBuyer)) + check(txKeys.size == 2) { "Something went wrong when generating confidential identities." } + + val anonymousMe = txKeys[ourIdentity] ?: throw FlowException("Couldn't create our anonymous identity.") + val anonymousCashLender = txKeys[securityBuyer] + ?: throw FlowException("Couldn't create lender's (securityBuyer) anonymous identity.") + + val asset = serviceHub.getAssetByCusip(cusip).state.data + val assetTransfer = AssetTransfer(asset, anonymousMe, anonymousCashLender, null, PENDING_CONFIRMATION) + val ourSigningKey = assetTransfer.securitySeller.owningKey + + // Step 2. Building. + progressTracker.currentStep = BUILDING + val txb = TransactionBuilder(serviceHub.firstNotary()) + .addOutputState(assetTransfer, ASSET_TRANSFER_CONTRACT_ID) + .addCommand(AssetTransferContract.Commands.CreateRequest(), assetTransfer.participants.map { it.owningKey }) + .setTimeWindow(serviceHub.clock.instant(), 30.seconds) + + // Step 3. Sign the transaction. + progressTracker.currentStep = SIGNING + val ptx = serviceHub.signInitialTransaction(txb, ourSigningKey) + + // Step 4. Get the counter-party signature. + progressTracker.currentStep = COLLECTING + val lenderOfCashFlowSession = initiateFlow(securityBuyer) + val stx = subFlow(CollectSignaturesFlow( + ptx, + setOf(lenderOfCashFlowSession), + listOf(ourSigningKey), + COLLECTING.childProgressTracker()) + ) + + // Step 5. Finalise the transaction. + progressTracker.currentStep = FINALISING + val ftx = subFlow(FinalityFlow(stx, FINALISING.childProgressTracker())) + return ftx + } +} \ No newline at end of file diff --git a/cordapp-security-seller/src/main/resources/META-INF/services/net.corda.core.serialization.SerializationWhitelist b/cordapp-security-seller/src/main/resources/META-INF/services/net.corda.core.serialization.SerializationWhitelist new file mode 100644 index 0000000..bee914d --- /dev/null +++ b/cordapp-security-seller/src/main/resources/META-INF/services/net.corda.core.serialization.SerializationWhitelist @@ -0,0 +1,2 @@ +# Register here any serialization whitelists for 3rd party classes extending from net.corda.core.serialization.SerializationWhitelist + diff --git a/cordapp-security-seller/src/main/resources/certificates/readme.txt b/cordapp-security-seller/src/main/resources/certificates/readme.txt new file mode 100644 index 0000000..a34e719 --- /dev/null +++ b/cordapp-security-seller/src/main/resources/certificates/readme.txt @@ -0,0 +1 @@ +These certificates are used for development mode only. \ No newline at end of file diff --git a/cordapp-security-seller/src/main/resources/certificates/sslkeystore.jks b/cordapp-security-seller/src/main/resources/certificates/sslkeystore.jks new file mode 100644 index 0000000..8d24454 Binary files /dev/null and b/cordapp-security-seller/src/main/resources/certificates/sslkeystore.jks differ diff --git a/cordapp-security-seller/src/main/resources/certificates/truststore.jks b/cordapp-security-seller/src/main/resources/certificates/truststore.jks new file mode 100644 index 0000000..f21f91e Binary files /dev/null and b/cordapp-security-seller/src/main/resources/certificates/truststore.jks differ diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..a6768d6 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,4 @@ +name=three-parties-dvp-atomic-tx-cordapp +group=com.synechron.cordapp +version=0.1-SNAPSHOT +kotlin.incremental=false \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..7a3265e Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..d38ec91 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri Aug 25 12:50:39 BST 2017 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..cccdd3d --- /dev/null +++ b/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..f955316 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/lib/README.txt b/lib/README.txt new file mode 100644 index 0000000..b0592c9 --- /dev/null +++ b/lib/README.txt @@ -0,0 +1,8 @@ +The Quasar.jar in this directory is for runtime instrumentation of classes by Quasar. + +When running corda outside of the given gradle building you must add the following flag with the +correct path to your call to Java: + + java -javaagent:path-to-quasar-jar.jar ... + +See the Quasar docs for more information: http://docs.paralleluniverse.co/quasar/ \ No newline at end of file diff --git a/lib/quasar.jar b/lib/quasar.jar new file mode 100644 index 0000000..286f065 Binary files /dev/null and b/lib/quasar.jar differ diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..af4b5cc --- /dev/null +++ b/settings.gradle @@ -0,0 +1,6 @@ +include 'cordapp-contracts-states' +include 'cordapp-common' +include 'cordapp-clearing-house' +include 'cordapp-security-buyer' +include 'cordapp-security-seller' + diff --git a/src/integrationTest/kotlin/com/synechron/cordapp/IntegrationTest.kt b/src/integrationTest/kotlin/com/synechron/cordapp/IntegrationTest.kt new file mode 100644 index 0000000..e655f3f --- /dev/null +++ b/src/integrationTest/kotlin/com/synechron/cordapp/IntegrationTest.kt @@ -0,0 +1,32 @@ +package com.synechron.cordapp + +import net.corda.core.identity.CordaX500Name +import net.corda.core.utilities.getOrThrow +import net.corda.testing.driver.DriverParameters +import net.corda.testing.driver.driver +import org.junit.Assert +import org.junit.Test + +class IntegrationTest { + private val nodeAName = CordaX500Name("NodeA", "", "GB") + private val nodeBName = CordaX500Name("NodeB", "", "US") + + @Test + fun `run driver test`() { + driver(DriverParameters(isDebug = true, startNodesInProcess = true)) { + // This starts three nodes simultaneously with startNode, which returns a future that completes when the node + // has completed startup. Then these are all resolved with getOrThrow which returns the NodeHandle list. + val (nodeAHandle, nodeBHandle) = listOf( + startNode(providedName = nodeAName), + startNode(providedName = nodeBName) + ).map { it.getOrThrow() } + + // This test will call via the RPC proxy to find a party of another node to verify that the nodes have + // started and can communicate. This is a very basic test, in practice tests would be starting flows, + // and verifying the states in the vault and other important metrics to ensure that your CorDapp is working + // as intended. + Assert.assertEquals(nodeAHandle.rpc.wellKnownPartyFromX500Name(nodeBName)!!.name, nodeBName) + Assert.assertEquals(nodeBHandle.rpc.wellKnownPartyFromX500Name(nodeAName)!!.name, nodeAName) + } + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/synechron/cordapp/NodeDriver.kt b/src/test/kotlin/com/synechron/cordapp/NodeDriver.kt new file mode 100644 index 0000000..e00d8d7 --- /dev/null +++ b/src/test/kotlin/com/synechron/cordapp/NodeDriver.kt @@ -0,0 +1,33 @@ +package com.synechron.cordapp + +import net.corda.core.identity.CordaX500Name +import net.corda.core.utilities.getOrThrow +import net.corda.testing.driver.DriverParameters +import net.corda.testing.driver.driver +import net.corda.testing.node.User + +/** + * This file is exclusively for being able to run your nodes through an IDE (as opposed to using deployNodes) + * Do not use in a production environment. + * + * To debug your CorDapp: + * + * 1. Run the "Run Template CorDapp" run configuration. + * 2. Wait for all the nodes to start. + * 3. Note the debug ports for each node, which should be output to the console. The "Debug CorDapp" configuration runs + * with port 5007, which should be "PartyA". In any case, double-check the console output to be sure. + * 4. Set your breakpoints in your CorDapp code. + * 5. Run the "Debug CorDapp" remote debug run configuration. + */ +fun main(args: Array) { + // No permissions required as we are not invoking flows. + val user = User("user1", "test", permissions = setOf("ALL")) + driver(DriverParameters(isDebug = true, waitForAllNodesToFinish = true)) { + val (partyA, partyB) = listOf( + startNode(providedName = CordaX500Name("PartyA", "London", "GB"), rpcUsers = listOf(user)), + startNode(providedName = CordaX500Name("PartyB", "New York", "US"), rpcUsers = listOf(user))).map { it.getOrThrow() } + + startWebserver(partyA) + startWebserver(partyB) + } +} diff --git a/src/test/kotlin/com/synechron/cordapp/flows/AbstractAssetJunitFlowTests.kt b/src/test/kotlin/com/synechron/cordapp/flows/AbstractAssetJunitFlowTests.kt new file mode 100644 index 0000000..0bc7fba --- /dev/null +++ b/src/test/kotlin/com/synechron/cordapp/flows/AbstractAssetJunitFlowTests.kt @@ -0,0 +1,137 @@ +package com.synechron.cordapp.flows + +import com.synechron.cordapp.buyer.flows.AssetSettlementResponderFlow +import com.synechron.cordapp.clearinghouse.flows.AssetSettlementInitiatorFlow +import com.synechron.cordapp.buyer.flows.ConfirmAssetTransferRequestInitiatorFlow +import com.synechron.cordapp.buyer.flows.CreateAssetTransferRequestResponderFlow +import com.synechron.cordapp.seller.flows.ConfirmAssetTransferRequestHandlerFlow +import com.synechron.cordapp.seller.flows.CreateAssetStateFlow +import com.synechron.cordapp.seller.flows.CreateAssetTransferRequestInitiatorFlow +import com.synechron.cordapp.state.Asset +import com.synechron.cordapp.obligation.exception.StateNotFoundOnVaultException +import net.corda.core.contracts.Amount +import net.corda.core.contracts.ContractState +import net.corda.core.contracts.StateAndRef +import net.corda.core.contracts.UniqueIdentifier +import net.corda.core.identity.AbstractParty +import net.corda.core.identity.Party +import net.corda.core.node.services.Vault +import net.corda.core.node.services.vault.QueryCriteria +import net.corda.core.transactions.SignedTransaction +import net.corda.core.utilities.OpaqueBytes +import net.corda.core.utilities.getOrThrow +import net.corda.finance.DOLLARS +import net.corda.finance.flows.CashIssueFlow +import net.corda.testing.internal.chooseIdentity +import net.corda.testing.node.MockNetwork +import net.corda.testing.node.StartedMockNode +import org.junit.After +import org.junit.Before +import java.util.* +import kotlin.test.assertEquals + +/** + * A base class to reduce the boilerplate when writing Asset flow tests. + */ +abstract class AbstractAssetJunitFlowTests { + lateinit var network: MockNetwork + lateinit var lenderOfSecurity: StartedMockNode + lateinit var lenderOfCash: StartedMockNode + lateinit var globalCustodian: StartedMockNode + lateinit var lenderOfSecurityParty: Party + lateinit var lenderOfCashParty: Party + lateinit var custodianParty: Party + + protected val cusip = "CUSIP123" + + @Before + fun setup() { + network = MockNetwork(listOf("com.synechron.cordapp", "net.corda.finance", "net.corda.finance.schemas"), threadPerNode = true) + + lenderOfSecurity = network.createNode() + lenderOfCash = network.createNode() + globalCustodian = network.createNode() + val nodes = listOf(lenderOfSecurity, lenderOfCash, globalCustodian) + + lenderOfCash.registerInitiatedFlow(CreateAssetTransferRequestResponderFlow::class.java) + lenderOfCash.registerInitiatedFlow(AssetSettlementResponderFlow::class.java) + + lenderOfSecurity.registerInitiatedFlow(ConfirmAssetTransferRequestHandlerFlow::class.java) + lenderOfSecurity.registerInitiatedFlow(com.synechron.cordapp.seller.flows.AssetSettlementResponderFlow::class.java) + + globalCustodian.registerInitiatedFlow(com.synechron.cordapp.clearinghouse.flows.ConfirmAssetTransferRequestResponderFlow::class.java) + + lenderOfSecurityParty = lenderOfSecurity.info.chooseIdentity() + lenderOfCashParty = lenderOfCash.info.chooseIdentity() + custodianParty = globalCustodian.info.chooseIdentity() + } + + @After + fun tearDown() { + network.stopNodes() + } + + protected fun createAsset(owner: StartedMockNode, + cusip: String, + assetName: String, + purchaseCost: Amount + ): SignedTransaction { + val flow = CreateAssetStateFlow.Initiator(cusip, assetName, purchaseCost) + return owner.startFlow(flow).getOrThrow() + } + + protected fun createAsset(): Asset { + val stx = createAsset(lenderOfSecurity, cusip, "US BOND", DOLLARS(1000)) + network.waitQuiescent() + + return lenderOfSecurity.transaction { + val asset = lenderOfSecurity.services.loadState(stx.tx.outRef(0).ref).data as Asset + assertEquals(asset.cusip, cusip) + asset + } + } + + protected fun createAssetTransferRequest(lenderOfSecurity: StartedMockNode, + lenderOfCash: Party, + cusip: String): SignedTransaction { + val flow = CreateAssetTransferRequestInitiatorFlow(cusip, lenderOfCash) + return lenderOfSecurity.startFlow(flow).getOrThrow() + } + + protected fun confirmAssetTransferRequest(lenderOfCash: StartedMockNode, + custodian: Party, + linearId: UniqueIdentifier): SignedTransaction { + val flow = ConfirmAssetTransferRequestInitiatorFlow(linearId, custodian) + return lenderOfCash.startFlow(flow).getOrThrow() + } + + protected fun settleAssetTransferRequest(custodianNode: StartedMockNode, + linearId: UniqueIdentifier): SignedTransaction { + val flow = AssetSettlementInitiatorFlow(linearId) + return custodianNode.startFlow(flow).getOrThrow() + } + + protected fun selfIssueCash(node: StartedMockNode, + amount: Amount): SignedTransaction { + val notary = node.services.networkMapCache.notaryIdentities.firstOrNull() + ?: throw IllegalStateException("Could not find a notary.") + val issueRef = OpaqueBytes.of(0) + val issueRequest = CashIssueFlow.IssueRequest(amount, issueRef, notary) + val flow = CashIssueFlow(issueRequest) + return node.startFlow(flow).getOrThrow().stx + } + + protected fun resolveIdentity(node: StartedMockNode, + anonymousParty: AbstractParty): Party { + return node.services.identityService.requireWellKnownPartyFromAnonymous(anonymousParty) + } + + protected fun getStateByLinearId(linearId: UniqueIdentifier, clazz: Class, mockNode: StartedMockNode): StateAndRef { + val queryCriteria = QueryCriteria.LinearStateQueryCriteria(null, + listOf(linearId), Vault.StateStatus.UNCONSUMED, null) + return mockNode.transaction { + mockNode.services.vaultService.queryBy(clazz, queryCriteria).states.firstOrNull() + ?: throw StateNotFoundOnVaultException("State with id $linearId not found.") + } + } +} diff --git a/src/test/kotlin/com/synechron/cordapp/flows/AssetSettlementFlowTests.kt b/src/test/kotlin/com/synechron/cordapp/flows/AssetSettlementFlowTests.kt new file mode 100644 index 0000000..9b09a34 --- /dev/null +++ b/src/test/kotlin/com/synechron/cordapp/flows/AssetSettlementFlowTests.kt @@ -0,0 +1,70 @@ +package com.synechron.cordapp.flows + +import com.synechron.cordapp.state.Asset +import com.synechron.cordapp.state.AssetTransfer +import com.synechron.cordapp.state.RequestStatus +import net.corda.finance.DOLLARS +import net.corda.finance.USD +import net.corda.finance.contracts.getCashBalances +import org.junit.Test + +class AssetSettlementFlowTests : AbstractAssetJunitFlowTests() { + + @Test + fun `process asset transfer settlement`() { + //1. Create Asset on the ledger. + createAsset() + //2. Create asset transfer request. + val stx1 = createAssetTransferRequest(lenderOfSecurity, lenderOfCashParty, cusip) + network.waitQuiescent() + //Get linearId of AssetTransfer request. + val assetTransfer = lenderOfSecurity.transaction { + val states = lenderOfSecurity.services.vaultService.queryBy(Asset::class.java).states + assert(states.size == 1) + + val states2 = lenderOfSecurity.services.vaultService.queryBy(AssetTransfer::class.java).states + states2.single().state.data + } + //3. Confirm asset transfer request. + confirmAssetTransferRequest(lenderOfCash, custodianParty, assetTransfer.linearId) + network.waitQuiescent() + + //Self issue cash and verify. + selfIssueCash(lenderOfCash, DOLLARS(2000)) + lenderOfCash.transaction { + assert(lenderOfCash.services.getCashBalances().getValue(USD) == DOLLARS(2000)) + } + //Verify cash balance of securitySeller. + lenderOfSecurity.transaction { + assert(lenderOfSecurity.services.getCashBalances().isEmpty()) + } + + //4. Settle asset transfer request. + val settleTx = settleAssetTransferRequest(globalCustodian, assetTransfer.linearId) + network.waitQuiescent() + + lenderOfSecurity.transaction { + assert(lenderOfSecurity.services.vaultService.queryBy(Asset::class.java).states.isEmpty()) + val assetTransfer2 = lenderOfSecurity.services.vaultService.queryBy(AssetTransfer::class.java).states.first().state.data + assert(assetTransfer2.status == RequestStatus.TRANSFERRED) + assert(assetTransfer2.asset.purchaseCost == lenderOfSecurity.services.getCashBalances().getValue(USD)) + } + + lenderOfCash.transaction { + assert(lenderOfCash.services.getCashBalances().getValue(USD) == DOLLARS(1000)) + val assetTransfer3 = lenderOfCash.services.vaultService.queryBy(AssetTransfer::class.java).states.first().state.data + assert(assetTransfer3.status == RequestStatus.TRANSFERRED) + val assetStates = lenderOfCash.services.vaultService.queryBy(Asset::class.java).states + assert(assetStates.size == 1) + assert(resolveIdentity(lenderOfCash, assetStates.first().state.data.owner).name == lenderOfCashParty.name) + assert(assetStates.first().state.data.owner == assetTransfer.securityBuyer) + assert(assetStates.first().state.data == assetTransfer.asset.copy(owner = assetTransfer.securityBuyer)) + } + + globalCustodian.transaction { + val assetTransfer4 = lenderOfCash.services.vaultService.queryBy(AssetTransfer::class.java).states.first().state.data + assert(assetTransfer4.status == RequestStatus.TRANSFERRED) + } + //TODO Verify all Nodes able to resolve the every participants in Contract states received. + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/synechron/cordapp/flows/ConfirmAssetTransferRequestFlowTests.kt b/src/test/kotlin/com/synechron/cordapp/flows/ConfirmAssetTransferRequestFlowTests.kt new file mode 100644 index 0000000..2e33f96 --- /dev/null +++ b/src/test/kotlin/com/synechron/cordapp/flows/ConfirmAssetTransferRequestFlowTests.kt @@ -0,0 +1,35 @@ +package com.synechron.cordapp.flows + +import com.synechron.cordapp.state.AssetTransfer +import com.synechron.cordapp.state.RequestStatus +import net.corda.core.contracts.UniqueIdentifier +import org.junit.Test + +class ConfirmAssetTransferRequestFlowTests : AbstractAssetJunitFlowTests() { + + @Test + fun `confirm asset transfer request successfully`() { + //1. Create Asset on the ledger. + createAsset() + //2. Create asset transfer request. + val stx1 = createAssetTransferRequest(lenderOfSecurity, lenderOfCashParty, cusip) + network.waitQuiescent() + var linearId: UniqueIdentifier? = null + lenderOfCash.transaction { + linearId = (lenderOfCash.services.loadState(stx1.tx.outRef(0).ref).data as AssetTransfer).linearId + } + //3. Confirm asset transfer request. + val stx2 = confirmAssetTransferRequest(lenderOfCash, custodianParty, linearId!!) + network.waitQuiescent() + + var assetTransfer : AssetTransfer? = null + globalCustodian.transaction { + assetTransfer = globalCustodian.services.loadState(stx2.tx.outRef(0).ref).data as AssetTransfer + assert(assetTransfer!!.status == RequestStatus.PENDING) + } + val maybePartyBLookedUpByC = resolveIdentity(globalCustodian, assetTransfer!!.securityBuyer) + val maybePartyALookedUpByC = resolveIdentity(globalCustodian, assetTransfer!!.securitySeller) + assert(lenderOfSecurityParty == maybePartyALookedUpByC) + assert(lenderOfCashParty == maybePartyBLookedUpByC) + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/synechron/cordapp/flows/CreateAssetStateFlowTests.kt b/src/test/kotlin/com/synechron/cordapp/flows/CreateAssetStateFlowTests.kt new file mode 100644 index 0000000..adeee4f --- /dev/null +++ b/src/test/kotlin/com/synechron/cordapp/flows/CreateAssetStateFlowTests.kt @@ -0,0 +1,19 @@ +package com.synechron.cordapp.flows + +import com.synechron.cordapp.state.Asset +import net.corda.finance.DOLLARS +import org.junit.Test +import kotlin.test.assertEquals + +class CreateAssetStateFlowTests : AbstractAssetJunitFlowTests() { + + @Test + fun `create Asset on ledger successfully`() { + val stx = createAsset(lenderOfSecurity, cusip, "US BOND", DOLLARS(1000)) + network.waitQuiescent() + + val asset = lenderOfSecurity.services.loadState(stx.tx.outRef(0).ref).data as Asset + + assertEquals(asset.cusip, cusip) + } +} diff --git a/src/test/kotlin/com/synechron/cordapp/flows/CreateAssetTransferRequestFlowTests.kt b/src/test/kotlin/com/synechron/cordapp/flows/CreateAssetTransferRequestFlowTests.kt new file mode 100644 index 0000000..ac82bc2 --- /dev/null +++ b/src/test/kotlin/com/synechron/cordapp/flows/CreateAssetTransferRequestFlowTests.kt @@ -0,0 +1,38 @@ +package com.synechron.cordapp.flows + +import com.synechron.cordapp.state.AssetTransfer +import net.corda.testing.internal.chooseIdentity +import org.junit.Test +import kotlin.test.assertEquals + +class CreateAssetTransferRequestFlowTests : AbstractAssetJunitFlowTests() { + + @Test + fun `create asset transfer request successfully`() { + val lenderOfSecurityParty = lenderOfSecurity.info.chooseIdentity() + val lenderOfCashParty = lenderOfCash.info.chooseIdentity() + + //1. Create Asset on the ledger. + createAsset() + //2. Create asset transfer request. + val stx = createAssetTransferRequest(lenderOfSecurity, lenderOfCashParty, cusip) + network.waitQuiescent() + + val assetTransfer1 = lenderOfCash.services.loadState(stx.tx.outRef(0).ref).data as AssetTransfer + val assetTransfer2 = lenderOfSecurity.services.loadState(stx.tx.outRef(0).ref).data as AssetTransfer + + assertEquals(assetTransfer1, assetTransfer2) + + val maybePartyALookedUpByA = resolveIdentity(lenderOfSecurity, assetTransfer1.securitySeller) + val maybePartyALookedUpByB = resolveIdentity(lenderOfSecurity, assetTransfer1.securityBuyer) + + assertEquals(lenderOfSecurityParty, maybePartyALookedUpByA) + assertEquals(lenderOfCashParty, maybePartyALookedUpByB) + + val maybePartyBLookedUpByA = resolveIdentity(lenderOfCash, assetTransfer1.securityBuyer) + val maybePartyBLookedUpByB = resolveIdentity(lenderOfCash, assetTransfer1.securitySeller) + + assertEquals(lenderOfCashParty, maybePartyBLookedUpByA) + assertEquals(lenderOfSecurityParty, maybePartyBLookedUpByB) + } +} \ No newline at end of file