mirror of
https://github.com/corda/corda.git
synced 2024-12-20 05:28:21 +00:00
Implement a simple transaction graph visualiser tool.
This commit is contained in:
parent
31ca78533b
commit
a95cd056ea
@ -33,9 +33,9 @@ dependencies {
|
|||||||
compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
||||||
compile "com.google.guava:guava:18.0"
|
compile "com.google.guava:guava:18.0"
|
||||||
compile "com.esotericsoftware:kryo:3.0.3"
|
compile "com.esotericsoftware:kryo:3.0.3"
|
||||||
}
|
|
||||||
|
|
||||||
sourceSets {
|
// For visualisation
|
||||||
main.java.srcDirs += 'src'
|
compile "org.graphstream:gs-core:1.3"
|
||||||
test.java.srcDirs += 'tests'
|
compile "org.graphstream:gs-ui:1.3"
|
||||||
|
compile "com.intellij:forms_rt:7.0.3"
|
||||||
}
|
}
|
||||||
|
@ -94,7 +94,7 @@ class CommercialPaperTests {
|
|||||||
// Generate a trade lifecycle with various parameters.
|
// Generate a trade lifecycle with various parameters.
|
||||||
private fun trade(redemptionTime: Instant = TEST_TX_TIME + 8.days,
|
private fun trade(redemptionTime: Instant = TEST_TX_TIME + 8.days,
|
||||||
aliceGetsBack: Amount = 1000.DOLLARS,
|
aliceGetsBack: Amount = 1000.DOLLARS,
|
||||||
destroyPaperAtRedemption: Boolean = true): TransactionGroupForTest<CommercialPaper.State> {
|
destroyPaperAtRedemption: Boolean = true): TransactionGroupDSL<CommercialPaper.State> {
|
||||||
val someProfits = 1200.DOLLARS
|
val someProfits = 1200.DOLLARS
|
||||||
return transactionGroupFor() {
|
return transactionGroupFor() {
|
||||||
roots {
|
roots {
|
@ -50,7 +50,7 @@ class CrowdFundTests {
|
|||||||
raiseFunds().verify()
|
raiseFunds().verify()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun raiseFunds(): TransactionGroupForTest<CrowdFund.State> {
|
private fun raiseFunds(): TransactionGroupDSL<CrowdFund.State> {
|
||||||
return transactionGroupFor<CrowdFund.State> {
|
return transactionGroupFor<CrowdFund.State> {
|
||||||
roots {
|
roots {
|
||||||
transaction(1000.DOLLARS.CASH `owned by` ALICE label "alice's $1000")
|
transaction(1000.DOLLARS.CASH `owned by` ALICE label "alice's $1000")
|
||||||
@ -79,7 +79,7 @@ class CrowdFundTests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 3. Close the opportunity, assuming the target has been met
|
// 3. Close the opportunity, assuming the target has been met
|
||||||
transaction(TEST_TX_TIME + 8.days) {
|
transaction(time = TEST_TX_TIME + 8.days) {
|
||||||
input ("pledged opportunity")
|
input ("pledged opportunity")
|
||||||
output ("funded and closed") { "pledged opportunity".output.copy(closed = true) }
|
output ("funded and closed") { "pledged opportunity".output.copy(closed = true) }
|
||||||
arg(MINI_CORP_PUBKEY) { CrowdFund.Commands.Funded() }
|
arg(MINI_CORP_PUBKEY) { CrowdFund.Commands.Funded() }
|
@ -210,8 +210,8 @@ open class TransactionForTest : AbstractTransactionForTest() {
|
|||||||
|
|
||||||
fun transaction(body: TransactionForTest.() -> Unit) = TransactionForTest().apply { body() }
|
fun transaction(body: TransactionForTest.() -> Unit) = TransactionForTest().apply { body() }
|
||||||
|
|
||||||
class TransactionGroupForTest<out T : ContractState>(private val stateType: Class<T>) {
|
class TransactionGroupDSL<T : ContractState>(private val stateType: Class<T>) {
|
||||||
open inner class LedgerTransactionForTest : AbstractTransactionForTest() {
|
open inner class LedgerTransactionDSL : AbstractTransactionForTest() {
|
||||||
private val inStates = ArrayList<ContractStateRef>()
|
private val inStates = ArrayList<ContractStateRef>()
|
||||||
|
|
||||||
fun input(label: String) {
|
fun input(label: String) {
|
||||||
@ -231,7 +231,7 @@ class TransactionGroupForTest<out T : ContractState>(private val stateType: Clas
|
|||||||
|
|
||||||
val String.output: T get() = labelToOutputs[this] ?: throw IllegalArgumentException("State with label '$this' was not found")
|
val String.output: T get() = labelToOutputs[this] ?: throw IllegalArgumentException("State with label '$this' was not found")
|
||||||
|
|
||||||
private inner class InternalLedgerTransactionForTest : LedgerTransactionForTest() {
|
private inner class InternalLedgerTransactionDSL : LedgerTransactionDSL() {
|
||||||
fun finaliseAndInsertLabels(time: Instant): LedgerTransaction {
|
fun finaliseAndInsertLabels(time: Instant): LedgerTransaction {
|
||||||
val ltx = toLedgerTransaction(time)
|
val ltx = toLedgerTransaction(time)
|
||||||
for ((index, labelledState) in outStates.withIndex()) {
|
for ((index, labelledState) in outStates.withIndex()) {
|
||||||
@ -240,6 +240,7 @@ class TransactionGroupForTest<out T : ContractState>(private val stateType: Clas
|
|||||||
if (stateType.isInstance(labelledState.state)) {
|
if (stateType.isInstance(labelledState.state)) {
|
||||||
labelToOutputs[labelledState.label] = labelledState.state as T
|
labelToOutputs[labelledState.label] = labelledState.state as T
|
||||||
}
|
}
|
||||||
|
outputsToLabels[labelledState.state] = labelledState.label
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ltx
|
return ltx
|
||||||
@ -249,34 +250,47 @@ class TransactionGroupForTest<out T : ContractState>(private val stateType: Clas
|
|||||||
private val rootTxns = ArrayList<LedgerTransaction>()
|
private val rootTxns = ArrayList<LedgerTransaction>()
|
||||||
private val labelToRefs = HashMap<String, ContractStateRef>()
|
private val labelToRefs = HashMap<String, ContractStateRef>()
|
||||||
private val labelToOutputs = HashMap<String, T>()
|
private val labelToOutputs = HashMap<String, T>()
|
||||||
|
private val outputsToLabels = HashMap<ContractState, String>()
|
||||||
|
|
||||||
|
fun labelForState(state: T): String? = outputsToLabels[state]
|
||||||
|
|
||||||
inner class Roots {
|
inner class Roots {
|
||||||
fun transaction(vararg outputStates: LabeledOutput) {
|
fun transaction(vararg outputStates: LabeledOutput) {
|
||||||
val outs = outputStates.map { it.state }
|
val outs = outputStates.map { it.state }
|
||||||
val wtx = WireTransaction(emptyList(), outs, emptyList())
|
val wtx = WireTransaction(emptyList(), outs, emptyList())
|
||||||
val ltx = wtx.toLedgerTransaction(TEST_TX_TIME, TEST_KEYS_TO_CORP_MAP, SecureHash.randomSHA256())
|
val ltx = wtx.toLedgerTransaction(TEST_TX_TIME, TEST_KEYS_TO_CORP_MAP, SecureHash.randomSHA256())
|
||||||
outputStates.forEachIndexed { index, labeledOutput -> labelToRefs[labeledOutput.label!!] = ContractStateRef(ltx.hash, index) }
|
for ((index, state) in outputStates.withIndex()) {
|
||||||
|
val label = state.label!!
|
||||||
|
labelToRefs[label] = ContractStateRef(ltx.hash, index)
|
||||||
|
outputsToLabels[state.state] = label
|
||||||
|
}
|
||||||
rootTxns.add(ltx)
|
rootTxns.add(ltx)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Deprecated("Does not nest ", level = DeprecationLevel.ERROR)
|
@Deprecated("Does not nest ", level = DeprecationLevel.ERROR)
|
||||||
fun roots(body: Roots.() -> Unit) {}
|
fun roots(body: Roots.() -> Unit) {}
|
||||||
@Deprecated("Use the vararg form of transaction inside roots", level = DeprecationLevel.ERROR)
|
@Deprecated("Use the vararg form of transaction inside roots", level = DeprecationLevel.ERROR)
|
||||||
fun transaction(time: Instant = TEST_TX_TIME, body: LedgerTransactionForTest.() -> Unit) {}
|
fun transaction(time: Instant = TEST_TX_TIME, body: LedgerTransactionDSL.() -> Unit) {}
|
||||||
}
|
}
|
||||||
fun roots(body: Roots.() -> Unit) = Roots().apply { body() }
|
fun roots(body: Roots.() -> Unit) = Roots().apply { body() }
|
||||||
|
|
||||||
val txns = ArrayList<LedgerTransaction>()
|
val txns = ArrayList<LedgerTransaction>()
|
||||||
|
private val txnToLabelMap = HashMap<LedgerTransaction, String>()
|
||||||
|
|
||||||
fun transaction(time: Instant = TEST_TX_TIME, body: LedgerTransactionForTest.() -> Unit): LedgerTransaction {
|
fun transaction(label: String? = null, time: Instant = TEST_TX_TIME, body: LedgerTransactionDSL.() -> Unit): LedgerTransaction {
|
||||||
val forTest = InternalLedgerTransactionForTest()
|
val forTest = InternalLedgerTransactionDSL()
|
||||||
forTest.body()
|
forTest.body()
|
||||||
val ltx = forTest.finaliseAndInsertLabels(time)
|
val ltx = forTest.finaliseAndInsertLabels(time)
|
||||||
txns.add(ltx)
|
txns.add(ltx)
|
||||||
|
if (label != null)
|
||||||
|
txnToLabelMap[ltx] = label
|
||||||
return ltx
|
return ltx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun labelForTransaction(ltx: LedgerTransaction): String? = txnToLabelMap[ltx]
|
||||||
|
|
||||||
@Deprecated("Does not nest ", level = DeprecationLevel.ERROR)
|
@Deprecated("Does not nest ", level = DeprecationLevel.ERROR)
|
||||||
fun transactionGroup(body: TransactionGroupForTest<T>.() -> Unit) {}
|
fun transactionGroup(body: TransactionGroupDSL<T>.() -> Unit) {}
|
||||||
|
|
||||||
fun toTransactionGroup() = TransactionGroup(txns.map { it }.toSet(), rootTxns.toSet())
|
fun toTransactionGroup() = TransactionGroup(txns.map { it }.toSet(), rootTxns.toSet())
|
||||||
|
|
||||||
@ -304,5 +318,5 @@ class TransactionGroupForTest<out T : ContractState>(private val stateType: Clas
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inline fun <reified T : ContractState> transactionGroupFor(body: TransactionGroupForTest<T>.() -> Unit) = TransactionGroupForTest<T>(T::class.java).apply { this.body() }
|
inline fun <reified T : ContractState> transactionGroupFor(body: TransactionGroupDSL<T>.() -> Unit) = TransactionGroupDSL<T>(T::class.java).apply { this.body() }
|
||||||
fun transactionGroup(body: TransactionGroupForTest<ContractState>.() -> Unit) = TransactionGroupForTest(ContractState::class.java).apply { this.body() }
|
fun transactionGroup(body: TransactionGroupDSL<ContractState>.() -> Unit) = TransactionGroupDSL(ContractState::class.java).apply { this.body() }
|
95
src/test/kotlin/core/visualiser/GraphStream.kt
Normal file
95
src/test/kotlin/core/visualiser/GraphStream.kt
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2015, R3 CEV. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package core.visualiser
|
||||||
|
|
||||||
|
import org.graphstream.graph.Edge
|
||||||
|
import org.graphstream.graph.Element
|
||||||
|
import org.graphstream.graph.Graph
|
||||||
|
import org.graphstream.graph.Node
|
||||||
|
import org.graphstream.graph.implementations.SingleGraph
|
||||||
|
import org.graphstream.ui.layout.Layout
|
||||||
|
import org.graphstream.ui.layout.springbox.implementations.SpringBox
|
||||||
|
import org.graphstream.ui.swingViewer.DefaultView
|
||||||
|
import org.graphstream.ui.view.Viewer
|
||||||
|
import org.graphstream.ui.view.ViewerListener
|
||||||
|
import java.util.*
|
||||||
|
import javax.swing.JFrame
|
||||||
|
import kotlin.reflect.KProperty
|
||||||
|
|
||||||
|
// Some utilities to make the GraphStream API a bit nicer to work with. For some reason GS likes to use a non-type safe
|
||||||
|
// string->value map type API for configuring common things. We fix it up here:
|
||||||
|
|
||||||
|
class GSPropertyDelegate<T>(private val prefix: String) {
|
||||||
|
operator fun getValue(thisRef: Element, property: KProperty<*>): T = thisRef.getAttribute("$prefix.${property.name}")
|
||||||
|
operator fun setValue(thisRef: Element, property: KProperty<*>, value: T) = thisRef.setAttribute("$prefix.${property.name}", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
var Node.label: String by GSPropertyDelegate<String>("ui")
|
||||||
|
var Graph.stylesheet: String by GSPropertyDelegate<String>("ui")
|
||||||
|
var Edge.weight: Double by GSPropertyDelegate<Double>("layout")
|
||||||
|
|
||||||
|
// Do this one by hand as 'class' is a reserved word.
|
||||||
|
var Node.styleClass: String
|
||||||
|
set(value) = setAttribute("ui.class", value)
|
||||||
|
get() = getAttribute("ui.class")
|
||||||
|
|
||||||
|
fun createGraph(name: String, styles: String): SingleGraph {
|
||||||
|
System.setProperty("org.graphstream.ui.renderer", "org.graphstream.ui.j2dviewer.J2DGraphRenderer");
|
||||||
|
return SingleGraph(name).apply {
|
||||||
|
stylesheet = styles
|
||||||
|
setAttribute("ui.quality")
|
||||||
|
setAttribute("ui.antialias")
|
||||||
|
setAttribute("layout.quality", 0)
|
||||||
|
setAttribute("layout.force", 0.9)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MyViewer(graph: Graph) : Viewer(graph, Viewer.ThreadingModel.GRAPH_IN_ANOTHER_THREAD) {
|
||||||
|
override fun enableAutoLayout(layoutAlgorithm: Layout) {
|
||||||
|
super.enableAutoLayout(layoutAlgorithm)
|
||||||
|
|
||||||
|
// Setting shortNap to 1 stops things bouncing around horribly at the start.
|
||||||
|
optLayout.setNaps(50, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun runGraph(graph: SingleGraph, nodeOnClick: (Node) -> Unit) {
|
||||||
|
// Use a bit of custom code here instead of calling graph.display() so we can maximize the window.
|
||||||
|
val viewer = MyViewer(graph)
|
||||||
|
val view: DefaultView = object : DefaultView(viewer, Viewer.DEFAULT_VIEW_ID, Viewer.newGraphRenderer()) {
|
||||||
|
override fun openInAFrame(on: Boolean) {
|
||||||
|
super.openInAFrame(on)
|
||||||
|
if (frame != null) {
|
||||||
|
frame.extendedState = frame.extendedState or JFrame.MAXIMIZED_BOTH
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
viewer.addView(view)
|
||||||
|
|
||||||
|
var loop: Boolean = true
|
||||||
|
val viewerPipe = viewer.newViewerPipe()
|
||||||
|
viewerPipe.addViewerListener(object : ViewerListener {
|
||||||
|
override fun buttonPushed(id: String?) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun buttonReleased(id: String?) {
|
||||||
|
val node = graph.getNode<Node>(id)
|
||||||
|
nodeOnClick(node)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun viewClosed(viewName: String?) {
|
||||||
|
loop = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
view.openInAFrame(true)
|
||||||
|
// Seed determined through trial and error: it gives a reasonable layout for the Wednesday demo.
|
||||||
|
val springBox = SpringBox(false, Random(-103468310429824593L))
|
||||||
|
viewer.enableAutoLayout(springBox)
|
||||||
|
|
||||||
|
while (loop) {
|
||||||
|
viewerPipe.blockingPump()
|
||||||
|
}
|
||||||
|
}
|
82
src/test/kotlin/core/visualiser/GraphStreamVisualiser.kt
Normal file
82
src/test/kotlin/core/visualiser/GraphStreamVisualiser.kt
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2015, R3 CEV. All rights reserved.
|
||||||
|
*/
|
||||||
|
@file:Suppress("CAST_NEVER_SUCCEEDS")
|
||||||
|
|
||||||
|
package core.visualiser
|
||||||
|
|
||||||
|
import contracts.Cash
|
||||||
|
import contracts.CommercialPaper
|
||||||
|
import core.Amount
|
||||||
|
import core.ContractState
|
||||||
|
import core.DOLLARS
|
||||||
|
import core.days
|
||||||
|
import core.testutils.*
|
||||||
|
import java.time.Instant
|
||||||
|
import kotlin.reflect.memberProperties
|
||||||
|
|
||||||
|
|
||||||
|
val PAPER_1 = CommercialPaper.State(
|
||||||
|
issuance = MEGA_CORP.ref(123),
|
||||||
|
owner = MEGA_CORP_PUBKEY,
|
||||||
|
faceValue = 1000.DOLLARS,
|
||||||
|
maturityDate = TEST_TX_TIME + 7.days
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun trade(redemptionTime: Instant = TEST_TX_TIME + 8.days,
|
||||||
|
aliceGetsBack: Amount = 1000.DOLLARS,
|
||||||
|
destroyPaperAtRedemption: Boolean = true): TransactionGroupDSL<ContractState> {
|
||||||
|
val someProfits = 1200.DOLLARS
|
||||||
|
return transactionGroupFor<CommercialPaper.State>() {
|
||||||
|
roots {
|
||||||
|
transaction(900.DOLLARS.CASH `owned by` ALICE label "alice's $900")
|
||||||
|
transaction(someProfits.CASH `owned by` MEGA_CORP_PUBKEY label "some profits")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Some CP is issued onto the ledger by MegaCorp.
|
||||||
|
transaction("Issuance") {
|
||||||
|
output("paper") { PAPER_1 }
|
||||||
|
arg(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() }
|
||||||
|
}
|
||||||
|
|
||||||
|
// The CP is sold to alice for her $900, $100 less than the face value. At 10% interest after only 7 days,
|
||||||
|
// that sounds a bit too good to be true!
|
||||||
|
transaction("Trade") {
|
||||||
|
input("paper")
|
||||||
|
input("alice's $900")
|
||||||
|
output("borrowed $900") { 900.DOLLARS.CASH `owned by` MEGA_CORP_PUBKEY }
|
||||||
|
output("alice's paper") { "paper".output `owned by` ALICE }
|
||||||
|
arg(ALICE) { Cash.Commands.Move() }
|
||||||
|
arg(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move() }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Time passes, and Alice redeem's her CP for $1000, netting a $100 profit. MegaCorp has received $1200
|
||||||
|
// as a single payment from somewhere and uses it to pay Alice off, keeping the remaining $200 as change.
|
||||||
|
transaction("Redemption", redemptionTime) {
|
||||||
|
input("alice's paper")
|
||||||
|
input("some profits")
|
||||||
|
|
||||||
|
output("Alice's profit") { aliceGetsBack.CASH `owned by` ALICE }
|
||||||
|
output("Change") { (someProfits - aliceGetsBack).CASH `owned by` MEGA_CORP_PUBKEY }
|
||||||
|
if (!destroyPaperAtRedemption)
|
||||||
|
output { "paper".output }
|
||||||
|
|
||||||
|
arg(MEGA_CORP_PUBKEY) { Cash.Commands.Move() }
|
||||||
|
arg(ALICE) { CommercialPaper.Commands.Redeem() }
|
||||||
|
}
|
||||||
|
} as TransactionGroupDSL<ContractState>
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun main(args: Array<String>) {
|
||||||
|
val tg = trade()
|
||||||
|
val graph = GraphConverter(tg).convert()
|
||||||
|
runGraph(graph, nodeOnClick = { node ->
|
||||||
|
val state: ContractState? = node.getAttribute("state")
|
||||||
|
if (state != null) {
|
||||||
|
val props: List<Pair<String, Any?>> = state.javaClass.kotlin.memberProperties.map { it.name to it.getter.call(state) }
|
||||||
|
StateViewer.show(props)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
68
src/test/kotlin/core/visualiser/GroupToGraphConversion.kt
Normal file
68
src/test/kotlin/core/visualiser/GroupToGraphConversion.kt
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2015, R3 CEV. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package core.visualiser
|
||||||
|
|
||||||
|
import core.Command
|
||||||
|
import core.ContractState
|
||||||
|
import core.SecureHash
|
||||||
|
import core.testutils.TransactionGroupDSL
|
||||||
|
import org.graphstream.graph.Edge
|
||||||
|
import org.graphstream.graph.Node
|
||||||
|
import org.graphstream.graph.implementations.SingleGraph
|
||||||
|
|
||||||
|
class GraphConverter(val dsl: TransactionGroupDSL<in ContractState>) {
|
||||||
|
companion object {
|
||||||
|
val css = GraphConverter::class.java.getResourceAsStream("graph.css").bufferedReader().readText()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun convert(): SingleGraph {
|
||||||
|
val tg = dsl.toTransactionGroup()
|
||||||
|
val graph = createGraph("Transaction group", css)
|
||||||
|
|
||||||
|
// Map all the transactions, including the bogus non-verified ones (with no inputs) to graph nodes.
|
||||||
|
for ((txIndex, tx) in (tg.transactions + tg.nonVerifiedRoots).withIndex()) {
|
||||||
|
val txNode = graph.addNode<Node>("tx$txIndex")
|
||||||
|
if (tx !in tg.nonVerifiedRoots)
|
||||||
|
txNode.label = dsl.labelForTransaction(tx).let { it ?: "TX ${tx.hash.prefixChars()}" }
|
||||||
|
txNode.styleClass = "tx"
|
||||||
|
|
||||||
|
// Now create a vertex for each output state.
|
||||||
|
for (outIndex in tx.outStates.indices) {
|
||||||
|
val node = graph.addNode<Node>(tx.outRef<ContractState>(outIndex).ref.toString())
|
||||||
|
val state = tx.outStates[outIndex]
|
||||||
|
node.label = stateToLabel(state)
|
||||||
|
node.styleClass = stateToCSSClass(state) + ",state"
|
||||||
|
node.setAttribute("state", state)
|
||||||
|
val edge = graph.addEdge<Edge>("tx$txIndex-out$outIndex", txNode, node, true)
|
||||||
|
edge.weight = 0.7
|
||||||
|
}
|
||||||
|
|
||||||
|
// And a vertex for each command.
|
||||||
|
for ((index, cmd) in tx.commands.withIndex()) {
|
||||||
|
val node = graph.addNode<Node>(SecureHash.randomSHA256().prefixChars())
|
||||||
|
node.label = commandToTypeName(cmd.value)
|
||||||
|
node.styleClass = "command"
|
||||||
|
val edge = graph.addEdge<Edge>("tx$txIndex-cmd-$index", node, txNode)
|
||||||
|
edge.weight = 0.4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// And now all states and transactions were mapped to graph nodes, hook up the input edges.
|
||||||
|
for ((txIndex, tx) in tg.transactions.withIndex()) {
|
||||||
|
for ((inputIndex, ref) in tx.inStateRefs.withIndex()) {
|
||||||
|
val edge = graph.addEdge<Edge>("tx$txIndex-in$inputIndex", ref.toString(), "tx$txIndex", true)
|
||||||
|
edge.weight = 1.2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return graph
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun stateToLabel(state: ContractState): String {
|
||||||
|
return dsl.labelForState(state) ?: stateToTypeName(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun commandToTypeName(state: Command) = state.javaClass.canonicalName.removePrefix("contracts.").replace('$', '.')
|
||||||
|
private fun stateToTypeName(state: ContractState) = state.javaClass.canonicalName.removePrefix("contracts.").removeSuffix(".State")
|
||||||
|
private fun stateToCSSClass(state: ContractState) = stateToTypeName(state).replace('.', '_').toLowerCase()
|
||||||
|
}
|
37
src/test/kotlin/core/visualiser/StateViewer.form
Normal file
37
src/test/kotlin/core/visualiser/StateViewer.form
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<form xmlns="http://www.intellij.com/uidesigner/form/" version="1" bind-to-class="core.visualiser.StateViewer">
|
||||||
|
<grid id="27dc6" binding="root" layout-manager="BorderLayout" hgap="15" vgap="15">
|
||||||
|
<constraints>
|
||||||
|
<xy x="20" y="20" width="500" height="400"/>
|
||||||
|
</constraints>
|
||||||
|
<properties>
|
||||||
|
<background color="-1"/>
|
||||||
|
</properties>
|
||||||
|
<border type="empty">
|
||||||
|
<size top="15" left="15" bottom="15" right="15"/>
|
||||||
|
</border>
|
||||||
|
<children>
|
||||||
|
<component id="c1614" class="javax.swing.JLabel">
|
||||||
|
<constraints border-constraint="North"/>
|
||||||
|
<properties>
|
||||||
|
<font style="1"/>
|
||||||
|
<text value="State viewer"/>
|
||||||
|
</properties>
|
||||||
|
</component>
|
||||||
|
<scrollpane id="2974d">
|
||||||
|
<constraints border-constraint="Center"/>
|
||||||
|
<properties/>
|
||||||
|
<border type="none"/>
|
||||||
|
<children>
|
||||||
|
<component id="8f1af" class="javax.swing.JTable" binding="propsTable">
|
||||||
|
<constraints/>
|
||||||
|
<properties>
|
||||||
|
<autoResizeMode value="3"/>
|
||||||
|
<showHorizontalLines value="false"/>
|
||||||
|
</properties>
|
||||||
|
</component>
|
||||||
|
</children>
|
||||||
|
</scrollpane>
|
||||||
|
</children>
|
||||||
|
</grid>
|
||||||
|
</form>
|
113
src/test/kotlin/core/visualiser/StateViewer.java
Normal file
113
src/test/kotlin/core/visualiser/StateViewer.java
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2015, R3 CEV. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package core.visualiser;
|
||||||
|
|
||||||
|
import kotlin.*;
|
||||||
|
|
||||||
|
import javax.swing.*;
|
||||||
|
import javax.swing.table.*;
|
||||||
|
import java.awt.*;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class StateViewer {
|
||||||
|
private JPanel root;
|
||||||
|
private JTable propsTable;
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
JFrame frame = new JFrame("StateViewer");
|
||||||
|
List<Pair<String, Object>> props = new ArrayList<>();
|
||||||
|
props.add(new Pair<>("a", 123));
|
||||||
|
props.add(new Pair<>("things", "bar"));
|
||||||
|
frame.setContentPane(new StateViewer(props).root);
|
||||||
|
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
|
||||||
|
frame.pack();
|
||||||
|
frame.setVisible(true);
|
||||||
|
frame.setSize(800, 600);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void show(List<Pair<String, Object>> props) {
|
||||||
|
JFrame frame = new JFrame("StateViewer");
|
||||||
|
StateViewer viewer = new StateViewer(props);
|
||||||
|
frame.setContentPane(viewer.root);
|
||||||
|
frame.pack();
|
||||||
|
frame.setSize(600, 300);
|
||||||
|
|
||||||
|
viewer.propsTable.getColumnModel().getColumn(0).setMinWidth(150);
|
||||||
|
viewer.propsTable.getColumnModel().getColumn(0).setMaxWidth(150);
|
||||||
|
|
||||||
|
frame.setVisible(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public StateViewer(List<Pair<String, Object>> props) {
|
||||||
|
propsTable.setModel(new AbstractTableModel() {
|
||||||
|
@Override
|
||||||
|
public int getRowCount() {
|
||||||
|
return props.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getColumnCount() {
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getColumnName(int column) {
|
||||||
|
if (column == 0)
|
||||||
|
return "Attribute";
|
||||||
|
else if (column == 1)
|
||||||
|
return "Value";
|
||||||
|
else
|
||||||
|
return "?";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object getValueAt(int rowIndex, int columnIndex) {
|
||||||
|
if (columnIndex == 0)
|
||||||
|
return props.get(rowIndex).getFirst();
|
||||||
|
else
|
||||||
|
return props.get(rowIndex).getSecond();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// GUI initializer generated by IntelliJ IDEA GUI Designer
|
||||||
|
// >>> IMPORTANT!! <<<
|
||||||
|
// DO NOT EDIT OR ADD ANY CODE HERE!
|
||||||
|
$$$setupUI$$$();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method generated by IntelliJ IDEA GUI Designer
|
||||||
|
* >>> IMPORTANT!! <<<
|
||||||
|
* DO NOT edit this method OR call it in your code!
|
||||||
|
*
|
||||||
|
* @noinspection ALL
|
||||||
|
*/
|
||||||
|
private void $$$setupUI$$$() {
|
||||||
|
root = new JPanel();
|
||||||
|
root.setLayout(new BorderLayout(15, 15));
|
||||||
|
root.setBackground(new Color(-1));
|
||||||
|
root.setBorder(BorderFactory.createTitledBorder(BorderFactory.createEmptyBorder(15, 15, 15, 15), null));
|
||||||
|
final JLabel label1 = new JLabel();
|
||||||
|
label1.setFont(new Font(label1.getFont().getName(), Font.BOLD, label1.getFont().getSize()));
|
||||||
|
label1.setText("State viewer");
|
||||||
|
root.add(label1, BorderLayout.NORTH);
|
||||||
|
final JScrollPane scrollPane1 = new JScrollPane();
|
||||||
|
root.add(scrollPane1, BorderLayout.CENTER);
|
||||||
|
propsTable = new JTable();
|
||||||
|
propsTable.setAutoResizeMode(3);
|
||||||
|
propsTable.setShowHorizontalLines(false);
|
||||||
|
scrollPane1.setViewportView(propsTable);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @noinspection ALL
|
||||||
|
*/
|
||||||
|
public JComponent $$$getRootComponent$$$() {
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
}
|
41
src/test/resources/core/visualiser/graph.css
Normal file
41
src/test/resources/core/visualiser/graph.css
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
node.tx {
|
||||||
|
size: 10px;
|
||||||
|
fill-color: blue;
|
||||||
|
shape: rounded-box;
|
||||||
|
z-index: 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
node.state {
|
||||||
|
size: 25px;
|
||||||
|
fill-color: beige;
|
||||||
|
stroke-width: 2px;
|
||||||
|
stroke-color: black;
|
||||||
|
stroke-mode: plain;
|
||||||
|
}
|
||||||
|
|
||||||
|
node {
|
||||||
|
text-background-mode: rounded-box;
|
||||||
|
text-background-color: darkslategrey;
|
||||||
|
text-padding: 5px;
|
||||||
|
text-offset: 10px;
|
||||||
|
text-color: white;
|
||||||
|
text-alignment: under;
|
||||||
|
text-size: 16;
|
||||||
|
}
|
||||||
|
|
||||||
|
node.command {
|
||||||
|
text-size: 12;
|
||||||
|
size: 8px;
|
||||||
|
fill-color: white;
|
||||||
|
stroke-width: 2px;
|
||||||
|
stroke-color: black;
|
||||||
|
stroke-mode: plain;
|
||||||
|
}
|
||||||
|
|
||||||
|
node.cash {
|
||||||
|
fill-color: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
graph {
|
||||||
|
padding: 100px;
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user