mirror of
https://github.com/corda/corda.git
synced 2025-01-01 02:36:44 +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.
|
||||
|
||||
@ -83,4 +73,4 @@ dependencies {
|
||||
task integrationTest(type: Test) {
|
||||
testClassesDir = sourceSets.integrationTest.output.classesDir
|
||||
classpath = sourceSets.integrationTest.runtimeClasspath
|
||||
}
|
||||
}
|
@ -2,14 +2,14 @@ apply plugin: 'kotlin'
|
||||
apply plugin: 'net.corda.plugins.quasar-utils'
|
||||
apply plugin: 'net.corda.plugins.publish-utils'
|
||||
|
||||
description 'Corda core'
|
||||
|
||||
buildscript {
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
// apply plugin: 'org.jetbrains.dokka'
|
||||
|
||||
repositories {
|
||||
mavenLocal()
|
||||
mavenCentral()
|
||||
@ -88,16 +88,4 @@ dependencies {
|
||||
|
||||
// RS API: Response type and codes for ApiUtils.
|
||||
compile "javax.ws.rs:javax.ws.rs-api:2.0"
|
||||
}
|
||||
|
||||
publishing {
|
||||
publications {
|
||||
core(MavenPublication) {
|
||||
from components.java
|
||||
artifactId 'core'
|
||||
|
||||
artifact sourceJar
|
||||
artifact javadocJar
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
@ -28,16 +30,4 @@ sourceSets {
|
||||
srcDir "../config/test"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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')
|
||||
}
|
||||
|
||||
// Aliasing the publishToMavenLocal for simplicity.
|
||||
task(install, dependsOn: 'publishToMavenLocal')
|
||||
|
||||
bintrayConfig {
|
||||
user = System.getenv('CORDA_BINTRAY_USER')
|
||||
key = System.getenv('CORDA_BINTRAY_KEY')
|
||||
repo = 'corda'
|
||||
org = 'r3'
|
||||
licenses = ['Apache-2.0']
|
||||
vcsUrl = 'https://github.com/corda/corda'
|
||||
projectUrl = 'https://github.com/corda/corda'
|
||||
gpgSign = true
|
||||
gpgPassphrase = System.getenv('CORDA_BINTRAY_GPG_PASSPHRASE')
|
||||
publications = ['cordformation', 'quasar-utils']
|
||||
license {
|
||||
name = 'Apache-2.0'
|
||||
url = 'https://www.apache.org/licenses/LICENSE-2.0'
|
||||
distribution = 'repo'
|
||||
}
|
||||
developer {
|
||||
id = 'R3'
|
||||
name = 'R3'
|
||||
email = 'dev@corda.net'
|
||||
}
|
||||
}
|
@ -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> {
|
||||
void apply(Project project) {
|
||||
project.task("sourceJar", type: Jar, dependsOn: project.classes) {
|
||||
classifier = 'sources'
|
||||
from project.sourceSets.main.allSource
|
||||
}
|
||||
Project project
|
||||
String publishName
|
||||
ProjectPublishExtension publishConfig
|
||||
|
||||
project.task("javadocJar", type: Jar, dependsOn: project.javadoc) {
|
||||
classifier = 'javadoc'
|
||||
from project.javadoc.destinationDir
|
||||
void apply(Project project) {
|
||||
this.project = project
|
||||
|
||||
createTasks()
|
||||
createExtensions()
|
||||
createConfigurations()
|
||||
|
||||
project.afterEvaluate {
|
||||
configurePublishingName()
|
||||
checkAndConfigurePublishing()
|
||||
}
|
||||
}
|
||||
|
||||
void configurePublishingName() {
|
||||
if(publishConfig.name != null) {
|
||||
project.logger.info("Changing publishing name for ${project.name} to ${publishConfig.name}")
|
||||
publishName = publishConfig.name
|
||||
} else {
|
||||
publishName = project.name
|
||||
}
|
||||
}
|
||||
|
||||
void checkAndConfigurePublishing() {
|
||||
project.logger.info("Checking whether to publish $publishName")
|
||||
def bintrayConfig = project.rootProject.extensions.findByType(BintrayConfigExtension.class)
|
||||
if((bintrayConfig != null) && (bintrayConfig.publications) && (bintrayConfig.publications.findAll { it == publishName }.size() > 0)) {
|
||||
configurePublishing(bintrayConfig)
|
||||
}
|
||||
}
|
||||
|
||||
void configurePublishing(BintrayConfigExtension bintrayConfig) {
|
||||
project.logger.info("Configuring bintray for ${publishName}")
|
||||
configureMavenPublish(bintrayConfig)
|
||||
configureBintray(bintrayConfig)
|
||||
}
|
||||
|
||||
void configureMavenPublish(BintrayConfigExtension bintrayConfig) {
|
||||
project.apply([plugin: 'maven-publish'])
|
||||
project.publishing.publications.create(publishName, MavenPublication) {
|
||||
if(!publishConfig.disableDefaultJar) {
|
||||
from project.components.java
|
||||
}
|
||||
groupId project.group
|
||||
artifactId publishName
|
||||
|
||||
artifact project.tasks.sourceJar
|
||||
artifact project.tasks.javadocJar
|
||||
|
||||
project.configurations.publish.artifacts.each {
|
||||
project.logger.debug("Adding artifact: $it")
|
||||
delegate.artifact it
|
||||
}
|
||||
|
||||
extendPomForMavenCentral(pom, bintrayConfig)
|
||||
}
|
||||
project.task("install", dependsOn: "publishToMavenLocal")
|
||||
}
|
||||
|
||||
// Maven central requires all of the below fields for this to be a valid POM
|
||||
void extendPomForMavenCentral(MavenPom pom, BintrayConfigExtension config) {
|
||||
pom.withXml {
|
||||
asNode().children().last() + {
|
||||
resolveStrategy = Closure.DELEGATE_FIRST
|
||||
name publishName
|
||||
description project.description
|
||||
url config.projectUrl
|
||||
scm {
|
||||
url config.vcsUrl
|
||||
}
|
||||
|
||||
licenses {
|
||||
license {
|
||||
name config.license.name
|
||||
url config.license.url
|
||||
distribution config.license.url
|
||||
}
|
||||
}
|
||||
|
||||
developers {
|
||||
developer {
|
||||
id config.developer.id
|
||||
name config.developer.name
|
||||
email config.developer.email
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void configureBintray(BintrayConfigExtension bintrayConfig) {
|
||||
project.apply([plugin: 'com.jfrog.bintray'])
|
||||
project.bintray {
|
||||
user = bintrayConfig.user
|
||||
key = bintrayConfig.key
|
||||
publications = [ publishName ]
|
||||
dryRun = bintrayConfig.dryRun ?: false
|
||||
pkg {
|
||||
repo = bintrayConfig.repo
|
||||
name = publishName
|
||||
userOrg = bintrayConfig.org
|
||||
licenses = bintrayConfig.licenses
|
||||
|
||||
version {
|
||||
gpg {
|
||||
sign = bintrayConfig.gpgSign ?: false
|
||||
passphrase = bintrayConfig.gpgPassphrase
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void createTasks() {
|
||||
if(project.hasProperty('classes')) {
|
||||
project.task("sourceJar", type: Jar, dependsOn: project.classes) {
|
||||
classifier = 'sources'
|
||||
from project.sourceSets.main.allSource
|
||||
}
|
||||
}
|
||||
|
||||
if(project.hasProperty('javadoc')) {
|
||||
project.task("javadocJar", type: Jar, dependsOn: project.javadoc) {
|
||||
classifier = 'javadoc'
|
||||
from project.javadoc.destinationDir
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void createExtensions() {
|
||||
if(project == project.rootProject) {
|
||||
project.extensions.create("bintrayConfig", BintrayConfigExtension)
|
||||
}
|
||||
publishConfig = project.extensions.create("publish", ProjectPublishExtension)
|
||||
}
|
||||
|
||||
void createConfigurations() {
|
||||
project.configurations.create("publish")
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
@ -168,50 +159,4 @@ dependencies {
|
||||
task integrationTest(type: Test) {
|
||||
testClassesDir = sourceSets.integrationTest.output.classesDir
|
||||
classpath = sourceSets.integrationTest.runtimeClasspath
|
||||
}
|
||||
|
||||
task buildCordaJAR(type: FatCapsule, dependsOn: ['jar', 'buildCertSigningRequestUtilityJAR']) {
|
||||
applicationClass 'net.corda.node.MainKt'
|
||||
archiveName "corda-${corda_version}.jar"
|
||||
applicationSource = files(project.tasks.findByName('jar'), 'build/classes/main/CordaCaplet.class', 'config/dev/log4j2.xml')
|
||||
|
||||
capsuleManifest {
|
||||
appClassPath = ["jolokia-agent-war-${project.rootProject.ext.jolokia_version}.war"]
|
||||
javaAgents = ["quasar-core-${quasar_version}-jdk8.jar"]
|
||||
minJavaVersion = '1.8.0'
|
||||
caplets = ['CordaCaplet']
|
||||
}
|
||||
}
|
||||
|
||||
task buildCertSigningRequestUtilityJAR(type: FatCapsule, dependsOn: project.jar) {
|
||||
applicationClass 'net.corda.node.utilities.certsigning.CertificateSignerKt'
|
||||
archiveName 'certSigningRequestUtility.jar'
|
||||
capsuleManifest {
|
||||
systemProperties['log4j.configuration'] = 'log4j2.xml'
|
||||
minJavaVersion = '1.8.0'
|
||||
}
|
||||
}
|
||||
|
||||
artifacts {
|
||||
runtimeArtifacts buildCordaJAR
|
||||
}
|
||||
|
||||
publishing {
|
||||
publications {
|
||||
node(MavenPublication) {
|
||||
from components.java
|
||||
artifactId 'node'
|
||||
|
||||
artifact sourceJar
|
||||
artifact javadocJar
|
||||
}
|
||||
corda(MavenPublication) {
|
||||
artifactId 'corda'
|
||||
|
||||
artifact buildCordaJAR {
|
||||
classifier ""
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
83
node/capsule/build.gradle
Normal file
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)
|
||||
|
||||
|
@ -42,7 +42,7 @@ abstract class ServiceHubInternal : PluginServiceHub {
|
||||
abstract val schemaService: SchemaService
|
||||
|
||||
abstract override val networkService: MessagingServiceInternal
|
||||
|
||||
|
||||
/**
|
||||
* Given a list of [SignedTransaction]s, writes them to the given storage for validated transactions and then
|
||||
* sends them to the vault for further processing. This is intended for implementations to call from
|
||||
|
@ -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,13 +31,15 @@ class CashBalanceAsMetricsObserver(val serviceHubInternal: ServiceHubInternal) {
|
||||
//
|
||||
// Note: exported as pennies.
|
||||
val m = serviceHubInternal.monitoringService.metrics
|
||||
for ((key, value) in vault.cashBalances) {
|
||||
val metric = balanceMetrics.getOrPut(key) {
|
||||
val newMetric = BalanceMetric()
|
||||
m.register("VaultBalances.${key}Pennies", newMetric)
|
||||
newMetric
|
||||
databaseTransaction(database) {
|
||||
for ((key, value) in vault.cashBalances) {
|
||||
val metric = balanceMetrics.getOrPut(key) {
|
||||
val newMetric = BalanceMetric()
|
||||
m.register("VaultBalances.${key}Pennies", newMetric)
|
||||
newMetric
|
||||
}
|
||||
metric.pennies = value.quantity
|
||||
}
|
||||
metric.pennies = value.quantity
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
threadLocalTx.set(this)
|
||||
override fun newTransaction(isolation: Int): Transaction {
|
||||
val impl = StrandLocalTransaction(database, isolation, threadLocalTx, transactionBoundaries)
|
||||
return Transaction(impl).apply {
|
||||
threadLocalTx.set(this)
|
||||
putUserData(TX_ID, impl.id)
|
||||
}
|
||||
}
|
||||
|
||||
override fun currentOrNull(): Transaction? = threadLocalTx.get()
|
||||
|
||||
// Direct copy of [ThreadLocalTransaction].
|
||||
private class StrandLocalTransaction(override val db: Database, isolation: Int, val threadLocal: ThreadLocal<Transaction>) : TransactionInterface {
|
||||
private class StrandLocalTransaction(override val db: Database, isolation: Int, val threadLocal: ThreadLocal<Transaction>, val transactionBoundaries: PublishSubject<Boundary>) : TransactionInterface {
|
||||
val id = UUID.randomUUID()
|
||||
|
||||
override val connection: Connection by lazy(LazyThreadSafetyMode.NONE) {
|
||||
db.connector().apply {
|
||||
@ -145,13 +175,33 @@ class StrandLocalTransactionManager(initWithDatabase: Database) : TransactionMan
|
||||
override fun close() {
|
||||
connection.close()
|
||||
threadLocal.set(outerTransaction)
|
||||
if (outerTransaction == null) {
|
||||
transactionBoundaries.onNext(Boundary(id))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Buffer observations until after the current database transaction has been closed. Observations are never
|
||||
* dropped, simply delayed.
|
||||
*
|
||||
* Primarily for use by component authors to publish observations during database transactions without racing against
|
||||
* closing the database transaction.
|
||||
*
|
||||
* For examples, see the call hierarchy of this function.
|
||||
*/
|
||||
fun <T : Any> rx.Observer<T>.bufferUntilDatabaseCommit(): rx.Observer<T> {
|
||||
val currentTxId = StrandLocalTransactionManager.transactionId
|
||||
val databaseTxBoundary: Observable<StrandLocalTransactionManager.Boundary> = StrandLocalTransactionManager.transactionBoundaries.filter { it.txId == currentTxId }.first()
|
||||
val subject = UnicastSubject.create<T>()
|
||||
subject.delaySubscription(databaseTxBoundary).subscribe(this)
|
||||
databaseTxBoundary.doOnCompleted { subject.onCompleted() }
|
||||
return subject
|
||||
}
|
||||
|
||||
// Composite columns for use with below Exposed helpers.
|
||||
data class PartyColumns(val name: Column<String>, val owningKey: Column<CompositeKey>)
|
||||
|
||||
data class StateRefColumns(val txId: Column<SecureHash>, val index: Column<Int>)
|
||||
data class TxnNoteColumns(val txId: Column<SecureHash>, val note: Column<String>)
|
||||
|
||||
|
@ -1,12 +1,13 @@
|
||||
package net.corda.node.utilities.certsigning
|
||||
|
||||
import net.corda.core.crypto.CertificateStream
|
||||
import org.apache.commons.io.IOUtils
|
||||
import org.bouncycastle.pkcs.PKCS10CertificationRequest
|
||||
import java.io.IOException
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.HttpURLConnection.*
|
||||
import java.net.URL
|
||||
import java.security.cert.Certificate
|
||||
import java.security.cert.CertificateFactory
|
||||
import java.util.*
|
||||
import java.util.zip.ZipInputStream
|
||||
|
||||
@ -24,18 +25,17 @@ class HTTPCertificateSigningService(val server: URL) : CertificateSigningService
|
||||
conn.requestMethod = "GET"
|
||||
|
||||
return when (conn.responseCode) {
|
||||
HttpURLConnection.HTTP_OK -> conn.inputStream.use {
|
||||
ZipInputStream(it).use {
|
||||
val certificates = ArrayList<Certificate>()
|
||||
while (it.nextEntry != null) {
|
||||
certificates.add(CertificateFactory.getInstance("X.509").generateCertificate(it))
|
||||
}
|
||||
certificates.toTypedArray()
|
||||
HTTP_OK -> ZipInputStream(conn.inputStream).use {
|
||||
val certificates = ArrayList<Certificate>()
|
||||
val stream = CertificateStream(it)
|
||||
while (it.nextEntry != null) {
|
||||
certificates.add(stream.nextCertificate())
|
||||
}
|
||||
certificates.toTypedArray()
|
||||
}
|
||||
HttpURLConnection.HTTP_NO_CONTENT -> null
|
||||
HttpURLConnection.HTTP_UNAUTHORIZED -> throw IOException("Certificate signing request has been rejected, please contact Corda network administrator for more information.")
|
||||
else -> throw IOException("Unexpected response code ${conn.responseCode} - ${IOUtils.toString(conn.errorStream)}")
|
||||
HTTP_NO_CONTENT -> null
|
||||
HTTP_UNAUTHORIZED -> throw IOException("Certificate signing request has been rejected: ${conn.errorMessage}")
|
||||
else -> throwUnexpectedResponseCode(conn)
|
||||
}
|
||||
}
|
||||
|
||||
@ -49,10 +49,15 @@ class HTTPCertificateSigningService(val server: URL) : CertificateSigningService
|
||||
conn.outputStream.write(request.encoded)
|
||||
|
||||
return when (conn.responseCode) {
|
||||
HttpURLConnection.HTTP_OK -> IOUtils.toString(conn.inputStream)
|
||||
HttpURLConnection.HTTP_FORBIDDEN -> throw IOException("Client version $clientVersion is forbidden from accessing permissioning server, please upgrade to newer version.")
|
||||
else -> throw IOException("Unexpected response code ${conn.responseCode} - ${IOUtils.toString(conn.errorStream)}")
|
||||
HTTP_OK -> IOUtils.toString(conn.inputStream)
|
||||
HTTP_FORBIDDEN -> throw IOException("Client version $clientVersion is forbidden from accessing permissioning server, please upgrade to newer version.")
|
||||
else -> throwUnexpectedResponseCode(conn)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun throwUnexpectedResponseCode(connection: HttpURLConnection): Nothing {
|
||||
throw IOException("Unexpected response code ${connection.responseCode} - ${connection.errorMessage}")
|
||||
}
|
||||
|
||||
private val HttpURLConnection.errorMessage: String get() = IOUtils.toString(errorStream)
|
||||
}
|
||||
|
@ -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)
|
||||
|
||||
nodeA.netMapCache.addNode(nodeB.info)
|
||||
databaseTransaction(nodeA.database) {
|
||||
nodeA.netMapCache.addNode(nodeB.info)
|
||||
}
|
||||
// Now both nodes match, so it throws an error
|
||||
expect<IllegalStateException> {
|
||||
nodeA.netMapCache.getNodeByCompositeKey(keyPair.public.composite)
|
||||
|
@ -16,6 +16,7 @@ import net.corda.node.services.network.NetworkMapService.Companion.REGISTER_FLOW
|
||||
import net.corda.node.services.network.NetworkMapService.Companion.SUBSCRIPTION_FLOW_TOPIC
|
||||
import net.corda.node.services.network.NodeRegistration
|
||||
import net.corda.node.utilities.AddOrRemove
|
||||
import net.corda.node.utilities.databaseTransaction
|
||||
import net.corda.testing.node.MockNetwork
|
||||
import net.corda.testing.node.MockNetwork.MockNode
|
||||
import org.junit.Before
|
||||
@ -201,7 +202,9 @@ class InMemoryNetworkMapServiceTest : AbstractNetworkMapServiceTest() {
|
||||
fun success() {
|
||||
val (mapServiceNode, registerNode) = network.createTwoNodes()
|
||||
val service = mapServiceNode.inNodeNetworkMapService!! as InMemoryNetworkMapService
|
||||
success(mapServiceNode, registerNode, { service }, { })
|
||||
databaseTransaction(mapServiceNode.database) {
|
||||
success(mapServiceNode, registerNode, { service }, { })
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -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
|
@ -7,4 +7,5 @@ Please refer to `README.md` in the individual project folders. There are the fo
|
||||
* **trader-demo** A simple driver for exercising the two party trading flow. In this scenario, a buyer wants to purchase some commercial paper by swapping his cash for commercial paper. The seller learns that the buyer exists, and sends them a message to kick off the trade. The seller, having obtained his CP, then quits and the buyer goes back to waiting. The buyer will sell as much CP as he can! **We recommend starting with this demo.**
|
||||
* **Network-visualiser** A tool that uses a simulation to visualise the interaction and messages between nodes on the Corda network. Currently only works for the IRS demo.
|
||||
* **simm-valudation-demo** A demo showing two nodes reaching agreement on the valuation of a derivatives portfolio.
|
||||
* **raft-notary-demo** A simple demonstration of a node getting multiple transactions notarised by a distributed (Raft-based) notary.
|
||||
* **raft-notary-demo** A simple demonstration of a node getting multiple transactions notarised by a distributed (Raft-based) notary.
|
||||
* **bank-of-corda-demo** A demo showing a node acting as an issuer of fungible assets (initially Cash)
|
@ -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'
|
||||
@ -18,4 +19,5 @@ include 'samples:trader-demo'
|
||||
include 'samples:irs-demo'
|
||||
include 'samples:network-visualiser'
|
||||
include 'samples:simm-valuation-demo'
|
||||
include 'samples:raft-notary-demo'
|
||||
include 'samples:raft-notary-demo'
|
||||
include 'samples:bank-of-corda-demo'
|
@ -2,6 +2,8 @@ apply plugin: 'kotlin'
|
||||
apply plugin: 'net.corda.plugins.quasar-utils'
|
||||
apply plugin: 'net.corda.plugins.publish-utils'
|
||||
|
||||
description 'Testing utilities for Corda'
|
||||
|
||||
repositories {
|
||||
mavenLocal()
|
||||
mavenCentral()
|
||||
@ -16,7 +18,6 @@ repositories {
|
||||
|
||||
//noinspection GroovyAssignabilityCheck
|
||||
configurations {
|
||||
|
||||
// we don't want isolated.jar in classPath, since we want to test jar being dynamically loaded as an attachment
|
||||
runtime.exclude module: 'isolated'
|
||||
}
|
||||
@ -47,16 +48,4 @@ dependencies {
|
||||
|
||||
// OkHTTP: Simple HTTP library.
|
||||
compile 'com.squareup.okhttp3:okhttp:3.3.1'
|
||||
}
|
||||
|
||||
publishing {
|
||||
publications {
|
||||
testutils(MavenPublication) {
|
||||
from components.java
|
||||
artifactId 'test-utils'
|
||||
|
||||
artifact sourceJar
|
||||
artifact javadocJar
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user