mirror of
https://github.com/corda/corda.git
synced 2025-01-27 14:49:35 +00:00
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:
parent
5f79acaa88
commit
9a75adb0ab
@ -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>
|
@ -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>
|
@ -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.
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user