Visualiser: reorganise, make it easier to invoke, document it.

This commit is contained in:
Mike Hearn 2015-12-03 11:54:17 +00:00
parent 2193dced0a
commit 58c526c68b
16 changed files with 181 additions and 153 deletions

View File

@ -26,5 +26,6 @@ Read on to learn:
overview
getting-set-up
tutorial
visualiser
roadmap

View File

@ -524,8 +524,13 @@ As this is JUnit, we must remember to annotate each test method with @Test. Let'
We are trying to check that it's not possible for just anyone to issue commercial paper in MegaCorp's name. That would
be bad!
The ``transactionGroup`` function works the same way as the ``requireThat`` construct above. It is an example of what
Kotlin calls a type safe builder, which you can read about in `the documentation for builders <https://kotlinlang.org/docs/reference/type-safe-builders.html>`_.
The ``transactionGroup`` function works the same way as the ``requireThat`` construct above.
.. note:: This DSL is an example of what Kotlin calls a type safe builder, which you can read about in `the
documentation for builders <https://kotlinlang.org/docs/reference/type-safe-builders.html>`_. You can mix and match
ordinary code inside such DSLs so please read the linked page to make sure you fully understand what they are capable
of.
The code block that follows it is run in the scope of a freshly created ``TransactionGroupForTest`` object, which assists
you with building little transaction graphs and verifying them as a whole. Here, our "group" only actually has a
single transaction in it, with a single output, no inputs, and an Issue command signed by ``DUMMY_PUBKEY_1`` which is just

View File

@ -85,6 +85,7 @@
<li class="toctree-l1"><a class="reference internal" href="overview.html">Overview</a></li>
<li class="toctree-l1"><a class="reference internal" href="getting-set-up.html">Getting set up</a></li>
<li class="toctree-l1"><a class="reference internal" href="tutorial.html">Tutorial</a></li>
<li class="toctree-l1"><a class="reference internal" href="visualiser.html">Using the visualiser</a></li>
<li class="toctree-l1"><a class="reference internal" href="roadmap.html">Roadmap</a></li>
</ul>

View File

@ -85,6 +85,7 @@
<li class="toctree-l1"><a class="reference internal" href="overview.html">Overview</a></li>
<li class="toctree-l1"><a class="reference internal" href="getting-set-up.html">Getting set up</a></li>
<li class="toctree-l1"><a class="reference internal" href="tutorial.html">Tutorial</a></li>
<li class="toctree-l1"><a class="reference internal" href="visualiser.html">Using the visualiser</a></li>
<li class="toctree-l1"><a class="reference internal" href="roadmap.html">Roadmap</a></li>
</ul>
@ -170,6 +171,7 @@ prove or disprove the following hypothesis:</p>
<li class="toctree-l2"><a class="reference internal" href="tutorial.html#non-asset-oriented-based-smart-contracts">Non-asset-oriented based smart contracts</a></li>
</ul>
</li>
<li class="toctree-l1"><a class="reference internal" href="visualiser.html">Using the visualiser</a></li>
<li class="toctree-l1"><a class="reference internal" href="roadmap.html">Roadmap</a></li>
</ul>
</div>

View File

@ -84,6 +84,7 @@
<li class="toctree-l1"><a class="reference internal" href="overview.html">Overview</a></li>
<li class="toctree-l1"><a class="reference internal" href="getting-set-up.html">Getting set up</a></li>
<li class="toctree-l1"><a class="reference internal" href="tutorial.html">Tutorial</a></li>
<li class="toctree-l1"><a class="reference internal" href="visualiser.html">Using the visualiser</a></li>
<li class="toctree-l1"><a class="reference internal" href="roadmap.html">Roadmap</a></li>
</ul>

File diff suppressed because one or more lines are too long

View File

