From 598809559d98fe6c77c807d0e0ca67e4e11c57e9 Mon Sep 17 00:00:00 2001 From: Andras Slemmer Date: Tue, 30 Aug 2016 13:05:28 +0100 Subject: [PATCH 1/7] client: Add Generator lib, and generators for client<->service events --- .../kotlin/com/r3corda/client/mock/ErrorOr.kt | 40 +++++ .../com/r3corda/client/mock/EventGenerator.kt | 102 +++++++++++ .../com/r3corda/client/mock/Generator.kt | 167 ++++++++++++++++++ 3 files changed, 309 insertions(+) create mode 100644 client/src/main/kotlin/com/r3corda/client/mock/ErrorOr.kt create mode 100644 client/src/main/kotlin/com/r3corda/client/mock/EventGenerator.kt create mode 100644 client/src/main/kotlin/com/r3corda/client/mock/Generator.kt diff --git a/client/src/main/kotlin/com/r3corda/client/mock/ErrorOr.kt b/client/src/main/kotlin/com/r3corda/client/mock/ErrorOr.kt new file mode 100644 index 0000000000..4d54dcc8cf --- /dev/null +++ b/client/src/main/kotlin/com/r3corda/client/mock/ErrorOr.kt @@ -0,0 +1,40 @@ +package com.r3corda.client.mock + +sealed class ErrorOr { + class Error(val error: String): ErrorOr() + class Success(val value: A): ErrorOr() + + fun getValueOrThrow(): A { + return when (this) { + is ErrorOr.Error -> throw Exception(this.error) + is ErrorOr.Success -> this.value + } + } + + // Functor + fun map(function: (A) -> B): ErrorOr { + return when (this) { + is ErrorOr.Error -> ErrorOr.Error(error) + is ErrorOr.Success -> ErrorOr.Success(function(value)) + } + } + + // Applicative + fun combine(other: ErrorOr, function: (A, B) -> C): ErrorOr { + return when (this) { + is ErrorOr.Error -> ErrorOr.Error(error) + is ErrorOr.Success -> when (other) { + is ErrorOr.Error -> ErrorOr.Error(other.error) + is ErrorOr.Success -> ErrorOr.Success(function(value, other.value)) + } + } + } + + // Monad + fun bind(function: (A) -> ErrorOr): ErrorOr { + return when (this) { + is ErrorOr.Error -> ErrorOr.Error(error) + is ErrorOr.Success -> function(value) + } + } +} diff --git a/client/src/main/kotlin/com/r3corda/client/mock/EventGenerator.kt b/client/src/main/kotlin/com/r3corda/client/mock/EventGenerator.kt new file mode 100644 index 0000000000..480aa17916 --- /dev/null +++ b/client/src/main/kotlin/com/r3corda/client/mock/EventGenerator.kt @@ -0,0 +1,102 @@ +package com.r3corda.client.mock + +import com.r3corda.contracts.asset.Cash +import com.r3corda.core.contracts.* +import com.r3corda.core.crypto.Party +import com.r3corda.core.serialization.OpaqueBytes +import com.r3corda.core.testing.ledger +import com.r3corda.node.services.monitor.ServiceToClientEvent +import java.time.Instant + +/** + * [Generator]s for incoming/outgoing events to/from the [WalletMonitorService]. Internally it keeps track of owned + * state/ref pairs, but it doesn't necessarily generate "correct" events! + */ +class EventGenerator( + val parties: List, + val notary: Party +) { + + private var wallet = listOf>() + + val issuerGenerator = + Generator.pickOne(parties).combine(Generator.intRange(0, 1)) { party, ref -> party.ref(ref.toByte()) } + + val currencies = setOf(USD, GBP, CHF).toList() // + Currency.getAvailableCurrencies().toList().subList(0, 3).toSet()).toList() + val currencyGenerator = Generator.pickOne(currencies) + + val amountIssuedGenerator = + Generator.intRange(1, 10000).combine(issuerGenerator, currencyGenerator) { amount, issuer, currency -> + Amount(amount.toLong(), Issued(issuer, currency)) + } + + val publicKeyGenerator = Generator.oneOf(parties.map { it.owningKey }) + val partyGenerator = Generator.oneOf(parties) + + val cashStateGenerator = amountIssuedGenerator.combine(publicKeyGenerator) { amount, from -> + ledger { + transaction { + output("state", Cash.State(amount, from)) + command(amount.token.issuer.party.owningKey, Cash.Commands.Issue()) + verifies() + } + }.retrieveOutputStateAndRef(Cash.State::class.java, "state") + } + + val consumedGenerator: Generator> = Generator.frequency( + 0.7 to Generator.pure(setOf()), + 0.3 to Generator.impure { wallet }.bind { states -> + Generator.sampleBernoulli(states, 0.2).map { someStates -> + val consumedSet = someStates.map { it.ref }.toSet() + wallet = wallet.filter { it.ref !in consumedSet } + consumedSet + } + } + ) + val producedGenerator: Generator>> = Generator.frequency( +// 0.1 to Generator.pure(setOf()) + 0.9 to Generator.impure { wallet }.bind { states -> + Generator.replicate(2, cashStateGenerator).map { + wallet = states + it + it.toSet() + } + } + ) + + val outputStateGenerator = consumedGenerator.combine(producedGenerator) { consumed, produced -> + ServiceToClientEvent.OutputState(Instant.now(), consumed, produced) + } + + val issueRefGenerator = Generator.intRange(0, 1).map { number -> OpaqueBytes(ByteArray(1, { number.toByte() })) } + + val amountGenerator = Generator.intRange(0, 10000).combine(currencyGenerator) { quantity, currency -> Amount(quantity.toLong(), currency) } + + val issueCashGenerator = + amountGenerator.combine(partyGenerator, issueRefGenerator) { amount, to, issueRef -> + ClientToServiceCommand.IssueCash( + amount, + issueRef, + to, + notary + ) + } + + val moveCashGenerator = + amountIssuedGenerator.combine( + partyGenerator + ) { amountIssued, recipient -> + ClientToServiceCommand.PayCash( + amount = amountIssued, + recipient = recipient + ) + } + + val serviceToClientEventGenerator = Generator.frequency( + 1.0 to outputStateGenerator + ) + + val clientToServiceCommandGenerator = Generator.frequency( + 0.33 to issueCashGenerator, + 0.33 to moveCashGenerator + ) +} diff --git a/client/src/main/kotlin/com/r3corda/client/mock/Generator.kt b/client/src/main/kotlin/com/r3corda/client/mock/Generator.kt new file mode 100644 index 0000000000..c6c235fd32 --- /dev/null +++ b/client/src/main/kotlin/com/r3corda/client/mock/Generator.kt @@ -0,0 +1,167 @@ +package com.r3corda.client.mock + +import java.util.* + +/** + * This file defines a basic [Generator] library for composing random generators of objects. + * + * An object of type [Generator]<[A]> captures a generator of [A]s. Generators may be composed in several ways. + * + * [Generator.choice] picks a generator from the specified list and runs that. + * [Generator.frequency] is similar to [choice] but the probability may be specified for each generator (it is normalised before picking). + * [Generator.combine] combines two generators of A and B with a function (A, B) -> C. Variants exist for other arities. + * [Generator.bind] sequences two generators using an arbitrary A->Generator function. Keep the usage of this + * function minimal as it may explode the stack, especially when using recursion. + * + * There are other utilities as well, the type of which are usually descriptive. + * + * Example: + * val birdNameGenerator = Generator.pickOne(listOf("raven", "pigeon")) + * val birdHeightGenerator = Generator.doubleRange(from = 10.0, to = 30.0) + * val birdGenerator = birdNameGenerator.combine(birdHeightGenerator) { name, height -> Bird(name, height) } + * val birdsGenerator = Generator.replicate(2, birdGenerator) + * val mammalsGenerator = Generator.sampleBernoulli(listOf(Mammal("fox"), Mammal("elephant"))) + * val animalsGenerator = Generator.frequency( + * 0.2 to birdsGenerator, + * 0.8 to mammalsGenerator + * ) + * val animals = animalsGenerator.generate(Random()).getOrThrow() + * + * The above will generate a random list of animals. + */ +class Generator(val generate: (Random) -> ErrorOr) { + + // Functor + fun map(function: (A) -> B): Generator = + Generator { generate(it).map(function) } + + // Applicative + fun product(other: Generator<(A) -> B>) = + Generator { generate(it).combine(other.generate(it)) { a, f -> f(a) } } + fun combine(other1: Generator, function: (A, B) -> R) = + product(other1.product(pure({ b -> { a -> function(a, b) } }))) + fun combine(other1: Generator, other2: Generator, function: (A, B, C) -> R) = + product(other1.product(other2.product(pure({ c -> { b -> { a -> function(a, b, c) } } })))) + fun combine(other1: Generator, other2: Generator, other3: Generator, function: (A, B, C, D) -> R) = + product(other1.product(other2.product(other3.product(pure({ d -> { c -> { b -> { a -> function(a, b, c, d) } } } }))))) + fun combine(other1: Generator, other2: Generator, other3: Generator, other4: Generator, function: (A, B, C, D, E) -> R) = + product(other1.product(other2.product(other3.product(other4.product(pure({ e -> { d -> { c -> { b -> { a -> function(a, b, c, d, e) } } } } })))))) + + // Monad + fun bind(function: (A) -> Generator) = + Generator { generate(it).bind { a -> function(a).generate(it) } } + + companion object { + fun pure(value: A) = Generator { ErrorOr.Success(value) } + fun impure(valueClosure: () -> A) = Generator { ErrorOr.Success(valueClosure()) } + fun fail(error: String) = Generator { ErrorOr.Error(error) } + + // Alternative + fun choice(generators: List>) = intRange(0, generators.size - 1).bind { generators[it] } + + fun success(generate: (Random) -> A) = Generator { ErrorOr.Success(generate(it)) } + fun frequency(vararg generators: Pair>): Generator { + val ranges = mutableListOf>() + var current = 0.0 + generators.forEach { + val next = current + it.first + ranges.add(Pair(current, next)) + current = next + } + return doubleRange(0.0, current).bind { value -> + generators[ranges.binarySearch { range -> + if (value < range.first) { + 1 + } else if (value < range.second) { + 0 + } else { + -1 + } + }].second + } + } + + fun sequence(generators: List>) = Generator> { + val result = mutableListOf() + for (generator in generators) { + val element = generator.generate(it) + when (element) { + is ErrorOr.Error -> return@Generator ErrorOr.Error(element.error) + is ErrorOr.Success -> result.add(element.value) + } + } + ErrorOr.Success(result) + } + } +} + +fun Generator.Companion.oneOf(list: List) = intRange(0, list.size - 1).map { list[it] } + +fun Generator.generateOrFail(random: Random, numberOfTries: Int = 1): A { + var error: String? = null + for (i in 0 .. numberOfTries - 1) { + val result = generate(random) + when (result) { + is ErrorOr.Error -> error = result.error + is ErrorOr.Success -> return result.value + } + } + if (error == null) { + throw IllegalArgumentException("numberOfTries cannot be <= 0") + } else { + throw Exception("Failed to generate, last error $error") + } +} + +fun Generator.Companion.int() = Generator.success { it.nextInt() } +fun Generator.Companion.intRange(from: Int, to: Int): Generator = Generator.success { + (from + Math.abs(it.nextInt()) % (to - from + 1)).toInt() +} +fun Generator.Companion.double() = Generator.success { it.nextDouble() } +fun Generator.Companion.doubleRange(from: Double, to: Double): Generator = Generator.success { + from + it.nextDouble() % (to - from) +} + +fun Generator.Companion.replicate(number: Int, generator: Generator): Generator> { + val generators = mutableListOf>() + for (i in 1 .. number) { + generators.add(generator) + } + return sequence(generators) +} + + +fun Generator.Companion.replicatePoisson(meanSize: Double, generator: Generator) = Generator> { + val chance = (meanSize - 1) / meanSize + val result = mutableListOf() + var finish = false + while (!finish) { + val error = Generator.doubleRange(0.0, 1.0).generate(it).bind { value -> + if (value < chance) { + generator.generate(it).map { result.add(it) } + } else { + finish = true + ErrorOr.Success(Unit) + } + } + if (error is ErrorOr.Error) { + return@Generator ErrorOr.Error(error.error) + } + } + ErrorOr.Success(result) +} + +fun Generator.Companion.pickOne(list: List) = Generator.intRange(0, list.size - 1).map { list[it] } + +fun Generator.Companion.sampleBernoulli(collection: Collection, maxRatio: Double = 1.0): Generator> = + intRange(0, (maxRatio * collection.size).toInt()).bind { howMany -> + replicate(collection.size, Generator.doubleRange(0.0, 1.0)).map { chances -> + val result = mutableListOf() + collection.forEachIndexed { index, element -> + if (chances[index] < howMany.toDouble() / collection.size.toDouble()) { + result.add(element) + } + } + result + } + } From 2a7115318de814239f6e062614b842f9312d6704 Mon Sep 17 00:00:00 2001 From: Andras Slemmer Date: Thu, 1 Sep 2016 11:13:32 +0100 Subject: [PATCH 2/7] client: Fix compile error after rebase --- .../src/main/kotlin/com/r3corda/client/mock/EventGenerator.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/main/kotlin/com/r3corda/client/mock/EventGenerator.kt b/client/src/main/kotlin/com/r3corda/client/mock/EventGenerator.kt index 480aa17916..d3521cd78b 100644 --- a/client/src/main/kotlin/com/r3corda/client/mock/EventGenerator.kt +++ b/client/src/main/kotlin/com/r3corda/client/mock/EventGenerator.kt @@ -4,8 +4,8 @@ import com.r3corda.contracts.asset.Cash import com.r3corda.core.contracts.* import com.r3corda.core.crypto.Party import com.r3corda.core.serialization.OpaqueBytes -import com.r3corda.core.testing.ledger import com.r3corda.node.services.monitor.ServiceToClientEvent +import com.r3corda.testing.ledger import java.time.Instant /** From 6f8d92d3684b4cea581345214eaf93086efcf78f Mon Sep 17 00:00:00 2001 From: Andras Slemmer Date: Fri, 2 Sep 2016 10:30:18 +0100 Subject: [PATCH 3/7] client: Remove test-utils dep from compile --- .../kotlin/com/r3corda/client/mock/EventGenerator.kt | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/client/src/main/kotlin/com/r3corda/client/mock/EventGenerator.kt b/client/src/main/kotlin/com/r3corda/client/mock/EventGenerator.kt index d3521cd78b..3a3a88fcab 100644 --- a/client/src/main/kotlin/com/r3corda/client/mock/EventGenerator.kt +++ b/client/src/main/kotlin/com/r3corda/client/mock/EventGenerator.kt @@ -34,13 +34,10 @@ class EventGenerator( val partyGenerator = Generator.oneOf(parties) val cashStateGenerator = amountIssuedGenerator.combine(publicKeyGenerator) { amount, from -> - ledger { - transaction { - output("state", Cash.State(amount, from)) - command(amount.token.issuer.party.owningKey, Cash.Commands.Issue()) - verifies() - } - }.retrieveOutputStateAndRef(Cash.State::class.java, "state") + val builder = TransactionBuilder() + builder.addOutputState(Cash.State(amount, from)) + builder.addCommand(Command(Cash.Commands.Issue(), amount.token.issuer.party.owningKey)) + builder.toWireTransaction().outRef(0) } val consumedGenerator: Generator> = Generator.frequency( From 69fecf94d159a1056fd63bd49b1a3215b7d18eb9 Mon Sep 17 00:00:00 2001 From: Andras Slemmer Date: Fri, 2 Sep 2016 10:38:00 +0100 Subject: [PATCH 4/7] client: Add varargs convenience sampleBernoulli --- client/src/main/kotlin/com/r3corda/client/mock/Generator.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/src/main/kotlin/com/r3corda/client/mock/Generator.kt b/client/src/main/kotlin/com/r3corda/client/mock/Generator.kt index c6c235fd32..ec27ec654c 100644 --- a/client/src/main/kotlin/com/r3corda/client/mock/Generator.kt +++ b/client/src/main/kotlin/com/r3corda/client/mock/Generator.kt @@ -153,6 +153,8 @@ fun Generator.Companion.replicatePoisson(meanSize: Double, generator: Genera fun Generator.Companion.pickOne(list: List) = Generator.intRange(0, list.size - 1).map { list[it] } +fun Generator.Companion.sampleBernoulli(maxRatio: Double = 1.0, vararg collection: A) = + sampleBernoulli(listOf(collection), maxRatio) fun Generator.Companion.sampleBernoulli(collection: Collection, maxRatio: Double = 1.0): Generator> = intRange(0, (maxRatio * collection.size).toInt()).bind { howMany -> replicate(collection.size, Generator.doubleRange(0.0, 1.0)).map { chances -> From 8b6b9977ec7b31bdde2d3f3727c5c75b753dca9f Mon Sep 17 00:00:00 2001 From: Andras Slemmer Date: Tue, 6 Sep 2016 10:28:31 +0100 Subject: [PATCH 5/7] client: ErrorOrs use nullable values and Exception --- .../kotlin/com/r3corda/client/mock/ErrorOr.kt | 42 +++++++++---------- .../com/r3corda/client/mock/Generator.kt | 38 +++++++++-------- 2 files changed, 41 insertions(+), 39 deletions(-) diff --git a/client/src/main/kotlin/com/r3corda/client/mock/ErrorOr.kt b/client/src/main/kotlin/com/r3corda/client/mock/ErrorOr.kt index 4d54dcc8cf..1dee4278be 100644 --- a/client/src/main/kotlin/com/r3corda/client/mock/ErrorOr.kt +++ b/client/src/main/kotlin/com/r3corda/client/mock/ErrorOr.kt @@ -1,40 +1,40 @@ package com.r3corda.client.mock -sealed class ErrorOr { - class Error(val error: String): ErrorOr() - class Success(val value: A): ErrorOr() +class ErrorOr private constructor( + val value: A?, + val error: Exception? +) { + constructor(value: A): this(value, null) + constructor(error: Exception): this(null, error) + + fun match(onValue: (A) -> T, onError: (Exception) -> T): T { + if (value != null) { + return onValue(value) + } else { + return onError(error!!) + } + } fun getValueOrThrow(): A { - return when (this) { - is ErrorOr.Error -> throw Exception(this.error) - is ErrorOr.Success -> this.value + if (value != null) { + return value + } else { + throw error!! } } // Functor fun map(function: (A) -> B): ErrorOr { - return when (this) { - is ErrorOr.Error -> ErrorOr.Error(error) - is ErrorOr.Success -> ErrorOr.Success(function(value)) - } + return ErrorOr(value?.let(function), error) } // Applicative fun combine(other: ErrorOr, function: (A, B) -> C): ErrorOr { - return when (this) { - is ErrorOr.Error -> ErrorOr.Error(error) - is ErrorOr.Success -> when (other) { - is ErrorOr.Error -> ErrorOr.Error(other.error) - is ErrorOr.Success -> ErrorOr.Success(function(value, other.value)) - } - } + return ErrorOr(value?.let { a -> other.value?.let { b -> function(a, b) } }, error ?: other.error) } // Monad fun bind(function: (A) -> ErrorOr): ErrorOr { - return when (this) { - is ErrorOr.Error -> ErrorOr.Error(error) - is ErrorOr.Success -> function(value) - } + return value?.let(function) ?: ErrorOr(error!!) } } diff --git a/client/src/main/kotlin/com/r3corda/client/mock/Generator.kt b/client/src/main/kotlin/com/r3corda/client/mock/Generator.kt index ec27ec654c..6991dec7a5 100644 --- a/client/src/main/kotlin/com/r3corda/client/mock/Generator.kt +++ b/client/src/main/kotlin/com/r3corda/client/mock/Generator.kt @@ -52,14 +52,14 @@ class Generator(val generate: (Random) -> ErrorOr) { Generator { generate(it).bind { a -> function(a).generate(it) } } companion object { - fun pure(value: A) = Generator { ErrorOr.Success(value) } - fun impure(valueClosure: () -> A) = Generator { ErrorOr.Success(valueClosure()) } - fun fail(error: String) = Generator { ErrorOr.Error(error) } + fun pure(value: A) = Generator { ErrorOr(value) } + fun impure(valueClosure: () -> A) = Generator { ErrorOr(valueClosure()) } + fun fail(error: Exception) = Generator { ErrorOr(error) } // Alternative fun choice(generators: List>) = intRange(0, generators.size - 1).bind { generators[it] } - fun success(generate: (Random) -> A) = Generator { ErrorOr.Success(generate(it)) } + fun success(generate: (Random) -> A) = Generator { ErrorOr(generate(it)) } fun frequency(vararg generators: Pair>): Generator { val ranges = mutableListOf>() var current = 0.0 @@ -85,12 +85,13 @@ class Generator(val generate: (Random) -> ErrorOr) { val result = mutableListOf() for (generator in generators) { val element = generator.generate(it) - when (element) { - is ErrorOr.Error -> return@Generator ErrorOr.Error(element.error) - is ErrorOr.Success -> result.add(element.value) + if (element.value != null) { + result.add(element.value) + } else { + return@Generator ErrorOr(element.error!!) } } - ErrorOr.Success(result) + ErrorOr(result) } } } @@ -98,18 +99,19 @@ class Generator(val generate: (Random) -> ErrorOr) { fun Generator.Companion.oneOf(list: List) = intRange(0, list.size - 1).map { list[it] } fun Generator.generateOrFail(random: Random, numberOfTries: Int = 1): A { - var error: String? = null + var error: Exception? = null for (i in 0 .. numberOfTries - 1) { val result = generate(random) - when (result) { - is ErrorOr.Error -> error = result.error - is ErrorOr.Success -> return result.value + if (result.value != null) { + return result.value + } else { + error = result.error } } if (error == null) { throw IllegalArgumentException("numberOfTries cannot be <= 0") } else { - throw Exception("Failed to generate, last error $error") + throw Exception("Failed to generate", error) } } @@ -136,19 +138,19 @@ fun Generator.Companion.replicatePoisson(meanSize: Double, generator: Genera val result = mutableListOf() var finish = false while (!finish) { - val error = Generator.doubleRange(0.0, 1.0).generate(it).bind { value -> + val errorOr = Generator.doubleRange(0.0, 1.0).generate(it).bind { value -> if (value < chance) { generator.generate(it).map { result.add(it) } } else { finish = true - ErrorOr.Success(Unit) + ErrorOr(Unit) } } - if (error is ErrorOr.Error) { - return@Generator ErrorOr.Error(error.error) + if (errorOr.error != null) { + return@Generator ErrorOr(errorOr.error) } } - ErrorOr.Success(result) + ErrorOr(result) } fun Generator.Companion.pickOne(list: List) = Generator.intRange(0, list.size - 1).map { list[it] } From e97cbec755a9271490a40ea477b9af468e7899ba Mon Sep 17 00:00:00 2001 From: Andras Slemmer Date: Tue, 6 Sep 2016 11:54:32 +0100 Subject: [PATCH 6/7] client: Fix compile error after rebase --- client/src/main/kotlin/com/r3corda/client/mock/EventGenerator.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/client/src/main/kotlin/com/r3corda/client/mock/EventGenerator.kt b/client/src/main/kotlin/com/r3corda/client/mock/EventGenerator.kt index 3a3a88fcab..c96979e4b2 100644 --- a/client/src/main/kotlin/com/r3corda/client/mock/EventGenerator.kt +++ b/client/src/main/kotlin/com/r3corda/client/mock/EventGenerator.kt @@ -5,7 +5,6 @@ import com.r3corda.core.contracts.* import com.r3corda.core.crypto.Party import com.r3corda.core.serialization.OpaqueBytes import com.r3corda.node.services.monitor.ServiceToClientEvent -import com.r3corda.testing.ledger import java.time.Instant /** From a845cf6bdf6e41e6499ac3b4ac41a5800bb9afc7 Mon Sep 17 00:00:00 2001 From: Andras Slemmer Date: Thu, 8 Sep 2016 11:05:53 +0100 Subject: [PATCH 7/7] client: Fix compile after rebase --- client/src/main/kotlin/com/r3corda/client/mock/EventGenerator.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/client/src/main/kotlin/com/r3corda/client/mock/EventGenerator.kt b/client/src/main/kotlin/com/r3corda/client/mock/EventGenerator.kt index c96979e4b2..9692ef04f6 100644 --- a/client/src/main/kotlin/com/r3corda/client/mock/EventGenerator.kt +++ b/client/src/main/kotlin/com/r3corda/client/mock/EventGenerator.kt @@ -4,6 +4,7 @@ import com.r3corda.contracts.asset.Cash import com.r3corda.core.contracts.* import com.r3corda.core.crypto.Party import com.r3corda.core.serialization.OpaqueBytes +import com.r3corda.core.transactions.TransactionBuilder import com.r3corda.node.services.monitor.ServiceToClientEvent import java.time.Instant