Simplify the attachments demo by getting rid of the web API and just using the RPC interface directly. The web API in this case wasn't actually adding anything that had to be server side.

This commit is contained in:
Mike Hearn 2017-01-06 14:35:07 +01:00
parent 5f79acaa88
commit 9a75adb0ab
9 changed files with 189 additions and 247 deletions

View File

@ -4,7 +4,7 @@
<option name="MAIN_CLASS_NAME" value="net.corda.attachmentdemo.AttachmentDemoKt" />
<option name="VM_PARAMETERS" value="-ea -javaagent:lib/quasar.jar " />
<option name="PROGRAM_PARAMETERS" value="--role=RECIPIENT" />
<option name="WORKING_DIRECTORY" value="" />
<option name="WORKING_DIRECTORY" value="file://samples/attachment-demo" />
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
<option name="ALTERNATIVE_JRE_PATH" />
<option name="PASS_PARENT_ENVS" value="true" />
@ -12,4 +12,4 @@
<envs />
<method />
</configuration>
</component>
</component>

View File

@ -4,7 +4,7 @@
<option name="MAIN_CLASS_NAME" value="net.corda.attachmentdemo.AttachmentDemoKt" />
<option name="VM_PARAMETERS" value="-ea -javaagent:lib/quasar.jar " />
<option name="PROGRAM_PARAMETERS" value="--role SENDER" />
<option name="WORKING_DIRECTORY" value="" />
<option name="WORKING_DIRECTORY" value="file://samples/attachment-demo" />
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
<option name="ALTERNATIVE_JRE_PATH" />
<option name="PASS_PARENT_ENVS" value="true" />
@ -12,4 +12,4 @@
<envs />
<method />
</configuration>
</component>
</component>

View File

