mirror of
https://github.com/corda/corda.git
synced 2024-12-28 00:38:55 +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="bank-of-corda-demo_test" target="1.8" />
|
||||||
<module name="buildSrc_main" target="1.8" />
|
<module name="buildSrc_main" target="1.8" />
|
||||||
<module name="buildSrc_test" 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_main" target="1.8" />
|
||||||
<module name="client_test" target="1.8" />
|
<module name="client_test" target="1.8" />
|
||||||
<module name="confidential-identities_main" 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_main" target="1.8" />
|
||||||
<module name="rpc_smokeTest" target="1.8" />
|
<module name="rpc_smokeTest" target="1.8" />
|
||||||
<module name="rpc_test" 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_main" target="1.8" />
|
||||||
<module name="samples_test" target="1.8" />
|
<module name="samples_test" target="1.8" />
|
||||||
<module name="sandbox_main" 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:simm-valuation-demo'
|
||||||
include 'samples:notary-demo'
|
include 'samples:notary-demo'
|
||||||
include 'samples:bank-of-corda-demo'
|
include 'samples:bank-of-corda-demo'
|
||||||
|
include 'samples:business-network-demo'
|
||||||
include 'cordform-common'
|
include 'cordform-common'
|
||||||
include 'network-management'
|
include 'network-management'
|
||||||
include 'verify-enclave'
|
include 'verify-enclave'
|
||||||
|
@ -30,6 +30,9 @@ dependencies {
|
|||||||
compile project(':node-driver')
|
compile project(':node-driver')
|
||||||
compile project(':finance')
|
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.
|
// Capsule is a library for building independently executable fat JARs.
|
||||||
// We only need this dependency to compile our Caplet against.
|
// We only need this dependency to compile our Caplet against.
|
||||||
compileOnly "co.paralleluniverse:capsule:$capsule_version"
|
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.messaging.startFlow
|
||||||
import net.corda.core.utilities.OpaqueBytes
|
import net.corda.core.utilities.OpaqueBytes
|
||||||
import net.corda.core.utilities.getOrThrow
|
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.GBP
|
||||||
import net.corda.finance.USD
|
import net.corda.finance.USD
|
||||||
import net.corda.finance.contracts.asset.Cash
|
import net.corda.finance.contracts.asset.Cash
|
||||||
@ -33,8 +35,10 @@ import java.util.*
|
|||||||
class ExplorerSimulation(private val options: OptionSet) {
|
class ExplorerSimulation(private val options: OptionSet) {
|
||||||
private val user = User("user1", "test", permissions = setOf(
|
private val user = User("user1", "test", permissions = setOf(
|
||||||
startFlow<CashPaymentFlow>(),
|
startFlow<CashPaymentFlow>(),
|
||||||
startFlow<CashConfigDataFlow>()
|
startFlow<CashConfigDataFlow>(),
|
||||||
))
|
startFlow<IOUFlow>(),
|
||||||
|
startFlow<ObtainMembershipListContentFlow>())
|
||||||
|
)
|
||||||
private val manager = User("manager", "test", permissions = setOf(
|
private val manager = User("manager", "test", permissions = setOf(
|
||||||
startFlow<CashIssueAndPaymentFlow>(),
|
startFlow<CashIssueAndPaymentFlow>(),
|
||||||
startFlow<CashPaymentFlow>(),
|
startFlow<CashPaymentFlow>(),
|
||||||
@ -53,18 +57,15 @@ class ExplorerSimulation(private val options: OptionSet) {
|
|||||||
private val issuers = HashMap<Currency, CordaRPCOps>()
|
private val issuers = HashMap<Currency, CordaRPCOps>()
|
||||||
private val parties = ArrayList<Pair<Party, CordaRPCOps>>()
|
private val parties = ArrayList<Pair<Party, CordaRPCOps>>()
|
||||||
|
|
||||||
init {
|
|
||||||
startDemoNodes()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onEnd() {
|
private fun onEnd() {
|
||||||
println("Closing RPC connections")
|
println("Closing RPC connections")
|
||||||
RPCConnections.forEach { it.close() }
|
RPCConnections.forEach { it.close() }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startDemoNodes() {
|
fun startDemoNodes() {
|
||||||
val portAllocation = PortAllocation.Incremental(20000)
|
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.
|
// 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 alice = startNode(providedName = ALICE.name, rpcUsers = listOf(user))
|
||||||
val bob = startNode(providedName = BOB.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.model.SettingsModel
|
||||||
import net.corda.explorer.views.*
|
import net.corda.explorer.views.*
|
||||||
import net.corda.explorer.views.cordapps.cash.CashViewer
|
import net.corda.explorer.views.cordapps.cash.CashViewer
|
||||||
|
import net.corda.explorer.views.cordapps.iou.IOUViewer
|
||||||
import org.apache.commons.lang.SystemUtils
|
import org.apache.commons.lang.SystemUtils
|
||||||
import org.controlsfx.dialog.ExceptionDialog
|
import org.controlsfx.dialog.ExceptionDialog
|
||||||
import tornadofx.App
|
import tornadofx.App
|
||||||
@ -104,6 +105,7 @@ class Main : App(MainView::class) {
|
|||||||
registerView<StateMachineViewer>()
|
registerView<StateMachineViewer>()
|
||||||
// CordApps Views.
|
// CordApps Views.
|
||||||
registerView<CashViewer>()
|
registerView<CashViewer>()
|
||||||
|
registerView<IOUViewer>()
|
||||||
// Tools.
|
// Tools.
|
||||||
registerView<Network>()
|
registerView<Network>()
|
||||||
registerView<Settings>()
|
registerView<Settings>()
|
||||||
@ -128,5 +130,5 @@ class Main : App(MainView::class) {
|
|||||||
fun main(args: Array<String>) {
|
fun main(args: Array<String>) {
|
||||||
val parser = OptionParser("SF")
|
val parser = OptionParser("SF")
|
||||||
val options = parser.parse(*args)
|
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.TableView
|
||||||
import javafx.scene.control.TitledPane
|
import javafx.scene.control.TitledPane
|
||||||
import javafx.scene.layout.BorderPane
|
import javafx.scene.layout.BorderPane
|
||||||
|
import javafx.scene.layout.Pane
|
||||||
import javafx.scene.layout.VBox
|
import javafx.scene.layout.VBox
|
||||||
import net.corda.client.jfx.model.*
|
import net.corda.client.jfx.model.*
|
||||||
import net.corda.client.jfx.utils.filterNotNull
|
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.CordaX500Name
|
||||||
import net.corda.core.identity.Party
|
import net.corda.core.identity.Party
|
||||||
import net.corda.core.utilities.toBase58String
|
import net.corda.core.utilities.toBase58String
|
||||||
|
import net.corda.sample.businessnetwork.IOUState
|
||||||
import net.corda.explorer.AmountDiff
|
import net.corda.explorer.AmountDiff
|
||||||
import net.corda.explorer.formatters.AmountFormatter
|
import net.corda.explorer.formatters.AmountFormatter
|
||||||
import net.corda.explorer.formatters.Formatter
|
import net.corda.explorer.formatters.Formatter
|
||||||
|
import net.corda.explorer.formatters.NumberFormatter
|
||||||
import net.corda.explorer.formatters.PartyNameFormatter
|
import net.corda.explorer.formatters.PartyNameFormatter
|
||||||
import net.corda.explorer.identicon.identicon
|
import net.corda.explorer.identicon.identicon
|
||||||
import net.corda.explorer.identicon.identiconToolTip
|
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?
|
// TODO : Generic view using reflection?
|
||||||
else -> label {}
|
else -> label {}
|
||||||
}
|
}
|
||||||
@ -309,11 +331,15 @@ private fun calculateTotalEquiv(myIdentity: Party?,
|
|||||||
inputs: List<ContractState>,
|
inputs: List<ContractState>,
|
||||||
outputs: List<ContractState>): AmountDiff<Currency> {
|
outputs: List<ContractState>): AmountDiff<Currency> {
|
||||||
val (reportingCurrency, exchange) = reportingCurrencyExchange
|
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()
|
.filterNotNull()
|
||||||
.filter { it.owner.owningKey.toKnownParty().value == myIdentity }
|
.filter { it.owner.owningKey.toKnownParty().value == myIdentity }
|
||||||
.map { exchange(it.amount.withoutIssuer()).quantity }
|
.map { exchange(it.amount.withoutIssuer()).quantity }
|
||||||
.sum()
|
.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.
|
// 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 }
|
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