Merge remote-tracking branch 'github/master'

This commit is contained in:
Shams Asari 2016-12-09 17:41:17 +00:00
commit 3c507a9c76
80 changed files with 1798 additions and 364 deletions

View File

@ -0,0 +1,15 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Bank of Corda Demo: Run Issuer" type="JetRunConfigurationType" factoryName="Kotlin" singleton="true">
<extension name="coverage" enabled="false" merge="false" sample_coverage="true" runner="idea" />
<option name="MAIN_CLASS_NAME" value="net.corda.bank.BankOfCordaDriverKt" />
<option name="VM_PARAMETERS" value="-ea -javaagent:lib/quasar.jar " />
<option name="PROGRAM_PARAMETERS" value="--role ISSUER" />
<option name="WORKING_DIRECTORY" value="" />
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="true" />
<option name="ALTERNATIVE_JRE_PATH" value="1.8" />
<option name="PASS_PARENT_ENVS" value="true" />
<module name="bank-of-corda-demo_main" />
<envs />
<method />
</configuration>
</component>

View File

@ -0,0 +1,15 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Bank of Corda Demo: Run RPC Cash Issue" type="JetRunConfigurationType" factoryName="Kotlin" singleton="true">
<extension name="coverage" enabled="false" merge="false" sample_coverage="true" runner="idea" />
<option name="MAIN_CLASS_NAME" value="net.corda.bank.BankOfCordaDriverKt" />
<option name="VM_PARAMETERS" value="-ea -javaagent:lib/quasar.jar " />
<option name="PROGRAM_PARAMETERS" value="--role ISSUE_CASH_RPC --quantity 12345 --currency GBP" />
<option name="WORKING_DIRECTORY" value="" />
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
<option name="ALTERNATIVE_JRE_PATH" />
<option name="PASS_PARENT_ENVS" value="true" />
<module name="bank-of-corda-demo_main" />
<envs />
<method />
</configuration>
</component>

View File

@ -0,0 +1,15 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Bank of Corda Demo: Run Web Cash Issue" type="JetRunConfigurationType" factoryName="Kotlin" singleton="true">
<extension name="coverage" enabled="false" merge="false" sample_coverage="true" runner="idea" />
<option name="MAIN_CLASS_NAME" value="net.corda.bank.BankOfCordaDriverKt" />
<option name="VM_PARAMETERS" value="-ea -javaagent:lib/quasar.jar " />
<option name="PROGRAM_PARAMETERS" value="--role ISSUE_CASH_WEB --quantity 67890 --currency EUR" />
<option name="WORKING_DIRECTORY" value="" />
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
<option name="ALTERNATIVE_JRE_PATH" />
<option name="PASS_PARENT_ENVS" value="true" />
<module name="bank-of-corda-demo_main" />
<envs />
<method />
</configuration>
</component>

View File

@ -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'
}
}

View File

@ -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
}
}

View File

@ -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
}
}
}

View File

@ -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 <T> Observable<T>.bufferUntilSubscribed(): Observable<T> {
return subject.doOnUnsubscribe { subscription.unsubscribe() }
}
/**
* Copy an [Observer] to multiple other [Observer]s.
*/
fun <T> Observer<T>.tee(vararg teeTo: Observer<T>): Observer<T> {
val subject = PublishSubject.create<T>()
subject.subscribe(this)
teeTo.forEach { subject.subscribe(it) }
return subject
}
/** Allows summing big decimals that are in iterable collections */
fun Iterable<BigDecimal>.sum(): BigDecimal = fold(BigDecimal.ZERO) { a, b -> a + b }

View File

@ -61,6 +61,8 @@ inline fun <R> 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 <reified T : CommandData> Collection<AuthenticatedObject<CommandData>>.select(signer: CompositeKey? = null,
party: Party? = null) =
@ -69,6 +71,8 @@ inline fun <reified T : CommandData> Collection<AuthenticatedObject<CommandData>
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 <reified T : CommandData> Collection<AuthenticatedObject<CommandData>>.select(signers: Collection<CompositeKey>?,
parties: Collection<Party>?) =

View File

@ -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
}

View File

