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