mirror of
https://github.com/corda/corda.git
synced 2024-12-26 16:11:12 +00:00
R3NET-546: Business Network PoC work (#101)
This commit is contained in:
parent
ca2267f87f
commit
c516a4b028
5
.idea/compiler.xml
generated
5
.idea/compiler.xml
generated
@ -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" />
|
||||
|
49
samples/business-network-demo/build.gradle
Normal file
49
samples/business-network-demo/build.gradle
Normal 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'
|
||||
)
|
||||
}
|
||||
}
|
@ -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)))
|
||||
}
|
||||
}
|
||||
}
|
@ -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))
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
@ -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>
|
||||
}
|
@ -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()
|
||||
}
|
@ -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
|
||||
}
|
@ -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)
|
||||
}
|
@ -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)
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
Party name, Custom Field
|
||||
"C=ES,L=Madrid,O=Alice Corp",UserCustomValue1
|
||||
"C=IT,L=Rome,O=Bob Plc",UserCustomValue2
|
|
@ -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'
|
||||
|
@ -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"
|
||||
|
@ -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))
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
@ -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 }
|
||||
.filterNotNull()
|
||||
.filter { it.owner.owningKey.toKnownParty().value == myIdentity }
|
||||
.map { exchange(it.amount.withoutIssuer()).quantity }
|
||||
.sum()
|
||||
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 }
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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())
|
||||
}
|
||||
}
|
@ -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>
|
@ -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>
|
Loading…
Reference in New Issue
Block a user