@ -31,7 +31,7 @@
<link rel="top" title="R3 Prototyping 0.1 documentation" href="index.html"/>
<link rel="next" title="Roadmap" href="roadmap.html"/>
<link rel="next" title="Using the visualiser" href="visualiser.html"/>
<link rel="prev" title="Getting set up" href="getting-set-up.html"/>
@ -97,6 +97,7 @@
<li class="toctree-l2"><a class="reference internal" href="#non-asset-oriented-based-smart-contracts">Non-asset-oriented based smart contracts</a></li>
</ul>
</li>
<li class="toctree-l1"><a class="reference internal" href="visualiser.html">Using the visualiser</a></li>
<li class="toctree-l1"><a class="reference internal" href="roadmap.html">Roadmap</a></li>
</ul>
@ -608,9 +609,15 @@ and returns a <code class="docutils literal"><span class="pre">java.time.Duratio
<p>As this is JUnit, we must remember to annotate each test method with &#64;Test. Let&#8217;s examine the contents of the first test.
We are trying to check that it&#8217;s not possible for just anyone to issue commercial paper in MegaCorp&#8217;s name. That would
be bad!</p>
<p>The <code class="docutils literal"><span class="pre">transactionGroup</span></code> function works the same way as the <code class="docutils literal"><span class="pre">requireThat</span></code> construct above. It is an example of what
Kotlin calls a type safe builder, which you can read about in <a class="reference external" href="https://kotlinlang.org/docs/reference/type-safe-builders.html">the documentation for builders</a>.
The code block that follows it is run in the scope of a freshly created <code class="docutils literal"><span class="pre">TransactionGroupForTest</span></code> object, which assists
<p>The <code class="docutils literal"><span class="pre">transactionGroup</span></code> function works the same way as the <code class="docutils literal"><span class="pre">requireThat</span></code> construct above.</p>
<div class="admonition note">
<p class="first admonition-title">Note</p>
<p class="last">This DSL is an example of what Kotlin calls a type safe builder, which you can read about in <a class="reference external" href="https://kotlinlang.org/docs/reference/type-safe-builders.html">the
documentation for builders</a>. You can mix and match
ordinary code inside such DSLs so please read the linked page to make sure you fully understand what they are capable
of.</p>
</div>
<p>The code block that follows it is run in the scope of a freshly created <code class="docutils literal"><span class="pre">TransactionGroupForTest</span></code> object, which assists
you with building little transaction graphs and verifying them as a whole. Here, our &#8220;group&#8221; only actually has a
single transaction in it, with a single output, no inputs, and an Issue command signed by <code class="docutils literal"><span class="pre">DUMMY_PUBKEY_1</span></code> which is just
an arbitrary public key. As the paper claims to be issued by <code class="docutils literal"><span class="pre">MEGA_CORP</span></code>, this doesn&#8217;t match and should cause a
@ -850,7 +857,7 @@ be implemented once in a separate contract, with the controlling data being held
<div class="rst-footer-buttons" role="navigation" aria-label="footer navigation">
<a href="roadmap.html" class="btn btn-neutral float-right" title="Roadmap" accesskey="n">Next <span class="fa fa-arrow-circle-right"></span></a>
<a href="visualiser.html" class="btn btn-neutral float-right" title="Using the visualiser" accesskey="n">Next <span class="fa fa-arrow-circle-right"></span></a>
<a href="getting-set-up.html" class="btn btn-neutral" title="Getting set up" accesskey="p"><span class="fa fa-arrow-circle-left"></span> Previous</a>

View File

@ -26,5 +26,6 @@ Read on to learn:
overview
getting-set-up
tutorial
visualiser
roadmap

View File

@ -524,8 +524,13 @@ As this is JUnit, we must remember to annotate each test method with @Test. Let'
We are trying to check that it's not possible for just anyone to issue commercial paper in MegaCorp's name. That would
be bad!
The ``transactionGroup`` function works the same way as the ``requireThat`` construct above. It is an example of what
Kotlin calls a type safe builder, which you can read about in `the documentation for builders <https://kotlinlang.org/docs/reference/type-safe-builders.html>`_.
The ``transactionGroup`` function works the same way as the ``requireThat`` construct above.
.. note:: This DSL is an example of what Kotlin calls a type safe builder, which you can read about in `the
documentation for builders <https://kotlinlang.org/docs/reference/type-safe-builders.html>`_. You can mix and match
ordinary code inside such DSLs so please read the linked page to make sure you fully understand what they are capable
of.
The code block that follows it is run in the scope of a freshly created ``TransactionGroupForTest`` object, which assists
you with building little transaction graphs and verifying them as a whole. Here, our "group" only actually has a
single transaction in it, with a single output, no inputs, and an Issue command signed by ``DUMMY_PUBKEY_1`` which is just

BIN
docs/source/visualiser.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

View File

@ -0,0 +1,78 @@
.. highlight:: kotlin
.. raw:: html
<script type="text/javascript" src="_static/jquery.js"></script>
<script type="text/javascript" src="_static/codesets.js"></script>
Using the visualiser
====================
In order to assist with understanding of the state model, the repository includes a simple graph visualiser. The
visualiser is integrated with the unit test framework and the same domain specific language. It is currently very
early and the diagrams it produces are not especially beautiful. The intention is to improve it in future releases.
.. image:: visualiser.png
An example of how to use it can be seen in ``src/test/kotlin/contracts/CommercialPaperTests.kt``.
Briefly, define a set of transactions in a group using the same DSL that is used in the unit tests. Here's an example
of a trade lifecycle using the commercial paper contract
.. container:: codeset
.. sourcecode:: kotlin
val group: TransactionGroupDSL<ContractState> = transactionGroupFor() {
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() }
}
}
Now you can define a main method in your unit test class that takes the ``TransactionGroupDSL`` object and uses it:
.. container:: codeset
.. sourcecode:: kotlin
CommercialPaperTests().trade().visualise()
This will open up a window with the following features:
* The nodes can be dragged around to try and obtain a better layout (an improved layout algorithm will be a future
feature).
* States are rendered as circles. Transactions are small blue squares. Commands are small diamonds.
* Clicking a state will open up a window that shows its fields.

