mirror of
https://github.com/corda/corda.git
synced 2025-05-29 05:34:22 +00:00
Fix missing snippets (#1154)
This commit is contained in:
parent
532a03adf8
commit
dbd07a8e6c
@ -67,11 +67,15 @@ Let's take an example of the interest rate swap fixings for our scheduled events
|
|||||||
|
|
||||||
.. container:: codeset
|
.. container:: codeset
|
||||||
|
|
||||||
.. literalinclude:: ../../samples/irs-demo/cordapp/src/main/kotlin/net/corda/irs/contract/IRS.kt
|
.. code-block:: kotlin
|
||||||
:language: kotlin
|
|
||||||
:start-after: DOCSTART 1
|
override fun nextScheduledActivity(thisStateRef: StateRef, flowLogicRefFactory: FlowLogicRefFactory): ScheduledActivity? {
|
||||||
:end-before: DOCEND 1
|
val nextFixingOf = nextFixingOf() ?: return null
|
||||||
:dedent: 8
|
|
||||||
|
// 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``
|
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
|
to indicate that there is no activity to schedule. Otherwise it calculates the ``Instant`` at which the interest rate
|
||||||
|
@ -101,15 +101,15 @@ class that binds it to the network layer.
|
|||||||
|
|
||||||
Here is an extract from the ``NodeInterestRates.Oracle`` class and supporting types:
|
Here is an extract from the ``NodeInterestRates.Oracle`` class and supporting types:
|
||||||
|
|
||||||
.. literalinclude:: ../../finance/src/main/kotlin/net/corda/finance/contracts/FinanceTypes.kt
|
.. sourcecode:: kotlin
|
||||||
:language: kotlin
|
|
||||||
:start-after: DOCSTART 1
|
|
||||||
:end-before: DOCEND 1
|
|
||||||
|
|
||||||
.. literalinclude:: ../../finance/src/main/kotlin/net/corda/finance/contracts/FinanceTypes.kt
|
@CordaSerializable
|
||||||
:language: kotlin
|
data class FixOf(val name: String, val forDay: LocalDate, val ofTenor: Tenor)
|
||||||
:start-after: DOCSTART 2
|
|
||||||
:end-before: DOCEND 2
|
.. 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
|
.. sourcecode:: kotlin
|
||||||
|
|
||||||
@ -166,11 +166,40 @@ parameter and ``CommandData`` classes.
|
|||||||
|
|
||||||
Let's see how the ``sign`` method for ``NodeInterestRates.Oracle`` is written:
|
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
|
.. container:: codeset
|
||||||
:language: kotlin
|
|
||||||
:start-after: DOCSTART 1
|
.. code-block:: kotlin
|
||||||
:end-before: DOCEND 1
|
|
||||||
:dedent: 8
|
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:
|
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
|
The first step is to create the oracle as a service by annotating its class with ``@CordaService``. Let's see how that's
|
||||||
done:
|
done:
|
||||||
|
|
||||||
.. literalinclude:: ../../samples/irs-demo/cordapp/src/main/kotlin/net/corda/irs/api/NodeInterestRates.kt
|
.. container:: codeset
|
||||||
:language: kotlin
|
|
||||||
:start-after: DOCSTART 3
|
.. code-block:: kotlin
|
||||||
:end-before: DOCEND 3
|
|
||||||
:dedent: 4
|
@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
|
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``.
|
a constructor with a single parameter of type ``ServiceHub``.
|
||||||
|
|
||||||
.. literalinclude:: ../../samples/irs-demo/cordapp/src/main/kotlin/net/corda/irs/api/NodeInterestRates.kt
|
.. container:: codeset
|
||||||
:language: kotlin
|
|
||||||
:start-after: DOCSTART 2
|
.. code-block:: kotlin
|
||||||
:end-before: DOCEND 2
|
|
||||||
:dedent: 4
|
@InitiatedBy(RatesFixFlow.FixSignFlow::class)
|
||||||
|
class FixSignHandler(private val otherPartySession: FlowSession) : FlowLogic<Unit>() {
|
||||||
|
@Suspendable
|
||||||
|
override fun call() {
|
||||||
|
val request = otherPartySession.receive<RatesFixFlow.SignRequest>().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<Unit>() {
|
||||||
|
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<RatesFixFlow.QueryRequest>().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,
|
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
|
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
|
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``.
|
those for ``NodeInterestRates.Oracle``.
|
||||||
|
|
||||||
.. literalinclude:: ../../samples/irs-demo/cordapp/src/main/kotlin/net/corda/irs/flows/RatesFixFlow.kt
|
.. container:: codeset
|
||||||
:language: kotlin
|
|
||||||
:start-after: DOCSTART 1
|
.. code-block:: kotlin
|
||||||
:end-before: DOCEND 1
|
|
||||||
:dedent: 4
|
@InitiatingFlow
|
||||||
|
class FixQueryFlow(val fixOf: FixOf, val oracle: Party) : FlowLogic<Fix>() {
|
||||||
|
@Suspendable
|
||||||
|
override fun call(): Fix {
|
||||||
|
val oracleSession = initiateFlow(oracle)
|
||||||
|
// TODO: add deadline to receive
|
||||||
|
val resp = oracleSession.sendAndReceive<List<Fix>>(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<TransactionSignature>() {
|
||||||
|
@Suspendable
|
||||||
|
override fun call(): TransactionSignature {
|
||||||
|
val oracleSession = initiateFlow(oracle)
|
||||||
|
val resp = oracleSession.sendAndReceive<TransactionSignature>(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'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
|
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
|
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.
|
called ``RatesFixFlow``. Here's the ``call`` method of that flow.
|
||||||
|
|
||||||
.. literalinclude:: ../../samples/irs-demo/cordapp/src/main/kotlin/net/corda/irs/flows/RatesFixFlow.kt
|
.. container:: codeset
|
||||||
:language: kotlin
|
|
||||||
:start-after: DOCSTART 2
|
.. code-block:: kotlin
|
||||||
:end-before: DOCEND 2
|
|
||||||
:dedent: 4
|
@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:
|
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``.
|
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
|
.. container:: codeset
|
||||||
:language: kotlin
|
|
||||||
:start-after: DOCSTART 1
|
.. code-block:: kotlin
|
||||||
:end-before: DOCEND 1
|
|
||||||
:dedent: 4
|
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::
|
.. note::
|
||||||
When overriding be careful when making the sub-class an anonymous or inner class (object declarations in Kotlin),
|
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
|
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.
|
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
|
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
|
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 service in each node. Then an oracle node with an oracle service which is initialised with some data is created on
|
||||||
the mock network:
|
the mock network:
|
||||||
|
|
||||||
.. literalinclude:: ../../samples/irs-demo/cordapp/src/test/kotlin/net/corda/irs/api/OracleNodeTearOffTests.kt
|
.. container:: codeset
|
||||||
:language: kotlin
|
|
||||||
:start-after: DOCSTART 1
|
.. code-block:: kotlin
|
||||||
:end-before: DOCEND 1
|
|
||||||
:dedent: 4
|
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.
|
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
|
.. container:: codeset
|
||||||
:language: kotlin
|
|
||||||
:start-after: DOCSTART 2
|
.. code-block:: kotlin
|
||||||
:end-before: DOCEND 2
|
|
||||||
:dedent: 4
|
@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 <https://github.com/corda/corda/samples/irs-demo/cordapp/src/test/kotlin/net/corda/irs/api/OracleNodeTearOffTests.kt>`_ for more examples.
|
See `here <https://github.com/corda/corda/samples/irs-demo/cordapp/src/test/kotlin/net/corda/irs/api/OracleNodeTearOffTests.kt>`_ for more examples.
|
||||||
|
@ -1,4 +1,8 @@
|
|||||||
.. highlight:: kotlin
|
.. highlight:: kotlin
|
||||||
|
.. raw:: html
|
||||||
|
|
||||||
|
<script type="text/javascript" src="_static/jquery.js"></script>
|
||||||
|
<script type="text/javascript" src="_static/codesets.js"></script>
|
||||||
|
|
||||||
Using attachments
|
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)
|
* Nullability (IS_NULL, NOT_NULL)
|
||||||
* Collection based (IN, NOT_IN)
|
* 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
|
:language: kotlin
|
||||||
:start-after: DOCSTART AttachmentQueryExample1
|
:start-after: DOCSTART AttachmentQueryExample1
|
||||||
:end-before: DOCEND AttachmentQueryExample1
|
:end-before: DOCEND AttachmentQueryExample1
|
||||||
:dedent: 12
|
:dedent: 8
|
||||||
|
|
||||||
Protocol
|
Protocol
|
||||||
--------
|
--------
|
||||||
@ -99,20 +105,85 @@ and if so, printed out.
|
|||||||
|
|
||||||
.. container:: codeset
|
.. container:: codeset
|
||||||
|
|
||||||
.. literalinclude:: ../../samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/AttachmentDemo.kt
|
.. code-block:: kotlin
|
||||||
:language: kotlin
|
|
||||||
:start-after: DOCSTART 1
|
fun recipient(rpc: CordaRPCOps, webPort: Int) {
|
||||||
:end-before: DOCEND 1
|
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<AttachmentContract.State>().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
|
The sender correspondingly builds a transaction with the attachment, then calls ``FinalityFlow`` to complete the
|
||||||
transaction and send it to the recipient node:
|
transaction and send it to the recipient node:
|
||||||
|
|
||||||
.. container:: codeset
|
.. container:: codeset
|
||||||
|
|
||||||
.. literalinclude:: ../../samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/AttachmentDemo.kt
|
.. code-block:: kotlin
|
||||||
:language: kotlin
|
|
||||||
:start-after: DOCSTART 2
|
fun sender(rpc: CordaRPCOps, numOfClearBytes: Int = 1024) { // default size 1K.
|
||||||
:end-before: DOCEND 2
|
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<Party> = poll(executor, DUMMY_NOTARY_NAME.toString()) { rpc.wellKnownPartyFromX500Name(DUMMY_NOTARY_NAME) }
|
||||||
|
val otherSideFuture: CordaFuture<Party> = 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
|
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
|
doesn't already have the attachment in its storage, we upload it from a JAR resource and check the hash was what
|
||||||
|
@ -1,4 +1,8 @@
|
|||||||
.. highlight:: kotlin
|
.. highlight:: kotlin
|
||||||
|
.. raw:: html
|
||||||
|
|
||||||
|
<script type="text/javascript" src="_static/jquery.js"></script>
|
||||||
|
<script type="text/javascript" src="_static/codesets.js"></script>
|
||||||
|
|
||||||
Writing a custom notary service (experimental)
|
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
|
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``.
|
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
|
.. container:: codeset
|
||||||
:language: kotlin
|
|
||||||
:start-after: START 1
|
.. code-block:: kotlin
|
||||||
:end-before: END 1
|
|
||||||
|
@CordaService
|
||||||
|
class MyCustomValidatingNotaryService(override val services: AppServiceHub, override val notaryIdentityKey: PublicKey) : TrustedAuthorityNotaryService() {
|
||||||
|
override val uniquenessProvider = PersistentUniquenessProvider(services.clock)
|
||||||
|
|
||||||
|
override fun createServiceFlow(otherPartySession: FlowSession): FlowLogic<Void?> = 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
|
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
|
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:
|
``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
|
.. container:: codeset
|
||||||
:language: kotlin
|
|
||||||
:start-after: START 2
|
.. code-block:: kotlin
|
||||||
:end-before: END 2
|
|
||||||
|
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:
|
To enable the service, add the following to the node configuration:
|
||||||
|
|
||||||
|
@ -17,10 +17,42 @@ Just define a new flow that wraps the SendTransactionFlow/ReceiveTransactionFlow
|
|||||||
|
|
||||||
.. container:: codeset
|
.. container:: codeset
|
||||||
|
|
||||||
.. literalinclude:: ../../samples/irs-demo/cordapp/src/main/kotlin/net/corda/irs/flows/AutoOfferFlow.kt
|
.. code-block:: kotlin
|
||||||
:language: kotlin
|
|
||||||
:start-after: DOCSTART 1
|
@InitiatedBy(Requester::class)
|
||||||
:end-before: DOCEND 1
|
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<Unit>() {
|
||||||
|
@Suspendable
|
||||||
|
override fun call() {
|
||||||
|
val session = initiateFlow(regulator)
|
||||||
|
subFlow(SendTransactionFlow(session, finalTx))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@InitiatedBy(ReportToRegulatorFlow::class)
|
||||||
|
class ReceiveRegulatoryReportFlow(private val otherSideSession: FlowSession) : FlowLogic<Unit>() {
|
||||||
|
@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
|
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:
|
the transaction to the regulator. There are two important aspects to note here:
|
||||||
|
@ -1,3 +1,9 @@
|
|||||||
|
.. highlight:: kotlin
|
||||||
|
.. raw:: html
|
||||||
|
|
||||||
|
<script type="text/javascript" src="_static/jquery.js"></script>
|
||||||
|
<script type="text/javascript" src="_static/codesets.js"></script>
|
||||||
|
|
||||||
Transaction tear-offs
|
Transaction tear-offs
|
||||||
=====================
|
=====================
|
||||||
|
|
||||||
@ -41,13 +47,42 @@ transaction components is exactly the same. Note that unlike ``WireTransaction``
|
|||||||
:end-before: DOCEND 3
|
:end-before: DOCEND 3
|
||||||
:dedent: 4
|
: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
|
.. container:: codeset
|
||||||
:language: kotlin
|
|
||||||
:start-after: DOCSTART 1
|
.. code-block:: kotlin
|
||||||
:end-before: DOCEND 1
|
|
||||||
:dedent: 8
|
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
|
.. 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.
|
components (leaves). However, it can happen that having transaction with multiple commands one party reveals only subset of them to the Oracle.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user