@ -11,10 +11,9 @@ nodes already. Examples include:
* Metadata about a transaction, such as PDF version of an invoice being settled
* Shared information to be permanently recorded on the ledger
To add attachments the file must first be added to the node's storage service using ``StorageService.importAttachment()``,
which returns a unique ID that can be added using ``TransactionBuilder.addAttachment()``. Attachments can also be
uploaded and downloaded via HTTP, to enable integration with external systems. For instructions on HTTP upload/download
please see ":doc:`node-administration`".
To add attachments the file must first be added to uploaded to the node, which returns a unique ID that can be added
using ``TransactionBuilder.addAttachment()``. Attachments can be uploaded and downloaded via RPC and HTTP. For
instructions on HTTP upload/download please see ":doc:`node-administration`".
Normally attachments on transactions are fetched automatically via the ``ResolveTransactionsFlow`` when verifying
received transactions. Attachments are needed in order to validate a transaction (they include, for example, the
@ -30,69 +29,74 @@ There is a worked example of attachments, which relays a simple document from on
trade flow" also includes an attachment, however it is a significantly more complex demo, and less well suited
for a tutorial.
The demo code is in the file "src/main/kotlin/net.corda.demos/attachment/AttachmentDemo.kt", with the core logic
contained within the two functions ``runRecipient()`` and ``runSender()``. We'll look at the recipient function first;
this subscribes to notifications of new validated transactions, and if it receives a transaction containing attachments,
loads the first attachment from storage, and checks it matches the expected attachment ID. ``ResolveTransactionsFlow``
has already fetched all attachments from the remote node, and as such the attachments are available from the node's
storage service. Once the attachment is verified, the node shuts itself down.
The demo code is in the file ``samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/AttachmentDemo.kt``,
with the core logic contained within the two functions ``recipient()`` and ``sender()``. The first thing it does is set
up an RPC connection to node B using a demo user account (this is all configured in the gradle build script for the demo
and the nodes will be created using the ``deployNodes`` gradle task as normal). The ``CordaRPCClient.use`` method is a
convenience helper intended for small tools that sets up an RPC connection scoped to the provided block, and brings all
the RPCs into scope. Once connected the sender/recipient functions are run with the RPC proxy as a parameter.
We'll look at the recipient function first.
The first thing it does is wait to receive a notification of a new transaction by calling the ``verifiedTransactions``
RPC, which returns both a snapshot and an observable of changes. The observable is made blocking and the next
transaction the node verifies is retrieved. That transaction is checked to see if it has the expected attachment
and if so, printed out.
.. sourcecode:: kotlin
private fun runRecipient(node: Node) {
val serviceHub = node.services
// Normally we would receive the transaction from a more specific flow, but in this case we let [FinalityFlow]
// handle receiving it for us.
serviceHub.storageService.validatedTransactions.updates.subscribe { event ->
// When the transaction is received, it's passed through [ResolveTransactionsFlow], which first fetches any
// attachments for us, then verifies the transaction. As such, by the time it hits the validated transaction store,
// we have a copy of the attachment.
val tx = event.tx
if (tx.attachments.isNotEmpty()) {
val attachment = serviceHub.storageService.attachments.openAttachment(tx.attachments.first())
assertEquals(PROSPECTUS_HASH, attachment?.id)
println("File received - we're happy!\n\nFinal transaction is:\n\n${Emoji.renderIfSupported(event.tx)}")
thread {
node.stop()
}
}
}
}
fun recipient(rpc: CordaRPCOps) {
println("Waiting to receive transaction ...")
val stx = rpc.verifiedTransactions().second.toBlocking().first()
val wtx = stx.tx
if (wtx.attachments.isNotEmpty()) {
assertEquals(PROSPECTUS_HASH, wtx.attachments.first())
require(rpc.attachmentExists(PROSPECTUS_HASH))
println("File received - we're happy!\n\nFinal transaction is:\n\n${Emoji.renderIfSupported(wtx)}")
} 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:
.. sourcecode:: kotlin
private fun runSender(node: Node, otherSide: Party) {
val serviceHub = node.services
// Make sure we have the file in storage
if (serviceHub.storageService.attachments.openAttachment(PROSPECTUS_HASH) == null) {
net.corda.demos.Role::class.java.getResourceAsStream("bank-of-london-cp.jar").use {
val id = node.storage.attachments.importAttachment(it)
assertEquals(PROSPECTUS_HASH, id)
}
}
fun sender(rpc: CordaRPCOps) {
// Get the identity key of the other side (the recipient).
val otherSide: Party = rpc.partyFromName("Bank B")!!
// Create a trivial transaction that just passes across the attachment - in normal cases there would be
// inputs, outputs and commands that refer to this attachment.
val ptx = TransactionType.General.Builder(notary = null)
ptx.addAttachment(serviceHub.storageService.attachments.openAttachment(PROSPECTUS_HASH)!!.id)
// Make sure we have the file in storage
// TODO: We should have our own demo file, not share the trader demo file
if (!rpc.attachmentExists(PROSPECTUS_HASH)) {
Thread.currentThread().contextClassLoader.getResourceAsStream("bank-of-london-cp.jar").use {
val id = rpc.uploadAttachment(it)
assertEquals(PROSPECTUS_HASH, id)
}
}
// Despite not having any states, we have to have at least one signature on the transaction
ptx.signWith(ALICE_KEY)
// Create a trivial transaction that just passes across the attachment - in normal cases there would be
// inputs, outputs and commands that refer to this attachment.
val ptx = TransactionType.General.Builder(notary = null)
require(rpc.attachmentExists(PROSPECTUS_HASH))
ptx.addAttachment(PROSPECTUS_HASH)
// TODO: Add a dummy state and specify a notary, so that the tx hash is randomised each time and the demo can be repeated.
// Send the transaction to the other recipient
val tx = ptx.toSignedTransaction()
serviceHub.startFlow(LOG_SENDER, FinalityFlow(tx, emptySet(), setOf(otherSide))).success {
thread {
Thread.sleep(1000L) // Give the other side time to request the attachment
node.stop()
}
}.failure {
println("Failed to relay message ")
}
}
// Despite not having any states, we have to have at least one signature on the transaction
ptx.signWith(ALICE_KEY)
// Send the transaction to the other recipient
val stx = ptx.toSignedTransaction()
println("Sending ${stx.id}")
val protocolHandle = rpc.startFlow(::FinalityFlow, stx, setOf(otherSide))
protocolHandle.progress.subscribe(::println)
protocolHandle.returnValue.toBlocking().first()
}
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
we expected. Then a trivial transaction is built that has the attachment and a single signature and it's sent to
the other side using the FinalityFlow. The result of starting the flow is a stream of progress messages and a
``returnValue`` observable that can be used to watch out for the flow completing successfully.

View File

@ -60,6 +60,8 @@ dependencies {
}
task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['build']) {
ext.rpcUsers = [ ['user' : "demo", 'password' : "demo", 'permissions' : ["StartFlow.net.corda.flows.FinalityFlow"]] ]
directory "./build/nodes"
networkMap "Controller"
node {
@ -70,6 +72,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['build']) {
artemisPort 10002
webPort 10003
cordapps = []
rpcUsers = ext.rpcUsers
}
node {
name "Bank A"
@ -79,6 +82,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['build']) {
artemisPort 10004
webPort 10005
cordapps = []
rpcUsers = ext.rpcUsers
}
node {
name "Bank B"
@ -88,6 +92,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['build']) {
artemisPort 10006
webPort 10007
cordapps = []
rpcUsers = ext.rpcUsers
}
}

View File

@ -4,33 +4,35 @@ import com.google.common.util.concurrent.Futures
import net.corda.core.getOrThrow
import net.corda.core.node.services.ServiceInfo
import net.corda.node.driver.driver
import net.corda.node.services.User
import net.corda.node.services.transactions.SimpleNotaryService
import org.junit.Test
import kotlin.concurrent.thread
import java.util.concurrent.CompletableFuture
class AttachmentDemoTest {
@Test fun `runs attachment demo`() {
driver(dsl = {
val demoUser = listOf(User("demo", "demo", setOf("StartFlow.net.corda.flows.FinalityFlow")))
val (nodeA, nodeB) = Futures.allAsList(
startNode("Bank A"),
startNode("Bank B"),
startNode("Bank A", rpcUsers = demoUser),
startNode("Bank B", rpcUsers = demoUser),
startNode("Notary", setOf(ServiceInfo(SimpleNotaryService.Companion.type)))
).getOrThrow()
var recipientReturn: Boolean? = null
var senderReturn: Boolean? = null
val recipientThread = thread {
recipientReturn = AttachmentDemoClientApi(nodeA.configuration.webAddress).runRecipient()
val senderThread = CompletableFuture.runAsync {
nodeA.rpcClientToNode().use(demoUser[0].username, demoUser[0].password) {
sender(this)
}
}
val senderThread = thread {
val counterpartyKey = nodeA.nodeInfo.legalIdentity.owningKey.toBase58String()
senderReturn = AttachmentDemoClientApi(nodeB.configuration.webAddress).runSender(counterpartyKey)
val recipientThread = CompletableFuture.runAsync {
nodeB.rpcClientToNode().use(demoUser[0].username, demoUser[0].password) {
recipient(this)
}
}
recipientThread.join()
senderThread.join()
assert(recipientReturn == true)
assert(senderReturn == true)
// Just check they don't throw any exceptions.d
recipientThread.get()
senderThread.get()
}, isDebug = true)
}
}

View File

@ -2,52 +2,119 @@ package net.corda.attachmentdemo
import com.google.common.net.HostAndPort
import joptsimple.OptionParser
import net.corda.core.utilities.loggerFor
import net.corda.core.contracts.TransactionType
import net.corda.core.crypto.Party
import net.corda.core.crypto.SecureHash
import net.corda.core.div
import net.corda.core.messaging.CordaRPCOps
import net.corda.core.messaging.startFlow
import net.corda.core.utilities.Emoji
import net.corda.flows.FinalityFlow
import net.corda.node.services.config.NodeSSLConfiguration
import net.corda.node.services.messaging.CordaRPCClient
import net.corda.testing.ALICE_KEY
import java.nio.file.Path
import java.nio.file.Paths
import kotlin.system.exitProcess
import kotlin.test.assertEquals
fun main(args: Array<String>) {
AttachmentDemo().main(args)
internal enum class Role {
SENDER,
RECIPIENT
}
private class AttachmentDemo {
internal enum class Role() {
SENDER,
RECIPIENT
fun main(args: Array<String>) {
val parser = OptionParser()
val roleArg = parser.accepts("role").withRequiredArg().ofType(Role::class.java).required()
val options = try {
parser.parse(*args)
} catch (e: Exception) {
println(e.message)
printHelp(parser)
exitProcess(1)
}
private companion object {
val log = loggerFor<AttachmentDemo>()
}
fun main(args: Array<String>) {
val parser = OptionParser()
val roleArg = parser.accepts("role").withRequiredArg().ofType(Role::class.java).required()
val options = try {
parser.parse(*args)
} catch (e: Exception) {
log.error(e.message)
printHelp(parser)
exitProcess(1)
}
val role = options.valueOf(roleArg)!!
when (role) {
Role.SENDER -> {
val api = AttachmentDemoClientApi(HostAndPort.fromString("localhost:10005"))
api.runSender(api.getOtherSideKey())
val role = options.valueOf(roleArg)!!
when (role) {
Role.SENDER -> {
val host = HostAndPort.fromString("localhost:10004")
println("Connecting to sender node ($host)")
CordaRPCClient(host, sslConfigFor("nodea")).use("demo", "demo") {
sender(this)
}
Role.RECIPIENT -> AttachmentDemoClientApi(HostAndPort.fromString("localhost:10007")).runRecipient()
}
Role.RECIPIENT -> {
val host = HostAndPort.fromString("localhost:10006")
println("Connecting to the recipient node ($host)")
CordaRPCClient(host, sslConfigFor("nodeb")).use("demo", "demo") {
recipient(this)
}
}
}
}
val PROSPECTUS_HASH = SecureHash.parse("decd098666b9657314870e192ced0c3519c2c9d395507a238338f8d003929de9")
fun sender(rpc: CordaRPCOps) {
// Get the identity key of the other side (the recipient).
val otherSide: Party = rpc.partyFromName("Bank B")!!
// Make sure we have the file in storage
// TODO: We should have our own demo file, not share the trader demo file
if (!rpc.attachmentExists(PROSPECTUS_HASH)) {
Thread.currentThread().contextClassLoader.getResourceAsStream("bank-of-london-cp.jar").use {
val id = rpc.uploadAttachment(it)
assertEquals(PROSPECTUS_HASH, id)
}
}
private fun printHelp(parser: OptionParser) {
println("""
// Create a trivial transaction that just passes across the attachment - in normal cases there would be
// inputs, outputs and commands that refer to this attachment.
val ptx = TransactionType.General.Builder(notary = null)
require(rpc.attachmentExists(PROSPECTUS_HASH))
ptx.addAttachment(PROSPECTUS_HASH)
// TODO: Add a dummy state and specify a notary, so that the tx hash is randomised each time and the demo can be repeated.
// Despite not having any states, we have to have at least one signature on the transaction
ptx.signWith(ALICE_KEY)
// Send the transaction to the other recipient
val stx = ptx.toSignedTransaction()
println("Sending ${stx.id}")
val protocolHandle = rpc.startFlow(::FinalityFlow, stx, setOf(otherSide))
protocolHandle.progress.subscribe(::println)
protocolHandle.returnValue.toBlocking().first()
}
fun recipient(rpc: CordaRPCOps) {
println("Waiting to receive transaction ...")
val stx = rpc.verifiedTransactions().second.toBlocking().first()
val wtx = stx.tx
if (wtx.attachments.isNotEmpty()) {
assertEquals(PROSPECTUS_HASH, wtx.attachments.first())
require(rpc.attachmentExists(PROSPECTUS_HASH))
println("File received - we're happy!\n\nFinal transaction is:\n\n${Emoji.renderIfSupported(wtx)}")
} else {
println("Error: no attachments found in ${wtx.id}")
}
}
private fun printHelp(parser: OptionParser) {
println("""
Usage: attachment-demo --role [RECIPIENT|SENDER] [options]
Please refer to the documentation in docs/build/index.html for more info.
""".trimIndent())
parser.printHelpOn(System.out)
}
parser.printHelpOn(System.out)
}
// TODO: Take this out once we have a dedicated RPC port and allow SSL on it to be optional.
private fun sslConfigFor(nodename: String): NodeSSLConfiguration {
return object : NodeSSLConfiguration {
override val keyStorePassword: String = "cordacadevpass"
override val trustStorePassword: String = "trustpass"
override val certificatesPath: Path = Paths.get("build") / "nodes" / nodename / "certificates"
}
}

View File

@ -1,35 +0,0 @@
package net.corda.attachmentdemo
import com.google.common.net.HostAndPort
import net.corda.testing.http.HttpApi
import okhttp3.OkHttpClient
import okhttp3.Request
import java.util.concurrent.TimeUnit
/**
* Interface for using the attachment demo API from a client.
*/
class AttachmentDemoClientApi(val hostAndPort: HostAndPort) {
private val api = HttpApi.fromHostAndPort(hostAndPort, apiRoot)
fun runRecipient(): Boolean {
return api.postJson("await-transaction")
}
fun runSender(otherSide: String): Boolean {
return api.postJson("$otherSide/send")
}
fun getOtherSideKey(): String {
// TODO: Add getJson to the API utils
val client = OkHttpClient.Builder().connectTimeout(5, TimeUnit.SECONDS).readTimeout(60, TimeUnit.SECONDS).build()
val request = Request.Builder().url("http://$hostAndPort/$apiRoot/other-side-key").build()
val response = client.newCall(request).execute()
require(response.isSuccessful) // TODO: Handle more gracefully.
return response.body().string()
}
private companion object {
private val apiRoot = "api/attachmentdemo"
}
}

View File

@ -1,97 +0,0 @@
package net.corda.attachmentdemo.api
import net.corda.core.contracts.TransactionType
import net.corda.core.crypto.SecureHash
import net.corda.core.messaging.CordaRPCOps
import net.corda.core.messaging.startFlow
import net.corda.core.utilities.ApiUtils
import net.corda.core.utilities.Emoji
import net.corda.core.utilities.loggerFor
import net.corda.flows.FinalityFlow
import net.corda.testing.ALICE_KEY
import java.util.concurrent.CompletableFuture
import javax.ws.rs.*
import javax.ws.rs.core.MediaType
import javax.ws.rs.core.Response
import kotlin.test.assertEquals
@Path("attachmentdemo")
class AttachmentDemoApi(val rpc: CordaRPCOps) {
private val utils = ApiUtils(rpc)
private companion object {
val PROSPECTUS_HASH = SecureHash.parse("decd098666b9657314870e192ced0c3519c2c9d395507a238338f8d003929de9")
val logger = loggerFor<AttachmentDemoApi>()
}
@POST
@Path("{party}/send")
@Consumes(MediaType.APPLICATION_JSON)
fun runSender(@PathParam("party") partyKey: String): Response {
return utils.withParty(partyKey) {
// Make sure we have the file in storage
// TODO: We should have our own demo file, not share the trader demo file
if (!rpc.attachmentExists(PROSPECTUS_HASH)) {
javaClass.classLoader.getResourceAsStream("bank-of-london-cp.jar").use {
val id = rpc.uploadAttachment(it)
assertEquals(PROSPECTUS_HASH, id)
}
}
// Create a trivial transaction that just passes across the attachment - in normal cases there would be
// inputs, outputs and commands that refer to this attachment.
val ptx = TransactionType.General.Builder(notary = null)
require(rpc.attachmentExists(PROSPECTUS_HASH))
ptx.addAttachment(PROSPECTUS_HASH)
// Despite not having any states, we have to have at least one signature on the transaction
ptx.signWith(ALICE_KEY)
// Send the transaction to the other recipient
val tx = ptx.toSignedTransaction()
val protocolHandle = rpc.startFlow(::FinalityFlow, tx, setOf(it))
protocolHandle.returnValue.toBlocking().first()
Response.accepted().build()
}
}
@POST
@Path("await-transaction")
@Consumes(MediaType.APPLICATION_JSON)
fun runRecipient(): Response {
val future = CompletableFuture<Response>()
// Normally we would receive the transaction from a more specific flow, but in this case we let [FinalityFlow]
// handle receiving it for us.
rpc.verifiedTransactions().second.subscribe { event ->
// When the transaction is received, it's passed through [ResolveTransactionsFlow], which first fetches any
// attachments for us, then verifies the transaction. As such, by the time it hits the validated transaction store,
// we have a copy of the attachment.
val tx = event.tx
val response = if (tx.attachments.isNotEmpty()) {
assertEquals(PROSPECTUS_HASH, tx.attachments.first())
require(rpc.attachmentExists(PROSPECTUS_HASH))
println("File received - we're happy!\n\nFinal transaction is:\n\n${Emoji.renderIfSupported(event.tx)}")
Response.ok().entity("Final transaction is: ${Emoji.renderIfSupported(event.tx)}").build()
} else {
Response.serverError().entity("No attachments passed").build()
}
future.complete(response)
}
return future.get()
}
/**
* Gets details of the other side. To be removed when identity API is added.
*/
@GET
@Path("other-side-key")
@Produces(MediaType.APPLICATION_JSON)
fun getOtherSide(): Response? {
val myInfo = rpc.nodeIdentity()
val key = rpc.networkMapUpdates().first.first { it != myInfo }.legalIdentity.owningKey.toBase58String()
return Response.ok().entity(key).build()
}
}

View File

@ -1,14 +1,10 @@
package net.corda.attachmentdemo.plugin
import net.corda.attachmentdemo.api.AttachmentDemoApi
import net.corda.core.node.CordaPluginRegistry
import net.corda.core.transactions.SignedTransaction
import net.corda.flows.FinalityFlow
import net.corda.core.node.CordaPluginRegistry
import java.util.function.Function
class AttachmentDemoPlugin : CordaPluginRegistry() {
// A list of classes that expose web APIs.
override val webApis = listOf(Function(::AttachmentDemoApi))
// A list of Flows that are required for this cordapp
override val requiredFlows: Map<String, Set<String>> = mapOf(
FinalityFlow::class.java.name to setOf(SignedTransaction::class.java.name, setOf(Unit).javaClass.name, setOf(Unit).javaClass.name)