View File

@ -95,51 +95,6 @@ class CommercialPaperTests {
trade(destroyPaperAtRedemption = false).expectFailureOfTx(3, "must be destroyed")
}
// Generate a trade lifecycle with various parameters.
private fun trade(redemptionTime: Instant = TEST_TX_TIME + 8.days,
aliceGetsBack: Amount = 1000.DOLLARS,
destroyPaperAtRedemption: Boolean = true): TransactionGroupDSL<CommercialPaper.State> {
val someProfits = 1200.DOLLARS
return transactionGroupFor() {
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 {
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 {
input("paper")
input("alice's $900")
output { 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(time = redemptionTime) {
input("alice's paper")
input("some profits")
output { aliceGetsBack.CASH `owned by` ALICE }
output { (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() }
}
}
}
fun cashOutputsToWallet(vararg states: Cash.State): Pair<LedgerTransaction, List<StateAndRef<Cash.State>>> {
val ltx = LedgerTransaction(emptyList(), listOf(*states), emptyList(), TEST_TX_TIME, SecureHash.randomSHA256())
return Pair(ltx, states.mapIndexed { index, state -> StateAndRef(state, ContractStateRef(ltx.hash, index)) })
@ -196,4 +151,53 @@ class CommercialPaperTests {
TransactionGroup(setOf(issueTX, moveTX, validRedemption), setOf(corpWalletTX, alicesWalletTX)).verify(TEST_PROGRAM_MAP)
}
}
// Generate a trade lifecycle with various parameters.
fun trade(redemptionTime: Instant = TEST_TX_TIME + 8.days,
aliceGetsBack: Amount = 1000.DOLLARS,
destroyPaperAtRedemption: Boolean = true): TransactionGroupDSL<CommercialPaper.State> {
val someProfits = 1200.DOLLARS
return transactionGroupFor() {
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() }
}
}
}
}
fun main(args: Array<String>) {
CommercialPaperTests().trade().visualise()
}

View File

@ -13,6 +13,7 @@ package core.testutils
import com.google.common.io.BaseEncoding
import contracts.*
import core.*
import core.visualiser.GraphVisualiser
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.DataInputStream
@ -320,7 +321,12 @@ class TransactionGroupDSL<T : ContractState>(private val stateType: Class<T>) {
throw AssertionError("Exception should have said '$message' but was actually: ${e.cause.message}", e.cause)
return e
}
fun visualise() {
@Suppress("CAST_NEVER_SUCCEEDS")
GraphVisualiser(this as TransactionGroupDSL<ContractState>).display()
}
}
inline fun <reified T : ContractState> transactionGroupFor(body: TransactionGroupDSL<T>.() -> Unit) = TransactionGroupDSL<T>(T::class.java).apply { this.body() }
fun transactionGroup(body: TransactionGroupDSL<ContractState>.() -> Unit) = TransactionGroupDSL(ContractState::class.java).apply { this.body() }
fun transactionGroup(body: TransactionGroupDSL<ContractState>.() -> Unit) = TransactionGroupDSL(ContractState::class.java).apply { this.body() }

View File

@ -1,86 +0,0 @@
/*
* Copyright 2015 Distributed Ledger Group LLC. Distributed as Licensed Company IP to DLG Group Members
* pursuant to the August 7, 2015 Advisory Services Agreement and subject to the Company IP License terms
* set forth therein.
*
* All other 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)
}
})
}

View File

@ -15,10 +15,11 @@ import core.testutils.TransactionGroupDSL
import org.graphstream.graph.Edge
import org.graphstream.graph.Node
import org.graphstream.graph.implementations.SingleGraph
import kotlin.reflect.memberProperties
class GraphConverter(val dsl: TransactionGroupDSL<in ContractState>) {
class GraphVisualiser(val dsl: TransactionGroupDSL<in ContractState>) {
companion object {
val css = GraphConverter::class.java.getResourceAsStream("graph.css").bufferedReader().readText()
val css = GraphVisualiser::class.java.getResourceAsStream("graph.css").bufferedReader().readText()
}
fun convert(): SingleGraph {
@ -69,4 +70,14 @@ class GraphConverter(val dsl: TransactionGroupDSL<in ContractState>) {
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()
fun display() {
runGraph(convert(), 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)
}
})
}
}

View File

@ -1,11 +1,3 @@
/*
* Copyright 2015 Distributed Ledger Group LLC. Distributed as Licensed Company IP to DLG Group Members
* pursuant to the August 7, 2015 Advisory Services Agreement and subject to the Company IP License terms
* set forth therein.
*
* All other rights reserved.
*/
node.tx {
size: 10px;
fill-color: blue;