Issue commercial paper from the Bank of Corda node (#1196)

* Remove hard coded commercial paper issuer from trader demo
* Issue commercial paper from the Bank of Corda node, as per previous hard-coded issuer name
* Issue cash from the Bank of Corda node rather than in response to the buyer node requesting issuance
This commit is contained in:
Ross Nicoll 2017-08-11 10:20:27 +01:00 committed by GitHub
parent 0977ffca54
commit 4602739e93
9 changed files with 153 additions and 86 deletions

View File

@ -32,6 +32,9 @@ UNRELEASED
of different soft locking retrieval behaviours (exclusive of soft locked states, soft locked states only, specified
by set of lock ids)
* Trader demo now issues cash and commercial paper directly from the bank node, rather than the seller node self-issuing
commercial paper but labelling it as if issued by the bank.
Milestone 14
------------

View File

@ -32,7 +32,7 @@ To run from the command line in Unix:
1. Run ``./gradlew samples:trader-demo:deployNodes`` to create a set of configs and installs under ``samples/trader-demo/build/nodes``
2. Run ``./samples/trader-demo/build/nodes/runnodes`` to open up four new terminals with the four nodes
3. Run ``./gradlew samples:trader-demo:runBuyer`` to instruct the buyer node to request issuance of some cash from the Bank of Corda node
3. Run ``./gradlew samples:trader-demo:runBank`` to instruct the bank node to issue cash and commercial paper to the buyer and seller nodes respectively.
4. Run ``./gradlew samples:trader-demo:runSeller`` to trigger the transaction. If you entered ``flow watch``
you can see flows running on both sides of transaction. Additionally you should see final trade information displayed
to your terminal.

View File

@ -142,7 +142,8 @@ object TwoPartyTradeFlow {
// Put together a proposed transaction that performs the trade, and sign it.
progressTracker.currentStep = SIGNING
val (ptx, cashSigningPubKeys) = assembleSharedTX(assetForSale, tradeRequest)
val partSignedTx = signWithOurKeys(cashSigningPubKeys, ptx)
// Now sign the transaction with whatever keys we need to move the cash.
val partSignedTx = serviceHub.signInitialTransaction(ptx, cashSigningPubKeys)
// Send the signed transaction to the seller, who must then sign it themselves and commit
// it to the ledger by sending it to the notary.
@ -168,10 +169,6 @@ object TwoPartyTradeFlow {
}
}
private fun signWithOurKeys(cashSigningPubKeys: List<PublicKey>, ptx: TransactionBuilder): SignedTransaction {
// Now sign the transaction with whatever keys we need to move the cash.
return serviceHub.signInitialTransaction(ptx, cashSigningPubKeys)
}
@Suspendable
private fun assembleSharedTX(assetForSale: StateAndRef<OwnableState>, tradeRequest: SellerTradeInfo): Pair<TransactionBuilder, List<PublicKey>> {

View File

@ -40,8 +40,9 @@ dependencies {
task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
ext.rpcUsers = [['username': "demo", 'password': "demo", 'permissions': [
'StartFlow.net.corda.flows.IssuerFlow$IssuanceRequester',
"StartFlow.net.corda.traderdemo.flow.SellerFlow"
'StartFlow.net.corda.flows.CashIssueFlow',
'StartFlow.net.corda.traderdemo.flow.CommercialPaperIssueFlow',
'StartFlow.net.corda.traderdemo.flow.SellerFlow'
]]]
directory "./build/nodes"
@ -74,7 +75,9 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
name "CN=BankOfCorda,O=R3,L=New York,C=US"
advertisedServices = []
p2pPort 10011
rpcPort 10012
cordapps = []
rpcUsers = ext.rpcUsers
}
}
@ -102,11 +105,11 @@ publishing {
}
}
task runBuyer(type: JavaExec) {
task runBank(type: JavaExec) {
classpath = sourceSets.main.runtimeClasspath
main = 'net.corda.traderdemo.TraderDemoKt'
args '--role'
args 'BUYER'
args 'BANK'
}
task runSeller(type: JavaExec) {

View File

@ -6,6 +6,7 @@ import net.corda.core.utilities.millis
import net.corda.core.node.services.ServiceInfo
import net.corda.core.internal.concurrent.transpose
import net.corda.core.utilities.getOrThrow
import net.corda.flows.CashIssueFlow
import net.corda.testing.DUMMY_BANK_A
import net.corda.testing.DUMMY_BANK_B
import net.corda.testing.DUMMY_NOTARY
@ -17,6 +18,7 @@ import net.corda.testing.BOC
import net.corda.testing.driver.poll
import net.corda.testing.node.NodeBasedTest
import net.corda.traderdemo.flow.BuyerFlow
import net.corda.traderdemo.flow.CommercialPaperIssueFlow
import net.corda.traderdemo.flow.SellerFlow
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
@ -25,15 +27,13 @@ import java.util.concurrent.Executors
class TraderDemoTest : NodeBasedTest() {
@Test
fun `runs trader demo`() {
val permissions = setOf(
startFlowPermission<IssuerFlow.IssuanceRequester>(),
startFlowPermission<SellerFlow>())
val demoUser = listOf(User("demo", "demo", permissions))
val user = User("user1", "test", permissions = setOf(startFlowPermission<IssuerFlow.IssuanceRequester>()))
val (nodeA, nodeB) = listOf(
startNode(DUMMY_BANK_A.name, rpcUsers = demoUser),
startNode(DUMMY_BANK_B.name, rpcUsers = demoUser),
startNode(BOC.name, rpcUsers = listOf(user)),
val demoUser = User("demo", "demo", setOf(startFlowPermission<SellerFlow>()))
val bankUser = User("user1", "test", permissions = setOf(startFlowPermission<CashIssueFlow>(),
startFlowPermission<CommercialPaperIssueFlow>()))
val (nodeA, nodeB, bankNode, notaryNode) = listOf(
startNode(DUMMY_BANK_A.name, rpcUsers = listOf(demoUser)),
startNode(DUMMY_BANK_B.name, rpcUsers = listOf(demoUser)),
startNode(BOC.name, rpcUsers = listOf(bankUser)),
startNode(DUMMY_NOTARY.name, advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type)))
).transpose().getOrThrow()
@ -41,19 +41,24 @@ class TraderDemoTest : NodeBasedTest() {
val (nodeARpc, nodeBRpc) = listOf(nodeA, nodeB).map {
val client = CordaRPCClient(it.configuration.rpcAddress!!, initialiseSerialization = false)
client.start(demoUser[0].username, demoUser[0].password).proxy
client.start(demoUser.username, demoUser.password).proxy
}
val nodeBankRpc = let {
val client = CordaRPCClient(bankNode.configuration.rpcAddress!!, initialiseSerialization = false)
client.start(bankUser.username, bankUser.password).proxy
}
val clientA = TraderDemoClientApi(nodeARpc)
val clientB = TraderDemoClientApi(nodeBRpc)
val clientBank = TraderDemoClientApi(nodeBankRpc)
val originalACash = clientA.cashCount // A has random number of issued amount
val expectedBCash = clientB.cashCount + 1
val expectedPaper = listOf(clientA.commercialPaperCount + 1, clientB.commercialPaperCount)
// TODO: Enable anonymisation
clientA.runBuyer(amount = 100.DOLLARS, anonymous = false)
clientB.runSeller(counterparty = nodeA.info.legalIdentity.name, amount = 5.DOLLARS)
clientBank.runIssuer(amount = 100.DOLLARS, buyerName = nodeA.info.legalIdentity.name, sellerName = nodeB.info.legalIdentity.name, notaryName = notaryNode.info.legalIdentity.name)
clientB.runSeller(buyerName = nodeA.info.legalIdentity.name, amount = 5.DOLLARS)
assertThat(clientA.cashCount).isGreaterThan(originalACash)
assertThat(clientB.cashCount).isEqualTo(expectedBCash)

View File

@ -4,8 +4,10 @@ import joptsimple.OptionParser
import net.corda.client.rpc.CordaRPCClient
import net.corda.core.contracts.DOLLARS
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.testing.DUMMY_BANK_A
import net.corda.core.utilities.loggerFor
import net.corda.testing.DUMMY_BANK_A
import net.corda.testing.DUMMY_BANK_B
import net.corda.testing.DUMMY_NOTARY
import org.slf4j.Logger
import kotlin.system.exitProcess
@ -18,12 +20,18 @@ fun main(args: Array<String>) {
private class TraderDemo {
enum class Role {
BUYER,
BANK,
SELLER
}
companion object {
val logger: Logger = loggerFor<TraderDemo>()
val buyerName = DUMMY_BANK_A.name
val sellerName = DUMMY_BANK_B.name
val notaryName = DUMMY_NOTARY.name
val buyerRpcPort = 10006
val sellerRpcPort = 10009
val bankRpcPort = 10012
}
fun main(args: Array<String>) {
@ -41,15 +49,15 @@ private class TraderDemo {
// 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) {
val host = NetworkHostAndPort("localhost", 10006)
CordaRPCClient(host).start("demo", "demo").use {
TraderDemoClientApi(it.proxy).runBuyer()
if (role == Role.BANK) {
val bankHost = NetworkHostAndPort("localhost", bankRpcPort)
CordaRPCClient(bankHost).use("demo", "demo") {
TraderDemoClientApi(it.proxy).runIssuer(1100.DOLLARS, buyerName, sellerName, notaryName)
}
} else {
val host = NetworkHostAndPort("localhost", 10009)
CordaRPCClient(host).use("demo", "demo") {
TraderDemoClientApi(it.proxy).runSeller(1000.DOLLARS, DUMMY_BANK_A.name)
val sellerHost = NetworkHostAndPort("localhost", sellerRpcPort)
CordaRPCClient(sellerHost).use("demo", "demo") {
TraderDemoClientApi(it.proxy).runSeller(1000.DOLLARS, buyerName)
}
}
}

View File

@ -16,11 +16,11 @@ import net.corda.core.node.services.vault.builder
import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.getOrThrow
import net.corda.core.utilities.loggerFor
import net.corda.flows.IssuerFlow.IssuanceRequester
import net.corda.flows.CashIssueFlow
import net.corda.node.services.vault.VaultSchemaV1
import net.corda.testing.BOC
import net.corda.testing.DUMMY_NOTARY
import net.corda.testing.contracts.calculateRandomlySizedAmounts
import net.corda.traderdemo.flow.CommercialPaperIssueFlow
import net.corda.traderdemo.flow.SellerFlow
import org.bouncycastle.asn1.x500.X500Name
import java.util.*
@ -47,25 +47,42 @@ class TraderDemoClientApi(val rpc: CordaRPCOps) {
return rpc.vaultQueryBy<CommercialPaper.State>(countCriteria).otherResults.single() as Long
}
fun runBuyer(amount: Amount<Currency> = 30000.DOLLARS, anonymous: Boolean = false) {
val bankOfCordaParty = rpc.partyFromX500Name(BOC.name)
?: throw IllegalStateException("Unable to locate ${BOC.name} in Network Map Service")
fun runIssuer(amount: Amount<Currency> = 1100.0.DOLLARS, buyerName: X500Name, sellerName: X500Name, notaryName: X500Name) {
val ref = OpaqueBytes.of(1)
val buyer = rpc.partyFromX500Name(buyerName) ?: throw IllegalStateException("Don't know $buyerName")
val seller = rpc.partyFromX500Name(sellerName) ?: throw IllegalStateException("Don't know $sellerName")
val notaryLegalIdentity = rpc.partyFromX500Name(DUMMY_NOTARY.name)
?: throw IllegalStateException("Unable to locate ${DUMMY_NOTARY.name} in Network Map Service")
val notaryNode = rpc.nodeIdentityFromParty(notaryLegalIdentity)
?: throw IllegalStateException("Unable to locate notary node in network map cache")
val me = rpc.nodeIdentity()
val amounts = calculateRandomlySizedAmounts(amount, 3, 10, Random())
// issuer random amounts of currency totaling 30000.DOLLARS in parallel
val anonymous = false
// issue random amounts of currency up to the requested amount, in parallel
val resultFutures = amounts.map { pennies ->
rpc.startFlow(::IssuanceRequester, Amount(pennies, amount.token), me.legalIdentity, OpaqueBytes.of(1), bankOfCordaParty, notaryNode.notaryIdentity, anonymous).returnValue
rpc.startFlow(::CashIssueFlow, amount.copy(quantity = pennies), OpaqueBytes.of(1), buyer, notaryNode.notaryIdentity, anonymous).returnValue
}
resultFutures.transpose().getOrThrow()
println("Cash issued to buyer")
// 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 (!rpc.attachmentExists(SellerFlow.PROSPECTUS_HASH)) {
javaClass.classLoader.getResourceAsStream("bank-of-london-cp.jar").use {
val id = rpc.uploadAttachment(it)
check(SellerFlow.PROSPECTUS_HASH == id)
}
}
// The line below blocks and waits for the future to resolve.
val stx = rpc.startFlow(::CommercialPaperIssueFlow, amount, ref, seller, notaryNode.notaryIdentity).returnValue.getOrThrow()
println("Commercial paper issued to seller")
}
fun runSeller(amount: Amount<Currency> = 1000.0.DOLLARS, counterparty: X500Name) {
val otherParty = rpc.partyFromX500Name(counterparty) ?: throw IllegalStateException("Don't know $counterparty")
fun runSeller(amount: Amount<Currency> = 1000.0.DOLLARS, buyerName: X500Name) {
val otherParty = rpc.partyFromX500Name(buyerName) ?: throw IllegalStateException("Don't know $buyerName")
// 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

View File

@ -0,0 +1,76 @@
package net.corda.traderdemo.flow
import co.paralleluniverse.fibers.Suspendable
import net.corda.contracts.CommercialPaper
import net.corda.contracts.asset.DUMMY_CASH_ISSUER
import net.corda.core.contracts.Amount
import net.corda.core.contracts.`issued by`
import net.corda.core.crypto.SecureHash
import net.corda.core.flows.FinalityFlow
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.InitiatingFlow
import net.corda.core.flows.StartableByRPC
import net.corda.core.identity.Party
import net.corda.core.node.NodeInfo
import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.ProgressTracker
import net.corda.core.utilities.days
import net.corda.core.utilities.seconds
import java.time.Instant
import java.util.*
/**
* Flow for the Bank of Corda node to issue some commercial paper to the seller's node, to sell to the buyer.
*/
@InitiatingFlow
@StartableByRPC
class CommercialPaperIssueFlow(val amount: Amount<Currency>,
val issueRef: OpaqueBytes,
val recipient: Party,
val notary: Party,
override val progressTracker: ProgressTracker) : FlowLogic<SignedTransaction>() {
constructor(amount: Amount<Currency>, issueRef: OpaqueBytes, recipient: Party, notary: Party) : this(amount, issueRef, recipient, notary, tracker())
companion object {
val PROSPECTUS_HASH = SecureHash.parse("decd098666b9657314870e192ced0c3519c2c9d395507a238338f8d003929de9")
object ISSUING : ProgressTracker.Step("Issuing and timestamping some commercial paper")
fun tracker() = ProgressTracker(ISSUING)
}
@Suspendable
override fun call(): SignedTransaction {
progressTracker.currentStep = ISSUING
val me = serviceHub.myInfo.legalIdentity
val issuance: SignedTransaction = run {
val tx = CommercialPaper().generateIssue(me.ref(issueRef), amount `issued by` me.ref(issueRef),
Instant.now() + 10.days, notary)
// TODO: Consider moving these two steps below into generateIssue.
// Attach the prospectus.
tx.addAttachment(serviceHub.attachments.openAttachment(PROSPECTUS_HASH)!!.id)
// Requesting a time-window to be set, all CP must have a validation window.
tx.setTimeWindow(Instant.now(), 30.seconds)
// Sign it as ourselves.
val stx = serviceHub.signInitialTransaction(tx)
subFlow(FinalityFlow(stx)).single()
}
// 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 = TransactionBuilder(notary)
CommercialPaper().generateMove(builder, issuance.tx.outRef(0), recipient)
val stx = serviceHub.signInitialTransaction(builder)
subFlow(FinalityFlow(stx)).single()
}
return move
}
}

View File

@ -2,25 +2,17 @@ package net.corda.traderdemo.flow
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.contracts.Amount
import net.corda.core.crypto.SecureHash
import net.corda.core.utilities.days
import net.corda.core.flows.FinalityFlow
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.InitiatingFlow
import net.corda.core.flows.StartableByRPC
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.AnonymousParty
import net.corda.core.identity.Party
import net.corda.core.node.NodeInfo
import net.corda.core.utilities.seconds
import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.ProgressTracker
import net.corda.flows.TwoPartyTradeFlow
import org.bouncycastle.asn1.x500.X500Name
import java.time.Instant
import java.util.*
@InitiatingFlow
@ -51,7 +43,8 @@ class SellerFlow(val otherParty: Party,
val notary: NodeInfo = serviceHub.networkMapCache.notaryNodes[0]
val cpOwnerKey = serviceHub.keyManagementService.freshKey()
val commercialPaper = selfIssueSomeCommercialPaper(serviceHub.myInfo.legalIdentity, notary)
val commercialPaper = serviceHub.vaultQueryService.queryBy(CommercialPaper.State::class.java).states.first()
progressTracker.currentStep = TRADING
@ -66,39 +59,4 @@ class SellerFlow(val otherParty: Party,
progressTracker.getChildProgressTracker(TRADING)!!)
return subFlow(seller)
}
@Suspendable
fun selfIssueSomeCommercialPaper(ownedBy: AbstractParty, notaryNode: NodeInfo): StateAndRef<CommercialPaper.State> {
// Make a fake company that's issued its own paper.
val party = Party(X500Name("CN=BankOfCorda,O=R3,L=New York,C=US"), serviceHub.legalIdentityKey)
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.attachments.openAttachment(PROSPECTUS_HASH)!!.id)
// Requesting a time-window to be set, all CP must have a validation window.
tx.setTimeWindow(Instant.now(), 30.seconds)
// Sign it as ourselves.
val stx = serviceHub.signInitialTransaction(tx)
subFlow(FinalityFlow(stx)).single()
}
// 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 = TransactionBuilder(notaryNode.notaryIdentity)
CommercialPaper().generateMove(builder, issuance.tx.outRef(0), ownedBy)
val stx = serviceHub.signInitialTransaction(builder)
subFlow(FinalityFlow(stx)).single()
}
return move.tx.outRef(0)
}
}