diff --git a/.idea/compiler.xml b/.idea/compiler.xml
index 723b83cbb3..27f4989aa7 100644
--- a/.idea/compiler.xml
+++ b/.idea/compiler.xml
@@ -12,6 +12,9 @@
+
+
+
@@ -115,6 +118,8 @@
+
+
diff --git a/finance/src/main/resources/database/postgresql/scripts/create-db.sql b/finance/src/main/resources/database/postgresql/scripts/create-db.sql
new file mode 100644
index 0000000000..66240e8db1
--- /dev/null
+++ b/finance/src/main/resources/database/postgresql/scripts/create-db.sql
@@ -0,0 +1,32 @@
+create table contract_cash_states (
+ output_index int4 not null,
+ transaction_id varchar(64) not null,
+ ccy_code varchar(3),
+ issuer_key_hash varchar(130),
+ issuer_ref bytea,
+ owner_name varchar(255),
+ pennies int8,
+ primary key (output_index, transaction_id)
+);
+
+create index ccy_code_idx on contract_cash_states (ccy_code);
+create index pennies_idx on contract_cash_states (pennies);
+
+
+create table cp_states (
+ output_index int4 not null,
+ transaction_id varchar(64) not null,
+ ccy_code varchar(3),
+ face_value int8,
+ face_value_issuer_key_hash varchar(130),
+ face_value_issuer_ref bytea,
+ issuance_key_hash varchar(130),
+ issuance_ref bytea,
+ maturity_instant timestamp,
+ owner_key_hash varchar(130),
+ primary key (output_index, transaction_id)
+);
+
+create index ccy_code_index on cp_states (ccy_code);
+create index maturity_index on cp_states (maturity_instant);
+create index face_value_index on cp_states (face_value);
diff --git a/node/src/main/resources/database/postgresql/scripts/create-db.sql b/node/src/main/resources/database/postgresql/scripts/create-db.sql
new file mode 100644
index 0000000000..55eda544c8
--- /dev/null
+++ b/node/src/main/resources/database/postgresql/scripts/create-db.sql
@@ -0,0 +1,238 @@
+
+create table link_nodeinfo_party (
+ node_info_id int4 not null,
+ party_name varchar(255) not null
+);
+
+
+create table node_attachments (
+ att_id varchar(255) not null,
+ content oid,
+ filename varchar(255),
+ insertion_date timestamp not null,
+ uploader varchar(255),
+ primary key (att_id)
+);
+
+
+create table node_bft_committed_states (
+ output_index int4 not null,
+ transaction_id varchar(64) not null,
+ consuming_input_index int4,
+ consuming_transaction_id varchar(255),
+ requesting_party_name varchar(255),
+ requesting_party_key bytea,
+ primary key (output_index, transaction_id)
+);
+
+
+create table node_checkpoints (
+ checkpoint_id varchar(64) not null,
+ checkpoint_value oid,
+ primary key (checkpoint_id)
+);
+
+
+create table node_contract_upgrades (
+ state_ref varchar(96) not null,
+ contract_class_name varchar(255),
+ primary key (state_ref)
+);
+
+
+create table node_identities (
+ pk_hash varchar(130) not null,
+ identity_value oid,
+ primary key (pk_hash)
+);
+
+
+create table node_info_hosts (
+ host varchar(255) not null,
+ port int4 not null,
+ node_info_id int4,
+ primary key (host, port)
+);
+
+
+create table node_info_party_cert (
+ party_name varchar(255) not null,
+ isMain boolean not null,
+ owning_key_hash varchar(130),
+ party_cert_binary oid,
+ primary key (party_name)
+);
+
+
+create table node_infos (
+ node_info_id int4 not null,
+ node_info_hash varchar(64),
+ platform_version int4,
+ serial int8,
+ primary key (node_info_id)
+);
+
+
+create table node_message_ids (
+ message_id varchar(36) not null,
+ insertion_time timestamp,
+ primary key (message_id)
+);
+
+
+create table node_message_retry (
+ message_id int8 not null,
+ message oid,
+ recipients oid,
+ primary key (message_id)
+);
+
+
+create table node_named_identities (
+ name varchar(128) not null,
+ pk_hash varchar(130),
+ primary key (name)
+);
+
+
+create table node_notary_commit_log (
+ output_index int4 not null,
+ transaction_id varchar(64) not null,
+ consuming_input_index int4,
+ consuming_transaction_id varchar(255),
+ requesting_party_name varchar(255),
+ requesting_party_key bytea,
+ primary key (output_index, transaction_id)
+);
+
+
+create table node_our_key_pairs (
+ public_key_hash varchar(130) not null,
+ private_key oid,
+ public_key oid,
+ primary key (public_key_hash)
+);
+
+
+create table node_raft_committed_states (
+ id varchar(255) not null,
+ state_index int8,
+ state_value oid,
+ primary key (id)
+);
+
+
+create table node_scheduled_states (
+ output_index int4 not null,
+ transaction_id varchar(64) not null,
+ scheduled_at timestamp not null,
+ primary key (output_index, transaction_id)
+);
+
+
+create table node_transaction_mappings (
+ tx_id varchar(64) not null,
+ state_machine_run_id varchar(36),
+ primary key (tx_id)
+);
+
+
+create table node_transactions (
+ tx_id varchar(64) not null,
+ transaction_value oid,
+ primary key (tx_id)
+);
+
+
+create table vault_fungible_states (
+ output_index int4 not null,
+ transaction_id varchar(64) not null,
+ issuer_name varchar(255),
+ issuer_ref bytea,
+ owner_name varchar(255),
+ quantity int8,
+ primary key (output_index, transaction_id)
+);
+
+
+create table vault_fungible_states_parts (
+ output_index int4 not null,
+ transaction_id varchar(64) not null,
+ participants varchar(255)
+);
+
+
+create table vault_linear_states (
+ output_index int4 not null,
+ transaction_id varchar(64) not null,
+ external_id varchar(255),
+ uuid bytea not null,
+ primary key (output_index, transaction_id)
+);
+
+
+create table vault_linear_states_parts (
+ output_index int4 not null,
+ transaction_id varchar(64) not null,
+ participants varchar(255)
+);
+
+
+create table vault_states (
+ output_index int4 not null,
+ transaction_id varchar(64) not null,
+ consumed_timestamp timestamp,
+ contract_state_class_name varchar(255),
+ lock_id varchar(255),
+ lock_timestamp timestamp,
+ notary_name varchar(255),
+ recorded_timestamp timestamp,
+ state_status int4,
+ primary key (output_index, transaction_id)
+);
+
+
+create table vault_transaction_notes (
+ seq_no int4 not null,
+ note varchar(255),
+ transaction_id varchar(64),
+ primary key (seq_no)
+);
+
+create index att_id_idx on node_attachments (att_id);
+create index external_id_index on vault_linear_states (external_id);
+create index uuid_index on vault_linear_states (uuid);
+create index state_status_idx on vault_states (state_status);
+create index lock_id_idx on vault_states (lock_id, state_status);
+create index transaction_id_index on vault_transaction_notes (transaction_id);
+create sequence hibernate_sequence start 1 increment 1;
+
+
+alter table link_nodeinfo_party
+ add constraint FK1ua3h6nwwfji0mn23c5d1xx8e
+ foreign key (party_name)
+ references node_info_party_cert;
+
+
+alter table link_nodeinfo_party
+ add constraint FK544l9wsec35ph7hxrtwfd2lws
+ foreign key (node_info_id)
+ references node_infos;
+
+
+alter table node_info_hosts
+ add constraint FK5ie46htdrkftmwe6rpwrnp0mp
+ foreign key (node_info_id)
+ references node_infos;
+
+
+alter table vault_fungible_states_parts
+ add constraint FKchmfeq1ldqnoq9idv9ogxauqm
+ foreign key (output_index, transaction_id)
+ references vault_fungible_states;
+
+
+alter table vault_linear_states_parts
+ add constraint FKhafsv733d0bo9j1tg352koq3y
+ foreign key (output_index, transaction_id)
+ references vault_linear_states;
diff --git a/node/src/main/resources/database/sqlserver/scripts/create-db.sql b/node/src/main/resources/database/sqlserver/scripts/create-db.sql
index 04a799d97d..1596909999 100644
--- a/node/src/main/resources/database/sqlserver/scripts/create-db.sql
+++ b/node/src/main/resources/database/sqlserver/scripts/create-db.sql
@@ -201,6 +201,7 @@ create index att_id_idx on node_attachments (att_id);
create index external_id_index on vault_linear_states (external_id);
create index uuid_index on vault_linear_states (uuid);
create index state_status_idx on vault_states (state_status);
+create index lock_id_idx on vault_states (lock_id, state_status);
create index transaction_id_index on vault_transaction_notes (transaction_id);
create sequence hibernate_sequence start with 1 increment by 1;
diff --git a/samples/business-network-demo/build.gradle b/samples/business-network-demo/build.gradle
new file mode 100644
index 0000000000..ada57a4a0e
--- /dev/null
+++ b/samples/business-network-demo/build.gradle
@@ -0,0 +1,49 @@
+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.cordapp'
+apply plugin: 'net.corda.plugins.cordformation'
+apply plugin: 'maven-publish'
+
+sourceSets {
+ integrationTest {
+ kotlin {
+ compileClasspath += main.output + test.output
+ runtimeClasspath += main.output + test.output
+ }
+ }
+}
+
+dependencies {
+ compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version"
+
+ // For CSV parsing.
+ compile "com.opencsv:opencsv:4.0"
+
+ // Corda integration dependencies
+ cordaCompile project(':core')
+ cordaCompile project(':client:rpc')
+
+ // Cordapp dependencies
+ // Specify your cordapp's dependencies below, including dependent cordapps
+
+ // Test dependencies
+ testCompile "junit:junit:$junit_version"
+}
+
+idea {
+ module {
+ downloadJavadoc = true // defaults to false
+ downloadSources = true
+ }
+}
+
+jar {
+ manifest {
+ attributes(
+ 'Automatic-Module-Name': 'net.corda.samples.demos.businessnetwork'
+ )
+ }
+}
diff --git a/samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/contract.kt b/samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/contract.kt
new file mode 100644
index 0000000000..3bafcf3a03
--- /dev/null
+++ b/samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/contract.kt
@@ -0,0 +1,32 @@
+package net.corda.sample.businessnetwork
+
+import net.corda.core.contracts.CommandData
+import net.corda.core.contracts.Contract
+import net.corda.core.contracts.requireSingleCommand
+import net.corda.core.contracts.requireThat
+import net.corda.core.transactions.LedgerTransaction
+
+class IOUContract : Contract {
+ // Our Create command.
+ class Create : CommandData
+
+ override fun verify(tx: LedgerTransaction) {
+ val command = tx.commands.requireSingleCommand()
+
+ requireThat {
+ // Constraints on the shape of the transaction.
+ "No inputs should be consumed when issuing an IOU." using (tx.inputs.isEmpty())
+ "There should be one output state of type IOUState." using (tx.outputs.size == 1)
+
+ // IOU-specific constraints.
+ val out = tx.outputsOfType().single()
+ "The IOU's value must be non-negative." using (out.value > 0)
+ "The lender and the borrower cannot be the same entity." using (out.lender != out.borrower)
+
+ // Constraints on the signers.
+ "There must be two signers." using (command.signers.toSet().size == 2)
+ "The borrower and lender must be signers." using (command.signers.containsAll(listOf(
+ out.borrower.owningKey, out.lender.owningKey)))
+ }
+ }
+}
\ No newline at end of file
diff --git a/samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/flow.kt b/samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/flow.kt
new file mode 100644
index 0000000000..444e99b8d9
--- /dev/null
+++ b/samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/flow.kt
@@ -0,0 +1,71 @@
+package net.corda.sample.businessnetwork
+
+import co.paralleluniverse.fibers.Suspendable
+import net.corda.core.contracts.Command
+import net.corda.core.contracts.StateAndContract
+import net.corda.core.flows.*
+import net.corda.core.identity.CordaX500Name
+import net.corda.core.identity.Party
+import net.corda.core.transactions.SignedTransaction
+import net.corda.core.transactions.TransactionBuilder
+import net.corda.core.utilities.ProgressTracker
+import net.corda.sample.businessnetwork.membership.MembershipAware
+import kotlin.reflect.jvm.jvmName
+
+@InitiatingFlow
+@StartableByRPC
+class IOUFlow(val iouValue: Int,
+ val otherParty: Party) : FlowLogic(), MembershipAware {
+
+ companion object {
+ val allowedMembershipName =
+ CordaX500Name("AliceBobMembershipList", "AliceBob", "Washington", "US")
+ }
+
+ /** The progress tracker provides checkpoints indicating the progress of the flow to observers. */
+ override val progressTracker = ProgressTracker()
+
+ /** The flow logic is encapsulated within the call() method. */
+ @Suspendable
+ override fun call(): SignedTransaction {
+
+ // Check whether the other party belongs to the membership list important for us.
+ otherParty.checkMembership(allowedMembershipName, this)
+
+ // Prior to creating any state - obtain consent from [otherParty] to borrow from us.
+ // This is done early enough in the flow such that if the other party rejects - do not do any unnecessary processing in this flow.
+ // Even if this is not done, later on upon signatures collection phase membership will be checked on the other side and
+ // transaction rejected if this doesn't hold. See [IOUFlowResponder] for more information.
+ otherParty.checkSharesSameMembershipWithUs(allowedMembershipName, this)
+
+ // We retrieve the notary identity from the network map.
+ val notary = serviceHub.networkMapCache.notaryIdentities[0]
+
+ // We create a transaction builder
+ val txBuilder = TransactionBuilder(notary = notary)
+
+ // We create the transaction components.
+ val outputState = IOUState(iouValue, ourIdentity, otherParty)
+ val outputContract = IOUContract::class.jvmName
+ val outputContractAndState = StateAndContract(outputState, outputContract)
+ val cmd = Command(IOUContract.Create(), listOf(ourIdentity.owningKey, otherParty.owningKey))
+
+ // We add the items to the builder.
+ txBuilder.withItems(outputContractAndState, cmd)
+
+ // Verifying the transaction.
+ txBuilder.verify(serviceHub)
+
+ // Signing the transaction.
+ val signedTx = serviceHub.signInitialTransaction(txBuilder)
+
+ // Creating a session with the other party.
+ val otherpartySession = initiateFlow(otherParty)
+
+ // Obtaining the counterparty's signature.
+ val fullySignedTx = subFlow(CollectSignaturesFlow(signedTx, listOf(otherpartySession), CollectSignaturesFlow.tracker()))
+
+ // Finalising the transaction.
+ return subFlow(FinalityFlow(fullySignedTx))
+ }
+}
\ No newline at end of file
diff --git a/samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/flowResponder.kt b/samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/flowResponder.kt
new file mode 100644
index 0000000000..628fbccefd
--- /dev/null
+++ b/samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/flowResponder.kt
@@ -0,0 +1,27 @@
+package net.corda.sample.businessnetwork
+
+import co.paralleluniverse.fibers.Suspendable
+import net.corda.core.contracts.requireThat
+import net.corda.core.flows.FlowLogic
+import net.corda.core.flows.FlowSession
+import net.corda.core.flows.InitiatedBy
+import net.corda.core.flows.SignTransactionFlow
+import net.corda.core.transactions.SignedTransaction
+import net.corda.sample.businessnetwork.membership.CheckMembershipFlow
+
+@InitiatedBy(IOUFlow::class)
+class IOUFlowResponder(val otherPartySession: FlowSession) : FlowLogic() {
+ @Suspendable
+ override fun call() {
+ subFlow(CheckMembershipFlow(IOUFlow.allowedMembershipName, otherPartySession.counterparty))
+
+ subFlow(object : SignTransactionFlow(otherPartySession, SignTransactionFlow.tracker()) {
+ override fun checkTransaction(stx: SignedTransaction) = requireThat {
+ val output = stx.tx.outputs.single().data
+ "This must be an IOU transaction." using (output is IOUState)
+ val iou = output as IOUState
+ "The IOU's value can't be too high." using (iou.value < 100)
+ }
+ })
+ }
+}
\ No newline at end of file
diff --git a/samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/membership/CheckMembershipFlow.kt b/samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/membership/CheckMembershipFlow.kt
new file mode 100644
index 0000000000..ff46de4f9d
--- /dev/null
+++ b/samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/membership/CheckMembershipFlow.kt
@@ -0,0 +1,11 @@
+package net.corda.sample.businessnetwork.membership
+
+import net.corda.core.flows.FlowLogic
+import net.corda.core.identity.AbstractParty
+import net.corda.core.identity.CordaX500Name
+
+class CheckMembershipFlow(private val membershipName: CordaX500Name, private val counterParty: AbstractParty) : FlowLogic(), MembershipAware {
+ override fun call() {
+ counterParty.checkMembership(membershipName, this)
+ }
+}
\ No newline at end of file
diff --git a/samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/membership/MembershipAware.kt b/samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/membership/MembershipAware.kt
new file mode 100644
index 0000000000..a8d87e707f
--- /dev/null
+++ b/samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/membership/MembershipAware.kt
@@ -0,0 +1,31 @@
+package net.corda.sample.businessnetwork.membership
+
+import net.corda.core.flows.FlowException
+import net.corda.core.flows.FlowLogic
+import net.corda.core.identity.AbstractParty
+import net.corda.core.identity.CordaX500Name
+import net.corda.core.identity.Party
+import net.corda.core.node.ServiceHub
+import net.corda.sample.businessnetwork.membership.internal.MembershipListProvider
+
+interface MembershipAware {
+ /**
+ * Checks that party has at least one common membership list with current node.
+ * TODO: This functionality ought to be moved into a dedicated CordaService.
+ */
+ fun AbstractParty.checkMembership(membershipName: CordaX500Name, initiatorFlow: FlowLogic) {
+ val membershipList = getMembershipList(membershipName, initiatorFlow.serviceHub)
+ if (this !in membershipList) {
+ val msg = "'$this' doesn't belong to membership list: ${membershipName.commonName}"
+ throw MembershipViolationException(msg)
+ }
+ }
+
+ fun getMembershipList(listName: CordaX500Name, serviceHub: ServiceHub): MembershipList = MembershipListProvider.obtainMembershipList(listName, serviceHub.networkMapCache)
+
+ fun Party.checkSharesSameMembershipWithUs(membershipName: CordaX500Name, initiatorFlow: FlowLogic) {
+ initiatorFlow.stateMachine.initiateFlow(this, CheckMembershipFlow(membershipName, initiatorFlow.ourIdentity))
+ }
+}
+
+class MembershipViolationException(msg: String) : FlowException(msg)
\ No newline at end of file
diff --git a/samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/membership/MembershipList.kt b/samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/membership/MembershipList.kt
new file mode 100644
index 0000000000..a8cc101346
--- /dev/null
+++ b/samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/membership/MembershipList.kt
@@ -0,0 +1,19 @@
+package net.corda.sample.businessnetwork.membership
+
+import net.corda.core.identity.AbstractParty
+
+/**
+ * Represents a concept of a parties member list.
+ * Nodes or other parties can be grouped into membership lists to represent business network relationship among them
+ */
+interface MembershipList {
+ /**
+ * @return true if a particular party belongs to a list, false otherwise.
+ */
+ operator fun contains(party: AbstractParty): Boolean = content().contains(party)
+
+ /**
+ * Obtains a full content of a membership list.
+ */
+ fun content(): Set
+}
\ No newline at end of file
diff --git a/samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/membership/ObtainMembershipListContentFlow.kt b/samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/membership/ObtainMembershipListContentFlow.kt
new file mode 100644
index 0000000000..d44e3865fc
--- /dev/null
+++ b/samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/membership/ObtainMembershipListContentFlow.kt
@@ -0,0 +1,16 @@
+package net.corda.sample.businessnetwork.membership
+
+import co.paralleluniverse.fibers.Suspendable
+import net.corda.core.flows.FlowLogic
+import net.corda.core.flows.StartableByRPC
+import net.corda.core.identity.AbstractParty
+import net.corda.core.identity.CordaX500Name
+
+/**
+ * Flow to obtain content of the membership lists this node belongs to.
+ */
+@StartableByRPC
+class ObtainMembershipListContentFlow(private val membershipListName: CordaX500Name) : FlowLogic>(), MembershipAware {
+ @Suspendable
+ override fun call(): Set = getMembershipList(membershipListName, serviceHub).content()
+}
\ No newline at end of file
diff --git a/samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/membership/internal/CsvMembershipList.kt b/samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/membership/internal/CsvMembershipList.kt
new file mode 100644
index 0000000000..5a98412c97
--- /dev/null
+++ b/samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/membership/internal/CsvMembershipList.kt
@@ -0,0 +1,30 @@
+package net.corda.sample.businessnetwork.membership.internal
+
+import com.opencsv.CSVReaderBuilder
+import net.corda.core.identity.AbstractParty
+import net.corda.core.identity.CordaX500Name
+import net.corda.core.node.services.NetworkMapCache
+import net.corda.sample.businessnetwork.membership.MembershipList
+import java.io.InputStream
+
+/**
+ * Implementation of a MembershipList that reads the content from CSV file.
+ */
+class CsvMembershipList(private val inputStream: InputStream, private val networkMapCache: NetworkMapCache) : MembershipList {
+
+ private val allParties by lazy {
+ fun lookUpParty(name: CordaX500Name): AbstractParty? = networkMapCache.getPeerByLegalName(name)
+
+ inputStream.use {
+ val reader = CSVReaderBuilder(it.reader()).withSkipLines(1).build()
+ reader.use {
+ val linesRead = reader.readAll()
+ val commentsRemoved = linesRead.filterNot { line -> line.isEmpty() || line[0].startsWith("#") }
+ val partiesList = commentsRemoved.mapNotNull { line -> lookUpParty(CordaX500Name.parse(line[0])) }
+ partiesList.toSet()
+ }
+ }
+ }
+
+ override fun content(): Set = allParties
+}
\ No newline at end of file
diff --git a/samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/membership/internal/MembershipListProvider.kt b/samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/membership/internal/MembershipListProvider.kt
new file mode 100644
index 0000000000..b9ad6e1f92
--- /dev/null
+++ b/samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/membership/internal/MembershipListProvider.kt
@@ -0,0 +1,10 @@
+package net.corda.sample.businessnetwork.membership.internal
+
+import net.corda.core.identity.CordaX500Name
+import net.corda.core.node.services.NetworkMapCache
+import net.corda.sample.businessnetwork.membership.MembershipList
+
+object MembershipListProvider {
+ fun obtainMembershipList(listName: CordaX500Name, networkMapCache: NetworkMapCache): MembershipList =
+ CsvMembershipList(MembershipListProvider::class.java.getResourceAsStream("${listName.commonName}.csv"), networkMapCache)
+}
\ No newline at end of file
diff --git a/samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/state.kt b/samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/state.kt
new file mode 100644
index 0000000000..9cd8b888e4
--- /dev/null
+++ b/samples/business-network-demo/src/main/kotlin/net/corda/sample/businessnetwork/state.kt
@@ -0,0 +1,10 @@
+package net.corda.sample.businessnetwork
+
+import net.corda.core.contracts.ContractState
+import net.corda.core.identity.Party
+
+class IOUState(val value: Int,
+ val lender: Party,
+ val borrower: Party) : ContractState {
+ override val participants get() = listOf(lender, borrower)
+}
\ No newline at end of file
diff --git a/samples/business-network-demo/src/main/resources/net/corda/sample/businessnetwork/membership/internal/AliceBobMembershipList.csv b/samples/business-network-demo/src/main/resources/net/corda/sample/businessnetwork/membership/internal/AliceBobMembershipList.csv
new file mode 100644
index 0000000000..d10e451a83
--- /dev/null
+++ b/samples/business-network-demo/src/main/resources/net/corda/sample/businessnetwork/membership/internal/AliceBobMembershipList.csv
@@ -0,0 +1,3 @@
+Party name, Custom Field
+"C=ES,L=Madrid,O=Alice Corp",UserCustomValue1
+"C=IT,L=Rome,O=Bob Plc",UserCustomValue2
\ No newline at end of file
diff --git a/settings.gradle b/settings.gradle
index d9596e00a6..d25a21d690 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -46,6 +46,7 @@ include 'samples:network-visualiser'
include 'samples:simm-valuation-demo'
include 'samples:notary-demo'
include 'samples:bank-of-corda-demo'
+include 'samples:business-network-demo'
include 'cordform-common'
include 'network-management'
include 'verify-enclave'
diff --git a/tools/explorer/build.gradle b/tools/explorer/build.gradle
index 3f719c7d58..dfd1ea7d00 100644
--- a/tools/explorer/build.gradle
+++ b/tools/explorer/build.gradle
@@ -30,6 +30,9 @@ dependencies {
compile project(':node-driver')
compile project(':finance')
+ // Additional CorDapps that will be displayed in the GUI.
+ compile project(':samples:business-network-demo')
+
// Capsule is a library for building independently executable fat JARs.
// We only need this dependency to compile our Caplet against.
compileOnly "co.paralleluniverse:capsule:$capsule_version"
diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt
index ee1495ebd6..f9a79e9cb2 100644
--- a/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt
+++ b/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt
@@ -14,6 +14,8 @@ import net.corda.core.messaging.FlowHandle
import net.corda.core.messaging.startFlow
import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.getOrThrow
+import net.corda.sample.businessnetwork.IOUFlow
+import net.corda.sample.businessnetwork.membership.ObtainMembershipListContentFlow
import net.corda.finance.GBP
import net.corda.finance.USD
import net.corda.finance.contracts.asset.Cash
@@ -33,8 +35,10 @@ import java.util.*
class ExplorerSimulation(private val options: OptionSet) {
private val user = User("user1", "test", permissions = setOf(
startFlow(),
- startFlow()
- ))
+ startFlow(),
+ startFlow(),
+ startFlow())
+ )
private val manager = User("manager", "test", permissions = setOf(
startFlow(),
startFlow(),
@@ -53,18 +57,15 @@ class ExplorerSimulation(private val options: OptionSet) {
private val issuers = HashMap()
private val parties = ArrayList>()
- init {
- startDemoNodes()
- }
-
private fun onEnd() {
println("Closing RPC connections")
RPCConnections.forEach { it.close() }
}
- private fun startDemoNodes() {
+ fun startDemoNodes() {
val portAllocation = PortAllocation.Incremental(20000)
- driver(portAllocation = portAllocation, extraCordappPackagesToScan = listOf("net.corda.finance"), waitForAllNodesToFinish = true) {
+ driver(portAllocation = portAllocation, extraCordappPackagesToScan = listOf("net.corda.finance", IOUFlow::class.java.`package`.name),
+ isDebug = true, waitForAllNodesToFinish = true) {
// TODO : Supported flow should be exposed somehow from the node instead of set of ServiceInfo.
val alice = startNode(providedName = ALICE.name, rpcUsers = listOf(user))
val bob = startNode(providedName = BOB.name, rpcUsers = listOf(user))
diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/Main.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/Main.kt
index 0bacc9cb99..3c5237515e 100644
--- a/tools/explorer/src/main/kotlin/net/corda/explorer/Main.kt
+++ b/tools/explorer/src/main/kotlin/net/corda/explorer/Main.kt
@@ -16,6 +16,7 @@ import net.corda.explorer.model.CordaViewModel
import net.corda.explorer.model.SettingsModel
import net.corda.explorer.views.*
import net.corda.explorer.views.cordapps.cash.CashViewer
+import net.corda.explorer.views.cordapps.iou.IOUViewer
import org.apache.commons.lang.SystemUtils
import org.controlsfx.dialog.ExceptionDialog
import tornadofx.App
@@ -104,6 +105,7 @@ class Main : App(MainView::class) {
registerView()
// CordApps Views.
registerView()
+ registerView()
// Tools.
registerView()
registerView()
@@ -128,5 +130,5 @@ class Main : App(MainView::class) {
fun main(args: Array) {
val parser = OptionParser("SF")
val options = parser.parse(*args)
- ExplorerSimulation(options)
+ ExplorerSimulation(options).startDemoNodes()
}
diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/model/MembershipListModel.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/model/MembershipListModel.kt
new file mode 100644
index 0000000000..0c39bfe950
--- /dev/null
+++ b/tools/explorer/src/main/kotlin/net/corda/explorer/model/MembershipListModel.kt
@@ -0,0 +1,22 @@
+package net.corda.explorer.model
+
+import javafx.collections.FXCollections
+import javafx.collections.ObservableList
+import net.corda.client.jfx.model.NodeMonitorModel
+import net.corda.client.jfx.model.observableValue
+import net.corda.client.jfx.utils.ChosenList
+import net.corda.client.jfx.utils.map
+import net.corda.core.identity.AbstractParty
+import net.corda.core.messaging.startFlow
+import net.corda.core.utilities.getOrThrow
+import net.corda.sample.businessnetwork.IOUFlow
+import net.corda.sample.businessnetwork.membership.ObtainMembershipListContentFlow
+
+class MembershipListModel {
+ private val proxy by observableValue(NodeMonitorModel::proxyObservable)
+ private val members = proxy.map { it?.startFlow(::ObtainMembershipListContentFlow, IOUFlow.allowedMembershipName)?.returnValue?.getOrThrow() }
+ private val observableValueOfParties = members.map {
+ FXCollections.observableList(it?.toList() ?: emptyList())
+ }
+ val allParties: ObservableList = ChosenList(observableValueOfParties)
+}
\ No newline at end of file
diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/views/TransactionViewer.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/views/TransactionViewer.kt
index 128c2f221b..8d75aec165 100644
--- a/tools/explorer/src/main/kotlin/net/corda/explorer/views/TransactionViewer.kt
+++ b/tools/explorer/src/main/kotlin/net/corda/explorer/views/TransactionViewer.kt
@@ -14,6 +14,7 @@ import javafx.scene.control.ListView
import javafx.scene.control.TableView
import javafx.scene.control.TitledPane
import javafx.scene.layout.BorderPane
+import javafx.scene.layout.Pane
import javafx.scene.layout.VBox
import net.corda.client.jfx.model.*
import net.corda.client.jfx.utils.filterNotNull
@@ -27,9 +28,11 @@ import net.corda.core.identity.AbstractParty
import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.Party
import net.corda.core.utilities.toBase58String
+import net.corda.sample.businessnetwork.IOUState
import net.corda.explorer.AmountDiff
import net.corda.explorer.formatters.AmountFormatter
import net.corda.explorer.formatters.Formatter
+import net.corda.explorer.formatters.NumberFormatter
import net.corda.explorer.formatters.PartyNameFormatter
import net.corda.explorer.identicon.identicon
import net.corda.explorer.identicon.identiconToolTip
@@ -290,6 +293,25 @@ class TransactionViewer : CordaView("Transactions") {
}
}
}
+ is IOUState -> {
+ fun Pane.partyLabel(party: Party) = label(party.nameOrNull().let { PartyNameFormatter.short.format(it) } ?: "Anonymous") {
+ tooltip(party.owningKey.toBase58String())
+ }
+ row {
+ label("Amount :") { gridpaneConstraints { hAlignment = HPos.RIGHT } }
+ label(NumberFormatter.boring.format(data.value))
+ }
+ row {
+ label("Borrower :") { gridpaneConstraints { hAlignment = HPos.RIGHT } }
+ val party = data.borrower
+ partyLabel(party)
+ }
+ row {
+ label("Lender :") { gridpaneConstraints { hAlignment = HPos.RIGHT } }
+ val party = data.lender
+ partyLabel(party)
+ }
+ }
// TODO : Generic view using reflection?
else -> label {}
}
@@ -309,11 +331,15 @@ private fun calculateTotalEquiv(myIdentity: Party?,
inputs: List,
outputs: List): AmountDiff {
val (reportingCurrency, exchange) = reportingCurrencyExchange
- fun List.sum() = this.map { it as? Cash.State }
- .filterNotNull()
- .filter { it.owner.owningKey.toKnownParty().value == myIdentity }
- .map { exchange(it.amount.withoutIssuer()).quantity }
- .sum()
+ fun List.sum(): Long {
+ val cashSum: Long = map { it as? Cash.State }
+ .filterNotNull()
+ .filter { it.owner.owningKey.toKnownParty().value == myIdentity }
+ .map { exchange(it.amount.withoutIssuer()).quantity }
+ .sum()
+ val iouSum: Int = mapNotNull {it as? IOUState }.map { it.value }.sum() * 100
+ return cashSum + iouSum
+ }
// For issuing cash, if I am the issuer and not the owner (e.g. issuing cash to other party), count it as negative.
val issuedAmount = if (inputs.isEmpty()) outputs.map { it as? Cash.State }
diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/views/cordapps/iou/IOUViewer.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/views/cordapps/iou/IOUViewer.kt
new file mode 100644
index 0000000000..689c314582
--- /dev/null
+++ b/tools/explorer/src/main/kotlin/net/corda/explorer/views/cordapps/iou/IOUViewer.kt
@@ -0,0 +1,27 @@
+package net.corda.explorer.views.cordapps.iou
+
+import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon
+import de.jensd.fx.glyphs.fontawesome.FontAwesomeIconView
+import javafx.scene.input.MouseButton
+import javafx.scene.layout.BorderPane
+import net.corda.explorer.model.CordaView
+import tornadofx.*
+
+class IOUViewer : CordaView("IOU") {
+ // Inject UI elements.
+ override val root: BorderPane by fxml()
+ override val icon: FontAwesomeIcon = FontAwesomeIcon.CHEVRON_CIRCLE_RIGHT
+
+ // Wire up UI
+ init {
+ root.top = hbox(5.0) {
+ button("New Transaction", FontAwesomeIconView(FontAwesomeIcon.PLUS)) {
+ setOnMouseClicked {
+ if (it.button == MouseButton.PRIMARY) {
+ find().show(this@IOUViewer.root.scene.window)
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/views/cordapps/iou/NewTransaction.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/views/cordapps/iou/NewTransaction.kt
new file mode 100644
index 0000000000..b675b8d2a5
--- /dev/null
+++ b/tools/explorer/src/main/kotlin/net/corda/explorer/views/cordapps/iou/NewTransaction.kt
@@ -0,0 +1,139 @@
+package net.corda.explorer.views.cordapps.iou
+
+import com.google.common.base.Splitter
+import javafx.beans.binding.Bindings
+import javafx.beans.binding.BooleanBinding
+import javafx.collections.FXCollections
+import javafx.geometry.Insets
+import javafx.geometry.VPos
+import javafx.scene.control.*
+import javafx.scene.layout.GridPane
+import javafx.scene.text.Font
+import javafx.scene.text.FontWeight
+import javafx.stage.Window
+import net.corda.client.jfx.model.*
+import net.corda.client.jfx.utils.isNotNull
+import net.corda.client.jfx.utils.map
+import net.corda.core.flows.FlowException
+import net.corda.core.identity.Party
+import net.corda.core.identity.PartyAndCertificate
+import net.corda.core.messaging.FlowHandle
+import net.corda.core.messaging.startFlow
+import net.corda.core.transactions.SignedTransaction
+import net.corda.core.utilities.getOrThrow
+import net.corda.sample.businessnetwork.IOUFlow
+import net.corda.explorer.formatters.PartyNameFormatter
+import net.corda.explorer.model.MembershipListModel
+import net.corda.explorer.views.bigDecimalFormatter
+import net.corda.explorer.views.stringConverter
+import net.corda.testing.chooseIdentityAndCert
+import org.controlsfx.dialog.ExceptionDialog
+import tornadofx.*
+
+class NewTransaction : Fragment() {
+ override val root by fxml()
+ // Components
+ private val partyATextField by fxid()
+ private val partyBChoiceBox by fxid>()
+ private val amountTextField by fxid()
+ // Inject data
+ private val parties by observableList(NetworkIdentityModel::parties)
+ private val rpcProxy by observableValue(NodeMonitorModel::proxyObservable)
+ private val myIdentity by observableValue(NetworkIdentityModel::myIdentity)
+ private val notaries by observableList(NetworkIdentityModel::notaries)
+ private val executeButton = ButtonType("Execute", ButtonBar.ButtonData.APPLY)
+
+ fun show(window: Window) {
+
+ // Every time re-query from the server side
+ val elementsFromServer = MembershipListModel().allParties
+
+ partyBChoiceBox.apply {
+ items = FXCollections.observableList(parties.map { it.chooseIdentityAndCert() }).filtered { elementsFromServer.contains(it.party) }.sorted()
+ }
+
+ newTransactionDialog(window).showAndWait().ifPresent { request ->
+ val dialog = Alert(Alert.AlertType.INFORMATION).apply {
+ headerText = null
+ contentText = "Transaction Started."
+ dialogPane.isDisable = true
+ initOwner(window)
+ show()
+ }
+ val handle: FlowHandle = rpcProxy.value!!.startFlow(::IOUFlow, request.first, request.second)
+ runAsync {
+ try {
+ handle.returnValue.getOrThrow()
+ } finally {
+ dialog.dialogPane.isDisable = false
+ }
+ }.ui {
+ val stx: SignedTransaction = it
+ val type = "IOU posting completed successfully"
+ dialog.alertType = Alert.AlertType.INFORMATION
+ dialog.dialogPane.content = gridpane {
+ padding = Insets(10.0, 40.0, 10.0, 20.0)
+ vgap = 10.0
+ hgap = 10.0
+ row { label(type) { font = Font.font(font.family, FontWeight.EXTRA_BOLD, font.size + 2) } }
+ row {
+ label("Transaction ID :") { GridPane.setValignment(this, VPos.TOP) }
+ label { text = Splitter.fixedLength(16).split("${stx.id}").joinToString("\n") }
+ }
+ }
+ dialog.dialogPane.scene.window.sizeToScene()
+ }.setOnFailed {
+ val ex = it.source.exception
+ when (ex) {
+ is FlowException -> {
+ dialog.alertType = Alert.AlertType.ERROR
+ dialog.contentText = ex.message
+ }
+ else -> {
+ dialog.close()
+ ExceptionDialog(ex).apply { initOwner(window) }.showAndWait()
+ }
+ }
+ }
+ }
+ }
+
+ private fun newTransactionDialog(window: Window) = Dialog>().apply {
+ dialogPane = root
+ initOwner(window)
+ setResultConverter {
+ when (it) {
+ executeButton -> Pair(amountTextField.text.toInt(), partyBChoiceBox.value.party)
+ else -> null
+ }
+ }
+ }
+
+ init {
+ // Disable everything when not connected to node.
+ val notariesNotNullBinding = Bindings.createBooleanBinding({ notaries.isNotEmpty() }, arrayOf(notaries))
+ val enableProperty = myIdentity.isNotNull().and(rpcProxy.isNotNull()).and(notariesNotNullBinding)
+ root.disableProperty().bind(enableProperty.not())
+
+ // Party A text field always display my identity name, not editable.
+ partyATextField.textProperty().bind(myIdentity.map { it?.let { PartyNameFormatter.short.format(it.name) } ?: "" })
+
+ // Party B
+ partyBChoiceBox.apply {
+ converter = stringConverter { it?.let { PartyNameFormatter.short.format(it.name) } ?: "" }
+ }
+ // Amount
+ amountTextField.textFormatter = bigDecimalFormatter()
+
+ // Validate inputs.
+ val formValidCondition = arrayOf(
+ myIdentity.isNotNull(),
+ partyBChoiceBox.visibleProperty().not().or(partyBChoiceBox.valueProperty().isNotNull),
+ amountTextField.textProperty().isNotEmpty
+ ).reduce(BooleanBinding::and)
+
+ // Enable execute button when form is valid.
+ root.buttonTypes.add(executeButton)
+ root.lookupButton(executeButton).disableProperty().bind(formValidCondition.not())
+ }
+}
\ No newline at end of file
diff --git a/tools/explorer/src/main/resources/net/corda/explorer/views/cordapps/iou/IOUViewer.fxml b/tools/explorer/src/main/resources/net/corda/explorer/views/cordapps/iou/IOUViewer.fxml
new file mode 100644
index 0000000000..07d11be69d
--- /dev/null
+++ b/tools/explorer/src/main/resources/net/corda/explorer/views/cordapps/iou/IOUViewer.fxml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/tools/explorer/src/main/resources/net/corda/explorer/views/cordapps/iou/NewTransaction.fxml b/tools/explorer/src/main/resources/net/corda/explorer/views/cordapps/iou/NewTransaction.fxml
new file mode 100644
index 0000000000..45d10e2989
--- /dev/null
+++ b/tools/explorer/src/main/resources/net/corda/explorer/views/cordapps/iou/NewTransaction.fxml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tools/jmeter/src/main/kotlin/com/r3/corda/jmeter/Launcher.kt b/tools/jmeter/src/main/kotlin/com/r3/corda/jmeter/Launcher.kt
index b11d1d9f0d..59e9ce5d92 100644
--- a/tools/jmeter/src/main/kotlin/com/r3/corda/jmeter/Launcher.kt
+++ b/tools/jmeter/src/main/kotlin/com/r3/corda/jmeter/Launcher.kt
@@ -51,6 +51,9 @@ class Launcher {
}
jmeter.start(arrayOf("-s", "-p", (capsuleDirPath / "jmeter.properties").toString()) + extraArgs + args)
} else {
+ val searchPath = Files.readAllLines(Paths.get(System.getProperty("search_paths_file"))).first()
+ logger.info("search_paths = $searchPath")
+ System.setProperty("search_paths", searchPath)
jmeter.start(maybeOpenSshTunnels(args))
}
}