ProgressTracker emits exception thrown by the flow, allowing the ANSI renderer to correctly stop and print the error (#189)

This commit is contained in:
Shams Asari 2017-02-15 10:14:24 +00:00
parent ed093cdb9d
commit f13817efb3
10 changed files with 186 additions and 106 deletions

View File

@ -403,3 +403,9 @@ private class ObservableToFuture<T>(observable: Observable<T>) : AbstractFuture<
/** Return the sum of an Iterable of [BigDecimal]s. */
fun Iterable<BigDecimal>.sum(): BigDecimal = fold(BigDecimal.ZERO) { a, b -> a + b }
fun codePointsString(vararg codePoints: Int): String {
val builder = StringBuilder()
codePoints.forEach { builder.append(Character.toChars(it)) }
return builder.toString()
}

View File

@ -1,5 +1,7 @@
package net.corda.core.utilities
import net.corda.core.codePointsString
/**
* A simple wrapper class that contains icons and support for printing them only when we're connected to a terminal.
*/
@ -7,15 +9,17 @@ object Emoji {
// Unfortunately only Apple has a terminal that can do colour emoji AND an emoji font installed by default.
val hasEmojiTerminal by lazy { listOf("Apple_Terminal", "iTerm.app").contains(System.getenv("TERM_PROGRAM")) }
const val CODE_SANTA_CLAUS = "\ud83c\udf85"
const val CODE_DIAMOND = "\ud83d\udd37"
const val CODE_BAG_OF_CASH = "\ud83d\udcb0"
const val CODE_NEWSPAPER = "\ud83d\udcf0"
const val CODE_RIGHT_ARROW = "\u27a1\ufe0f"
const val CODE_LEFT_ARROW = "\u2b05\ufe0f"
const val CODE_GREEN_TICK = "\u2705"
const val CODE_PAPERCLIP = "\ud83d\udcce"
const val CODE_COOL_GUY = "\ud83d\ude0e"
@JvmStatic val CODE_SANTA_CLAUS: String = codePointsString(0x1F385)
@JvmStatic val CODE_DIAMOND: String = codePointsString(0x1F537)
@JvmStatic val CODE_BAG_OF_CASH: String = codePointsString(0x1F4B0)
@JvmStatic val CODE_NEWSPAPER: String = codePointsString(0x1F4F0)
@JvmStatic val CODE_RIGHT_ARROW: String = codePointsString(0x27A1, 0xFE0F)
@JvmStatic val CODE_LEFT_ARROW: String = codePointsString(0x2B05, 0xFE0F)
@JvmStatic val CODE_GREEN_TICK: String = codePointsString(0x2705)
@JvmStatic val CODE_PAPERCLIP: String = codePointsString(0x1F4CE)
@JvmStatic val CODE_COOL_GUY: String = codePointsString(0x1F60E)
@JvmStatic val CODE_NO_ENTRY: String = codePointsString(0x1F6AB)
@JvmStatic val SKULL_AND_CROSSBONES: String = codePointsString(0x2620)
/**
* When non-null, toString() methods are allowed to use emoji in the output as we're going to render them to a
@ -55,4 +59,5 @@ object Emoji {
emojiMode.set(null)
}
}
}

View File

@ -55,7 +55,7 @@ class ProgressTracker(vararg steps: Step) {
/** This class makes it easier to relabel a step on the fly, to provide transient information. */
open inner class RelabelableStep(currentLabel: String) : Step(currentLabel) {
override val changes = BehaviorSubject.create<Change>()
override val changes: BehaviorSubject<Change> = BehaviorSubject.create()
var currentLabel: String = currentLabel
set(value) {
@ -105,27 +105,26 @@ class ProgressTracker(vararg steps: Step) {
var currentStep: Step
get() = steps[stepIndex]
set(value) {
if (currentStep != value) {
check(currentStep != DONE) { "Cannot rewind a progress tracker once it reaches the done state" }
check(!hasEnded) { "Cannot rewind a progress tracker once it has ended" }
if (currentStep == value) return
val index = steps.indexOf(value)
require(index != -1)
val index = steps.indexOf(value)
require(index != -1)
if (index < stepIndex) {
// We are going backwards: unlink and unsubscribe from any child nodes that we're rolling back
// through, in preparation for moving through them again.
for (i in stepIndex downTo index) {
removeChildProgressTracker(steps[i])
}
if (index < stepIndex) {
// We are going backwards: unlink and unsubscribe from any child nodes that we're rolling back
// through, in preparation for moving through them again.
for (i in stepIndex downTo index) {
removeChildProgressTracker(steps[i])
}
curChangeSubscription?.unsubscribe()
stepIndex = index
_changes.onNext(Change.Position(this, steps[index]))
curChangeSubscription = currentStep.changes.subscribe { _changes.onNext(it) }
if (currentStep == DONE) _changes.onCompleted()
}
curChangeSubscription?.unsubscribe()
stepIndex = index
_changes.onNext(Change.Position(this, steps[index]))
curChangeSubscription = currentStep.changes.subscribe( { _changes.onNext(it) }, { _changes.onError(it) })
if (currentStep == DONE) _changes.onCompleted()
}
/** Returns the current step, descending into children to find the deepest step we are up to. */
@ -149,6 +148,15 @@ class ProgressTracker(vararg steps: Step) {
_changes.onNext(Change.Structural(this, step))
}
/**
* Ends the progress tracker with the given error, bypassing any remaining steps. [changes] will emit the exception
* as an error.
*/
fun endWithError(error: Throwable) {
check(!hasEnded) { "Progress tracker has already ended" }
_changes.onError(error)
}
/** The parent of this tracker: set automatically by the parent when a tracker is added as a child */
var parent: ProgressTracker? = null
private set
@ -195,6 +203,9 @@ class ProgressTracker(vararg steps: Step) {
* if a step changed its label or rendering).
*/
val changes: Observable<Change> get() = _changes
/** Returns true if the progress tracker has ended, either by reaching the [DONE] step or prematurely with an error */
val hasEnded: Boolean get() = _changes.hasCompleted() || _changes.hasThrowable()
}

View File

@ -15,6 +15,7 @@ import net.corda.core.flows.FlowStateMachine
import net.corda.core.flows.StateMachineRunId
import net.corda.core.random63BitValue
import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.ProgressTracker
import net.corda.core.utilities.UntrustworthyData
import net.corda.core.utilities.debug
import net.corda.core.utilities.trace
@ -56,8 +57,8 @@ class FlowStateMachineImpl<R>(override val id: StateMachineRunId,
@Transient private var _logger: Logger? = null
/**
* Return the logger for this state machine. The logger name incorporates [id] and so including this in the log
* message is not necessary.
* Return the logger for this state machine. The logger name incorporates [id] and so including it in the log message
* is not necessary.
*/
override val logger: Logger get() {
return _logger ?: run {
@ -94,14 +95,12 @@ class FlowStateMachineImpl<R>(override val id: StateMachineRunId,
} catch (e: FlowException) {
// Check if the FlowException was propagated by looking at where the stack trace originates (see suspendAndExpectReceive).
val propagated = e.stackTrace[0].className == javaClass.name
actionOnEnd(e, propagated)
_resultFuture?.setException(e)
processException(e, propagated)
logger.debug(if (propagated) "Flow ended due to receiving exception" else "Flow finished with exception", e)
return
} catch (t: Throwable) {
logger.warn("Terminated by unexpected exception", t)
actionOnEnd(t, false)
_resultFuture?.setException(t)
processException(t, false)
return
}
@ -112,6 +111,7 @@ class FlowStateMachineImpl<R>(override val id: StateMachineRunId,
// This is to prevent actionOnEnd being called twice if it throws an exception
actionOnEnd(null, false)
_resultFuture?.set(result)
logic.progressTracker?.currentStep = ProgressTracker.DONE
logger.debug { "Flow finished with result $result" }
}
@ -121,6 +121,12 @@ class FlowStateMachineImpl<R>(override val id: StateMachineRunId,
logger.trace { "Starting database transaction ${TransactionManager.currentOrNull()} on ${Strand.currentStrand()}" }
}
private fun processException(exception: Throwable, propagated: Boolean) {
actionOnEnd(exception, propagated)
_resultFuture?.setException(exception)
logic.progressTracker?.endWithError(exception)
}
internal fun commitTransaction() {
val transaction = TransactionManager.current()
try {

View File

@ -24,7 +24,6 @@ import net.corda.core.messaging.send
import net.corda.core.random63BitValue
import net.corda.core.serialization.*
import net.corda.core.then
import net.corda.core.utilities.ProgressTracker
import net.corda.core.utilities.debug
import net.corda.core.utilities.loggerFor
import net.corda.core.utilities.trace
@ -391,7 +390,6 @@ class StateMachineManager(val serviceHub: ServiceHubInternal,
}
fiber.actionOnEnd = { exception, propagated ->
try {
fiber.logic.progressTracker?.currentStep = ProgressTracker.DONE
mutex.locked {
stateMachines.remove(fiber)?.let { checkpointStorage.removeCheckpoint(it) }
notifyChangeObservers(fiber, AddOrRemove.REMOVE)

View File

@ -2,7 +2,6 @@ package net.corda.node.utilities
import net.corda.core.ThreadBox
import net.corda.core.flows.FlowLogic
import net.corda.core.utilities.ProgressTracker
import net.corda.node.services.statemachine.StateMachineManager
import java.util.*
@ -35,13 +34,12 @@ class ANSIProgressObserver(val smm: StateMachineManager) {
if (currentlyRendering?.progressTracker != null) {
ANSIProgressRenderer.progressTracker = currentlyRendering!!.progressTracker
}
} while (currentlyRendering?.progressTracker?.currentStep == ProgressTracker.DONE)
} while (currentlyRendering?.progressTracker?.hasEnded ?: false)
}
}
private fun removeFlowLogic(flowLogic: FlowLogic<*>) {
state.locked {
flowLogic.progressTracker?.currentStep = ProgressTracker.DONE
if (currentlyRendering == flowLogic) {
wireUpProgressRendering()
}
@ -51,7 +49,7 @@ class ANSIProgressObserver(val smm: StateMachineManager) {
private fun addFlowLogic(flowLogic: FlowLogic<*>) {
state.locked {
pending.add(flowLogic)
if ((currentlyRendering?.progressTracker?.currentStep ?: ProgressTracker.DONE) == ProgressTracker.DONE) {
if (currentlyRendering?.progressTracker?.hasEnded ?: true) {
wireUpProgressRendering()
}
}

View File

@ -1,6 +1,9 @@
package net.corda.node.utilities
import net.corda.core.utilities.Emoji
import net.corda.core.utilities.Emoji.CODE_GREEN_TICK
import net.corda.core.utilities.Emoji.CODE_NO_ENTRY
import net.corda.core.utilities.Emoji.CODE_RIGHT_ARROW
import net.corda.core.utilities.Emoji.SKULL_AND_CROSSBONES
import net.corda.core.utilities.ProgressTracker
import net.corda.node.utilities.ANSIProgressRenderer.progressTracker
import org.apache.logging.log4j.LogManager
@ -43,7 +46,7 @@ object ANSIProgressRenderer {
prevMessagePrinted = null
prevLinesDrawn = 0
draw(true)
subscription = value?.changes?.subscribe { draw(true) }
subscription = value?.changes?.subscribe({ draw(true) }, { draw(true, it) })
}
private fun setup() {
@ -102,7 +105,7 @@ object ANSIProgressRenderer {
// prevLinesDraw is just for ANSI mode.
private var prevLinesDrawn = 0
@Synchronized private fun draw(moveUp: Boolean) {
@Synchronized private fun draw(moveUp: Boolean, error: Throwable? = null) {
val pt = progressTracker!!
if (!usingANSI) {
@ -122,7 +125,15 @@ object ANSIProgressRenderer {
// Put a blank line between any logging and us.
ansi.eraseLine()
ansi.newline()
val newLinesDrawn = 1 + pt.renderLevel(ansi, 0, pt.allSteps)
var newLinesDrawn = 1 + pt.renderLevel(ansi, 0, error != null)
if (error != null) {
ansi.a("$SKULL_AND_CROSSBONES $error")
ansi.eraseLine(Ansi.Erase.FORWARD)
ansi.newline()
newLinesDrawn++
}
if (newLinesDrawn < prevLinesDrawn) {
// If some steps were removed from the progress tracker, we don't want to leave junk hanging around below.
val linesToClear = prevLinesDrawn - newLinesDrawn
@ -140,7 +151,7 @@ object ANSIProgressRenderer {
}
// Returns number of lines rendered.
private fun ProgressTracker.renderLevel(ansi: Ansi, indent: Int, allSteps: List<Pair<Int, ProgressTracker.Step>>): Int {
private fun ProgressTracker.renderLevel(ansi: Ansi, indent: Int, error: Boolean): Int {
with(ansi) {
var lines = 0
for ((index, step) in steps.withIndex()) {
@ -149,10 +160,11 @@ object ANSIProgressRenderer {
if (indent > 0 && step == ProgressTracker.DONE) continue
val marker = when {
index < stepIndex -> Emoji.CODE_GREEN_TICK + " "
index == stepIndex && step == ProgressTracker.DONE -> Emoji.CODE_GREEN_TICK + " "
index == stepIndex -> Emoji.CODE_RIGHT_ARROW + " "
else -> " "
index < stepIndex -> "$CODE_GREEN_TICK "
index == stepIndex && step == ProgressTracker.DONE -> "$CODE_GREEN_TICK "
index == stepIndex -> "$CODE_RIGHT_ARROW "
error -> "$CODE_NO_ENTRY "
else -> " "
}
a(" ".repeat(indent))
a(marker)
@ -168,7 +180,7 @@ object ANSIProgressRenderer {
val child = getChildProgressTracker(step)
if (child != null)
lines += child.renderLevel(ansi, indent + 1, allSteps)
lines += child.renderLevel(ansi, indent + 1, error)
}
return lines
}

View File

@ -3,6 +3,7 @@ package net.corda.node.services.statemachine
import co.paralleluniverse.fibers.Fiber
import co.paralleluniverse.fibers.Suspendable
import com.google.common.util.concurrent.ListenableFuture
import net.corda.core.*
import net.corda.core.contracts.DOLLARS
import net.corda.core.contracts.DummyState
import net.corda.core.contracts.issuedBy
@ -10,17 +11,16 @@ import net.corda.core.crypto.Party
import net.corda.core.crypto.generateKeyPair
import net.corda.core.flows.FlowException
import net.corda.core.flows.FlowLogic
import net.corda.core.getOrThrow
import net.corda.core.map
import net.corda.core.messaging.MessageRecipients
import net.corda.core.node.services.PartyInfo
import net.corda.core.node.services.ServiceInfo
import net.corda.core.random63BitValue
import net.corda.core.serialization.OpaqueBytes
import net.corda.core.serialization.deserialize
import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.LogHelper
import net.corda.core.utilities.ProgressTracker
import net.corda.core.utilities.ProgressTracker.Change
import net.corda.core.utilities.unwrap
import net.corda.flows.CashIssueFlow
import net.corda.flows.CashPaymentFlow
@ -44,6 +44,7 @@ import org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType
import org.junit.After
import org.junit.Before
import org.junit.Test
import rx.Notification
import rx.Observable
import java.util.*
import kotlin.reflect.KClass
@ -379,18 +380,36 @@ class StateMachineManagerTests {
net.runNetwork()
assertThatExceptionOfType(FlowSessionException::class.java).isThrownBy {
resultFuture.getOrThrow()
}.withMessageContaining(String::class.java.name)
}.withMessageContaining(String::class.java.name) // Make sure the exception message mentions the type the flow was expecting to receive
}
@Test
fun `non-FlowException thrown on other side`() {
node2.services.registerFlowInitiator(ReceiveFlow::class) { ExceptionFlow { Exception("evil bug!") } }
val resultFuture = node1.services.startFlow(ReceiveFlow(node2.info.legalIdentity)).resultFuture
net.runNetwork()
val exceptionResult = assertFailsWith(FlowSessionException::class) {
resultFuture.getOrThrow()
val erroringFlowFuture = node2.initiateSingleShotFlow(ReceiveFlow::class) {
ExceptionFlow { Exception("evil bug!") }
}
assertThat(exceptionResult.message).doesNotContain("evil bug!")
val erroringFlowSteps = erroringFlowFuture.flatMap { it.progressSteps }
val receiveFlow = ReceiveFlow(node2.info.legalIdentity)
val receiveFlowSteps = receiveFlow.progressSteps
val receiveFlowResult = node1.services.startFlow(receiveFlow).resultFuture
net.runNetwork()
assertThat(erroringFlowSteps.get()).containsExactly(
Notification.createOnNext(ExceptionFlow.START_STEP),
Notification.createOnError(erroringFlowFuture.get().exceptionThrown)
)
val receiveFlowException = assertFailsWith(FlowSessionException::class) {
receiveFlowResult.getOrThrow()
}
assertThat(receiveFlowException.message).doesNotContain("evil bug!")
assertThat(receiveFlowSteps.get()).containsExactly(
Notification.createOnNext(ReceiveFlow.START_STEP),
Notification.createOnError(receiveFlowException)
)
assertSessionTransfers(
node1 sent sessionInit(ReceiveFlow::class) to node2,
node2 sent sessionConfirm to node1,
@ -400,11 +419,15 @@ class StateMachineManagerTests {
@Test
fun `FlowException thrown on other side`() {
val erroringFlowFuture = node2.initiateSingleShotFlow(ReceiveFlow::class) {
val erroringFlow = node2.initiateSingleShotFlow(ReceiveFlow::class) {
ExceptionFlow { MyFlowException("Nothing useful") }
}
val erroringFlowSteps = erroringFlow.flatMap { it.progressSteps }
val receivingFiber = node1.services.startFlow(ReceiveFlow(node2.info.legalIdentity)) as FlowStateMachineImpl
net.runNetwork()
assertThatExceptionOfType(MyFlowException::class.java)
.isThrownBy { receivingFiber.resultFuture.getOrThrow() }
.withMessage("Nothing useful")
@ -412,13 +435,18 @@ class StateMachineManagerTests {
databaseTransaction(node2.database) {
assertThat(node2.checkpointStorage.checkpoints()).isEmpty()
}
val errorFlow = erroringFlowFuture.getOrThrow()
assertThat(receivingFiber.isTerminated).isTrue()
assertThat((errorFlow.stateMachine as FlowStateMachineImpl).isTerminated).isTrue()
assertThat((erroringFlow.get().stateMachine as FlowStateMachineImpl).isTerminated).isTrue()
assertThat(erroringFlowSteps.get()).containsExactly(
Notification.createOnNext(ExceptionFlow.START_STEP),
Notification.createOnError(erroringFlow.get().exceptionThrown)
)
assertSessionTransfers(
node1 sent sessionInit(ReceiveFlow::class) to node2,
node2 sent sessionConfirm to node1,
node2 sent erroredEnd(errorFlow.exceptionThrown) to node1
node2 sent erroredEnd(erroringFlow.get().exceptionThrown) to node1
)
// Make sure the original stack trace isn't sent down the wire
assertThat((sessionTransfers.last().message as ErrorSessionEnd).errorResponse!!.stackTrace).isEmpty()
@ -606,6 +634,15 @@ class StateMachineManagerTests {
private infix fun MockNode.sent(message: SessionMessage): Pair<Int, SessionMessage> = Pair(id, message)
private infix fun Pair<Int, SessionMessage>.to(node: MockNode): SessionTransfer = SessionTransfer(first, second, node.net.myAddress)
private val FlowLogic<*>.progressSteps: ListenableFuture<List<Notification<ProgressTracker.Step>>> get() {
return progressTracker!!.changes
.ofType(Change.Position::class.java)
.map { it.newStep }
.materialize()
.toList()
.toFuture()
}
private class NoOpFlow(val nonTerminating: Boolean = false) : FlowLogic<Unit>() {
@Transient var flowStarted = false
@ -630,17 +667,22 @@ class StateMachineManagerTests {
private class ReceiveFlow(vararg val otherParties: Party) : FlowLogic<Unit>() {
private var nonTerminating: Boolean = false
object START_STEP : ProgressTracker.Step("Starting")
object RECEIVED_STEP : ProgressTracker.Step("Received")
init {
require(otherParties.isNotEmpty())
}
override val progressTracker: ProgressTracker = ProgressTracker(START_STEP, RECEIVED_STEP)
private var nonTerminating: Boolean = false
@Transient var receivedPayloads: List<String> = emptyList()
@Suspendable
override fun call() {
progressTracker.currentStep = START_STEP
receivedPayloads = otherParties.map { receive<String>(it).unwrap { it } }
progressTracker.currentStep = RECEIVED_STEP
if (nonTerminating) {
Fiber.park()
}
@ -664,8 +706,13 @@ class StateMachineManagerTests {
}
private class ExceptionFlow<E : Exception>(val exception: () -> E) : FlowLogic<Nothing>() {
object START_STEP : ProgressTracker.Step("Starting")
override val progressTracker: ProgressTracker = ProgressTracker(START_STEP)
lateinit var exceptionThrown: E
override fun call(): Nothing {
progressTracker.currentStep = START_STEP
exceptionThrown = exception()
throw exceptionThrown
}

View File

@ -4,31 +4,34 @@ import com.google.common.util.concurrent.Futures
import net.corda.core.getOrThrow
import net.corda.core.node.services.ServiceInfo
import net.corda.flows.IssuerFlow
import net.corda.node.driver.driver
import net.corda.node.services.User
import net.corda.node.services.messaging.CordaRPCClient
import net.corda.node.services.startFlowPermission
import net.corda.node.services.transactions.SimpleNotaryService
import net.corda.testing.node.NodeBasedTest
import org.junit.Test
class TraderDemoTest {
@Test fun `runs trader demo`() {
driver(isDebug = true) {
val permissions = setOf(
startFlowPermission<IssuerFlow.IssuanceRequester>(),
startFlowPermission<net.corda.traderdemo.flow.SellerFlow>())
val demoUser = listOf(User("demo", "demo", permissions))
val user = User("user1", "test", permissions = setOf(startFlowPermission<IssuerFlow.IssuanceRequester>()))
val (nodeA, nodeB) = Futures.allAsList(
startNode("Bank A", rpcUsers = demoUser),
startNode("Bank B", rpcUsers = demoUser),
startNode("BankOfCorda", rpcUsers = listOf(user)),
startNode("Notary", setOf(ServiceInfo(SimpleNotaryService.type)))
).getOrThrow()
val (nodeARpc, nodeBRpc) = listOf(nodeA, nodeB)
.map { it.rpcClientToNode().start(demoUser[0].username, demoUser[0].password).proxy() }
class TraderDemoTest : NodeBasedTest() {
@Test
fun `runs trader demo`() {
val permissions = setOf(
startFlowPermission<IssuerFlow.IssuanceRequester>(),
startFlowPermission<net.corda.traderdemo.flow.SellerFlow>())
val demoUser = listOf(User("demo", "demo", permissions))
val user = User("user1", "test", permissions = setOf(startFlowPermission<IssuerFlow.IssuanceRequester>()))
val (nodeA, nodeB) = Futures.allAsList(
startNode("Bank A", rpcUsers = demoUser),
startNode("Bank B", rpcUsers = demoUser),
startNode("BankOfCorda", rpcUsers = listOf(user)),
startNode("Notary", setOf(ServiceInfo(SimpleNotaryService.type)))
).getOrThrow()
assert(TraderDemoClientApi(nodeARpc).runBuyer())
assert(TraderDemoClientApi(nodeBRpc).runSeller(counterparty = nodeA.nodeInfo.legalIdentity.name))
val (nodeARpc, nodeBRpc) = listOf(nodeA, nodeB).map {
val client = CordaRPCClient(it.configuration.artemisAddress, it.configuration)
client.start(demoUser[0].username, demoUser[0].password).proxy()
}
TraderDemoClientApi(nodeARpc).runBuyer()
TraderDemoClientApi(nodeBRpc).runSeller(counterparty = nodeA.info.legalIdentity.name)
}
}

View File

@ -24,7 +24,7 @@ class TraderDemoClientApi(val rpc: CordaRPCOps) {
val logger = loggerFor<TraderDemoClientApi>()
}
fun runBuyer(amount: Amount<Currency> = 30000.0.DOLLARS): Boolean {
fun runBuyer(amount: Amount<Currency> = 30000.0.DOLLARS) {
val bankOfCordaParty = rpc.partyFromName(BOC.name)
?: throw Exception("Unable to locate ${BOC.name} in Network Map Service")
val me = rpc.nodeIdentity()
@ -35,31 +35,25 @@ class TraderDemoClientApi(val rpc: CordaRPCOps) {
}
Futures.allAsList(resultFutures).getOrThrow()
return true
}
fun runSeller(amount: Amount<Currency> = 1000.0.DOLLARS, counterparty: String): Boolean {
val otherParty = rpc.partyFromName(counterparty)
if (otherParty != null) {
// The seller will sell some commercial paper to the buyer, who will pay with (self issued) cash.
//
// The CP sale transaction comes with a prospectus PDF, which will tag along for the ride in an
// attachment. Make sure we have the transaction prospectus attachment loaded into our store.
//
// This can also be done via an HTTP upload, but here we short-circuit and do it from code.
if (!rpc.attachmentExists(SellerFlow.PROSPECTUS_HASH)) {
javaClass.classLoader.getResourceAsStream("bank-of-london-cp.jar").use {
val id = rpc.uploadAttachment(it)
assertEquals(SellerFlow.PROSPECTUS_HASH, id)
}
fun runSeller(amount: Amount<Currency> = 1000.0.DOLLARS, counterparty: String) {
val otherParty = rpc.partyFromName(counterparty) ?: throw IllegalStateException("Don't know $counterparty")
// The seller will sell some commercial paper to the buyer, who will pay with (self issued) cash.
//
// The CP sale transaction comes with a prospectus PDF, which will tag along for the ride in an
// attachment. Make sure we have the transaction prospectus attachment loaded into our store.
//
// This can also be done via an HTTP upload, but here we short-circuit and do it from code.
if (!rpc.attachmentExists(SellerFlow.PROSPECTUS_HASH)) {
javaClass.classLoader.getResourceAsStream("bank-of-london-cp.jar").use {
val id = rpc.uploadAttachment(it)
assertEquals(SellerFlow.PROSPECTUS_HASH, id)
}
// The line below blocks and waits for the future to resolve.
val stx = rpc.startFlow(::SellerFlow, otherParty, amount).returnValue.getOrThrow()
logger.info("Sale completed - we have a happy customer!\n\nFinal transaction is:\n\n${Emoji.renderIfSupported(stx.tx)}")
return true
} else {
return false
}
// The line below blocks and waits for the future to resolve.
val stx = rpc.startFlow(::SellerFlow, otherParty, amount).returnValue.getOrThrow()
logger.info("Sale completed - we have a happy customer!\n\nFinal transaction is:\n\n${Emoji.renderIfSupported(stx.tx)}")
}
}