mirror of
https://github.com/corda/corda.git
synced 2025-06-12 20:28:18 +00:00
Reimport samples to main repo
This commit is contained in:
64
samples/trader-demo/README.md
Normal file
64
samples/trader-demo/README.md
Normal file
@ -0,0 +1,64 @@
|
||||
# Trader Demo
|
||||
|
||||
Please see docs/build/html/running-the-demos.html
|
||||
|
||||
This program is a simple driver for exercising the two party trading protocol. Until Corda has a unified node server
|
||||
programs like this are required to wire up the pieces and run a demo scenario end to end.
|
||||
|
||||
If you are creating a new scenario, you can use this program as a template for creating your own driver. Make sure to
|
||||
copy/paste the right parts of the build.gradle file to make sure it gets a script to run it deposited in
|
||||
build/install/r3prototyping/bin
|
||||
|
||||
In this scenario, a buyer wants to purchase some commercial paper by swapping his cash for the CP. The seller learns
|
||||
that the buyer exists, and sends them a message to kick off the trade. The seller, having obtained his CP, then quits
|
||||
and the buyer goes back to waiting. The buyer will sell as much CP as he can!
|
||||
|
||||
The different roles in the scenario this program can adopt are:
|
||||
|
||||
This template contains the build system and an example application required to get started with [Corda](http://todo.todo).
|
||||
|
||||
## Prerequisites
|
||||
|
||||
You will need to have [JDK 8](http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html)
|
||||
installed and available on your path.
|
||||
|
||||
## Getting Started
|
||||
|
||||
First clone this repository and the Corda repository locally. Then open a terminal window in the Corda directory and run:
|
||||
|
||||
Unix:
|
||||
|
||||
./gradlew publishToMavenLocal
|
||||
|
||||
Windows:
|
||||
|
||||
gradle.bat publishToMavenLocal
|
||||
|
||||
This will publish a copy of Corda to your local Maven repository for your Cordapp to use. Next open a terminal window
|
||||
in your Cordapp directory (this one) and run:
|
||||
|
||||
Unix:
|
||||
|
||||
./gradlew deployNodes
|
||||
|
||||
Windows:
|
||||
|
||||
gradlew.bat deployNodes
|
||||
|
||||
This command will create several nodes in `build/nodes` that you can now run with:
|
||||
|
||||
Unix:
|
||||
|
||||
cd build/nodes
|
||||
./runnodes
|
||||
|
||||
Windows:
|
||||
|
||||
Windows users currently have to manually enter each directory in `build/nodes` and run `java -jar corda.jar` in each.
|
||||
This will be updated soon.
|
||||
|
||||
This will now have nodes running on your machine running this Cordapp. You can now begin developing your Cordapp.
|
||||
|
||||
## Further Reading
|
||||
|
||||
Tutorials and developer docs for Cordapps and Corda are [here](https://docs.corda.r3cev.com).
|
139
samples/trader-demo/build.gradle
Normal file
139
samples/trader-demo/build.gradle
Normal file
@ -0,0 +1,139 @@
|
||||
buildscript {
|
||||
repositories {
|
||||
mavenLocal()
|
||||
mavenCentral()
|
||||
jcenter()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
classpath "net.corda.plugins:quasar-utils:$corda_version"
|
||||
classpath "net.corda.plugins:publish-utils:$corda_version"
|
||||
classpath "net.corda.plugins:cordformation:$corda_version"
|
||||
}
|
||||
}
|
||||
|
||||
apply plugin: 'java'
|
||||
apply plugin: 'kotlin'
|
||||
apply plugin: 'idea'
|
||||
apply plugin: 'net.corda.plugins.quasar-utils'
|
||||
apply plugin: 'net.corda.plugins.publish-utils'
|
||||
apply plugin: 'net.corda.plugins.cordformation'
|
||||
apply plugin: 'maven-publish'
|
||||
|
||||
repositories {
|
||||
mavenLocal()
|
||||
mavenCentral()
|
||||
jcenter()
|
||||
maven {
|
||||
url 'https://dl.bintray.com/kotlin/exposed'
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main {
|
||||
resources {
|
||||
srcDir "config/dev"
|
||||
}
|
||||
}
|
||||
test {
|
||||
resources {
|
||||
srcDir "config/test"
|
||||
}
|
||||
}
|
||||
integrationTest {
|
||||
kotlin {
|
||||
compileClasspath += main.output + test.output
|
||||
runtimeClasspath += main.output + test.output
|
||||
srcDir file('src/integration-test/kotlin')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
configurations {
|
||||
integrationTestCompile.extendsFrom testCompile
|
||||
integrationTestRuntime.extendsFrom testRuntime
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||
testCompile group: 'junit', name: 'junit', version: '4.11'
|
||||
|
||||
// Corda integration dependencies
|
||||
compile "net.corda:client:$corda_version"
|
||||
compile "net.corda:core:$corda_version"
|
||||
compile "net.corda:finance:$corda_version"
|
||||
compile "net.corda:node:$corda_version"
|
||||
compile "net.corda:corda:$corda_version"
|
||||
compile "net.corda:test-utils:$corda_version"
|
||||
|
||||
// Javax is required for webapis
|
||||
compile "org.glassfish.jersey.core:jersey-server:${jersey_version}"
|
||||
|
||||
// GraphStream: For visualisation (required by ExampleClientRPC app)
|
||||
compile "org.graphstream:gs-core:1.3"
|
||||
compile("org.graphstream:gs-ui:1.3") {
|
||||
exclude group: "bouncycastle"
|
||||
}
|
||||
|
||||
// Cordapp dependencies
|
||||
// Specify your cordapp's dependencies below, including dependent cordapps
|
||||
}
|
||||
|
||||
task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['build']) {
|
||||
directory "./build/nodes"
|
||||
networkMap "Controller"
|
||||
node {
|
||||
name "Controller"
|
||||
dirName "controller"
|
||||
nearestCity "London"
|
||||
advertisedServices = ["corda.notary.validating"]
|
||||
artemisPort 10002
|
||||
webPort 10003
|
||||
cordapps = []
|
||||
}
|
||||
node {
|
||||
name "NodeA"
|
||||
dirName "nodea"
|
||||
nearestCity "London"
|
||||
advertisedServices = []
|
||||
artemisPort 10004
|
||||
webPort 10005
|
||||
cordapps = []
|
||||
}
|
||||
node {
|
||||
name "NodeB"
|
||||
dirName "nodeb"
|
||||
nearestCity "New York"
|
||||
advertisedServices = []
|
||||
artemisPort 10006
|
||||
webPort 10007
|
||||
cordapps = []
|
||||
}
|
||||
}
|
||||
|
||||
task integrationTest(type: Test, dependsOn: []) {
|
||||
testClassesDir = sourceSets.integrationTest.output.classesDir
|
||||
classpath = sourceSets.integrationTest.runtimeClasspath
|
||||
}
|
||||
|
||||
idea {
|
||||
module {
|
||||
downloadJavadoc = true // defaults to false
|
||||
downloadSources = true
|
||||
}
|
||||
}
|
||||
|
||||
quasarScan.dependsOn('classes')
|
||||
|
||||
publishing {
|
||||
publications {
|
||||
jarAndSources(MavenPublication) {
|
||||
from components.java
|
||||
artifactId 'traderdemo'
|
||||
|
||||
artifact sourceJar
|
||||
artifact javadocJar
|
||||
}
|
||||
}
|
||||
}
|
59
samples/trader-demo/config/dev/log4j2.xml
Normal file
59
samples/trader-demo/config/dev/log4j2.xml
Normal file
@ -0,0 +1,59 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Configuration status="info">
|
||||
|
||||
<Properties>
|
||||
<Property name="log-path">logs</Property>
|
||||
<Property name="log-name">node-${hostName}</Property>
|
||||
<Property name="archive">${log-path}/archive</Property>
|
||||
</Properties>
|
||||
|
||||
<ThresholdFilter level="trace"/>
|
||||
|
||||
<Appenders>
|
||||
<Console name="Console-Appender" target="SYSTEM_OUT">
|
||||
<PatternLayout>
|
||||
<pattern>
|
||||
%highlight{%level{length=1} %d{HH:mm:ss} %T %c{1}.%M - %msg%n}{INFO=white,WARN=red,FATAL=bright red blink}
|
||||
</pattern>>
|
||||
</PatternLayout>
|
||||
</Console>
|
||||
|
||||
<!-- Will generate up to 10 log files for a given day. During every rollover it will delete
|
||||
those that are older than 60 days, but keep the most recent 10 GB -->
|
||||
<RollingFile name="RollingFile-Appender"
|
||||
fileName="${log-path}/${log-name}.log"
|
||||
filePattern="${archive}/${log-name}.%d{yyyy-MM-dd}-%i.log.gz">
|
||||
|
||||
<PatternLayout pattern="[%-5level] %d{ISO8601}{GMT+0} [%t] %c{1} - %msg%n"/>
|
||||
|
||||
<Policies>
|
||||
<TimeBasedTriggeringPolicy/>
|
||||
<SizeBasedTriggeringPolicy size="10MB"/>
|
||||
</Policies>
|
||||
|
||||
<DefaultRolloverStrategy min="1" max="10">
|
||||
<Delete basePath="${archive}" maxDepth="1">
|
||||
<IfFileName glob="${log-name}*.log.gz"/>
|
||||
<IfLastModified age="60d">
|
||||
<IfAny>
|
||||
<IfAccumulatedFileSize exceeds="10 GB"/>
|
||||
</IfAny>
|
||||
</IfLastModified>
|
||||
</Delete>
|
||||
</DefaultRolloverStrategy>
|
||||
|
||||
</RollingFile>
|
||||
</Appenders>
|
||||
|
||||
<Loggers>
|
||||
<Root level="info">
|
||||
<AppenderRef ref="Console-Appender"/>
|
||||
<AppenderRef ref="RollingFile-Appender"/>
|
||||
</Root>
|
||||
<Logger name="com.r3corda" level="info" additivity="false">
|
||||
<AppenderRef ref="Console-Appender"/>
|
||||
<AppenderRef ref="RollingFile-Appender"/>
|
||||
</Logger>
|
||||
</Loggers>
|
||||
|
||||
</Configuration>
|
20
samples/trader-demo/config/test/log4j2.xml
Normal file
20
samples/trader-demo/config/test/log4j2.xml
Normal file
@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Configuration status="info">
|
||||
<Appenders>
|
||||
<Console name="Console-Appender" target="SYSTEM_OUT">
|
||||
<PatternLayout>
|
||||
<pattern>
|
||||
[%-5level] %d{HH:mm:ss.SSS} [%t] %c{1}.%M - %msg%n
|
||||
</pattern>>
|
||||
</PatternLayout>
|
||||
</Console>
|
||||
</Appenders>
|
||||
<Loggers>
|
||||
<Root level="info">
|
||||
<AppenderRef ref="Console-Appender"/>
|
||||
</Root>
|
||||
<Logger name="com.r3corda" level="info" additivity="false">
|
||||
<AppenderRef ref="Console-Appender"/>
|
||||
</Logger>
|
||||
</Loggers>
|
||||
</Configuration>
|
2
samples/trader-demo/gradle.properties
Normal file
2
samples/trader-demo/gradle.properties
Normal file
@ -0,0 +1,2 @@
|
||||
name = Trader Demo
|
||||
kotlin.incremental=false
|
@ -0,0 +1,21 @@
|
||||
package net.corda.traderdemo
|
||||
|
||||
import net.corda.core.node.services.ServiceInfo
|
||||
import net.corda.node.driver.driver
|
||||
import net.corda.node.services.transactions.SimpleNotaryService
|
||||
import net.corda.testing.getHostAndPort
|
||||
import org.junit.Test
|
||||
|
||||
class TraderDemoTest {
|
||||
@Test fun `runs trader demo`() {
|
||||
driver(dsl = {
|
||||
startNode("Notary", setOf(ServiceInfo(SimpleNotaryService.type)))
|
||||
val nodeA = startNode("Bank A").get()
|
||||
val nodeAApiAddr = nodeA.config.getHostAndPort("webAddress")
|
||||
val nodeBApiAddr = startNode("Bank B").get().config.getHostAndPort("webAddress")
|
||||
|
||||
assert(TraderDemoClientApi(nodeAApiAddr).runBuyer())
|
||||
assert(TraderDemoClientApi(nodeBApiAddr).runSeller(counterparty = nodeA.nodeInfo.legalIdentity.name))
|
||||
}, isDebug = true)
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
package net.corda.traderdemo
|
||||
|
||||
import net.corda.core.node.services.ServiceInfo
|
||||
import net.corda.node.driver.driver
|
||||
import net.corda.node.services.transactions.SimpleNotaryService
|
||||
|
||||
/**
|
||||
* This file is exclusively for being able to run your nodes through an IDE (as opposed to running deployNodes)
|
||||
* Do not use in a production environment.
|
||||
*/
|
||||
fun main(args: Array<String>) {
|
||||
driver(dsl = {
|
||||
startNode("Notary", setOf(ServiceInfo(SimpleNotaryService.type)))
|
||||
startNode("Bank A")
|
||||
startNode("Bank B")
|
||||
waitForAllNodesToFinish()
|
||||
}, isDebug = true)
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
package net.corda.traderdemo
|
||||
|
||||
import com.google.common.net.HostAndPort
|
||||
import net.corda.core.contracts.DOLLARS
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import joptsimple.OptionParser
|
||||
import org.slf4j.Logger
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
/**
|
||||
* This entry point allows for command line running of the trader demo functions on nodes started by Main.kt.
|
||||
*/
|
||||
fun main(args: Array<String>) {
|
||||
TraderDemo().main(args)
|
||||
}
|
||||
|
||||
private class TraderDemo {
|
||||
enum class Role {
|
||||
BUYER,
|
||||
SELLER
|
||||
}
|
||||
|
||||
companion object {
|
||||
val logger: Logger = loggerFor<TraderDemo>()
|
||||
}
|
||||
|
||||
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) {
|
||||
logger.error(e.message)
|
||||
printHelp(parser)
|
||||
exitProcess(1)
|
||||
}
|
||||
|
||||
// What happens next depends on the role. The buyer sits around waiting for a trade to start. The seller role
|
||||
// will contact the buyer and actually make something happen.
|
||||
val role = options.valueOf(roleArg)!!
|
||||
if (role == Role.BUYER) {
|
||||
TraderDemoClientApi(HostAndPort.fromString("localhost:10005")).runBuyer()
|
||||
} else {
|
||||
TraderDemoClientApi(HostAndPort.fromString("localhost:10007")).runSeller(1000.DOLLARS, "Bank A")
|
||||
}
|
||||
}
|
||||
|
||||
fun printHelp(parser: OptionParser) {
|
||||
println("""
|
||||
Usage: trader-demo --role [BUYER|SELLER]
|
||||
Please refer to the documentation in docs/build/index.html for more info.
|
||||
|
||||
""".trimIndent())
|
||||
parser.printHelpOn(System.out)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,26 @@
|
||||
package net.corda.traderdemo
|
||||
|
||||
import com.google.common.net.HostAndPort
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.core.contracts.DOLLARS
|
||||
import net.corda.testing.http.HttpApi
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Interface for communicating with nodes running the trader demo.
|
||||
*/
|
||||
class TraderDemoClientApi(hostAndPort: HostAndPort) {
|
||||
private val api = HttpApi.fromHostAndPort(hostAndPort, apiRoot)
|
||||
|
||||
fun runBuyer(amount: Amount<Currency> = 30000.0.DOLLARS, notary: String = "Notary"): Boolean {
|
||||
return api.putJson("create-test-cash", mapOf("amount" to amount.quantity, "notary" to notary))
|
||||
}
|
||||
|
||||
fun runSeller(amount: Amount<Currency> = 1000.0.DOLLARS, counterparty: String): Boolean {
|
||||
return api.postJson("$counterparty/sell-cash", mapOf("amount" to amount.quantity))
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private val apiRoot = "api/traderdemo"
|
||||
}
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
package net.corda.traderdemo.api
|
||||
|
||||
import net.corda.contracts.testing.fillWithSomeTestCash
|
||||
import net.corda.core.contracts.DOLLARS
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.Emoji
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import net.corda.traderdemo.protocol.SellerProtocol
|
||||
import javax.ws.rs.*
|
||||
import javax.ws.rs.core.MediaType
|
||||
import javax.ws.rs.core.Response
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
// API is accessible from /api/traderdemo. All paths specified below are relative to it.
|
||||
@Path("traderdemo")
|
||||
class TraderDemoApi(val services: ServiceHub) {
|
||||
data class TestCashParams(val amount: Int, val notary: String)
|
||||
data class SellParams(val amount: Int)
|
||||
private companion object {
|
||||
val logger = loggerFor<TraderDemoApi>()
|
||||
}
|
||||
|
||||
/**
|
||||
* Self issue some cash.
|
||||
* TODO: At some point this demo should be extended to have a central bank node.
|
||||
*/
|
||||
@PUT
|
||||
@Path("create-test-cash")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
fun createTestCash(params: TestCashParams): Response {
|
||||
val notary = services.networkMapCache.notaryNodes.single { it.legalIdentity.name == params.notary }.notaryIdentity
|
||||
services.fillWithSomeTestCash(params.amount.DOLLARS,
|
||||
outputNotary = notary,
|
||||
ownedBy = services.myInfo.legalIdentity.owningKey)
|
||||
return Response.status(Response.Status.CREATED).build()
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("{party}/sell-cash")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
fun sellCash(params: SellParams, @PathParam("party") partyName: String): Response {
|
||||
val otherParty = services.identityService.partyFromName(partyName)
|
||||
if (otherParty != null) {
|
||||
// The seller will sell some commercial paper to the buyer, who will pay with (self issued) cash.
|
||||
//
|
||||
// The CP sale transaction comes with a prospectus PDF, which will tag along for the ride in an
|
||||
// attachment. Make sure we have the transaction prospectus attachment loaded into our store.
|
||||
//
|
||||
// This can also be done via an HTTP upload, but here we short-circuit and do it from code.
|
||||
if (services.storageService.attachments.openAttachment(SellerProtocol.PROSPECTUS_HASH) == null) {
|
||||
javaClass.classLoader.getResourceAsStream("bank-of-london-cp.jar").use {
|
||||
val id = services.storageService.attachments.importAttachment(it)
|
||||
assertEquals(SellerProtocol.PROSPECTUS_HASH, id)
|
||||
}
|
||||
}
|
||||
|
||||
// The line below blocks and waits for the future to resolve.
|
||||
val stx = services.invokeProtocolAsync<SignedTransaction>(SellerProtocol::class.java, otherParty, params.amount.DOLLARS).get()
|
||||
logger.info("Sale completed - we have a happy customer!\n\nFinal transaction is:\n\n${Emoji.renderIfSupported(stx.tx)}")
|
||||
return Response.status(Response.Status.OK).build()
|
||||
} else {
|
||||
return Response.status(Response.Status.BAD_REQUEST).build()
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
package net.corda.traderdemo.plugin
|
||||
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.core.crypto.Party
|
||||
import net.corda.core.node.CordaPluginRegistry
|
||||
import net.corda.traderdemo.api.TraderDemoApi
|
||||
import net.corda.traderdemo.protocol.BuyerProtocol
|
||||
import net.corda.traderdemo.protocol.SellerProtocol
|
||||
|
||||
class TraderDemoPlugin : CordaPluginRegistry() {
|
||||
// A list of classes that expose web APIs.
|
||||
override val webApis: List<Class<*>> = listOf(TraderDemoApi::class.java)
|
||||
// A list of protocols that are required for this cordapp
|
||||
override val requiredProtocols: Map<String, Set<String>> = mapOf(
|
||||
SellerProtocol::class.java.name to setOf(Party::class.java.name, Amount::class.java.name)
|
||||
)
|
||||
override val servicePlugins: List<Class<*>> = listOf(BuyerProtocol.Service::class.java)
|
||||
}
|
@ -0,0 +1,86 @@
|
||||
package net.corda.traderdemo.protocol
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.contracts.CommercialPaper
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.core.contracts.TransactionGraphSearch
|
||||
import net.corda.core.crypto.Party
|
||||
import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.node.PluginServiceHub
|
||||
import net.corda.core.protocols.ProtocolLogic
|
||||
import net.corda.core.serialization.SingletonSerializeAsToken
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.Emoji
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
import net.corda.node.services.api.ServiceHubInternal
|
||||
import net.corda.node.services.persistence.NodeAttachmentService
|
||||
import net.corda.protocols.TwoPartyTradeProtocol
|
||||
import java.nio.file.Path
|
||||
import java.util.*
|
||||
|
||||
class BuyerProtocol(val otherParty: Party,
|
||||
private val attachmentsPath: Path,
|
||||
override val progressTracker: ProgressTracker = ProgressTracker(STARTING_BUY)) : ProtocolLogic<Unit>() {
|
||||
|
||||
object STARTING_BUY : ProgressTracker.Step("Seller connected, purchasing commercial paper asset")
|
||||
|
||||
class Service(services: PluginServiceHub) : SingletonSerializeAsToken() {
|
||||
init {
|
||||
// Buyer will fetch the attachment from the seller automatically when it resolves the transaction.
|
||||
// For demo purposes just extract attachment jars when saved to disk, so the user can explore them.
|
||||
val attachmentsPath = (services.storageService.attachments as NodeAttachmentService).let {
|
||||
it.automaticallyExtractAttachments = true
|
||||
it.storePath
|
||||
}
|
||||
services.registerProtocolInitiator(SellerProtocol::class) { BuyerProtocol(it, attachmentsPath) }
|
||||
}
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
progressTracker.currentStep = STARTING_BUY
|
||||
|
||||
// Receive the offered amount and automatically agree to it (in reality this would be a longer negotiation)
|
||||
val amount = receive<Amount<Currency>>(otherParty).unwrap { it }
|
||||
val notary: NodeInfo = serviceHub.networkMapCache.notaryNodes[0]
|
||||
val buyer = TwoPartyTradeProtocol.Buyer(
|
||||
otherParty,
|
||||
notary.notaryIdentity,
|
||||
amount,
|
||||
CommercialPaper.State::class.java)
|
||||
|
||||
// This invokes the trading protocol and out pops our finished transaction.
|
||||
val tradeTX: SignedTransaction = subProtocol(buyer, shareParentSessions = true)
|
||||
// TODO: This should be moved into the protocol itself.
|
||||
serviceHub.recordTransactions(listOf(tradeTX))
|
||||
|
||||
logger.info("Purchase complete - we are a happy customer! Final transaction is: " +
|
||||
"\n\n${Emoji.renderIfSupported(tradeTX.tx)}")
|
||||
|
||||
logIssuanceAttachment(tradeTX)
|
||||
logBalance()
|
||||
}
|
||||
|
||||
private fun logBalance() {
|
||||
val balances = serviceHub.vaultService.cashBalances.entries.map { "${it.key.currencyCode} ${it.value}" }
|
||||
logger.info("Remaining balance: ${balances.joinToString()}")
|
||||
}
|
||||
|
||||
private fun logIssuanceAttachment(tradeTX: SignedTransaction) {
|
||||
// Find the original CP issuance.
|
||||
val search = TransactionGraphSearch(serviceHub.storageService.validatedTransactions, listOf(tradeTX.tx))
|
||||
search.query = TransactionGraphSearch.Query(withCommandOfType = CommercialPaper.Commands.Issue::class.java,
|
||||
followInputsOfType = CommercialPaper.State::class.java)
|
||||
val cpIssuance = search.call().single()
|
||||
|
||||
cpIssuance.attachments.first().let {
|
||||
val p = attachmentsPath.toAbsolutePath().resolve("$it.jar")
|
||||
logger.info("""
|
||||
|
||||
The issuance of the commercial paper came with an attachment. You can find it expanded in this directory:
|
||||
$p
|
||||
|
||||
${Emoji.renderIfSupported(cpIssuance)}""")
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,110 @@
|
||||
package net.corda.traderdemo.protocol
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.contracts.CommercialPaper
|
||||
import net.corda.contracts.asset.DUMMY_CASH_ISSUER
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.*
|
||||
import net.corda.core.days
|
||||
import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.protocols.ProtocolLogic
|
||||
import net.corda.core.seconds
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
import net.corda.protocols.NotaryProtocol
|
||||
import net.corda.protocols.TwoPartyTradeProtocol
|
||||
import java.security.PublicKey
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
|
||||
class SellerProtocol(val otherParty: Party,
|
||||
val amount: Amount<Currency>,
|
||||
override val progressTracker: ProgressTracker = tracker()) : ProtocolLogic<SignedTransaction>() {
|
||||
companion object {
|
||||
val PROSPECTUS_HASH = SecureHash.parse("decd098666b9657314870e192ced0c3519c2c9d395507a238338f8d003929de9")
|
||||
|
||||
object SELF_ISSUING : ProgressTracker.Step("Got session ID back, issuing and timestamping some commercial paper")
|
||||
|
||||
object TRADING : ProgressTracker.Step("Starting the trade protocol") {
|
||||
override fun childProgressTracker(): ProgressTracker = TwoPartyTradeProtocol.Seller.tracker()
|
||||
}
|
||||
|
||||
// We vend a progress tracker that already knows there's going to be a TwoPartyTradingProtocol involved at some
|
||||
// point: by setting up the tracker in advance, the user can see what's coming in more detail, instead of being
|
||||
// surprised when it appears as a new set of tasks below the current one.
|
||||
fun tracker() = ProgressTracker(SELF_ISSUING, TRADING)
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
override fun call(): SignedTransaction {
|
||||
progressTracker.currentStep = SELF_ISSUING
|
||||
|
||||
val notary: NodeInfo = serviceHub.networkMapCache.notaryNodes[0]
|
||||
val cpOwnerKey = serviceHub.legalIdentityKey
|
||||
val commercialPaper = selfIssueSomeCommercialPaper(cpOwnerKey.public.tree, notary)
|
||||
|
||||
progressTracker.currentStep = TRADING
|
||||
|
||||
// Send the offered amount.
|
||||
send(otherParty, amount)
|
||||
val seller = TwoPartyTradeProtocol.Seller(
|
||||
otherParty,
|
||||
notary,
|
||||
commercialPaper,
|
||||
amount,
|
||||
cpOwnerKey,
|
||||
progressTracker.getChildProgressTracker(TRADING)!!)
|
||||
val tradeTX: SignedTransaction = subProtocol(seller, shareParentSessions = true)
|
||||
serviceHub.recordTransactions(listOf(tradeTX))
|
||||
|
||||
return tradeTX
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
fun selfIssueSomeCommercialPaper(ownedBy: PublicKeyTree, notaryNode: NodeInfo): StateAndRef<CommercialPaper.State> {
|
||||
// Make a fake company that's issued its own paper.
|
||||
val keyPair = generateKeyPair()
|
||||
val party = Party("Bank of London", keyPair.public)
|
||||
|
||||
val issuance: SignedTransaction = run {
|
||||
val tx = CommercialPaper().generateIssue(party.ref(1, 2, 3), 1100.DOLLARS `issued by` DUMMY_CASH_ISSUER,
|
||||
Instant.now() + 10.days, notaryNode.notaryIdentity)
|
||||
|
||||
// TODO: Consider moving these two steps below into generateIssue.
|
||||
|
||||
// Attach the prospectus.
|
||||
tx.addAttachment(serviceHub.storageService.attachments.openAttachment(PROSPECTUS_HASH)!!.id)
|
||||
|
||||
// Requesting timestamping, all CP must be timestamped.
|
||||
tx.setTime(Instant.now(), 30.seconds)
|
||||
|
||||
// Sign it as ourselves.
|
||||
tx.signWith(keyPair)
|
||||
|
||||
// Get the notary to sign the timestamp
|
||||
val notarySig = subProtocol(NotaryProtocol.Client(tx.toSignedTransaction(false)))
|
||||
tx.addSignatureUnchecked(notarySig)
|
||||
|
||||
// Commit it to local storage.
|
||||
val stx = tx.toSignedTransaction(true)
|
||||
serviceHub.recordTransactions(listOf(stx))
|
||||
|
||||
stx
|
||||
}
|
||||
|
||||
// Now make a dummy transaction that moves it to a new key, just to show that resolving dependencies works.
|
||||
val move: SignedTransaction = run {
|
||||
val builder = TransactionType.General.Builder(notaryNode.notaryIdentity)
|
||||
CommercialPaper().generateMove(builder, issuance.tx.outRef(0), ownedBy)
|
||||
builder.signWith(keyPair)
|
||||
val notarySignature = subProtocol(NotaryProtocol.Client(builder.toSignedTransaction(false)))
|
||||
builder.addSignatureUnchecked(notarySignature)
|
||||
val tx = builder.toSignedTransaction(true)
|
||||
serviceHub.recordTransactions(listOf(tx))
|
||||
tx
|
||||
}
|
||||
|
||||
return move.tx.outRef(0)
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
# Register a ServiceLoader service extending from net.corda.node.CordaPluginRegistry
|
||||
net.corda.traderdemo.plugin.TraderDemoPlugin
|
BIN
samples/trader-demo/src/main/resources/bank-of-london-cp.jar
Normal file
BIN
samples/trader-demo/src/main/resources/bank-of-london-cp.jar
Normal file
Binary file not shown.
@ -0,0 +1 @@
|
||||
These certificates are used for development mode only (and are copies of those contained within the TraderDemo jar file)
|
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user