diff --git a/.idea/runConfigurations/Bank_of_Corda_Demo__Run_Issuer.xml b/.idea/runConfigurations/Bank_of_Corda_Demo__Run_Issuer.xml new file mode 100644 index 0000000000..314089c670 --- /dev/null +++ b/.idea/runConfigurations/Bank_of_Corda_Demo__Run_Issuer.xml @@ -0,0 +1,15 @@ + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations/Bank_of_Corda_Demo__Run_RPC_Cash_Issue.xml b/.idea/runConfigurations/Bank_of_Corda_Demo__Run_RPC_Cash_Issue.xml new file mode 100644 index 0000000000..dfe14546a3 --- /dev/null +++ b/.idea/runConfigurations/Bank_of_Corda_Demo__Run_RPC_Cash_Issue.xml @@ -0,0 +1,15 @@ + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations/Bank_of_Corda_Demo__Run_Web_Cash_Issue.xml b/.idea/runConfigurations/Bank_of_Corda_Demo__Run_Web_Cash_Issue.xml new file mode 100644 index 0000000000..ce474a1406 --- /dev/null +++ b/.idea/runConfigurations/Bank_of_Corda_Demo__Run_Web_Cash_Issue.xml @@ -0,0 +1,15 @@ + + + + + \ No newline at end of file diff --git a/build.gradle b/build.gradle index a16f42c558..b619f77eee 100644 --- a/build.gradle +++ b/build.gradle @@ -1,9 +1,12 @@ buildscript { + // For sharing constants between builds + Properties props = new Properties() + file("publish.properties").withInputStream { props.load(it) } + // Our version: bump this on release. ext.corda_version = "0.7-SNAPSHOT" - ext.gradle_plugins_version = "0.6.1" - - ext.kotlin_version = '1.0.5' + ext.gradle_plugins_version = props.getProperty("gradlePluginsVersion") + ext.kotlin_version = '1.0.5-2' ext.quasar_version = '0.7.6' ext.asm_version = '0.5.3' ext.artemis_version = '1.4.0' @@ -45,7 +48,7 @@ plugins { apply plugin: 'kotlin' apply plugin: 'project-report' apply plugin: 'com.github.ben-manes.versions' -apply plugin: 'maven-publish' +apply plugin: 'net.corda.plugins.publish-utils' apply plugin: 'net.corda.plugins.quasar-utils' apply plugin: 'net.corda.plugins.cordformation' @@ -93,7 +96,7 @@ repositories { dependencies { compile project(':node') compile "com.google.guava:guava:19.0" - runtime project(path: ":node", configuration: 'runtimeArtifacts') + runtime project(path: ":node:capsule", configuration: 'runtimeArtifacts') } task jacocoRootReport(type: org.gradle.testing.jacoco.tasks.JacocoReport) { @@ -153,5 +156,25 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['build']) { } } -// Aliasing the publishToMavenLocal for simplicity. -task(install, dependsOn: 'publishToMavenLocal') +bintrayConfig { + user = System.getenv('CORDA_BINTRAY_USER') + key = System.getenv('CORDA_BINTRAY_KEY') + repo = 'corda' + org = 'r3' + licenses = ['Apache-2.0'] + vcsUrl = 'https://github.com/corda/corda' + projectUrl = 'https://github.com/corda/corda' + gpgSign = true + gpgPassphrase = System.getenv('CORDA_BINTRAY_GPG_PASSPHRASE') + publications = ['client', 'core', 'corda', 'finance', 'node', 'test-utils'] + license { + name = 'Apache-2.0' + url = 'https://www.apache.org/licenses/LICENSE-2.0' + distribution = 'repo' + } + developer { + id = 'R3' + name = 'R3' + email = 'dev@corda.net' + } +} \ No newline at end of file diff --git a/client/build.gradle b/client/build.gradle index bf04cfe540..1748bef18d 100644 --- a/client/build.gradle +++ b/client/build.gradle @@ -2,6 +2,8 @@ apply plugin: 'kotlin' apply plugin: 'net.corda.plugins.quasar-utils' apply plugin: 'net.corda.plugins.publish-utils' +description 'Corda client modules' + repositories { mavenLocal() mavenCentral() @@ -38,18 +40,6 @@ sourceSets { } } -publishing { - publications { - client(MavenPublication) { - from components.java - artifactId 'client' - - artifact sourceJar - artifact javadocJar - } - } -} - // To find potential version conflicts, run "gradle htmlDependencyReport" and then look in // build/reports/project/dependencies/index.html for green highlighted parts of the tree. @@ -83,4 +73,4 @@ dependencies { task integrationTest(type: Test) { testClassesDir = sourceSets.integrationTest.output.classesDir classpath = sourceSets.integrationTest.runtimeClasspath -} +} \ No newline at end of file diff --git a/core/build.gradle b/core/build.gradle index 6f7cc5512d..2265ea4402 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -2,14 +2,14 @@ apply plugin: 'kotlin' apply plugin: 'net.corda.plugins.quasar-utils' apply plugin: 'net.corda.plugins.publish-utils' +description 'Corda core' + buildscript { repositories { mavenCentral() } } -// apply plugin: 'org.jetbrains.dokka' - repositories { mavenLocal() mavenCentral() @@ -88,16 +88,4 @@ dependencies { // RS API: Response type and codes for ApiUtils. compile "javax.ws.rs:javax.ws.rs-api:2.0" -} - -publishing { - publications { - core(MavenPublication) { - from components.java - artifactId 'core' - - artifact sourceJar - artifact javadocJar - } - } } \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/Utils.kt b/core/src/main/kotlin/net/corda/core/Utils.kt index 97d8d1dce8..fea248b305 100644 --- a/core/src/main/kotlin/net/corda/core/Utils.kt +++ b/core/src/main/kotlin/net/corda/core/Utils.kt @@ -13,6 +13,8 @@ import kotlinx.support.jdk7.use import net.corda.core.crypto.newSecureRandom import org.slf4j.Logger import rx.Observable +import rx.Observer +import rx.subjects.PublishSubject import rx.subjects.UnicastSubject import java.io.BufferedInputStream import java.io.InputStream @@ -363,5 +365,15 @@ fun Observable.bufferUntilSubscribed(): Observable { return subject.doOnUnsubscribe { subscription.unsubscribe() } } +/** + * Copy an [Observer] to multiple other [Observer]s. + */ +fun Observer.tee(vararg teeTo: Observer): Observer { + val subject = PublishSubject.create() + subject.subscribe(this) + teeTo.forEach { subject.subscribe(it) } + return subject +} + /** Allows summing big decimals that are in iterable collections */ fun Iterable.sum(): BigDecimal = fold(BigDecimal.ZERO) { a, b -> a + b } diff --git a/core/src/main/kotlin/net/corda/core/contracts/ContractsDSL.kt b/core/src/main/kotlin/net/corda/core/contracts/ContractsDSL.kt index d1396f40e3..ce2dbea0b5 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/ContractsDSL.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/ContractsDSL.kt @@ -61,6 +61,8 @@ inline fun requireThat(body: Requirements.() -> R) = Requirements.body() //// Authenticated commands /////////////////////////////////////////////////////////////////////////////////////////// +// TODO: Provide a version of select that interops with Java + /** Filters the command list by type, party and public key all at once. */ inline fun Collection>.select(signer: CompositeKey? = null, party: Party? = null) = @@ -69,6 +71,8 @@ inline fun Collection filter { if (party == null) true else party in it.signingParties }. map { AuthenticatedObject(it.signers, it.signingParties, it.value as T) } +// TODO: Provide a version of select that interops with Java + /** Filters the command list by type, parties and public keys all at once. */ inline fun Collection>.select(signers: Collection?, parties: Collection?) = diff --git a/core/src/main/kotlin/net/corda/core/crypto/X509Utilities.kt b/core/src/main/kotlin/net/corda/core/crypto/X509Utilities.kt index fb9cdb33c9..c29454b74c 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/X509Utilities.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/X509Utilities.kt @@ -430,14 +430,8 @@ object X509Utilities { fun loadCertificateFromPEMFile(filename: Path): X509Certificate { val reader = PemReader(FileReader(filename.toFile())) val pemObject = reader.readPemObject() - val certFact = CertificateFactory.getInstance("X.509", BouncyCastleProvider.PROVIDER_NAME) - val inputStream = ByteArrayInputStream(pemObject.content) - try { - val cert = certFact.generateCertificate(inputStream) as X509Certificate - cert.checkValidity() - return cert - } finally { - inputStream.close() + return CertificateStream(pemObject.content.inputStream()).nextCertificate().apply { + checkValidity() } } @@ -609,3 +603,11 @@ object X509Utilities { return keyStore } } + +val X500Name.commonName: String get() = getRDNs(BCStyle.CN).first().first.value.toString() + +class CertificateStream(val input: InputStream) { + private val certificateFactory = CertificateFactory.getInstance("X.509") + + fun nextCertificate(): X509Certificate = certificateFactory.generateCertificate(input) as X509Certificate +} \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/flows/FlowLogic.kt b/core/src/main/kotlin/net/corda/core/flows/FlowLogic.kt index 3a9e7186b0..08f619797d 100644 --- a/core/src/main/kotlin/net/corda/core/flows/FlowLogic.kt +++ b/core/src/main/kotlin/net/corda/core/flows/FlowLogic.kt @@ -54,6 +54,8 @@ abstract class FlowLogic { return sendAndReceive(otherParty, payload, T::class.java) } + // TODO: Move the receiveType param to first position for readability + @Suspendable fun sendAndReceive(otherParty: Party, payload: Any, receiveType: Class): UntrustworthyData { return fsm.sendAndReceive(otherParty, payload, receiveType, sessionFlow) @@ -61,6 +63,8 @@ abstract class FlowLogic { inline fun receive(otherParty: Party): UntrustworthyData = receive(otherParty, T::class.java) + // TODO: Move the receiveType param to first position for readability + @Suspendable fun receive(otherParty: Party, receiveType: Class): UntrustworthyData { return fsm.receive(otherParty, receiveType, sessionFlow) diff --git a/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt b/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt index 56f9384747..5b5b8ba8a6 100644 --- a/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt +++ b/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt @@ -31,6 +31,9 @@ sealed class StateMachineUpdate(val id: StateMachineRunId) { * RPC operations that the node exposes to clients using the Java client library. These can be called from * client apps and are implemented by the node in the [CordaRPCOpsImpl] class. */ + +// TODO: The use of Pairs throughout is unfriendly for Java interop. + interface CordaRPCOps : RPCOps { /** * Returns a pair of currently in-progress state machine infos and an observable of future state machine adds/removes. diff --git a/core/src/main/kotlin/net/corda/core/messaging/Messaging.kt b/core/src/main/kotlin/net/corda/core/messaging/Messaging.kt index 4d2d628976..0a55d7dca4 100644 --- a/core/src/main/kotlin/net/corda/core/messaging/Messaging.kt +++ b/core/src/main/kotlin/net/corda/core/messaging/Messaging.kt @@ -8,7 +8,6 @@ import net.corda.core.serialization.DeserializeAsKotlinObjectDef import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize import org.bouncycastle.asn1.x500.X500Name -import org.bouncycastle.asn1.x500.style.BCStyle import java.time.Instant import java.util.* import java.util.concurrent.atomic.AtomicBoolean @@ -184,7 +183,6 @@ interface Message { interface ReceivedMessage : Message { /** The authenticated sender. */ val peer: X500Name - val peerLegalName: String get() = peer.getRDNs(BCStyle.CN).first().first.value.toString() } /** A singleton that's useful for validating topic strings */ diff --git a/core/src/main/kotlin/net/corda/core/node/services/Services.kt b/core/src/main/kotlin/net/corda/core/node/services/Services.kt index ece53304f7..5f5215f1ac 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/Services.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/Services.kt @@ -100,8 +100,19 @@ interface VaultService { val currentVault: Vault /** + * Prefer the use of [updates] unless you know why you want to use this instead. + * * Get a synchronous Observable of updates. When observations are pushed to the Observer, the Vault will already incorporate - * the update. + * the update, and the database transaction associated with the update will still be open and current. If for some + * reason the processing crosses outside of the database transaction (for example, the update is pushed outside the current + * JVM or across to another [Thread] which is executing in a different database transaction) then the Vault may + * not incorporate the update due to racing with committing the current database transaction. + */ + val rawUpdates: Observable + + /** + * Get a synchronous Observable of updates. When observations are pushed to the Observer, the Vault will already incorporate + * the update, and the database transaction associated with the update will have been committed and closed. */ val updates: Observable diff --git a/core/src/test/kotlin/net/corda/core/crypto/X509UtilitiesTest.kt b/core/src/test/kotlin/net/corda/core/crypto/X509UtilitiesTest.kt index afd80a5259..a1d354316b 100644 --- a/core/src/test/kotlin/net/corda/core/crypto/X509UtilitiesTest.kt +++ b/core/src/test/kotlin/net/corda/core/crypto/X509UtilitiesTest.kt @@ -2,7 +2,6 @@ package net.corda.core.crypto import net.corda.core.div import org.bouncycastle.asn1.x500.X500Name -import org.bouncycastle.asn1.x500.style.BCStyle import org.bouncycastle.asn1.x509.GeneralName import org.junit.Rule import org.junit.Test @@ -249,8 +248,7 @@ class X509UtilitiesTest { val peerChain = clientSocket.session.peerCertificates val peerX500Principal = (peerChain[0] as X509Certificate).subjectX500Principal val x500name = X500Name(peerX500Principal.name) - val cn = x500name.getRDNs(BCStyle.CN).first().first.value.toString() - assertEquals("Mega Corp.", cn) + assertEquals("Mega Corp.", x500name.commonName) val output = DataOutputStream(clientSocket.outputStream) diff --git a/docs/source/example-code/build.gradle b/docs/source/example-code/build.gradle index 470ef52172..9bc0aeb012 100644 --- a/docs/source/example-code/build.gradle +++ b/docs/source/example-code/build.gradle @@ -36,7 +36,7 @@ sourceSets { } } -compileTestJava.dependsOn tasks.getByPath(':node:buildCordaJAR') +compileTestJava.dependsOn tasks.getByPath(':node:capsule:buildCordaJAR') dependencies { compile project(':core') @@ -48,7 +48,7 @@ dependencies { exclude group: "bouncycastle" } - runtime project(path: ":node", configuration: 'runtimeArtifacts') + runtime project(path: ":node:capsule", configuration: 'runtimeArtifacts') } mainClassName = "net.corda.docs.ClientRpcTutorialKt" diff --git a/docs/source/example-code/src/test/kotlin/net/corda/docs/FxTransactionBuildTutorialTest.kt b/docs/source/example-code/src/test/kotlin/net/corda/docs/FxTransactionBuildTutorialTest.kt index 22335d0483..4467b69b16 100644 --- a/docs/source/example-code/src/test/kotlin/net/corda/docs/FxTransactionBuildTutorialTest.kt +++ b/docs/source/example-code/src/test/kotlin/net/corda/docs/FxTransactionBuildTutorialTest.kt @@ -3,9 +3,7 @@ package net.corda.docs import com.google.common.util.concurrent.SettableFuture import net.corda.core.contracts.* import net.corda.core.getOrThrow -import net.corda.core.node.ServiceHub import net.corda.core.node.services.ServiceInfo -import net.corda.core.node.services.linearHeadsOfType import net.corda.core.serialization.OpaqueBytes import net.corda.core.utilities.DUMMY_NOTARY import net.corda.core.utilities.DUMMY_NOTARY_KEY @@ -18,7 +16,6 @@ import net.corda.testing.node.MockNetwork import org.junit.After import org.junit.Before import org.junit.Test -import java.util.* import kotlin.test.assertEquals class FxTransactionBuildTutorialTest { @@ -69,13 +66,13 @@ class FxTransactionBuildTutorialTest { printBalances() // Setup some futures on the vaults to await the arrival of the exchanged funds at both nodes - val done2 = SettableFuture.create>>() - val done3 = SettableFuture.create>>() + val done2 = SettableFuture.create() + val done3 = SettableFuture.create() val subs2 = nodeA.services.vaultService.updates.subscribe { - done2.set(nodeA.services.vaultService.cashBalances) + done2.set(Unit) } val subs3 = nodeB.services.vaultService.updates.subscribe { - done3.set(nodeB.services.vaultService.cashBalances) + done3.set(Unit) } // Now run the actual Fx exchange val doIt = nodeA.services.startFlow(ForeignExchangeFlow("trade1", @@ -86,8 +83,14 @@ class FxTransactionBuildTutorialTest { // wait for the flow to finish and the vault updates to be done doIt.resultFuture.getOrThrow() // Get the balances when the vault updates - val balancesA = done2.get() - val balancesB = done3.get() + done2.get() + val balancesA = databaseTransaction(nodeA.database) { + nodeA.services.vaultService.cashBalances + } + done3.get() + val balancesB = databaseTransaction(nodeB.database) { + nodeB.services.vaultService.cashBalances + } subs2.unsubscribe() subs3.unsubscribe() println("BalanceA\n" + balancesA) diff --git a/docs/source/example-code/src/test/kotlin/net/corda/docs/WorkflowTransactionBuildTutorialTest.kt b/docs/source/example-code/src/test/kotlin/net/corda/docs/WorkflowTransactionBuildTutorialTest.kt index d663b1b36b..a39c6a3734 100644 --- a/docs/source/example-code/src/test/kotlin/net/corda/docs/WorkflowTransactionBuildTutorialTest.kt +++ b/docs/source/example-code/src/test/kotlin/net/corda/docs/WorkflowTransactionBuildTutorialTest.kt @@ -53,7 +53,7 @@ class WorkflowTransactionBuildTutorialTest { net.stopNodes() } - //@Test + @Test fun `Run workflow to completion`() { // Setup a vault subscriber to wait for successful upload of the proposal to NodeB val done1 = SettableFuture.create() diff --git a/docs/source/index.rst b/docs/source/index.rst index 00373ace5a..a67d3f46e7 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -107,6 +107,7 @@ Read on to learn: release-notes codestyle building-the-docs + publishing-corda .. toctree:: :maxdepth: 2 diff --git a/docs/source/publishing-corda.rst b/docs/source/publishing-corda.rst new file mode 100644 index 0000000000..ab04bcf3e9 --- /dev/null +++ b/docs/source/publishing-corda.rst @@ -0,0 +1,76 @@ +Publishing Corda +================ + +Before Publishing +----------------- + +Before publishing you must make sure the version you plan to publish has a unique version number. Jcenter and Maven +Central will not allow overwriting old versions _unless_ the version is a snapshot. + +This guide assumes you are trying to publish to net.corda.*. Any other Maven coordinates require approval from Jcenter +and Maven Central. + +Publishing Locally +------------------ + +To publish the codebase locally to Maven Local you must run: + +.. code-block:: text + + gradlew install + +.. note:: This command is an alias for `publishToMavenLocal`. + +Publishing to Jcenter +--------------------- + +.. note:: The module you wish to publish must be linked to jcenter in bintray. Only the founding account can do this. + +To publish to Jcenter you must first have the following; + +1. An account on bintray in the R3 organisation +2. Our GPG key's passphrase for signing the binaries to publish + +Getting Setup +````````````` + +You must now set the following environment variables: + +* CORDA_BINTRAY_USER your Bintray username +* CORDA_BINTRAY_KEY to your bintray API key (found at: https://bintray.com/profile/edit) +* CORDA_BINTRAY_GPG_PASSPHRASE to our GPG passphrase + +Publishing +`````````` + +Once you are setup you can upload all modules in a project with + +.. code-block:: text + + gradlew bintrayUpload + +Now login to Bintray and navigate to the corda repository, you will see a box stating you have published N files +and asking if you wish to publish. You can now publish to Bintray and Jcenter by clicking this button. + +.. warning:: Before publishing you should check that all of the files are uploaded and are signed. + +Within a minute your new version will be available to download and use. + +Publishing to Maven Central +--------------------------- + +To publish to Maven Central you need the following; + +1. An admin account on our Bintray R3 organisation +2. A published version in Bintray +3. An account with our Sonatype organisation (Maven Central's host) + +Publishing +`````````` + +1. Publish to Bintray +2. Navigate to the project you wish to publish +3. Click "Maven Central" +4. Enter your Sonatype credentials to publish a new version + +.. note:: The project you publish must be already published to Bintray and the project must be linked to Jcenter \ No newline at end of file diff --git a/docs/source/running-the-demos.rst b/docs/source/running-the-demos.rst index a1a3939591..d27907da4d 100644 --- a/docs/source/running-the-demos.rst +++ b/docs/source/running-the-demos.rst @@ -13,6 +13,8 @@ so far. We have: 5. The SIMM valuation demo, a large demo which shows two nodes agreeing on a portfolio and valuing the initial margin using the Standard Initial Margin Model. 6. The distributed notary demo, which demonstrates a single node getting multiple transactions notarised by a distributed (Raft-based) notary. +7. The Bank of Corda demo, which demonstrates a node acting as an issuer of assets (the Bank of Corda) and remote client + applications requesting issuance (via RPC, HTTP) of some cash on behalf of a node called Big Corporation. .. note:: If any demos don't work please jump on our mailing list and let us know. @@ -156,6 +158,58 @@ by using the H2 web console: - The committed states are stored in the ``NOTARY_COMMITTED_STATES`` table. Note that the raw data is not human-readable, but we're only interested in the row count for this demo. +Bank Of Corda demo +------------------ + +This demo brings up three nodes: a notary, a node acting as the Bank of Corda that accepts requests for issuance of some asset +and a node acting as Big Corporation which requests issuance of an asset (cash in this example). +Upon receipt of a request the Bank of Corda node self-issues the asset and then transfers ownership to the requester +after successful notarisation and recording of the issue transaction on the ledger. + +.. note:: The Bank of Corda is somewhat like the "Bitcoin faucet", that used to dispense free bitcoins to developers for + testing and experimentation purposes. + +To run from the command line (recommended for Mac/UNIX users!): + +1. Run ``./gradlew samples:bank-of-corda-demo:deployNodes`` to create a set of configs and installs under ``samples/bank-of-corda-demo/build/nodes`` +2. Run ``./samples/bank-of-corda-demo/build/nodes/runnodes`` to open up three new terminal tabs/windows with the three nodes. + +.. note:: to verify the Bank of Corda node is alive and running navigate to the following URL + http://localhost:10005/api/bank/date + +3. Run ``./gradlew samples:bank-of-corda-demo:runRPCCashIssue`` in another terminal window to trigger a cash issuance request +4. Run ``./gradlew samples:bank-of-corda-demo:runWebCashIssue`` in another terminal window to trigger another cash issuance request + Now look at the other windows to see the output of the demo. + +Or you can run them from inside IntelliJ as follows: + +1. Open the Corda project in IntelliJ and run the "Install" configuration +2. Open the Corda samples project in IntelliJ and run the "Bank Of Corda Demo: Run Issuer" configuration +3. Run "Bank Of Corda Demo: Run RPC Cash Issue" - requests issuance of some cash on behalf of Big Corporation via RPC +4. Run "Bank Of Corda Demo: Run Web Cash Issue" - requests issuance of some cash on behalf of Big Corporation via HTTP + +In the "Bank Of Corda Demo: Run Issuer" window you should see the following information lines displayed: + +- Awaiting issuance request +- Self issuing asset +- Transferring asset to issuance requester +- Confirming asset issuance to requester + +In the the client issue request window you should see the following printed: + +- Successfully processed Cash Issue request + +Launch the Explorer application to visualize the issuance and transfer of cash on each node: + + ``./gradlew tools:explorer:run`` + +And use the following logon details: + +- for the Bank of Corda node specify localhost, port 10004, username user1, password test +- for the Big Corporation node specify localhost, port 10006, username user1, password test + +See https://docs.corda.net/node-explorer.html for further details on usage. + SIMM and Portfolio Demo - aka the Initial Margin Agreement Demo --------------------------------------------------------------- diff --git a/docs/source/tutorial-cordapp.rst b/docs/source/tutorial-cordapp.rst index 2097ac4267..a65f91ca7a 100644 --- a/docs/source/tutorial-cordapp.rst +++ b/docs/source/tutorial-cordapp.rst @@ -252,7 +252,7 @@ example CorDapp. The ``deployNodes`` Gradle task allows you easily create a formation of Corda nodes. In the case of the example CorDapp we are creating ``four`` nodes. -After the building process has finished to see the newly built nodes, you can navigate to the ``/build/nodes`` folder +After the building process has finished to see the newly built nodes, you can navigate to the ``kotlin/build/nodes`` folder located in the ``cordapp-template`` root directory. You can ignore the other folders in ``/build`` for now. The ``nodes`` folder has the following structure: @@ -300,7 +300,7 @@ Running the Sample CorDapp Running the Sample CorDapp from the command line ------------------------------------------------ -To run the sample CorDapp navigate to the ``build/nodes`` folder and execute the ``runnodes`` shell script with: +To run the sample CorDapp navigate to the ``kotlin/build/nodes`` folder and execute the ``runnodes`` shell script with: Unix: ``./runnodes`` or ``sh runnodes`` @@ -319,7 +319,7 @@ message and some pertinent config information, see below: --- DEVELOPER SNAPSHOT ------------------------------------------------------------ - Logs can be found in : /Users/rogerwillis/Documents/Corda/cordapp-template/build/nodes/nodea/logs + Logs can be found in : /Users/rogerwillis/Documents/Corda/cordapp-template/kotlin/build/nodes/nodea/logs Database connection url is : jdbc:h2:tcp://10.18.0.196:50661/node Node listening on address : localhost:10004 Loaded plugins : com.example.plugin.ExamplePlugin @@ -368,7 +368,7 @@ Running CorDapps on separate machines Corda nodes can be run on separate machines with little additional configuration to the above instructions. When you have successfully run the ``deployNodes`` gradle task, choose which nodes you would like to run on separate -machines. Copy the folders for those nodes from ``build/nodes`` to the other machines. Make sure that you set the +machines. Copy the folders for those nodes from ``kotlin/build/nodes`` to the other machines. Make sure that you set the ``networkMapAddress`` property in ``node.conf`` to the correct hostname:port where the network map service node is hosted. @@ -434,7 +434,7 @@ The CorDapp defines a few HTTP API end-points and also serves some static web co list purchase orders and add purchase orders. The nodes can be found using the following port numbers, defined in build.gradle and the respective node.conf file for -each node found in `build/nodes/NodeX`` etc: +each node found in `kotlin/build/nodes/NodeX`` etc: * Controller: ``localhost:10003`` * NodeA: ``localhost:10005`` @@ -758,7 +758,7 @@ like to deploy for testing. See further details below: .. sourcecode:: groovy task deployNodes(type: com.r3corda.plugins.Cordform, dependsOn: ['build']) { - directory "./build/nodes" // The output directory. + directory "./kotlin/build/nodes" // The output directory. networkMap "Controller" // The artemis address of the node to be used as the network map. node { name "Controller" // Artemis name of node to be deployed. @@ -804,7 +804,7 @@ Re-Deploying Your Nodes Locally If you need to create any additional nodes you can do it via the ``build.gradle`` file as discussed above in ``the build.gradle file`` and in more detail in the "cordFormation" section. -You may also wish to edit the ``/build/nodes//node.conf`` files for your nodes. For more information on +You may also wish to edit the ``/kotlin/build/nodes//node.conf`` files for your nodes. For more information on doing this, see the :doc:`Corda configuration file ` page. Once you have made some changes to your CorDapp you can redeploy it with the following command: diff --git a/finance/build.gradle b/finance/build.gradle index 5ecc8f533c..656e4e0b1a 100644 --- a/finance/build.gradle +++ b/finance/build.gradle @@ -3,6 +3,8 @@ apply plugin: CanonicalizerPlugin apply plugin: 'net.corda.plugins.publish-utils' apply plugin: 'net.corda.plugins.quasar-utils' +description 'Corda finance modules' + repositories { mavenLocal() mavenCentral() @@ -28,16 +30,4 @@ sourceSets { srcDir "../config/test" } } -} - -publishing { - publications { - finance(MavenPublication) { - from components.java - artifactId 'finance' - - artifact sourceJar - artifact javadocJar - } - } -} +} \ No newline at end of file diff --git a/gradle-plugins/README.rst b/gradle-plugins/README.rst index 4ce50a24c2..cf22310800 100644 --- a/gradle-plugins/README.rst +++ b/gradle-plugins/README.rst @@ -15,5 +15,10 @@ Installing If you need to bootstrap the corda repository you can install these plugins with +.. code-block:: text + + cd publish-utils + ../../gradlew -u install + cd ../ + ../gradlew install - gradle install \ No newline at end of file diff --git a/gradle-plugins/build.gradle b/gradle-plugins/build.gradle index f80527f8c5..f961d7f4e0 100644 --- a/gradle-plugins/build.gradle +++ b/gradle-plugins/build.gradle @@ -2,30 +2,50 @@ // or if you are developing these plugins. See the readme for more information. buildscript { - ext.gradle_plugins_version = "0.6.1" // Our version: bump this on release. - ext.corda_published_version = "0.5" // Depend on our existing published publishing plugin. + // For sharing constants between builds + Properties props = new Properties() + file("../publish.properties").withInputStream { props.load(it) } + + // If you bump this version you must re-bootstrap the codebase. See the README for more information. + ext.gradle_plugins_version = props.getProperty("gradlePluginsVersion") repositories { + mavenLocal() jcenter() } dependencies { - classpath "net.corda.plugins:publish-utils:$corda_published_version" + classpath "net.corda.plugins:publish-utils:$gradle_plugins_version" classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.4' } } -apply plugin: 'maven-publish' +apply plugin: 'net.corda.plugins.publish-utils' allprojects { version "$gradle_plugins_version" - group 'net.corda' + group 'net.corda.plugins' } -subprojects { - task(install, dependsOn: 'publishToMavenLocal') -} - -// Aliasing the publishToMavenLocal for simplicity. -task(install, dependsOn: 'publishToMavenLocal') - +bintrayConfig { + user = System.getenv('CORDA_BINTRAY_USER') + key = System.getenv('CORDA_BINTRAY_KEY') + repo = 'corda' + org = 'r3' + licenses = ['Apache-2.0'] + vcsUrl = 'https://github.com/corda/corda' + projectUrl = 'https://github.com/corda/corda' + gpgSign = true + gpgPassphrase = System.getenv('CORDA_BINTRAY_GPG_PASSPHRASE') + publications = ['cordformation', 'quasar-utils'] + license { + name = 'Apache-2.0' + url = 'https://www.apache.org/licenses/LICENSE-2.0' + distribution = 'repo' + } + developer { + id = 'R3' + name = 'R3' + email = 'dev@corda.net' + } +} \ No newline at end of file diff --git a/gradle-plugins/cordformation/build.gradle b/gradle-plugins/cordformation/build.gradle index 9ae980c4b1..80b92a58dc 100644 --- a/gradle-plugins/cordformation/build.gradle +++ b/gradle-plugins/cordformation/build.gradle @@ -1,7 +1,11 @@ apply plugin: 'groovy' apply plugin: 'net.corda.plugins.publish-utils' -apply plugin: 'maven-publish' -apply plugin: 'com.jfrog.bintray' + +description 'A small gradle plugin for adding some basic Quasar tasks and configurations to reduce build.gradle bloat.' + +repositories { + mavenCentral() +} dependencies { compile gradleApi() @@ -10,67 +14,3 @@ dependencies { compile "com.typesafe:config:1.3.0" } -repositories { - mavenCentral() -} - -bintray { - user = System.getenv('CORDA_BINTRAY_USER') - key = System.getenv('CORDA_BINTRAY_KEY') - publications = ['cordformation'] - dryRun = false - pkg { - repo = 'corda' - name = 'cordformation' - userOrg = 'r3' - licenses = ['Apache-2.0'] - - version { - gpg { - sign = true - passphrase = System.getenv('CORDA_BINTRAY_GPG_PASSPHRASE') - } - } - } -} - -publishing { - publications { - cordformation(MavenPublication) { - from components.java - groupId 'net.corda.plugins' - artifactId 'cordformation' - - artifact sourceJar - artifact javadocJar - - pom.withXml { - asNode().children().last() + { - resolveStrategy = Closure.DELEGATE_FIRST - name 'cordformation' - description 'A small gradle plugin for adding some basic Quasar tasks and configurations to reduce build.gradle bloat.' - url 'https://github.com/corda/corda' - scm { - url 'https://github.com/corda/corda' - } - - licenses { - license { - name 'Apache-2.0' - url 'https://www.apache.org/licenses/LICENSE-2.0' - distribution 'repo' - } - } - - developers { - developer { - id 'R3' - name 'R3' - email 'dev@corda.net' - } - } - } - } - } - } -} diff --git a/gradle-plugins/publish-utils/README.rst b/gradle-plugins/publish-utils/README.rst index e75667abc9..3ca1c2febf 100644 --- a/gradle-plugins/publish-utils/README.rst +++ b/gradle-plugins/publish-utils/README.rst @@ -23,3 +23,70 @@ It is used within the `publishing` block of a build.gradle as such; } } +Bintray Publishing +------------------ + +For large multibuild projects it can be inconvenient to store the entire configuration for bintray and maven central +per project (with a bintray and publishing block with extended POM information). Publish utils can bring the number of +configuration blocks down to one in the ideal scenario. + +To use this plugin you must first apply it to both the root project and any project that will be published with + +.. code-block:: text + + apply plugin: 'net.corda.plugins.publish-utils' + +Next you must setup the general bintray configuration you wish to use project wide, for example: + +.. code-block:: text + + bintrayConfig { + user = + key = + repo = 'example repo' + org = 'example organisation' + licenses = ['a license'] + vcsUrl = 'https://example.com' + projectUrl = 'https://example.com' + gpgSign = true // Whether to GPG sign + gpgPassphrase = // Only required if gpgSign is true and your key is passworded + publications = ['example'] // a list of publications (see below) + license { + name = 'example' + url = 'https://example.com' + distribution = 'repo' + } + developer { + id = 'a developer id' + name = 'a developer name' + email = 'example@example.com' + } + } + +.. note:: You can currently only have one license and developer in the maven POM sections + +**Publications** + +This plugin assumes, by default, that publications match the name of the project. This means, by default, you can +just list the names of the projects you wish to publish (e.g. to publish `test:myapp` you need `publications = ['myapp']`. +If a project requires a different name you can configure it *per project* with the project configuration block. + +The project configuration block has the following structure: + +.. code-block:: text + + publish { + name = 'non-default-project-name' + disableDefaultJar = false // set to true to disable the default JAR being created (e.g. when creating a fat JAR) + } + +**Artifacts** + +To add additional artifacts to the project you can use the default gradle `artifacts` block with the `publish` +configuration. For example: + + artifacts { + publish buildFatJar { + // You can configure this as a regular maven publication + } + } diff --git a/gradle-plugins/publish-utils/build.gradle b/gradle-plugins/publish-utils/build.gradle index bdaff87a9e..733de1bbee 100644 --- a/gradle-plugins/publish-utils/build.gradle +++ b/gradle-plugins/publish-utils/build.gradle @@ -2,6 +2,25 @@ apply plugin: 'groovy' apply plugin: 'maven-publish' apply plugin: 'com.jfrog.bintray' +// Used for bootstrapping project +buildscript { + // For sharing constants between builds + Properties props = new Properties() + file("../../publish.properties").withInputStream { props.load(it) } + + ext.gradle_plugins_version = props.getProperty("gradlePluginsVersion") + + repositories { + jcenter() + } + + dependencies { + classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.4' + } +} + +version "$gradle_plugins_version" + dependencies { compile gradleApi() compile localGroovy() @@ -81,3 +100,6 @@ publishing { } } } + +// Aliasing the publishToMavenLocal for simplicity. +task(install, dependsOn: 'publishToMavenLocal') diff --git a/gradle-plugins/publish-utils/src/main/groovy/net/corda/plugins/ProjectPublishExtension.groovy b/gradle-plugins/publish-utils/src/main/groovy/net/corda/plugins/ProjectPublishExtension.groovy new file mode 100644 index 0000000000..fbccf8b4dd --- /dev/null +++ b/gradle-plugins/publish-utils/src/main/groovy/net/corda/plugins/ProjectPublishExtension.groovy @@ -0,0 +1,12 @@ +package net.corda.plugins + +class ProjectPublishExtension { + /** + * Use a different name from the current project name for publishing + */ + String name + /** + * True when we do not want to publish default Java components + */ + Boolean disableDefaultJar = false +} \ No newline at end of file diff --git a/gradle-plugins/publish-utils/src/main/groovy/net/corda/plugins/PublishTasks.groovy b/gradle-plugins/publish-utils/src/main/groovy/net/corda/plugins/PublishTasks.groovy index c2d38e1f81..d94880dfe5 100644 --- a/gradle-plugins/publish-utils/src/main/groovy/net/corda/plugins/PublishTasks.groovy +++ b/gradle-plugins/publish-utils/src/main/groovy/net/corda/plugins/PublishTasks.groovy @@ -4,20 +4,159 @@ import org.gradle.api.* import org.gradle.api.tasks.bundling.Jar import org.gradle.api.tasks.javadoc.Javadoc import org.gradle.api.Project +import org.gradle.api.publish.maven.MavenPublication +import org.gradle.api.publish.maven.MavenPom +import net.corda.plugins.bintray.* /** * A utility plugin that when applied will automatically create source and javadoc publishing tasks + * To apply this plugin you must also add 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.4' to your + * buildscript's classpath dependencies. + * + * To use this plugin you can add a new configuration block (extension) to your root build.gradle. See the fields + * in BintrayConfigExtension. */ class PublishTasks implements Plugin { - void apply(Project project) { - project.task("sourceJar", type: Jar, dependsOn: project.classes) { - classifier = 'sources' - from project.sourceSets.main.allSource - } + Project project + String publishName + ProjectPublishExtension publishConfig - project.task("javadocJar", type: Jar, dependsOn: project.javadoc) { - classifier = 'javadoc' - from project.javadoc.destinationDir + void apply(Project project) { + this.project = project + + createTasks() + createExtensions() + createConfigurations() + + project.afterEvaluate { + configurePublishingName() + checkAndConfigurePublishing() } } + + void configurePublishingName() { + if(publishConfig.name != null) { + project.logger.info("Changing publishing name for ${project.name} to ${publishConfig.name}") + publishName = publishConfig.name + } else { + publishName = project.name + } + } + + void checkAndConfigurePublishing() { + project.logger.info("Checking whether to publish $publishName") + def bintrayConfig = project.rootProject.extensions.findByType(BintrayConfigExtension.class) + if((bintrayConfig != null) && (bintrayConfig.publications) && (bintrayConfig.publications.findAll { it == publishName }.size() > 0)) { + configurePublishing(bintrayConfig) + } + } + + void configurePublishing(BintrayConfigExtension bintrayConfig) { + project.logger.info("Configuring bintray for ${publishName}") + configureMavenPublish(bintrayConfig) + configureBintray(bintrayConfig) + } + + void configureMavenPublish(BintrayConfigExtension bintrayConfig) { + project.apply([plugin: 'maven-publish']) + project.publishing.publications.create(publishName, MavenPublication) { + if(!publishConfig.disableDefaultJar) { + from project.components.java + } + groupId project.group + artifactId publishName + + artifact project.tasks.sourceJar + artifact project.tasks.javadocJar + + project.configurations.publish.artifacts.each { + project.logger.debug("Adding artifact: $it") + delegate.artifact it + } + + extendPomForMavenCentral(pom, bintrayConfig) + } + project.task("install", dependsOn: "publishToMavenLocal") + } + + // Maven central requires all of the below fields for this to be a valid POM + void extendPomForMavenCentral(MavenPom pom, BintrayConfigExtension config) { + pom.withXml { + asNode().children().last() + { + resolveStrategy = Closure.DELEGATE_FIRST + name publishName + description project.description + url config.projectUrl + scm { + url config.vcsUrl + } + + licenses { + license { + name config.license.name + url config.license.url + distribution config.license.url + } + } + + developers { + developer { + id config.developer.id + name config.developer.name + email config.developer.email + } + } + } + } + } + + void configureBintray(BintrayConfigExtension bintrayConfig) { + project.apply([plugin: 'com.jfrog.bintray']) + project.bintray { + user = bintrayConfig.user + key = bintrayConfig.key + publications = [ publishName ] + dryRun = bintrayConfig.dryRun ?: false + pkg { + repo = bintrayConfig.repo + name = publishName + userOrg = bintrayConfig.org + licenses = bintrayConfig.licenses + + version { + gpg { + sign = bintrayConfig.gpgSign ?: false + passphrase = bintrayConfig.gpgPassphrase + } + } + } + } + } + + void createTasks() { + if(project.hasProperty('classes')) { + project.task("sourceJar", type: Jar, dependsOn: project.classes) { + classifier = 'sources' + from project.sourceSets.main.allSource + } + } + + if(project.hasProperty('javadoc')) { + project.task("javadocJar", type: Jar, dependsOn: project.javadoc) { + classifier = 'javadoc' + from project.javadoc.destinationDir + } + } + } + + void createExtensions() { + if(project == project.rootProject) { + project.extensions.create("bintrayConfig", BintrayConfigExtension) + } + publishConfig = project.extensions.create("publish", ProjectPublishExtension) + } + + void createConfigurations() { + project.configurations.create("publish") + } } diff --git a/gradle-plugins/publish-utils/src/main/groovy/net/corda/plugins/bintray/BintrayConfigExtension.groovy b/gradle-plugins/publish-utils/src/main/groovy/net/corda/plugins/bintray/BintrayConfigExtension.groovy new file mode 100644 index 0000000000..1a1c4e49e5 --- /dev/null +++ b/gradle-plugins/publish-utils/src/main/groovy/net/corda/plugins/bintray/BintrayConfigExtension.groovy @@ -0,0 +1,70 @@ +package net.corda.plugins.bintray + +import org.gradle.util.ConfigureUtil + +class BintrayConfigExtension { + /** + * Bintray username + */ + String user + /** + * Bintray access key + */ + String key + /** + * Bintray repository + */ + String repo + /** + * Bintray organisation + */ + String org + /** + * Licenses for packages uploaded by this configuration + */ + String[] licenses + /** + * Whether to sign packages uploaded by this configuration + */ + Boolean gpgSign + /** + * The passphrase for the key used to sign releases. + */ + String gpgPassphrase + /** + * VCS URL + */ + String vcsUrl + /** + * Project URL + */ + String projectUrl + /** + * The publications that will be uploaded as a part of this configuration. These must match both the name on + * bintray and the gradle module name. ie; it must be "some-package" as a gradle sub-module (root project not + * supported, this extension is to improve multi-build bintray uploads). The publication must also be called + * "some-package". Only one publication can be uploaded per module (a bintray plugin restriction(. + * If any of these conditions are not met your package will not be uploaded. + */ + String[] publications + /** + * Whether to test the publication without uploading to bintray. + */ + Boolean dryRun + /** + * The license this project will use (currently limited to one) + */ + License license = new License() + /** + * The developer of this project (currently limited to one) + */ + Developer developer = new Developer() + + void license(Closure closure) { + ConfigureUtil.configure(closure, license) + } + + void developer(Closure closure) { + ConfigureUtil.configure(closure, developer) + } +} \ No newline at end of file diff --git a/gradle-plugins/publish-utils/src/main/groovy/net/corda/plugins/bintray/Developer.groovy b/gradle-plugins/publish-utils/src/main/groovy/net/corda/plugins/bintray/Developer.groovy new file mode 100644 index 0000000000..1d66f68c7d --- /dev/null +++ b/gradle-plugins/publish-utils/src/main/groovy/net/corda/plugins/bintray/Developer.groovy @@ -0,0 +1,16 @@ +package net.corda.plugins.bintray + +class Developer { + /** + * A unique identifier the developer (eg; organisation ID) + */ + String id + /** + * The full name of the developer + */ + String name + /** + * An email address for contacting the developer + */ + String email +} \ No newline at end of file diff --git a/gradle-plugins/publish-utils/src/main/groovy/net/corda/plugins/bintray/License.groovy b/gradle-plugins/publish-utils/src/main/groovy/net/corda/plugins/bintray/License.groovy new file mode 100644 index 0000000000..1d06867bcf --- /dev/null +++ b/gradle-plugins/publish-utils/src/main/groovy/net/corda/plugins/bintray/License.groovy @@ -0,0 +1,16 @@ +package net.corda.plugins.bintray + +class License { + /** + * The name of license (eg; Apache 2.0) + */ + String name + /** + * URL to the full license file + */ + String url + /** + * The distribution level this license corresponds to (eg: repo) + */ + String distribution +} \ No newline at end of file diff --git a/gradle-plugins/quasar-utils/build.gradle b/gradle-plugins/quasar-utils/build.gradle index 32ebb113c9..ad4a10ecff 100644 --- a/gradle-plugins/quasar-utils/build.gradle +++ b/gradle-plugins/quasar-utils/build.gradle @@ -1,74 +1,14 @@ apply plugin: 'groovy' -apply plugin: 'net.corda.plugins.publish-utils' -apply plugin: 'com.jfrog.bintray' apply plugin: 'maven-publish' +apply plugin: 'net.corda.plugins.publish-utils' -dependencies { - compile gradleApi() - compile localGroovy() -} +description 'A small gradle plugin for adding some basic Quasar tasks and configurations to reduce build.gradle bloat.' repositories { mavenCentral() } -bintray { - user = System.getenv('CORDA_BINTRAY_USER') - key = System.getenv('CORDA_BINTRAY_KEY') - publications = ['quasarUtils'] - dryRun = false - pkg { - repo = 'corda' - name = 'quasar-utils' - userOrg = 'r3' - licenses = ['Apache-2.0'] - - version { - gpg { - sign = true - passphrase = System.getenv('CORDA_BINTRAY_GPG_PASSPHRASE') - } - } - } -} - -publishing { - publications { - quasarUtils(MavenPublication) { - from components.java - groupId 'net.corda.plugins' - artifactId 'quasar-utils' - - artifact sourceJar - artifact javadocJar - - pom.withXml { - asNode().children().last() + { - resolveStrategy = Closure.DELEGATE_FIRST - name 'quasar-utils' - description 'A small gradle plugin for adding some basic Quasar tasks and configurations to reduce build.gradle bloat.' - url 'https://github.com/corda/corda' - scm { - url 'https://github.com/corda/corda' - } - - licenses { - license { - name 'Apache-2.0' - url 'https://www.apache.org/licenses/LICENSE-2.0' - distribution 'repo' - } - } - - developers { - developer { - id 'R3' - name 'R3' - email 'dev@corda.net' - } - } - } - } - } - } -} +dependencies { + compile gradleApi() + compile localGroovy() +} \ No newline at end of file diff --git a/gradle-plugins/settings.gradle b/gradle-plugins/settings.gradle index 77b6f8f6b4..50e3950b93 100644 --- a/gradle-plugins/settings.gradle +++ b/gradle-plugins/settings.gradle @@ -1,4 +1,4 @@ rootProject.name = 'corda-gradle-plugins' -include 'quasar-utils' include 'publish-utils' +include 'quasar-utils' include 'cordformation' \ No newline at end of file diff --git a/node/build.gradle b/node/build.gradle index ed2cd6e5e6..7e39c1b5e6 100644 --- a/node/build.gradle +++ b/node/build.gradle @@ -2,7 +2,8 @@ apply plugin: 'kotlin' apply plugin: 'java' apply plugin: 'net.corda.plugins.quasar-utils' apply plugin: 'net.corda.plugins.publish-utils' -apply plugin: 'us.kirchmeier.capsule' + +description 'Corda node modules' repositories { mavenLocal() @@ -24,18 +25,8 @@ configurations { integrationTestCompile.extendsFrom testCompile integrationTestRuntime.extendsFrom testRuntime - - runtimeArtifacts.extendsFrom runtime } -// Force the Caplet to target Java 6. This ensures that running 'java -jar corda.jar' on any Java 6 VM upwards -// will get as far as the Capsule version checks, meaning that if your JVM is too old, you will at least get -// a sensible error message telling you what to do rather than a bytecode version exception that doesn't. -// If we introduce .java files into this module that need Java 8+ then we will have to push the caplet into -// its own module so its target can be controlled individually, but for now this suffices. -sourceCompatibility = 1.6 -targetCompatibility = 1.6 - sourceSets { integrationTest { kotlin { @@ -168,50 +159,4 @@ dependencies { task integrationTest(type: Test) { testClassesDir = sourceSets.integrationTest.output.classesDir classpath = sourceSets.integrationTest.runtimeClasspath -} - -task buildCordaJAR(type: FatCapsule, dependsOn: ['jar', 'buildCertSigningRequestUtilityJAR']) { - applicationClass 'net.corda.node.MainKt' - archiveName "corda-${corda_version}.jar" - applicationSource = files(project.tasks.findByName('jar'), 'build/classes/main/CordaCaplet.class', 'config/dev/log4j2.xml') - - capsuleManifest { - appClassPath = ["jolokia-agent-war-${project.rootProject.ext.jolokia_version}.war"] - javaAgents = ["quasar-core-${quasar_version}-jdk8.jar"] - minJavaVersion = '1.8.0' - caplets = ['CordaCaplet'] - } -} - -task buildCertSigningRequestUtilityJAR(type: FatCapsule, dependsOn: project.jar) { - applicationClass 'net.corda.node.utilities.certsigning.CertificateSignerKt' - archiveName 'certSigningRequestUtility.jar' - capsuleManifest { - systemProperties['log4j.configuration'] = 'log4j2.xml' - minJavaVersion = '1.8.0' - } -} - -artifacts { - runtimeArtifacts buildCordaJAR -} - -publishing { - publications { - node(MavenPublication) { - from components.java - artifactId 'node' - - artifact sourceJar - artifact javadocJar - } - corda(MavenPublication) { - artifactId 'corda' - - artifact buildCordaJAR { - classifier "" - } - } - - } } \ No newline at end of file diff --git a/node/capsule/build.gradle b/node/capsule/build.gradle new file mode 100644 index 0000000000..4598daecbd --- /dev/null +++ b/node/capsule/build.gradle @@ -0,0 +1,83 @@ +/** + * This build.gradle exists to publish our capsule (executable fat jar) to maven. It cannot be placed in the + * node project because the bintray plugin cannot publish two modules from one project. + */ +apply plugin: 'net.corda.plugins.publish-utils' +apply plugin: 'us.kirchmeier.capsule' + +description 'Corda standalone node' + +repositories { + mavenLocal() + mavenCentral() + maven { + url 'http://oss.sonatype.org/content/repositories/snapshots' + } + jcenter() + maven { + url 'https://dl.bintray.com/kotlin/exposed' + } +} + +configurations { + runtimeArtifacts.extendsFrom runtime +} + +// Force the Caplet to target Java 6. This ensures that running 'java -jar corda.jar' on any Java 6 VM upwards +// will get as far as the Capsule version checks, meaning that if your JVM is too old, you will at least get +// a sensible error message telling you what to do rather than a bytecode version exception that doesn't. +// If we introduce .java files into this module that need Java 8+ then we will have to push the caplet into +// its own module so its target can be controlled individually, but for now this suffices. +sourceCompatibility = 1.6 +targetCompatibility = 1.6 + +sourceSets { + test { + resources { + srcDir "../../config/test" + } + } + main { + resources { + srcDir "../../config/dev" + } + } +} + +dependencies { + compile project(':node') +} + +task buildCordaJAR(type: FatCapsule, dependsOn: ['buildCertSigningRequestUtilityJAR']) { + applicationClass 'net.corda.node.MainKt' + archiveName "corda-${corda_version}.jar" + applicationSource = files(project.tasks.findByName('jar'), 'build/classes/main/CordaCaplet.class', 'config/dev/log4j2.xml') + + capsuleManifest { + appClassPath = ["jolokia-agent-war-${project.rootProject.ext.jolokia_version}.war"] + javaAgents = ["quasar-core-${quasar_version}-jdk8.jar"] + minJavaVersion = '1.8.0' + caplets = ['CordaCaplet'] + } +} + +task buildCertSigningRequestUtilityJAR(type: FatCapsule) { + applicationClass 'net.corda.node.utilities.certsigning.CertificateSignerKt' + archiveName 'certSigningRequestUtility.jar' + capsuleManifest { + systemProperties['log4j.configuration'] = 'log4j2.xml' + minJavaVersion = '1.8.0' + } +} + +artifacts { + runtimeArtifacts buildCordaJAR + publish buildCordaJAR { + classifier "" + } +} + +publish { + name = 'corda' + disableDefaultJar = true +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/driver/Driver.kt b/node/src/main/kotlin/net/corda/node/driver/Driver.kt index 437a5b159c..b98e64f5f3 100644 --- a/node/src/main/kotlin/net/corda/node/driver/Driver.kt +++ b/node/src/main/kotlin/net/corda/node/driver/Driver.kt @@ -132,6 +132,9 @@ sealed class PortAllocation { * @param dsl The dsl itself. * @return The value returned in the [dsl] closure. */ + +// TODO: Add an @JvmOverloads annotation + fun driver( driverDirectory: Path = Paths.get("build", getTimestampAsDirectoryName()), portAllocation: PortAllocation = PortAllocation.Incremental(10000), diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index ac5bea5091..4b43c904aa 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -247,7 +247,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, val netwo // TODO: this model might change but for now it provides some de-coupling // Add vault observers - CashBalanceAsMetricsObserver(services) + CashBalanceAsMetricsObserver(services, database) ScheduledActivityObserver(services) HibernateObserver(services) diff --git a/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt b/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt index ac32fbf048..5ef9163882 100644 --- a/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt +++ b/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt @@ -42,7 +42,7 @@ abstract class ServiceHubInternal : PluginServiceHub { abstract val schemaService: SchemaService abstract override val networkService: MessagingServiceInternal - + /** * Given a list of [SignedTransaction]s, writes them to the given storage for validated transactions and then * sends them to the vault for further processing. This is intended for implementations to call from diff --git a/node/src/main/kotlin/net/corda/node/services/events/ScheduledActivityObserver.kt b/node/src/main/kotlin/net/corda/node/services/events/ScheduledActivityObserver.kt index 5a2acb09d2..5babdb4ea6 100644 --- a/node/src/main/kotlin/net/corda/node/services/events/ScheduledActivityObserver.kt +++ b/node/src/main/kotlin/net/corda/node/services/events/ScheduledActivityObserver.kt @@ -13,7 +13,7 @@ import net.corda.node.services.api.ServiceHubInternal */ class ScheduledActivityObserver(val services: ServiceHubInternal) { init { - services.vaultService.updates.subscribe { update -> + services.vaultService.rawUpdates.subscribe { update -> update.consumed.forEach { services.schedulerService.unscheduleStateActivity(it) } update.produced.forEach { scheduleStateActivity(it, services.flowLogicRefFactory) } } diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingComponent.kt b/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingComponent.kt index 004e7db67b..26ae128a26 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingComponent.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingComponent.kt @@ -136,6 +136,8 @@ abstract class ArtemisMessagingComponent() : SingletonSerializeAsToken() { TransportConstants.ENABLED_CIPHER_SUITES_PROP_NAME to CIPHER_SUITES.joinToString(","), TransportConstants.ENABLED_PROTOCOLS_PROP_NAME to "TLSv1.2", TransportConstants.NEED_CLIENT_AUTH_PROP_NAME to true + + // TODO: Set up the connector's host name verifier logic to ensure we connect to the expected node even in case of MITM or BGP hijacks ) ) } diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/CordaRPCClient.kt b/node/src/main/kotlin/net/corda/node/services/messaging/CordaRPCClient.kt index b5485a333c..e959406619 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/CordaRPCClient.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/CordaRPCClient.kt @@ -96,6 +96,9 @@ class CordaRPCClient(val host: HostAndPort, override val config: NodeSSLConfigur * * @throws RPCException if the server version is too low or if the server isn't reachable within the given time. */ + + // TODO: Add an @JvmOverloads annotation + @Throws(RPCException::class) fun proxy(timeout: Duration? = null, minVersion: Int = 0): CordaRPCOps { return state.locked { diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/RPCDispatcher.kt b/node/src/main/kotlin/net/corda/node/services/messaging/RPCDispatcher.kt index af64bd8353..daf96cb592 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/RPCDispatcher.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/RPCDispatcher.kt @@ -8,6 +8,7 @@ import com.esotericsoftware.kryo.io.Output import com.google.common.annotations.VisibleForTesting import com.google.common.collect.HashMultimap import net.corda.core.ErrorOr +import net.corda.core.crypto.commonName import net.corda.core.messaging.RPCOps import net.corda.core.messaging.RPCReturnsObservables import net.corda.core.serialization.SerializedBytes @@ -16,14 +17,12 @@ import net.corda.core.serialization.serialize import net.corda.core.utilities.debug import net.corda.node.services.RPCUserService import net.corda.node.services.User -import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.messaging.ArtemisMessagingComponent.Companion.NODE_USER import net.corda.node.utilities.AffinityExecutor import org.apache.activemq.artemis.api.core.Message import org.apache.activemq.artemis.api.core.client.ClientConsumer import org.apache.activemq.artemis.api.core.client.ClientMessage import org.bouncycastle.asn1.x500.X500Name -import org.bouncycastle.asn1.x500.style.BCStyle import rx.Notification import rx.Observable import rx.Subscription @@ -168,7 +167,7 @@ abstract class RPCDispatcher(val ops: RPCOps, val userService: RPCUserService, v val rpcUser = userService.getUser(validatedUser) if (rpcUser != null) { return rpcUser - } else if (X500Name(validatedUser).getRDNs(BCStyle.CN).first().first.value.toString() == nodeLegalName) { + } else if (X500Name(validatedUser).commonName == nodeLegalName) { return nodeUser } else { throw IllegalArgumentException("Validated user '$validatedUser' is not an RPC user nor the NODE user") diff --git a/node/src/main/kotlin/net/corda/node/services/network/InMemoryNetworkMapCache.kt b/node/src/main/kotlin/net/corda/node/services/network/InMemoryNetworkMapCache.kt index 3dafa190c6..09ff3ce2c2 100644 --- a/node/src/main/kotlin/net/corda/node/services/network/InMemoryNetworkMapCache.kt +++ b/node/src/main/kotlin/net/corda/node/services/network/InMemoryNetworkMapCache.kt @@ -24,6 +24,7 @@ import net.corda.node.services.network.NetworkMapService.Companion.SUBSCRIPTION_ import net.corda.node.services.network.NetworkMapService.FetchMapResponse import net.corda.node.services.network.NetworkMapService.SubscribeResponse import net.corda.node.utilities.AddOrRemove +import net.corda.node.utilities.bufferUntilDatabaseCommit import rx.Observable import rx.subjects.PublishSubject import java.security.SignatureException @@ -42,7 +43,9 @@ open class InMemoryNetworkMapCache : SingletonSerializeAsToken(), NetworkMapCach override val partyNodes: List get() = registeredNodes.map { it.value } override val networkMapNodes: List get() = getNodesWithService(NetworkMapService.type) private val _changed = PublishSubject.create() - override val changed: Observable = _changed + override val changed: Observable get() = _changed + private val changePublisher: rx.Observer get() = _changed.bufferUntilDatabaseCommit() + private val _registrationFuture = SettableFuture.create() override val mapServiceRegistered: ListenableFuture get() = _registrationFuture @@ -91,9 +94,9 @@ open class InMemoryNetworkMapCache : SingletonSerializeAsToken(), NetworkMapCach synchronized(_changed) { val previousNode = registeredNodes.put(node.legalIdentity, node) if (previousNode == null) { - _changed.onNext(MapChange.Added(node)) + changePublisher.onNext(MapChange.Added(node)) } else if (previousNode != node) { - _changed.onNext(MapChange.Modified(node, previousNode)) + changePublisher.onNext(MapChange.Modified(node, previousNode)) } } } @@ -101,7 +104,7 @@ open class InMemoryNetworkMapCache : SingletonSerializeAsToken(), NetworkMapCach override fun removeNode(node: NodeInfo) { synchronized(_changed) { registeredNodes.remove(node.legalIdentity) - _changed.onNext(MapChange.Removed(node)) + changePublisher.onNext(MapChange.Removed(node)) } } diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionMappingStorage.kt b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionMappingStorage.kt index ee4e962c94..6cca46d1b9 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionMappingStorage.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionMappingStorage.kt @@ -50,7 +50,7 @@ class DBTransactionMappingStorage : StateMachineRecordedTransactionMappingStorag override fun addMapping(stateMachineRunId: StateMachineRunId, transactionId: SecureHash) { mutex.locked { stateMachineTransactionMap[transactionId] = stateMachineRunId - updates.onNext(StateMachineTransactionMapping(stateMachineRunId, transactionId)) + updates.bufferUntilDatabaseCommit().onNext(StateMachineTransactionMapping(stateMachineRunId, transactionId)) } } diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt index fca852a687..3ee8a0416f 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/DBTransactionStorage.kt @@ -40,7 +40,7 @@ class DBTransactionStorage : TransactionStorage { val old = txStorage.get(transaction.id) if (old == null) { txStorage.put(transaction.id, transaction) - updatesPublisher.onNext(transaction) + updatesPublisher.bufferUntilDatabaseCommit().onNext(transaction) true } else { false diff --git a/node/src/main/kotlin/net/corda/node/services/schema/HibernateObserver.kt b/node/src/main/kotlin/net/corda/node/services/schema/HibernateObserver.kt index f8dd0d4599..5e32108c33 100644 --- a/node/src/main/kotlin/net/corda/node/services/schema/HibernateObserver.kt +++ b/node/src/main/kotlin/net/corda/node/services/schema/HibernateObserver.kt @@ -35,7 +35,7 @@ class HibernateObserver(services: ServiceHubInternal) { val sessionFactories = ConcurrentHashMap() init { - services.vaultService.updates.subscribe { persist(it.produced) } + services.vaultService.rawUpdates.subscribe { persist(it.produced) } } private fun sessionFactoryForSchema(schema: MappedSchema): SessionFactory { diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStateMachineImpl.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStateMachineImpl.kt index c929cb6319..3d2cff0cc5 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStateMachineImpl.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStateMachineImpl.kt @@ -86,15 +86,16 @@ class FlowStateMachineImpl(override val id: StateMachineRunId, val result = try { logic.call() } catch (t: Throwable) { - processException(t) + actionOnEnd() commitTransaction() + _resultFuture?.setException(t) throw ExecutionException(t) } // This is to prevent actionOnEnd being called twice if it throws an exception actionOnEnd() - _resultFuture?.set(result) commitTransaction() + _resultFuture?.set(result) return result } @@ -264,8 +265,8 @@ class FlowStateMachineImpl(override val id: StateMachineRunId, // This can get called in actionOnSuspend *after* we commit the database transaction, so optionally open a new one here. databaseTransaction(database) { actionOnEnd() - _resultFuture?.setException(t) } + _resultFuture?.setException(t) } internal fun resume(scheduler: FiberScheduler) { diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt index 2d947cd152..69769b2799 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt @@ -12,6 +12,7 @@ import kotlinx.support.jdk8.collections.removeIf import net.corda.core.ThreadBox import net.corda.core.abbreviate import net.corda.core.crypto.Party +import net.corda.core.crypto.commonName import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowStateMachine import net.corda.core.flows.StateMachineRunId @@ -29,6 +30,7 @@ import net.corda.node.services.api.CheckpointStorage import net.corda.node.services.api.ServiceHubInternal import net.corda.node.utilities.AddOrRemove import net.corda.node.utilities.AffinityExecutor +import net.corda.node.utilities.bufferUntilDatabaseCommit import net.corda.node.utilities.isolatedTransaction import org.apache.activemq.artemis.utils.ReusableLatch import org.jetbrains.exposed.sql.Database @@ -93,7 +95,7 @@ class StateMachineManager(val serviceHub: ServiceHubInternal, val changesPublisher = PublishSubject.create() fun notifyChangeObservers(psm: FlowStateMachineImpl<*>, addOrRemove: AddOrRemove) { - changesPublisher.onNext(Change(psm.logic, addOrRemove, psm.id)) + changesPublisher.bufferUntilDatabaseCommit().onNext(Change(psm.logic, addOrRemove, psm.id)) } }) @@ -219,7 +221,7 @@ class StateMachineManager(val serviceHub: ServiceHubInternal, // isn't required to be unique // TODO For now have the doorman block signups with identical names, and names with characters that // are used in X.500 name textual serialisation - val otherParty = serviceHub.networkMapCache.getNodeByLegalName(message.peerLegalName)?.legalIdentity + val otherParty = serviceHub.networkMapCache.getNodeByLegalName(message.peer.commonName)?.legalIdentity if (otherParty != null) { onSessionInit(sessionMessage, otherParty) } else { @@ -393,13 +395,14 @@ class StateMachineManager(val serviceHub: ServiceHubInternal, * restarted with checkpointed state machines in the storage service. */ fun add(logic: FlowLogic): FlowStateMachine { - val fiber = createFiber(logic) // We swap out the parent transaction context as using this frequently leads to a deadlock as we wait // on the flow completion future inside that context. The problem is that any progress checkpoints are // unable to acquire the table lock and move forward till the calling transaction finishes. // Committing in line here on a fresh context ensure we can progress. - isolatedTransaction(database) { + val fiber = isolatedTransaction(database) { + val fiber = createFiber(logic) updateCheckpoint(fiber) + fiber } // If we are not started then our checkpoint will be picked up during start mutex.locked { diff --git a/node/src/main/kotlin/net/corda/node/services/vault/CashBalanceAsMetricsObserver.kt b/node/src/main/kotlin/net/corda/node/services/vault/CashBalanceAsMetricsObserver.kt index a9fe08bf33..b6e05fdf17 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/CashBalanceAsMetricsObserver.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/CashBalanceAsMetricsObserver.kt @@ -3,12 +3,14 @@ package net.corda.node.services.vault import com.codahale.metrics.Gauge import net.corda.core.node.services.VaultService import net.corda.node.services.api.ServiceHubInternal +import net.corda.node.utilities.databaseTransaction +import org.jetbrains.exposed.sql.Database import java.util.* /** * This class observes the vault and reflect current cash balances as exposed metrics in the monitoring service. */ -class CashBalanceAsMetricsObserver(val serviceHubInternal: ServiceHubInternal) { +class CashBalanceAsMetricsObserver(val serviceHubInternal: ServiceHubInternal, val database: Database) { init { // TODO: Need to consider failure scenarios. This needs to run if the TX is successfully recorded serviceHubInternal.vaultService.updates.subscribe { update -> @@ -29,13 +31,15 @@ class CashBalanceAsMetricsObserver(val serviceHubInternal: ServiceHubInternal) { // // Note: exported as pennies. val m = serviceHubInternal.monitoringService.metrics - for ((key, value) in vault.cashBalances) { - val metric = balanceMetrics.getOrPut(key) { - val newMetric = BalanceMetric() - m.register("VaultBalances.${key}Pennies", newMetric) - newMetric + databaseTransaction(database) { + for ((key, value) in vault.cashBalances) { + val metric = balanceMetrics.getOrPut(key) { + val newMetric = BalanceMetric() + m.register("VaultBalances.${key}Pennies", newMetric) + newMetric + } + metric.pennies = value.quantity } - metric.pennies = value.quantity } } } diff --git a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt index a03b65cbf7..72946d5272 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt @@ -12,6 +12,7 @@ import net.corda.core.node.ServiceHub import net.corda.core.node.services.Vault import net.corda.core.node.services.VaultService import net.corda.core.serialization.SingletonSerializeAsToken +import net.corda.core.tee import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.WireTransaction import net.corda.core.utilities.loggerFor @@ -80,6 +81,9 @@ class NodeVaultService(private val services: ServiceHub) : SingletonSerializeAsT } val _updatesPublisher = PublishSubject.create() + val _rawUpdatesPublisher = PublishSubject.create() + // For use during publishing only. + val updatesPublisher: rx.Observer get() = _updatesPublisher.bufferUntilDatabaseCommit().tee(_rawUpdatesPublisher) fun allUnconsumedStates(): Iterable> { // Order by txhash for if and when transaction storage has some caching. @@ -104,6 +108,9 @@ class NodeVaultService(private val services: ServiceHub) : SingletonSerializeAsT override val currentVault: Vault get() = mutex.locked { Vault(allUnconsumedStates()) } + override val rawUpdates: Observable + get() = mutex.locked { _rawUpdatesPublisher } + override val updates: Observable get() = mutex.locked { _updatesPublisher } @@ -127,7 +134,7 @@ class NodeVaultService(private val services: ServiceHub) : SingletonSerializeAsT if (netDelta != Vault.NoUpdate) { mutex.locked { recordUpdate(netDelta) - _updatesPublisher.onNext(netDelta) + updatesPublisher.onNext(netDelta) } } return currentVault diff --git a/node/src/main/kotlin/net/corda/node/utilities/DatabaseSupport.kt b/node/src/main/kotlin/net/corda/node/utilities/DatabaseSupport.kt index 16d7953a3b..4bdab93154 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/DatabaseSupport.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/DatabaseSupport.kt @@ -7,9 +7,13 @@ import net.corda.core.crypto.CompositeKey import net.corda.core.crypto.SecureHash import net.corda.core.crypto.parsePublicKeyBase58 import net.corda.core.crypto.toBase58String +import net.corda.node.utilities.StrandLocalTransactionManager.Boundary import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.transactions.TransactionInterface import org.jetbrains.exposed.sql.transactions.TransactionManager +import rx.Observable +import rx.subjects.PublishSubject +import rx.subjects.UnicastSubject import java.io.Closeable import java.security.PublicKey import java.sql.Connection @@ -18,6 +22,7 @@ import java.time.LocalDate import java.time.LocalDateTime import java.time.ZoneOffset import java.util.* +import java.util.concurrent.ConcurrentHashMap /** * Table prefix for all tables owned by the node module. @@ -78,12 +83,19 @@ fun isolatedTransaction(database: Database, block: Transaction.() -> T): T { * over each other. So here we use a companion object to hold them as [ThreadLocal] and [StrandLocalTransactionManager] * is otherwise effectively stateless so it's replacement does not matter. The [ThreadLocal] is then set correctly and * explicitly just prior to initiating a transaction in [databaseTransaction] and [createDatabaseTransaction] above. + * + * The [StrandLocalTransactionManager] instances have an [Observable] of the transaction close [Boundary]s which + * facilitates the use of [Observable.afterDatabaseCommit] to create event streams that only emit once the database + * transaction is closed and the data has been persisted and becomes visible to other observers. */ class StrandLocalTransactionManager(initWithDatabase: Database) : TransactionManager { companion object { + private val TX_ID = Key() + private val threadLocalDb = ThreadLocal() private val threadLocalTx = ThreadLocal() + private val databaseToInstance = ConcurrentHashMap() fun setThreadLocalTx(tx: Transaction?): Pair { val oldTx = threadLocalTx.get() @@ -101,10 +113,21 @@ class StrandLocalTransactionManager(initWithDatabase: Database) : TransactionMan set(value: Database) { threadLocalDb.set(value) } + + val transactionId: UUID + get() = threadLocalTx.get()?.getUserData(TX_ID) ?: throw IllegalStateException("Was expecting to find transaction set on current strand: ${Strand.currentStrand()}") + + val manager: StrandLocalTransactionManager get() = databaseToInstance[database]!! + + val transactionBoundaries: PublishSubject get() = manager._transactionBoundaries } + + data class Boundary(val txId: UUID) + + private val _transactionBoundaries = PublishSubject.create() + init { - database = initWithDatabase // Found a unit test that was forgetting to close the database transactions. When you close() on the top level // database transaction it will reset the threadLocalTx back to null, so if it isn't then there is still a // databae transaction open. The [databaseTransaction] helper above handles this in a finally clause for you @@ -112,16 +135,23 @@ class StrandLocalTransactionManager(initWithDatabase: Database) : TransactionMan if (threadLocalTx.get() != null) { throw IllegalStateException("Was not expecting to find existing database transaction on current strand when setting database: ${Strand.currentStrand()}, ${threadLocalTx.get()}") } + database = initWithDatabase + databaseToInstance[database] = this } - override fun newTransaction(isolation: Int): Transaction = Transaction(StrandLocalTransaction(database, isolation, threadLocalTx)).apply { - threadLocalTx.set(this) + override fun newTransaction(isolation: Int): Transaction { + val impl = StrandLocalTransaction(database, isolation, threadLocalTx, transactionBoundaries) + return Transaction(impl).apply { + threadLocalTx.set(this) + putUserData(TX_ID, impl.id) + } } override fun currentOrNull(): Transaction? = threadLocalTx.get() // Direct copy of [ThreadLocalTransaction]. - private class StrandLocalTransaction(override val db: Database, isolation: Int, val threadLocal: ThreadLocal) : TransactionInterface { + private class StrandLocalTransaction(override val db: Database, isolation: Int, val threadLocal: ThreadLocal, val transactionBoundaries: PublishSubject) : TransactionInterface { + val id = UUID.randomUUID() override val connection: Connection by lazy(LazyThreadSafetyMode.NONE) { db.connector().apply { @@ -145,13 +175,33 @@ class StrandLocalTransactionManager(initWithDatabase: Database) : TransactionMan override fun close() { connection.close() threadLocal.set(outerTransaction) + if (outerTransaction == null) { + transactionBoundaries.onNext(Boundary(id)) + } } } } +/** + * Buffer observations until after the current database transaction has been closed. Observations are never + * dropped, simply delayed. + * + * Primarily for use by component authors to publish observations during database transactions without racing against + * closing the database transaction. + * + * For examples, see the call hierarchy of this function. + */ +fun rx.Observer.bufferUntilDatabaseCommit(): rx.Observer { + val currentTxId = StrandLocalTransactionManager.transactionId + val databaseTxBoundary: Observable = StrandLocalTransactionManager.transactionBoundaries.filter { it.txId == currentTxId }.first() + val subject = UnicastSubject.create() + subject.delaySubscription(databaseTxBoundary).subscribe(this) + databaseTxBoundary.doOnCompleted { subject.onCompleted() } + return subject +} + // Composite columns for use with below Exposed helpers. data class PartyColumns(val name: Column, val owningKey: Column) - data class StateRefColumns(val txId: Column, val index: Column) data class TxnNoteColumns(val txId: Column, val note: Column) diff --git a/node/src/main/kotlin/net/corda/node/utilities/certsigning/HTTPCertificateSigningService.kt b/node/src/main/kotlin/net/corda/node/utilities/certsigning/HTTPCertificateSigningService.kt index 2c5f1ed6f6..b634289cef 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/certsigning/HTTPCertificateSigningService.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/certsigning/HTTPCertificateSigningService.kt @@ -1,12 +1,13 @@ package net.corda.node.utilities.certsigning +import net.corda.core.crypto.CertificateStream import org.apache.commons.io.IOUtils import org.bouncycastle.pkcs.PKCS10CertificationRequest import java.io.IOException import java.net.HttpURLConnection +import java.net.HttpURLConnection.* import java.net.URL import java.security.cert.Certificate -import java.security.cert.CertificateFactory import java.util.* import java.util.zip.ZipInputStream @@ -24,18 +25,17 @@ class HTTPCertificateSigningService(val server: URL) : CertificateSigningService conn.requestMethod = "GET" return when (conn.responseCode) { - HttpURLConnection.HTTP_OK -> conn.inputStream.use { - ZipInputStream(it).use { - val certificates = ArrayList() - while (it.nextEntry != null) { - certificates.add(CertificateFactory.getInstance("X.509").generateCertificate(it)) - } - certificates.toTypedArray() + HTTP_OK -> ZipInputStream(conn.inputStream).use { + val certificates = ArrayList() + val stream = CertificateStream(it) + while (it.nextEntry != null) { + certificates.add(stream.nextCertificate()) } + certificates.toTypedArray() } - HttpURLConnection.HTTP_NO_CONTENT -> null - HttpURLConnection.HTTP_UNAUTHORIZED -> throw IOException("Certificate signing request has been rejected, please contact Corda network administrator for more information.") - else -> throw IOException("Unexpected response code ${conn.responseCode} - ${IOUtils.toString(conn.errorStream)}") + HTTP_NO_CONTENT -> null + HTTP_UNAUTHORIZED -> throw IOException("Certificate signing request has been rejected: ${conn.errorMessage}") + else -> throwUnexpectedResponseCode(conn) } } @@ -49,10 +49,15 @@ class HTTPCertificateSigningService(val server: URL) : CertificateSigningService conn.outputStream.write(request.encoded) return when (conn.responseCode) { - HttpURLConnection.HTTP_OK -> IOUtils.toString(conn.inputStream) - HttpURLConnection.HTTP_FORBIDDEN -> throw IOException("Client version $clientVersion is forbidden from accessing permissioning server, please upgrade to newer version.") - else -> throw IOException("Unexpected response code ${conn.responseCode} - ${IOUtils.toString(conn.errorStream)}") + HTTP_OK -> IOUtils.toString(conn.inputStream) + HTTP_FORBIDDEN -> throw IOException("Client version $clientVersion is forbidden from accessing permissioning server, please upgrade to newer version.") + else -> throwUnexpectedResponseCode(conn) } - } + + private fun throwUnexpectedResponseCode(connection: HttpURLConnection): Nothing { + throw IOException("Unexpected response code ${connection.responseCode} - ${connection.errorMessage}") + } + + private val HttpURLConnection.errorMessage: String get() = IOUtils.toString(errorStream) } diff --git a/node/src/test/kotlin/net/corda/node/services/ArtemisMessagingTests.kt b/node/src/test/kotlin/net/corda/node/services/ArtemisMessagingTests.kt index b474ce1138..1f8fd97558 100644 --- a/node/src/test/kotlin/net/corda/node/services/ArtemisMessagingTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/ArtemisMessagingTests.kt @@ -8,6 +8,7 @@ import com.typesafe.config.ConfigFactory import net.corda.core.crypto.composite import net.corda.core.crypto.generateKeyPair import net.corda.core.messaging.Message +import net.corda.core.messaging.RPCOps import net.corda.core.messaging.createMessage import net.corda.core.node.services.DEFAULT_SESSION_ID import net.corda.core.utilities.LogHelper @@ -16,7 +17,6 @@ import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.config.configureWithDevSSLCertificate import net.corda.node.services.messaging.ArtemisMessagingServer import net.corda.node.services.messaging.NodeMessagingClient -import net.corda.core.messaging.RPCOps import net.corda.node.services.network.InMemoryNetworkMapCache import net.corda.node.services.network.NetworkMapService import net.corda.node.services.transactions.PersistentUniquenessProvider diff --git a/node/src/test/kotlin/net/corda/node/services/InMemoryNetworkMapCacheTest.kt b/node/src/test/kotlin/net/corda/node/services/InMemoryNetworkMapCacheTest.kt index de5d6b4e96..9fbc81b658 100644 --- a/node/src/test/kotlin/net/corda/node/services/InMemoryNetworkMapCacheTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/InMemoryNetworkMapCacheTest.kt @@ -5,6 +5,7 @@ import net.corda.core.crypto.generateKeyPair import net.corda.core.getOrThrow import net.corda.core.node.services.ServiceInfo import net.corda.node.services.network.NetworkMapService +import net.corda.node.utilities.databaseTransaction import net.corda.testing.expect import net.corda.testing.node.MockNetwork import org.junit.Test @@ -30,7 +31,9 @@ class InMemoryNetworkMapCacheTest { // Node A currently knows only about itself, so this returns node A assertEquals(nodeA.netMapCache.getNodeByCompositeKey(keyPair.public.composite), nodeA.info) - nodeA.netMapCache.addNode(nodeB.info) + databaseTransaction(nodeA.database) { + nodeA.netMapCache.addNode(nodeB.info) + } // Now both nodes match, so it throws an error expect { nodeA.netMapCache.getNodeByCompositeKey(keyPair.public.composite) diff --git a/node/src/test/kotlin/net/corda/node/services/InMemoryNetworkMapServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/InMemoryNetworkMapServiceTest.kt index be387b6131..b0a0ebab1a 100644 --- a/node/src/test/kotlin/net/corda/node/services/InMemoryNetworkMapServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/InMemoryNetworkMapServiceTest.kt @@ -16,6 +16,7 @@ import net.corda.node.services.network.NetworkMapService.Companion.REGISTER_FLOW import net.corda.node.services.network.NetworkMapService.Companion.SUBSCRIPTION_FLOW_TOPIC import net.corda.node.services.network.NodeRegistration import net.corda.node.utilities.AddOrRemove +import net.corda.node.utilities.databaseTransaction import net.corda.testing.node.MockNetwork import net.corda.testing.node.MockNetwork.MockNode import org.junit.Before @@ -201,7 +202,9 @@ class InMemoryNetworkMapServiceTest : AbstractNetworkMapServiceTest() { fun success() { val (mapServiceNode, registerNode) = network.createTwoNodes() val service = mapServiceNode.inNodeNetworkMapService!! as InMemoryNetworkMapService - success(mapServiceNode, registerNode, { service }, { }) + databaseTransaction(mapServiceNode.database) { + success(mapServiceNode, registerNode, { service }, { }) + } } @Test diff --git a/node/src/test/kotlin/net/corda/node/utilities/ObservablesTests.kt b/node/src/test/kotlin/net/corda/node/utilities/ObservablesTests.kt new file mode 100644 index 0000000000..6368c33bb8 --- /dev/null +++ b/node/src/test/kotlin/net/corda/node/utilities/ObservablesTests.kt @@ -0,0 +1,142 @@ +package net.corda.node.utilities + +import com.google.common.util.concurrent.SettableFuture +import net.corda.core.tee +import net.corda.testing.node.makeTestDataSourceProperties +import org.assertj.core.api.Assertions.assertThat +import org.jetbrains.exposed.sql.transactions.TransactionManager +import org.junit.Test +import rx.Observable +import rx.subjects.PublishSubject + +class ObservablesTests { + + private fun isInDatabaseTransaction(): Boolean = (TransactionManager.currentOrNull() != null) + + @Test + fun `bufferUntilDatabaseCommit delays until transaction closed`() { + val (toBeClosed, database) = configureDatabase(makeTestDataSourceProperties()) + + val subject = PublishSubject.create() + val observable: Observable = subject + + val firstEvent = SettableFuture.create>() + val secondEvent = SettableFuture.create>() + + observable.first().subscribe { firstEvent.set(it to isInDatabaseTransaction()) } + observable.skip(1).first().subscribe { secondEvent.set(it to isInDatabaseTransaction()) } + + databaseTransaction(database) { + val delayedSubject = subject.bufferUntilDatabaseCommit() + assertThat(subject).isNotEqualTo(delayedSubject) + delayedSubject.onNext(0) + subject.onNext(1) + assertThat(firstEvent.isDone).isTrue() + assertThat(secondEvent.isDone).isFalse() + } + assertThat(secondEvent.isDone).isTrue() + + assertThat(firstEvent.get()).isEqualTo(1 to true) + assertThat(secondEvent.get()).isEqualTo(0 to false) + + toBeClosed.close() + } + + @Test + fun `bufferUntilDatabaseCommit delays until transaction closed repeatable`() { + val (toBeClosed, database) = configureDatabase(makeTestDataSourceProperties()) + + val subject = PublishSubject.create() + val observable: Observable = subject + + val firstEvent = SettableFuture.create>() + val secondEvent = SettableFuture.create>() + + observable.first().subscribe { firstEvent.set(it to isInDatabaseTransaction()) } + observable.skip(1).first().subscribe { secondEvent.set(it to isInDatabaseTransaction()) } + + databaseTransaction(database) { + val delayedSubject = subject.bufferUntilDatabaseCommit() + assertThat(subject).isNotEqualTo(delayedSubject) + delayedSubject.onNext(0) + assertThat(firstEvent.isDone).isFalse() + assertThat(secondEvent.isDone).isFalse() + } + assertThat(firstEvent.isDone).isTrue() + assertThat(firstEvent.get()).isEqualTo(0 to false) + assertThat(secondEvent.isDone).isFalse() + + databaseTransaction(database) { + val delayedSubject = subject.bufferUntilDatabaseCommit() + assertThat(subject).isNotEqualTo(delayedSubject) + delayedSubject.onNext(1) + assertThat(secondEvent.isDone).isFalse() + } + assertThat(secondEvent.isDone).isTrue() + assertThat(secondEvent.get()).isEqualTo(1 to false) + + toBeClosed.close() + } + + @Test + fun `tee correctly copies observations to multiple observers`() { + + val subject1 = PublishSubject.create() + val subject2 = PublishSubject.create() + val subject3 = PublishSubject.create() + + val event1 = SettableFuture.create() + val event2 = SettableFuture.create() + val event3 = SettableFuture.create() + + subject1.subscribe { event1.set(it) } + subject2.subscribe { event2.set(it) } + subject3.subscribe { event3.set(it) } + + val tee = subject1.tee(subject2, subject3) + tee.onNext(0) + + assertThat(event1.isDone).isTrue() + assertThat(event2.isDone).isTrue() + assertThat(event3.isDone).isTrue() + assertThat(event1.get()).isEqualTo(0) + assertThat(event2.get()).isEqualTo(0) + assertThat(event3.get()).isEqualTo(0) + + tee.onCompleted() + assertThat(subject1.hasCompleted()).isTrue() + assertThat(subject2.hasCompleted()).isTrue() + assertThat(subject3.hasCompleted()).isTrue() + } + + @Test + fun `combine tee and bufferUntilDatabaseCommit`() { + val (toBeClosed, database) = configureDatabase(makeTestDataSourceProperties()) + + val subject = PublishSubject.create() + val teed = PublishSubject.create() + + val observable: Observable = subject + + val firstEvent = SettableFuture.create>() + val teedEvent = SettableFuture.create>() + + observable.first().subscribe { firstEvent.set(it to isInDatabaseTransaction()) } + + teed.first().subscribe { teedEvent.set(it to isInDatabaseTransaction()) } + + databaseTransaction(database) { + val delayedSubject = subject.bufferUntilDatabaseCommit().tee(teed) + assertThat(subject).isNotEqualTo(delayedSubject) + delayedSubject.onNext(0) + assertThat(firstEvent.isDone).isFalse() + assertThat(teedEvent.isDone).isTrue() + } + assertThat(firstEvent.isDone).isTrue() + + assertThat(firstEvent.get()).isEqualTo(0 to false) + assertThat(teedEvent.get()).isEqualTo(0 to true) + + toBeClosed.close() + } +} \ No newline at end of file diff --git a/publish.properties b/publish.properties new file mode 100644 index 0000000000..1c38857662 --- /dev/null +++ b/publish.properties @@ -0,0 +1 @@ +gradlePluginsVersion=0.6.2 \ No newline at end of file diff --git a/samples/README.md b/samples/README.md index 2c9d3fbaa2..fd33b5e052 100644 --- a/samples/README.md +++ b/samples/README.md @@ -7,4 +7,5 @@ Please refer to `README.md` in the individual project folders. There are the fo * **trader-demo** A simple driver for exercising the two party trading flow. In this scenario, a buyer wants to purchase some commercial paper by swapping his cash for commercial paper. The seller learns that the buyer exists, and sends them a message to kick off the trade. The seller, having obtained his CP, then quits and the buyer goes back to waiting. The buyer will sell as much CP as he can! **We recommend starting with this demo.** * **Network-visualiser** A tool that uses a simulation to visualise the interaction and messages between nodes on the Corda network. Currently only works for the IRS demo. * **simm-valudation-demo** A demo showing two nodes reaching agreement on the valuation of a derivatives portfolio. -* **raft-notary-demo** A simple demonstration of a node getting multiple transactions notarised by a distributed (Raft-based) notary. \ No newline at end of file +* **raft-notary-demo** A simple demonstration of a node getting multiple transactions notarised by a distributed (Raft-based) notary. +* **bank-of-corda-demo** A demo showing a node acting as an issuer of fungible assets (initially Cash) \ No newline at end of file diff --git a/samples/attachment-demo/build.gradle b/samples/attachment-demo/build.gradle index ae1132d1de..5c80cc3649 100644 --- a/samples/attachment-demo/build.gradle +++ b/samples/attachment-demo/build.gradle @@ -45,7 +45,7 @@ dependencies { testCompile group: 'junit', name: 'junit', version: '4.11' // Corda integration dependencies - runtime project(path: ":node", configuration: 'runtimeArtifacts') + runtime project(path: ":node:capsule", configuration: 'runtimeArtifacts') compile project(':core') compile project(':test-utils') diff --git a/samples/bank-of-corda-demo/README.md b/samples/bank-of-corda-demo/README.md new file mode 100644 index 0000000000..89bf18fc0e --- /dev/null +++ b/samples/bank-of-corda-demo/README.md @@ -0,0 +1,57 @@ +# Bank of Corda demo +Please see docs/build/html/running-the-demos.html + +This program simulates the role of an asset issuing authority (eg. central bank for cash) by accepting requests +from third parties to issue some quantity of an asset and transfer that ownership to the requester. +The issuing authority accepts requests via the [IssuerFlow] flow, self-issues the asset and transfers +ownership to the issue requester. Notarisation and signing form part of the flow. + +The requesting party can be a CorDapp (running locally or remotely to the Bank of Corda node), a remote RPC client or +a Web Client. + +## Prerequisites + +You will need to have [JDK 8](http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html) +installed and available on your path. + +## Getting Started + +1. Launch the Bank of Corda node (and associated Notary) by running: +[BankOfCordaDriver] --role ISSUER +(to validate your Node is running you can try navigating to this sample link: http://localhost:10005/api/bank/date) + +Each of the following commands will launch a separate Node called Big Corporation which will become the owner +of some Cash following an issue request: + +2. Run the Bank of Corda Client driver (to simulate a web issue requester) by running: +[BankOfCordaDriver] --role ISSUE_CASH_WEB +This demonstrates a remote application acting on behalf of the Big Corporation and communicating directly with the +Bank of Corda node via HTTP to request issuance of some cash. + +3. Run the Bank of Corda Client driver (to simulate an RPC issue requester) by running: +[BankOfCordaDriver] --role ISSUE_CASH_RPC +Similar to 3 above, but using RPC as the remote communications mechanism. + +## Developer notes + +Testing of the Bank of Corda application is demonstrated at two levels: +1. Unit testing the flow uses the [LedgerDSL] and [MockServices] +2. Integration testing via RPC and HTTP uses the [Driver] DSL to launch standalone node instances + +Security +The RPC API requires a client to pass in user credentials: + client.start("user1","test") +which are validated on the Bank of Corda node against those configured at node startup: + User("user1", "test", permissions = setOf(startFlowPermission())) + startNode("BankOfCorda", rpcUsers = listOf(user)) + +Notary +We are using a [SimpleNotaryService] in this example, but could easily switch to a [ValidatingNotaryService] + +## Future + +The Bank of Corda node will become an integral part of other Corda samples that require initial issuance of some asset. + +## Further Reading + +Tutorials and developer docs for Cordapps and Corda are [here](https://docs.corda.net/). \ No newline at end of file diff --git a/samples/bank-of-corda-demo/build.gradle b/samples/bank-of-corda-demo/build.gradle new file mode 100644 index 0000000000..d4b0471a01 --- /dev/null +++ b/samples/bank-of-corda-demo/build.gradle @@ -0,0 +1,148 @@ +apply plugin: 'java' +apply plugin: 'kotlin' +apply plugin: 'idea' +apply plugin: 'net.corda.plugins.quasar-utils' +apply plugin: 'net.corda.plugins.publish-utils' +apply plugin: 'net.corda.plugins.cordformation' +apply plugin: 'maven-publish' + +repositories { + mavenLocal() + mavenCentral() + jcenter() + maven { + url 'https://dl.bintray.com/kotlin/exposed' + } +} + +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:$kotlin_version" + testCompile group: 'junit', name: 'junit', version: '4.11' + + // Corda integration dependencies + runtime project(path: ":node:capsule", configuration: 'runtimeArtifacts') + compile project(':core') + compile project(':client') + compile project(':node') + compile project(':finance') + compile project(':test-utils') + + // Javax is required for webapis + compile "org.glassfish.jersey.core:jersey-server:${jersey_version}" +} + +task deployNodes(type: net.corda.plugins.Cordform, dependsOn: [':install', 'build']) { + directory "./build/nodes" + // This name "Notary" is hard-coded into BankOfCordaClientApi so if you change it here, change it there too. + // In this demo the node that runs a standalone notary also acts as the network map server. + networkMap "Notary" + node { + name "Notary" + dirName "notary" + nearestCity "London" + advertisedServices = ["corda.notary.validating"] + artemisPort 10002 + webPort 10003 + cordapps = [] + } + node { + name "BankOfCorda" + dirName "node-bank-of-corda" + nearestCity "London" + advertisedServices = [] + artemisPort 10004 + webPort 10005 + cordapps = [] +// TODO: task needs to parse this item when generating node.conf +// rpcUsers : [ +// { user=user1, password=test, permissions=[ StartFlow.net.corda.bank.flow.IssuerFlow$IssuanceRequester ] } +// ] + } + node { + name "BigCorporation" + dirName "node-big-corp" + nearestCity "New York" + advertisedServices = [] + artemisPort 10006 + webPort 10007 + cordapps = [] + } +} + +task integrationTest(type: Test, dependsOn: []) { + testClassesDir = sourceSets.integrationTest.output.classesDir + classpath = sourceSets.integrationTest.runtimeClasspath +} + +idea { + module { + downloadJavadoc = true // defaults to false + downloadSources = true + } +} + +publishing { + publications { + jarAndSources(MavenPublication) { + from components.java + artifactId 'bankofcorda' + + artifact sourceJar + artifact javadocJar + } + } +} + +task runIssuer(type: JavaExec) { + classpath = sourceSets.main.runtimeClasspath + main = 'net.corda.bank.BankOfCordaDriverKt' + args '--role' + args 'ISSUER' +} + +task runRPCCashIssue(type: JavaExec) { + classpath = sourceSets.main.runtimeClasspath + main = 'net.corda.bank.BankOfCordaDriverKt' + args '--role' + args 'ISSUE_CASH_RPC' + args '--quantity' + args 20000 + args '--currency' + args 'USD' +} + +task runWebCashIssue(type: JavaExec) { + classpath = sourceSets.main.runtimeClasspath + main = 'net.corda.bank.BankOfCordaDriverKt' + args '--role' + args 'ISSUE_CASH_WEB' + args '--quantity' + args 30000 + args '--currency' + args 'GBP' +} \ No newline at end of file diff --git a/samples/bank-of-corda-demo/gradle.properties b/samples/bank-of-corda-demo/gradle.properties new file mode 100644 index 0000000000..97464c746e --- /dev/null +++ b/samples/bank-of-corda-demo/gradle.properties @@ -0,0 +1,2 @@ +name = BankOfCorda +kotlin.incremental=false \ No newline at end of file diff --git a/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaHttpAPITest.kt b/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaHttpAPITest.kt new file mode 100644 index 0000000000..035d80b26f --- /dev/null +++ b/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaHttpAPITest.kt @@ -0,0 +1,20 @@ +package net.corda.bank + +import net.corda.bank.api.BankOfCordaClientApi +import net.corda.bank.api.BankOfCordaWebApi.IssueRequestParams +import net.corda.core.node.services.ServiceInfo +import net.corda.node.driver.driver +import net.corda.node.services.transactions.SimpleNotaryService +import net.corda.testing.getHostAndPort +import org.junit.Test + +class BankOfCordaHttpAPITest { + @Test fun `test issuer flow via Http`() { + driver(dsl = { + val nodeBankOfCorda = startNode("BankOfCorda", setOf(ServiceInfo(SimpleNotaryService.type))).get() + val nodeBankOfCordaApiAddr = nodeBankOfCorda.config.getHostAndPort("webAddress") + startNode("BigCorporation").get() + assert(BankOfCordaClientApi(nodeBankOfCordaApiAddr).requestWebIssue(IssueRequestParams(1000, "USD", "BigCorporation", "1", "BankOfCorda"))) + }, isDebug = true) + } +} \ No newline at end of file diff --git a/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaRPCClientTest.kt b/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaRPCClientTest.kt new file mode 100644 index 0000000000..26eff429f7 --- /dev/null +++ b/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaRPCClientTest.kt @@ -0,0 +1,86 @@ +package net.corda.bank + +import net.corda.bank.api.BOC_ISSUER_PARTY_REF +import net.corda.bank.flow.IssuerFlow.IssuanceRequester +import net.corda.core.contracts.DOLLARS +import net.corda.core.messaging.startFlow +import net.corda.core.node.services.ServiceInfo +import net.corda.core.transactions.SignedTransaction +import net.corda.node.driver.driver +import net.corda.node.services.User +import net.corda.node.services.config.configureTestSSL +import net.corda.node.services.messaging.CordaRPCClient +import net.corda.node.services.startFlowPermission +import net.corda.node.services.transactions.SimpleNotaryService +import net.corda.testing.expect +import net.corda.testing.expectEvents +import net.corda.testing.getHostAndPort +import net.corda.testing.sequence +import org.junit.Test +import kotlin.test.assertTrue + +class BankOfCordaRPCClientTest { + + @Test fun `test issuer flow via RPC`() { + driver(dsl = { + val user = User("user1", "test", permissions = setOf(startFlowPermission())) + val nodeBankOfCorda = startNode("BankOfCorda", setOf(ServiceInfo(SimpleNotaryService.type)), arrayListOf(user)).get() + val nodeBankOfCordaApiAddr = nodeBankOfCorda.config.getHostAndPort("artemisAddress") + val bankOfCordaParty = nodeBankOfCorda.nodeInfo.legalIdentity + val nodeBigCorporation = startNode("BigCorporation", rpcUsers = arrayListOf(user)).get() + val bigCorporationParty = nodeBigCorporation.nodeInfo.legalIdentity + + // Bank of Corda RPC Client + val bocClient = CordaRPCClient(nodeBankOfCordaApiAddr, configureTestSSL()) + bocClient.start("user1","test") + val bocProxy = bocClient.proxy() + + // Big Corporation RPC Client + val bigCorpClient = CordaRPCClient(nodeBankOfCordaApiAddr, configureTestSSL()) + bigCorpClient.start("user1","test") + val bigCorpProxy = bigCorpClient.proxy() + + // Register for Bank of Corda Vault updates + val vaultUpdatesBoc = bocProxy.vaultAndUpdates().second + + // Register for Big Corporation Vault updates + val vaultUpdatesBigCorp = bigCorpProxy.vaultAndUpdates().second + + // Kick-off actual Issuer Flow + val result = bocProxy.startFlow(::IssuanceRequester, 1000.DOLLARS, bigCorporationParty, BOC_ISSUER_PARTY_REF, bankOfCordaParty).returnValue.toBlocking().first() + assertTrue { result is SignedTransaction } + + // Check Bank of Corda Vault Updates + vaultUpdatesBoc.expectEvents { + sequence( + // ISSUE + expect { update -> + require(update.consumed.size == 0) { update.consumed.size } + require(update.produced.size == 1) { update.produced.size } + }, + // MOVE + expect { update -> + require(update.consumed.size == 1) { update.consumed.size } + require(update.produced.size == 0) { update.produced.size } + } + ) + } + + // Check Big Corporation Vault Updates + vaultUpdatesBigCorp.expectEvents { + sequence( + // ISSUE + expect { update -> + require(update.consumed.size == 0) { update.consumed.size } + require(update.produced.size == 1) { update.produced.size } + }, + // MOVE + expect { update -> + require(update.consumed.size == 1) { update.consumed.size } + require(update.produced.size == 0) { update.produced.size } + } + ) + } + }, isDebug = true) + } +} \ No newline at end of file diff --git a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/BankOfCordaDriver.kt b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/BankOfCordaDriver.kt new file mode 100644 index 0000000000..816a557708 --- /dev/null +++ b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/BankOfCordaDriver.kt @@ -0,0 +1,93 @@ +package net.corda.bank + +import com.google.common.net.HostAndPort +import joptsimple.OptionParser +import net.corda.bank.api.BankOfCordaClientApi +import net.corda.bank.api.BankOfCordaWebApi.IssueRequestParams +import net.corda.bank.flow.IssuerFlow +import net.corda.core.node.services.ServiceInfo +import net.corda.core.transactions.SignedTransaction +import net.corda.node.driver.driver +import net.corda.node.services.User +import net.corda.node.services.startFlowPermission +import net.corda.node.services.transactions.SimpleNotaryService +import kotlin.system.exitProcess + +/** + * This entry point allows for command line running of the Bank of Corda functions on nodes started by BankOfCordaDriver.kt. + */ +fun main(args: Array) { + BankOfCordaDriver().main(args) +} + +private class BankOfCordaDriver { + enum class Role { + ISSUE_CASH_RPC, + ISSUE_CASH_WEB, + ISSUER + } + + fun main(args: Array) { + val parser = OptionParser() + val roleArg = parser.accepts("role").withRequiredArg().ofType(Role::class.java).describedAs("[ISSUER|ISSUE_CASH_RPC|ISSUE_CASH_WEB]") + val quantity = parser.accepts("quantity").withOptionalArg().ofType(Long::class.java) + val currency = parser.accepts("currency").withOptionalArg().ofType(String::class.java).describedAs("[GBP|USD|CHF|EUR]") + val options = try { + parser.parse(*args) + } catch (e: Exception) { + println(e.message) + printHelp(parser) + exitProcess(1) + } + + // What happens next depends on the role. + // The ISSUER will launch a Bank of Corda node + // The ISSUE_CASH will request some Cash from the ISSUER on behalf of Big Corporation node + val role = options.valueOf(roleArg)!! + if (role == Role.ISSUER) { + driver(dsl = { + val user = User("user1", "test", permissions = setOf(startFlowPermission())) + startNode("Notary", setOf(ServiceInfo(SimpleNotaryService.type))) + startNode("BankOfCorda", rpcUsers = listOf(user)) + startNode("BigCorporation") + waitForAllNodesToFinish() + }, isDebug = true) + } + else { + try { + val requestParams = IssueRequestParams(options.valueOf(quantity), options.valueOf(currency), "BigCorporation", "1", "BankOfCorda") + when (role) { + Role.ISSUE_CASH_RPC -> { + println("Requesting Cash via RPC ...") + val result = BankOfCordaClientApi(HostAndPort.fromString("localhost:10004")).requestRPCIssue(requestParams) + if (result is SignedTransaction) + println("Success!! You transaction receipt is ${result.tx.id}") + } + Role.ISSUE_CASH_WEB -> { + println("Requesting Cash via Web ...") + val result = BankOfCordaClientApi(HostAndPort.fromString("localhost:10005")).requestWebIssue(requestParams) + if (result) + println("Successfully processed Cash Issue request") + } + Role.ISSUER -> {} + } + } + catch (e: Exception) { + printHelp(parser) + } + } + } + + fun printHelp(parser: OptionParser) { + println(""" + Usage: bank-of-corda --role ISSUER + bank-of-corda --role (ISSUE_CASH_RPC|ISSUE_CASH_WEB) --quantity --currency + + Please refer to the documentation in docs/build/index.html for more info. + + """.trimIndent()) + parser.printHelpOn(System.out) + } +} + + diff --git a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaClientApi.kt b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaClientApi.kt new file mode 100644 index 0000000000..89e6d62ab9 --- /dev/null +++ b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaClientApi.kt @@ -0,0 +1,48 @@ +package net.corda.bank.api + +import com.google.common.net.HostAndPort +import net.corda.bank.api.BankOfCordaWebApi.IssueRequestParams +import net.corda.bank.flow.IssuerFlow.IssuanceRequester +import net.corda.node.services.messaging.CordaRPCClient +import net.corda.core.contracts.Amount +import net.corda.core.contracts.currency +import net.corda.core.messaging.startFlow +import net.corda.core.serialization.OpaqueBytes +import net.corda.core.transactions.SignedTransaction +import net.corda.node.services.config.configureTestSSL +import net.corda.testing.http.HttpApi + +/** + * Interface for communicating with Bank of Corda node + */ +class BankOfCordaClientApi(val hostAndPort: HostAndPort) { + private val apiRoot = "api/bank" + /** + * HTTP API + */ + // TODO: security controls required + fun requestWebIssue(params: IssueRequestParams): Boolean { + val api = HttpApi.fromHostAndPort(hostAndPort, apiRoot) + return api.postJson("issue-asset-request", params) + } + /** + * RPC API + */ + fun requestRPCIssue(params: IssueRequestParams): SignedTransaction { + val client = CordaRPCClient(hostAndPort, configureTestSSL()) + // TODO: privileged security controls required + client.start("user1","test") + val proxy = client.proxy() + + // Resolve parties via RPC + val issueToParty = proxy.partyFromName(params.issueToPartyName) + ?: throw Exception("Unable to locate ${params.issueToPartyName} in Network Map Service") + val issuerBankParty = proxy.partyFromName(params.issuerBankName) + ?: throw Exception("Unable to locate ${params.issuerBankName} in Network Map Service") + + val amount = Amount(params.amount, currency(params.currency)) + val issuerToPartyRef = OpaqueBytes.of(params.issueToPartyRefAsString.toByte()) + + return proxy.startFlow(::IssuanceRequester, amount, issueToParty, issuerToPartyRef, issuerBankParty).returnValue.toBlocking().first() + } +} diff --git a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaWebApi.kt b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaWebApi.kt new file mode 100644 index 0000000000..edaea3373d --- /dev/null +++ b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaWebApi.kt @@ -0,0 +1,57 @@ +package net.corda.bank.api + +import net.corda.bank.flow.IssuerFlow.IssuanceRequester +import net.corda.core.contracts.Amount +import net.corda.core.contracts.currency +import net.corda.core.messaging.CordaRPCOps +import net.corda.core.messaging.startFlow +import net.corda.core.serialization.OpaqueBytes +import net.corda.core.transactions.SignedTransaction +import net.corda.core.utilities.loggerFor +import java.time.LocalDateTime +import javax.ws.rs.* +import javax.ws.rs.core.MediaType +import javax.ws.rs.core.Response + +// API is accessible from /api/bank. All paths specified below are relative to it. +@Path("bank") +class BankOfCordaWebApi(val rpc: CordaRPCOps) { + data class IssueRequestParams(val amount: Long, val currency: String, + val issueToPartyName: String, val issueToPartyRefAsString: String, + val issuerBankName: String) + private companion object { + val logger = loggerFor() + } + @GET + @Path("date") + @Produces(MediaType.APPLICATION_JSON) + fun getCurrentDate(): Any { + return mapOf("date" to LocalDateTime.now().toLocalDate()) + } + /** + * Request asset issuance + */ + @POST + @Path("issue-asset-request") + @Consumes(MediaType.APPLICATION_JSON) + fun issueAssetRequest(params: IssueRequestParams): Response { + // Resolve parties via RPC + val issueToParty = rpc.partyFromName(params.issueToPartyName) + ?: throw Exception("Unable to locate ${params.issueToPartyName} in Network Map Service") + val issuerBankParty = rpc.partyFromName(params.issuerBankName) + ?: throw Exception("Unable to locate ${params.issuerBankName} in Network Map Service") + + val amount = Amount(params.amount, currency(params.currency)) + val issuerToPartyRef = OpaqueBytes.of(params.issueToPartyRefAsString.toByte()) + + // invoke client side of Issuer Flow: IssuanceRequester + // The line below blocks and waits for the future to resolve. + val result = rpc.startFlow(::IssuanceRequester, amount, issueToParty, issuerToPartyRef, issuerBankParty).returnValue.toBlocking().first() + if (result is SignedTransaction) { + logger.info("Issue request completed successfully: ${params}") + return Response.status(Response.Status.CREATED).build() + } else { + return Response.status(Response.Status.BAD_REQUEST).build() + } + } +} \ No newline at end of file diff --git a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/Constants.kt b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/Constants.kt new file mode 100644 index 0000000000..d44d1577da --- /dev/null +++ b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/Constants.kt @@ -0,0 +1,21 @@ +package net.corda.bank.api + +import net.corda.core.crypto.CompositeKey +import net.corda.core.crypto.Party +import net.corda.core.crypto.composite +import net.corda.core.crypto.generateKeyPair +import net.corda.core.serialization.OpaqueBytes +import net.corda.testing.MEGA_CORP +import java.security.KeyPair +import java.security.PublicKey + +val defaultRef = OpaqueBytes.of(1) + +/* + * Bank Of Corda (BOC_ISSUER_PARTY) + */ +val BOC_KEY: KeyPair by lazy { generateKeyPair() } +val BOC_PUBKEY: CompositeKey get() = BOC_KEY.public.composite +val BOC_ISSUER_PARTY: Party get() = Party("BankOfCorda", BOC_PUBKEY) +val BOC_ISSUER_PARTY_AND_REF = BOC_ISSUER_PARTY.ref(defaultRef) +val BOC_ISSUER_PARTY_REF = BOC_ISSUER_PARTY_AND_REF.reference \ No newline at end of file diff --git a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/flow/IssuerFlow.kt b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/flow/IssuerFlow.kt new file mode 100644 index 0000000000..ccfe1b3bb1 --- /dev/null +++ b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/flow/IssuerFlow.kt @@ -0,0 +1,109 @@ +package net.corda.bank.flow + +import co.paralleluniverse.fibers.Suspendable +import net.corda.core.contracts.* +import net.corda.core.crypto.Party +import net.corda.core.crypto.SecureHash +import net.corda.core.flows.FlowLogic +import net.corda.core.node.NodeInfo +import net.corda.core.node.PluginServiceHub +import net.corda.core.serialization.OpaqueBytes +import net.corda.core.transactions.SignedTransaction +import net.corda.core.utilities.ProgressTracker +import net.corda.flows.CashCommand +import net.corda.flows.CashFlow +import net.corda.flows.CashFlowResult +import java.util.* + +/** + * This flow enables a client to request issuance of some [FungibleAsset] from a + * server acting as an issuer (see [Issued]) of FungibleAssets. + * + * It is not intended for production usage, but rather for experimentation and testing purposes where it may be + * useful for creation of fake assets. + */ +object IssuerFlow { + data class IssuanceRequestState(val amount: Amount, val issueToParty: Party, val issuerPartyRef: OpaqueBytes) + + /** + * IssuanceRequester should be used by a client to ask a remote note to issue some [FungibleAsset] with the given details. + * Returns the transaction created by the Issuer to move the cash to the Requester. + */ + class IssuanceRequester(val amount: Amount, val issueToParty: Party, val issueToPartyRef: OpaqueBytes, + val issuerBankParty: Party): FlowLogic() { + @Suspendable + override fun call(): SignedTransaction { + val issueRequest = IssuanceRequestState(amount, issueToParty, issueToPartyRef) + return sendAndReceive(issuerBankParty, issueRequest).unwrap { it } + } + } + + /** + * Issuer refers to a Node acting as a Bank Issuer of [FungibleAsset], and processes requests from a [IssuanceRequester] client. + * Returns the generated transaction representing the transfer of the [Issued] [FungibleAsset] to the issue requester. + */ + class Issuer(val otherParty: Party): FlowLogic() { + override val progressTracker: ProgressTracker = Issuer.tracker() + companion object { + object AWAITING_REQUEST : ProgressTracker.Step("Awaiting issuance request") + object ISSUING : ProgressTracker.Step("Self issuing asset") + object TRANSFERRING : ProgressTracker.Step("Transferring asset to issuance requester") + object SENDING_CONFIRM : ProgressTracker.Step("Confirming asset issuance to requester") + fun tracker() = ProgressTracker(AWAITING_REQUEST, ISSUING, TRANSFERRING, SENDING_CONFIRM) + } + + @Suspendable + override fun call(): SignedTransaction { + progressTracker.currentStep = AWAITING_REQUEST + val issueRequest = receive(otherParty).unwrap { it } + // validate request inputs (for example, lets restrict the types of currency that can be issued) + require(listOf(USD, GBP, EUR, CHF).contains(issueRequest.amount.token)) { + logger.error("Currency must be one of USD, GBP, EUR, CHF") + } + // TODO: parse request to determine Asset to issue + val txn = issueCashTo(issueRequest.amount, issueRequest.issueToParty, issueRequest.issuerPartyRef) + progressTracker.currentStep = SENDING_CONFIRM + send(otherParty, txn) + return txn + } + + @Suspendable + private fun issueCashTo(amount: Amount, + issueTo: Party, issuerPartyRef: OpaqueBytes): SignedTransaction { + val notaryParty = serviceHub.networkMapCache.notaryNodes[0].notaryIdentity + // invoke Cash subflow to issue Asset + progressTracker.currentStep = ISSUING + val bankOfCordaParty = serviceHub.myInfo.legalIdentity + val issueCashFlow = CashFlow(CashCommand.IssueCash(amount, issuerPartyRef, bankOfCordaParty, notaryParty)) + val resultIssue = subFlow(issueCashFlow) + // NOTE: issueCashFlow performs a Broadcast (which stores a local copy of the txn to the ledger) + if (resultIssue is CashFlowResult.Failed) { + logger.error("Problem issuing cash: ${resultIssue.message}") + throw Exception(resultIssue.message) + } + // now invoke Cash subflow to Move issued assetType to issue requester + progressTracker.currentStep = TRANSFERRING + val moveCashFlow = CashFlow(CashCommand.PayCash(amount.issuedBy(bankOfCordaParty.ref(issuerPartyRef)), issueTo)) + val resultMove = subFlow(moveCashFlow) + // NOTE: CashFlow PayCash calls FinalityFlow which performs a Broadcast (which stores a local copy of the txn to the ledger) + if (resultMove is CashFlowResult.Failed) { + logger.error("Problem transferring cash: ${resultMove.message}") + throw Exception(resultMove.message) + } + val txn = (resultMove as CashFlowResult.Success).transaction + txn?.let { + return txn + } + // NOTE: CashFlowResult.Success should always return a signedTransaction + throw Exception("Missing CashFlow transaction [${(resultMove)}]") + } + + class Service(services: PluginServiceHub) { + init { + services.registerFlowInitiator(IssuanceRequester::class) { + Issuer(it) + } + } + } + } +} \ No newline at end of file diff --git a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/plugin/BankOfCordaPlugin.kt b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/plugin/BankOfCordaPlugin.kt new file mode 100644 index 0000000000..5c50b7d0a4 --- /dev/null +++ b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/plugin/BankOfCordaPlugin.kt @@ -0,0 +1,19 @@ +package net.corda.bank.plugin + +import net.corda.bank.api.BankOfCordaWebApi +import net.corda.bank.flow.IssuerFlow +import net.corda.core.contracts.Amount +import net.corda.core.crypto.Party +import net.corda.core.node.CordaPluginRegistry +import net.corda.core.serialization.OpaqueBytes +import java.util.function.Function + +class BankOfCordaPlugin : CordaPluginRegistry() { + // A list of classes that expose web APIs. + override val webApis = listOf(Function(::BankOfCordaWebApi)) + // A list of flow that are required for this cordapp + override val requiredFlows: Map> = + mapOf(IssuerFlow.IssuanceRequester::class.java.name to setOf(Amount::class.java.name, Party::class.java.name, OpaqueBytes::class.java.name, Party::class.java.name) + ) + override val servicePlugins = listOf(Function(IssuerFlow.Issuer::Service)) +} diff --git a/samples/bank-of-corda-demo/src/main/resources/META-INF/services/net.corda.core.node.CordaPluginRegistry b/samples/bank-of-corda-demo/src/main/resources/META-INF/services/net.corda.core.node.CordaPluginRegistry new file mode 100644 index 0000000000..b4dddc2d14 --- /dev/null +++ b/samples/bank-of-corda-demo/src/main/resources/META-INF/services/net.corda.core.node.CordaPluginRegistry @@ -0,0 +1,2 @@ +# Register a ServiceLoader service extending from net.corda.node.CordaPluginRegistry +net.corda.bank.plugin.BankOfCordaPlugin diff --git a/samples/bank-of-corda-demo/src/test/kotlin/net/corda/bank/flow/IssuerFlowTest.kt b/samples/bank-of-corda-demo/src/test/kotlin/net/corda/bank/flow/IssuerFlowTest.kt new file mode 100644 index 0000000000..5e03a4ea4e --- /dev/null +++ b/samples/bank-of-corda-demo/src/test/kotlin/net/corda/bank/flow/IssuerFlowTest.kt @@ -0,0 +1,74 @@ +package net.corda.bank.flow + +import com.google.common.util.concurrent.ListenableFuture +import net.corda.bank.api.BOC_ISSUER_PARTY +import net.corda.bank.api.BOC_KEY +import net.corda.bank.flow.IssuerFlow.IssuanceRequester +import net.corda.core.contracts.Amount +import net.corda.core.contracts.DOLLARS +import net.corda.core.contracts.PartyAndReference +import net.corda.core.contracts.currency +import net.corda.core.flows.FlowStateMachine +import net.corda.core.map +import net.corda.core.serialization.OpaqueBytes +import net.corda.core.transactions.SignedTransaction +import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.core.utilities.DUMMY_NOTARY_KEY +import net.corda.testing.MEGA_CORP +import net.corda.testing.MEGA_CORP_KEY +import net.corda.testing.initiateSingleShotFlow +import net.corda.testing.ledger +import net.corda.testing.node.MockNetwork +import org.junit.Test +import java.util.* +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith + +class IssuerFlowTest { + lateinit var net: MockNetwork + lateinit var notaryNode: MockNetwork.MockNode + lateinit var bankOfCordaNode: MockNetwork.MockNode + lateinit var bankClientNode: MockNetwork.MockNode + + @Test + fun `test issuer flow`() { + net = MockNetwork(false, true) + ledger { + notaryNode = net.createNotaryNode(DUMMY_NOTARY.name, DUMMY_NOTARY_KEY) + bankOfCordaNode = net.createPartyNode(notaryNode.info.address, BOC_ISSUER_PARTY.name, BOC_KEY) + bankClientNode = net.createPartyNode(notaryNode.info.address, MEGA_CORP.name, MEGA_CORP_KEY) + + // using default IssueTo Party Reference + val issueToPartyAndRef = MEGA_CORP.ref(OpaqueBytes.of(123)) + val (issuer, issuerResult) = runIssuerAndIssueRequester(1000000.DOLLARS, issueToPartyAndRef) + assertEquals(issuerResult.get(), issuer.get().resultFuture.get()) + + // try to issue an amount of a restricted currency + assertFailsWith { + runIssuerAndIssueRequester(Amount(100000L, currency("BRL")), issueToPartyAndRef).issueRequestResult.get() + } + + bankOfCordaNode.stop() + bankClientNode.stop() + + bankOfCordaNode.manuallyCloseDB() + bankClientNode.manuallyCloseDB() + } + } + + private fun runIssuerAndIssueRequester(amount: Amount, issueToPartyAndRef: PartyAndReference) : RunResult { + val issuerFuture = bankOfCordaNode.initiateSingleShotFlow(IssuerFlow.IssuanceRequester::class) { + otherParty -> IssuerFlow.Issuer(issueToPartyAndRef.party) + }.map { it.fsm } + + val issueRequest = IssuanceRequester(amount, issueToPartyAndRef.party, issueToPartyAndRef.reference, bankOfCordaNode.info.legalIdentity) + val issueRequestResultFuture = bankClientNode.smm.add(issueRequest).resultFuture + + return RunResult(issuerFuture, issueRequestResultFuture) + } + + private data class RunResult( + val issuer: ListenableFuture>, + val issueRequestResult: ListenableFuture + ) +} \ No newline at end of file diff --git a/samples/irs-demo/build.gradle b/samples/irs-demo/build.gradle index ff76afd800..85d30012fb 100644 --- a/samples/irs-demo/build.gradle +++ b/samples/irs-demo/build.gradle @@ -48,7 +48,7 @@ dependencies { testCompile group: 'junit', name: 'junit', version: '4.11' // Corda integration dependencies - runtime project(path: ":node", configuration: 'runtimeArtifacts') + runtime project(path: ":node:capsule", configuration: 'runtimeArtifacts') compile project(':core') compile project(':finance') compile project(':test-utils') diff --git a/samples/network-visualiser/build.gradle b/samples/network-visualiser/build.gradle index f070c6d410..d3db156d44 100644 --- a/samples/network-visualiser/build.gradle +++ b/samples/network-visualiser/build.gradle @@ -21,7 +21,7 @@ dependencies { testCompile group: 'junit', name: 'junit', version: '4.11' // Corda integration dependencies - runtime project(path: ":node", configuration: 'runtimeArtifacts') + runtime project(path: ":node:capsule", configuration: 'runtimeArtifacts') compile project(':core') compile project(':finance') testCompile project(':test-utils') diff --git a/samples/raft-notary-demo/build.gradle b/samples/raft-notary-demo/build.gradle index 9493636407..c0ed340679 100644 --- a/samples/raft-notary-demo/build.gradle +++ b/samples/raft-notary-demo/build.gradle @@ -45,7 +45,7 @@ dependencies { testCompile group: 'junit', name: 'junit', version: '4.11' // Corda integration dependencies - runtime project(path: ":node", configuration: 'runtimeArtifacts') + runtime project(path: ":node:capsule", configuration: 'runtimeArtifacts') compile project(':core') compile project(':client') compile project(':node') diff --git a/samples/simm-valuation-demo/build.gradle b/samples/simm-valuation-demo/build.gradle index 9a1a4f829e..2463df37a0 100644 --- a/samples/simm-valuation-demo/build.gradle +++ b/samples/simm-valuation-demo/build.gradle @@ -41,7 +41,7 @@ dependencies { testCompile group: 'junit', name: 'junit', version: '4.11' // Corda integration dependencies - runtime project(path: ":node", configuration: 'runtimeArtifacts') + runtime project(path: ":node:capsule", configuration: 'runtimeArtifacts') compile project(':core') compile project(':node') compile project(':finance') diff --git a/samples/trader-demo/build.gradle b/samples/trader-demo/build.gradle index 9fa03f942e..229fbc3787 100644 --- a/samples/trader-demo/build.gradle +++ b/samples/trader-demo/build.gradle @@ -45,7 +45,7 @@ dependencies { testCompile group: 'junit', name: 'junit', version: '4.11' // Corda integration dependencies - runtime project(path: ":node", configuration: 'runtimeArtifacts') + runtime project(path: ":node:capsule", configuration: 'runtimeArtifacts') compile project(':core') compile project(':finance') compile project(':test-utils') diff --git a/settings.gradle b/settings.gradle index e45532de1d..50d2165602 100644 --- a/settings.gradle +++ b/settings.gradle @@ -5,6 +5,7 @@ include 'finance' include 'finance:isolated' include 'core' include 'node' +include 'node:capsule' include 'client' include 'netpermission' include 'experimental' @@ -18,4 +19,5 @@ include 'samples:trader-demo' include 'samples:irs-demo' include 'samples:network-visualiser' include 'samples:simm-valuation-demo' -include 'samples:raft-notary-demo' \ No newline at end of file +include 'samples:raft-notary-demo' +include 'samples:bank-of-corda-demo' \ No newline at end of file diff --git a/test-utils/build.gradle b/test-utils/build.gradle index 9736593c7e..b220b3ce4b 100644 --- a/test-utils/build.gradle +++ b/test-utils/build.gradle @@ -2,6 +2,8 @@ apply plugin: 'kotlin' apply plugin: 'net.corda.plugins.quasar-utils' apply plugin: 'net.corda.plugins.publish-utils' +description 'Testing utilities for Corda' + repositories { mavenLocal() mavenCentral() @@ -16,7 +18,6 @@ repositories { //noinspection GroovyAssignabilityCheck configurations { - // we don't want isolated.jar in classPath, since we want to test jar being dynamically loaded as an attachment runtime.exclude module: 'isolated' } @@ -47,16 +48,4 @@ dependencies { // OkHTTP: Simple HTTP library. compile 'com.squareup.okhttp3:okhttp:3.3.1' -} - -publishing { - publications { - testutils(MavenPublication) { - from components.java - artifactId 'test-utils' - - artifact sourceJar - artifact javadocJar - } - } -} +} \ No newline at end of file