diff --git a/core/src/main/kotlin/net/corda/core/Utils.kt b/core/src/main/kotlin/net/corda/core/Utils.kt index abb9107a2e..b052f0f7bf 100644 --- a/core/src/main/kotlin/net/corda/core/Utils.kt +++ b/core/src/main/kotlin/net/corda/core/Utils.kt @@ -403,3 +403,9 @@ private class ObservableToFuture(observable: Observable) : AbstractFuture< /** Return the sum of an Iterable of [BigDecimal]s. */ fun Iterable.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() +} diff --git a/core/src/main/kotlin/net/corda/core/utilities/Emoji.kt b/core/src/main/kotlin/net/corda/core/utilities/Emoji.kt index f1a21c8ebb..146cb150b7 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/Emoji.kt +++ b/core/src/main/kotlin/net/corda/core/utilities/Emoji.kt @@ -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) } } + } diff --git a/core/src/main/kotlin/net/corda/core/utilities/ProgressTracker.kt b/core/src/main/kotlin/net/corda/core/utilities/ProgressTracker.kt index 55b344c485..39514b8ed3 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/ProgressTracker.kt +++ b/core/src/main/kotlin/net/corda/core/utilities/ProgressTracker.kt @@ -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() + override val changes: BehaviorSubject = 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 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() } diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStateMachineImpl.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStateMachineImpl.kt index eda1e3a192..5758aa9312 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStateMachineImpl.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStateMachineImpl.kt @@ -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(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(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(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(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 { diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt index bd00a31fda..f308007029 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt @@ -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) diff --git a/node/src/main/kotlin/net/corda/node/utilities/ANSIProgressObserver.kt b/node/src/main/kotlin/net/corda/node/utilities/ANSIProgressObserver.kt index 6f449311ce..42881896db 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/ANSIProgressObserver.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/ANSIProgressObserver.kt @@ -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() } } diff --git a/node/src/main/kotlin/net/corda/node/utilities/ANSIProgressRenderer.kt b/node/src/main/kotlin/net/corda/node/utilities/ANSIProgressRenderer.kt index 46afd91900..a6b9ba9179 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/ANSIProgressRenderer.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/ANSIProgressRenderer.kt @@ -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>): 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 } diff --git a/node/src/test/kotlin/net/corda/node/services/statemachine/StateMachineManagerTests.kt b/node/src/test/kotlin/net/corda/node/services/statemachine/StateMachineManagerTests.kt index 3ddb3977e2..d112c0c034 100644 --- a/node/src/test/kotlin/net/corda/node/services/statemachine/StateMachineManagerTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/statemachine/StateMachineManagerTests.kt @@ -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 = Pair(id, message) private infix fun Pair.to(node: MockNode): SessionTransfer = SessionTransfer(first, second, node.net.myAddress) + private val FlowLogic<*>.progressSteps: ListenableFuture>> get() { + return progressTracker!!.changes + .ofType(Change.Position::class.java) + .map { it.newStep } + .materialize() + .toList() + .toFuture() + } + private class NoOpFlow(val nonTerminating: Boolean = false) : FlowLogic() { @Transient var flowStarted = false @@ -630,17 +667,22 @@ class StateMachineManagerTests { private class ReceiveFlow(vararg val otherParties: Party) : FlowLogic() { - 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 = emptyList() @Suspendable override fun call() { + progressTracker.currentStep = START_STEP receivedPayloads = otherParties.map { receive(it).unwrap { it } } + progressTracker.currentStep = RECEIVED_STEP if (nonTerminating) { Fiber.park() } @@ -664,8 +706,13 @@ class StateMachineManagerTests { } private class ExceptionFlow(val exception: () -> E) : FlowLogic() { + 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 } diff --git a/samples/trader-demo/src/integration-test/kotlin/net/corda/traderdemo/TraderDemoTest.kt b/samples/trader-demo/src/integration-test/kotlin/net/corda/traderdemo/TraderDemoTest.kt index f28cb2ab75..7e71e87780 100644 --- a/samples/trader-demo/src/integration-test/kotlin/net/corda/traderdemo/TraderDemoTest.kt +++ b/samples/trader-demo/src/integration-test/kotlin/net/corda/traderdemo/TraderDemoTest.kt @@ -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(), - startFlowPermission()) - val demoUser = listOf(User("demo", "demo", permissions)) - val user = User("user1", "test", permissions = setOf(startFlowPermission())) - 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(), + startFlowPermission()) + val demoUser = listOf(User("demo", "demo", permissions)) + val user = User("user1", "test", permissions = setOf(startFlowPermission())) + 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) } } diff --git a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TraderDemoClientApi.kt b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TraderDemoClientApi.kt index 366940a3b6..3a516322de 100644 --- a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TraderDemoClientApi.kt +++ b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TraderDemoClientApi.kt @@ -24,7 +24,7 @@ class TraderDemoClientApi(val rpc: CordaRPCOps) { val logger = loggerFor() } - fun runBuyer(amount: Amount = 30000.0.DOLLARS): Boolean { + fun runBuyer(amount: Amount = 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 = 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 = 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)}") } }