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:
@ -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