@ -54,6 +54,8 @@ abstract class FlowLogic<out T> {
return sendAndReceive(otherParty, payload, T::class.java)
}
// TODO: Move the receiveType param to first position for readability
@Suspendable
fun <T : Any> sendAndReceive(otherParty: Party, payload: Any, receiveType: Class<T>): UntrustworthyData<T> {
return fsm.sendAndReceive(otherParty, payload, receiveType, sessionFlow)
@ -61,6 +63,8 @@ abstract class FlowLogic<out T> {
inline fun <reified T : Any> receive(otherParty: Party): UntrustworthyData<T> = receive(otherParty, T::class.java)
// TODO: Move the receiveType param to first position for readability
@Suspendable
fun <T : Any> receive(otherParty: Party, receiveType: Class<T>): UntrustworthyData<T> {
return fsm.receive(otherParty, receiveType, sessionFlow)

View File

@ -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.

View File

@ -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 */

View File

@ -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<Vault.Update>
/**
* 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<Vault.Update>

View File

@ -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)

View File

@ -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"

View File

@ -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<Map<Currency, Amount<Currency>>>()
val done3 = SettableFuture.create<Map<Currency, Amount<Currency>>>()
val done2 = SettableFuture.create<Unit>()
val done3 = SettableFuture.create<Unit>()
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)

View File

@ -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<Unit>()

View File

@ -107,6 +107,7 @@ Read on to learn:
release-notes
codestyle
building-the-docs
publishing-corda
.. toctree::
:maxdepth: 2

View File

@ -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

View File

@ -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
---------------------------------------------------------------

View File

@ -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 name>/node.conf`` files for your nodes. For more information on
You may also wish to edit the ``/kotlin/build/nodes/<node name>/node.conf`` files for your nodes. For more information on
doing this, see the :doc:`Corda configuration file <corda-configuration-file>` page.
Once you have made some changes to your CorDapp you can redeploy it with the following command:

View File

@ -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
}
}
}
}

View File

@ -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

View File

@ -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'
}
}

View File

@ -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'
}
}
}
}
}
}
}

View File

@ -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 = <your bintray username>
key = <your bintray 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 = <your bintray GPG key passphrase> // 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
}
}

View File

@ -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')

View File

@ -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
}

View File

@ -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<Project> {
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")
}
}

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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()
}

View File

@ -1,4 +1,4 @@
rootProject.name = 'corda-gradle-plugins'
include 'quasar-utils'
include 'publish-utils'
include 'quasar-utils'
include 'cordformation'

View File

@ -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 ""
}
}
}
}

83
node/capsule/build.gradle Normal file
View File

@ -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
}

View File

@ -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 <A> driver(
driverDirectory: Path = Paths.get("build", getTimestampAsDirectoryName()),
portAllocation: PortAllocation = PortAllocation.Incremental(10000),

View File

@ -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)

View File

@ -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

View File

@ -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) }
}

View File

@ -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
)
)
}

View File

@ -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 {

View File

@ -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")

View File

@ -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<NodeInfo> get() = registeredNodes.map { it.value }
override val networkMapNodes: List<NodeInfo> get() = getNodesWithService(NetworkMapService.type)
private val _changed = PublishSubject.create<MapChange>()
override val changed: Observable<MapChange> = _changed
override val changed: Observable<MapChange> get() = _changed
private val changePublisher: rx.Observer<MapChange> get() = _changed.bufferUntilDatabaseCommit()
private val _registrationFuture = SettableFuture.create<Unit>()
override val mapServiceRegistered: ListenableFuture<Unit> 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))
}
}

View File

@ -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))
}
}

View File

@ -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

View File

@ -35,7 +35,7 @@ class HibernateObserver(services: ServiceHubInternal) {
val sessionFactories = ConcurrentHashMap<MappedSchema, SessionFactory>()
init {
services.vaultService.updates.subscribe { persist(it.produced) }
services.vaultService.rawUpdates.subscribe { persist(it.produced) }
}
private fun sessionFactoryForSchema(schema: MappedSchema): SessionFactory {

View File

@ -86,15 +86,16 @@ class FlowStateMachineImpl<R>(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<R>(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) {

View File

@ -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<Change>()
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 <T> add(logic: FlowLogic<T>): FlowStateMachine<T> {
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 {

View File

@ -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
}
}
}

View File

@ -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<Vault.Update>()
val _rawUpdatesPublisher = PublishSubject.create<Vault.Update>()
// For use during publishing only.
val updatesPublisher: rx.Observer<Vault.Update> get() = _updatesPublisher.bufferUntilDatabaseCommit().tee(_rawUpdatesPublisher)
fun allUnconsumedStates(): Iterable<StateAndRef<ContractState>> {
// 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<Vault.Update>
get() = mutex.locked { _rawUpdatesPublisher }
override val updates: Observable<Vault.Update>
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

View File

@ -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 <T> 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<UUID>()
private val threadLocalDb = ThreadLocal<Database>()
private val threadLocalTx = ThreadLocal<Transaction>()
private val databaseToInstance = ConcurrentHashMap<Database, StrandLocalTransactionManager>()
fun setThreadLocalTx(tx: Transaction?): Pair<Database?, Transaction?> {
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<Boundary> get() = manager._transactionBoundaries
}
data class Boundary(val txId: UUID)
private val _transactionBoundaries = PublishSubject.create<Boundary>()
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<Transaction>) : TransactionInterface {
private class StrandLocalTransaction(override val db: Database, isolation: Int, val threadLocal: ThreadLocal<Transaction>, val transactionBoundaries: PublishSubject<Boundary>) : 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 <T : Any> rx.Observer<T>.bufferUntilDatabaseCommit(): rx.Observer<T> {
val currentTxId = StrandLocalTransactionManager.transactionId
val databaseTxBoundary: Observable<StrandLocalTransactionManager.Boundary> = StrandLocalTransactionManager.transactionBoundaries.filter { it.txId == currentTxId }.first()
val subject = UnicastSubject.create<T>()
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<String>, val owningKey: Column<CompositeKey>)
data class StateRefColumns(val txId: Column<SecureHash>, val index: Column<Int>)
data class TxnNoteColumns(val txId: Column<SecureHash>, val note: Column<String>)

View File

@ -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<Certificate>()
while (it.nextEntry != null) {
certificates.add(CertificateFactory.getInstance("X.509").generateCertificate(it))
}
certificates.toTypedArray()
HTTP_OK -> ZipInputStream(conn.inputStream).use {
val certificates = ArrayList<Certificate>()
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)
}

View File

@ -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

View File

@ -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<IllegalStateException> {
nodeA.netMapCache.getNodeByCompositeKey(keyPair.public.composite)

View File

@ -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

View File

@ -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<Int>()
val observable: Observable<Int> = subject
val firstEvent = SettableFuture.create<Pair<Int, Boolean>>()
val secondEvent = SettableFuture.create<Pair<Int, Boolean>>()
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<Int>()
val observable: Observable<Int> = subject
val firstEvent = SettableFuture.create<Pair<Int, Boolean>>()
val secondEvent = SettableFuture.create<Pair<Int, Boolean>>()
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<Int>()
val subject2 = PublishSubject.create<Int>()
val subject3 = PublishSubject.create<Int>()
val event1 = SettableFuture.create<Int>()
val event2 = SettableFuture.create<Int>()
val event3 = SettableFuture.create<Int>()
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<Int>()
val teed = PublishSubject.create<Int>()
val observable: Observable<Int> = subject
val firstEvent = SettableFuture.create<Pair<Int, Boolean>>()
val teedEvent = SettableFuture.create<Pair<Int, Boolean>>()
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()
}
}

1
publish.properties Normal file
View File

@ -0,0 +1 @@
gradlePluginsVersion=0.6.2

View File

@ -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.
* **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)

View File

@ -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')

View File

@ -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<IssuerFlow.IssuanceRequester>()))
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/).

View File

@ -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'
}

View File

@ -0,0 +1,2 @@
name = BankOfCorda
kotlin.incremental=false

View File

@ -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)
}
}

View File

@ -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<IssuanceRequester>()))
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)
}
}

View File

@ -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<String>) {
BankOfCordaDriver().main(args)
}
private class BankOfCordaDriver {
enum class Role {
ISSUE_CASH_RPC,
ISSUE_CASH_WEB,
ISSUER
}
fun main(args: Array<String>) {
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<IssuerFlow.IssuanceRequester>()))
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 <quantity> --currency <currency>
Please refer to the documentation in docs/build/index.html for more info.
""".trimIndent())
parser.printHelpOn(System.out)
}
}

View File

@ -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()
}
}

View File

@ -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<BankOfCordaWebApi>()
}
@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()
}
}
}

View File

@ -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

View File

@ -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<Currency>, 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<Currency>, val issueToParty: Party, val issueToPartyRef: OpaqueBytes,
val issuerBankParty: Party): FlowLogic<SignedTransaction>() {
@Suspendable
override fun call(): SignedTransaction {
val issueRequest = IssuanceRequestState(amount, issueToParty, issueToPartyRef)
return sendAndReceive<SignedTransaction>(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<SignedTransaction>() {
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<IssuanceRequestState>(otherParty).unwrap { it }
// validate request inputs (for example, lets restrict the types of currency that can be issued)
require(listOf<Currency>(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<Currency>,
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)
}
}
}
}
}

View File

@ -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<String, Set<String>> =
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))
}

View File

@ -0,0 +1,2 @@
# Register a ServiceLoader service extending from net.corda.node.CordaPluginRegistry
net.corda.bank.plugin.BankOfCordaPlugin

View File

@ -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<Exception> {
runIssuerAndIssueRequester(Amount(100000L, currency("BRL")), issueToPartyAndRef).issueRequestResult.get()
}
bankOfCordaNode.stop()
bankClientNode.stop()
bankOfCordaNode.manuallyCloseDB()
bankClientNode.manuallyCloseDB()
}
}
private fun runIssuerAndIssueRequester(amount: Amount<Currency>, 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<FlowStateMachine<*>>,
val issueRequestResult: ListenableFuture<SignedTransaction>
)
}

View File

@ -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')

View File

@ -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')

View File

@ -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')

View File

@ -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')

View File

@ -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')

View File

@ -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'
include 'samples:raft-notary-demo'
include 'samples:bank-of-corda-demo'

View File

@ -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
}
}
}
}