mirror of
https://github.com/corda/corda.git
synced 2025-01-04 04:04:27 +00:00
Merge remote-tracking branch 'github/master'
This commit is contained in:
commit
3c507a9c76
15
.idea/runConfigurations/Bank_of_Corda_Demo__Run_Issuer.xml
generated
Normal file
15
.idea/runConfigurations/Bank_of_Corda_Demo__Run_Issuer.xml
generated
Normal 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>
|
15
.idea/runConfigurations/Bank_of_Corda_Demo__Run_RPC_Cash_Issue.xml
generated
Normal file
15
.idea/runConfigurations/Bank_of_Corda_Demo__Run_RPC_Cash_Issue.xml
generated
Normal 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>
|
15
.idea/runConfigurations/Bank_of_Corda_Demo__Run_Web_Cash_Issue.xml
generated
Normal file
15
.idea/runConfigurations/Bank_of_Corda_Demo__Run_Web_Cash_Issue.xml
generated
Normal 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>
|
37
build.gradle
37
build.gradle
@ -1,9 +1,12 @@
|
||||
buildscript {
|
||||
// For sharing constants between builds
|
||||
Properties props = new Properties()
|
||||
file("publish.properties").withInputStream { props.load(it) }
|
||||
|
||||
// Our version: bump this on release.
|
||||
ext.corda_version = "0.7-SNAPSHOT"
|
||||
ext.gradle_plugins_version = "0.6.1"
|
||||
|
||||
ext.kotlin_version = '1.0.5'
|
||||
ext.gradle_plugins_version = props.getProperty("gradlePluginsVersion")
|
||||
ext.kotlin_version = '1.0.5-2'
|
||||
ext.quasar_version = '0.7.6'
|
||||
ext.asm_version = '0.5.3'
|
||||
ext.artemis_version = '1.4.0'
|
||||
@ -45,7 +48,7 @@ plugins {
|
||||
apply plugin: 'kotlin'
|
||||
apply plugin: 'project-report'
|
||||
apply plugin: 'com.github.ben-manes.versions'
|
||||
apply plugin: 'maven-publish'
|
||||
apply plugin: 'net.corda.plugins.publish-utils'
|
||||
apply plugin: 'net.corda.plugins.quasar-utils'
|
||||
apply plugin: 'net.corda.plugins.cordformation'
|
||||
|
||||
@ -93,7 +96,7 @@ repositories {
|
||||
dependencies {
|
||||
compile project(':node')
|
||||
compile "com.google.guava:guava:19.0"
|
||||
runtime project(path: ":node", configuration: 'runtimeArtifacts')
|
||||
runtime project(path: ":node:capsule", configuration: 'runtimeArtifacts')
|
||||
}
|
||||
|
||||
task jacocoRootReport(type: org.gradle.testing.jacoco.tasks.JacocoReport) {
|
||||
@ -153,5 +156,25 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['build']) {
|
||||
}
|
||||
}
|
||||
|
||||
// Aliasing the publishToMavenLocal for simplicity.
|
||||
task(install, dependsOn: 'publishToMavenLocal')
|
||||
bintrayConfig {
|
||||
user = System.getenv('CORDA_BINTRAY_USER')
|
||||
key = System.getenv('CORDA_BINTRAY_KEY')
|
||||
repo = 'corda'
|
||||
org = 'r3'
|
||||
licenses = ['Apache-2.0']
|
||||
vcsUrl = 'https://github.com/corda/corda'
|
||||
projectUrl = 'https://github.com/corda/corda'
|
||||
gpgSign = true
|
||||
gpgPassphrase = System.getenv('CORDA_BINTRAY_GPG_PASSPHRASE')
|
||||
publications = ['client', 'core', 'corda', 'finance', 'node', 'test-utils']
|
||||
license {
|
||||
name = 'Apache-2.0'
|
||||
url = 'https://www.apache.org/licenses/LICENSE-2.0'
|
||||
distribution = 'repo'
|
||||
}
|
||||
developer {
|
||||
id = 'R3'
|
||||
name = 'R3'
|
||||
email = 'dev@corda.net'
|
||||
}
|
||||
}
|
@ -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.
|
||||
|
||||
|
@ -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()
|
||||
@ -89,15 +89,3 @@ 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
|
||||
}
|
||||
}
|
||||
}
|
@ -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 }
|
||||
|
@ -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>?) =
|
||||
|
@ -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
|
||||
}
|
@ -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)
|
||||
|
@ -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.
|
||||
|
@ -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 */
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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"
|
||||
|
@ -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)
|
||||
|
@ -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>()
|
||||
|
@ -107,6 +107,7 @@ Read on to learn:
|
||||
release-notes
|
||||
codestyle
|
||||
building-the-docs
|
||||
publishing-corda
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
76
docs/source/publishing-corda.rst
Normal file
76
docs/source/publishing-corda.rst
Normal 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
|
@ -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
|
||||
---------------------------------------------------------------
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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()
|
||||
@ -29,15 +31,3 @@ sourceSets {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
publishing {
|
||||
publications {
|
||||
finance(MavenPublication) {
|
||||
from components.java
|
||||
artifactId 'finance'
|
||||
|
||||
artifact sourceJar
|
||||
artifact javadocJar
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
@ -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')
|
||||
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'
|
||||
}
|
||||
}
|
||||
|
||||
// Aliasing the publishToMavenLocal for simplicity.
|
||||
task(install, dependsOn: 'publishToMavenLocal')
|
||||
|
||||
|
@ -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'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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')
|
||||
|
@ -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
|
||||
}
|
@ -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> {
|
||||
Project project
|
||||
String publishName
|
||||
ProjectPublishExtension publishConfig
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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()
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
rootProject.name = 'corda-gradle-plugins'
|
||||
include 'quasar-utils'
|
||||
include 'publish-utils'
|
||||
include 'quasar-utils'
|
||||
include 'cordformation'
|
@ -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 {
|
||||
@ -169,49 +160,3 @@ 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
83
node/capsule/build.gradle
Normal 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
|
||||
}
|
@ -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),
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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) }
|
||||
}
|
||||
|
@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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")
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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) {
|
||||
|
@ -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 {
|
||||
|
@ -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,6 +31,7 @@ class CashBalanceAsMetricsObserver(val serviceHubInternal: ServiceHubInternal) {
|
||||
//
|
||||
// Note: exported as pennies.
|
||||
val m = serviceHubInternal.monitoringService.metrics
|
||||
databaseTransaction(database) {
|
||||
for ((key, value) in vault.cashBalances) {
|
||||
val metric = balanceMetrics.getOrPut(key) {
|
||||
val newMetric = BalanceMetric()
|
||||
@ -39,3 +42,4 @@ class CashBalanceAsMetricsObserver(val serviceHubInternal: ServiceHubInternal) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
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>)
|
||||
|
||||
|
@ -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 {
|
||||
HTTP_OK -> ZipInputStream(conn.inputStream).use {
|
||||
val certificates = ArrayList<Certificate>()
|
||||
val stream = CertificateStream(it)
|
||||
while (it.nextEntry != null) {
|
||||
certificates.add(CertificateFactory.getInstance("X.509").generateCertificate(it))
|
||||
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)
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
||||
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)
|
||||
|
@ -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,8 +202,10 @@ class InMemoryNetworkMapServiceTest : AbstractNetworkMapServiceTest() {
|
||||
fun success() {
|
||||
val (mapServiceNode, registerNode) = network.createTwoNodes()
|
||||
val service = mapServiceNode.inNodeNetworkMapService!! as InMemoryNetworkMapService
|
||||
databaseTransaction(mapServiceNode.database) {
|
||||
success(mapServiceNode, registerNode, { service }, { })
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `success with network`() {
|
||||
|
@ -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
1
publish.properties
Normal file
@ -0,0 +1 @@
|
||||
gradlePluginsVersion=0.6.2
|
@ -8,3 +8,4 @@ Please refer to `README.md` in the individual project folders. There are the fo
|
||||
* **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.
|
||||
* **bank-of-corda-demo** A demo showing a node acting as an issuer of fungible assets (initially Cash)
|
@ -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')
|
||||
|
||||
|
57
samples/bank-of-corda-demo/README.md
Normal file
57
samples/bank-of-corda-demo/README.md
Normal 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/).
|
148
samples/bank-of-corda-demo/build.gradle
Normal file
148
samples/bank-of-corda-demo/build.gradle
Normal 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'
|
||||
}
|
2
samples/bank-of-corda-demo/gradle.properties
Normal file
2
samples/bank-of-corda-demo/gradle.properties
Normal file
@ -0,0 +1,2 @@
|
||||
name = BankOfCorda
|
||||
kotlin.incremental=false
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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))
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
# Register a ServiceLoader service extending from net.corda.node.CordaPluginRegistry
|
||||
net.corda.bank.plugin.BankOfCordaPlugin
|
@ -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>
|
||||
)
|
||||
}
|
@ -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')
|
||||
|
@ -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')
|
||||
|
@ -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')
|
||||
|
@ -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')
|
||||
|
@ -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')
|
||||
|
@ -5,6 +5,7 @@ include 'finance'
|
||||
include 'finance:isolated'
|
||||
include 'core'
|
||||
include 'node'
|
||||
include 'node:capsule'
|
||||
include 'client'
|
||||
include 'netpermission'
|
||||
include 'experimental'
|
||||
@ -19,3 +20,4 @@ include 'samples:irs-demo'
|
||||
include 'samples:network-visualiser'
|
||||
include 'samples:simm-valuation-demo'
|
||||
include 'samples:raft-notary-demo'
|
||||
include 'samples:bank-of-corda-demo'
|
@ -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'
|
||||
}
|
||||
@ -48,15 +49,3 @@ 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user