R3NET-546: Business Network PoC work (#101)

This commit is contained in:
Viktor Kolomeyko 2017-11-24 09:59:35 +00:00 committed by GitHub
parent ca2267f87f
commit c516a4b028
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 588 additions and 14 deletions

5
.idea/compiler.xml generated
View File

@ -12,6 +12,9 @@
<module name="bank-of-corda-demo_test" target="1.8" />
<module name="buildSrc_main" target="1.8" />
<module name="buildSrc_test" target="1.8" />
<module name="business-network-demo_integrationTest" target="1.8" />
<module name="business-network-demo_main" target="1.8" />
<module name="business-network-demo_test" target="1.8" />
<module name="client_main" target="1.8" />
<module name="client_test" target="1.8" />
<module name="confidential-identities_main" target="1.8" />
@ -115,6 +118,8 @@
<module name="rpc_main" target="1.8" />
<module name="rpc_smokeTest" target="1.8" />
<module name="rpc_test" target="1.8" />
<module name="samples-business-network-demo_main" target="1.8" />
<module name="samples-business-network-demo_test" target="1.8" />
<module name="samples_main" target="1.8" />
<module name="samples_test" target="1.8" />
<module name="sandbox_main" target="1.8" />

View File

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

View File

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

View File

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

View File

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

View File

@ -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<Unit>(), MembershipAware {
override fun call() {
counterParty.checkMembership(membershipName, this)
}
}

View File

@ -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 <T> AbstractParty.checkMembership(membershipName: CordaX500Name, initiatorFlow: FlowLogic<T>) {
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 <T> Party.checkSharesSameMembershipWithUs(membershipName: CordaX500Name, initiatorFlow: FlowLogic<T>) {
initiatorFlow.stateMachine.initiateFlow(this, CheckMembershipFlow(membershipName, initiatorFlow.ourIdentity))
}
}
class MembershipViolationException(msg: String) : FlowException(msg)

View File

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

View File

@ -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<Set<AbstractParty>>(), MembershipAware {
@Suspendable
override fun call(): Set<AbstractParty> = getMembershipList(membershipListName, serviceHub).content()
}

View File

@ -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<AbstractParty> = allParties
}

View File

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

View File

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

View File

@ -0,0 +1,3 @@
Party name, Custom Field
"C=ES,L=Madrid,O=Alice Corp",UserCustomValue1
"C=IT,L=Rome,O=Bob Plc",UserCustomValue2
1 Party name Custom Field
2 C=ES,L=Madrid,O=Alice Corp UserCustomValue1
3 C=IT,L=Rome,O=Bob Plc UserCustomValue2

View File

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

View File

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

View File

@ -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<CashPaymentFlow>(),
startFlow<CashConfigDataFlow>()
))
startFlow<CashConfigDataFlow>(),
startFlow<IOUFlow>(),
startFlow<ObtainMembershipListContentFlow>())
)
private val manager = User("manager", "test", permissions = setOf(
startFlow<CashIssueAndPaymentFlow>(),
startFlow<CashPaymentFlow>(),
@ -53,18 +57,15 @@ class ExplorerSimulation(private val options: OptionSet) {
private val issuers = HashMap<Currency, CordaRPCOps>()
private val parties = ArrayList<Pair<Party, CordaRPCOps>>()
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))

View File

@ -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<StateMachineViewer>()
// CordApps Views.
registerView<CashViewer>()
registerView<IOUViewer>()
// Tools.
registerView<Network>()
registerView<Settings>()
@ -128,5 +130,5 @@ class Main : App(MainView::class) {
fun main(args: Array<String>) {
val parser = OptionParser("SF")
val options = parser.parse(*args)
ExplorerSimulation(options)
ExplorerSimulation(options).startDemoNodes()
}

View File

@ -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<AbstractParty>())
}
val allParties: ObservableList<AbstractParty> = ChosenList(observableValueOfParties)
}

View File

@ -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<ContractState>,
outputs: List<ContractState>): AmountDiff<Currency> {
val (reportingCurrency, exchange) = reportingCurrencyExchange
fun List<ContractState>.sum() = this.map { it as? Cash.State }
fun List<ContractState>.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 }

View File

@ -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<NewTransaction>().show(this@IOUViewer.root.scene.window)
}
}
}
}
}
}

View File

@ -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<DialogPane>()
// Components
private val partyATextField by fxid<TextField>()
private val partyBChoiceBox by fxid<ChoiceBox<PartyAndCertificate>>()
private val amountTextField by fxid<TextField>()
// 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<SignedTransaction> = 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<Pair<Int, Party>>().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())
}
}

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.*?>
<?import javafx.scene.layout.*?>
<BorderPane stylesheets="@../../../css/corda.css" xmlns="http://javafx.com/javafx/8.0.76-ea">
<padding>
<Insets right="5" left="5" bottom="5" top="5"/>
</padding>
</BorderPane>

View File

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<DialogPane stylesheets="@../../../css/corda.css" xmlns="http://javafx.com/javafx/8.0.76-ea"
xmlns:fx="http://javafx.com/fxml/1">
<content>
<GridPane hgap="10" vgap="10">
<!-- Row 0 -->
<Label fx:id="partyALabel" GridPane.halignment="RIGHT" GridPane.rowIndex="1" text="Lender : "/>
<TextField fx:id="partyATextField" GridPane.columnIndex="1" GridPane.columnSpan="4" GridPane.rowIndex="1"
editable="false" disable="true"/>
<!-- Row 1 -->
<Label fx:id="partyBLabel" GridPane.halignment="RIGHT" GridPane.rowIndex="2" text="Borrower : "/>
<ChoiceBox fx:id="partyBChoiceBox" maxWidth="Infinity" GridPane.columnIndex="1" GridPane.columnSpan="4"
GridPane.fillWidth="true" GridPane.hgrow="ALWAYS" GridPane.rowIndex="2"/>
<!-- Row 2 -->
<Label fx:id="amountLabel" text="Amount : " GridPane.halignment="RIGHT" GridPane.rowIndex="5"/>
<TextField fx:id="amountTextField" maxWidth="Infinity" GridPane.columnIndex="1" GridPane.hgrow="ALWAYS"
GridPane.rowIndex="5" GridPane.columnSpan="4"/>
<padding>
<Insets bottom="20.0" left="30.0" right="30.0" top="30.0"/>
</padding>
</GridPane>
</content>
</DialogPane>