From dbd07a8e6c770349e2ef7e91df7ae768035bfc30 Mon Sep 17 00:00:00 2001 From: Anthony Keenan Date: Fri, 29 Jun 2018 13:08:09 +0100 Subject: [PATCH] Fix missing snippets (#1154) --- docs/source/event-scheduling.rst | 14 +- docs/source/oracles.rst | 254 +++++++++++++++++++----- docs/source/tutorial-attachments.rst | 93 ++++++++- docs/source/tutorial-custom-notary.rst | 78 +++++++- docs/source/tutorial-observer-nodes.rst | 40 +++- docs/source/tutorial-tear-offs.rst | 47 ++++- 6 files changed, 443 insertions(+), 83 deletions(-) diff --git a/docs/source/event-scheduling.rst b/docs/source/event-scheduling.rst index 34633c61dc..1ef258f3bf 100644 --- a/docs/source/event-scheduling.rst +++ b/docs/source/event-scheduling.rst @@ -67,11 +67,15 @@ Let's take an example of the interest rate swap fixings for our scheduled events .. container:: codeset - .. literalinclude:: ../../samples/irs-demo/cordapp/src/main/kotlin/net/corda/irs/contract/IRS.kt - :language: kotlin - :start-after: DOCSTART 1 - :end-before: DOCEND 1 - :dedent: 8 + .. code-block:: kotlin + + override fun nextScheduledActivity(thisStateRef: StateRef, flowLogicRefFactory: FlowLogicRefFactory): ScheduledActivity? { + val nextFixingOf = nextFixingOf() ?: return null + + // This is perhaps not how we should determine the time point in the business day, but instead expect the schedule to detail some of these aspects + val instant = suggestInterestRateAnnouncementTimeWindow(index = nextFixingOf.name, source = floatingLeg.indexSource, date = nextFixingOf.forDay).fromTime!! + return ScheduledActivity(flowLogicRefFactory.create("net.corda.irs.flows.FixingFlow\$FixingRoleDecider", thisStateRef), instant) + } The first thing this does is establish if there are any remaining fixings. If there are none, then it returns ``null`` to indicate that there is no activity to schedule. Otherwise it calculates the ``Instant`` at which the interest rate diff --git a/docs/source/oracles.rst b/docs/source/oracles.rst index 5066fe2f93..c803185dee 100644 --- a/docs/source/oracles.rst +++ b/docs/source/oracles.rst @@ -101,15 +101,15 @@ class that binds it to the network layer. Here is an extract from the ``NodeInterestRates.Oracle`` class and supporting types: -.. literalinclude:: ../../finance/src/main/kotlin/net/corda/finance/contracts/FinanceTypes.kt - :language: kotlin - :start-after: DOCSTART 1 - :end-before: DOCEND 1 +.. sourcecode:: kotlin -.. literalinclude:: ../../finance/src/main/kotlin/net/corda/finance/contracts/FinanceTypes.kt - :language: kotlin - :start-after: DOCSTART 2 - :end-before: DOCEND 2 + @CordaSerializable + data class FixOf(val name: String, val forDay: LocalDate, val ofTenor: Tenor) + +.. sourcecode:: kotlin + + /** A [Fix] represents a named interest rate, on a given day, for a given duration. It can be embedded in a tx. */ + data class Fix(val of: FixOf, val value: BigDecimal) : CommandData .. sourcecode:: kotlin @@ -166,11 +166,40 @@ parameter and ``CommandData`` classes. Let's see how the ``sign`` method for ``NodeInterestRates.Oracle`` is written: -.. literalinclude:: ../../samples/irs-demo/cordapp/src/main/kotlin/net/corda/irs/api/NodeInterestRates.kt - :language: kotlin - :start-after: DOCSTART 1 - :end-before: DOCEND 1 - :dedent: 8 +.. container:: codeset + + .. code-block:: kotlin + + fun sign(ftx: FilteredTransaction): TransactionSignature { + ftx.verify() + // Performing validation of obtained filtered components. + fun commandValidator(elem: Command<*>): Boolean { + require(services.myInfo.legalIdentities.first().owningKey in elem.signers && elem.value is Fix) { + "Oracle received unknown command (not in signers or not Fix)." + } + val fix = elem.value as Fix + val known = knownFixes[fix.of] + if (known == null || known != fix) + throw UnknownFix(fix.of) + return true + } + + fun check(elem: Any): Boolean { + return when (elem) { + is Command<*> -> commandValidator(elem) + else -> throw IllegalArgumentException("Oracle received data of different type than expected.") + } + } + + require(ftx.checkWithFun(::check)) + ftx.checkCommandVisibility(services.myInfo.legalIdentities.first().owningKey) + // It all checks out, so we can return a signature. + // + // Note that we will happily sign an invalid transaction, as we are only being presented with a filtered + // version so we can't resolve or check it ourselves. However, that doesn't matter much, as if we sign + // an invalid transaction the signature is worthless. + return services.createSignature(ftx, services.myInfo.legalIdentities.first().owningKey) + } Here we can see that there are several steps: @@ -192,20 +221,55 @@ Binding to the network The first step is to create the oracle as a service by annotating its class with ``@CordaService``. Let's see how that's done: -.. literalinclude:: ../../samples/irs-demo/cordapp/src/main/kotlin/net/corda/irs/api/NodeInterestRates.kt - :language: kotlin - :start-after: DOCSTART 3 - :end-before: DOCEND 3 - :dedent: 4 +.. container:: codeset + + .. code-block:: kotlin + + @CordaService + class Oracle(private val services: AppServiceHub) : SingletonSerializeAsToken() { + private val mutex = ThreadBox(InnerState()) + + init { + // Set some default fixes to the Oracle, so we can smoothly run the IRS Demo without uploading fixes. + // This is required to avoid a situation where the runnodes version of the demo isn't in a good state + // upon startup. + addDefaultFixes() + } The Corda node scans for any class with this annotation and initialises them. The only requirement is that the class provide a constructor with a single parameter of type ``ServiceHub``. -.. literalinclude:: ../../samples/irs-demo/cordapp/src/main/kotlin/net/corda/irs/api/NodeInterestRates.kt - :language: kotlin - :start-after: DOCSTART 2 - :end-before: DOCEND 2 - :dedent: 4 +.. container:: codeset + + .. code-block:: kotlin + + @InitiatedBy(RatesFixFlow.FixSignFlow::class) + class FixSignHandler(private val otherPartySession: FlowSession) : FlowLogic() { + @Suspendable + override fun call() { + val request = otherPartySession.receive().unwrap { it } + val oracle = serviceHub.cordaService(Oracle::class.java) + otherPartySession.send(oracle.sign(request.ftx)) + } + } + + @InitiatedBy(RatesFixFlow.FixQueryFlow::class) + class FixQueryHandler(private val otherPartySession: FlowSession) : FlowLogic() { + object RECEIVED : ProgressTracker.Step("Received fix request") + object SENDING : ProgressTracker.Step("Sending fix response") + + override val progressTracker = ProgressTracker(RECEIVED, SENDING) + + @Suspendable + override fun call() { + val request = otherPartySession.receive().unwrap { it } + progressTracker.currentStep = RECEIVED + val oracle = serviceHub.cordaService(Oracle::class.java) + val answers = oracle.query(request.queries) + progressTracker.currentStep = SENDING + otherPartySession.send(answers) + } + } These two flows leverage the oracle to provide the querying and signing operations. They get reference to the oracle, which will have already been initialised by the node, using ``ServiceHub.cordaService``. Both flows are annotated with @@ -219,11 +283,41 @@ We mentioned the client sub-flow briefly above. They are the mechanism that cli use to interact with your oracle. Typically there will be one for querying and one for signing. Let's take a look at those for ``NodeInterestRates.Oracle``. -.. literalinclude:: ../../samples/irs-demo/cordapp/src/main/kotlin/net/corda/irs/flows/RatesFixFlow.kt - :language: kotlin - :start-after: DOCSTART 1 - :end-before: DOCEND 1 - :dedent: 4 +.. container:: codeset + + .. code-block:: kotlin + + @InitiatingFlow + class FixQueryFlow(val fixOf: FixOf, val oracle: Party) : FlowLogic() { + @Suspendable + override fun call(): Fix { + val oracleSession = initiateFlow(oracle) + // TODO: add deadline to receive + val resp = oracleSession.sendAndReceive>(QueryRequest(listOf(fixOf))) + + return resp.unwrap { + val fix = it.first() + // Check the returned fix is for what we asked for. + check(fix.of == fixOf) + fix + } + } + } + + @InitiatingFlow + class FixSignFlow(val tx: TransactionBuilder, val oracle: Party, + val partialMerkleTx: FilteredTransaction) : FlowLogic() { + @Suspendable + override fun call(): TransactionSignature { + val oracleSession = initiateFlow(oracle) + val resp = oracleSession.sendAndReceive(SignRequest(partialMerkleTx)) + return resp.unwrap { sig -> + check(oracleSession.counterparty.owningKey.isFulfilledBy(listOf(sig.by))) + tx.toWireTransaction(serviceHub).checkSignature(sig) + sig + } + } + } You'll note that the ``FixSignFlow`` requires a ``FilterTransaction`` instance which includes only ``Fix`` commands. You can find a further explanation of this in :doc:`key-concepts-oracles`. Below you will see how to build such a @@ -238,11 +332,22 @@ The oracle is invoked through sub-flows to query for values, add them to the tra the transaction signed by the oracle. Following on from the above examples, this is all encapsulated in a sub-flow called ``RatesFixFlow``. Here's the ``call`` method of that flow. -.. literalinclude:: ../../samples/irs-demo/cordapp/src/main/kotlin/net/corda/irs/flows/RatesFixFlow.kt - :language: kotlin - :start-after: DOCSTART 2 - :end-before: DOCEND 2 - :dedent: 4 +.. container:: codeset + + .. code-block:: kotlin + + @Suspendable + override fun call(): TransactionSignature { + progressTracker.currentStep = progressTracker.steps[1] + val fix = subFlow(FixQueryFlow(fixOf, oracle)) + progressTracker.currentStep = WORKING + checkFixIsNearExpected(fix) + tx.addCommand(fix, oracle.owningKey) + beforeSigning(fix) + progressTracker.currentStep = SIGNING + val mtx = tx.toWireTransaction(serviceHub).buildFilteredTransaction(Predicate { filtering(it) }) + return subFlow(FixSignFlow(tx, oracle, mtx)) + } As you can see, this: @@ -255,11 +360,31 @@ As you can see, this: Here's an example of it in action from ``FixingFlow.Fixer``. -.. literalinclude:: ../../samples/irs-demo/cordapp/src/main/kotlin/net/corda/irs/flows/FixingFlow.kt - :language: kotlin - :start-after: DOCSTART 1 - :end-before: DOCEND 1 - :dedent: 4 +.. container:: codeset + + .. code-block:: kotlin + + val addFixing = object : RatesFixFlow(ptx, handshake.payload.oracle, fixOf, BigDecimal.ZERO, BigDecimal.ONE) { + @Suspendable + override fun beforeSigning(fix: Fix) { + newDeal.generateFix(ptx, StateAndRef(txState, handshake.payload.ref), fix) + + // We set the transaction's time-window: it may be that none of the contracts need this! + // But it can't hurt to have one. + ptx.setTimeWindow(serviceHub.clock.instant(), 30.seconds) + } + + @Suspendable + override fun filtering(elem: Any): Boolean { + return when (elem) { + // Only expose Fix commands in which the oracle is on the list of requested signers + // to the oracle node, to avoid leaking privacy + is Command<*> -> handshake.payload.oracle.owningKey in elem.signers && elem.value is Fix + else -> false + } + } + } + val sig = subFlow(addFixing) .. note:: When overriding be careful when making the sub-class an anonymous or inner class (object declarations in Kotlin), @@ -271,24 +396,55 @@ Testing The ``MockNetwork`` allows the creation of ``MockNode`` instances, which are simplified nodes which can be used for testing (see :doc:`api-testing`). When creating the ``MockNetwork`` you supply a list of packages to scan for CorDapps. -Make sure the packages you provide include your oracle service, and it automatically be installed in the test nodes. +Make sure the packages you provide include your oracle service, and it will automatically be installed in the test nodes. Then you can create an oracle node on the ``MockNetwork`` and insert any initialisation logic you want to use. In this case, our ``Oracle`` service is in the ``net.corda.irs.api`` package, so the following test setup will install the service in each node. Then an oracle node with an oracle service which is initialised with some data is created on the mock network: -.. literalinclude:: ../../samples/irs-demo/cordapp/src/test/kotlin/net/corda/irs/api/OracleNodeTearOffTests.kt - :language: kotlin - :start-after: DOCSTART 1 - :end-before: DOCEND 1 - :dedent: 4 +.. container:: codeset + + .. code-block:: kotlin + + fun setUp() { + mockNet = MockNetwork(cordappPackages = listOf("net.corda.finance.contracts", "net.corda.irs")) + aliceNode = mockNet.createPartyNode(ALICE_NAME) + oracleNode = mockNet.createNode(MockNodeParameters(legalName = BOB_NAME)).apply { + transaction { + services.cordaService(NodeInterestRates.Oracle::class.java).knownFixes = TEST_DATA + } + } + } You can then write tests on your mock network to verify the nodes interact with your Oracle correctly. -.. literalinclude:: ../../samples/irs-demo/cordapp/src/test/kotlin/net/corda/irs/api/OracleNodeTearOffTests.kt - :language: kotlin - :start-after: DOCSTART 2 - :end-before: DOCEND 2 - :dedent: 4 +.. container:: codeset + + .. code-block:: kotlin + + @Test + fun verify_that_the_oracle_signs_the_transaction_if_the_interest_rate_within_allowed_limit() { + // Create a partial transaction + val tx = TransactionBuilder(DUMMY_NOTARY) + .withItems(TransactionState(1000.DOLLARS.CASH issuedBy dummyCashIssuer.party ownedBy alice.party, Cash.PROGRAM_ID, DUMMY_NOTARY)) + // Specify the rate we wish to get verified by the oracle + val fixOf = NodeInterestRates.parseFixOf("LIBOR 2016-03-16 1M") + + // Create a new flow for the fix + val flow = FilteredRatesFlow(tx, oracle, fixOf, BigDecimal("0.675"), BigDecimal("0.1")) + // Run the mock network and wait for a result + mockNet.runNetwork() + val future = aliceNode.startFlow(flow) + mockNet.runNetwork() + future.getOrThrow() + + // We should now have a valid rate on our tx from the oracle. + val fix = tx.toWireTransaction(aliceNode.services).commands.map { it }.first() + assertEquals(fixOf, (fix.value as Fix).of) + // Check that the response contains the valid rate, which is within the supplied tolerance + assertEquals(BigDecimal("0.678"), (fix.value as Fix).value) + // Check that the transaction has been signed by the oracle + assertContains(fix.signers, oracle.owningKey) + } See `here `_ for more examples. diff --git a/docs/source/tutorial-attachments.rst b/docs/source/tutorial-attachments.rst index 297951f1d2..6f991d22d4 100644 --- a/docs/source/tutorial-attachments.rst +++ b/docs/source/tutorial-attachments.rst @@ -1,4 +1,8 @@ .. highlight:: kotlin +.. raw:: html + + + Using attachments ================= @@ -57,13 +61,15 @@ Attachments metadata can be used to query, in the similar manner as :doc:`api-va * Nullability (IS_NULL, NOT_NULL) * Collection based (IN, NOT_IN) -``And`` and ``or`` operators can be used to build queries of arbitrary complexity. Example of such query: +``And`` and ``or`` operators can be used to build queries of arbitrary complexity. For example: -.. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentStorageTest.kt +.. container:: codeset + + .. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentStorageTest.kt :language: kotlin :start-after: DOCSTART AttachmentQueryExample1 :end-before: DOCEND AttachmentQueryExample1 - :dedent: 12 + :dedent: 8 Protocol -------- @@ -99,20 +105,85 @@ and if so, printed out. .. container:: codeset - .. literalinclude:: ../../samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/AttachmentDemo.kt - :language: kotlin - :start-after: DOCSTART 1 - :end-before: DOCEND 1 + .. code-block:: kotlin + + fun recipient(rpc: CordaRPCOps, webPort: Int) { + println("Waiting to receive transaction ...") + val stx = rpc.internalVerifiedTransactionsFeed().updates.toBlocking().first() + val wtx = stx.tx + if (wtx.attachments.isNotEmpty()) { + if (wtx.outputs.isNotEmpty()) { + val state = wtx.outputsOfType().single() + require(rpc.attachmentExists(state.hash)) + + // Download the attachment via the Web endpoint. + val connection = URL("http://localhost:$webPort/attachments/${state.hash}").openConnection() as HttpURLConnection + try { + require(connection.responseCode == SC_OK) { "HTTP status code was ${connection.responseCode}" } + require(connection.contentType == APPLICATION_OCTET_STREAM) { "Content-Type header was ${connection.contentType}" } + require(connection.getHeaderField(CONTENT_DISPOSITION) == "attachment; filename=\"${state.hash}.zip\"") { + "Content-Disposition header was ${connection.getHeaderField(CONTENT_DISPOSITION)}" + } + + // Write out the entries inside this jar. + println("Attachment JAR contains these entries:") + JarInputStream(connection.inputStream).use { it -> + while (true) { + val e = it.nextJarEntry ?: break + println("Entry> ${e.name}") + it.closeEntry() + } + } + } finally { + connection.disconnect() + } + println("File received - we're happy!\n\nFinal transaction is:\n\n${Emoji.renderIfSupported(wtx)}") + } else { + println("Error: no output state found in ${wtx.id}") + } + } else { + println("Error: no attachments found in ${wtx.id}") + } + } + The sender correspondingly builds a transaction with the attachment, then calls ``FinalityFlow`` to complete the transaction and send it to the recipient node: .. container:: codeset - .. literalinclude:: ../../samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/AttachmentDemo.kt - :language: kotlin - :start-after: DOCSTART 2 - :end-before: DOCEND 2 + .. code-block:: kotlin + + fun sender(rpc: CordaRPCOps, numOfClearBytes: Int = 1024) { // default size 1K. + val (inputStream, hash) = InputStreamAndHash.createInMemoryTestZip(numOfClearBytes, 0) + val executor = Executors.newScheduledThreadPool(2) + try { + sender(rpc, inputStream, hash, executor) + } finally { + executor.shutdown() + } + } + + private fun sender(rpc: CordaRPCOps, inputStream: InputStream, hash: SecureHash.SHA256, executor: ScheduledExecutorService) { + + // Get the identity key of the other side (the recipient). + val notaryFuture: CordaFuture = poll(executor, DUMMY_NOTARY_NAME.toString()) { rpc.wellKnownPartyFromX500Name(DUMMY_NOTARY_NAME) } + val otherSideFuture: CordaFuture = poll(executor, DUMMY_BANK_B_NAME.toString()) { rpc.wellKnownPartyFromX500Name(DUMMY_BANK_B_NAME) } + // Make sure we have the file in storage + if (!rpc.attachmentExists(hash)) { + inputStream.use { + val avail = inputStream.available() + val id = rpc.uploadAttachment(it) + require(hash == id) { "Id was '$id' instead of '$hash'" } + } + require(rpc.attachmentExists(hash)) + } + + val flowHandle = rpc.startTrackedFlow(::AttachmentDemoFlow, otherSideFuture.get(), notaryFuture.get(), hash) + flowHandle.progress.subscribe(::println) + val stx = flowHandle.returnValue.getOrThrow() + println("Sent ${stx.id}") + } This side is a bit more complex. Firstly it looks up its counterparty by name in the network map. Then, if the node doesn't already have the attachment in its storage, we upload it from a JAR resource and check the hash was what diff --git a/docs/source/tutorial-custom-notary.rst b/docs/source/tutorial-custom-notary.rst index cd102e484f..267252bd67 100644 --- a/docs/source/tutorial-custom-notary.rst +++ b/docs/source/tutorial-custom-notary.rst @@ -1,4 +1,8 @@ .. highlight:: kotlin +.. raw:: html + + + Writing a custom notary service (experimental) ============================================== @@ -12,19 +16,77 @@ Similarly to writing an oracle service, the first step is to create a service cl with ``@CordaService``. The Corda node scans for any class with this annotation and initialises them. The custom notary service class should provide a constructor with two parameters of types ``AppServiceHub`` and ``PublicKey``. -.. literalinclude:: ../../samples/notary-demo/src/main/kotlin/net/corda/notarydemo/MyCustomNotaryService.kt - :language: kotlin - :start-after: START 1 - :end-before: END 1 +.. container:: codeset + + .. code-block:: kotlin + + @CordaService + class MyCustomValidatingNotaryService(override val services: AppServiceHub, override val notaryIdentityKey: PublicKey) : TrustedAuthorityNotaryService() { + override val uniquenessProvider = PersistentUniquenessProvider(services.clock) + + override fun createServiceFlow(otherPartySession: FlowSession): FlowLogic = MyValidatingNotaryFlow(otherPartySession, this) + + override fun start() {} + override fun stop() {} + } The next step is to write a notary service flow. You are free to copy and modify the existing built-in flows such as ``ValidatingNotaryFlow``, ``NonValidatingNotaryFlow``, or implement your own from scratch (following the ``NotaryFlow.Service`` template). Below is an example of a custom flow for a *validating* notary service: -.. literalinclude:: ../../samples/notary-demo/src/main/kotlin/net/corda/notarydemo/MyCustomNotaryService.kt - :language: kotlin - :start-after: START 2 - :end-before: END 2 +.. container:: codeset + + .. code-block:: kotlin + + class MyValidatingNotaryFlow(otherSide: FlowSession, service: MyCustomValidatingNotaryService) : NotaryServiceFlow(otherSide, service) { + /** + * The received transaction is checked for contract-validity, for which the caller also has to to reveal the whole + * transaction dependency chain. + */ + @Suspendable + override fun validateRequest(requestPayload: NotarisationPayload): TransactionParts { + try { + val stx = requestPayload.signedTransaction + validateRequestSignature(NotarisationRequest(stx.inputs, stx.id), requestPayload.requestSignature) + val notary = stx.notary + checkNotary(notary) + verifySignatures(stx) + resolveAndContractVerify(stx) + val timeWindow: TimeWindow? = if (stx.coreTransaction is WireTransaction) stx.tx.timeWindow else null + return TransactionParts(stx.id, stx.inputs, timeWindow, notary!!) + } catch (e: Exception) { + throw when (e) { + is TransactionVerificationException, + is SignatureException -> NotaryInternalException(NotaryError.TransactionInvalid(e)) + else -> e + } + } + } + + @Suspendable + private fun resolveAndContractVerify(stx: SignedTransaction) { + subFlow(ResolveTransactionsFlow(stx, otherSideSession)) + stx.verify(serviceHub, false) + customVerify(stx) + } + + private fun verifySignatures(stx: SignedTransaction) { + val transactionWithSignatures = stx.resolveTransactionWithSignatures(serviceHub) + checkSignatures(transactionWithSignatures) + } + + private fun checkSignatures(tx: TransactionWithSignatures) { + try { + tx.verifySignaturesExcept(service.notaryIdentityKey) + } catch (e: SignatureException) { + throw NotaryInternalException(NotaryError.TransactionInvalid(e)) + } + } + + private fun customVerify(stx: SignedTransaction) { + // Add custom verification logic + } + } To enable the service, add the following to the node configuration: diff --git a/docs/source/tutorial-observer-nodes.rst b/docs/source/tutorial-observer-nodes.rst index 85940ac24c..355eae9dde 100644 --- a/docs/source/tutorial-observer-nodes.rst +++ b/docs/source/tutorial-observer-nodes.rst @@ -17,10 +17,42 @@ Just define a new flow that wraps the SendTransactionFlow/ReceiveTransactionFlow .. container:: codeset - .. literalinclude:: ../../samples/irs-demo/cordapp/src/main/kotlin/net/corda/irs/flows/AutoOfferFlow.kt - :language: kotlin - :start-after: DOCSTART 1 - :end-before: DOCEND 1 + .. code-block:: kotlin + + @InitiatedBy(Requester::class) + class AutoOfferAcceptor(otherSideSession: FlowSession) : Acceptor(otherSideSession) { + @Suspendable + override fun call(): SignedTransaction { + val finalTx = super.call() + // Our transaction is now committed to the ledger, so report it to our regulator. We use a custom flow + // that wraps SendTransactionFlow to allow the receiver to customise how ReceiveTransactionFlow is run, + // and because in a real life app you'd probably have more complex logic here e.g. describing why the report + // was filed, checking that the reportee is a regulated entity and not some random node from the wrong + // country and so on. + val regulator = serviceHub.identityService.partiesFromName("Regulator", true).single() + subFlow(ReportToRegulatorFlow(regulator, finalTx)) + return finalTx + } + } + + @InitiatingFlow + class ReportToRegulatorFlow(private val regulator: Party, private val finalTx: SignedTransaction) : FlowLogic() { + @Suspendable + override fun call() { + val session = initiateFlow(regulator) + subFlow(SendTransactionFlow(session, finalTx)) + } + } + + @InitiatedBy(ReportToRegulatorFlow::class) + class ReceiveRegulatoryReportFlow(private val otherSideSession: FlowSession) : FlowLogic() { + @Suspendable + override fun call() { + // Start the matching side of SendTransactionFlow above, but tell it to record all visible states even + // though they (as far as the node can tell) are nothing to do with us. + subFlow(ReceiveTransactionFlow(otherSideSession, true, StatesToRecord.ALL_VISIBLE)) + } + } In this example, the ``AutoOfferFlow`` is the business logic, and we define two very short and simple flows to send the transaction to the regulator. There are two important aspects to note here: diff --git a/docs/source/tutorial-tear-offs.rst b/docs/source/tutorial-tear-offs.rst index 4370965414..9b8b1b1a5e 100644 --- a/docs/source/tutorial-tear-offs.rst +++ b/docs/source/tutorial-tear-offs.rst @@ -1,3 +1,9 @@ +.. highlight:: kotlin +.. raw:: html + + + + Transaction tear-offs ===================== @@ -41,13 +47,42 @@ transaction components is exactly the same. Note that unlike ``WireTransaction`` :end-before: DOCEND 3 :dedent: 4 -The following code snippet is taken from ``NodeInterestRates.kt`` and implements a signing part of an Oracle. +The following code snippet is taken from the IRS Demo and implements a signing part of an Oracle. -.. literalinclude:: ../../samples/irs-demo/cordapp/src/main/kotlin/net/corda/irs/api/NodeInterestRates.kt - :language: kotlin - :start-after: DOCSTART 1 - :end-before: DOCEND 1 - :dedent: 8 +.. container:: codeset + + .. code-block:: kotlin + + fun sign(ftx: FilteredTransaction): TransactionSignature { + ftx.verify() + // Performing validation of obtained filtered components. + fun commandValidator(elem: Command<*>): Boolean { + require(services.myInfo.legalIdentities.first().owningKey in elem.signers && elem.value is Fix) { + "Oracle received unknown command (not in signers or not Fix)." + } + val fix = elem.value as Fix + val known = knownFixes[fix.of] + if (known == null || known != fix) + throw UnknownFix(fix.of) + return true + } + + fun check(elem: Any): Boolean { + return when (elem) { + is Command<*> -> commandValidator(elem) + else -> throw IllegalArgumentException("Oracle received data of different type than expected.") + } + } + + require(ftx.checkWithFun(::check)) + ftx.checkCommandVisibility(services.myInfo.legalIdentities.first().owningKey) + // It all checks out, so we can return a signature. + // + // Note that we will happily sign an invalid transaction, as we are only being presented with a filtered + // version so we can't resolve or check it ourselves. However, that doesn't matter much, as if we sign + // an invalid transaction the signature is worthless. + return services.createSignature(ftx, services.myInfo.legalIdentities.first().owningKey) + } .. note:: The way the ``FilteredTransaction`` is constructed ensures that after signing of the root hash it's impossible to add or remove components (leaves). However, it can happen that having transaction with multiple commands one party reveals only subset of them to the Oracle.